- gateway/sense/:config(SenseConfig:mode 灰度三态/特征门/conformal/consumers 档位映射,mode 非法回落 collect)、errors(EmbedderDown/ArtifactMissing,D-G4 降级)、 store(tier_observations + sense_artifacts DDL,WAL;观察写入/标签回填/ labeled 查询/180d 清理/工件登记与 active 切换)、routes(/sense/health 透出灰度状态) - settings DEFAULTS 增 sense 段(enabled 默认 False,mode 默认 collect——D-G7) - api.py include_router 门控(装配失败不拖垮主应用) - 测试 +5:门控 404/DDL 幂等/配置缺省与 mode 回落/独立挂载 health/ 观察写入-回填-计数-清理链路,全量 375 passed
163 lines
7.0 KiB
Python
163 lines
7.0 KiB
Python
"""可调整的运行设置(SettingsStore)—— 让用户自定义内置小模型 / 大模型 / 管线。
|
||
|
||
用户可在 Web 界面"模型设置"里调整并持久化到 config/settings.json(gitignore),
|
||
重启后保留。调整会触发 v2 管线重建(gateway.api.build_v2_pipeline 重新读取)。
|
||
|
||
默认值与 config/config.yaml 的 v2 段一致;settings.json 只存用户改动覆盖项。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any, Dict, Optional
|
||
|
||
_SETTINGS_PATH = Path(__file__).resolve().parent.parent / "config" / "settings.json"
|
||
|
||
# 可调整项(含默认值);用户改动存这里
|
||
DEFAULTS: Dict[str, Any] = {
|
||
"worker": {
|
||
"backend": "llama_server", # mock | openai/api | llama_server
|
||
"model": "models/qwen3.5-4b-q4_k_m.gguf",
|
||
"base_url": "", # openai 后端填 http://127.0.0.1:11434/v1 等
|
||
"port": 8901, # llama_server 端口
|
||
"temperature": 0.3,
|
||
"max_fix_attempts": 2,
|
||
"per_step_timeout_s": 15,
|
||
"code_timeout_s": 10,
|
||
},
|
||
"architect": {
|
||
"model": "deepseek-v4-flash",
|
||
"base_url": "https://api.deepseek.com",
|
||
"api_key": "",
|
||
},
|
||
"pipeline": {
|
||
"fast_path": True,
|
||
"rounds_cap": 6,
|
||
"api_token_cap": 8000,
|
||
"breach_policy": "architect_do",
|
||
},
|
||
"agent": {
|
||
"workspace_dir": "agent_workspace", # 智能体工作区根目录(越界即拒)
|
||
"recent_workspaces": [], # 最近打开的工作区(供快速切换)
|
||
"max_rounds": 8, # 工具循环轮数上限
|
||
"token_cap": 20000, # 单次智能体任务 token 熔断
|
||
"allow_shell": False, # 允许 run_command 执行 shell(默认关)
|
||
"shell_timeout_s": 20, # shell 命令超时
|
||
"allow_net": True, # 允许 web_fetch 抓取公网页面(SSRF 防护内置)
|
||
"max_handoffs": 2, # 两级模式:规划者<->执行者交接轮数上限
|
||
"approval_policy": "dangerous", # 审批策略:off | dangerous(写/编辑/命令询问)| all
|
||
"approval_timeout_s": 120, # 审批等待超时(超时自动拒绝)
|
||
},
|
||
# 校园 AI 代理层(T-P0;结构见《实施方案_代理层与缓存层.md》§4)
|
||
"proxy": {
|
||
"enabled": False, # D-P7:默认关,开启需显式配置(重启生效)
|
||
"admin_key": "", # 管理面密钥;空 = 仅 loopback 放行
|
||
"db_path": "data/proxy.sqlite3",
|
||
"buckets": { # 桶配置(嵌套对象原样存储)
|
||
"default": {"system_template": "你是校园学习助手。",
|
||
"doc_prefix_file": None, "doc_version": 1, "ttl_hours": 72},
|
||
},
|
||
"pricing": { # 元/1M tokens(加载期转毫元整数,D-P1)
|
||
"deepseek-chat": {"in_miss": 3.0, "in_hit": 0.1, "out": 9.0},
|
||
"peak_window": {"start": "08:30", "end": "23:59"},
|
||
"offpeak_factor": 0.5,
|
||
"sale_discount": {"in": 0.5, "out": 0.8}, # 差异化(设计文档 §2.5)
|
||
},
|
||
"limits": {"rpm_per_key": 10, "day_req_cap": 200,
|
||
"concurrent_per_key": 2, "max_body_chars": 60000},
|
||
"semcache": {"enabled": True, "sim_threshold": 0.92,
|
||
"max_entries": 300000, "promote_frequency": 5},
|
||
},
|
||
# 语义分析器与三级分级(T-G0;结构见《实施方案_语义分析器与三级分级.md》§5)
|
||
"sense": {
|
||
"enabled": False, # D-G7 总开关:默认关
|
||
"mode": "collect", # collect | shadow | live(collect 攒满标签前禁 live)
|
||
"db_path": "data/sense.sqlite3",
|
||
"admin_key": "",
|
||
"embedder": {"base_url": "http://127.0.0.1:8902/v1",
|
||
"model": "bge-m3-Q4_K_M", "timeout_s": 5, "dim": 1024},
|
||
"features": {"t1_max_tokens": 512, "t1_max_turns": 2,
|
||
"intent_blacklist": ["重构", "脚手架", "迁移", "实现", "多文件", "项目"],
|
||
"code_t1_kinds": ["解释", "补全"]},
|
||
"policy": {"alpha": 0.05, "min_labels": 500,
|
||
"t2_prefer_local_when_idle": True},
|
||
"consumers": {"proxy": {"t1": "local-small", "t2": "budget", "t3": "premium"}},
|
||
},
|
||
}
|
||
|
||
|
||
class SettingsStore:
|
||
"""用户可调整设置(内存 + settings.json 持久化)。"""
|
||
|
||
def __init__(self, path: Optional[Path] = None):
|
||
self._path = Path(path) if path else _SETTINGS_PATH
|
||
self._data: Dict[str, Any] = {}
|
||
self.load()
|
||
|
||
# ---------- 持久化 ----------
|
||
def load(self) -> None:
|
||
if self._path.exists():
|
||
try:
|
||
self._data = json.loads(self._path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
self._data = {}
|
||
else:
|
||
self._data = {}
|
||
|
||
def save(self) -> None:
|
||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||
self._path.write_text(
|
||
json.dumps(self._data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
# ---------- 访问 ----------
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""返回 默认值 + 用户覆盖 合并后的完整设置。"""
|
||
merged: Dict[str, Any] = {}
|
||
for section, defaults in DEFAULTS.items():
|
||
ov = self._data.get(section, {})
|
||
merged[section] = {**defaults, **(ov if isinstance(ov, dict) else {})}
|
||
return merged
|
||
|
||
def get(self, section: str, key: str, default: Any = None) -> Any:
|
||
merged = self.to_dict()
|
||
return merged.get(section, {}).get(key, default)
|
||
|
||
def update(self, patch: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""应用部分更新(可只传改动的 section/key)。返回合并后的完整设置。"""
|
||
for section, values in patch.items():
|
||
if section not in DEFAULTS or not isinstance(values, dict):
|
||
continue
|
||
cur = self._data.setdefault(section, {})
|
||
for k, v in values.items():
|
||
if k in DEFAULTS[section]:
|
||
cur[k] = _coerce(v, DEFAULTS[section][k])
|
||
self.save()
|
||
return self.to_dict()
|
||
|
||
def reset(self) -> Dict[str, Any]:
|
||
"""恢复默认。"""
|
||
self._data = {}
|
||
self.save()
|
||
return self.to_dict()
|
||
|
||
|
||
def _coerce(value: Any, template: Any) -> Any:
|
||
"""按默认值的类型把输入转成一致类型(数值容错)。"""
|
||
if isinstance(template, bool):
|
||
return bool(value)
|
||
if isinstance(template, int):
|
||
try:
|
||
return int(float(value))
|
||
except (TypeError, ValueError):
|
||
return template
|
||
if isinstance(template, float):
|
||
try:
|
||
return float(value)
|
||
except (TypeError, ValueError):
|
||
return template
|
||
return value
|
||
|
||
|
||
def load_settings(path: Optional[Path] = None) -> SettingsStore:
|
||
return SettingsStore(path)
|