Component Development Pattern - Two-Phase Initialization
Overview
SyntH uses a two-phase initialization pattern to ensure components (interfaces, plugins, LLM engines) have access to their configuration variables loaded from the database before being instantiated.
This pattern also supports hot-reload when configuration changes.
Why Two Phases?
The Problem
Components need to register their configuration variables with config_registry.get_var(), but:
Variables must be registered before loading from DB (so we know what to load)
Component instances need the loaded values to initialize correctly
Components must be reloadable when configuration changes
The Solution: Two-Phase Pattern
Phase 1: Discovery & Variable Registration
Module is imported
Variables are declared with
config_registry.get_var()at module levelNO instances are created yet
Phase 2: Initialization
Core loads all registered variables from database
Core notifies all listeners with updated values
Core calls
initialize_interface()/initialize_plugin()to create instancesInstances now see correct values from DB
Implementation Pattern
For Interface Developers
# interface/my_interface.py
from core.config_manager import config_registry
from core.core_initializer import register_interface
# PHASE 1: Declare variables at module level (runs during import)
MY_TOKEN = config_registry.get_var(
"MY_TOKEN",
None, # default value
label="My Service Token",
description="Authentication token for my service",
group="interface",
component="my_interface",
sensitive=True,
)
MY_SETTING = config_registry.get_var(
"MY_SETTING",
"default_value",
label="My Setting",
description="Some configurable setting",
group="interface",
component="my_interface",
)
# Global instance variable
my_interface = None
# PHASE 2: Initialization function (called by core AFTER DB load)
def initialize_interface():
"""Initialize the interface after config has been loaded from DB."""
global my_interface
# Reload if already exists (for hot-reload)
if my_interface is not None:
shutdown_interface()
# Create instance - variables now have correct values from DB
my_interface = MyInterface()
register_interface("my_interface", my_interface)
return my_interface
def shutdown_interface():
"""Cleanup before reload or shutdown."""
global my_interface
if my_interface is None:
return
# Cleanup resources
if my_interface.connection:
my_interface.connection.close()
# Unregister
from core.core_initializer import INTERFACE_REGISTRY
if "my_interface" in INTERFACE_REGISTRY:
del INTERFACE_REGISTRY["my_interface"]
my_interface = None
def reload_interface():
"""Reload with updated configuration."""
return initialize_interface()
class MyInterface:
def __init__(self):
# Read variables - they now have correct values from DB
self.token = MY_TOKEN
self.setting = MY_SETTING
# Check configuration
self.is_enabled = bool(self.token)
self.disabled_reason = None if self.is_enabled else "Token not configured"
if not self.is_enabled:
log_warning(f"[my_interface] Loaded in disabled state: {self.disabled_reason}")
async def start(self):
"""Start the interface (called by core after initialization)."""
if not self.is_enabled:
return
# Connect, authenticate, etc.
await self._connect()
@staticmethod
def get_supported_actions() -> dict:
return {
"message_my_interface": {
"required_fields": ["text"],
"optional_fields": ["target"],
"description": "Send a message via my interface",
}
}
# Export for core
__all__ = ['initialize_interface', 'shutdown_interface', 'reload_interface', 'MyInterface']
Core Initialization Sequence
The core follows this sequence in core_initializer.py:
async def initialize_all(self):
# 1. Initialize base systems (DB, registries)
await self._initialize_registries()
# 2. PHASE 1: Import all components (registers variables)
self._load_plugins()
self._discover_interfaces()
# At this point: variables registered, but NO instances created
# 3. Load variables from database
await config_registry.load_all_from_db()
config_registry.notify_all_listeners()
# At this point: all variables have correct values from DB
# 4. PHASE 2: Initialize component instances
self._initialize_interface_instances()
self._initialize_plugin_instances()
# At this point: instances created with correct configuration
# 5. Start components
await self._start_interfaces()
Hot Reload Support
When a configuration variable changes (e.g., user updates token in WebUI):
WebUI saves new value to database
WebUI calls
reload_interface("my_interface")Core calls
interface.shutdown_interface()to cleanupCore reloads variables from DB
Core calls
interface.initialize_interface()to recreate with new configCore calls
interface.start()if it has a start method
Rules for Component Developers
✅ DO:
Declare all config variables with
config_registry.get_var()at module levelCreate instances only in
initialize_*()functionImplement
shutdown_*()for proper cleanupExport
initialize_*,shutdown_*,reload_*in__all__Check configuration in
__init__and setis_enabled/disabled_reason
❌ DON’T:
Create instances directly at module level during import
Assume variables have DB values during import
Keep global state that survives reload (use instance variables)
Call
register_interface()during import (do it ininitialize_*())
Migration Guide
Old Pattern (breaks with DB config):
# ❌ Creates instance during import - DB not loaded yet!
my_interface = MyInterface(os.getenv('MY_TOKEN'))
register_interface("my_interface", my_interface)
New Pattern (works with DB config):
# ✅ Registers variables during import
MY_TOKEN = config_registry.get_var("MY_TOKEN", None, ...)
my_interface = None
def initialize_interface():
global my_interface
# Creates instance AFTER DB load - has correct values
my_interface = MyInterface()
register_interface("my_interface", my_interface)
return my_interface
Benefits
Database Configuration: Variables can be stored and managed in DB
Environment Override: Env vars still work (with higher priority)
Hot Reload: Components can be reloaded when config changes
Consistency: All components follow the same pattern
Simplicity: Developers don’t manage listeners manually
WebUI Integration: Configuration appears automatically in WebUI
Testing
Components can be tested in isolation:
# test_my_interface.py
from interface import my_interface
# Simulate config load
my_interface.MY_TOKEN = "test_token"
my_interface.MY_SETTING = "test_value"
# Initialize
interface = my_interface.initialize_interface()
assert interface.is_enabled
assert interface.token == "test_token"
# Test reload
my_interface.MY_TOKEN = "new_token"
interface = my_interface.reload_interface()
assert interface.token == "new_token"
See Also
interface/telegram_bot.py- Reference implementationinterface/discord_interface.py- Another examplecore/core_initializer.py- Core initialization logiccore/config_manager.py- Configuration system