feat(ai): SSE chat streaming + exhaustive today-listing prompt rules - #199
Conversation
…pt rules - POST /api/chat/stream emits started/round/delta/reset/final/error SSE events; buffered /api/chat unchanged for API-key consumers - AiIntentService streams the completion when a sink is provided: content deltas flush immediately (TTFT logged), tool-call deltas accumulate across chunks, reset covers content-before-tool-calls - ProcessUserChatCommand carries an optional StreamSink; the handler emits round keepalives per tool iteration - Prompt rules force exhaustive index listings checksummed against the heading counts; done-today habits stay excluded from today answers Refs thomasluizon/orbit-ui-mobile#159 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
There was a problem hiding this comment.
SSE streaming lands cleanly. The implementation is solid across all the areas that matter:
Authorization & rate-limiting are both class-level attributes on ChatController, so the new /stream endpoint inherits them without any extra decoration needed. Pre-stream validation (message length, image, history, clientContext) correctly returns plain HTTP errors before headers are committed; post-commit failures route through SSE error events — exactly the right split.
Streaming pipeline in AiIntentService is well-structured: buffered and streaming paths share the CompletedRound abstraction, tool-call args are accumulated correctly across chunks via StreamingToolCallBuilder, and the content-before-tool-calls reset case is handled and tested. The AssistantChatMessage reconstruction ensures the tool-loop conversation context is identical on both paths.
Tests are thorough: real ChatClient pipeline over scripted SSE bytes covers text deltas, split tool-args, content-then-reset, and mid-stream drop; serialization contract tests lock the camelCase/string-enum/null-omit wire format the shared Zod schema depends on; handler tests verify round keepalives and Domain→Application sink bridging.
Prompt fix for today-listing is minimal and targeted: the exhaustive-enumeration instruction is gated on UserToday.HasValue (correct null contract), and rule 10/12 changes are tested.
…) (#273) * feat(api): general cheer + social award triggers + profile handle (#199) Relax Cheer.HabitId to optional so cheers are general encouragement, not habit-specific: nullable entity property + factory, optional command/body/DTO, validator drops the NotEmpty rule, EF config switches the habit FK to IsRequired(false) + OnDelete SetNull, and a RelaxCheerHabitId migration alters the column and FK. SendCheer skips the habit-ownership check when no habitId is supplied. Surface Handle + SocialOptIn on ProfileResponse so the client can render a first-run opt-in gate instead of a raw 403. Wire the three social-achievement award triggers via the existing TryGrant funnel using conventional achievement ids ("first_friend", "squad_goals", "cheerleader"): First Friend + Squad Goals (at 5) award BOTH participants on accept; Cheerleader on the 10th sent cheer. These stay dormant until the definitions ship in #196, then activate automatically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(api): integrate main, rescaffold RelaxCheerHabitId, merge profile response fields Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>


Refs thomasluizon/orbit-ui-mobile#159. Paired frontend PR: thomasluizon/orbit-ui-mobile#160 (merge THIS one first — the SSE endpoint must be live before the clients consume it).
SSE chat streaming
New
POST /api/chat/streamnext to the unchanged bufferedPOST /api/chat(kept — it's public surface for API-key/agent consumers). Event protocol (data:frames, camelCase, enums as strings, nulls omitted):startedrounditerationdeltatextresetfinalChatResponseerrorstatus,error,code?AiIntentServiceswitches toCompleteChatStreamingAsyncwhen a sink is provided: content deltas flush immediately (First content token after Nmslogged for the ~2s TTFT acceptance check), tool-call deltas accumulate via an index-keyed builder (args joined across chunks, empty args normalized to{}), and theAssistantChatMessageis rebuilt so the tool loop's conversation context is identical to the buffered path. Null sink → the exact pre-existing code path.reset.correlationId, persistence: all untouched.NetworkTimeout+ retry policy from Harden calendar auto-sync event dedupe #156 still apply; for unbuffered responses the timeout acts per-read (server-side idle guard), and retries only cover the pre-headers leg — mid-stream drops surface as anerrorevent.Today-listing completeness (user-reported regression, prompt-only)
"liste meus hábitos de hoje" was dropping due-today habits (entire Before Bed group skipped despite being fully labeled
[TODAY]in the index). The index data was already correct — done-today habits drop out naturally via DueDate advancement (desired), and remaining habits carryTODAY/OVERDUE. The failure was pure model listing decay, encouraged by rule 10's "keep responses concise".Fix: the index instruction now demands enumerating EVERY labeled entry and verifying the list against the heading's exact counts (
N due today, M overdue— computed with the same predicates as the labels, so they're a ready-made checksum); rule 12 forbids omitting/sampling/summarizing listing answers; rule 10 scopes conciseness to prose, never lists. No data changes, no extra LLM round — the single-round speed from #156 is preserved, and done-today habits stay excluded.Tests (+16; suite now 3,143)
AiIntentServiceStreamingTests— realChatClientpipeline over scripted OpenAI SSE bytes: text-round deltas, tool-args split across chunks, content-then-tools reset, mid-stream drop → failure, buffered path untouched.ChatStreamEventTests— serialization contract locks (camelCase, string enums, null omission) matching the shared Zod union.roundper iteration + Domain→Application sink bridging; null-sink passthrough.UserTodayso the null-today contract holds); GlobalRules additions locked.ProcessChatStreamcapability mapping (included).🤖 Generated with Claude Code