mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(ai): route the score, biomarker and coach prompts by instruction locale
The derived-score archetype and the Coach preference overrides branched on the raw locale, so fr/es/it/pl composed German instruction text. Both now route through `instructionLocale`. The score archetype rides on `getBaseSystemPrompt` and therefore appends no directive of its own — the base prompt already carries it. The biomarker assessment is the one prompt in the family that does not compose the base scaffold, so it appends the reader's directive itself; the append collapses into the shared path once that prompt moves onto the base scaffold. The two prompt builders in each of those modules now take the full `Locale` rather than the narrowed de/en alias, so the routing is right the moment the pipeline stops collapsing the four locales on the way in. Also targets the output-language contract test's regex at the compile target instead of the dotAll flag, which does not compile. German and English prompts are unchanged, byte for byte.
This commit is contained in:
1 parent
fff84ea84f
commit
13197c7ee9
4 files changed
+57
-18
No files matched your search
@@ -19,6 +19,7 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { PROMPT_VERSION } from "@/lib/ai/prompts/insight-generator";
|
||||
import { buildNativeCoachPrompt } from "@/lib/ai/prompts/native-prompts";
|
||||
import { instructionLocale } from "@/lib/ai/prompts/output-language";
|
||||
import { learnCatalogPromptBlock } from "./learn-catalog";
|
||||
import {
|
||||
DEFAULT_COACH_PREFS,
|
||||
@@ -1224,7 +1225,7 @@ function buildCoachV122Addendum(
|
||||
locale: Locale,
|
||||
personalization: CoachPersonalization,
|
||||
): string {
|
||||
const isDe = locale === "de";
|
||||
const isDe = instructionLocale(locale) === "de";
|
||||
const parts: string[] = [];
|
||||
parts.push(isDe ? "COACH-ZUSATZ (v1.22)" : "COACH ADDENDUM (v1.22)");
|
||||
|
||||
@@ -1347,7 +1348,11 @@ ${fenceSelfReport(aboutMe)}`;
|
||||
*/
|
||||
function buildPrefsPrefix(locale: Locale, prefs: CoachPrefs): string {
|
||||
const parts: string[] = [];
|
||||
const isEn = locale === "en";
|
||||
// These override lines are internal instructions, so they follow the same
|
||||
// routing as every other prompt body: German only for German readers,
|
||||
// English for everyone else. The former `locale === "en"` sent a French
|
||||
// Coach prompt a German tone override on top of its French body.
|
||||
const isEn = instructionLocale(locale) === "en";
|
||||
|
||||
// Tone
|
||||
if (prefs.tone === "neutral") {
|
||||
|
||||
@@ -128,8 +128,10 @@ describe("base system prompt — every locale", () => {
|
||||
|
||||
it("ends the four non-de/en prompts with their own directive", () => {
|
||||
for (const locale of ["fr", "es", "it", "pl"] as const) {
|
||||
// `[\s\S]` rather than `.` + the `s` flag — the compile target predates
|
||||
// dotAll, and the directive can span lines.
|
||||
expect(getBaseSystemPrompt(locale).trimEnd()).toMatch(
|
||||
/OUTPUT LANGUAGE: .+$/s,
|
||||
/OUTPUT LANGUAGE: [\s\S]+$/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { PROMPT_VERSION } from "@/lib/ai/prompts/base-system";
|
||||
import {
|
||||
instructionLocale,
|
||||
withOutputLanguage,
|
||||
} from "@/lib/ai/prompts/output-language";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { classifyReferenceRange } from "@/lib/labs/reference-range";
|
||||
import { getNoKeyGeneralStatusText } from "@/lib/insights/no-key-fallbacks";
|
||||
import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash";
|
||||
@@ -82,8 +87,21 @@ function insufficient(): BiomarkerStatusResult {
|
||||
};
|
||||
}
|
||||
|
||||
function getSystemPrompt(locale: SupportedLocale, markerName: string): string {
|
||||
if (locale === "de") {
|
||||
/**
|
||||
* Unlike every other assessment prompt in the family, this one does NOT
|
||||
* compose `getBaseSystemPrompt` — it is a self-contained inline scaffold. So
|
||||
* the reader's output-language directive, which the base prompt would have
|
||||
* carried, has to be appended here. When this prompt moves onto the shared
|
||||
* base scaffold the append collapses into the shared path and this call goes
|
||||
* away; until then it is the only thing making a non-de/en reader's language
|
||||
* explicit on this surface.
|
||||
*
|
||||
* The parameter is the full `Locale` rather than `SupportedLocale` so the
|
||||
* routing is correct the moment the pipeline stops collapsing fr/es/it/pl on
|
||||
* the way in; today's callers still pass a narrowed locale.
|
||||
*/
|
||||
function getSystemPrompt(locale: Locale, markerName: string): string {
|
||||
if (instructionLocale(locale) === "de") {
|
||||
return [
|
||||
`Du bist ein vorsichtiger, faktenbasierter Gesundheitsbegleiter und beurteilst den Laborwert „${markerName}" einer Person.`,
|
||||
"Beziehe dich AUSSCHLIESSLICH auf die im Snapshot gelieferten Zahlen. Erfinde keine Werte, keine Diagnosen und keine Medikamente.",
|
||||
@@ -92,21 +110,24 @@ function getSystemPrompt(locale: SupportedLocale, markerName: string): string {
|
||||
'Antworte als JSON-Objekt der Form { "summary": "…" } ohne weiteren Text.',
|
||||
].join(" ");
|
||||
}
|
||||
return [
|
||||
`You are a careful, fact-based health companion assessing a person's lab marker "${markerName}".`,
|
||||
"Use ONLY the figures provided in the snapshot. Do not invent values, diagnoses, or medications.",
|
||||
"Write 2 to 4 calm sentences in plain language: state the current value and how it sits against the reference range, place the trend across recent readings, and add at most one factual pointer.",
|
||||
"No alarmism, no marketing tone, no commands. For notably out-of-range values, refer neutrally to a clinician for interpretation.",
|
||||
'Respond as a JSON object of the form { "summary": "…" } with no other text.',
|
||||
].join(" ");
|
||||
return withOutputLanguage(
|
||||
[
|
||||
`You are a careful, fact-based health companion assessing a person's lab marker "${markerName}".`,
|
||||
"Use ONLY the figures provided in the snapshot. Do not invent values, diagnoses, or medications.",
|
||||
"Write 2 to 4 calm sentences in plain language: state the current value and how it sits against the reference range, place the trend across recent readings, and add at most one factual pointer.",
|
||||
"No alarmism, no marketing tone, no commands. For notably out-of-range values, refer neutrally to a clinician for interpretation.",
|
||||
'Respond as a JSON object of the form { "summary": "…" } with no other text.',
|
||||
].join(" "),
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
function getUserPrompt(
|
||||
snapshotJson: string,
|
||||
todayKey: string,
|
||||
locale: SupportedLocale,
|
||||
locale: Locale,
|
||||
): string {
|
||||
return locale === "de"
|
||||
return instructionLocale(locale) === "de"
|
||||
? `Heutiges Datum: ${todayKey}. Beurteile den folgenden Laborwert-Snapshot:\n${snapshotJson}`
|
||||
: `Today's date: ${todayKey}. Assess the following lab-marker snapshot:\n${snapshotJson}`;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getBaseSystemPrompt,
|
||||
PROMPT_VERSION,
|
||||
} from "@/lib/ai/prompts/base-system";
|
||||
import { instructionLocale } from "@/lib/ai/prompts/output-language";
|
||||
import {
|
||||
normalizeLocale,
|
||||
normalizeSummaryText,
|
||||
@@ -87,10 +88,20 @@ const SCORE_INPUT_TYPES: readonly MeasurementType[] = [
|
||||
|
||||
// ── prompt ────────────────────────────────────────────────────────────────
|
||||
|
||||
function scoreSystemPrompt(locale: SupportedLocale): string {
|
||||
/**
|
||||
* The archetype section rides on top of `getBaseSystemPrompt`, which already
|
||||
* appends the reader's own output-language directive — so this composer must
|
||||
* NOT append one of its own. It only picks the instruction body: German for
|
||||
* German readers, English for every other locale.
|
||||
*
|
||||
* The parameter is the full `Locale` rather than `SupportedLocale` so the
|
||||
* routing is correct the moment the pipeline stops collapsing fr/es/it/pl on
|
||||
* the way in; today's callers still pass a narrowed locale.
|
||||
*/
|
||||
function scoreSystemPrompt(locale: Locale): string {
|
||||
const base = getBaseSystemPrompt(locale);
|
||||
const section =
|
||||
locale === "en"
|
||||
instructionLocale(locale) === "en"
|
||||
? `ARCHETYPE — COMPOSITE WELLNESS SCORE (write like a premium recovery coach — WHOOP / Oura — who genuinely wants this person to do well). This is a 0–100 composite, not a raw measurement. Weave these FOUR beats into varied, connected prose (NOT a fixed template, NOT a labelled list) — lead per the OPENER HINT if one is given:
|
||||
- STANDING — the score and its band, in one short clause.
|
||||
- WHAT DROVE IT — name the contributor(s) in \`signal.contributors[]\` that HELPED (the strongest, highest values) AND the one(s) that HURT (the weakest, lowest values), so a good score earns its win and a soft score is explained honestly. Each contributor is itself 0–100; a low value drags the score down, a high one carries it. Never invent a contributor that is not listed. When none are listed, use the trend (signal.delta) instead.
|
||||
@@ -110,7 +121,7 @@ function scoreUserPrompt(
|
||||
signal: MetricSignal,
|
||||
band: string,
|
||||
todayKey: string,
|
||||
locale: SupportedLocale,
|
||||
locale: Locale,
|
||||
openerHint: string,
|
||||
/** v1.30.3 (QA F5) — the user's own IANA tz; was hardcoded "Europe/Berlin". */
|
||||
tz: string,
|
||||
@@ -120,7 +131,7 @@ function scoreUserPrompt(
|
||||
null,
|
||||
2,
|
||||
);
|
||||
if (locale === "en") {
|
||||
if (instructionLocale(locale) === "en") {
|
||||
return `Date: ${todayKey} (${tz})
|
||||
OPENER HINT: ${openerHint}
|
||||
Write an assessment of ${signal.metric} today across the four beats — the standing, what helped AND what hurt it, what the band means for the day, and one grounded nudge. Aim for 3–5 sentences, roughly 45–75 words (this overrides the shorter base length cap). Connected prose, not a checklist.
|
||||
|
||||
Reference in new issue
Block a user