mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(ai): keep the output-language directive terminal on composed prompts
getBaseSystemPrompt appended the directive, then each metric module appended its own section after it, so the directive was no longer the last instruction the model reads - the one property that makes it authoritative. The base now exposes its body, and every composing module wraps the finished prompt so the directive ends it. Also drops a duplicate import that survived the merge of the two prompt branches.
This commit is contained in:
1 parent
5c83156dcf
commit
d0a74e37da
13 files changed
+88
-39
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.30.14",
|
||||
"version": "1.30.15",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ try {
|
||||
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
|
||||
const CACHE_VERSION =
|
||||
(typeof self !== "undefined" && self.__APP_VERSION__) ||
|
||||
/* @sw-version-fallback */ "v1.30.14";
|
||||
/* @sw-version-fallback */ "v1.30.15";
|
||||
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
|
||||
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
||||
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
|
||||
|
||||
@@ -303,14 +303,26 @@ export const ASSESSMENT_SECTION_PAIRS: readonly {
|
||||
de: string;
|
||||
}[] = ASSESSMENT_SECTIONS;
|
||||
|
||||
export function getBaseSystemPrompt(locale: Locale): string {
|
||||
export function getBaseSystemPromptBody(locale: Locale): string {
|
||||
// German readers compose the German body; every other locale composes the
|
||||
// English body and is told, in its own language, which language to write in.
|
||||
// The former `locale === "en" ? "en" : "de"` sent French, Spanish, Italian
|
||||
// and Polish readers a German prompt that asked for German prose.
|
||||
const composed = composeAssessmentPrompt(
|
||||
return composeAssessmentPrompt(
|
||||
instructionLocale(locale),
|
||||
targetLanguageName(locale),
|
||||
);
|
||||
return withOutputLanguage(composed, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base prompt as a STANDALONE system prompt, language directive included.
|
||||
*
|
||||
* Use this only when the result is handed to the provider as-is. A module that
|
||||
* appends its own metric section must instead compose
|
||||
* `getBaseSystemPromptBody` and wrap the finished string in
|
||||
* `withOutputLanguage`, so the directive stays the last instruction the model
|
||||
* reads rather than being buried mid-prompt by the appended section.
|
||||
*/
|
||||
export function getBaseSystemPrompt(locale: Locale): string {
|
||||
return withOutputLanguage(getBaseSystemPromptBody(locale), locale);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const BP_SECTION_DE = `METRIK — BLUTDRUCK:
|
||||
@@ -31,9 +32,12 @@ export function getBloodPressureSystemPrompt(locale: Locale): string {
|
||||
// language and appends their own directive); only de takes the German one.
|
||||
const section =
|
||||
instructionLocale(locale) === "en" ? BP_SECTION_EN : BP_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getBloodPressureUserPrompt(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const BMI_SECTION_DE = `METRIK — BMI:
|
||||
@@ -23,9 +24,12 @@ export function getBmiSystemPrompt(locale: Locale): string {
|
||||
// language and appends their own directive); only de takes the German one.
|
||||
const section =
|
||||
instructionLocale(locale) === "en" ? BMI_SECTION_EN : BMI_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getBmiUserPrompt(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const GENERAL_SECTION_DE = `METRIK — GESAMTBEWERTUNG:
|
||||
@@ -27,9 +28,12 @@ export function getGeneralStatusSystemPrompt(locale: Locale): string {
|
||||
instructionLocale(locale) === "en"
|
||||
? GENERAL_SECTION_EN
|
||||
: GENERAL_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getGeneralStatusUserPrompt(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const MEDCO_SECTION_DE = `METRIK — MEDIKAMENTEN-EINNAHMETREUE:
|
||||
@@ -25,9 +26,12 @@ export function getMedicationComplianceSystemPrompt(locale: Locale): string {
|
||||
// language and appends their own directive); only de takes the German one.
|
||||
const section =
|
||||
instructionLocale(locale) === "en" ? MEDCO_SECTION_EN : MEDCO_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getMedicationComplianceUserPrompt(
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
* block, exactly like the specialised generators.
|
||||
*/
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { instructionLocale } from "./output-language";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { instructionLocale, withOutputLanguage } from "./output-language";
|
||||
import type {
|
||||
MetricArchetype,
|
||||
MetricStatusMeta,
|
||||
@@ -110,11 +110,14 @@ export function getMetricArchetypeSystemPrompt(
|
||||
const section = ARCHETYPE_SECTION[meta.archetype];
|
||||
const archetypeText =
|
||||
instructionLocale(locale) === "en" ? section.en : section.de;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${archetypeText}
|
||||
|
||||
${metaBlock(meta, locale)}`;
|
||||
${metaBlock(meta, locale)}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getMetricArchetypeUserPrompt(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const MOOD_SECTION_DE = `METRIK — STIMMUNG / WOHLBEFINDEN:
|
||||
@@ -25,9 +26,12 @@ export function getMoodSystemPrompt(locale: Locale): string {
|
||||
// language and appends their own directive); only de takes the German one.
|
||||
const section =
|
||||
instructionLocale(locale) === "en" ? MOOD_SECTION_EN : MOOD_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getMoodUserPrompt(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const PULSE_SECTION_DE = `METRIK — PULS / HERZFREQUENZ:
|
||||
@@ -23,9 +24,12 @@ export function getPulseSystemPrompt(locale: Locale): string {
|
||||
// language and appends their own directive); only de takes the German one.
|
||||
const section =
|
||||
instructionLocale(locale) === "en" ? PULSE_SECTION_EN : PULSE_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPulseUserPrompt(
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
* very snapshot + per-metric suffix it would have sent on its own.
|
||||
*/
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { instructionLocale } from "./output-language";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { instructionLocale, withOutputLanguage } from "./output-language";
|
||||
|
||||
/**
|
||||
* The stable per-metric output keys the batch envelope uses. The InsightStatus
|
||||
@@ -47,7 +47,7 @@ export function getStatusBatchSystemPrompt(
|
||||
locale: Locale,
|
||||
presentKeys: readonly string[],
|
||||
): string {
|
||||
const base = getBaseSystemPrompt(locale);
|
||||
const base = getBaseSystemPromptBody(locale);
|
||||
const keyList = presentKeys.join(", ");
|
||||
const en = `BATCHED OUTPUT — you are assessing SEVERAL of this user's metrics in one pass. Each metric below carries its OWN graded snapshot under a "### <key>" heading; assess each one STRICTLY from its own snapshot block, applying every rule above per metric. Do NOT let one metric's finding leak into another. Do NOT invent a metric: assess ONLY the keys present below.
|
||||
|
||||
@@ -59,7 +59,10 @@ Include a key ONLY if its snapshot block is present below (keys present: ${keyLi
|
||||
AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. "perMetric" enthält je vorhandenem Key eine kurze Einschätzung (je 2-4 Sätze, derselbe Vertrag wie eine Einzelkarte):
|
||||
{ "perMetric": { ${presentKeys.map((k) => `"${k}": "..."`).join(", ")} } }
|
||||
Nimm einen Key NUR auf, wenn sein Snapshot-Block unten vorhanden ist (vorhandene Keys: ${keyList}). Lass jede Metrik ohne Block weg — erfinde niemals eine.`;
|
||||
return `${base}\n\n${instructionLocale(locale) === "en" ? en : de}`;
|
||||
return withOutputLanguage(
|
||||
`${base}\n\n${instructionLocale(locale) === "en" ? en : de}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
import { getBaseSystemPromptBody } from "./base-system";
|
||||
import { withOutputLanguage } from "./output-language";
|
||||
import { instructionLocale } from "./output-language";
|
||||
|
||||
const WEIGHT_SECTION_DE = `METRIK — GEWICHT:
|
||||
@@ -29,9 +30,12 @@ export function getWeightSystemPrompt(locale: Locale): string {
|
||||
// language and appends their own directive); only de takes the German one.
|
||||
const section =
|
||||
instructionLocale(locale) === "en" ? WEIGHT_SECTION_EN : WEIGHT_SECTION_DE;
|
||||
return `${getBaseSystemPrompt(locale)}
|
||||
return withOutputLanguage(
|
||||
`${getBaseSystemPromptBody(locale)}
|
||||
|
||||
${section}`;
|
||||
${section}`,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
export function getWeightUserPrompt(
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
* Server-only — reads the DB + the provider chain.
|
||||
*/
|
||||
import {
|
||||
getBaseSystemPrompt,
|
||||
getBaseSystemPromptBody,
|
||||
PROMPT_VERSION,
|
||||
} from "@/lib/ai/prompts/base-system";
|
||||
import { instructionLocale } from "@/lib/ai/prompts/output-language";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import {
|
||||
instructionLocale,
|
||||
withOutputLanguage,
|
||||
} from "@/lib/ai/prompts/output-language";
|
||||
import {
|
||||
normalizeLocale,
|
||||
normalizeSummaryText,
|
||||
@@ -43,7 +47,6 @@ import {
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { instructionLocale } from "@/lib/ai/prompts/output-language";
|
||||
import {
|
||||
openerArchetypeHint,
|
||||
dayRotatedSeed,
|
||||
@@ -98,7 +101,7 @@ const SCORE_INPUT_TYPES: readonly MeasurementType[] = [
|
||||
* the way in; today's callers still pass a narrowed locale.
|
||||
*/
|
||||
function scoreSystemPrompt(locale: Locale): string {
|
||||
const base = getBaseSystemPrompt(locale);
|
||||
const base = getBaseSystemPromptBody(locale);
|
||||
// The archetype section rides the same reviewed instruction body the base
|
||||
// composes: German for a German reader, English for everyone else. The base
|
||||
// already names the reader's own language, so a French reader gets the
|
||||
@@ -118,7 +121,7 @@ This is a daily wellness proxy, not a clinical or training-recovery verdict. Sta
|
||||
- WAS ES HEUTE BEDEUTET — übersetze das Band in einen Ausblick: ein starker Tagesform-/Erholungs-Score → ein guter Tag, um mehr zu wagen / etwas zu pushen; ein schwacher Score → ein ruhigerer Tag tut gut, nimm ihn als Erholungshinweis. Das ist eine band-bedingte Deutung, KEINE neue Zahl — erfinde dafür keine Zahl.
|
||||
- EIN ANSTOSS — schließe mit dem einen gegroundeten nächsten Schritt am schwächsten verhaltens-adressierbaren Beitrag. Ist nichts verhaltens-adressierbar (der schwache Treiber ist reine Physiologie), bestätige und nenne einen Punkt zum Beobachten, statt einen Schritt zu erfinden.
|
||||
Das ist ein täglicher Wellness-Indikator, kein klinisches Urteil und keine Trainings-Recovery-Bewertung. Bleibe beschreibend, nie diagnostisch, nie alarmierend; die Ermutigung ist durch die Beiträge / den Trend verdient, nie ein reflexhaftes Kompliment und nie bloße Zahlenwiederholung.`;
|
||||
return `${base}\n\n${section}`;
|
||||
return withOutputLanguage(`${base}\n\n${section}`, locale);
|
||||
}
|
||||
|
||||
function scoreUserPrompt(
|
||||
|
||||
Reference in new issue
Block a user