"""SenseConfig:从 settings 的 sense 段构建语义分析器配置(T-G0)。 灰度纪律(D-G7): - enabled 默认 False(总开关,关闭时不注册任何 /sense 或 /v1/route 路由); - mode 默认 "collect"(只记不决策);collect 攒满 min_labels 前禁止 live (晋升校验在 grader/admin,配置层不阻止写值——但 admin promote 会复核)。 """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Dict MODES = ("collect", "shadow", "live") @dataclass class EmbedderCfg: base_url: str = "http://127.0.0.1:8902/v1" model: str = "bge-m3-Q4_K_M" timeout_s: float = 5.0 dim: int = 1024 @dataclass class SenseConfig: enabled: bool = False mode: str = "collect" # collect | shadow | live db_path: str = "data/sense.sqlite3" admin_key: str = "" embedder: EmbedderCfg = field(default_factory=EmbedderCfg) t1_max_tokens: int = 512 t1_max_turns: int = 2 intent_blacklist: tuple = ("重构", "脚手架", "迁移", "实现", "多文件", "项目") code_t1_kinds: tuple = ("解释", "补全") alpha: float = 0.05 min_labels: int = 500 t2_prefer_local_when_idle: bool = True consumers: Dict[str, Dict[str, str]] = field(default_factory=lambda: { "proxy": {"t1": "local-small", "t2": "budget", "t3": "premium"}}) models_dir: str = "data/sense_models" retention_days: int = 180 def tier_pool_hint(self, consumer: str, tier: str) -> str: """消费方×档位 -> model_pool 档位名(§5 consumers;缺省兜底 local/budget/premium)。""" m = self.consumers.get(consumer) or {} return m.get(tier) or {"t1": "local", "t2": "budget", "t3": "premium"}.get(tier, "budget") def build_sense_config(settings_dict: Dict[str, Any]) -> SenseConfig: """从 settings.to_dict() 的 sense 段构建(缺省兜底;mode 非法回落 collect)。""" raw = settings_dict.get("sense") or {} if not isinstance(raw, dict): raw = {} emb = raw.get("embedder") or {} if not isinstance(emb, dict): emb = {} feats = raw.get("features") or {} if not isinstance(feats, dict): feats = {} policy = raw.get("policy") or {} if not isinstance(policy, dict): policy = {} consumers = raw.get("consumers") or {} if not isinstance(consumers, dict): consumers = {} mode = str(raw.get("mode", "collect") or "collect") if mode not in MODES: mode = "collect" return SenseConfig( enabled=bool(raw.get("enabled", False)), mode=mode, db_path=str(raw.get("db_path") or "data/sense.sqlite3"), admin_key=str(raw.get("admin_key") or ""), embedder=EmbedderCfg( base_url=str(emb.get("base_url") or "http://127.0.0.1:8902/v1"), model=str(emb.get("model") or "bge-m3-Q4_K_M"), timeout_s=float(emb.get("timeout_s", 5) or 5), dim=int(emb.get("dim", 1024) or 1024), ), t1_max_tokens=int(feats.get("t1_max_tokens", 512) or 512), t1_max_turns=int(feats.get("t1_max_turns", 2) or 2), intent_blacklist=tuple(feats.get("intent_blacklist") or ("重构", "脚手架", "迁移", "实现", "多文件", "项目")), code_t1_kinds=tuple(feats.get("code_t1_kinds") or ("解释", "补全")), alpha=float(policy.get("alpha", 0.05) or 0.05), min_labels=int(policy.get("min_labels", 500) or 500), t2_prefer_local_when_idle=bool(policy.get("t2_prefer_local_when_idle", True)), consumers=consumers or SenseConfig().consumers, )