feat(coach): quote the real BP range, address by name, render action cards

Feed the user's own usual blood-pressure range — the median plus or minus
MAD band, computed from the readings already in the snapshot — into the Coach
context with a clause to quote those bounds verbatim, so a band is never
invented from window means. Thread the stored first name into the prompt and
surface it sparingly on roughly one turn in three, never as a rote greeting.
Add the context-layer rule that names the relevant context before a
suggestion, a single-chart-token allowance that works on every provider, and
a per-turn opener hint.

Wire the already-shipped suggested-action SSE frame to render: the streaming
hook collects it and the message thread shows the action card, mirroring the
reminder-suggestion path. Extend the eval golden set with band-verbatim,
chart-token, and no-overclaim cases, and pin a red-team case proving the
chart-token syntax cannot smuggle a dose past the outbound screen.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 14:40:07 +02:00
1 parent b393527cac
commit 5674f10b2f
10 files changed
+329 -13

No files matched your search

+19 -2
View File
@@ -72,6 +72,11 @@ import {
coachOutboundFallback,
} from "@/lib/ai/coach/outbound-guard";
import { getCoachSystemPrompt } from "@/lib/ai/coach/system-prompt";
import {
openerArchetypeHint,
shouldUseNameForTurn,
firstNameFromDisplayName,
} from "@/lib/ai/prompts/opener-archetype";
import { getSelfContextTextForUser } from "@/lib/ai/coach/about-me";
import { buildCoachSnapshot } from "@/lib/ai/coach/snapshot";
import {
@@ -366,7 +371,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
// we don't accidentally widen narrow per-call scopes.
const prefsRow = await prisma.user.findUnique({
where: { id: userId },
select: { coachPrefsJson: true },
select: { coachPrefsJson: true, displayName: true },
});
const coachPrefs = parseCoachPrefs(prefsRow?.coachPrefsJson);
const effectiveScope =
@@ -386,7 +391,19 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
// teaches the model to emit `---REMEMBER---` (durable "remind me" capture),
// one teaches the closed `---SUGGEST-ACTION---` confirm-card allowlist. Both
// are provider-neutral sentinels stripped from the prose before it streams.
const systemPrompt = `${getCoachSystemPrompt(locale, coachPrefs, aboutMe)}\n\n${buildRememberAddendum(locale)}\n\n${buildSuggestActionAddendum(locale)}`;
// v1.22 (W6) — per-turn personalization: a sparse, hash-gated first name and
// an opener-archetype hint so multi-turn sessions vary. The turn index is the
// count of prior turns, so the name surfaces on ~1-in-3 turns, varied and
// never on a fixed cadence; both omit cleanly when no display name is set.
const turnIndex = priorTurns.length;
const firstName = firstNameFromDisplayName(prefsRow?.displayName ?? null);
const coachPersonalization = {
firstName,
mayUseName:
firstName != null && shouldUseNameForTurn(`${userId}:${turnIndex}`),
openerHint: openerArchetypeHint(`${userId}:${turnIndex}`, locale),
};
const systemPrompt = `${getCoachSystemPrompt(locale, coachPrefs, aboutMe, coachPersonalization)}\n\n${buildRememberAddendum(locale)}\n\n${buildSuggestActionAddendum(locale)}`;
const allTurns: CoachTurn[] = [
...priorTurns,
{ role: "user", content: message },