Download brain/server.py from ScoobyBaby1999/doomalay-e2e-docker: direct link, hf CLI and curl.
- Browser
- Download file 33.8 kB
-
https://huggingface.co/spaces/ScoobyBaby1999/doomalay-e2e-docker/resolve/main/brain/server.py
- Command line
-
hf download hf://spaces/ScoobyBaby1999/doomalay-e2e-docker/brain/server.py
-
curl -L -o server.py https://huggingface.co/spaces/ScoobyBaby1999/doomalay-e2e-docker/resolve/main/brain/server.py
33.8 kB
| """Doomalay brain β full-capability AI layer. | |
| Endpoints (all localhost-only, called by the Go engine): | |
| GET /health β liveness probe | |
| GET /models β provider catalog + sync status (dynamic) | |
| GET /templates β list the whole template library (v0.44: orchestrator | |
| stage-JSONs + superpowers user templates + the | |
| DEFAULT_TEMPLATES flows, merged + deduped) | |
| GET /templates/<id> β get one template's full definition | |
| POST /chat β run one agent turn, streams SSE events back | |
| POST /judge β run the multi-model judge panel (fan-out + merge) | |
| GET /panel β judge panel config | |
| POST /panel β update judge panel config | |
| The brain is the maximal-capability AI layer: Strands agent with the full | |
| tool suite (shell, files, editor, web_search, web_fetch, http_request, | |
| calculator, memory, delegate, agent_panel, git, glob, grep, think, journal, | |
| memorize, slug, retrieve, current_time, env), the multi-model judge panel | |
| (critique_service), the template library (14 multi-stage templates with | |
| sophisticated roles + dynamic variables), the .pied memory layer, effort | |
| detection, pricing, the scheduler. | |
| Persistence is the Go engine's job β the brain only emits events, it never | |
| writes to the DB. Provider keys arrive as X-Env-* headers (never from disk). | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import traceback | |
| from pathlib import Path | |
| import uvicorn | |
| from fastapi import FastAPI, Header, HTTPException, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import StreamingResponse | |
| # Brain modules (siblings). | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from agent import run_turn # noqa: E402 | |
| # v0.43 β stranded-coroutine watchdog (KEEP): every strands run_async loop | |
| # registers itself here; a global thread dumps any task still pending after | |
| # 50s so cross-loop deadlocks show their AWAITING stack (thread dumps alone | |
| # only ever showed "parked in asyncio.run" β this is how the sub-agent | |
| # deadlock was diagnosed). Zero overhead when turns behave. | |
| _ALL_LOOPS: "list[object]" = [] | |
| _LOOPS_LOCK = __import__("threading").Lock() | |
| def _install_loop_watchdog() -> None: # noqa: D401 β side-effecting patch | |
| try: | |
| import strands._async as _sa | |
| import asyncio as _aio | |
| import traceback as _tb | |
| import time as _time | |
| _orig = _sa.run_async | |
| def run_async_watched(async_func): | |
| def execute(): | |
| loop_holder: "dict[str, object]" = {} | |
| with _LOOPS_LOCK: | |
| _ALL_LOOPS.append(loop_holder) | |
| async def _main(): | |
| loop_holder["loop"] = _aio.get_running_loop() | |
| return await async_func() | |
| async def _dump_later(): | |
| await _aio.sleep(50) | |
| loop = loop_holder.get("loop") | |
| if loop is None: | |
| return | |
| pending = [t for t in _aio.all_tasks(loop) | |
| if t is not _aio.current_task()] | |
| if not pending: | |
| return | |
| print(f"\n=== WATCHDOG: loop {id(loop)} still has " | |
| f"{len(pending)} pending tasks after 50s ===", | |
| file=sys.stderr, flush=True) | |
| for t in pending: | |
| st = t.get_stack() | |
| if st: | |
| print(f"--- task {t.get_name()} ---", | |
| file=sys.stderr, flush=True) | |
| # st[0]=oldest/outermost; st[-1]=newest/innermost | |
| # await β printing FROM it walks f_back up the | |
| # whole chain (printing st[0] alone showed only | |
| # one frame, which is how this bug hid). | |
| _tb.print_stack(f=st[-1], file=sys.stderr) | |
| print("=== WATCHDOG END ===", file=sys.stderr, flush=True) | |
| async def _wrapped(): | |
| _aio.ensure_future(_dump_later()) | |
| return await _main() | |
| return _aio.run(_wrapped()) | |
| def _wrapped_sync(): | |
| token = _sa._RUN_ASYNC_BRIDGE.set(True) | |
| try: | |
| return execute() | |
| finally: | |
| _sa._RUN_ASYNC_BRIDGE.reset(token) | |
| import concurrent.futures as _cf | |
| import contextvars as _cv | |
| with _cf.ThreadPoolExecutor() as _ex: | |
| ctx = _cv.copy_context() | |
| fut = _ex.submit(ctx.run, _wrapped_sync) | |
| return fut.result() | |
| _sa.run_async = run_async_watched | |
| import strands.agent.agent as _agent_mod | |
| _agent_mod.run_async = run_async_watched | |
| except Exception: | |
| pass | |
| _install_loop_watchdog() | |
| from providers import ( # noqa: E402 | |
| load_provider_catalog, | |
| make_provider_registry, | |
| load_models_catalog, | |
| load_reasoning_catalog, | |
| load_benchmarks, | |
| ) | |
| app = FastAPI(title="Doomalay Brain", version="0.2.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # localhost only | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| CATALOG_PATH = Path(__file__).parent / "catalog" / "providers_catalog.json" | |
| TEMPLATES_PATH = Path(__file__).parent / "templates.py" | |
| PANEL_PATH = Path(__file__).parent / "catalog" / "panel.json" | |
| def health(): | |
| return {"status": "ok", "version": "0.2.0", "brain": True, "strands": _check_strands()} | |
| def _check_strands() -> bool: | |
| try: | |
| import strands # noqa: F401 | |
| return True | |
| except ImportError: | |
| return False | |
| def list_models(refresh: bool = False, request: Request = None): | |
| """Return the provider catalog + per-provider sync status. | |
| Uses the full providers.py: load_provider_catalog() for the catalog, | |
| make_provider_registry() for the live provider list (only providers | |
| with keys set are registered). | |
| """ | |
| # Inject provider keys from headers. | |
| if request is not None: | |
| for h_name, h_val in request.headers.items(): | |
| if h_name.lower().startswith("x-env-"): | |
| env_name = h_name[6:].upper() | |
| os.environ[env_name] = h_val | |
| catalog = load_provider_catalog() # list of dicts (from providers_catalog.json) | |
| models_catalog = load_models_catalog() # logical β (provider, model) mapping | |
| # Build the providers dict (keyed by provider name) for the PWA. | |
| providers_dict = {} | |
| for entry in catalog: | |
| name = entry.get("name", entry.get("provider", "unknown")) | |
| env_var = entry.get("env_var", "") | |
| # env_var can be a list β take the first for the PWA display. | |
| if isinstance(env_var, list): | |
| env_var = env_var[0] if env_var else "" | |
| providers_dict[name] = { | |
| "env_var": str(env_var), | |
| "base_url": entry.get("base_url", ""), | |
| "litellm_prefix": entry.get("litellm_prefix", name), | |
| "label": entry.get("label", name), | |
| "description": entry.get("description", ""), | |
| "signup_url": entry.get("signup_url", ""), | |
| "free_tier": entry.get("free_tier", False), | |
| "color": entry.get("color", "#888888"), | |
| } | |
| # Build the sync status (which providers have keys). | |
| sync_status = [] | |
| all_models = [] | |
| for name, cfg in providers_dict.items(): | |
| env_var = cfg["env_var"] | |
| # env_var can be a string or a list (the old catalog format allows multiple). | |
| if isinstance(env_var, list): | |
| has_key = any(os.environ.get(str(v)) for v in env_var) | |
| env_var_str = str(env_var[0]) if env_var else "" | |
| else: | |
| env_var_str = str(env_var) | |
| has_key = bool(os.environ.get(env_var_str)) | |
| model_count = 0 | |
| if has_key: | |
| # Count models from the models_catalog for this provider. | |
| for logical, mapping in models_catalog.items(): | |
| if isinstance(mapping, dict): | |
| candidates = mapping.get("candidates", []) | |
| if any(c.get("provider") == name for c in candidates): | |
| model_count += 1 | |
| # Add a model entry for the provider's default. | |
| all_models.append({ | |
| "id": f"{name}/auto", | |
| "provider": name, | |
| "label": "auto", | |
| }) | |
| sync_status.append({ | |
| "provider": name, "has_key": has_key, "model_count": model_count, | |
| "env_var": env_var_str, | |
| }) | |
| return { | |
| "providers": providers_dict, | |
| "models": all_models, | |
| "models_catalog": _to_jsonable(models_catalog), | |
| "syncStatus": sync_status, | |
| "totalModels": len(all_models), | |
| } | |
| # ββ Templates ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # | |
| # v0.44 TEMPLATE PILL (user spec: "change the deep research pill entirely to | |
| # a template pill where user can select templates and browse the library, | |
| # with the deep research being one of the default templates"): the index | |
| # grew beyond templates.py's DEFAULT_TEMPLATES. The library now merges | |
| # THREE sources, each tolerant (corrupt/missing β skip + warn, never 500): | |
| # | |
| # 1. DEFAULT_TEMPLATES (templates.py) β kind "flow" | |
| # 2. brain/orchestrator/templates/*.json (13) β kind "orchestrator" | |
| # (stage-JSON pipelines; descriptions come from the "//" comment keys) | |
| # 3. brain/superpowers_user_templates.json (5) β kind "user" | |
| # (markdown disciplines) | |
| # | |
| # Dedup rule (mirrors tools/dt_template.py index_templates): a flow/user | |
| # entry whose task_type/id matches an orchestrator stem IS the same | |
| # pipeline β listed once. User ids are hyphenated ("superpowers-plan") so | |
| # they never collide with the underscored orchestrator stems. | |
| # | |
| # Entry shape (one for all three kinds): | |
| # {id, name, description, task_type, kind, stage_count, tags, | |
| # stages?: [{name, role, instructions, fanout?}], markdown?: "..."} | |
| def _tpl_first_comment_line(data: dict) -> str: | |
| """Orchestrator JSONs describe themselves in "//", "//1", β¦ comment | |
| keys (dict order = file order) β first one's first line, else "".""" | |
| for key, val in data.items(): | |
| if key.startswith("//") and isinstance(val, str): | |
| line = val.strip().splitlines()[0].strip() if val.strip() else "" | |
| if line: | |
| return line | |
| return "" | |
| def _tpl_slug(name: str) -> str: | |
| """Name β id: lowercase alphanumerics + hyphens ("Superpowers Plan" β | |
| "superpowers-plan"). Hyphenated on purpose so user ids never collide | |
| with the underscored orchestrator file stems (dt_template's rule).""" | |
| import re as _re | |
| s = _re.sub(r"[^0-9A-Za-z]+", "-", str(name or "")).strip("-").lower() | |
| return s or "template" | |
| def _tpl_stage_shape(st: dict) -> dict: | |
| """One stage normalized to the index shape (name/role/instructions/ | |
| fanout β inputs + max_tokens stay in the source files, the index | |
| doesn't need them).""" | |
| out = { | |
| "name": str(st.get("name") or ""), | |
| "role": str(st.get("role") or ""), | |
| "instructions": str(st.get("instructions") or ""), | |
| } | |
| fo = st.get("fanout") | |
| if isinstance(fo, dict): | |
| out["fanout"] = { | |
| "over": str(fo.get("over") or ""), | |
| "max_parallel": fo.get("max_parallel", 1), | |
| } | |
| return out | |
| def _scan_orchestrator_templates(): | |
| """brain/orchestrator/templates/*.json β index entries (kind | |
| "orchestrator"). Pure DATA β the orchestrator engine is never imported | |
| (dt_template's hard rule, kept here). Returns (entries, warnings).""" | |
| root = Path(__file__).resolve().parent | |
| tdir = root / "orchestrator" / "templates" | |
| out, warnings = [], [] | |
| if not tdir.is_dir(): | |
| return out, warnings | |
| for path in sorted(tdir.glob("*.json")): | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8", errors="replace")) | |
| except Exception as exc: # corrupt JSON β skip + warn | |
| warnings.append(f"{path.name}: unreadable JSON ({type(exc).__name__}) β skipped") | |
| continue | |
| if not isinstance(data, dict): | |
| warnings.append(f"{path.name}: not a JSON object β skipped") | |
| continue | |
| stages = [s for s in data.get("stages") or [] if isinstance(s, dict)] | |
| if not stages: | |
| warnings.append(f"{path.name}: no stages[] β not an orchestrator template, skipped") | |
| continue | |
| task_type = str(data.get("task_type") or path.stem) | |
| desc = _tpl_first_comment_line(data) | |
| if not desc: | |
| desc = f"{task_type.replace('_', ' ')} pipeline with {len(stages)} stages" | |
| out.append({ | |
| "id": path.stem, | |
| "kind": "orchestrator", | |
| "name": str(data.get("name") or path.stem), | |
| "task_type": task_type, | |
| "description": desc, | |
| "stage_count": len(stages), | |
| "stages": [_tpl_stage_shape(st) for st in stages], | |
| "tags": [], | |
| }) | |
| return out, warnings | |
| def _scan_user_templates(): | |
| """brain/superpowers_user_templates.json β index entries (kind "user", | |
| markdown disciplines). Returns (entries, warnings).""" | |
| root = Path(__file__).resolve().parent | |
| upath = root / "superpowers_user_templates.json" | |
| out, warnings = [], [] | |
| if not upath.is_file(): | |
| return out, warnings | |
| try: | |
| data = json.loads(upath.read_text(encoding="utf-8", errors="replace")) | |
| except Exception as exc: | |
| warnings.append(f"{upath.name}: unreadable JSON ({type(exc).__name__}) β skipped") | |
| return out, warnings | |
| if not isinstance(data, list): | |
| warnings.append(f"{upath.name}: expected a list β skipped") | |
| return out, warnings | |
| for raw in data: | |
| if not isinstance(raw, dict) or not str(raw.get("name", "")).strip(): | |
| warnings.append(f"{upath.name}: entry without a name β skipped") | |
| continue | |
| name = str(raw["name"]).strip() | |
| stages = [s for s in raw.get("stages") or [] if isinstance(s, dict)] | |
| out.append({ | |
| "id": _tpl_slug(name), | |
| "kind": "user", | |
| "name": name, | |
| "task_type": str(raw.get("task_type") or _tpl_slug(name)), | |
| "description": str(raw.get("description") or raw.get("task") or name), | |
| "stage_count": len(stages), | |
| "stages": [_tpl_stage_shape(st) for st in stages], | |
| "markdown": str(raw.get("markdown") or ""), | |
| "tags": [str(t) for t in raw.get("tags") or [] if str(t).strip()], | |
| }) | |
| return out, warnings | |
| def _default_flow_entries(): | |
| """templates.py's DEFAULT_TEMPLATES normalized to the index shape | |
| (kind "flow"; id = task_type). Import is best-effort β a broken | |
| templates.py just means no flow entries (the JSON sources carry the | |
| same pipelines). Returns (entries, warnings) like its siblings.""" | |
| out = [] | |
| try: | |
| import templates as tl | |
| raw = getattr(tl, "DEFAULT_TEMPLATES", None) | |
| if not isinstance(raw, list): | |
| return out, [] | |
| for r in raw: | |
| if not isinstance(r, dict) or not str(r.get("name", "")).strip(): | |
| continue | |
| stages = [s for s in r.get("stages") or [] if isinstance(s, dict)] | |
| out.append({ | |
| "id": str(r.get("task_type") or _tpl_slug(r["name"])), | |
| "kind": "flow", | |
| "name": str(r["name"]), | |
| "task_type": str(r.get("task_type") or _tpl_slug(r["name"])), | |
| "description": str(r.get("description") or r.get("task") or r["name"]), | |
| "stage_count": len(stages), | |
| "stages": [_tpl_stage_shape(st) for st in stages], | |
| "markdown": str(r.get("markdown") or ""), | |
| "tags": [str(t) for t in r.get("tags") or [] if str(t).strip()], | |
| }) | |
| except Exception: | |
| out = [] | |
| return out, [] | |
| def _merged_template_index(): | |
| """The whole library view: orchestrator + user + flow, deduped. | |
| Returns (entries, warnings). Order: orchestrator, user, flow (stable; | |
| the frontend groups by kind/tags anyway).""" | |
| orch, warnings = _scan_orchestrator_templates() | |
| user, uwarn = _scan_user_templates() | |
| warnings += uwarn | |
| flow, _ = _default_flow_entries() | |
| # dt_template's dedup: a flow entry whose task_type matches an | |
| # orchestrator stem is the SAME pipeline β listed once. | |
| orch_stems = {e["id"] for e in orch} | |
| used_ids = set(orch_stems) | |
| entries = list(orch) | |
| for e in user: | |
| if e["id"] in used_ids: | |
| warnings.append(f"user template {e['id']}: duplicate id β skipped") | |
| continue | |
| used_ids.add(e["id"]) | |
| entries.append(e) | |
| for e in flow: | |
| if e["id"] in used_ids or e["task_type"] in orch_stems: | |
| continue # same pipeline, already listed from the JSON source | |
| used_ids.add(e["id"]) | |
| entries.append(e) | |
| return entries, warnings | |
| def _find_extra_template(template_id: str): | |
| """One template from the three-file sources (orchestrator/user/flow). | |
| EXACT id match first (so the hyphenated user ids stay distinct from | |
| the underscored orchestrator stems), then a loose alphanumeric match | |
| (the frontend and the model both type both shapes). Returns the entry | |
| dict or None.""" | |
| entries, _ = _merged_template_index() | |
| def _norm(s: str) -> str: | |
| return "".join(ch for ch in str(s).lower() if ch.isalnum()) | |
| want = str(template_id or "").strip() | |
| if not want: | |
| return None | |
| for e in entries: # exact first β ids are the contract | |
| if e["id"] == want: | |
| return e | |
| nwant = _norm(want) | |
| if not nwant: | |
| return None | |
| for e in entries: | |
| if _norm(e["id"]) == nwant or _norm(e.get("task_type") or "") == nwant: | |
| return e | |
| return None | |
| def list_templates(): | |
| """List the whole template library (v0.44: orchestrator stage-JSONs + | |
| superpowers user templates + the DEFAULT_TEMPLATES flows, merged and | |
| deduped; warnings ride along so a corrupt file is visible, not | |
| fatal).""" | |
| try: | |
| entries, warnings = _merged_template_index() | |
| out = { | |
| "templates": entries, | |
| "total": len(entries), | |
| } | |
| if warnings: | |
| out["warnings"] = warnings | |
| return out | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return {"templates": [], "error": str(e)} | |
| def get_template(template_id: str): | |
| """Get one template's full definition (v0.44: searched across ALL | |
| three sources first β orchestrator JSONs, user markdown templates, | |
| DEFAULT flows β then templates.py's DB-backed get_template for | |
| user-created/downloaded rows).""" | |
| try: | |
| hit = _find_extra_template(template_id) | |
| if hit is not None: | |
| return _to_jsonable(hit) | |
| import templates as tl | |
| if hasattr(tl, "get_template"): | |
| t = tl.get_template(template_id) | |
| if t: | |
| return _to_jsonable(t) | |
| # Fallback: search the registry. | |
| for reg_name in ["DEFAULT_TEMPLATES", "TEMPLATE_LIBRARY", "TEMPLATES"]: | |
| reg = getattr(tl, reg_name, None) | |
| if reg and isinstance(reg, dict) and template_id in reg: | |
| return _to_jsonable(reg[template_id]) | |
| if reg and isinstance(reg, list): | |
| for t in reg: | |
| if isinstance(t, dict) and t.get("id") == template_id: | |
| return _to_jsonable(t) | |
| raise HTTPException(status_code=404, detail=f"template {template_id} not found") | |
| except ImportError as e: | |
| raise HTTPException(status_code=500, detail=f"templates module not loaded: {e}") | |
| def _scan_templates(module): | |
| """Scan a module for template-like objects (fallback).""" | |
| out = [] | |
| for name in dir(module): | |
| obj = getattr(module, name) | |
| if isinstance(obj, dict) and ("id" in obj or "name" in obj or "stages" in obj): | |
| out.append(obj) | |
| elif isinstance(obj, list) and all(isinstance(x, dict) for x in obj): | |
| out.extend(obj) | |
| return out | |
| def _to_jsonable(obj): | |
| """Recursively convert an object to JSON-serializable primitives. | |
| Handles dataclasses, Pydantic models, objects with __dict__, etc. | |
| """ | |
| import dataclasses | |
| if obj is None or isinstance(obj, (str, int, float, bool)): | |
| return obj | |
| if isinstance(obj, dict): | |
| return {str(k): _to_jsonable(v) for k, v in obj.items()} | |
| if isinstance(obj, (list, tuple, set)): | |
| return [_to_jsonable(x) for x in obj] | |
| if dataclasses.is_dataclass(obj): | |
| return {k: _to_jsonable(v) for k, v in dataclasses.asdict(obj).items()} | |
| if hasattr(obj, "model_dump"): # Pydantic v2 | |
| return _to_jsonable(obj.model_dump()) | |
| if hasattr(obj, "dict"): # Pydantic v1 | |
| try: | |
| return _to_jsonable(obj.dict()) | |
| except Exception: | |
| pass | |
| if hasattr(obj, "__dict__"): | |
| return {k: _to_jsonable(v) for k, v in vars(obj).items() if not k.startswith("_")} | |
| return str(obj) | |
| # ββ Judge Panel ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_panel(): | |
| """Get the judge panel config.""" | |
| try: | |
| with open(PANEL_PATH) as f: | |
| return json.load(f) | |
| except Exception: | |
| return {"slots": [], "default_panel": []} | |
| def update_panel(config: dict): | |
| """Update the judge panel config.""" | |
| try: | |
| with open(PANEL_PATH, "w") as f: | |
| json.dump(config, f, indent=2) | |
| return {"ok": True} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def judge(request: Request): | |
| """Run the multi-model judge panel β fan out to N diverse models + merge. | |
| Body: { "input": "...", "template": "critique", "count": 3, "chat_session_id": "..." } | |
| Returns SSE stream of each judge's output + the final merged result. | |
| """ | |
| body = await request.json() | |
| user_input = body.get("input", "") | |
| template = body.get("template", "critique") | |
| count = body.get("count", 3) | |
| session_id = body.get("chat_session_id", "unknown") | |
| # Inject provider keys from headers. | |
| for h_name, h_val in request.headers.items(): | |
| if h_name.lower().startswith("x-env-"): | |
| env_name = h_name[6:].upper() | |
| os.environ[env_name] = h_val | |
| async def event_stream(): | |
| try: | |
| async for ev in _run_judge_panel(user_input, template, count, session_id): | |
| yield f"data: {json.dumps(ev)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| except Exception as e: | |
| traceback.print_exc() | |
| err_ev = {"type": "error", "error": "judge", "message": str(e)} | |
| yield f"data: {json.dumps(err_ev)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse( | |
| event_stream(), | |
| media_type="text/event-stream", | |
| headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, | |
| ) | |
| async def _run_judge_panel(user_input: str, template: str, count: int, session_id: str): | |
| """Fan out to N diverse models + merge the results. | |
| Uses the scheduler (ported from scheduler.py) to pick models, and merge.py | |
| to combine outputs. | |
| """ | |
| yield {"type": "status", "state": "running", "message": f"Spawning {count} judgesβ¦"} | |
| catalog = load_provider_catalog(CATALOG_PATH) | |
| available_providers = [] | |
| for name, cfg in catalog.items(): | |
| if os.environ.get(cfg["env_var"]): | |
| available_providers.append((name, cfg)) | |
| if not available_providers: | |
| yield {"type": "error", "error": "no_keys", "message": "No provider keys available for the judge panel."} | |
| yield {"type": "status", "state": "error"} | |
| return | |
| # Pick `count` diverse models (round-robin across providers). | |
| selected = [] | |
| for i in range(count): | |
| name, cfg = available_providers[i % len(available_providers)] | |
| # Pick the first model from this provider (or a default). | |
| model_id = f"{cfg['litellm_prefix']}/auto" | |
| selected.append((name, cfg, model_id)) | |
| # Run each judge in parallel. | |
| import asyncio | |
| import litellm | |
| async def run_one(idx: int, provider_name: str, cfg: dict, model_id: str): | |
| api_key = os.environ.get(cfg["env_var"], "") | |
| system_prompt = f"You are Judge {idx + 1} ({provider_name}). Review the following input critically and provide your assessment." | |
| try: | |
| response = await litellm.acompletion( | |
| model=model_id, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_input}, | |
| ], | |
| api_key=api_key, | |
| api_base=cfg["base_url"], | |
| stream=False, | |
| ) | |
| content = response.choices[0].message.content | |
| yield_event = { | |
| "type": "judge_result", | |
| "judge": idx + 1, | |
| "provider": provider_name, | |
| "model": model_id, | |
| "content": content, | |
| } | |
| return yield_event | |
| except Exception as e: | |
| return { | |
| "type": "judge_error", | |
| "judge": idx + 1, | |
| "provider": provider_name, | |
| "error": str(e), | |
| } | |
| tasks = [run_one(i, name, cfg, mid) for i, (name, cfg, mid) in enumerate(selected)] | |
| results = await asyncio.gather(*tasks) | |
| for ev in results: | |
| yield ev | |
| # Merge (best-effort β uses merge.py if available). | |
| try: | |
| from merge import merge_outputs | |
| outputs = [r.get("content", "") for r in results if r.get("type") == "judge_result"] | |
| if outputs: | |
| merged = merge_outputs(outputs, template=template) | |
| yield {"type": "judge_merged", "content": merged} | |
| except Exception: | |
| # Fallback: concatenate. | |
| outputs = [r.get("content", "") for r in results if r.get("type") == "judge_result"] | |
| if outputs: | |
| merged = "\n\n---\n\n".join(outputs) | |
| yield {"type": "judge_merged", "content": merged} | |
| yield {"type": "status", "state": "idle"} | |
| # ββ Chat (the main endpoint) βββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def chat(request: Request): | |
| """Run one agent turn. Streams SSE events back. | |
| The Go engine reads the SSE stream, persists each event to chat_events | |
| (V0 fix: backend-writes-events-as-it-emits), and forwards each event to | |
| the PWA over WebSocket. | |
| Body: | |
| { "session_id", "message", "model", "provider", "effort", | |
| "mode", "system_prompt", "web_search", "deep_research", | |
| "workspace", "history", | |
| "template_id", "template_brief" # v0.44: informational β the | |
| # METHOD TEMPLATE block is already prepended to system_prompt | |
| # by the engine's handleTurn. } | |
| Provider keys arrive as X-Env-<ENV_VAR> headers. | |
| """ | |
| body = await request.json() | |
| session_id = body.get("session_id", "unknown") | |
| message = body.get("message", "") | |
| model = body.get("model", "") | |
| provider = body.get("provider", "") | |
| # Inject provider keys from headers. | |
| for h_name, h_val in request.headers.items(): | |
| if h_name.lower().startswith("x-env-"): | |
| env_name = h_name[6:].upper() | |
| os.environ[env_name] = h_val | |
| if not message: | |
| raise HTTPException(status_code=400, detail="message is required") | |
| if not model: | |
| raise HTTPException(status_code=400, detail="model is required") | |
| # Resolve the model via the provider registry. | |
| # model can be "openrouter/auto" or a logical name like "glm-5.2". | |
| # v0.38: the registry resolves env_var + api_key from the X-Env-* headers | |
| # (make_provider_registry reads os.environ AFTER the header injection | |
| # above). litellm needs the provider PREFIX STRIPPED (the engine sends | |
| # "nvidia/z-ai/glm-5.3-flash"; NVIDIA's own model id is "z-ai/glm-5.3-flash") | |
| # and the openai-compatible client is forced in agent.py via base_url. | |
| registry = make_provider_registry() | |
| litellm_model = model | |
| base_url = "" | |
| env_var = "" | |
| # If the model has a provider prefix (e.g. "openrouter/auto"), find that provider. | |
| if "/" in model: | |
| prov_name, model_name = model.split("/", 1) | |
| for p in registry: | |
| if p.name == prov_name: | |
| if model_name == "auto": | |
| # v0.44 QA FIX (live redteam): the engine resolves | |
| # "<provider>/auto" to a concrete model BEFORE the turn | |
| # reaches us β but a stale session (or a direct brain | |
| # caller) can still land here, and p.models was an | |
| # EMPTY tuple for nvidia (stale providers_catalog.json) | |
| # β IndexError β 500 "provider error". Fall back to the | |
| # known-good quick models (the engine's FALLBACK_MODELS | |
| # philosophy β resilience, not discovery; the live | |
| # catalog remains the primary mechanism) and fail with | |
| # an actionable 400 when even that's impossible. | |
| AUTO_MODEL_FALLBACK = { | |
| "nvidia": "z-ai/glm-5.3-flash", | |
| "privatemodeai": "kimi-k2.6", | |
| "opencode": "big-pickle", | |
| "openrouter": "auto", # OpenRouter natively routes auto | |
| } | |
| if p.models: | |
| litellm_model = p.models[0] | |
| elif prov_name in AUTO_MODEL_FALLBACK: | |
| litellm_model = AUTO_MODEL_FALLBACK[prov_name] | |
| else: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=( | |
| f"model 'auto' could not be resolved for " | |
| f"{prov_name} (provider catalog is empty β " | |
| "pick a specific model in the model browser)" | |
| ), | |
| ) | |
| else: | |
| litellm_model = model_name | |
| base_url = p.url | |
| env_var = p.env_var if hasattr(p, "env_var") else "" | |
| break | |
| else: | |
| # Logical model name β look it up in the models catalog. | |
| models_catalog = load_models_catalog() | |
| if model in models_catalog: | |
| entry = models_catalog[model] | |
| candidates = entry.get("candidates", []) if isinstance(entry, dict) else [] | |
| for c in candidates: | |
| prov_name = c.get("provider") | |
| for p in registry: | |
| if p.name == prov_name: | |
| litellm_model = c.get("model", model) | |
| base_url = p.url | |
| env_var = p.env_var | |
| break | |
| if env_var: | |
| break | |
| if not env_var: | |
| raise HTTPException(status_code=400, detail=f"could not resolve model {model} (provider {provider})") | |
| if not os.environ.get(env_var): | |
| raise HTTPException( | |
| status_code=401, | |
| detail=f"no API key set for {env_var} (provider {provider})", | |
| ) | |
| async def event_stream(): | |
| try: | |
| async for ev in run_turn( | |
| session_id=session_id, | |
| message=message, | |
| model=litellm_model, | |
| base_url=base_url, | |
| env_var=env_var, | |
| system_prompt=body.get("system_prompt", ""), | |
| effort=body.get("effort", "med"), | |
| workspace=body.get("workspace", ""), | |
| web_search=body.get("web_search", True), # v0.45 ITEM 2: default-on | |
| deep_research=body.get("deep_research", False), | |
| mode=body.get("mode", "auto"), | |
| history=body.get("history", []), | |
| workspaces=body.get("workspaces", []), | |
| ): | |
| yield f"data: {json.dumps(ev)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| except Exception as e: | |
| traceback.print_exc() | |
| err_ev = {"type": "error", "error": "brain", "message": str(e)} | |
| yield f"data: {json.dumps(err_ev)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse( | |
| event_stream(), | |
| media_type="text/event-stream", | |
| headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, | |
| ) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--port", type=int, default=9090) | |
| parser.add_argument("--host", default="127.0.0.1") | |
| args = parser.parse_args() | |
| print(f"doomalay brain on http://{args.host}:{args.port}", flush=True) | |
| uvicorn.run(app, host=args.host, port=args.port, log_level="warning") | |
| if __name__ == "__main__": | |
| main() | |