Skip to content

feat: send chat turns to POST /v1/chat and stream the answer - #19

Merged
RohanReddy44 merged 3 commits into
mainfrom
feat/experience-api-2-chat-call
Sep 23, 2026
Merged

RohanReddy44 merged 3 commits into
mainfrom
feat/experience-api-2-chat-call

Conversation

@RohanReddy44

@RohanReddy44 RohanReddy44 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

The client now speaks the Experience API contract for chat.

  • sendUserQuery posts to {api.baseUrl}/v1/chat with the contract's body (sessionId, messageId, query, history, language, location when known) and reads the reply as events: started, delta, completed, error. completed is the answer and replaces the streamed text.
  • Every failure is one ApiError with a code. From the API where it sent one. Made up where it did not: history_too_large for a bare 413, upstream_error for a stream that ends without completed or error or is not an event stream, unknown otherwise.
  • The stub returns the same event stream. It hands back a Response of SSE frames, so the real parser and event handling run on every stubbed reply. Its cycle is three answers, a refusal, and a turn that failed after the DSS accepted it, so Retry can be seen with stubs on.
  • The store has one turn runner where sendText and sendImage had two copies of the streaming and error code. The bubble is drawn on the first delta with the API's assistantMessageId as its id. traceId replaces qid. The "daily question limit" message for 429 is gone: 429 is busy and retryable.
  • Dev proxy. npm run dev forwards the configured api.baseUrl path (/experience/api, read from the same config file) to a local Experience API (http://localhost:8078, DEV_API_URL overrides), prefix stripped as the deployment proxy does. Stubs on: nothing changes.

Not in this PR, by plan

  • history is sent as []. Session id on clear chat, real history and the same messageId on retry are the next PR.
  • Errors show the one generic message. Copy per error code, including "refresh the page" for history_too_large, follows after that.

Verified

  • npm run lint, typecheck, knip, test (45 tests in 10 files) and build pass at each commit.
  • The real call was run against the Experience API on localhost through this code and streamed started, two deltas and completed.
  • A POST through the dev server at /experience/api/v1/chat reaches the API and streams back.
  • Seen on the live API: its error responses are FastAPI defaults ({"detail": ...}), not yet the contract's {"error": {...}}. The client's fallback covers it and the 422 case is in the tests.

Follow-ups noted from review, pending decision

  • Validate started like completed and error are, instead of String()-coercing missing ids.
  • Strip a trailing slash from api.baseUrl.
  • Validate completed.error and reject empty content without error.
  • The suggestions block is still duplicated in sendText and sendImage.

🤖 Generated with Claude Code

RohanReddy44 and others added 2 commits September 24, 2026 01:13
The client spoke the old backend's shape: GET /chat/ with the question
in the query string, the answer read as raw text chunks with a magic
keep-alive word filtered out, a trace id in an X-QID header, and
"connection closed" taken as success. None of that survives the
Experience API contract (docs/implementation/experience-api-contract.md).

sendUserQuery now posts the contract's body (sessionId, messageId,
query, history, language, location when known) and reads the reply as
server-sent events: started, delta, completed, error. `completed` is
the answer and replaces the streamed text. Every failure is one
ApiError with a code the rest of the client can branch on. Where the
API sent no code the client makes one up: history_too_large for a bare
413, upstream_error for a stream that ends without completed or error
or is not an event stream at all, unknown for anything else.

The stub no longer bypasses this. It returns a Response whose body is
the same event stream, so the parser and event handling run on every
stubbed reply too. Its cycle now covers a refusal and a turn that
failed after the DSS accepted it, so Retry can be seen with stubs on.

In the store, sendText and sendImage shared two copies of the
streaming and error code; they now share one turn runner. The bubble
is drawn on the first delta, with the API's assistantMessageId as its
id; traceId replaces qid. The "daily question limit" message for 429
goes: under the contract 429 means busy and is retryable. Errors still
show the one generic message; copy per code follows in a later change,
as does sending the real history, which is [] here.

Verified: lint, typecheck, knip, 45 tests and the build pass. The real
call was also run against the Experience API on localhost and streamed
started, two deltas and completed through this code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With stubs off, the dev server answered POST /experience/api/v1/chat
itself with index.html, which the client reported as upstream_error.
Now that an Experience API runs locally there is something real to
talk to.

The dev server forwards `api.baseUrl` the way the front proxy does in
deployment: same origin for the browser, the prefix stripped before
the API sees the path. The path is read from the same config file the
app uses, so the two cannot drift. The API's own port is the default
and DEV_API_URL overrides it. Nothing changes with stubs on, which is
still the default.

Verified: a POST through localhost:3000/experience/api/v1/chat streams
the API's started, delta and completed events back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@RohanReddy44
RohanReddy44 force-pushed the feat/experience-api-2-chat-call branch from 1b07c5b to b5d54fa Compare September 23, 2026 19:44
Production runs the shipped config.json as it is, so the bundled
default decides what a deployment does. With stubs on it would answer
farmers with canned advice. Now that the client speaks the Experience
API contract, the default is off and chat goes to `api.baseUrl`.

What changes for a developer: `npm run dev` needs an Experience API,
which the dev server proxies to (DEV_API_URL, default localhost:8078).
Without one, every question shows an error card. Flipping
`stubs.enabled` to true locally brings the canned replies back; the
flag is a runtime config value, so nothing else changes, and the stub
still speaks the API's own event stream. The change must not be
committed, and the docs now say so.

ADR-0000 §6.1, README, CLAUDE.md and AGENTS.md are updated in this
commit, as the ADR rule for a flag flip asks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@RohanReddy44
RohanReddy44 force-pushed the feat/experience-api-2-chat-call branch from cf8eca3 to fb411f5 Compare September 23, 2026 19:47
@RohanReddy44
RohanReddy44 changed the base branch from feat/experience-api-1-config-and-sse to main September 23, 2026 20:04
@RohanReddy44
RohanReddy44 merged commit 5ec5e17 into main Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant