Files
projectAIpopular/gateway/settings.py
T
tzt c3efa70da2 feat(sense): T-G0 语义分析器骨架(包结构/DDL/灰度门控挂载)
- 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
2026-09-05 11:07:04 +08:00

163 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""可调整的运行设置(SettingsStore)—— 让用户自定义内置小模型 / 大模型 / 管线。
用户可在 Web 界面"模型设置"里调整并持久化到 config/settings.jsongitignore),
重启后保留。调整会触发 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 | livecollect 攒满标签前禁 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)