Web ↔︎ AI Integration

How the web application reaches the AI Python package.

The request path

The browser never talks to the AI backend directly. A chat message travels through a Next.js proxy route, then the AI Gateway, which fans out to the selected engine:

The proxy route

web/app/api/ag-ui-chat/route.ts (POST) is the single entry point:

  • Accepts { message, auth, stream, sessionId, system_type, dataset_scope, conversation_history } from the client.
  • With stream: true it returns a Response(new ReadableStream(...)) with Content-Type: text/event-stream. Inside the stream it server-side fetches ${AI_SERVER_URL}/chat/agui/stream, then pumps the backend SSE through — buffering by newline and re-emitting each data: line — before appending a synthetic RUN_FINISHED and a SESSION_METADATA custom event and persisting the assistant artifacts to Supabase.
  • maxDuration = 600 with a 10-minute abort on the upstream fetch.

The body sent upstream is { message, conversation_history, session_id, threadId, jwt_token, system_type, dataset_scope } with an Authorization: Bearer <jwt> header.

Two supporting routes: web/app/api/systems/route.ts (GET /api/systems proxies the gateway’s /systems · /systems/verified) and the older web/app/api/mcp-chat/route.ts.

Configuration

Env var Target Notes
AI_SERVER_URL Gateway :8080 (fallback http://127.0.0.1:8080) The only backend URL web code reads (ag-ui-chat, systems, mcp-chat). Set to http://ai-server:8080 in docker-compose.yml.
KAWA_ADAPTER_URL Kawa adapter :8082 Set on the web container but consumed by the gateway, not by web code.
ADK_BACKEND_URL ADK Runtime :50052 Used by the gateway.
KAVAI_GATEWAY_URL Gateway :8080 Read by Kawa, calling back for image skills (ADR-007). http://ai-server:8080 under compose; localhost on Cloud Run.
NEXT_PUBLIC_APP_URL Web app :3000 Read by the gateway to reach POST /api/media/resolve. The browser-facing value on the web container is the public origin; the gateway’s own default (http://localhost:3000) is the in-pod one.

The web app targets the gateway only; the gateway does the engine fan-out from system_type (SYSTEM_TO_BACKEND in ai/src/kavai/gateway/app.py).

Traffic is not one-way, though. The gateway calls into the web app for media resolution, and Kawa calls into the gateway for image skills — so all three processes must be able to reach each other, which is why they are co-located rather than addressed by URL. See Environments and operations.

The AG-UI client

web/hooks/use-agui-client.ts — useAGUIClient() — is the browser consumer:

  • sendMessage() optimistically appends the user message, resolves/refreshes the JWT, POSTs { message, stream: true, sessionId, system_type, auth: { jwt_token }, threadId } to /api/ag-ui-chat, then reads the SSE via response.body.getReader(), splits on newlines, and parses each data: line into an AGUIEvent.
  • Events are dispatched through the official AgentSubscriber pattern (createSubscriber): TEXT_MESSAGE_START/CONTENT/END (token buffering), TOOL_CALL_START/RESULT, STATE_SNAPSHOT, STATE_DELTA (JSON-Patch), RUN_STARTED/FINISHED/ERROR, and CUSTOM events (IMAGE_GALLERY, DATASET_LIST, THERMAL_ANALYSIS, MARKDOWN_REPORT, THREE_D_OVERVIEW, CDC_PROVENANCE, IMAGE_ANALYSIS_RESULT — the latter recorded per image UUID for the gallery viewer’s annotation overlays).
  • It exposes messages, agentState, isProcessing, and lastProvenance to React; queues messages while offline (@/lib/message-queue) and drains on reconnect; and hydrates history from Supabase via loadSession().

Related: web/lib/ag-ui/types.ts (EventType, AGUIEvent, AgentSubscriber), web/lib/chat/ag-ui-persistence.ts and ag-ui-message-deserializer.ts (persist / restore), and web/lib/copilot/actions.ts (callAGUIBackend() for CopilotKit actions). The normative event shapes are in CONTRACT-CDC-001.

Engine selection

The valid engine ids are argus | dataadk | kawa | orion (SystemId in web/lib/workspaces/config.ts). The selection chain:

  1. Verified list — web/hooks/use-ai-engines.ts (useVerifiedAIEngines) fetches /api/systems?verified=true → the gateway’s /systems/verified.
  2. User default — localStorage['kavai_default_system'], set from the Settings card (components/settings/system-settings-card.tsx); legacy hermes normalizes to kawa.
  3. Workspace default — WorkspaceConfig.defaultEngine (e.g. the Integrity workspace defaults to kawa).
  4. useSelectedEngine() resolves the effective engine (stored default if still verified, else dataadk, else the first verified engine).
  5. The chat surface (components/chat/ai-chat-interface.tsx) treats selectedSystem as the source of truth and sets system_type on the request; useAGUIClient({ system }) maps it into the POST body.

So: kavai_default_system / workspace defaultEngine → useSelectedEngine → selectedSystem → system_type → /api/ag-ui-chat → gateway → ADK (:50052) or Kawa (:8082).