mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(insights): extend the anti-repetition diversity context to the headline cards
The v1.12.1 diversity block (variety lead, data-strength line, steady-run repetition signal, FDR-screened cross-metric relations) reached only the generic archetype cards. The seven hand-tuned headline cards — the ones users see most — ran at temperature 0.3 with only the previous-context block, so they stayed prone to repetitive openers and forced filler steps. Thread the same assessmentContextBlock through the specialized get*UserPrompt builders and raise their temperature to 0.45 to match the archetypes. Soften the unconditional "close with one step" across the specialized sections and the activity / body-composition / sleep archetypes to the base prompt's conditional form — close with a step only when the finding implies one, otherwise affirm and name one thing to watch — so a steady metric no longer manufactures a filler step. EN/DE parity preserved; guard computeSteadyRun against an absent graded slice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
941947399a
commit
b6126db754
16 files changed
+282
-43
No files matched your search
@@ -11,7 +11,7 @@ const BP_SECTION_DE = `METRIK — BLUTDRUCK:
|
||||
- targets: Falls bloodPressure.targets vorhanden, ordne gegen das persönliche Zielband ein; inTargetPctLast30DailyPoints ist der Anteil der letzten Tage im Ziel.
|
||||
- Medikamenten-Bezug: bpMedications trägt compliance7/compliance30. bpMedicationContinuityVsSystolic.correlation und weightVsSystolic.correlation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.
|
||||
- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.
|
||||
- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe mit EINEM machbaren Schritt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen.`;
|
||||
- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const BP_SECTION_EN = `METRIC — BLOOD PRESSURE:
|
||||
- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a summary + graded series; judge both components TOGETHER, never in isolation.
|
||||
@@ -23,7 +23,7 @@ const BP_SECTION_EN = `METRIC — BLOOD PRESSURE:
|
||||
- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.
|
||||
- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.
|
||||
- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.
|
||||
- One message: if the values sit above the person's baseline, close with ONE doable step (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly.`;
|
||||
- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step.`;
|
||||
|
||||
export function getBloodPressureSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? BP_SECTION_EN : BP_SECTION_DE;
|
||||
@@ -37,19 +37,30 @@ export function getBloodPressureUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/**
|
||||
* v1.12.7 — diversity / anti-repetition context (variety lead, data
|
||||
* strength, steady-run repetition signal, cross-metric relations), the
|
||||
* same block the archetype cards carry. Grounded in already-computed
|
||||
* data; optional and may be empty.
|
||||
*/
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
const ctxBlock =
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short assessment of this person's blood pressure: name the current systolic/diastolic level, place the recent days against their own weekly/monthly baseline, and close with one doable step. Judge confidence from the measurement count and recency.${ctxBlock}
|
||||
Write one short assessment of this person's blood pressure: name the current systolic/diastolic level, place the recent days against their own weekly/monthly baseline, and — when something is genuinely actionable — close with one doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Einschätzung zum Blutdruck dieser Person: benenne das aktuelle systolisch/diastolisch-Niveau, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline ein und schließe mit einem machbaren Schritt. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Einschätzung zum Blutdruck dieser Person: benenne das aktuelle systolisch/diastolisch-Niveau, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline ein und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const BMI_SECTION_DE = `METRIK — BMI:
|
||||
- WHO-Bänder zur groben Einordnung, nie als Etikett für die Person: Untergewicht < 18.5, Normal 18.5-24.9, Übergewicht 25.0-29.9, Adipositas I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. Ab 65+ gilt 22-27 als günstig (DEGAM).
|
||||
- Bandbezug: Vergleiche den recent-Wert gegen das weekly/monthly-Mittel und nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.
|
||||
- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.
|
||||
- Eine Botschaft: Schließe mit EINEM machbaren Schritt, der zur Bandlage passt — bei stabilem Wert im günstigen Band das ehrlich anerkennen, statt einen Befund zu erzwingen.`;
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const BMI_SECTION_EN = `METRIC — BMI:
|
||||
- The snapshot carries bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).
|
||||
@@ -15,7 +15,7 @@ const BMI_SECTION_EN = `METRIC — BMI:
|
||||
- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).
|
||||
- Band reference: compare the recent value against the weekly/monthly mean and call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.
|
||||
- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.
|
||||
- One message: close with ONE doable step that fits the band placement — when the value is stable in the favourable band, acknowledge that honestly rather than forcing a finding.`;
|
||||
- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step.`;
|
||||
|
||||
export function getBmiSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? BMI_SECTION_EN : BMI_SECTION_DE;
|
||||
@@ -29,19 +29,25 @@ export function getBmiUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/** v1.12.7 — diversity / anti-repetition context; see blood-pressure.ts. */
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
const ctxBlock =
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short assessment of this person's BMI: name the current value and WHO band, say whether it has crossed a band or is nearing a band boundary against their own weekly/monthly baseline, and close with one doable step. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.${ctxBlock}
|
||||
Write one short assessment of this person's BMI: name the current value and WHO band, say whether it has crossed a band or is nearing a band boundary against their own weekly/monthly baseline, and — when something is genuinely actionable — close with one doable step; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Einschätzung zum BMI dieser Person: benenne den aktuellen Wert und das WHO-Band, sage gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert, und schließe mit einem machbaren Schritt. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Einschätzung zum BMI dieser Person: benenne den aktuellen Wert und das WHO-Band, sage gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert, und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const GENERAL_SECTION_DE = `METRIK — GESAMTBEWERTUNG:
|
||||
- Pro Metrik dieselbe Logik: das recent-Mittel gegen das weekly/monthly-Mittel der Person stellen und nur signifikante Abweichungen von der eigenen Baseline melden.
|
||||
- Positives ausdrücklich anerkennen — ein stabiler oder sich verbessernder Wert ist eine echte Botschaft, kein Lückenfüller.
|
||||
- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.
|
||||
- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung mit GENAU EINER wichtigsten, machbaren Empfehlung ("eine Sache").`;
|
||||
- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung ("eine Sache"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen.`;
|
||||
|
||||
const GENERAL_SECTION_EN = `METRIC — OVERALL ASSESSMENT:
|
||||
- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.
|
||||
@@ -17,7 +17,7 @@ const GENERAL_SECTION_EN = `METRIC — OVERALL ASSESSMENT:
|
||||
- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.
|
||||
- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.
|
||||
- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.
|
||||
- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion ("one thing").`;
|
||||
- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion ("one thing") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation.`;
|
||||
|
||||
export function getGeneralStatusSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? GENERAL_SECTION_EN : GENERAL_SECTION_DE;
|
||||
@@ -31,6 +31,8 @@ export function getGeneralStatusUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/** v1.12.7 — diversity / anti-repetition context; see blood-pressure.ts. */
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
// v1.4: when the previous-analysis context block is supplied, the
|
||||
// model is instructed to call out improvements / regressions
|
||||
@@ -40,14 +42,18 @@ export function getGeneralStatusUserPrompt(
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short overall assessment across the available health metrics: name what stands out, place the recent days against the person's own weekly/monthly baseline, and close with the single most important doable step. Judge confidence from the measurement count, density and recency.${ctxBlock}
|
||||
Write one short overall assessment across the available health metrics: name what stands out, place the recent days against the person's own weekly/monthly baseline, and — when something is genuinely actionable — close with the single most important doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten: benenne das Auffälligste, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline ein und schließe mit dem einen wichtigsten machbaren Schritt. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten: benenne das Auffälligste, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline ein und schließe — wenn etwas wirklich umsetzbar ist — mit dem einen wichtigsten machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const MEDCO_SECTION_DE = `METRIK — MEDIKAMENTEN-EINNAHMETREUE:
|
||||
- Einordnung (zur Orientierung, nicht als Urteil): ≥ 90 % sehr verlässlich, 80-89 % gut, 70-79 % lückenhaft, < 70 % deutlich lückenhaft — die therapeutische Wirkung kann dann eingeschränkt sein.
|
||||
- Positiv rahmen: streak ist die längste ununterbrochene Serie — bei guter Treue ausdrücklich anerkennen. Bei mehreren Medikamenten das mit der niedrigsten Treue benennen.
|
||||
- Zusammenhänge zu Vitalwerten nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Keine Wirkung erfinden, wenn die Daten fehlen.
|
||||
- Eine Botschaft: Schließe mit EINEM machbaren Schritt, der zur Lücke passt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Keine Schuldzuweisung.`;
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung.`;
|
||||
|
||||
const MEDCO_SECTION_EN = `METRIC — MEDICATION ADHERENCE:
|
||||
- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, dailySeries (graded) and latestDay.
|
||||
@@ -17,7 +17,7 @@ const MEDCO_SECTION_EN = `METRIC — MEDICATION ADHERENCE:
|
||||
- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.
|
||||
- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.
|
||||
- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.
|
||||
- One message: close with ONE doable step that fits the gap (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). No blame.`;
|
||||
- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame.`;
|
||||
|
||||
export function getMedicationComplianceSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? MEDCO_SECTION_EN : MEDCO_SECTION_DE;
|
||||
@@ -31,19 +31,25 @@ export function getMedicationComplianceUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/** v1.12.7 — diversity / anti-repetition context; see blood-pressure.ts. */
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
const ctxBlock =
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short assessment of this person's medication adherence: name the recent rate, place it against their own baseline (compliance7 vs compliance30), and close with one doable, blame-free step. Judge confidence from the event count and recency.${ctxBlock}
|
||||
Write one short assessment of this person's medication adherence: name the recent rate, place it against their own baseline (compliance7 vs compliance30), and — when something is genuinely actionable — close with one doable, blame-free step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person: benenne die jüngste Rate, ordne sie gegen die eigene Baseline ein (compliance7 vs. compliance30) und schließe mit einem machbaren, wertfreien Schritt. Konfidenz aus Ereignisanzahl und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person: benenne die jüngste Rate, ordne sie gegen die eigene Baseline ein (compliance7 vs. compliance30) und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren, wertfreien Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -87,12 +87,12 @@ const ARCHETYPE_SECTION: Record<
|
||||
de: "ARCHETYP — PHYSIOLOGISCHER VITALWERT:\n- Das ist ein Momentanwert. Benenne das aktuelle Niveau und ordne es sowohl gegen die eigene Baseline (recent vs. Monats-/Jahresmittel) als auch — falls ein Referenzband vorliegt — gegen dieses Band ein.\n- Ein einzelner Wert außerhalb des Bands ist selten ein Befund; eine anhaltende Verschiebung über das recent-Fenster gegen die Baseline schon. Erwähne einen Trend außerhalb des Bands sachlich, ohne zu alarmieren.\n- Formuliere jede Richtung (steigend/fallend) als mögliche Erklärung (Fitness, Stress, Schlaf, Krankheit, Medikament), nie als Diagnose.",
|
||||
},
|
||||
"activity-fitness": {
|
||||
en: "ARCHETYPE — ACTIVITY / FITNESS:\n- This is a daily activity or fitness measure, mostly higher-better. Read the recent daily mean and compare it to the user's own weekly/monthly baseline — is their activity up, down, or steady?\n- Day-to-day swings are expected; report the multi-day trend, not a single low or high day. When a reference band is given, use it as a coarse target, not a verdict.\n- Close with one doable, specific step toward more (or sustained) movement — never the generic 'exercise more' platitude.",
|
||||
de: "ARCHETYP — AKTIVITÄT / FITNESS:\n- Das ist ein tägliches Aktivitäts- oder Fitnessmaß, meist höher = besser. Lies das recent-Tagesmittel und vergleiche es mit der eigenen Wochen-/Monats-Baseline — ist die Aktivität gestiegen, gesunken oder stabil?\n- Tagesschwankungen sind normal; melde den Mehrtagestrend, nicht einen einzelnen niedrigen oder hohen Tag. Ein Referenzband ist ein grobes Ziel, kein Urteil.\n- Schließe mit einem machbaren, konkreten Schritt für mehr (oder gleichbleibende) Bewegung — nie die generische Floskel 'mehr Bewegung'.",
|
||||
en: "ARCHETYPE — ACTIVITY / FITNESS:\n- This is a daily activity or fitness measure, mostly higher-better. Read the recent daily mean and compare it to the user's own weekly/monthly baseline — is their activity up, down, or steady?\n- Day-to-day swings are expected; report the multi-day trend, not a single low or high day. When a reference band is given, use it as a coarse target, not a verdict.\n- Close with one doable, specific step toward more (or sustained) movement ONLY when the finding implies one — never the generic 'exercise more' platitude. When activity is steady and already strong, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.",
|
||||
de: "ARCHETYP — AKTIVITÄT / FITNESS:\n- Das ist ein tägliches Aktivitäts- oder Fitnessmaß, meist höher = besser. Lies das recent-Tagesmittel und vergleiche es mit der eigenen Wochen-/Monats-Baseline — ist die Aktivität gestiegen, gesunken oder stabil?\n- Tagesschwankungen sind normal; melde den Mehrtagestrend, nicht einen einzelnen niedrigen oder hohen Tag. Ein Referenzband ist ein grobes Ziel, kein Urteil.\n- Schließe NUR DANN mit einem machbaren, konkreten Schritt für mehr (oder gleichbleibende) Bewegung, wenn der Befund einen nahelegt — nie die generische Floskel 'mehr Bewegung'. Ist die Aktivität stabil und bereits gut, erkenne das an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.",
|
||||
},
|
||||
"body-composition": {
|
||||
en: "ARCHETYPE — BODY COMPOSITION:\n- This is a body-composition mass (or rating). A healthy absolute value depends entirely on body size, so judge the TREND toward or away from the user's own baseline, not an absolute number.\n- Report a sustained directional change across weeks/months; single readings carry scale + hydration noise. Tie it to weight context only if the snapshot supports it.\n- Close with one step that supports the favourable direction for THIS metric.",
|
||||
de: "ARCHETYP — KÖRPERZUSAMMENSETZUNG:\n- Das ist eine Körperzusammensetzungs-Masse (oder Bewertung). Ein gesunder Absolutwert hängt stark von der Körpergröße ab — beurteile den TREND zur bzw. weg von der eigenen Baseline, nicht eine Absolutzahl.\n- Melde eine anhaltende Richtungsänderung über Wochen/Monate; Einzelwerte tragen Waagen- und Hydrationsrauschen. Stelle einen Gewichtsbezug nur her, wenn der Snapshot ihn stützt.\n- Schließe mit einem Schritt, der die für DIESE Metrik günstige Richtung unterstützt.",
|
||||
en: "ARCHETYPE — BODY COMPOSITION:\n- This is a body-composition mass (or rating). A healthy absolute value depends entirely on body size, so judge the TREND toward or away from the user's own baseline, not an absolute number.\n- Report a sustained directional change across weeks/months; single readings carry scale + hydration noise. Tie it to weight context only if the snapshot supports it.\n- Close with one step that supports the favourable direction for THIS metric ONLY when the finding implies one. When the trend is steady and favourable, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.",
|
||||
de: "ARCHETYP — KÖRPERZUSAMMENSETZUNG:\n- Das ist eine Körperzusammensetzungs-Masse (oder Bewertung). Ein gesunder Absolutwert hängt stark von der Körpergröße ab — beurteile den TREND zur bzw. weg von der eigenen Baseline, nicht eine Absolutzahl.\n- Melde eine anhaltende Richtungsänderung über Wochen/Monate; Einzelwerte tragen Waagen- und Hydrationsrauschen. Stelle einen Gewichtsbezug nur her, wenn der Snapshot ihn stützt.\n- Schließe NUR DANN mit einem Schritt, der die für DIESE Metrik günstige Richtung unterstützt, wenn der Befund einen nahelegt. Ist der Trend stabil und günstig, erkenne das an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.",
|
||||
},
|
||||
"mobility-gait": {
|
||||
en: "ARCHETYPE — MOBILITY / GAIT:\n- This is a gait/mobility-stability signal Apple surfaces in its Mobility section. Its value is as a stability + anomaly flag: a steady reading near the user's baseline is reassuring; a sustained drift in the unfavourable direction is the signal to surface.\n- These metrics move slowly; one off day is noise. Only flag a multi-week drift, and frame it as a mobility observation, never a fall-risk diagnosis.\n- If steady, say so plainly. If drifting unfavourably, give one gentle, doable step.",
|
||||
@@ -103,8 +103,8 @@ const ARCHETYPE_SECTION: Record<
|
||||
de: "ARCHETYP — UMWELT-EXPOSITION:\n- Das ist eine Umwelt-Exposition (Schallpegel / Anzahl lauter Ereignisse), niedriger = besser. Das Referenzband ist ein Schwellenwert (z.B. WHO 80 dBA) — recent-Werte, die ihn über mehrere Tage überschreiten, sind der erwähnenswerte Befund.\n- Ein einzelnes lautes Konzert oder ein Flug ist kein Muster. Melde wiederholte Überschreitungen, nicht einen Einzelausschlag.\n- Schließe mit einem praktischen Schritt zur Reduktion (z.B. Hörlautstärke senken, eine ruhige Pause) — schützend formuliert, nie alarmierend.",
|
||||
},
|
||||
sleep: {
|
||||
en: "ARCHETYPE — SLEEP:\n- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.\n- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.\n- Close with one doable step toward more consistent or sufficient sleep — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time).",
|
||||
de: "ARCHETYP — SCHLAF:\n- Das ist die nächtliche Schlafdauer. Die Snapshot-Werte sind in Minuten; rechne für deinen Text in Stunden um (z.B. 450 min = 7,5 h). Bewerte sowohl die typische Dauer als auch ihre KONSTANZ: stabile ~7–9 h sind das günstige Muster; hohe Schwankung von Nacht zu Nacht ist selbst dann erwähnenswert, wenn der Mittelwert passt.\n- Vergleiche die jüngsten Nächte mit der eigenen Wochen-/Monats-Baseline. Ein Referenzband von 7–9 h ist eine grobe Erwachsenen-Orientierung, kein Urteil — Kurz- und Langschläfer variieren.\n- Schließe mit einem machbaren Schritt zu konstanterem oder ausreichenderem Schlaf — nie die verbotene Floskel 'achte auf ausreichend Schlaf'; sei konkret (z.B. eine festere Einschlafzeit).",
|
||||
en: "ARCHETYPE — SLEEP:\n- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.\n- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.\n- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.",
|
||||
de: "ARCHETYP — SCHLAF:\n- Das ist die nächtliche Schlafdauer. Die Snapshot-Werte sind in Minuten; rechne für deinen Text in Stunden um (z.B. 450 min = 7,5 h). Bewerte sowohl die typische Dauer als auch ihre KONSTANZ: stabile ~7–9 h sind das günstige Muster; hohe Schwankung von Nacht zu Nacht ist selbst dann erwähnenswert, wenn der Mittelwert passt.\n- Vergleiche die jüngsten Nächte mit der eigenen Wochen-/Monats-Baseline. Ein Referenzband von 7–9 h ist eine grobe Erwachsenen-Orientierung, kein Urteil — Kurz- und Langschläfer variieren.\n- Schließe NUR DANN mit einem machbaren Schritt zu konstanterem oder ausreichenderem Schlaf, wenn der Befund einen nahelegt — nie die verbotene Floskel 'achte auf ausreichend Schlaf'; sei konkret (z.B. eine festere Einschlafzeit). Ist der Schlaf bereits konstant und ausreichend, erkenne das an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const MOOD_SECTION_DE = `METRIK — STIMMUNG / WOHLBEFINDEN:
|
||||
- Bei anhaltend sehr niedriger Stimmung (mehrere Tage deutlich unter ~2.5): biete ruhig und autonomie-wahrend einen Unterstützungs-Pfad an — etwa "bei anhaltender Belastung kann ein Gespräch mit einer Vertrauensperson oder einer Fachperson helfen". Nie alarmierend, nie als Diagnose, kein Notfall-Ton; eine sanfte Option, kein Imperativ.
|
||||
- Tags: Falls mood.tags vorhanden, kannst du ein wiederkehrendes Tag als möglichen Kontext nennen, ohne einen Zusammenhang zu behaupten.
|
||||
- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.
|
||||
- Eine Botschaft: Schließe mit EINEM machbaren, freundlichen Schritt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen.`;
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const MOOD_SECTION_EN = `METRIC — MOOD / WELL-BEING:
|
||||
- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).
|
||||
@@ -17,7 +17,7 @@ const MOOD_SECTION_EN = `METRIC — MOOD / WELL-BEING:
|
||||
- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. "if the strain persists, talking it through with someone you trust or a professional can help". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.
|
||||
- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.
|
||||
- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.
|
||||
- One message: close with ONE doable, kind step (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly.`;
|
||||
- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.`;
|
||||
|
||||
export function getMoodSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? MOOD_SECTION_EN : MOOD_SECTION_DE;
|
||||
@@ -31,19 +31,25 @@ export function getMoodUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/** v1.12.7 — diversity / anti-repetition context; see blood-pressure.ts. */
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
const ctxBlock =
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short assessment of this person's mood: name the recent level, place it against their own weekly/monthly baseline, and close with one kind, doable step. Judge confidence from the entry count and recency.${ctxBlock}
|
||||
Write one short assessment of this person's mood: name the recent level, place it against their own weekly/monthly baseline, and — when something is genuinely actionable — close with one kind, doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Einschätzung zur Stimmung dieser Person: benenne das jüngste Niveau, ordne es gegen die eigene Wochen-/Monats-Baseline ein und schließe mit einem freundlichen, machbaren Schritt. Konfidenz aus Eintragsanzahl und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Einschätzung zur Stimmung dieser Person: benenne das jüngste Niveau, ordne es gegen die eigene Wochen-/Monats-Baseline ein und schließe — wenn etwas wirklich umsetzbar ist — mit einem freundlichen, machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const PULSE_SECTION_DE = `METRIK — PULS / HERZFREQUENZ:
|
||||
- Richtung: Ein über Wochen sinkender Ruhepuls deutet auf bessere kardiovaskuläre Fitness; ein steigender kann auf Stress, Schlafmangel oder Übertraining hinweisen — als mögliche Erklärung, nie als Diagnose.
|
||||
- Einordnungs-Anker (Ruhepuls): athletisch 50-60, normal 60-100. Werte im Band 80-100 sind technisch normal, langfristig aber günstiger niedriger. < 50 oder > 100 über mehrere Tage ist erwähnenswert, ohne zu alarmieren.
|
||||
- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.
|
||||
- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe mit EINEM machbaren Schritt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich, erkenne das ehrlich an.`;
|
||||
- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const PULSE_SECTION_EN = `METRIC — PULSE / HEART RATE:
|
||||
- The snapshot carries pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.
|
||||
@@ -15,7 +15,7 @@ const PULSE_SECTION_EN = `METRIC — PULSE / HEART RATE:
|
||||
- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.
|
||||
- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.
|
||||
- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.
|
||||
- One message: if the resting pulse sits above the person's baseline, close with ONE doable step (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving, acknowledge that honestly.`;
|
||||
- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.`;
|
||||
|
||||
export function getPulseSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? PULSE_SECTION_EN : PULSE_SECTION_DE;
|
||||
@@ -29,19 +29,25 @@ export function getPulseUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/** v1.12.7 — diversity / anti-repetition context; see blood-pressure.ts. */
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
const ctxBlock =
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short assessment of this person's resting pulse: name the current level, place the recent days against their own weekly/monthly baseline, and close with one doable step. Judge confidence from the measurement count and recency.${ctxBlock}
|
||||
Write one short assessment of this person's resting pulse: name the current level, place the recent days against their own weekly/monthly baseline, and — when something is genuinely actionable — close with one doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person: benenne das aktuelle Niveau, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline ein und schließe mit einem machbaren Schritt. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person: benenne das aktuelle Niveau, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline ein und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const WEIGHT_SECTION_DE = `METRIK — GEWICHT:
|
||||
- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.
|
||||
- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.
|
||||
- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.
|
||||
- Eine Botschaft: Schließe mit EINEM machbaren Schritt, der zur Richtung passt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen).`;
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const WEIGHT_SECTION_EN = `METRIC — WEIGHT:
|
||||
- The snapshot carries weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.
|
||||
@@ -21,7 +21,7 @@ const WEIGHT_SECTION_EN = `METRIC — WEIGHT:
|
||||
- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.
|
||||
- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.
|
||||
- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.
|
||||
- One message: close with ONE doable step that fits the direction (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise).`;
|
||||
- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step.`;
|
||||
|
||||
export function getWeightSystemPrompt(locale: Locale): string {
|
||||
const section = locale === "en" ? WEIGHT_SECTION_EN : WEIGHT_SECTION_DE;
|
||||
@@ -35,19 +35,25 @@ export function getWeightUserPrompt(
|
||||
todayKey: string,
|
||||
locale: Locale,
|
||||
previousContextBlock?: string,
|
||||
/** v1.12.7 — diversity / anti-repetition context; see blood-pressure.ts. */
|
||||
assessmentContextBlock?: string,
|
||||
): string {
|
||||
const ctxBlock =
|
||||
previousContextBlock && previousContextBlock.trim().length > 0
|
||||
? `\n\n${previousContextBlock}\n`
|
||||
: "";
|
||||
const extraBlock =
|
||||
assessmentContextBlock && assessmentContextBlock.trim().length > 0
|
||||
? `\n\n${assessmentContextBlock}\n`
|
||||
: "";
|
||||
if (locale === "en") {
|
||||
return `Date: ${todayKey} (Europe/Berlin)
|
||||
Write one short assessment of this person's weight: name the current level and direction, place the recent days against their own weekly/monthly baseline as a continuous trend and pace (kg/week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers), and close with one doable step. Judge confidence from the measurement count and recency.${ctxBlock}
|
||||
Write one short assessment of this person's weight: name the current level and direction, place the recent days against their own weekly/monthly baseline as a continuous trend and pace (kg/week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers), and — when something is genuinely actionable — close with one doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
return `Datum: ${todayKey} (Europe/Berlin)
|
||||
Schreibe eine kurze Einschätzung zum Gewicht dieser Person: benenne Niveau und Richtung, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline als kontinuierlichen Trend und Tempo ein (kg/Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt) und schließe mit einem machbaren Schritt. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}
|
||||
Schreibe eine kurze Einschätzung zum Gewicht dieser Person: benenne Niveau und Richtung, ordne die jüngsten Tage gegen die eigene Wochen-/Monats-Baseline als kontinuierlichen Trend und Tempo ein (kg/Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt) und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
|
||||
|
||||
${snapshotJson}`;
|
||||
}
|
||||
@@ -186,10 +186,13 @@ export function formatRepetitionSignal(
|
||||
* mean that still counts as "no material change" (default 8 %).
|
||||
*/
|
||||
export function computeSteadyRun(
|
||||
weekly: ReadonlyArray<{ mean: number }>,
|
||||
monthly: ReadonlyArray<{ mean: number }>,
|
||||
weekly: ReadonlyArray<{ mean: number }> | undefined,
|
||||
monthly: ReadonlyArray<{ mean: number }> | undefined,
|
||||
bandFraction = 0.08,
|
||||
): number {
|
||||
// Defensive: a graded slice can be absent on a degraded / stubbed series.
|
||||
// Treat a missing slice as no signal so the repetition block stays silent.
|
||||
if (!weekly || !monthly) return 0;
|
||||
if (weekly.length < 2) return 0;
|
||||
|
||||
// Baseline: the longer-horizon mean the recent weeks are placed against.
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
computeSteadyRun,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target";
|
||||
import {
|
||||
@@ -540,6 +546,29 @@ export async function generateBloodPressureStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — same diversity / anti-repetition block the archetype cards
|
||||
// carry, now threaded into this headline card. Pure formatting of
|
||||
// already-computed data; the correlation fetch is best-effort and resolves
|
||||
// to no block on failure, never a generation failure.
|
||||
const varietyLead = pickVarietyLead(userId, "blood-pressure", todayKey);
|
||||
const steadyRun = computeSteadyRun(sysGraded.weekly, sysGraded.monthly);
|
||||
const relations = await getRelevantCorrelationsForMetric(
|
||||
userId,
|
||||
"BLOOD_PRESSURE_SYS",
|
||||
);
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: sysSeries.daily.length,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
},
|
||||
repeatCount: steadyRun,
|
||||
relations,
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
const outcome = await runStatusCompletion({
|
||||
userId,
|
||||
cacheAction,
|
||||
@@ -550,8 +579,11 @@ export async function generateBloodPressureStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45: more cadence entropy while
|
||||
// FACTS stay pinned by the snapshot + the forbidden-phrase guards.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
computeSteadyRun,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesWithRollups,
|
||||
@@ -238,6 +243,25 @@ export async function generateBmiStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
|
||||
// BMI is a per-user height transform of WEIGHT (no discovery channel of its
|
||||
// own), so the RELATIONS sub-block stays empty; the variety / data-strength
|
||||
// / repetition signals carry the rotation.
|
||||
const varietyLead = pickVarietyLead(userId, "bmi", todayKey);
|
||||
const steadyRun = computeSteadyRun(bmiGraded.weekly, bmiGraded.monthly);
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: bmiSeries.daily.length,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
},
|
||||
repeatCount: steadyRun,
|
||||
relations: [],
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
const outcome = await runStatusCompletion({
|
||||
userId,
|
||||
cacheAction,
|
||||
@@ -248,8 +272,10 @@ export async function generateBmiStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
buildGradedSeriesFromPoints,
|
||||
@@ -369,6 +373,25 @@ export async function generateGeneralStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
|
||||
// The overview spans many metrics with no single graded mean to band, so
|
||||
// the steady-run signal is left to the previous-context comparison
|
||||
// (repeatCount 0) and there is no single discovery channel (relations
|
||||
// empty); the variety lead + overall data-strength carry the rotation.
|
||||
const varietyLead = pickVarietyLead(userId, "general", todayKey);
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: measurements.length,
|
||||
newestDaysAgo,
|
||||
},
|
||||
repeatCount: 0,
|
||||
relations: [],
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
const outcome = await runStatusCompletion({
|
||||
userId,
|
||||
cacheAction,
|
||||
@@ -379,8 +402,10 @@ export async function generateGeneralStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import { buildGradedSeriesFromPoints } from "@/lib/insights/graded-series";
|
||||
import { degradeStatusSnapshotToBudget } from "@/lib/insights/graded-series";
|
||||
@@ -350,6 +354,29 @@ export async function generateMedicationComplianceStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
|
||||
// Adherence spans multiple medications with no single card-level graded
|
||||
// mean to band, so the steady-run signal is left to the previous-context
|
||||
// comparison (repeatCount 0) and there is no measurement discovery channel
|
||||
// (relations empty); the variety lead + data-strength carry the rotation.
|
||||
const varietyLead = pickVarietyLead(
|
||||
userId,
|
||||
"medication-compliance",
|
||||
todayKey,
|
||||
);
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: medicationEvents.length,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
},
|
||||
repeatCount: 0,
|
||||
relations: [],
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
const outcome = await runStatusCompletion({
|
||||
userId,
|
||||
cacheAction,
|
||||
@@ -360,8 +387,10 @@ export async function generateMedicationComplianceStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
computeSteadyRun,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { pearsonCorrelation } from "@/lib/analytics/correlations";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
@@ -456,6 +461,26 @@ export async function generateMoodStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
|
||||
// MOOD is mood-entry backed (not a MeasurementType), so the cross-metric
|
||||
// RELATIONS sub-block is sourced from the snapshot's own crossMetricContext
|
||||
// rather than the measurement discovery engine; relations stays empty here
|
||||
// and the variety / data-strength / repetition signals carry the rotation.
|
||||
const varietyLead = pickVarietyLead(userId, "mood", todayKey);
|
||||
const steadyRun = computeSteadyRun(moodGraded.weekly, moodGraded.monthly);
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: moodSeries.daily.length,
|
||||
newestDaysAgo: newestEntryDaysAgo,
|
||||
},
|
||||
repeatCount: steadyRun,
|
||||
relations: [],
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
const outcome = await runStatusCompletion({
|
||||
userId,
|
||||
cacheAction,
|
||||
@@ -466,8 +491,10 @@ export async function generateMoodStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
computeSteadyRun,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context";
|
||||
import {
|
||||
getAgeFromDateOfBirth,
|
||||
getPersonalizedPulseTarget,
|
||||
@@ -281,6 +287,23 @@ export async function generatePulseStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
|
||||
const varietyLead = pickVarietyLead(userId, "pulse", todayKey);
|
||||
const steadyRun = computeSteadyRun(pulseGraded.weekly, pulseGraded.monthly);
|
||||
const relations = await getRelevantCorrelationsForMetric(userId, "PULSE");
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: pulseSeries.daily.length,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
},
|
||||
repeatCount: steadyRun,
|
||||
relations,
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
const outcome = await runStatusCompletion({
|
||||
userId,
|
||||
cacheAction,
|
||||
@@ -291,8 +314,10 @@ export async function generatePulseStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ import {
|
||||
formatPreviousContextForPrompt,
|
||||
getPreviousInsightContext,
|
||||
} from "@/lib/insights/memory";
|
||||
import {
|
||||
buildAssessmentContextBlock,
|
||||
computeSteadyRun,
|
||||
pickVarietyLead,
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context";
|
||||
import {
|
||||
applyPayloadBudget,
|
||||
dayOffsetToBerlinDayKey,
|
||||
@@ -429,6 +435,23 @@ export async function generateWeightStatusForUser(
|
||||
locale,
|
||||
);
|
||||
|
||||
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
|
||||
const varietyLead = pickVarietyLead(userId, "weight", todayKey);
|
||||
const steadyRun = computeSteadyRun(weightGraded.weekly, weightGraded.monthly);
|
||||
const relations = await getRelevantCorrelationsForMetric(userId, "WEIGHT");
|
||||
const assessmentContextBlock = buildAssessmentContextBlock(
|
||||
{
|
||||
varietyLead,
|
||||
dataStrength: {
|
||||
points: weightSeries.daily.length,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
},
|
||||
repeatCount: steadyRun,
|
||||
relations,
|
||||
},
|
||||
locale,
|
||||
);
|
||||
|
||||
// Run the provider chain bounded by the aligned 60 s budget. A
|
||||
// timeout / error / empty content is a transient miss — serve the
|
||||
// fallback for this render without persisting it, so the next mount
|
||||
@@ -443,8 +466,10 @@ export async function generateWeightStatusForUser(
|
||||
todayKey,
|
||||
locale,
|
||||
previousContextBlock,
|
||||
assessmentContextBlock,
|
||||
),
|
||||
temperature: 0.3,
|
||||
// v1.12.7 — match the archetype cards' 0.45.
|
||||
temperature: 0.45,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user