Developer Guide
The chat-service is the conversation layer in front of the agent. The Agent Service is intentionally stateless — each POST /ask is an independent call that takes an optional history array but stores nothing. The chat-service is what turns that into a real chat product: it persists conversations, owns them per user, and feeds the recent history back into each agent call.
It is a Node.js / Express service in TypeScript, matching the rest of the backend. Keeping conversation storage out of the Python agent is deliberate: the agent stays a pure question-answering engine, while session persistence — a different concern with a different database and scaling profile — lives in its own service.
| Path | Responsibility |
|---|---|
src/router.ts | Route table for conversation CRUD and message sending, plus health and metrics. |
src/controller/conversation.ts | HTTP adapters over the conversation service. |
src/services/conversation.ts | Core logic: ownership checks, validation, history assembly, persistence. |
src/services/agent.ts | Thin client that calls agent-service /ask and validates its response. |
src/models/conversation.ts | Mongoose schema for Conversation, its embedded messages, and citations. |
src/middlewares/authentication.ts | Decodes the mesh-verified claims header and extracts the owning account id. |
graph TD
ROUTER["router.ts"] --> CTRL["controller/conversation.ts"]
CTRL --> CSV["services/conversation.ts\nownership, validation, history"]
CTRL --> ASV["services/agent.ts\nHTTP client only"]
CSV --> MODEL["models/conversation.ts"]
CSV -->|"sendMessage"| ASV
ASV -->|"x-gateway-claims forwarded"| AGENT["agent-service /ask"]A conversation is a single MongoDB document with its messages embedded rather than stored in a separate collection. A chat is loaded and saved as a whole, and message counts are bounded, so embedding avoids a join and makes each individual MongoDB save atomic.
Sending a message is not atomic end-to-end: it loads the conversation, waits for an external agent request, then saves the updated document without a transaction or per-conversation lock. Concurrent sends can therefore use stale history or race while saving. The current client prevents concurrent sends from one widget, but server-side concurrency control is future work.
interface IConversation {
userId: string; // owner; indexed { userId: 1 }
title: string; // defaults to "New chat"; auto-set from the first question
messages: IChatMessage[]; // embedded, capped at MAX_MESSAGES
}
interface IChatMessage {
role: "user" | "assistant";
content: string; // 1..MAX_MESSAGE_LENGTH chars
citations?: ICitation[]; // grounding sources, copied from the agent's answer
createdAt: Date;
}Citations are persisted on the assistant message so that reopening an old conversation shows the same grounded sources the agent originally returned, without re-querying.
Conversation routes require authentication. GET /health and GET /metrics are public operational endpoints. The service is exposed through Caddy at http://localhost/chat (and the same /chat ingress prefix in Kubernetes).
| Method · Path | Description |
|---|---|
POST /conversations | Create an empty conversation for the caller. Optional title. |
GET /conversations | List the caller’s conversations, newest first, without message bodies. |
GET /conversations/:id | Fetch one owned conversation with its full message history. |
PATCH /conversations/:id | Rename an owned conversation. |
DELETE /conversations/:id | Delete an owned conversation. |
POST /conversations/:id/messages | Send a user message; returns the assistant reply (see below). |
GET /health/, GET /metrics/ | Liveness and Prometheus metrics. |
Every conversation route resolves the document by { _id, userId }, so one user can never read or mutate another user’s chat — an unmatched id returns 404 rather than 403, to avoid revealing that the conversation exists.
POST /conversations/:id/messages is where the chat-service orchestrates the agent. It loads the conversation, takes the last HISTORY_MAX_MESSAGES turns as history, calls the agent, and persists both the user message and the assistant reply.
sequenceDiagram
autonumber
participant C as Client (ChatWidget)
participant CH as chat-service
participant DB as chat-db
participant A as agent-service
C->>CH: POST /conversations/:id/messages { content }
CH->>DB: load conversation by { _id, userId }
Note over CH: take last HISTORY_MAX_MESSAGES turns
CH->>A: POST /ask { question, history, stream:false } (+ x-gateway-claims header)
A-->>CH: { answer, citations }
CH->>DB: push user + assistant messages, save
CH-->>C: assistant messageTwo design points worth calling out:
x-gateway-claims header is forwarded to the agent verbatim, not a service account. This means the agent runs /ask under the user’s identity, so its permission-filtered retrieval only returns documents that user is allowed to see. The chat-service never widens access.HISTORY_MAX_MESSAGES turns before being sent. This bounds the prompt size (and therefore cost and latency) the agent has to process, and the agent already treats prior messages as untrusted context rather than instructions.The first user message also seeds the conversation title (the prior default is "New chat"), so a freshly created chat gets a meaningful name without a separate request.
| Variable | Default | Purpose |
|---|---|---|
AGENT_SERVICE_URL | http://agent-service:3000 | Where to reach the agent’s /ask endpoint. |
MONGO_URI | — | Connection string for chat-db. |
HISTORY_MAX_MESSAGES | 10 | Number of recent turns sent to the agent as context. |
MAX_MESSAGES (100), MAX_MESSAGE_LENGTH (8000), and MAX_TITLE_LENGTH (120) are fixed in-code limits that bound document growth and reject oversized input.
requireAuthentication decodes the mesh-injected x-gateway-claims header — Istio’s RequestAuthentication already verified the gateway JWT once, at the ingress — and extracts accountId as the owning userId. There is no signature verification here at all; chat-service trusts the header rather than re-checking a JWT itself. The implementation follows the repository’s Node-service middleware conventions, but chat-service owns its authentication middleware and error classes rather than sharing a package; see Node Auth & Error Middleware for the broader pattern. A failure to reach the agent surfaces as a 502 Bad Gateway, distinguishing an upstream agent problem from a client error.
In the frontend, the assistant is the floating ChatWidget.vue, driven by the useChatSessions composable, which calls these /chat/* routes. For what the assistant can and cannot answer from a user’s perspective, see Using the Assistant in the user guide.