feat(v2): 模型设置(用户可调小模型/大模型/管线,/config + UI)
This commit is contained in:
+58
-3
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
<button data-tab="work">🔗 协作过程</button>
|
||||
<button data-tab="review">🧑💻 人工检验</button>
|
||||
<button data-tab="metrics">📊 指标</button>
|
||||
<button data-tab="settings">⚙️ 模型设置</button>
|
||||
</nav>
|
||||
<main>
|
||||
<!-- 对话 -->
|
||||
@@ -115,6 +119,53 @@ footer{padding:8px 20px;color:var(--muted);font-size:11px;border-top:1px solid v
|
||||
<div class="row-btns" style="margin-bottom:10px"><button class="small" id="refreshMetrics">刷新</button></div>
|
||||
<div id="metricsResult"></div>
|
||||
</section>
|
||||
<!-- 模型设置 -->
|
||||
<section class="panel" id="panel-settings">
|
||||
<div class="card">
|
||||
<h3>⚙️ 内置小模型(Worker)</h3>
|
||||
<div class="form-grid">
|
||||
<label>后端 backend
|
||||
<select id="set-worker-backend">
|
||||
<option value="llama_server">llama_server(内置本地模型)</option>
|
||||
<option value="openai">openai/api(Ollama / vLLM 等兼容端点)</option>
|
||||
<option value="mock">mock(演示,零运行时)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>模型路径/名称 <input id="set-worker-model" type="text" placeholder="models/xxx.gguf 或模型名"></label>
|
||||
<label>端点 base_url(openai 用) <input id="set-worker-baseurl" type="text" placeholder="http://127.0.0.1:11434/v1"></label>
|
||||
<label>端口(llama_server) <input id="set-worker-port" type="number"></label>
|
||||
<label>temperature <input id="set-worker-temperature" type="number" step="0.1"></label>
|
||||
<label>max_fix_attempts <input id="set-worker-fix" type="number"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🧠 大模型(Architect / API)</h3>
|
||||
<div class="form-grid">
|
||||
<label>model <input id="set-arch-model" type="text"></label>
|
||||
<label>base_url <input id="set-arch-baseurl" type="text"></label>
|
||||
<label>api_key_env <input id="set-arch-keyenv" type="text"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🔀 管线 pipeline</h3>
|
||||
<div class="form-grid">
|
||||
<label>fast_path(快路径直答) <input id="set-pipe-fast" type="checkbox"></label>
|
||||
<label>rounds_cap <input id="set-pipe-rounds" type="number"></label>
|
||||
<label>api_token_cap <input id="set-pipe-tokens" type="number"></label>
|
||||
<label>breach_policy
|
||||
<select id="set-pipe-breach">
|
||||
<option value="architect_do">architect_do(兜底代做)</option>
|
||||
<option value="local_only">local_only(本地降级)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-btns">
|
||||
<button class="primary" id="saveSettingsBtn">保存并生效</button>
|
||||
<button class="small" id="resetSettingsBtn">恢复默认</button>
|
||||
<span class="muted" id="settingsMsg"></span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>端云协同 LLM 协作系统 · v2 · 交流文本协议 · 北极星:token 下降 ≥80%</footer>
|
||||
<script>
|
||||
@@ -123,7 +174,7 @@ let lastRequestId=null;
|
||||
const esc=s=>{const d=document.createElement("div");d.textContent=s==null?"":String(s);return d.innerHTML;};
|
||||
function el(tag,cls,txt){const n=document.createElement(tag);if(cls)n.className=cls;if(txt!=null)n.textContent=txt;return n;}
|
||||
function setTab(t){document.querySelectorAll("nav button").forEach(b=>b.classList.toggle("active",b.dataset.tab===t));document.querySelectorAll(".panel").forEach(p=>p.classList.toggle("active",p.id==="panel-"+t));}
|
||||
document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>{setTab(b.dataset.tab);if(b.dataset.tab==="review")loadReview();if(b.dataset.tab==="metrics")loadMetrics();});
|
||||
document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>{setTab(b.dataset.tab);if(b.dataset.tab==="review")loadReview();if(b.dataset.tab==="metrics")loadMetrics();if(b.dataset.tab==="settings")loadSettings();});
|
||||
|
||||
// ---- 健康状态 ----
|
||||
async function health(){try{const r=await fetch("/health");const d=await r.json();$("healthPill").textContent="健康 · "+d.domains.length+" 领域";}catch(e){$("healthPill").textContent="离线";}}
|
||||
@@ -154,6 +205,12 @@ async function sendLegacy(){const q=$("query").value.trim();if(!q)return;const h
|
||||
const SAMPLES=["用 Python 实现快速排序,并分析时间与空间复杂度","求解方程 x^2 - 5x + 6 = 0","高血压患者日常饮食需要注意什么","解释一下深度学习中的注意力机制"];
|
||||
function sample(){$("query").value=SAMPLES[Math.floor(Math.random()*SAMPLES.length)];}
|
||||
|
||||
// ---- 模型设置 ----
|
||||
async function loadSettings(){try{const r=await fetch("/config");const s=await r.json();const w=s.worker||{};$("set-worker-backend").value=w.backend||"llama_server";$("set-worker-model").value=w.model||"";$("set-worker-baseurl").value=w.base_url||"";$("set-worker-port").value=w.port||8901;$("set-worker-temperature").value=w.temperature||0.3;$("set-worker-fix").value=w.max_fix_attempts||2;const a=s.architect||{};$("set-arch-model").value=a.model||"";$("set-arch-baseurl").value=a.base_url||"";$("set-arch-keyenv").value=a.api_key_env||"";const p=s.pipeline||{};$("set-pipe-fast").checked=!!p.fast_path;$("set-pipe-rounds").value=p.rounds_cap||6;$("set-pipe-tokens").value=p.api_token_cap||8000;$("set-pipe-breach").value=p.breach_policy||"architect_do";$("settingsMsg").textContent="";}catch(e){$("settingsMsg").textContent="读取失败:"+e.message;}}
|
||||
async function saveSettings(){const patch={worker:{backend:$("set-worker-backend").value,model:$("set-worker-model").value,base_url:$("set-worker-baseurl").value,port:parseInt($("set-worker-port").value||8901),temperature:parseFloat($("set-worker-temperature").value||0.3),max_fix_attempts:parseInt($("set-worker-fix").value||2)},architect:{model:$("set-arch-model").value,base_url:$("set-arch-baseurl").value,api_key_env:$("set-arch-keyenv").value},pipeline:{fast_path:$("set-pipe-fast").checked,rounds_cap:parseInt($("set-pipe-rounds").value||6),api_token_cap:parseInt($("set-pipe-tokens").value||8000),breach_policy:$("set-pipe-breach").value}};try{const r=await fetch("/config",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(patch)});const d=await r.json();$("settingsMsg").textContent=r.ok?"✅ 已保存并重建管线":"❌ "+(d.detail||"保存失败");}catch(e){$("settingsMsg").textContent="保存失败:"+e.message;}}
|
||||
async function resetSettings(){try{await fetch("/config/reset",{method:"POST"});$("settingsMsg").textContent="已恢复默认设置";loadSettings();}catch(e){$("settingsMsg").textContent="重置失败:"+e.message;}}
|
||||
$("saveSettingsBtn").onclick=saveSettings;
|
||||
$("resetSettingsBtn").onclick=resetSettings;
|
||||
$("sendBtn").onclick=sendChat;
|
||||
$("legacyBtn").onclick=sendLegacy;
|
||||
$("sampleBtn").onclick=sample;
|
||||
|
||||
Reference in New Issue
Block a user