This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/agent_team/invoker_multi.py
Claude e5a8949d0d
feat(ws1): non-Claude in-process invokers + HTTP API (WS1, code)
- invoker_multi.py: multi_invoke(prompt, *, model) dispatches to GPT-4.1
  (cross_reviewer), DeepSeek (fast_coder), or Gemini (scanner) in-process;
  bind_multi_invoker() wires the review seam; lazy models import
- review_loop_llm: swap default_plan_reviewer from make_run_py_invoker()
  to make_cross_reviewer_invoker() (in-process GPT-4.1); subprocess path
  retained as make_run_py_invoker() for opt-in use
- builders_llm: add make_fast_coder_invoker() in-process DeepSeek path;
  rename subprocess path to subprocess_build (opt-in fallback); default_build
  now delegates to make_fast_coder_invoker()
- api.py: FastAPI app with bearer-token auth (AGENT_TEAM_API_TOKEN env),
  POST /tasks, GET /tasks/{id}, POST /orchestrator/invoke; binds 127.0.0.1;
  code only — not started here
- run-team.py: bind_multi_invoker() called in serve() alongside
  bind_subscription_invoker()
- tests: 21 new WS1 tests + updated builders_llm tests (1065 total, all passing)
2026-06-23 01:19:59 +00:00

171 lines
6.6 KiB
Python

