From a0dd76e3def212bda17dbd64bfd79b3918d5a069 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:33:23 +0000 Subject: [PATCH] Changes before error encountered Agent-Logs-Url: https://github.com/HiLleywyn/Hydra/sessions/60fe845f-c411-48b8-bef0-7ffef9c2e25f Co-authored-by: HiLleywyn <97213385+HiLleywyn@users.noreply.github.com> --- hydra/config_schema.py | 115 +++++++++++++++++++++++++++++++++++++++++ hydra/core/runtime.py | 43 ++++++++------- hydra/main.py | 26 ++++++---- 3 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 hydra/config_schema.py diff --git a/hydra/config_schema.py b/hydra/config_schema.py new file mode 100644 index 0000000..9e2dd96 --- /dev/null +++ b/hydra/config_schema.py @@ -0,0 +1,115 @@ +"""Config schema — Pydantic models for hydra/config.json validation. + +Validates the full framework configuration at startup so misconfigurations +are caught immediately rather than at runtime when a .get() path is first hit. +""" + +from __future__ import annotations + +from typing import Annotated, Literal, Union + +from pydantic import BaseModel, Field, field_validator + + +# --------------------------------------------------------------------------- +# Sub-section models +# --------------------------------------------------------------------------- + +class FrameworkConfig(BaseModel): + name: str = "Hydra" + version: str = "0.1.0" + debug: bool = False + log_level: str = "INFO" + + @field_validator("log_level") + @classmethod + def validate_log_level(cls, v: str) -> str: + valid = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} + upper = v.upper() + if upper not in valid: + raise ValueError(f"log_level must be one of {valid}, got '{v}'") + return upper + + +class DatabaseConfig(BaseModel): + url: str = "sqlite+aiosqlite:///hydra.db" + + +class DashboardConfig(BaseModel): + host: str = "0.0.0.0" + port: int = Field(default=8080, ge=1, le=65535) + enabled: bool = True + + +class ContextBusConfig(BaseModel): + backend: str = "memory" + max_queue_size: int = Field(default=10000, ge=1) + overflow_policy: str = "drop_oldest" + default_ttl: float | None = None + redis_url: str = "redis://localhost:6379/0" + + +class ProfileConfig(BaseModel): + id: str + name: str + avatar_url: str = "" + banner_url: str = "" + status: str = "" + theme: str = "default" + adapters: list[str] = [] + plugins: list[str] = [] + workers: list[str] = [] + + +# --------------------------------------------------------------------------- +# Adapter config models (discriminated union on "type") +# --------------------------------------------------------------------------- + +class MockAdapterConfig(BaseModel): + type: Literal["mock"] + interval: float = 15 + + +class DiscordAdapterConfig(BaseModel): + type: Literal["discord"] + token: str = "" + intents: int = 513 + + +class OpenAIAdapterConfig(BaseModel): + type: Literal["openai"] + base_url: str = "https://api.openai.com/v1" + api_key: str = "" + model: str = "gpt-4" + temperature: float = Field(default=0.7, ge=0.0, le=2.0) # OpenAI API accepted range + + +class MCPAdapterConfig(BaseModel): + type: Literal["mcp"] + command: str = "" + args: list[str] = [] + env: dict[str, str] = {} + + +AdapterConfig = Annotated[ + Union[ + MockAdapterConfig, + DiscordAdapterConfig, + OpenAIAdapterConfig, + MCPAdapterConfig, + ], + Field(discriminator="type"), +] + + +# --------------------------------------------------------------------------- +# Top-level config model +# --------------------------------------------------------------------------- + +class HydraConfig(BaseModel): + framework: FrameworkConfig = FrameworkConfig() + database: DatabaseConfig = DatabaseConfig() + dashboard: DashboardConfig = DashboardConfig() + context_bus: ContextBusConfig = ContextBusConfig() + profiles: list[ProfileConfig] = [] + adapters: dict[str, AdapterConfig] = {} diff --git a/hydra/core/runtime.py b/hydra/core/runtime.py index 2c34abe..8aa84e1 100644 --- a/hydra/core/runtime.py +++ b/hydra/core/runtime.py @@ -15,6 +15,7 @@ from enum import Enum from typing import Any +from hydra.config_schema import HydraConfig from hydra.core.context_bus import ContextBus from hydra.core.plugin_manager import PluginManager from hydra.core.registry import Registry @@ -50,18 +51,17 @@ class HydraRuntime: and graceful shutdown. Supports hot-reload of plugins and themes. """ - def __init__(self, config: dict[str, Any]) -> None: + def __init__(self, config: HydraConfig) -> None: self.config = config self.state = RuntimeState.INITIALIZING # Context bus with pluggable backend - bus_config = config.get("context_bus", {}) self.context_bus = ContextBus( - max_queue_size=bus_config.get("max_queue_size", 10000), - overflow_policy=bus_config.get("overflow_policy", "drop_oldest"), - default_ttl=bus_config.get("default_ttl"), - backend_type=bus_config.get("backend", "memory"), - redis_url=bus_config.get("redis_url", "redis://localhost:6379/0"), + max_queue_size=config.context_bus.max_queue_size, + overflow_policy=config.context_bus.overflow_policy, + default_ttl=config.context_bus.default_ttl, + backend_type=config.context_bus.backend, + redis_url=config.context_bus.redis_url, ) self.registry = Registry() self.plugin_manager = PluginManager() @@ -80,7 +80,7 @@ async def initialize(self) -> None: # Initialize database try: from hydra.database import init_database - db_url = self.config.get("database", {}).get("url", "sqlite+aiosqlite:///hydra.db") + db_url = self.config.database.url await init_database(db_url) self._db_available = True except Exception: @@ -128,10 +128,9 @@ async def initialize(self) -> None: logger.error("Plugin dependency resolution failed: %s", e) # Create adapters from config - adapters_config = self.config.get("adapters", {}) - for adapter_id, adapter_conf in adapters_config.items(): - adapter_type = adapter_conf.get("type", "") - adapter = self._create_adapter(adapter_id, adapter_type, adapter_conf) + for adapter_id, adapter_conf in self.config.adapters.items(): + adapter_type = adapter_conf.type + adapter = self._create_adapter(adapter_id, adapter_type, adapter_conf.model_dump()) if adapter: self.registry.register_adapter(adapter_id, adapter) @@ -140,17 +139,17 @@ async def initialize(self) -> None: await self._load_profiles_from_db() # Create profiles from config (overrides DB if same ID) - for profile_conf in self.config.get("profiles", []): + for profile_conf in self.config.profiles: profile = Profile( - id=profile_conf["id"], - name=profile_conf["name"], - avatar_url=profile_conf.get("avatar_url", ""), - banner_url=profile_conf.get("banner_url", ""), - status=profile_conf.get("status", ""), - theme=profile_conf.get("theme", "default"), - adapters=profile_conf.get("adapters", []), - plugins=profile_conf.get("plugins", []), - workers=profile_conf.get("workers", []), + id=profile_conf.id, + name=profile_conf.name, + avatar_url=profile_conf.avatar_url, + banner_url=profile_conf.banner_url, + status=profile_conf.status, + theme=profile_conf.theme, + adapters=list(profile_conf.adapters), + plugins=list(profile_conf.plugins), + workers=list(profile_conf.workers), ) self._profiles[profile.id] = profile self.registry.register_profile(profile.id, profile) diff --git a/hydra/main.py b/hydra/main.py index 45ad76c..bfa74d7 100644 --- a/hydra/main.py +++ b/hydra/main.py @@ -13,6 +13,10 @@ if _project_root not in sys.path: sys.path.insert(0, _project_root) +from pydantic import ValidationError + +from hydra.config_schema import HydraConfig + def setup_logging(level: str = "INFO") -> None: """Configure structured logging for the framework.""" @@ -28,17 +32,22 @@ def setup_logging(level: str = "INFO") -> None: logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) -def load_config(config_path: str) -> dict: - """Load framework configuration from JSON file.""" +def load_config(config_path: str) -> HydraConfig: + """Load and validate framework configuration from JSON file.""" path = Path(config_path) if not path.exists(): logging.error("Config file not found: %s", config_path) sys.exit(1) with open(path) as f: - return json.load(f) + raw = json.load(f) + try: + return HydraConfig.model_validate(raw) + except ValidationError as exc: + logging.error("Invalid configuration: %s", exc) + sys.exit(1) -async def run(config: dict, dashboard_host: str, dashboard_port: int) -> None: +async def run(config: HydraConfig, dashboard_host: str, dashboard_port: int) -> None: """Main async entry point — boots runtime and dashboard.""" from hydra.core.runtime import HydraRuntime from hydra.web_dashboard.app import create_app @@ -55,7 +64,7 @@ async def run(config: dict, dashboard_host: str, dashboard_port: int) -> None: # Create and start dashboard dashboard_task = None - if config.get("dashboard", {}).get("enabled", True): + if config.dashboard.enabled: app = create_app(runtime) import uvicorn uvi_config = uvicorn.Config( @@ -161,13 +170,12 @@ def main() -> None: config = load_config(args.config) # Setup logging - log_level = "DEBUG" if args.debug else config.get("framework", {}).get("log_level", "INFO") + log_level = "DEBUG" if args.debug else config.framework.log_level setup_logging(log_level) # Resolve dashboard host/port - dashboard_config = config.get("dashboard", {}) - host = args.host or dashboard_config.get("host", "0.0.0.0") - port = args.port or dashboard_config.get("port", 8080) + host = args.host or config.dashboard.host + port = args.port or config.dashboard.port # Run try: