feat(core,frontends,agents): agent kinds, frontend routing, anthropic on conversations, vault mirror

This commit is contained in:
hh
2026-08-28 03:50:33 +02:00
parent e3074c266a
commit 827fa0977b
17 changed files with 1009 additions and 292 deletions
+52 -15
View File
@@ -34,7 +34,7 @@ from beaver_gateway.frontends.base import Frontend
from beaver_gateway.storage.models import Usage
if TYPE_CHECKING:
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Mapping
from beaver_gateway.core.conversations import Conversations
from beaver_gateway.frontends.base import GatewayRuntime
@@ -48,18 +48,26 @@ SCOPE = "api"
class ApiFrontend(Frontend):
name = "api"
kinds = ("master", "branch", "deep", "job")
def __init__(
self,
*,
host: str = "0.0.0.0", # noqa: S104
port: int = 8004,
public_base_url: str | None = None,
default_agents: Mapping[str, str] | None = None,
) -> None:
self.host = host
self.port = port
self.public_base_url = public_base_url.rstrip("/") if public_base_url else None
self.default_agents = dict(default_agents or {})
self._app: FastAPI | None = None
def agent_for(self, kind: str) -> str | None:
return self.default_agents.get(kind)
def configure(self, runtime: GatewayRuntime) -> None:
if runtime.conversations is None or runtime.bus is None:
msg = "ApiFrontend needs runtime.conversations and runtime.bus"
@@ -130,6 +138,30 @@ def _build_app(runtime: GatewayRuntime) -> FastAPI: # noqa: PLR0915
async def healthz() -> dict[str, str]:
return {"status": "ok"}
@app.get("/api/agents")
async def list_agents(request: Request) -> dict[str, Any]:
await require_token(request, runtime, scope=SCOPE)
return {
"agents": [
{
"name": a.name,
"model": a.model,
"kinds": list(getattr(a, "kinds", ())),
}
for a in runtime.agents
],
"frontends": [
{
"name": fe.name,
"kinds": list(fe.kinds),
"default_agents": {
k: fe.agent_for(k) for k in fe.kinds if fe.agent_for(k)
},
}
for fe in conversations.frontends
],
}
@app.get("/api/conversations")
async def list_conversations(request: Request) -> dict[str, Any]:
await require_token(request, runtime, scope=SCOPE)
@@ -149,10 +181,10 @@ def _build_app(runtime: GatewayRuntime) -> FastAPI: # noqa: PLR0915
raise HTTPException(
status.HTTP_400_BAD_REQUEST, f"kind must be one of {KINDS[:-1]}"
)
if not isinstance(agent, str) or agent not in runtime.agents:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "unknown or missing `agent`"
)
if agent is not None and (
not isinstance(agent, str) or agent not in runtime.agents
):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "unknown `agent`")
seed = str(data.get("seed") or "clean")
if seed not in SEEDS:
raise HTTPException(
@@ -170,13 +202,13 @@ def _build_app(runtime: GatewayRuntime) -> FastAPI: # noqa: PLR0915
window=int_or_none(data, "window"),
origin="api",
)
except ValueError as exc:
except (ValueError, LookupError) as exc:
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
await audit.log(
runtime,
actor=f"token:{token}",
kind="api_spawn",
agent_name=agent,
agent_name=conv.agent_name,
conversation=conv.external_id,
seed=seed,
)
@@ -276,7 +308,9 @@ def _build_app(runtime: GatewayRuntime) -> FastAPI: # noqa: PLR0915
raise HTTPException(
status.HTTP_400_BAD_REQUEST, f"seed must be one of {SEEDS}"
)
agent = str(data.get("agent") or parent.agent_name)
agent = data.get("agent")
if agent is not None and not isinstance(agent, str):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "`agent` must be a string")
try:
child = await conversations.spawn(
kind="branch",
@@ -294,7 +328,7 @@ def _build_app(runtime: GatewayRuntime) -> FastAPI: # noqa: PLR0915
runtime,
actor=f"token:{token}",
kind="api_branch",
agent_name=agent,
agent_name=child.agent_name,
conversation=child.external_id,
parent=parent.external_id,
seed=seed,
@@ -350,12 +384,15 @@ def _build_app(runtime: GatewayRuntime) -> FastAPI: # noqa: PLR0915
data = await body_of(request)
frontend = text_of(data, "frontend")
external_id = text_of(data, "external_id")
await conversations.bind(
conv,
frontend=frontend,
external_id=external_id,
visible=bool(data.get("visible", True)),
)
try:
await conversations.bind(
conv,
frontend=frontend,
external_id=external_id,
visible=bool(data.get("visible", True)),
)
except ValueError as exc:
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
return await conversations.describe(conv)
@app.patch("/api/conversations/{public_id}/flags")