"""Khung nhìn có kiểu cho từng nhóm cấu hình — R02-T03. Vấn đề đang có: khắp nơi viết ``ctx.config.routing.get("switch_mode", "off")``. Gõ sai một chữ thì lặng lẽ nhận giá trị mặc định, không ai biết cho tới khi tính năng "không hiểu sao không chạy". Đếm được **156 lời gọi ``ctx.config.*`` trong 29 file** kiểu đó. Ở đây mỗi nhóm cấu hình có một lớp: gõ sai tên thuộc tính là lỗi ngay, và kiểu dữ liệu ghi rõ ràng nên đọc code là biết ``confirm_timeout_sec`` là số giây chứ không phải mili giây. Cố ý KHÔNG dùng dataclass đông cứng: đây là *khung nhìn* lên dict cấu hình sống, sửa qua đây là sửa vào dict rồi ``save()`` là xuống đĩa. Sao chép thành dataclass thì lại sinh chuyện đồng bộ hai chiều. """ from __future__ import annotations from typing import Any, Dict class _View: """Khung nhìn lên một nhánh của dict cấu hình.""" def __init__(self, data: Dict[str, Any]): self._d = data def _get(self, key: str, default: Any) -> Any: value = self._d.get(key, default) return default if value is None else value def raw(self) -> Dict[str, Any]: """Dict gốc — dùng khi cần đọc khoá chưa được đưa vào khung nhìn. Có mặt để không ai bị kẹt: thiếu thuộc tính thì dùng tạm ``raw()`` rồi mở issue bổ sung, chứ đừng vòng lại ``ctx.config.data``. """ return self._d class ProviderSettings(_View): """Một provider: đi đâu, model nào, khoá nào. ``api_key`` ở đây là thứ ``JsonConfigRepository.provider_conf()`` đã ghép sẵn từ kho bí mật — xem đường A trong ``GammaTeam_decisions.md``. """ @property def base_url(self) -> str: return str(self._get("base_url", "")) @property def model(self) -> str: return str(self._get("model", "")) @property def api_key(self) -> str: return str(self._get("api_key", "")) @property def configured(self) -> bool: """Đủ thông tin để gọi được chưa. Ollama chạy cục bộ nên không cần khoá — đó là lý do điều kiện là "có base_url và model", không phải "có api_key". """ return bool(self.base_url and self.model) class RoutingSettings(_View): """Định tuyến model tự động (``core/routing/``).""" @property def switch_mode(self) -> str: """``"off"`` | ``"auto"`` | ``"manual"``.""" return str(self._get("switch_mode", "off")) @switch_mode.setter def switch_mode(self, value: str) -> None: self._d["switch_mode"] = value @property def enabled(self) -> bool: return self.switch_mode != "off" @property def policy(self) -> str: """``"balanced"`` | ``"cheap"`` | ``"quality"``…""" return str(self._get("policy", "balanced")) @property def min_score_gain(self) -> float: """Phải hơn model hiện tại bao nhiêu điểm mới đáng đổi.""" return float(self._get("min_score_gain", 0.05)) @property def confirm_timeout_sec(self) -> int: """GIÂY, không phải mili giây — đọc tên là biết, khỏi phải mò.""" return int(self._get("confirm_timeout_sec", 60)) @property def reassess_interval_hours(self) -> int: return int(self._get("reassess_interval_hours", 24)) @property def per_provider_concurrency(self) -> int: return int(self._get("per_provider_concurrency", 2)) @property def judge_provider(self) -> str: return str(self._get("judge_provider", "")) @property def judge_model(self) -> str: return str(self._get("judge_model", "")) class SecuritySettings(_View): """Chính sách an toàn cho agent (``core/agent_security.py``).""" @property def enabled(self) -> bool: return bool(self._get("enabled", True)) @property def validate_prompt(self) -> bool: return bool(self._get("validate_prompt", True)) @property def validate_attachments(self) -> bool: return bool(self._get("validate_attachments", True)) @property def validate_commands(self) -> bool: return bool(self._get("validate_commands", True)) @property def command_ai_check(self) -> bool: return bool(self._get("command_ai_check", False)) @property def cowork_confirm_commands(self) -> bool: """Có hỏi trước khi chạy lệnh không. Ứng với ``PolicyOutcome.ASK`` trong ``domain/security/tool_policy.py``. """ return bool(self._get("cowork_confirm_commands", True)) @property def rules_onedrive_url(self) -> str: return str(self._get("rules_onedrive_url", "")) @property def admin_email(self) -> str: return str(self._get("admin_email", "")) class Settings: """Cửa vào duy nhất cho các nhóm cấu hình có kiểu. >>> s = Settings(repo) >>> if s.routing.enabled and s.provider().configured: ... ... """ def __init__(self, repo): self._repo = repo def provider(self, name: str | None = None) -> ProviderSettings: return ProviderSettings(self._repo.provider_conf(name)) @property def routing(self) -> RoutingSettings: return RoutingSettings(self._repo.routing) @property def security(self) -> SecuritySettings: return SecuritySettings(self._repo.agent_security)