mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat(coach): parse evidence-block sentinel out of streamed prose
Add a text-marker sentinel parser (`---KEYVALUES---` … `---END---`) that strips the Coach's evidence block out of the prose before it streams to the client, validates each line against a Zod schema, and attaches the parsed entries to `provenance.keyValues`. The persisted envelope round-trips so the disclosure renders on conversation reload. Hard-capped at 8 lines and 1 KB to guard against prompt-injection padding; malformed blocks log a wide-event and pass the prose through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bea55eac5c
commit
7b8efaae14
6 files changed
+579
-8
No files matched your search
@@ -60,6 +60,7 @@ import {
|
||||
import { detectRefusal } from "@/lib/ai/coach/refusal";
|
||||
import { getCoachSystemPrompt } from "@/lib/ai/coach/system-prompt";
|
||||
import { buildCoachSnapshot } from "@/lib/ai/coach/snapshot";
|
||||
import { parseKeyValuesSentinel } from "@/lib/ai/coach/keyvalues";
|
||||
|
||||
/**
|
||||
* Hard cap on total turns kept inside the per-call prompt window.
|
||||
@@ -260,18 +261,42 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const replyText = (result.content ?? "").trim();
|
||||
if (!replyText) {
|
||||
const rawReply = (result.content ?? "").trim();
|
||||
if (!rawReply) {
|
||||
return streamProviderError({ code: "coach.provider.empty" });
|
||||
}
|
||||
|
||||
// v1.4.22 — strip the optional `---KEYVALUES---` … `---END---`
|
||||
// sentinel out of the prose. The stripped prose is what we stream
|
||||
// to the client and persist; the parsed entries enrich the
|
||||
// provenance envelope so the UI can render the collapsible
|
||||
// "Worauf bezieht sich das?" disclosure.
|
||||
const sentinel = parseKeyValuesSentinel(rawReply);
|
||||
const replyText = sentinel.prose.trim() || rawReply;
|
||||
const enrichedProvenance =
|
||||
sentinel.keyValues.length > 0
|
||||
? { ...snapshot.provenance, keyValues: sentinel.keyValues }
|
||||
: snapshot.provenance;
|
||||
if (sentinel.malformed) {
|
||||
// Graceful degrade: log so ops can spot a provider whose
|
||||
// sentinel format has drifted, but pass the prose through
|
||||
// unchanged.
|
||||
annotate({
|
||||
action: { name: "coach.keyvalues.parse_failed" },
|
||||
meta: {
|
||||
kept: sentinel.keyValues.length,
|
||||
promptVersion: PROMPT_VERSION,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Persist the assistant message BEFORE we begin streaming; if the
|
||||
// client disconnects we still have the canonical row.
|
||||
const assistantMessage = await appendMessage({
|
||||
conversationId: workingConversationId,
|
||||
role: "assistant",
|
||||
content: replyText,
|
||||
metricSource: snapshot.provenance,
|
||||
metricSource: enrichedProvenance,
|
||||
providerType: workingProviderType,
|
||||
promptVersion: PROMPT_VERSION,
|
||||
});
|
||||
@@ -305,7 +330,7 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
|
||||
controller.enqueue(
|
||||
encodeFrame({
|
||||
type: "provenance",
|
||||
metricSource: snapshot.provenance,
|
||||
metricSource: enrichedProvenance,
|
||||
}),
|
||||
);
|
||||
controller.enqueue(
|
||||
|
||||
Reference in new issue
Block a user