Files
classic-models-agent-assistant/README.md
T
Stéphane Tailland e474ec1e7a Follow the supply agent's rename
Its Keycloak client and audience scope changed name; the peer entry and the
environment variables that point at it move with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:04:25 +02:00

109 lines
4.4 KiB
Markdown

# Assistant — the orchestrating agent
The single entry point for users. Understands a request, consults the
specialists that can settle it, and composes one answer.
**Framework:** LangGraph · **Mode:** conversational · **Client id:**
`classic-models-agent-assistant`
## It holds nothing, and that is enforced by the realm
This is the component that sees raw user text, so it is the prompt-injection
target. It has no MCP server, no domain scope, and no data of its own. That is
not a convention in the prompt — it is what the realm grants:
```
OBTAINED classic-models-agent-catalogue-audience aud=[classic-models-agent-catalogue]
refused mcp-catalog-audience → Invalid scopes
refused mcp-sales-audience → Invalid scopes
refused catalog-api-audience → Invalid scopes
```
An injection that succeeds against this agent finds no path to a domain,
because there is none to find. Its two optional scopes are one specialist and
the inference endpoint — inference is not a domain, and the agent has to reason.
Adding a specialist is an entry in `specialists.py` plus an audience scope
granted in the realm. Add the entry without the grant and every call to it fails
at the token exchange with `Invalid scopes`, loudly, before any data moves.
## The chain
Four hops, `sub` unchanged throughout — which is what makes the end user's own
roles apply at the far end:
```
1. user sub=pm-user azp=librechat aud=[…-agent-assistant]
2. assistant→cat sub=pm-user azp=classic-models-agent-assistant aud=[…-agent-catalogue]
3. cat→mcp sub=pm-user azp=classic-models-agent-catalogue aud=[mcp-catalog, token-exchange-broker]
4. MCP call → 200
```
`azp` names the last agent in the chain, not the first. Keycloak accepts
`actor_token` and emits no `act` claim, so an orchestrator's identity does not
survive a second exchange — the delegation is visible in the traces, not in the
token.
## What it does with an answer
A specialist returns an answer, never data. The orchestrator composes; it does
not aggregate records it was not given.
Two behaviours worth naming, both verified end to end:
- **Out of remit → it says so.** Asked for a company-wide revenue figure, it
reports that no specialist can settle it rather than answering from what the
model knows. An invented answer is indistinguishable from one the estate gave.
- **A held write is relayed verbatim.** Catalogue parks a proposed write in
`input-required`; this agent passes that message through unchanged and stops.
The user's `approve` comes back through the same conversation and reaches the
specialist, which owns the decision. The orchestrator never approves on
anyone's behalf.
## The graph
`plan → delegate → plan → …` until the model stops calling tools. One tool per
specialist, described by what it *decides*. Delegations in the same step run
concurrently — that is the point of fanning out at all — and are capped by
`ASSISTANT_MAX_DELEGATIONS` (8), counted in calls rather than model turns,
because one specialist consulted twice for the same question is the loop worth
catching.
The graph is rebuilt per turn: its tools carry the user's token, and a graph
held across turns would be a token held across turns.
## Running it
```
uv venv && uv pip install -e '.[dev]'
ASSISTANT_CLIENT_SECRET=… ASSISTANT_CA_BUNDLE=… \
ASSISTANT_URL_CATALOGUE=http://localhost:8099/ \
python -m assistant_agent
```
`ASSISTANT_URL_<NAME>` overrides a specialist's address, for running outside the
cluster against a port-forward. In the cluster nothing sets it.
macOS resolves every `.local` name through mDNS before falling back to
`/etc/hosts`, at a flat ~5s per host. `ASSISTANT_HTTP_TIMEOUT` defaults to 60s
for that reason; in the cluster the names resolve instantly.
## Deployment
```
gitea.sttlab.eu/stt/classic-models-agent-assistant:v0.3.0
```
`k8s/` is what runs, `dev/` is the Flux plumbing. Two manual applies close the
loop once — the `agents` namespace and the `sttlab-ca` ConfigMap already exist,
created by the catalogue agent's repo, and are deliberately not declared here so
two Kustomizations do not fight over pruning them.
```
kubectl apply -f dev/gitrepository.yaml
kubectl apply -f dev/bootstrap-kustomization.yaml
```
`replicas: 1` is a constraint: conversation history lives in the process, and a
specialist's approval round trip passes back through here.