Spaces:
Running
Running
| """Configuration system β hierarchical settings + constants. | |
| This module combines: | |
| - constants.py (app-wide constants, model registry, system prompt, languages) | |
| - settings (hierarchical Settings, MCP server config, policy rules, load_settings) | |
| Path layout: | |
| - WORKSPACE_ROOT: where the agent's sandboxed filesystem lives | |
| - CONFIG_DIR: where settings/agents/hooks/skills are stored (default: .sonicoder/) | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Any | |
| # ββ Re-export constants for backward compatibility βββββββββββββββββββββ | |
| from code.config.constants import ( | |
| APP_TITLE, | |
| DEFAULT_MAX_TOKENS, | |
| DEFAULT_MODEL_KEY, | |
| DEFAULT_TEMPERATURE, | |
| EXAMPLE_PROMPTS, | |
| LANGUAGE_MAP, | |
| LANGUAGE_OPTIONS, | |
| MODEL_CONFIGS, | |
| MODEL_ID, | |
| MODEL_URL, | |
| SYSTEM_PROMPT, | |
| ) | |
| # ββ Path Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| WORKSPACE_ROOT = Path( | |
| os.environ.get( | |
| "SONICODER_WORKSPACE", | |
| os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "workspace")), | |
| ) | |
| ) | |
| CONFIG_DIR = Path( | |
| os.environ.get( | |
| "SONICODER_CONFIG", | |
| os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".sonicoder")), | |
| ) | |
| ) | |
| # ββ Runtime Defaults (override constants) ββββββββββββββββββββββββββββββ | |
| DEFAULT_MAX_ITERATIONS = 12 | |
| AGENT_LOOP_TIMEOUT = 300 # seconds | |
| BASH_TIMEOUT = 120 | |
| PYTHON_TIMEOUT = 30 | |
| # ββ Settings Dataclasses βββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ModelConfig: | |
| """Configuration for a single model.""" | |
| name: str | |
| model_id: str | |
| model_type: str # "text" | "vlm" | |
| size_gb: float | |
| description: str | |
| dtype: str = "float16" | |
| trust_remote_code: bool = True | |
| device: str = "auto" | |
| class MCPServerConfig: | |
| """Configuration for an MCP server.""" | |
| name: str | |
| transport: str # "stdio" | "sse" | |
| command: str | None = None | |
| args: list[str] = field(default_factory=list) | |
| env: dict[str, str] = field(default_factory=dict) | |
| url: str | None = None | |
| enabled: bool = True | |
| class PolicyRule: | |
| """A policy rule for tool approval.""" | |
| tool_name: str # supports "*" wildcard | |
| args_pattern: str | None = None | |
| approval: str = "ask" # "allow" | "ask" | "deny" | |
| priority: int = 0 | |
| description: str = "" | |
| class Settings: | |
| """Hierarchical settings β cascades from global β project β local.""" | |
| # Model | |
| default_model: str = DEFAULT_MODEL_KEY | |
| temperature: float = DEFAULT_TEMPERATURE | |
| max_tokens: int = DEFAULT_MAX_TOKENS | |
| # Agent | |
| max_iterations: int = DEFAULT_MAX_ITERATIONS | |
| agent_timeout: int = AGENT_LOOP_TIMEOUT | |
| # Tools | |
| enabled_tools: list[str] = field(default_factory=lambda: ["*"]) | |
| disabled_tools: list[str] = field(default_factory=list) | |
| # Safety | |
| sandbox_enabled: bool = True | |
| bash_timeout: int = BASH_TIMEOUT | |
| python_timeout: int = PYTHON_TIMEOUT | |
| # MCP | |
| mcp_servers: list[MCPServerConfig] = field(default_factory=list) | |
| # Policy | |
| policy_rules: list[PolicyRule] = field(default_factory=list) | |
| # Context | |
| max_context_tokens: int = 32768 | |
| auto_compact: bool = True | |
| def from_dict(cls, data: dict[str, Any]) -> Settings: | |
| """Create settings from a dictionary, ignoring unknown fields.""" | |
| known_fields = {f.name for f in cls.__dataclass_fields__.values()} | |
| filtered = {k: v for k, v in data.items() if k in known_fields} | |
| return cls(**filtered) | |
| def merge(self, other: Settings) -> Settings: | |
| """Merge another settings object (other takes precedence for non-default values).""" | |
| result = Settings( | |
| default_model=other.default_model or self.default_model, | |
| temperature=( | |
| other.temperature | |
| if other.temperature != DEFAULT_TEMPERATURE | |
| else self.temperature | |
| ), | |
| max_tokens=( | |
| other.max_tokens | |
| if other.max_tokens != DEFAULT_MAX_TOKENS | |
| else self.max_tokens | |
| ), | |
| max_iterations=( | |
| other.max_iterations | |
| if other.max_iterations != DEFAULT_MAX_ITERATIONS | |
| else self.max_iterations | |
| ), | |
| agent_timeout=( | |
| other.agent_timeout | |
| if other.agent_timeout != AGENT_LOOP_TIMEOUT | |
| else self.agent_timeout | |
| ), | |
| sandbox_enabled=other.sandbox_enabled, | |
| bash_timeout=other.bash_timeout, | |
| python_timeout=other.python_timeout, | |
| auto_compact=other.auto_compact, | |
| ) | |
| # Merge lists | |
| result.enabled_tools = ( | |
| other.enabled_tools if other.enabled_tools != ["*"] else self.enabled_tools | |
| ) | |
| result.disabled_tools = list(set(self.disabled_tools + other.disabled_tools)) | |
| result.mcp_servers = self.mcp_servers + other.mcp_servers | |
| result.policy_rules = other.policy_rules + self.policy_rules | |
| return result | |
| # ββ Public Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_available_models() -> dict[str, dict[str, Any]]: | |
| """Return all registered model configs (from constants.MODEL_CONFIGS).""" | |
| return MODEL_CONFIGS.copy() | |
| def get_model_config(model_key: str) -> dict[str, Any] | None: | |
| """Get a model config by key.""" | |
| return MODEL_CONFIGS.get(model_key) | |
| def load_settings(project_dir: Path | None = None) -> Settings: | |
| """Load settings with hierarchical cascading. | |
| Priority (highest β lowest): | |
| 1. Environment variables | |
| 2. .sonicoder/settings.local.json (local overrides, gitignored) | |
| 3. .sonicoder/settings.json (project settings) | |
| 4. Defaults | |
| """ | |
| settings = Settings() | |
| search_dirs = [Path.cwd()] | |
| if project_dir: | |
| search_dirs.insert(0, project_dir) | |
| # Load project-level settings | |
| for d in search_dirs: | |
| local_path = d / CONFIG_DIR / "settings.local.json" | |
| proj_path = d / CONFIG_DIR / "settings.json" | |
| for path in [local_path, proj_path]: | |
| if path.exists(): | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| # Parse MCP server configs | |
| if "mcp_servers" in data: | |
| data["mcp_servers"] = [ | |
| MCPServerConfig(**s) for s in data["mcp_servers"] | |
| ] | |
| if "policy_rules" in data: | |
| data["policy_rules"] = [ | |
| PolicyRule(**r) for r in data["policy_rules"] | |
| ] | |
| settings = settings.merge(Settings.from_dict(data)) | |
| except (json.JSONDecodeError, TypeError) as e: | |
| print(f"[config] Warning: Failed to parse {path}: {e}") | |
| # Environment variable overrides | |
| env_model = os.environ.get("SONICODER_MODEL") | |
| if env_model and env_model in MODEL_CONFIGS: | |
| settings.default_model = env_model | |
| env_temp = os.environ.get("SONICODER_TEMPERATURE") | |
| if env_temp: | |
| try: | |
| settings.temperature = float(env_temp) | |
| except ValueError: | |
| pass | |
| return settings | |
| __all__ = [ | |
| # Constants | |
| "APP_TITLE", | |
| "DEFAULT_MAX_TOKENS", | |
| "DEFAULT_MODEL_KEY", | |
| "DEFAULT_TEMPERATURE", | |
| "EXAMPLE_PROMPTS", | |
| "LANGUAGE_MAP", | |
| "LANGUAGE_OPTIONS", | |
| "MODEL_CONFIGS", | |
| "MODEL_ID", | |
| "MODEL_URL", | |
| "SYSTEM_PROMPT", | |
| # Path constants | |
| "WORKSPACE_ROOT", | |
| "CONFIG_DIR", | |
| # Runtime defaults | |
| "DEFAULT_MAX_ITERATIONS", | |
| "AGENT_LOOP_TIMEOUT", | |
| "BASH_TIMEOUT", | |
| "PYTHON_TIMEOUT", | |
| # Dataclasses | |
| "ModelConfig", | |
| "MCPServerConfig", | |
| "PolicyRule", | |
| "Settings", | |
| # Helpers | |
| "get_available_models", | |
| "get_model_config", | |
| "load_settings", | |
| ] | |