from __future__ import annotations import json from dataclasses import dataclass, field from pathlib import Path from typing import Any import yaml from app.schemas.configuration import ( ActionsConfig, ContextRewriteConfig, DialogActsConfig, DialogRulesConfig, DomainConfig, FormsConfig, ResponsesConfig, WorkflowTemplatesConfig, ) from app.services.dialog_act import DialogActEngine from app.services.dialog_rules import DialogRuleEngine from app.services.intent_registry import IntentRegistry from app.services.rewrite_engine import ContextRewriteEngine @dataclass class RuntimeConfigBundle: intent_registry: IntentRegistry response_templates: dict[str, str] intent_hints: dict[str, str] dialog_rules: DialogRuleEngine dialog_act_engine: DialogActEngine workflow_templates: WorkflowTemplatesConfig rewrite_engine: ContextRewriteEngine = field(default_factory=ContextRewriteEngine) class ConfigLoader: def __init__( self, domain_path: str, action_path: str, response_path: str, form_path: str | None = None, rule_path: str | None = None, dialog_act_path: str | None = None, workflow_path: str | None = None, legacy_intent_path: str | None = None, context_rewrite_path: str | None = None, ) -> None: self._domain_path = Path(domain_path) self._action_path = Path(action_path) self._response_path = Path(response_path) self._form_path = Path(form_path) if form_path else None self._rule_path = Path(rule_path) if rule_path else None self._dialog_act_path = Path(dialog_act_path) if dialog_act_path else None self._workflow_path = Path(workflow_path) if workflow_path else None self._legacy_intent_path = Path(legacy_intent_path) if legacy_intent_path else None self._context_rewrite_path = Path(context_rewrite_path) if context_rewrite_path else None def load(self) -> RuntimeConfigBundle: if self._domain_path.exists() and self._action_path.exists(): return self._load_from_config_files() if self._legacy_intent_path is not None and self._legacy_intent_path.exists(): return RuntimeConfigBundle( intent_registry=IntentRegistry.from_json(str(self._legacy_intent_path)), response_templates=self._load_response_templates(), intent_hints={}, dialog_rules=self._load_dialog_rules(), dialog_act_engine=self._load_dialog_act_engine(), workflow_templates=self._load_workflow_templates(), rewrite_engine=self._load_rewrite_engine(), ) raise FileNotFoundError( "no runtime config found, expected config/*.yml or legacy intent json" ) def _load_from_config_files(self) -> RuntimeConfigBundle: domain = DomainConfig.model_validate(self._read_structured_file(self._domain_path)) actions = ActionsConfig.model_validate(self._read_structured_file(self._action_path)) forms = self._load_forms() action_map = {item.action_id: item for item in actions.actions} form_map = {item.intent_id: item for item in forms.forms} intents = [] for item in domain.intents: form = form_map.get(item.intent_id) if form is not None: item = item.model_copy( update={ "required_slots": form.required_slots, "ask_templates": form.ask_templates, } ) intents.append(item.to_intent_definition(action_map)) intent_hints = { item.intent_id: item.label.strip() for item in domain.intents if item.label and item.label.strip() } return RuntimeConfigBundle( intent_registry=IntentRegistry(intents), response_templates=self._load_response_templates(), intent_hints=intent_hints, dialog_rules=self._load_dialog_rules(), dialog_act_engine=self._load_dialog_act_engine(), workflow_templates=self._load_workflow_templates(), rewrite_engine=self._load_rewrite_engine(), ) def _load_response_templates(self) -> dict[str, str]: if not self._response_path.exists(): return {} raw = self._read_structured_file(self._response_path) parsed = ResponsesConfig.model_validate(raw) return parsed.templates def _load_forms(self) -> FormsConfig: if self._form_path is None or not self._form_path.exists(): return FormsConfig() raw = self._read_structured_file(self._form_path) return FormsConfig.model_validate(raw) def _load_dialog_rules(self) -> DialogRuleEngine: if self._rule_path is None or not self._rule_path.exists(): return DialogRuleEngine() raw = self._read_structured_file(self._rule_path) parsed = DialogRulesConfig.model_validate(raw) return DialogRuleEngine( stop_phrases=tuple(parsed.stop.phrases) or DialogRuleEngine.stop_phrases, positive_confirmation_tokens=tuple(parsed.confirmation.positive_tokens) or DialogRuleEngine.positive_confirmation_tokens, negative_confirmation_tokens=tuple(parsed.confirmation.negative_tokens) or DialogRuleEngine.negative_confirmation_tokens, confirmation_required_intents=tuple(parsed.confirmation.required_intents) or DialogRuleEngine.confirmation_required_intents, confirmation_required_risk_levels=tuple(parsed.confirmation.required_risk_levels) or DialogRuleEngine.confirmation_required_risk_levels, metadata={"source": str(self._rule_path)}, ) def _load_dialog_act_engine(self) -> DialogActEngine: if self._dialog_act_path is None or not self._dialog_act_path.exists(): return DialogActEngine() raw = self._read_structured_file(self._dialog_act_path) parsed = DialogActsConfig.model_validate(raw) return DialogActEngine( patterns={ item.act_id: tuple(item.phrases) for item in parsed.acts }, numeric_patterns={ item.act_id: tuple(item.numeric_patterns) for item in parsed.acts if item.numeric_patterns }, ) def _load_rewrite_engine(self) -> ContextRewriteEngine: if self._context_rewrite_path is None or not self._context_rewrite_path.exists(): return ContextRewriteEngine() raw = self._read_structured_file(self._context_rewrite_path) config = ContextRewriteConfig.model_validate(raw) return ContextRewriteEngine(config=config) def _load_workflow_templates(self) -> WorkflowTemplatesConfig: if self._workflow_path is None or not self._workflow_path.exists(): return WorkflowTemplatesConfig() raw = self._read_structured_file(self._workflow_path) return WorkflowTemplatesConfig.model_validate(raw) def _read_structured_file(self, path: Path) -> dict[str, Any]: if path.suffix.lower() == ".json": return json.loads(path.read_text(encoding="utf-8")) return yaml.safe_load(path.read_text(encoding="utf-8")) or {}