fix(conductor): restore standalone model selection
This commit is contained in:
1 parent
fb91ec22fa
commit
ffe97347df
3 files changed
+102
-4
No files matched your search
@@ -540,7 +540,12 @@
|
||||
messagesEl.innerHTML=''; (data.chat||[]).forEach(addMsg);
|
||||
renderLog(data.log||[]);
|
||||
llmEl.innerHTML=(data.llms||[]).map(x=>`<option value="${x[0]}">${esc(x[1])}</option>`).join(''); llmEl.value=data.llm; llmEl.disabled=!llmEl.options.length;
|
||||
} else if(data.type==='subagents'){ renderCards(data.items||[]); }
|
||||
} else if(data.type==='model_selected'){ llmEl.value=String(data.llm); }
|
||||
else if(data.type==='model_error'){
|
||||
if(data.llm!==undefined) llmEl.value=String(data.llm);
|
||||
showLimitToast(data.error||'Failed to select model');
|
||||
}
|
||||
else if(data.type==='subagents'){ renderCards(data.items||[]); }
|
||||
else if(data.type==='chat'){ addMsg(data.item); }
|
||||
else if(data.type==='chat_read'){ markAllRead(); }
|
||||
else if(data.type==='log'){ appendLog(data.item); }
|
||||
|
||||
+52
-3
@@ -1,5 +1,6 @@
|
||||
import os, sys, re, time, json, uuid, queue, asyncio, threading, argparse, base64, secrets
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
|
||||
@@ -8,6 +9,10 @@ from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
FRONTENDS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if FRONTENDS_DIR not in sys.path: sys.path.insert(0, FRONTENDS_DIR)
|
||||
from desktop_settings import DesktopSettingsError, update_settings
|
||||
|
||||
def _resolve_ga_root() -> str:
|
||||
"""Use the external core selected by the package-owned desktop bridge when valid."""
|
||||
value = (os.environ.get("GA_ROOT", "") or "").strip()
|
||||
@@ -67,15 +72,28 @@ HOST = args.host
|
||||
PORT = args.port
|
||||
|
||||
|
||||
SETTINGS_PATH = Path.home() / ".ga_desktop_settings.json"
|
||||
|
||||
|
||||
def _settings_doc() -> dict:
|
||||
try:
|
||||
from pathlib import Path
|
||||
doc = json.loads((Path.home() / ".ga_desktop_settings.json").read_text(encoding="utf-8"))
|
||||
doc = json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
|
||||
return doc if isinstance(doc, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _persist_conductor_llm_no(llm_no: int) -> None:
|
||||
def mutate(document: dict) -> None:
|
||||
section = document.get("conductor")
|
||||
if not isinstance(section, dict):
|
||||
section = {}
|
||||
document["conductor"] = section
|
||||
section["llmNo"] = llm_no
|
||||
|
||||
update_settings(SETTINGS_PATH, mutate)
|
||||
|
||||
|
||||
def _conductor_llm_no() -> Optional[int]:
|
||||
"""Read the model index bound to the conductor session.
|
||||
Falls back to the legacy desktop default ui.llmNo for existing installs."""
|
||||
@@ -180,6 +198,24 @@ def _apply_desktop_model(agent: "GenericAgent") -> dict:
|
||||
"fallbackReason": "no_models", "current": None}
|
||||
|
||||
|
||||
def _selected_conductor_llm_no(agent: "GenericAgent") -> int:
|
||||
configured = _conductor_llm_no()
|
||||
return configured if _usable_model(agent, configured) else agent.llm_no
|
||||
|
||||
|
||||
def _set_conductor_llm_no(agent: "GenericAgent", value: Any) -> int:
|
||||
"""Persist the standalone UI binding without mutating an in-flight client.
|
||||
|
||||
The Conductor loop applies this binding at its existing idle task boundary, so
|
||||
a selection made immediately before sending a message controls that next turn.
|
||||
"""
|
||||
no = _parse_model_no(value)
|
||||
if no is None or not _usable_model(agent, no):
|
||||
raise ValueError(f"llm index out of range or unavailable: {value}")
|
||||
_persist_conductor_llm_no(no)
|
||||
return no
|
||||
|
||||
|
||||
def _select_llm(agent: "GenericAgent", llm: Any) -> bool:
|
||||
if llm is None or str(llm).strip() == "": return False
|
||||
q = str(llm).strip()
|
||||
@@ -800,9 +836,22 @@ async def websocket(ws: WebSocket):
|
||||
await ws.send_json({"type": "hello", "subagents": pool.snapshot(), "chat": chat_messages,
|
||||
"log": conductor.log, "running": running,
|
||||
"model": conductor.model_snapshot(),
|
||||
"llms": conductor.agent.list_llms(), "llm": conductor.agent.llm_no})
|
||||
"llms": conductor.agent.list_llms(),
|
||||
"llm": _selected_conductor_llm_no(conductor.agent)})
|
||||
while True:
|
||||
data = await ws.receive_json()
|
||||
if "llm" in data:
|
||||
try:
|
||||
llm_no = _set_conductor_llm_no(conductor.agent, data["llm"])
|
||||
except (TypeError, ValueError, OSError, DesktopSettingsError) as error:
|
||||
await ws.send_json({
|
||||
"type": "model_error",
|
||||
"error": str(error),
|
||||
"llm": _selected_conductor_llm_no(conductor.agent),
|
||||
})
|
||||
else:
|
||||
await broadcast({"type": "model_selected", "llm": llm_no})
|
||||
continue
|
||||
msg = (data.get("msg") or "").strip()
|
||||
if not msg: continue
|
||||
add_chat(msg, role="user", files=data.get("files") or [], images=data.get("images") or [])
|
||||
|
||||
@@ -164,6 +164,50 @@ def test_explicit_numeric_worker_model_rejects_out_of_range():
|
||||
assert agent.next_llm_calls == []
|
||||
|
||||
|
||||
def test_standalone_model_selection_persists_without_mutating_live_client(tmp_path, monkeypatch):
|
||||
settings_path = tmp_path / "settings.json"
|
||||
settings_path.write_text('{"theme":"dark","ui":{"llmNo":0}}', encoding="utf-8")
|
||||
monkeypatch.setattr(conductor, "SETTINGS_PATH", settings_path)
|
||||
agent = FakeAgent(["zero", "one", "two"])
|
||||
|
||||
selected = conductor._set_conductor_llm_no(agent, 2)
|
||||
|
||||
assert selected == 2
|
||||
assert agent.llm_no == 0
|
||||
assert agent.next_llm_calls == []
|
||||
assert conductor._settings_doc() == {
|
||||
"theme": "dark",
|
||||
"ui": {"llmNo": 0},
|
||||
"conductor": {"llmNo": 2},
|
||||
}
|
||||
|
||||
|
||||
def test_standalone_model_selection_rejects_unavailable_model(tmp_path, monkeypatch):
|
||||
settings_path = tmp_path / "settings.json"
|
||||
settings_path.write_text('{"theme":"dark"}', encoding="utf-8")
|
||||
monkeypatch.setattr(conductor, "SETTINGS_PATH", settings_path)
|
||||
agent = FakeAgent(["zero", None])
|
||||
|
||||
with pytest.raises(ValueError, match="unavailable"):
|
||||
conductor._set_conductor_llm_no(agent, 1)
|
||||
|
||||
assert conductor._settings_doc() == {"theme": "dark"}
|
||||
|
||||
|
||||
def test_standalone_model_selection_applies_at_next_task_boundary(tmp_path, monkeypatch):
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(conductor, "SETTINGS_PATH", settings_path)
|
||||
agent = FakeAgent(["zero", "one", "two"])
|
||||
|
||||
conductor._set_conductor_llm_no(agent, 2)
|
||||
state = conductor._apply_desktop_model(agent)
|
||||
|
||||
assert agent.next_llm_calls == [2]
|
||||
assert state["configured"] == 2
|
||||
assert state["effective"] == 2
|
||||
assert state["fallbackReason"] is None
|
||||
|
||||
|
||||
def test_runtime_model_snapshot_is_broadcast_with_running_state(monkeypatch):
|
||||
instance = conductor.Conductor()
|
||||
payloads: list[dict] = []
|
||||
|
||||
Reference in new issue
Block a user