mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat(ai): system prompt requires rationale per recommendation
The output-format block now advertises the new `rationale` shape (dataWindow / comparedTo / deviation) and a fifth GROUND RULE mandates that every rec carry one. The dataWindow MUST equal metricSource.timeRange so the UI's mini-chart can pin to the same window the rec was derived from — bracket-mismatch prevention is cheaper to enforce in the prompt than to detect post-parse. Both EN + DE prompts updated symmetrically. PROMPT_VERSION stays at 4.16.0 since B5a already bumped it for this milestone — the content extension is a same-version revision, not a behavioural shape change that warrants 5.0.0. 6 new prompt-assertion tests pin the contract: - output-format block names the three rationale fields - enum values (last7days...allTime) mentioned for both locales - ground rule "MUST carry a rationale" present in EN + DE - "factual" / "sachlich" language guard against placeholders Existing 213 AI tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8a438a0680
commit
c39a527b8e
2 files changed
+89
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getStrictInsightsSystemPrompt } from "../prompts/insight-generator";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B5c — system prompt advertises the new `rationale`
|
||||
* field shape and instructs the model to populate it on every rec.
|
||||
*
|
||||
* The output-format block in the prompt was previously a 4-field
|
||||
* schema (id / text / severity / metricSource). With B5c the
|
||||
* recommendation shape grows a `rationale` object. The model is told
|
||||
* the exact shape AND that it MUST be populated for every rec —
|
||||
* legacy "drop the rec if you can't ground it" guidance from B5a
|
||||
* stays in place; the new ground rule extends "and you cannot ground
|
||||
* a rec without naming the data window, the comparison baseline, and
|
||||
* the deviation."
|
||||
*/
|
||||
|
||||
describe("system prompt — rationale block (B5c)", () => {
|
||||
it("EN prompt advertises the rationale shape in the output-format block", () => {
|
||||
const prompt = getStrictInsightsSystemPrompt("en");
|
||||
// The shape itself
|
||||
expect(prompt).toMatch(/rationale/i);
|
||||
expect(prompt).toMatch(/dataWindow/);
|
||||
expect(prompt).toMatch(/comparedTo/);
|
||||
expect(prompt).toMatch(/deviation/);
|
||||
// The enum is named in the prompt so the model doesn't have to
|
||||
// guess the value vocabulary
|
||||
expect(prompt).toMatch(/last7days/);
|
||||
expect(prompt).toMatch(/last30days/);
|
||||
expect(prompt).toMatch(/last90days/);
|
||||
expect(prompt).toMatch(/allTime/);
|
||||
});
|
||||
|
||||
it("DE prompt advertises the rationale shape in the output-format block", () => {
|
||||
const prompt = getStrictInsightsSystemPrompt("de");
|
||||
expect(prompt).toMatch(/rationale/i);
|
||||
expect(prompt).toMatch(/dataWindow/);
|
||||
expect(prompt).toMatch(/comparedTo/);
|
||||
expect(prompt).toMatch(/deviation/);
|
||||
expect(prompt).toMatch(/last7days/);
|
||||
expect(prompt).toMatch(/allTime/);
|
||||
});
|
||||
|
||||
it("EN prompt has a GROUND RULE requiring rationale on every rec", () => {
|
||||
const prompt = getStrictInsightsSystemPrompt("en");
|
||||
// The ground rule names the three required fields together so the
|
||||
// model can't truncate to one of them
|
||||
expect(prompt).toMatch(/MUST carry a rationale/);
|
||||
});
|
||||
|
||||
it("DE prompt has a GROUNDREGEL requiring rationale on every rec", () => {
|
||||
const prompt = getStrictInsightsSystemPrompt("de");
|
||||
expect(prompt).toMatch(/MUSS .* rationale/i);
|
||||
});
|
||||
|
||||
it("EN prompt mentions clear/factual language for rationale", () => {
|
||||
const prompt = getStrictInsightsSystemPrompt("en");
|
||||
// factual / actual data trends — guards against vague placeholder
|
||||
// rationale text
|
||||
expect(prompt).toMatch(/factual/i);
|
||||
});
|
||||
|
||||
it("DE prompt mentions clear/factual language for rationale", () => {
|
||||
const prompt = getStrictInsightsSystemPrompt("de");
|
||||
// German "sachlich" or "konkret" — same intent
|
||||
expect(prompt).toMatch(/sachlich|konkret/i);
|
||||
});
|
||||
});
|
||||
@@ -71,6 +71,11 @@ GROUND RULES — ZERO HALLUCINATIONS
|
||||
referencing population norms. "Your avg7 (78) is 5 bpm above your
|
||||
90-day median (73)" is preferred over "above the population
|
||||
average".
|
||||
5. Every recommendation MUST carry a rationale object with
|
||||
dataWindow, comparedTo, and deviation. Use clear, factual
|
||||
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.
|
||||
|
||||
GUIDELINE TARGETS — generic, do NOT compute precise risk scores
|
||||
- Adult resting blood pressure (ESH/ESC 2024 generic): aim < 140/90
|
||||
@@ -105,6 +110,11 @@ You MUST return JSON matching this schema exactly:
|
||||
"timeRange": "last7days | last30days | last90days | allTime",
|
||||
"summary": "concrete data point that justifies this recommendation",
|
||||
"n": optional integer sample count
|
||||
},
|
||||
"rationale": {
|
||||
"dataWindow": "last7days | last30days | last90days | allTime",
|
||||
"comparedTo": "what the deviation is measured against — user baseline (e.g. 'your 90-day median (73 bpm)') OR a guideline ceiling (e.g. 'ESH ceiling 140/90')",
|
||||
"deviation": "size + direction of the deviation — e.g. '+5 bpm above baseline over 7 of 7 days'"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -176,6 +186,12 @@ GRUNDREGELN — NULL HALLUZINATIONEN
|
||||
gegenüber Bevölkerungswerten. "Dein avg7 (78) liegt 5 bpm über
|
||||
deinem 90-Tage-Median (73)" ist besser als "über dem
|
||||
Bevölkerungsdurchschnitt".
|
||||
5. Jede Empfehlung MUSS ein rationale-Objekt mit dataWindow,
|
||||
comparedTo und deviation tragen. Schreibe sachlich und konkret.
|
||||
Beziehe dich auf die tatsächlichen Datentrends des Nutzers — kein
|
||||
Platzhaltertext. rationale.dataWindow MUSS gleich
|
||||
metricSource.timeRange sein, damit die UI das Mini-Chart auf das
|
||||
gleiche Fenster fixieren kann.
|
||||
|
||||
LEITLINIEN-ZIELWERTE — generisch, KEINE genauen Risiko-Scores berechnen
|
||||
- Erwachsenen-Ruheblutdruck (ESH/ESC 2024 generisch): Ziel < 140/90
|
||||
@@ -211,6 +227,11 @@ Du MUSST JSON exakt nach diesem Schema liefern:
|
||||
"timeRange": "last7days | last30days | last90days | allTime",
|
||||
"summary": "konkreter Datenpunkt, der die Empfehlung stützt",
|
||||
"n": optionale Sample-Anzahl
|
||||
},
|
||||
"rationale": {
|
||||
"dataWindow": "last7days | last30days | last90days | allTime",
|
||||
"comparedTo": "wogegen die Abweichung verglichen wird — Baseline des Nutzers (z.B. 'dein 90-Tage-Median (73 bpm)') ODER ein Leitlinien-Schwellwert (z.B. 'ESH-Ziel 140/90')",
|
||||
"deviation": "Größe + Richtung der Abweichung — z.B. '+5 bpm über Baseline an 7 von 7 Tagen'"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user