mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(insights): keep the briefing blocks of a reply that validates
The briefing prompt asks for dailyBriefing, trendAnnotations and storyboardAnnotations beside the insight fields, and the briefing card, the dashboard snapshot and the today read consume them. insightResultSchema was a strict object, so it dropped exactly those blocks from a reply that validated, while a reply that failed validation kept them. It is a loose object now: the named fields are checked and the rest of the reply stays.
This commit is contained in:
1 parent
b02caed221
commit
cf37f833e8
5 files changed
+77
-5
No files matched your search
+10
-1
@@ -75,7 +75,16 @@ export const insightRecommendationSchema = z.union([
|
||||
}),
|
||||
]);
|
||||
|
||||
export const insightResultSchema = z.object({
|
||||
/**
|
||||
* The fields every insight reply must carry. Loose on purpose: the briefing
|
||||
* prompt also asks for `dailyBriefing`, `trendAnnotations` and
|
||||
* `storyboardAnnotations`, each read downstream (the briefing card, the
|
||||
* dashboard snapshot, the today read), and a strict object dropped exactly
|
||||
* those from a reply that validated, while a reply that did not validate
|
||||
* kept them. Validation checks these fields and leaves the rest of the reply
|
||||
* as it came.
|
||||
*/
|
||||
export const insightResultSchema = z.looseObject({
|
||||
insightType: z.string().optional(),
|
||||
summary: z.string(),
|
||||
classification: z.enum([
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* A reply that fits the insight schema keeps the blocks the prompt asks for
|
||||
* beside it. The strict object used to drop `dailyBriefing` from exactly the
|
||||
* replies that validated.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseBriefingReply } from "../grounding";
|
||||
|
||||
const VALID = {
|
||||
summary: "Steady week.",
|
||||
classification: "gut",
|
||||
findings: [],
|
||||
correlations: [],
|
||||
recommendations: [],
|
||||
dataQuality: { coverage: "good", gaps: [], confidence: "hoch" },
|
||||
disclaimer: "Consult your doctor.",
|
||||
};
|
||||
|
||||
const BLOCKS = {
|
||||
dailyBriefing: { paragraph: "Readings held steady.", signalsOfDay: [] },
|
||||
trendAnnotations: [{ metric: "weight", note: "flat" }],
|
||||
storyboardAnnotations: [],
|
||||
};
|
||||
|
||||
describe("parseBriefingReply", () => {
|
||||
it("keeps the briefing blocks of a reply that validates", () => {
|
||||
const parsed = parseBriefingReply(JSON.stringify({ ...VALID, ...BLOCKS }));
|
||||
expect(parsed).toMatchObject(BLOCKS);
|
||||
expect(parsed).toMatchObject({ summary: "Steady week." });
|
||||
});
|
||||
|
||||
it("keeps a reply that does not validate as it was parsed", () => {
|
||||
const loose = { summary: "No classification.", ...BLOCKS };
|
||||
expect(parseBriefingReply(JSON.stringify(loose))).toEqual(loose);
|
||||
});
|
||||
});
|
||||
@@ -45,8 +45,8 @@ export function runBriefingCall(
|
||||
// `getInsightsSystemPrompt` returned the old `{changed, stable, drivers,
|
||||
// …}` shape and never asked the model for dailyBriefing, so the hero
|
||||
// strip's "Re-run analysis" button kept producing a payload with no
|
||||
// briefing block. The reply tolerates the strict shape via
|
||||
// `insightResultSchema.safeParse`'s soft fallback to the parsed object.
|
||||
// briefing block. `insightResultSchema` is loose, so a reply that
|
||||
// validates keeps these blocks, and one that does not is kept as parsed.
|
||||
systemPrompt: buildSystemPromptWithReferences(
|
||||
ctx.locale,
|
||||
prompt.referenceMetrics,
|
||||
|
||||
@@ -342,8 +342,8 @@ async function rerollBriefingParagraph(args: {
|
||||
|
||||
/**
|
||||
* Parse + (best-effort) validate a comprehensive provider reply. Returns the
|
||||
* parsed payload (validated when it matches `insightResultSchema`, else the
|
||||
* raw object so the richer optional blocks survive), or null when the reply
|
||||
* parsed payload (validated when it matches `insightResultSchema`, which
|
||||
* keeps the richer optional blocks, else the raw object), or null when the reply
|
||||
* is not valid JSON even after fence-stripping. Null signals the caller to
|
||||
* run its one corrective retry.
|
||||
*/
|
||||
|
||||
+26
@@ -14,6 +14,19 @@
|
||||
"confidence": "hoch"
|
||||
},
|
||||
"disclaimer": "Consult your doctor.",
|
||||
"dailyBriefing": {
|
||||
"paragraph": "Your readings held steady this week, with no surprises.",
|
||||
"signalsOfDay": [
|
||||
{
|
||||
"sourceMetric": "weight",
|
||||
"tone": "info",
|
||||
"headline": "Weight is steady",
|
||||
"nudge": "Keep weighing in at the same time each morning.",
|
||||
"delta": null
|
||||
}
|
||||
],
|
||||
"keyFindings": []
|
||||
},
|
||||
"briefingGeneratedAt": "anchor+0d+12:00:00"
|
||||
},
|
||||
"cached": false,
|
||||
@@ -222,6 +235,19 @@
|
||||
"confidence": "hoch"
|
||||
},
|
||||
"disclaimer": "Consult your doctor.",
|
||||
"dailyBriefing": {
|
||||
"paragraph": "Your readings held steady this week, with no surprises.",
|
||||
"signalsOfDay": [
|
||||
{
|
||||
"sourceMetric": "weight",
|
||||
"tone": "info",
|
||||
"headline": "Weight is steady",
|
||||
"nudge": "Keep weighing in at the same time each morning.",
|
||||
"delta": null
|
||||
}
|
||||
],
|
||||
"keyFindings": []
|
||||
},
|
||||
"briefingGeneratedAt": "anchor+0d+12:00:00"
|
||||
},
|
||||
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
|
||||
|
||||
Reference in new issue
Block a user