Source code for ralph.config.models

"""Pydantic v2 models for Ralph configuration."""

from __future__ import annotations

from pydantic import ConfigDict, Field, model_validator

from ralph.policy.models import AgentChainConfig, AgentDrainConfig
from ralph.pydantic_compat import RalphBaseModel

from .agent_config import AgentConfig
from .ccs_config import CcsAliasConfig, CcsConfig
from .general_config import GeneralConfig
from .prompt_helper_config import PromptHelperConfig


def _normalize_chain_value(value: object) -> AgentChainConfig:
    if isinstance(value, AgentChainConfig):
        return value
    if isinstance(value, list):
        return AgentChainConfig(agents=value)
    if isinstance(value, dict):
        return AgentChainConfig(
            agents=value.get("agents", []),
            max_retries=value.get("max_retries", 3),
            retry_delay_ms=value.get("retry_delay_ms", 1000),
        )
    return AgentChainConfig(agents=[str(value)])


def _normalize_drain_value(value: object) -> AgentDrainConfig:
    if isinstance(value, str):
        return AgentDrainConfig(chain=value)
    if isinstance(value, dict):
        return AgentDrainConfig(
            chain=value.get("chain", ""),
            drain_class=value.get("drain_class"),
            capability_class=value.get("capability_class"),
        )
    return AgentDrainConfig(chain=str(value))


[docs] class UnifiedConfig(RalphBaseModel): """Top-level merged configuration (global + local + CLI overrides).""" model_config = ConfigDict(frozen=True) general: GeneralConfig = Field(default_factory=GeneralConfig) ccs: CcsConfig = Field(default_factory=CcsConfig) agents: dict[str, AgentConfig] = Field(default_factory=dict) ccs_aliases: dict[str, str | CcsAliasConfig] = Field(default_factory=dict) agent_chains: dict[str, AgentChainConfig] = Field(default_factory=dict) agent_drains: dict[str, AgentDrainConfig] = Field(default_factory=dict) prompt_helper: PromptHelperConfig = Field(default_factory=PromptHelperConfig) @model_validator(mode="before") @classmethod def _normalize_agent_chains_and_drains(cls, data: object) -> object: """Accept both flat format (list[str]/str) and rich format for backward compat.""" if not isinstance(data, dict): return data normalized_data: dict[str, object] = { name: value for name, value in data.items() if isinstance(name, str) } chains = normalized_data.get("agent_chains") if isinstance(chains, dict): normalized_chains: dict[str, object] = {} for name, value in chains.items(): if not isinstance(name, str): continue normalized_chains[name] = _normalize_chain_value(value) normalized_data["agent_chains"] = normalized_chains drains = normalized_data.get("agent_drains") if isinstance(drains, dict): normalized_drains: dict[str, object] = {} for name, value in drains.items(): if not isinstance(name, str): continue normalized_drains[name] = _normalize_drain_value(value) normalized_data["agent_drains"] = normalized_drains return normalized_data @model_validator(mode="after") def _validate_drain_references(self) -> UnifiedConfig: """Ensure every drain references an existing chain.""" for drain_name, drain_cfg in self.agent_drains.items(): if drain_cfg.chain not in self.agent_chains: raise ValueError( f"Drain '{drain_name}' references unknown chain '{drain_cfg.chain}'" ) return self
__all__ = [ "AgentConfig", "CcsConfig", "GeneralConfig", "UnifiedConfig", ]