feat(v2): 模型设置(用户可调小模型/大模型/管线,/config + UI)
This commit is contained in:
@@ -12,6 +12,7 @@ htmlcov/
|
|||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
api_keys*.json
|
api_keys*.json
|
||||||
|
config/settings.json
|
||||||
|
|
||||||
# Models / data / runtime
|
# Models / data / runtime
|
||||||
models/
|
models/
|
||||||
|
|||||||
+58
-3
@@ -39,6 +39,7 @@ _router: Optional[Router] = None
|
|||||||
_pipeline: Optional["CollaborativePipeline"] = None
|
_pipeline: Optional["CollaborativePipeline"] = None
|
||||||
_v2stats = V2Stats() if _V2_OK else None
|
_v2stats = V2Stats() if _V2_OK else None
|
||||||
_review = None
|
_review = None
|
||||||
|
_settings = None
|
||||||
|
|
||||||
|
|
||||||
def get_router() -> Router:
|
def get_router() -> Router:
|
||||||
@@ -56,8 +57,23 @@ def get_review() -> "ReviewQueue":
|
|||||||
return _review
|
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):
|
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 会走
|
worker_cfg_override 可注入(测试/演示用 mock)。无 API key 时 /chat 会走
|
||||||
本地降级路径(不崩溃)。
|
本地降级路径(不崩溃)。
|
||||||
@@ -65,13 +81,27 @@ def build_v2_pipeline(worker_cfg_override: Optional[dict] = None):
|
|||||||
global _pipeline
|
global _pipeline
|
||||||
if _pipeline is None:
|
if _pipeline is None:
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
s = settings_store().to_dict() if _V2_OK else {}
|
||||||
kb = KnowledgeBase()
|
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 = dict(cfg.get("worker", {}))
|
||||||
|
wcfg.update(s.get("worker", {}))
|
||||||
if worker_cfg_override:
|
if worker_cfg_override:
|
||||||
wcfg.update(worker_cfg_override)
|
wcfg.update(worker_cfg_override)
|
||||||
worker = build_worker(wcfg, kb=kb)
|
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
|
return _pipeline
|
||||||
|
|
||||||
|
|
||||||
@@ -221,6 +251,31 @@ try:
|
|||||||
raise HTTPException(status_code=404, detail=f"审核记录不存在或已审核: {review_id}")
|
raise HTTPException(status_code=404, detail=f"审核记录不存在或已审核: {review_id}")
|
||||||
return {"ok": True, "review_id": review_id, "verdict": verdict}
|
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 ----------------
|
# ---------------- metrics ----------------
|
||||||
@app.get("/metrics", tags=["system"])
|
@app.get("/metrics", tags=["system"])
|
||||||
async def metrics():
|
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)}
|
.muted{color:var(--muted)}
|
||||||
.history{max-height:220px;overflow:auto;margin-top:8px}
|
.history{max-height:220px;overflow:auto;margin-top:8px}
|
||||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
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,td{text-align:left;padding:8px;border-bottom:1px solid var(--line)}
|
||||||
th{color:var(--muted);font-weight:600}
|
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)}
|
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="work">🔗 协作过程</button>
|
||||||
<button data-tab="review">🧑💻 人工检验</button>
|
<button data-tab="review">🧑💻 人工检验</button>
|
||||||
<button data-tab="metrics">📊 指标</button>
|
<button data-tab="metrics">📊 指标</button>
|
||||||
|
<button data-tab="settings">⚙️ 模型设置</button>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<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 class="row-btns" style="margin-bottom:10px"><button class="small" id="refreshMetrics">刷新</button></div>
|
||||||
<div id="metricsResult"></div>
|
<div id="metricsResult"></div>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
<footer>端云协同 LLM 协作系统 · v2 · 交流文本协议 · 北极星:token 下降 ≥80%</footer>
|
<footer>端云协同 LLM 协作系统 · v2 · 交流文本协议 · 北极星:token 下降 ≥80%</footer>
|
||||||
<script>
|
<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;};
|
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 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));}
|
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="离线";}}
|
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","高血压患者日常饮食需要注意什么","解释一下深度学习中的注意力机制"];
|
const SAMPLES=["用 Python 实现快速排序,并分析时间与空间复杂度","求解方程 x^2 - 5x + 6 = 0","高血压患者日常饮食需要注意什么","解释一下深度学习中的注意力机制"];
|
||||||
function sample(){$("query").value=SAMPLES[Math.floor(Math.random()*SAMPLES.length)];}
|
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;
|
$("sendBtn").onclick=sendChat;
|
||||||
$("legacyBtn").onclick=sendLegacy;
|
$("legacyBtn").onclick=sendLegacy;
|
||||||
$("sampleBtn").onclick=sample;
|
$("sampleBtn").onclick=sample;
|
||||||
|
|||||||
+15
-8
@@ -155,11 +155,15 @@ class WorkerLoop:
|
|||||||
def build_worker(cfg: Dict[str, Any], kb: Any = None,
|
def build_worker(cfg: Dict[str, Any], kb: Any = None,
|
||||||
generate: Optional[Callable[[str], Awaitable[str]]] = None) -> WorkerLoop:
|
generate: Optional[Callable[[str], Awaitable[str]]] = None) -> WorkerLoop:
|
||||||
"""cfg 为 config.worker 段。generate 缺省时按 backend 选择:
|
"""cfg 为 config.worker 段。generate 缺省时按 backend 选择:
|
||||||
mock(零运行时演示)| llama_server(真实本地模型,惰性连接)。"""
|
mock(零运行时演示)| openai/api(任意 OpenAI 兼容端点,如 Ollama/vLLM)|
|
||||||
|
llama_server(内置本地 llama-server)。"""
|
||||||
backend = cfg.get("backend", "llama_server")
|
backend = cfg.get("backend", "llama_server")
|
||||||
if generate is None:
|
if generate is None:
|
||||||
if backend == "mock":
|
if backend == "mock":
|
||||||
generate = _mock_generate()
|
generate = _mock_generate()
|
||||||
|
elif backend in ("openai", "api"):
|
||||||
|
generate = _make_llama_generate(
|
||||||
|
cfg, default_base_url=cfg.get("base_url") or "http://127.0.0.1:11434/v1")
|
||||||
else:
|
else:
|
||||||
generate = _make_llama_generate(cfg)
|
generate = _make_llama_generate(cfg)
|
||||||
verifier = Verifier(code_timeout_s=float(cfg.get("code_timeout_s", 10)))
|
verifier = Verifier(code_timeout_s=float(cfg.get("code_timeout_s", 10)))
|
||||||
@@ -182,10 +186,13 @@ def _mock_generate() -> Callable[[str], Awaitable[str]]:
|
|||||||
return _gen
|
return _gen
|
||||||
|
|
||||||
|
|
||||||
def _make_llama_generate(cfg: Dict[str, Any]) -> Callable[[str], Awaitable[str]]:
|
def _make_llama_generate(cfg: Dict[str, Any],
|
||||||
"""返回调用本地 llama-server(OpenAI 兼容 /v1/chat/completions)的生成器。"""
|
default_base_url: Optional[str] = None) -> Callable[[str], Awaitable[str]]:
|
||||||
base_url = cfg.get("base_url", f"http://127.0.0.1:{cfg.get('port', 8901)}/v1")
|
"""返回调用本地 OpenAI 兼容端点(llama-server / Ollama / vLLM)的生成器。"""
|
||||||
model = cfg.get("model", "local")
|
if default_base_url is None:
|
||||||
|
default_base_url = f"http://127.0.0.1:{cfg.get('port', 8901)}/v1"
|
||||||
|
base_url = cfg.get("base_url") or default_base_url
|
||||||
|
model = cfg.get("model") or "local"
|
||||||
temperature = float(cfg.get("temperature", 0.3))
|
temperature = float(cfg.get("temperature", 0.3))
|
||||||
timeout_s = float(cfg.get("per_step_timeout_s", 300))
|
timeout_s = float(cfg.get("per_step_timeout_s", 300))
|
||||||
|
|
||||||
@@ -201,8 +208,8 @@ def _make_llama_generate(cfg: Dict[str, Any]) -> Callable[[str], Awaitable[str]]
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()["choices"][0]["message"]["content"]
|
return resp.json()["choices"][0]["message"]["content"]
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
# 连不上本地模型 -> 优雅降级(不抛 500),提示用户准备运行时
|
# 连不上本地模型 -> 优雅降级(不抛 500),提示用户检查模型端点
|
||||||
return ("(本地降级)无法连接本地 llama-server,未能生成该步骤内容。"
|
return ("(本地降级)无法连接本地模型端点,未能生成该步骤内容。"
|
||||||
f"请先运行 scripts/setup_runtime.py 并启动模型。错误:{type(e).__name__}")
|
f"请检查模型后端配置或启动服务。错误:{type(e).__name__}")
|
||||||
|
|
||||||
return _gen
|
return _gen
|
||||||
|
|||||||
@@ -66,6 +66,24 @@ def test_metrics(client):
|
|||||||
assert "review" in data
|
assert "review" in data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_get_put_reset(client):
|
||||||
|
# GET 默认
|
||||||
|
r = client.get("/config")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["worker"]["backend"] in ("llama_server", "openai", "mock")
|
||||||
|
# PUT 更新 worker
|
||||||
|
r2 = client.put("/config", json={"worker": {"backend": "mock", "temperature": 0.5}})
|
||||||
|
assert r2.status_code == 200
|
||||||
|
assert r2.json()["worker"]["temperature"] == 0.5
|
||||||
|
# 重置
|
||||||
|
r3 = client.post("/config/reset")
|
||||||
|
assert r3.status_code == 200
|
||||||
|
assert r3.json()["worker"]["backend"] == "llama_server"
|
||||||
|
|
||||||
|
|
||||||
def test_workspace_not_found(client):
|
def test_workspace_not_found(client):
|
||||||
resp = client.get("/runs/nonexistent/workspace")
|
resp = client.get("/runs/nonexistent/workspace")
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""SettingsStore 单测(封闭:临时路径)。"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from gateway.settings import DEFAULTS, SettingsStore, _coerce
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults(tmp_path):
|
||||||
|
s = SettingsStore(path=tmp_path / "s.json")
|
||||||
|
d = s.to_dict()
|
||||||
|
assert d["worker"]["backend"] == "llama_server"
|
||||||
|
assert "pipeline" in d and "architect" in d
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_and_coerce(tmp_path):
|
||||||
|
s = SettingsStore(path=tmp_path / "s.json")
|
||||||
|
s.update({"worker": {"backend": "openai", "temperature": "0.4", "max_fix_attempts": "3"}})
|
||||||
|
d = s.to_dict()
|
||||||
|
assert d["worker"]["backend"] == "openai"
|
||||||
|
assert d["worker"]["temperature"] == 0.4 # 字符串转 float
|
||||||
|
assert d["worker"]["max_fix_attempts"] == 3 # 字符串转 int
|
||||||
|
|
||||||
|
|
||||||
|
def test_ignores_unknown_keys(tmp_path):
|
||||||
|
s = SettingsStore(path=tmp_path / "s.json")
|
||||||
|
s.update({"worker": {"not_a_key": 1}, "unknown_section": {"x": 1}})
|
||||||
|
d = s.to_dict()
|
||||||
|
assert "not_a_key" not in d["worker"]
|
||||||
|
assert "unknown_section" not in d
|
||||||
|
|
||||||
|
|
||||||
|
def test_persistence_roundtrip(tmp_path):
|
||||||
|
p = tmp_path / "s.json"
|
||||||
|
s = SettingsStore(path=p)
|
||||||
|
s.update({"worker": {"temperature": 0.5}})
|
||||||
|
s2 = SettingsStore(path=p) # 重新加载
|
||||||
|
assert s2.to_dict()["worker"]["temperature"] == 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset(tmp_path):
|
||||||
|
s = SettingsStore(path=tmp_path / "s.json")
|
||||||
|
s.update({"worker": {"backend": "mock"}})
|
||||||
|
d = s.reset()
|
||||||
|
assert d["worker"]["backend"] == "llama_server"
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce():
|
||||||
|
assert _coerce("true", True) is True
|
||||||
|
assert _coerce("42", 0) == 42
|
||||||
|
assert _coerce("bad", 0) == 0
|
||||||
|
assert _coerce(1.5, 0.0) == 1.5
|
||||||
Reference in New Issue
Block a user