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:

  1. Variables must be registered before loading from DB (so we know what to load)

  2. Component instances need the loaded values to initialize correctly

  3. 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 level

  • NO 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 instances

  • Instances 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):

  1. WebUI saves new value to database

  2. WebUI calls reload_interface("my_interface")

  3. Core calls interface.shutdown_interface() to cleanup

  4. Core reloads variables from DB

  5. Core calls interface.initialize_interface() to recreate with new config

  6. Core 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 level

  • Create instances only in initialize_*() function

  • Implement shutdown_*() for proper cleanup

  • Export initialize_*, shutdown_*, reload_* in __all__

  • Check configuration in __init__ and set is_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 in initialize_*())

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

  1. Database Configuration: Variables can be stored and managed in DB

  2. Environment Override: Env vars still work (with higher priority)

  3. Hot Reload: Components can be reloaded when config changes

  4. Consistency: All components follow the same pattern

  5. Simplicity: Developers don’t manage listeners manually

  6. 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 implementation

  • interface/discord_interface.py - Another example

  • core/core_initializer.py - Core initialization logic

  • core/config_manager.py - Configuration system