feat(ai): system prompt includes curated medical references for citation grounding

Builds a SOURCES block dynamically — only references whose
applicability overlaps the metrics in the current snapshot get
listed, so a weight-only call doesn't burn tokens on ESH BP guidance.
Each line carries the id (lowercase, parser-validated), org, year,
title (locale-routed) and direct URL so the model can cite by id.

Adds a GROUND RULE that tells the model to set
`recommendation.referenceId` to the matching SOURCES id when making a
target-range or normative claim, and to leave it empty for purely
observational comparisons. The schema check from the previous commit
rejects fabricated ids — defence in depth on the prompt instruction.

Plain `getStrictInsightsSystemPrompt(locale)` stays unchanged for
back-compat with the existing assertions and any call-site that
hasn't migrated yet. The new
`buildSystemPromptWithReferences(locale, metrics)` is the path
forward.

10 unit tests pin the SOURCES-block contents, locale-routed titles,
deduplication when metrics repeat, and that the existing scope-
hardened guards (refusal pattern, citation cross-check) survive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 23:49:25 +02:00
1 parent 466b8b5604
commit 7fbfca672e
2 files changed
+193

No files matched your search

@@ -0,0 +1,122 @@
import { describe, it, expect } from "vitest";
import {
getStrictInsightsSystemPrompt,
buildSystemPromptWithReferences,
PROMPT_VERSION,
} from "../prompts/insight-generator";
import {
MEDICAL_REFERENCES,
selectReferencesForMetrics,
} from "../medical-references";
/**
* v1.4.16 phase B5a — system prompt embeds the curated medical-
* reference bundle as a SOURCES block so the model can cite by id.
*
* The block is injected dynamically: only references that overlap the
* metrics in the current snapshot show up. This keeps the prompt
* focused (no irrelevant ESH text on a weight-only call) and stays
* inside the token budget.
*
* The plain `getStrictInsightsSystemPrompt(locale)` continues to work
* (no metrics → no SOURCES block) so legacy call-sites and the
* existing prompt assertions stay green during the migration.
*/
describe("PROMPT_VERSION", () => {
it("is 4.16.0 for the v1.4.16 medical-reference grounding update", () => {
expect(PROMPT_VERSION).toBe("4.16.0");
});
});
describe("buildSystemPromptWithReferences()", () => {
it("with no metrics returns the plain prompt with no SOURCES block", () => {
const prompt = buildSystemPromptWithReferences("en", []);
expect(prompt).toContain("YOUR ROLE");
expect(prompt).not.toContain("SOURCES");
expect(prompt).not.toContain("MEDICAL_REFERENCES");
});
it("for a bp-only snapshot includes a SOURCES block listing the bp-applicable references", () => {
const prompt = buildSystemPromptWithReferences("en", ["bp"]);
const refs = selectReferencesForMetrics(["bp"]);
expect(refs.length).toBeGreaterThan(0);
expect(prompt).toContain("SOURCES");
for (const ref of refs) {
expect(prompt).toContain(ref.id);
expect(prompt).toContain(ref.org);
expect(prompt).toContain(ref.title);
expect(prompt).toContain(ref.url);
}
});
it("English variant uses the English title; German variant uses titleDe", () => {
const en = buildSystemPromptWithReferences("en", ["bp"]);
const de = buildSystemPromptWithReferences("de", ["bp"]);
const sample = MEDICAL_REFERENCES.find((r) =>
r.metricApplicability.includes("bp"),
);
expect(sample).toBeDefined();
if (sample && sample.title !== sample.titleDe) {
expect(en).toContain(sample.title);
expect(de).toContain(sample.titleDe);
}
});
it("teaches the model the citation rule under GROUND RULES", () => {
const prompt = buildSystemPromptWithReferences("en", ["bp"]);
// Allow whitespace incl. newline between phrase halves so a future
// re-flow doesn't break the assertion.
expect(prompt).toMatch(/cite the matching\s+reference id/i);
expect(prompt).toMatch(/SOURCES/);
});
it("German prompt teaches the citation rule under GROUNDREGELN", () => {
const prompt = buildSystemPromptWithReferences("de", ["bp"]);
// The prompt wraps; allow whitespace incl. newline between phrase
// halves so a future re-flow doesn't break the assertion.
expect(prompt).toMatch(/zitiere die passende\s+Referenz-ID/i);
expect(prompt).toMatch(/SOURCES/);
});
it("dedupes references when multiple metrics map to the same source", () => {
const prompt = buildSystemPromptWithReferences("en", ["bp", "bp", "bp"]);
const sample = MEDICAL_REFERENCES.find((r) =>
r.metricApplicability.includes("bp"),
);
expect(sample).toBeDefined();
if (sample) {
// Each id appears at least once but not duplicated within the
// SOURCES block (a duplicate would confuse the model).
const occurrences = (
prompt.match(new RegExp(`id: ${sample.id}\\b`, "g")) ?? []
).length;
expect(occurrences).toBe(1);
}
});
it("keeps the existing scope-hardened guards (refusal pattern, citation cross-check)", () => {
const prompt = buildSystemPromptWithReferences("en", ["bp"]);
expect(prompt).toMatch(/OUT-OF-SCOPE/);
expect(prompt).toMatch(/ZERO HALLUCINATIONS/);
expect(prompt).toContain("citations[]");
});
it("contains the bumped PROMPT_VERSION", () => {
const prompt = buildSystemPromptWithReferences("en", ["bp"]);
expect(prompt).toContain(PROMPT_VERSION);
expect(prompt).toContain("4.16.0");
});
});
describe("plain getStrictInsightsSystemPrompt() backward compatibility", () => {
it("does NOT include the SOURCES block (back-compat)", () => {
const en = getStrictInsightsSystemPrompt("en");
expect(en).not.toContain("SOURCES");
});
it("contains the bumped PROMPT_VERSION 4.16.0", () => {
const en = getStrictInsightsSystemPrompt("en");
expect(en).toContain("4.16.0");
});
});
+71
View File
@@ -21,6 +21,10 @@
*/
import type { Locale } from "@/lib/i18n/config";
import {
selectReferencesForMetrics,
type MedicalReferenceMetric,
} from "../medical-references";
/** Stable identifier for the active system prompt revision. */
export const PROMPT_VERSION = "4.16.0" as const;
@@ -244,6 +248,73 @@ export function getStrictInsightsSystemPrompt(locale: Locale): string {
return locale === "en" ? SYSTEM_PROMPT_EN : SYSTEM_PROMPT_DE;
}
/**
* v1.4.16 phase B5a — return the scope-hardened prompt with a
* dynamically-built SOURCES block injected at the end. Only
* references whose `metricApplicability` overlaps the current
* `metrics[]` are listed, so a weight-only call doesn't burn tokens
* on ESH BP guidance.
*
* The model is told to cite the SOURCES id from the
* `recommendation.referenceId` field (validated against the curated
* bundle in `src/lib/ai/medical-references.ts`). When no metrics are
* supplied the function returns the plain prompt unchanged — useful
* for legacy call-sites and the existing prompt assertions.
*/
export function buildSystemPromptWithReferences(
locale: Locale,
metrics: readonly MedicalReferenceMetric[],
): string {
const base = getStrictInsightsSystemPrompt(locale);
if (metrics.length === 0) return base;
const refs = selectReferencesForMetrics(metrics);
if (refs.length === 0) return base;
if (locale === "en") {
const sourcesBlock = refs
.map(
(r) =>
`- id: ${r.id} | org: ${r.org} | year: ${r.publishedYear} | title: ${r.title} | url: ${r.url}`,
)
.join("\n");
return `${base}
SOURCES — curated medical references applicable to the current metrics
${sourcesBlock}
GROUND RULE — REFERENCE CITATION
When making a target-range claim or normative comparison ("target
< 140/90", "BMI 18.5-24.9", "≥ 7h sleep"), cite the matching
reference id from the SOURCES list above by setting
"recommendation.referenceId" to that id (lowercase, exact match).
Use null / omit the field when the recommendation is observational
only (e.g. "your avg7 is 4 mmHg above your 90-day median"). Never
invent an id — the parser rejects fabricated values.`;
}
const sourcesBlock = refs
.map(
(r) =>
`- id: ${r.id} | org: ${r.org} | jahr: ${r.publishedYear} | titel: ${r.titleDe} | url: ${r.url}`,
)
.join("\n");
return `${base}
SOURCES — kuratierte medizinische Referenzen für die aktuellen Metriken
${sourcesBlock}
GROUNDREGEL — REFERENZ-ZITAT
Bei einer Zielwert-Aussage oder einem normativen Vergleich ("Ziel
< 140/90", "BMI 18,5-24,9", "≥ 7 h Schlaf") zitiere die passende
Referenz-ID aus der obigen SOURCES-Liste, indem du
"recommendation.referenceId" auf diese ID setzt (Kleinbuchstaben,
exakter Treffer). Lass das Feld weg oder setze null, wenn die
Empfehlung rein beobachtend ist (z.B. "dein avg7 liegt 4 mmHg über
deinem 90-Tage-Median"). Erfinde nie eine ID — der Parser lehnt
erfundene Werte ab.`;
}
/**
* Out-of-scope refusal payload — what the prompt instructs the model
* to return when the snapshot has nothing to summarise. Exposed for