sonicoder / code /config /__init__.py
R-Kentaren's picture
fix: consolidate to code/ only, fix bugs, add missing UI options
e3e7994 verified
Raw
History Blame Contribute Delete
8.42 kB
"""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 ───────────────────────────────────────────────
@dataclass
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"
@dataclass
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
@dataclass
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 = ""
@dataclass
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
@classmethod
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",
]