"""In-process invokers for non-Claude models (GPT-4.1, DeepSeek, Gemini) — WS1.
Mirrors the pattern of :mod:`agent_team.invoker` (which supplies Claude invokers
for the billing seam) but targets the **non-Claude** model seams:
* :func:`agent_team.nodes.review_loop.set_review_invoker` — GPT-4.1
(``cross_reviewer``)
* :func:`agent_team.nodes.builders_llm.default_build` is replaced in-process
by binding :func:`make_fast_coder_invoker` as the builder
Each factory lazily imports the root orchestrator's ``models`` module (never at
module load) so importing this file on a machine where ``models.py`` is absent
(CI, Mac dev, tests) does not raise :class:`ModuleNotFoundError`. The ``models``
module itself is kept out of the agent-team package graph; we reach it at runtime
through the orchestrator root's ``sys.path`` entry that ``run-team.py`` and the
graph module already bootstrap.
Model keys accepted by :func:`multi_invoke`:
``cross_reviewer``
GPT-4.1 via ``models.get_cross_reviewer()``. The §3.2 cross-family reviewer.
``fast_coder``
DeepSeek via ``models.get_fast_coder()``. The P3 mechanical-edit builder.
``scanner``
Gemini via ``models.get_scanner()``. The P3 security-scan verifier.
Security
--------
All model outputs returned by :func:`multi_invoke` are UNTRUSTED. Callers are
responsible for defensive parsing before acting on the result. This module
performs no parsing — it returns the raw model response text and leaves the
fail-safe discipline to the caller (review_loop_llm.parse_verdict,
builders_llm._extract_diff, etc.).
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
from typing import Any
__all__ = [
"MODELS",
"MultiInvoker",
"bind_multi_invoker",
"make_fast_coder_invoker",
"make_scanner_invoker",
"multi_invoke",
]
# The set of model keys this module can dispatch to.
MODELS: frozenset[str] = frozenset({"cross_reviewer", "fast_coder", "scanner"})
# Callable type: (prompt, **kw) -> str — same shape as ReviewInvoker and BuildCallable.
MultiInvoker = Callable[..., str]
def _ensure_orchestrator_on_path() -> None:
"""Add the orchestrator root to sys.path if absent.
``models.py`` lives at the orchestrator root (not inside agent-team). This
file is at ``<root>/agent-team/agent_team/invoker_multi.py``, so the root
is ``parents[2]``. Mirroring the bootstrap in ``run-team.py`` ensures the
deferred import in :func:`_load_model` succeeds when ``multi_invoke`` is
called in a process that has NOT already bootstrapped ``sys.path``.
"""
root = str(Path(__file__).resolve().parents[2])
if root not in sys.path:
sys.path.insert(0, root)
def _load_model(key: str) -> Any:
"""Lazily import the orchestrator ``models`` module and return the model.
``key`` must be one of :data:`MODELS`. Raises :class:`ValueError` on an
unknown key and :class:`RuntimeError` when the ``models`` module is
unavailable (the orchestrator is not on ``sys.path``).
Deferred import keeps this module importable everywhere the orchestrator
root is absent (CI, tests, Mac dev).
"""
_ensure_orchestrator_on_path()
try:
import models # noqa: PLC0415 - intentional deferred import
except ImportError as exc:
raise RuntimeError(
"Cannot import orchestrator 'models' module — ensure the orchestrator "
"root is on sys.path (run-team.py bootstraps this automatically). "
"Tests should mock multi_invoke rather than call it."
) from exc
factories = {
"cross_reviewer": models.get_cross_reviewer,
"fast_coder": models.get_fast_coder,
"scanner": models.get_scanner,
}
if key not in factories:
raise ValueError(
f"unknown model key {key!r}; must be one of: "
+ ", ".join(sorted(MODELS))
)
return factories[key]()
def multi_invoke(prompt: str, *, model: str, **kw: Any) -> str:
"""Invoke a non-Claude model in-process and return its response text.
``model`` must be one of :data:`MODELS`. The underlying model is loaded
lazily via :func:`_load_model` so the orchestrator's ``models`` module is
not imported until first call. Any call error propagates to the caller, which
is responsible for fail-safe handling (e.g. :func:`review_plan` catches all
exceptions and returns REQUEST_CHANGES).
The response text is returned raw (UNTRUSTED); callers must parse/validate
before acting on it.
"""
model_obj = _load_model(model)
result = model_obj.invoke(prompt)
text = getattr(result, "content", result)
return text if isinstance(text, str) else str(text)
def make_fast_coder_invoker() -> MultiInvoker:
"""Return an in-process callable that routes prompts to DeepSeek ``fast_coder``.
Mirrors :func:`agent_team.nodes.review_loop_llm.make_cross_reviewer_invoker`
for the builder seam. The callable matches :data:`~agent_team.nodes.builders_llm.BuildCallable`
``(instruction: str) -> str`` and is suitable for passing as the ``build``
kwarg to :func:`agent_team.nodes.builders_llm.build_candidate_diff`.
"""
def _invoke(instruction: str, **_kw: Any) -> str:
return multi_invoke(instruction, model="fast_coder")
return _invoke
def make_scanner_invoker() -> MultiInvoker:
"""Return an in-process callable that routes prompts to Gemini ``scanner``.
The callable is suitable for injection into the verifier's scan seam.
"""
def _invoke(prompt: str, **_kw: Any) -> str:
return multi_invoke(prompt, model="scanner")
return _invoke
def bind_multi_invoker() -> None:
"""Bind in-process non-Claude invokers into the review and builder seams.
Call once at startup (alongside :func:`agent_team.invoker.bind_subscription_invoker`)
to replace the subprocess-based defaults with in-process model calls:
* ``review_loop.set_review_invoker`` → in-process GPT-4.1 via
:func:`~agent_team.nodes.review_loop_llm.make_cross_reviewer_invoker`
* Builder default (``builders_llm.default_build``) is already replaced at
module load in this build; :func:`make_fast_coder_invoker` is the callable
for explicit injection into :func:`~agent_team.nodes.builders_llm.build_candidate_diff`
when needed.
Lazy-imported so importing ``invoker_multi`` has no global side effects and
tests that never call ``bind_multi_invoker`` remain model-free.
"""
from agent_team.nodes import review_loop # noqa: PLC0415
from agent_team.nodes.review_loop_llm import ( # noqa: PLC0415
make_cross_reviewer_invoker,
)
review_loop.set_review_invoker(make_cross_reviewer_invoker())