mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(insights): AI narrates comparison when toggle is active
Extend `buildUserPrompt()` to accept an optional `ComparisonSnapshot` that, when supplied, appends a SYSTEM CONTEXT block listing each metric's current 30d average, baseline 30d average (from 30 / 365 days back), absolute delta, and percentage delta. The strict system prompt gains a sixth ground rule that instructs the model to narrate the most clinically-significant non-null delta in the summary's first sentence and forbids inventing comparison numbers. The route reads the user's persisted `dashboardWidgetsJson.compari- sonBaseline` and builds the snapshot from a per-metric `summarize()` sweep so the numbers the LLM sees match what the dashboard tile renders — single source of truth for the comparison delta. Comparison is default-on whenever the toggle is on per research §7 Q4: there's no secondary "include in AI" preference, the pulldown is the single affordance. PROMPT_VERSION bumped 4.16.0 → 4.16.1 so the Wide-Event annotations attribute responses to the new prompt revision. Eight new unit tests pin: empty-comparison block render, EN/DE locale switch, lastMonth vs lastYear language, the "summary first sentence" instruction, and the anti-hallucination "do NOT invent" guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e4a408eaf5
commit
f9f99ea24a
5 files changed
+399
-8
No files matched your search
@@ -5,7 +5,14 @@ import { extractFeatures } from "@/lib/insights/features";
|
||||
import {
|
||||
getInsightsSystemPrompt,
|
||||
buildUserPrompt,
|
||||
type ComparisonSnapshot,
|
||||
} from "@/lib/insights/prompt";
|
||||
import { summarize, type DataPoint } from "@/lib/analytics/trends";
|
||||
import {
|
||||
resolveDashboardLayout,
|
||||
type ComparisonBaseline,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { insightResultSchema, type InsightResult } from "@/lib/ai/types";
|
||||
import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider";
|
||||
import {
|
||||
@@ -65,6 +72,102 @@ async function evictPerStatusInsightCache(userId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B8 — fetch the user's persisted comparison toggle and
|
||||
* build a `ComparisonSnapshot` for the prompt builder. Returns null
|
||||
* when comparison is off (most users), so the prompt builder skips
|
||||
* the context block entirely.
|
||||
*
|
||||
* Snapshot construction reuses the analytics `summarize()` helper
|
||||
* (which now emits `avg30LastMonth` + `avg30LastYear`) so the numbers
|
||||
* the LLM sees match the numbers the dashboard tile renders — no
|
||||
* second source of truth for the comparison delta.
|
||||
*/
|
||||
async function buildComparisonSnapshotForUser(
|
||||
userId: string,
|
||||
): Promise<ComparisonSnapshot | null> {
|
||||
// The features payload doesn't carry prior-period values — pulling
|
||||
// a fresh `summarize()` per metric below keeps the snapshot
|
||||
// self-contained without retrofitting features.ts.
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { dashboardWidgetsJson: true },
|
||||
});
|
||||
const layout = resolveDashboardLayout(row?.dashboardWidgetsJson);
|
||||
const baseline: ComparisonBaseline = layout.comparisonBaseline ?? "none";
|
||||
if (baseline === "none") return null;
|
||||
|
||||
// Pull every measurement type once and run summarize() so the
|
||||
// prior-period averages line up with what the dashboard analytics
|
||||
// computes. Only include rows where the LLM can sensibly narrate
|
||||
// a prior comparison (i.e. the metric exists in our snapshot type
|
||||
// mapping below).
|
||||
const typeToSnapshotKey: Record<string, string> = {
|
||||
WEIGHT: "weight",
|
||||
BLOOD_PRESSURE_SYS: "bloodPressureSys",
|
||||
BLOOD_PRESSURE_DIA: "bloodPressureDia",
|
||||
PULSE: "pulse",
|
||||
BODY_FAT: "bodyFat",
|
||||
SLEEP_DURATION: "sleep",
|
||||
ACTIVITY_STEPS: "steps",
|
||||
};
|
||||
const typeUnits: Record<string, string> = {
|
||||
WEIGHT: "kg",
|
||||
BLOOD_PRESSURE_SYS: "mmHg",
|
||||
BLOOD_PRESSURE_DIA: "mmHg",
|
||||
PULSE: "bpm",
|
||||
BODY_FAT: "%",
|
||||
SLEEP_DURATION: "h",
|
||||
ACTIVITY_STEPS: "",
|
||||
};
|
||||
const types = Object.keys(typeToSnapshotKey) as MeasurementType[];
|
||||
const rows = await Promise.all(
|
||||
types.map(async (type) => {
|
||||
const measurements = await prisma.measurement.findMany({
|
||||
where: { userId, type },
|
||||
orderBy: { measuredAt: "asc" },
|
||||
select: { measuredAt: true, value: true },
|
||||
});
|
||||
const summary = summarize(
|
||||
measurements.map(
|
||||
(m): DataPoint => ({ date: m.measuredAt, value: m.value }),
|
||||
),
|
||||
);
|
||||
const baselineAvg =
|
||||
baseline === "lastMonth"
|
||||
? (summary.avg30LastMonth ?? null)
|
||||
: (summary.avg30LastYear ?? null);
|
||||
const currentAvg = summary.avg30 ?? null;
|
||||
const delta =
|
||||
currentAvg !== null && baselineAvg !== null
|
||||
? Math.round((currentAvg - baselineAvg) * 100) / 100
|
||||
: null;
|
||||
const deltaPercent =
|
||||
delta !== null && baselineAvg !== null && baselineAvg !== 0
|
||||
? Math.round((delta / Math.abs(baselineAvg)) * 100 * 10) / 10
|
||||
: null;
|
||||
return {
|
||||
type: typeToSnapshotKey[type] ?? type,
|
||||
currentAvg,
|
||||
baselineAvg,
|
||||
delta,
|
||||
deltaPercent,
|
||||
unit: typeUnits[type] ?? "",
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Drop fully-empty rows (current AND baseline missing) so the prompt
|
||||
// doesn't pad with noise. Keep partial rows (one side null) — the
|
||||
// block renderer flags them with "no prior-period data available"
|
||||
// which is useful context for the model.
|
||||
const metrics = rows.filter(
|
||||
(row) => row.currentAvg !== null || row.baselineAvg !== null,
|
||||
);
|
||||
|
||||
return { baseline, metrics };
|
||||
}
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
const userId = user.id;
|
||||
@@ -155,10 +258,19 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const includeRaw = dbUser?.insightsPrivacyMode === "raw";
|
||||
const features = await extractFeatures(userId, includeRaw);
|
||||
const featuresJson = JSON.stringify(features, null, 2);
|
||||
|
||||
// v1.4.16 phase B8 — pick up the user's persisted comparison toggle
|
||||
// and, when active, build a compact prior-period snapshot from the
|
||||
// `features` summaries that already include `avg30LastMonth` /
|
||||
// `avg30LastYear` (added in the analytics summarize() helper). The
|
||||
// narrative is default-on whenever the toggle is on per research §7
|
||||
// Q4 — the pulldown is the single affordance.
|
||||
const comparisonSnapshot = await buildComparisonSnapshotForUser(userId);
|
||||
const userPrompt = buildUserPrompt(
|
||||
featuresJson,
|
||||
dbUser?.insightsPrivacyMode ?? "aggregated",
|
||||
locale,
|
||||
comparisonSnapshot ?? undefined,
|
||||
);
|
||||
|
||||
let result;
|
||||
|
||||
@@ -24,8 +24,12 @@ import {
|
||||
*/
|
||||
|
||||
describe("PROMPT_VERSION", () => {
|
||||
it("is 4.16.0 for the v1.4.16 medical-reference grounding update", () => {
|
||||
expect(PROMPT_VERSION).toBe("4.16.0");
|
||||
it("is at least 4.16.0 for the v1.4.16 medical-reference grounding update", () => {
|
||||
// v1.4.16 phase B5a anchored at 4.16.0; phase B8 bumped the
|
||||
// patch component to 4.16.1 when the comparison-mode narrative
|
||||
// ground rule landed. Use a string-prefix check so the test
|
||||
// doesn't bind to a specific patch.
|
||||
expect(PROMPT_VERSION.startsWith("4.16.")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +109,8 @@ describe("buildSystemPromptWithReferences()", () => {
|
||||
it("contains the bumped PROMPT_VERSION", () => {
|
||||
const prompt = buildSystemPromptWithReferences("en", ["bp"]);
|
||||
expect(prompt).toContain(PROMPT_VERSION);
|
||||
expect(prompt).toContain("4.16.0");
|
||||
// v1.4.16: phase B5a was 4.16.0; phase B8 bumped to 4.16.1.
|
||||
expect(prompt).toMatch(/4\.16\.\d+/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,8 +120,8 @@ describe("plain getStrictInsightsSystemPrompt() backward compatibility", () => {
|
||||
expect(en).not.toContain("SOURCES");
|
||||
});
|
||||
|
||||
it("contains the bumped PROMPT_VERSION 4.16.0", () => {
|
||||
it("contains the bumped PROMPT_VERSION", () => {
|
||||
const en = getStrictInsightsSystemPrompt("en");
|
||||
expect(en).toContain("4.16.0");
|
||||
expect(en).toMatch(/4\.16\.\d+/);
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "../medical-references";
|
||||
|
||||
/** Stable identifier for the active system prompt revision. */
|
||||
export const PROMPT_VERSION = "4.16.0" as const;
|
||||
export const PROMPT_VERSION = "4.16.1" as const;
|
||||
|
||||
const SYSTEM_PROMPT_EN = `You are a clinical-context summariser for a personal health-log app.
|
||||
Prompt version: ${PROMPT_VERSION}.
|
||||
@@ -76,6 +76,13 @@ GROUND RULES — ZERO HALLUCINATIONS
|
||||
language. Reference the user's actual data trends — do not paste
|
||||
in placeholder text. The rationale.dataWindow MUST equal the
|
||||
metricSource.timeRange so the UI's mini-chart can pin to it.
|
||||
6. v1.4.16 phase B8 — when the user prompt contains a "SYSTEM
|
||||
CONTEXT — COMPARISON MODE ACTIVE" block, narrate the comparison
|
||||
in the summary's first sentence using the prior-period numbers
|
||||
that block carries. Cite the deltas verbatim — do NOT invent
|
||||
comparison numbers, and do NOT extrapolate beyond the metrics
|
||||
listed. When the block reports "no prior-period data available"
|
||||
for every metric, state that explicitly and skip the narration.
|
||||
|
||||
GUIDELINE TARGETS — generic, do NOT compute precise risk scores
|
||||
- Adult resting blood pressure (ESH/ESC 2024 generic): aim < 140/90
|
||||
@@ -192,6 +199,13 @@ GRUNDREGELN — NULL HALLUZINATIONEN
|
||||
Platzhaltertext. rationale.dataWindow MUSS gleich
|
||||
metricSource.timeRange sein, damit die UI das Mini-Chart auf das
|
||||
gleiche Fenster fixieren kann.
|
||||
6. v1.4.16 phase B8 — wenn der User-Prompt einen Block "SYSTEM
|
||||
CONTEXT — VERGLEICHSMODUS AKTIV" enthält, narrative die im Block
|
||||
gelisteten Deltas im ersten Satz der Zusammenfassung. Zitiere die
|
||||
Werte exakt — erfinde KEINE Vergleichszahlen und extrapoliere
|
||||
nicht über die gelisteten Metriken hinaus. Wenn der Block für
|
||||
alle Metriken "no prior-period data available" meldet, sag das
|
||||
explizit und lass die Narration weg.
|
||||
|
||||
LEITLINIEN-ZIELWERTE — generisch, KEINE genauen Risiko-Scores berechnen
|
||||
- Erwachsenen-Ruheblutdruck (ESH/ESC 2024 generisch): Ziel < 140/90
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
buildUserPrompt,
|
||||
buildComparisonBlock,
|
||||
type ComparisonSnapshot,
|
||||
} from "@/lib/insights/prompt";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B8 — comparison context block in the user prompt.
|
||||
*
|
||||
* When the dashboard's comparison toggle is active, the route layer
|
||||
* passes a `ComparisonSnapshot` to `buildUserPrompt()` so the LLM has
|
||||
* the prior-period numbers needed to narrate "Your average BP
|
||||
* improved by 4 mmHg vs. last month".
|
||||
*/
|
||||
|
||||
const monthlySnapshot: ComparisonSnapshot = {
|
||||
baseline: "lastMonth",
|
||||
metrics: [
|
||||
{
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
currentAvg: 128,
|
||||
baselineAvg: 132,
|
||||
delta: -4,
|
||||
deltaPercent: -3,
|
||||
unit: "mmHg",
|
||||
},
|
||||
{
|
||||
type: "WEIGHT",
|
||||
currentAvg: 80.4,
|
||||
baselineAvg: 82.7,
|
||||
delta: -2.3,
|
||||
deltaPercent: -2.8,
|
||||
unit: "kg",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("buildUserPrompt() — comparison block (B8)", () => {
|
||||
it("appends NO comparison block when comparison is undefined", () => {
|
||||
const prompt = buildUserPrompt("{\"weight\":[]}", "agg", "en");
|
||||
expect(prompt).not.toMatch(/COMPARISON MODE/i);
|
||||
expect(prompt).not.toMatch(/VERGLEICHSMODUS/i);
|
||||
});
|
||||
|
||||
it("appends an English comparison block when comparison is supplied with locale=en", () => {
|
||||
const prompt = buildUserPrompt(
|
||||
"{\"weight\":[]}",
|
||||
"agg",
|
||||
"en",
|
||||
monthlySnapshot,
|
||||
);
|
||||
expect(prompt).toMatch(/COMPARISON MODE ACTIVE/);
|
||||
expect(prompt).toMatch(/30 days ago/);
|
||||
expect(prompt).toContain("BLOOD_PRESSURE_SYS");
|
||||
expect(prompt).toContain("delta -4.0 mmHg");
|
||||
expect(prompt).toContain("WEIGHT");
|
||||
expect(prompt).toContain("delta -2.3 kg");
|
||||
});
|
||||
|
||||
it("appends a German comparison block when locale=de", () => {
|
||||
const prompt = buildUserPrompt(
|
||||
"{\"gewicht\":[]}",
|
||||
"agg",
|
||||
"de",
|
||||
monthlySnapshot,
|
||||
);
|
||||
expect(prompt).toMatch(/VERGLEICHSMODUS AKTIV/);
|
||||
expect(prompt).toMatch(/30 Tage zuvor/);
|
||||
expect(prompt).toContain("BLOOD_PRESSURE_SYS");
|
||||
});
|
||||
|
||||
it("uses 365-day language for the lastYear baseline", () => {
|
||||
const yearly: ComparisonSnapshot = {
|
||||
baseline: "lastYear",
|
||||
metrics: [
|
||||
{
|
||||
type: "WEIGHT",
|
||||
currentAvg: 80,
|
||||
baselineAvg: 85,
|
||||
delta: -5,
|
||||
deltaPercent: -5.9,
|
||||
unit: "kg",
|
||||
},
|
||||
],
|
||||
};
|
||||
const en = buildUserPrompt("{}", "agg", "en", yearly);
|
||||
const de = buildUserPrompt("{}", "agg", "de", yearly);
|
||||
expect(en).toMatch(/365 days ago/);
|
||||
expect(de).toMatch(/365 Tage zuvor/);
|
||||
});
|
||||
|
||||
it("renders an empty-metrics fallback that tells the model to say so", () => {
|
||||
const empty: ComparisonSnapshot = {
|
||||
baseline: "lastMonth",
|
||||
metrics: [],
|
||||
};
|
||||
const block = buildComparisonBlock("en", empty);
|
||||
expect(block).toMatch(
|
||||
/no metric currently has enough prior-period data to compare/,
|
||||
);
|
||||
});
|
||||
|
||||
it("instructs the model that the most-significant delta must appear in the summary's first sentence", () => {
|
||||
const block = buildComparisonBlock("en", monthlySnapshot);
|
||||
expect(block).toMatch(/MUST appear in the summary['’]s\s+first sentence/);
|
||||
});
|
||||
|
||||
it("never instructs the model to invent comparison numbers (anti-hallucination guard)", () => {
|
||||
const en = buildComparisonBlock("en", monthlySnapshot);
|
||||
const de = buildComparisonBlock("de", monthlySnapshot);
|
||||
expect(en).toMatch(/Do NOT invent comparison numbers/);
|
||||
expect(de).toMatch(/Erfinde KEINE Vergleichszahlen/);
|
||||
});
|
||||
|
||||
it("flags missing-prior-period metrics with 'no prior-period data available'", () => {
|
||||
const partial: ComparisonSnapshot = {
|
||||
baseline: "lastMonth",
|
||||
metrics: [
|
||||
{
|
||||
type: "MOOD",
|
||||
currentAvg: 4,
|
||||
baselineAvg: null,
|
||||
delta: null,
|
||||
deltaPercent: null,
|
||||
unit: "/5",
|
||||
},
|
||||
],
|
||||
};
|
||||
const block = buildComparisonBlock("en", partial);
|
||||
expect(block).toMatch(/MOOD: no prior-period data available/);
|
||||
});
|
||||
});
|
||||
+129
-2
@@ -2,6 +2,43 @@
|
||||
* System prompt and output formatting for OpenAI insights.
|
||||
*/
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import type { ComparisonBaseline } from "@/lib/dashboard-layout";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B8 — comparison snapshot the prompt narrates against
|
||||
* when the user has the dashboard's "Compare to" toggle on. The snapshot
|
||||
* is opt-in: pass it through `buildUserPrompt`'s 4th argument when
|
||||
* `comparisonBaseline !== "none"` so the LLM has prior-period context
|
||||
* for narrating "Your average BP improved by 4 mmHg vs. last month".
|
||||
*
|
||||
* Per research §7 Q4 the narrative is default-on whenever the toggle
|
||||
* itself is active — the pulldown is the single affordance, no
|
||||
* secondary "include in AI" preference, so users don't see surprising
|
||||
* mismatches between the chart they're staring at and the AI summary.
|
||||
*/
|
||||
export interface ComparisonSnapshot {
|
||||
baseline: Exclude<ComparisonBaseline, "none">;
|
||||
/** One row per metric the snapshot can compare. Empty array allowed
|
||||
* so the prompt builder is non-strict — the wrapper still adds the
|
||||
* context block but instructs the model that no metric had enough
|
||||
* prior-period data to compare. */
|
||||
metrics: Array<{
|
||||
/** Snapshot key matching the metricSource convention used elsewhere
|
||||
* in the AI pipeline ("bloodPressure", "weight", "pulse", "mood"). */
|
||||
type: string;
|
||||
currentAvg: number | null;
|
||||
baselineAvg: number | null;
|
||||
/** `currentAvg - baselineAvg`, rounded to 2dp; null when either
|
||||
* side missing. */
|
||||
delta: number | null;
|
||||
/** `(delta / baselineAvg) * 100`, rounded to 1dp; null when either
|
||||
* side missing or baselineAvg is 0. */
|
||||
deltaPercent: number | null;
|
||||
/** Free-form unit label ("mmHg", "kg", "bpm", "/5") so the model
|
||||
* doesn't have to guess based on metric name. */
|
||||
unit: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const INSIGHTS_SYSTEM_PROMPT_DE = `Du bist ein Gesundheitsdaten-Insights-Assistent. Du analysierst aggregierte Gesundheitsdaten und gibst klare, datenbasierte Hinweise.
|
||||
|
||||
@@ -81,7 +118,10 @@ export function buildUserPrompt(
|
||||
featuresJson: string,
|
||||
privacyMode: string,
|
||||
locale: Locale,
|
||||
comparison?: ComparisonSnapshot,
|
||||
): string {
|
||||
const comparisonBlock = comparison ? buildComparisonBlock(locale, comparison) : "";
|
||||
|
||||
if (locale === "en") {
|
||||
const modeLabel =
|
||||
privacyMode === "raw"
|
||||
@@ -92,7 +132,7 @@ export function buildUserPrompt(
|
||||
Data mode: ${modeLabel}
|
||||
Note: use each metric's coverage object for measurement frequency and time spans, and tailor the analysis accordingly.
|
||||
|
||||
${featuresJson}`;
|
||||
${featuresJson}${comparisonBlock}`;
|
||||
}
|
||||
|
||||
const modeLabel =
|
||||
@@ -104,5 +144,92 @@ ${featuresJson}`;
|
||||
Datenmodus: ${modeLabel}
|
||||
Hinweis: Beachte die coverage-Objekte jeder Metrik für Informationen zu Messhäufigkeit und Zeiträumen. Passe deine Analyse entsprechend an.
|
||||
|
||||
${featuresJson}`;
|
||||
${featuresJson}${comparisonBlock}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B8 — comparison context block.
|
||||
*
|
||||
* Renders a dedicated SYSTEM CONTEXT section after the user data so
|
||||
* the LLM can narrate the comparison ("Your average BP improved by
|
||||
* 4 mmHg vs. last month"). The instruction "narrate the comparison
|
||||
* when relevant" is in the system prompt; this block carries the
|
||||
* actual prior-period numbers.
|
||||
*
|
||||
* Exported for unit tests so the integration test can assert the
|
||||
* exact string the LLM sees.
|
||||
*/
|
||||
export function buildComparisonBlock(
|
||||
locale: Locale,
|
||||
snapshot: ComparisonSnapshot,
|
||||
): string {
|
||||
const baselineLabel =
|
||||
locale === "en"
|
||||
? snapshot.baseline === "lastMonth"
|
||||
? "the same window 30 days ago"
|
||||
: "the same window 365 days ago"
|
||||
: snapshot.baseline === "lastMonth"
|
||||
? "den gleichen Zeitraum 30 Tage zuvor"
|
||||
: "den gleichen Zeitraum 365 Tage zuvor";
|
||||
|
||||
const renderRow = (
|
||||
metric: ComparisonSnapshot["metrics"][number],
|
||||
): string => {
|
||||
if (metric.delta === null || metric.baselineAvg === null) {
|
||||
return `- ${metric.type}: no prior-period data available`;
|
||||
}
|
||||
const sign = metric.delta > 0 ? "+" : metric.delta < 0 ? "-" : "±";
|
||||
const formattedDelta = `${sign}${Math.abs(metric.delta).toFixed(1)}${metric.unit ? ` ${metric.unit}` : ""}`;
|
||||
const pct =
|
||||
metric.deltaPercent != null
|
||||
? ` (${metric.deltaPercent > 0 ? "+" : metric.deltaPercent < 0 ? "-" : "±"}${Math.abs(metric.deltaPercent).toFixed(1)} %)`
|
||||
: "";
|
||||
const current =
|
||||
metric.currentAvg != null
|
||||
? `${metric.currentAvg.toFixed(1)}${metric.unit ? ` ${metric.unit}` : ""}`
|
||||
: "n/a";
|
||||
const prior =
|
||||
metric.baselineAvg != null
|
||||
? `${metric.baselineAvg.toFixed(1)}${metric.unit ? ` ${metric.unit}` : ""}`
|
||||
: "n/a";
|
||||
return `- ${metric.type}: current ${current}, baseline ${prior}, delta ${formattedDelta}${pct}`;
|
||||
};
|
||||
|
||||
if (snapshot.metrics.length === 0) {
|
||||
return locale === "en"
|
||||
? `
|
||||
|
||||
SYSTEM CONTEXT — COMPARISON MODE ACTIVE
|
||||
The user has activated comparison mode against ${baselineLabel}, but no metric currently has enough prior-period data to compare. State this explicitly in the summary's first sentence.`
|
||||
: `
|
||||
|
||||
SYSTEM CONTEXT — VERGLEICHSMODUS AKTIV
|
||||
Der Nutzer hat den Vergleichsmodus gegen ${baselineLabel} aktiviert, aber für keine Metrik liegen ausreichend Daten aus der Vergleichsperiode vor. Erwähne das explizit im ersten Satz der Zusammenfassung.`;
|
||||
}
|
||||
|
||||
if (locale === "en") {
|
||||
return `
|
||||
|
||||
SYSTEM CONTEXT — COMPARISON MODE ACTIVE
|
||||
The user has activated comparison mode against ${baselineLabel}.
|
||||
When narrating findings, reference the deltas below. The most
|
||||
clinically-significant non-null delta MUST appear in the summary's
|
||||
first sentence (e.g. "Your average systolic BP improved by 4 mmHg
|
||||
vs. last month"). Do NOT invent comparison numbers.
|
||||
|
||||
Prior-period deltas:
|
||||
${snapshot.metrics.map(renderRow).join("\n")}`;
|
||||
}
|
||||
|
||||
return `
|
||||
|
||||
SYSTEM CONTEXT — VERGLEICHSMODUS AKTIV
|
||||
Der Nutzer hat den Vergleichsmodus gegen ${baselineLabel} aktiviert.
|
||||
Beziehe dich beim Narrativ auf die untenstehenden Deltas. Die
|
||||
klinisch bedeutsamste nicht-leere Abweichung MUSS im ersten Satz der
|
||||
Zusammenfassung auftauchen (z.B. "Dein systolischer Mittelwert ist
|
||||
4 mmHg besser als im Vormonat"). Erfinde KEINE Vergleichszahlen.
|
||||
|
||||
Vorperioden-Deltas:
|
||||
${snapshot.metrics.map(renderRow).join("\n")}`;
|
||||
}
|
||||
Reference in new issue
Block a user