diff --git a/.gitignore b/.gitignore index 928aa65..705b86e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ htmlcov/ .env *.env api_keys*.json +config/settings.json # Models / data / runtime models/ diff --git a/gateway/api.py b/gateway/api.py index ee7510b..35d6e51 100644 --- a/gateway/api.py +++ b/gateway/api.py @@ -39,6 +39,7 @@ _router: Optional[Router] = None _pipeline: Optional["CollaborativePipeline"] = None _v2stats = V2Stats() if _V2_OK else None _review = None +_settings = None def get_router() -> Router: @@ -56,8 +57,23 @@ def get_review() -> "ReviewQueue": return _review +def settings_store(): + """用户可调整设置(懒加载单例)。""" + global _settings + if _settings is None: + from gateway.settings import load_settings + _settings = load_settings() + return _settings + + +def rebuild_pipeline() -> None: + """清除管线单例,下次调用重建(配置改动后生效)。""" + global _pipeline + _pipeline = None + + def build_v2_pipeline(worker_cfg_override: Optional[dict] = None): - """从配置构建 v2 协作管线(architect + worker + pipeline)。 + """从配置 + 用户设置构建 v2 协作管线(architect + worker + pipeline)。 worker_cfg_override 可注入(测试/演示用 mock)。无 API key 时 /chat 会走 本地降级路径(不崩溃)。 @@ -65,13 +81,27 @@ def build_v2_pipeline(worker_cfg_override: Optional[dict] = None): global _pipeline if _pipeline is None: cfg = load_config() + s = settings_store().to_dict() if _V2_OK else {} kb = KnowledgeBase() - architect = build_architect(cfg.get("architect", {})) + + # architect(合并用户设置) + acfg = dict(cfg.get("architect", {})) + acfg.update(s.get("architect", {})) + architect = build_architect(acfg) + + # worker(合并用户设置;backend 可 mock/openai/llama_server) wcfg = dict(cfg.get("worker", {})) + wcfg.update(s.get("worker", {})) if worker_cfg_override: wcfg.update(worker_cfg_override) worker = build_worker(wcfg, kb=kb) - _pipeline = build_pipeline(cfg, architect, worker) + + # pipeline(合并用户设置) + cfg2 = dict(cfg) + pcfg = dict(cfg.get("pipeline", {})) + pcfg.update(s.get("pipeline", {})) + cfg2["pipeline"] = pcfg + _pipeline = build_pipeline(cfg2, architect, worker) return _pipeline @@ -221,6 +251,31 @@ try: raise HTTPException(status_code=404, detail=f"审核记录不存在或已审核: {review_id}") return {"ok": True, "review_id": review_id, "verdict": verdict} + # ---------------- 模型设置(用户可调整) ---------------- + @app.get("/config", tags=["settings"]) + async def get_config(): + """读取当前可调整设置(小模型 / 大模型 / 管线)。""" + return settings_store().to_dict() + + @app.put("/config", tags=["settings"]) + async def put_config(patch: dict): + """部分更新设置并重建管线。示例: + {"worker": {"backend": "openai", "base_url": "http://127.0.0.1:11434/v1", "temperature": 0.4}} + """ + try: + merged = settings_store().update(patch) + except Exception as e: + raise HTTPException(status_code=400, detail=f"设置非法: {e}") + rebuild_pipeline() + return merged + + @app.post("/config/reset", tags=["settings"]) + async def reset_config(): + """恢复默认设置并重建管线。""" + merged = settings_store().reset() + rebuild_pipeline() + return merged + # ---------------- metrics ---------------- @app.get("/metrics", tags=["system"]) async def metrics(): diff --git a/gateway/settings.py b/gateway/settings.py new file mode 100644 index 0000000..9600af8 --- /dev/null +++ b/gateway/settings.py @@ -0,0 +1,114 @@ +"""可调整的运行设置(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, + "code_timeout_s": 10, + }, + "architect": { + "model": "deepseek-chat", + "base_url": "https://api.deepseek.com/v1", + "api_key_env": "DEEPSEEK_API_KEY", + }, + "pipeline": { + "fast_path": True, + "rounds_cap": 6, + "api_token_cap": 8000, + "breach_policy": "architect_do", + }, +} + + +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) diff --git a/gateway/static/index.html b/gateway/static/index.html index 33d8924..df4000d 100644 --- a/gateway/static/index.html +++ b/gateway/static/index.html @@ -58,6 +58,9 @@ input,select{background:var(--panel2);border:1px solid var(--line);color:var(--t .muted{color:var(--muted)} .history{max-height:220px;overflow:auto;margin-top:8px} table{width:100%;border-collapse:collapse;font-size:13px} +.form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px} +.form-grid label{display:flex;flex-direction:column;gap:5px;font-size:12px;color:var(--muted)} +.form-grid input,.form-grid select{width:100%} th,td{text-align:left;padding:8px;border-bottom:1px solid var(--line)} th{color:var(--muted);font-weight:600} footer{padding:8px 20px;color:var(--muted);font-size:11px;border-top:1px solid var(--line);background:var(--panel)} @@ -74,6 +77,7 @@ footer{padding:8px 20px;color:var(--muted);font-size:11px;border-top:1px solid v 🔗 协作过程 🧑💻 人工检验 📊 指标 + ⚙️ 模型设置 @@ -115,6 +119,53 @@ footer{padding:8px 20px;color:var(--muted);font-size:11px;border-top:1px solid v 刷新 + + + + ⚙️ 内置小模型(Worker) + + 后端 backend + + llama_server(内置本地模型) + openai/api(Ollama / vLLM 等兼容端点) + mock(演示,零运行时) + + + 模型路径/名称 + 端点 base_url(openai 用) + 端口(llama_server) + temperature + max_fix_attempts + + + + 🧠 大模型(Architect / API) + + model + base_url + api_key_env + + + + 🔀 管线 pipeline + + fast_path(快路径直答) + rounds_cap + api_token_cap + breach_policy + + architect_do(兜底代做) + local_only(本地降级) + + + + + + 保存并生效 + 恢复默认 + + +