- config.py: extracted load_config(), find_workspace_config(), load_discussion_config() from cli.py; fixed Windows root detection - cli.py: run_discussion() now creates DiscussionEngine + EventEmitter, subscribes to events with ANSI-color output (identical to before), delegates to engine.run() - backends/custom/run.py: deprecation shim with DeprecationWarning, delegates to DiscussionEngine - 16 new config tests, all 184 tests passing Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
115 lines
3.2 KiB
Python
115 lines
3.2 KiB
Python
"""Configuration loading for Meeting Room.
|
|
|
|
Provides load_config(), find_workspace_config(), and load_discussion_config()
|
|
which converts a raw YAML dict into a typed DiscussionConfig.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
|
|
import yaml
|
|
|
|
from meeting_room.models import DefaultsConfig, DiscussionConfig, ProviderConfig, RoleConfig
|
|
|
|
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def load_config(config_path: str | None = None) -> dict:
|
|
"""Load config from file, resolve env vars, apply overrides.
|
|
|
|
Parameters
|
|
----------
|
|
config_path : str | None
|
|
Path to the YAML config file. When *None*, loads the package
|
|
default config at ``meeting_room/config/config.yaml``.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Raw configuration dictionary (the result of ``yaml.safe_load``).
|
|
"""
|
|
if config_path is None:
|
|
config_path = os.path.join(PACKAGE_DIR, "config", "config.yaml")
|
|
|
|
with open(config_path, encoding="utf-8") as f:
|
|
raw = f.read()
|
|
|
|
# Resolve ${ENV_VAR} placeholders
|
|
def _env_replace(match: re.Match) -> str:
|
|
var = match.group(1)
|
|
return os.environ.get(var, match.group(0))
|
|
|
|
raw = re.sub(r"\$\{(\w+)\}", _env_replace, raw)
|
|
|
|
config = yaml.safe_load(raw)
|
|
|
|
# Override API keys from environment if set
|
|
for prov_name, prov_cfg in config.get("providers", {}).items():
|
|
env_key = f"MEETING_ROOM_{prov_name.upper()}_API_KEY"
|
|
if os.environ.get(env_key):
|
|
prov_cfg["api_key"] = os.environ[env_key]
|
|
env_url = f"MEETING_ROOM_{prov_name.upper()}_BASE_URL"
|
|
if os.environ.get(env_url):
|
|
prov_cfg["base_url"] = os.environ[env_url]
|
|
|
|
return config
|
|
|
|
|
|
def find_workspace_config() -> str | None:
|
|
"""Search for .meeting-room/config.yaml starting from CWD upward.
|
|
|
|
Returns
|
|
-------
|
|
str | None
|
|
Absolute path to the workspace config file, or *None* if not
|
|
found.
|
|
"""
|
|
current = os.getcwd()
|
|
while True:
|
|
candidate = os.path.join(current, ".meeting-room", "config.yaml")
|
|
if os.path.exists(candidate):
|
|
return candidate
|
|
parent = os.path.dirname(current)
|
|
if parent == current:
|
|
# Reached filesystem root
|
|
break
|
|
current = parent
|
|
return None
|
|
|
|
|
|
def load_discussion_config(config_path: str | None = None) -> DiscussionConfig:
|
|
"""Load config and return a typed DiscussionConfig.
|
|
|
|
Convenience wrapper that calls :func:`load_config` and then
|
|
coerces the raw dict into :class:`DiscussionConfig`.
|
|
|
|
Parameters
|
|
----------
|
|
config_path : str | None
|
|
Path to the YAML config file. When *None*, loads the package
|
|
default.
|
|
|
|
Returns
|
|
-------
|
|
DiscussionConfig
|
|
Typed configuration object ready for use by DiscussionEngine.
|
|
"""
|
|
raw = load_config(config_path)
|
|
|
|
providers = {
|
|
name: ProviderConfig(**vals)
|
|
for name, vals in raw.get("providers", {}).items()
|
|
}
|
|
roles = {
|
|
name: RoleConfig(**vals)
|
|
for name, vals in raw.get("roles", {}).items()
|
|
}
|
|
defaults = DefaultsConfig(**raw.get("defaults", {}))
|
|
|
|
return DiscussionConfig(
|
|
providers=providers,
|
|
roles=roles,
|
|
defaults=defaults,
|
|
) |