Component Development Guide - Configuration Management
Overview
This guide explains how to properly use the configuration system when developing new components (interfaces, plugins, Cortex engines) for Synthetic Heart.
Warning
IMPORTANT: Use ConfigVar for Global Variables
When declaring configuration variables at module level (global variables), always use ``config_registry.get_var()`` instead of config_registry.get_value().
Why ConfigVar?
Auto-updating: ConfigVar automatically reflects database changes without manual listeners
No boilerplate: No need to write listener functions or update globals manually
Developer-friendly: Simpler code, less error-prone
Consistent behavior: Works the same way across all components
The Standard Pattern
✅ CORRECT: Using ConfigVar
from core.config_manager import config_registry
# Declare configuration using get_var() - this returns a ConfigVar object
MY_TOKEN = config_registry.get_var(
"MY_TOKEN",
"", # default value
label="My Token",
description="Authentication token for the service",
group="interface", # or "plugin", "llm", etc.
component="my_component",
sensitive=True, # hide value in UI
)
# Use the variable naturally - it auto-updates when DB changes
def start_service():
if MY_TOKEN: # ConfigVar supports __bool__
client = MyClient(token=str(MY_TOKEN)) # Convert to string when needed
else:
print("Token not configured")
# Fallback pattern
TOKEN_A = config_registry.get_var("TOKEN_A", "", label="Token A", ...)
TOKEN_B = config_registry.get_var("TOKEN_B", "", label="Token B", ...)
def get_token():
"""Get token with fallback logic."""
return str(TOKEN_A or TOKEN_B) # ConfigVar supports __or__
❌ WRONG: Old pattern with get_value() and listeners
# DON'T DO THIS - Old pattern, deprecated
MY_TOKEN = config_registry.get_value("MY_TOKEN", "", label="My Token", ...)
def _update_token(value):
global MY_TOKEN
MY_TOKEN = value
config_registry.add_listener("MY_TOKEN", _update_token)
ConfigVar API
ConfigVar objects support natural Python operations:
TOKEN = config_registry.get_var("TOKEN", "", ...)
# Boolean check
if TOKEN: # True if value exists and is not empty
...
# String conversion
token_str = str(TOKEN)
# Equality
if TOKEN == "expected_value":
...
# Fallback with or
active_token = TOKEN_A or TOKEN_B or "default"
# Access raw value (same as str())
token_value = TOKEN.value
When to Use get_value() vs get_var()
Use get_var() for:
Module-level global variables (most common case)
Any variable that needs to stay updated when DB changes
Interface tokens, bot names, feature flags, etc.
Use get_value() for:
Inside class constructors (when you want to capture value at init time)
One-time configuration reads
Values that shouldn’t change after initialization
class MyPlugin:
def __init__(self):
# get_value() is OK here - reads current value once during init
self.cache_dir = config_registry.get_value(
"CACHE_DIR",
"/tmp/cache",
label="Cache Directory",
...
)
Complete Example: Telegram Bot Interface
from core.config_manager import config_registry
from core.logging_utils import log_warning
# Configuration - use get_var() for module-level variables
BOTFATHER_TOKEN = config_registry.get_var(
"BOTFATHER_TOKEN",
"",
label="Telegram Bot Token",
description="Token provided by BotFather to access the Telegram Bot API.",
group="interface",
component="telegram_bot",
sensitive=True,
)
TELEGRAM_TOKEN = config_registry.get_var(
"TELEGRAM_TOKEN",
"",
label="Telegram Token (Alternative)",
description="Optional alternative Telegram bot token (fallback for BOTFATHER_TOKEN).",
group="interface",
component="telegram_bot",
sensitive=True,
)
def get_telegram_token() -> str:
"""
Get the active Telegram token with fallback logic.
Returns BOTFATHER_TOKEN if set, otherwise TELEGRAM_TOKEN.
"""
token = BOTFATHER_TOKEN or TELEGRAM_TOKEN
return str(token) if token else ""
async def start_bot():
token = get_telegram_token()
if not token:
log_warning("[telegram_bot] Token not configured - skipping startup")
return
# Token is always current from DB
app = ApplicationBuilder().token(token).build()
await app.run_polling()
Configuration Options
All configuration methods accept these parameters:
config_registry.get_var(
"CONFIG_KEY", # Unique identifier (UPPERCASE_WITH_UNDERSCORES)
"default_value", # Default if not in ENV or DB
label="Human Readable", # Display name in Web UI
description="...", # Help text in Web UI
value_type=str, # str, int, bool, float, or custom converter
group="core", # Grouping: "core", "interface", "plugin", "llm"
component="my_component", # Component name for attribution
advanced=False, # True to hide in basic settings view
sensitive=True, # True to hide value in UI (passwords, tokens)
tags=["bootstrap"], # Special tags (usually not needed)
constraints={"min": 0}, # Validation constraints (optional)
)
Configuration Precedence
The system follows this priority order:
Environment variable (highest priority, read-only in UI)
Database value (persisted user changes via Web UI)
Default value (fallback if not set anywhere)
When an ENV variable exists, it:
Overrides the database value
Is marked as read-only in the Web UI
Shows an “override” indicator
Still gets persisted to DB for visibility
Testing Your Component
After implementing configuration:
Test with ENV variable:
export MY_TOKEN="test_value" python main.py
→ Variable should be read-only in UI
Test with DB value:
Remove from ENV
Set value in Web UI
Restart application
→ Value should persist
Test default:
Remove from ENV and DB
→ Should use default value
Common Patterns
Feature Flags
ENABLE_FEATURE = config_registry.get_var(
"ENABLE_FEATURE",
False,
value_type="bool",
label="Enable Feature",
...
)
if ENABLE_FEATURE:
# Feature code
pass
Numeric Settings
TIMEOUT = config_registry.get_var(
"TIMEOUT",
30,
value_type=int,
label="Timeout (seconds)",
constraints={"min": 1, "max": 300},
...
)
await asyncio.wait_for(operation(), timeout=int(TIMEOUT))
List/Set Settings
ALLOWED_IDS = config_registry.get_var(
"ALLOWED_IDS",
"",
label="Allowed IDs",
description="Comma-separated list of allowed user IDs",
...
)
def get_allowed_ids() -> set[str]:
value = str(ALLOWED_IDS).strip()
return set(x.strip() for x in value.split(",") if x.strip())
Migration from Old Pattern
If you have existing code using the old pattern:
# Old
VAR = config_registry.get_value("VAR", "default", ...)
def _update_var(value):
global VAR
VAR = value
config_registry.add_listener("VAR", _update_var)
Convert to:
# New
VAR = config_registry.get_var("VAR", "default", ...)
That’s it! Remove the listener function and add_listener call.
Need Help?
Check existing interfaces:
interface/telegram_bot.py,interface/discord_interface.pyCheck core modules:
core/persona_manager.pyAsk in the development channel
Summary
✅ DO:
Use
get_var()for module-level configuration variablesUse ConfigVar objects naturally (they support bool, str, or, eq)
Create helper functions for complex value processing
❌ DON’T:
Use
get_value()+ manual listeners for global variablesUpdate globals manually in listener functions
Assume values stay constant (they update automatically)
Note
Remember: If you declare a configuration variable at module level, use get_var(). The system handles everything else automatically!