mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(insights): QA fixes — paired BP targets, stdDev, pulse pressure, personal tone, schema enrichment
- Fix pctInTarget to use paired-reading logic (both sys AND dia must be in range simultaneously) - Add sdSys30/sdDia30 (BP standard deviation) and pulsePressure30 (arterial stiffness marker) - Add mood scale descriptor for AI context - Extend medication data with compliance90, 30-day streak window, and category field - Rewrite base system prompt: personal advisor tone, second person, positive-first, "One Thing" pattern - Add conditional correlation instructions to all domain prompts (only mention when |r| > 0.4) - Enrich InsightResult schema with insightType, classificationLabel, primaryRecommendation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
64c72d241c
commit
7b5fabe46c
9 files changed
+89
-28
No files matched your search
@@ -1,4 +1,15 @@
|
||||
export const BASE_SYSTEM_PROMPT = `Du bist ein klinischer Gesundheitsanalyst mit Expertise in Innerer Medizin und Präventivmedizin. Deine Analysen basieren auf aktuellen medizinischen Leitlinien (ESC/ESH 2023, WHO, DGE, DEGAM).
|
||||
export const BASE_SYSTEM_PROMPT = `Du bist ein persönlicher Gesundheitsanalyst, der die Daten dieses Nutzers kennt. Deine Expertise umfasst Innere Medizin und Präventivmedizin. Deine Analysen basieren auf aktuellen medizinischen Leitlinien (ESC/ESH 2023, WHO, DGE, DEGAM), aber du beziehst dich immer auf die individuellen Werte und die persönliche Baseline des Nutzers.
|
||||
|
||||
TONALITÄT UND ANSPRACHE:
|
||||
- Verwende die zweite Person: "dein Blutdruck", "deine Werte", "dein Gewicht".
|
||||
- Beginne mit positiven Befunden, bevor du auf Bedenken eingehst.
|
||||
- Beziehe dich auf die eigene Baseline des Nutzers, nicht auf Bevölkerungsnormen (z.B. "dein systolischer Wert liegt 5 mmHg unter deinem 90-Tage-Durchschnitt" statt "unter dem Bevölkerungsdurchschnitt").
|
||||
- Formuliere eine zentrale Handlungsempfehlung ("One Thing") als wichtigste nächste Aktion.
|
||||
|
||||
DENKSCHRITTE (intern anwenden, nicht im Output zeigen):
|
||||
1. Was hat sich verändert? (Vergleiche 7d vs. 30d vs. 90d vs. allTime)
|
||||
2. Warum? (Korrelationen, Medikation, Stimmung, saisonale Muster)
|
||||
3. Was tun? (Eine primäre Empfehlung + ergänzende Vorschläge)
|
||||
|
||||
GRUNDREGELN:
|
||||
- Evidenzbasiert: Referenziere Grenzwerte und Leitlinien explizit bei Bewertungen.
|
||||
@@ -14,9 +25,11 @@ GRUNDREGELN:
|
||||
|
||||
KORRELATIONSANALYSE:
|
||||
- Du erhältst vorberechnete Pearson-Korrelationen zwischen Metriken.
|
||||
- Nur erwähnen wenn die Korrelation (r-Wert) im Snapshot vorhanden und |r| > 0.4 ist.
|
||||
- Falls das Feld nicht im Snapshot vorhanden ist, keine Korrelation interpretieren oder erfinden.
|
||||
- r > 0.7: starke Korrelation — klinisch relevant, detailliert kommentieren
|
||||
- r 0.4-0.7: moderate Korrelation — erwähnen, vorsichtig interpretieren
|
||||
- r < 0.4: schwache/keine Korrelation — nicht überinterpretieren
|
||||
- r < 0.4: schwache/keine Korrelation — nicht erwähnen
|
||||
- Korrelation ≠ Kausalität: immer als "Zusammenhang" formulieren, nicht als "Ursache"
|
||||
|
||||
HISTORISCHER VERGLEICH:
|
||||
@@ -28,8 +41,8 @@ HISTORISCHER VERGLEICH:
|
||||
* Puls: ±5 bpm relevant
|
||||
|
||||
STIMMUNGSDATEN:
|
||||
- Stimmung (1-5 Skala: 1=LAUSIG, 5=SUPER_GUT) als kontextuellen Faktor einbeziehen
|
||||
- Korrelation mit Vitalwerten nur erwähnen wenn statistisch auffällig (r > 0.3)
|
||||
- Stimmung (1-5 Skala: 1=LAUSIG, 2=SCHLECHT, 3=OKAY, 4=GUT, 5=SUPER_GUT) als kontextuellen Faktor einbeziehen
|
||||
- Korrelation mit Vitalwerten nur erwähnen wenn im Snapshot vorhanden und |r| > 0.4
|
||||
- Stress/Stimmung beeinflusst nachweislich Blutdruck und Herzfrequenz
|
||||
|
||||
TEMPORALE SCHICHTEN:
|
||||
@@ -39,11 +52,14 @@ TEMPORALE SCHICHTEN:
|
||||
|
||||
AUSGABEFORMAT: Antworte ausschließlich mit validem JSON im folgenden Schema:
|
||||
{
|
||||
"summary": "2-3 Sätze Gesamtbewertung",
|
||||
"insightType": "blood_pressure|weight|pulse|mood|bmi|medication_compliance|general",
|
||||
"summary": "2-3 Sätze Gesamtbewertung (in zweiter Person, positiv zuerst)",
|
||||
"classification": "optimal|gut|grenzwertig|erhoht|kritisch",
|
||||
"classificationLabel": "Menschenlesbare deutsche Bezeichnung (z.B. 'Adipositas Grad II', 'Hochnormal', 'Bradykardie')",
|
||||
"findings": [{"label": "...", "value": "...", "assessment": "positive|neutral|attention|warning", "guideline": "..."}],
|
||||
"correlations": [{"factor": "...", "effect": "...", "confidence": "hoch|mittel|gering"}],
|
||||
"recommendations": ["..."],
|
||||
"primaryRecommendation": "DIE eine wichtigste Handlungsempfehlung (max 20 Wörter)",
|
||||
"recommendations": ["2-3 ergänzende Vorschläge"],
|
||||
"dataQuality": {"coverage": "...", "gaps": ["..."], "confidence": "hoch|mittel|gering"},
|
||||
"disclaimer": "Diese Analyse ersetzt keine ärztliche Beratung. Bei Beschwerden oder auffälligen Werten konsultieren Sie Ihren Arzt."
|
||||
"disclaimer": "Diese Analyse ersetzt keine ärztliche Beratung. Bei Beschwerden oder auffälligen Werten konsultiere deinen Arzt."
|
||||
}`;
|
||||
@@ -17,9 +17,9 @@ FACHSPEZIFISCH — BLUTDRUCK:
|
||||
- Pulsdruck: (Systolisch - Diastolisch) > 60 mmHg als Marker für arterielle Steifigkeit bewerten.
|
||||
- Medikamenten-Korrelation: Einnahmetreue von Antihypertensiva mit Blutdruckverlauf korrelieren.
|
||||
- Gewichts-Korrelation: Gewichtstrend mit Blutdruckverlauf vergleichen (pro kg Gewichtsverlust ca. 1 mmHg Senkung erwartet).
|
||||
- Stimmungs-Korrelation: Falls Stimmungsdaten verfügbar, Zusammenhang mit Blutdruckvariabilität prüfen.
|
||||
- Analysiere die weightVsSystolic-Korrelation: Pro kg Gewichtsreduktion kann 1 mmHg systolische Senkung erwartet werden. Bewerte ob der berechnete Zusammenhang diesem klinischen Modell entspricht.
|
||||
- Analysiere die weightVsDiastolic-Korrelation analog und bewerte die klinische Plausibilität.
|
||||
- Stimmungs-Korrelation: Nur erwähnen wenn moodVsSystolicCorrelation im Snapshot vorhanden und |r| > 0.4. Falls nicht vorhanden, keine Korrelation interpretieren.
|
||||
- weightVsSystolic-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4. Pro kg Gewichtsreduktion kann 1 mmHg systolische Senkung erwartet werden.
|
||||
- weightVsDiastolic-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4.
|
||||
- Vergleiche avgSys30 und avgSys90 sowie allTimeAvg um langfristige Trends zu erkennen.
|
||||
- Nutze historicalComparison.systolic und historicalComparison.diastolic: Bei ≥5 mmHg systolischer bzw. ≥3 mmHg diastolischer Veränderung klinisch bewerten.`;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ FACHSPEZIFISCH — GESAMTBEWERTUNG:
|
||||
- Medikamenten-Adhärenz: Gesamteinnahmetreue bewerten und Zusammenhang mit Vitalwerten benennen.
|
||||
- Stimmung: Falls vorhanden, als kontextuellen Faktor einbeziehen (nicht übergewichten).
|
||||
- Positive Entwicklungen explizit hervorheben — Motivation ist therapeutisch relevant.
|
||||
- Nutze die correlations-Daten um Zusammenhänge zwischen Gewicht, Blutdruck, Puls und Stimmung zu identifizieren. Vergleiche die aktuellen 7-Tage-Werte mit den Langzeit-Durchschnitten (avg90, allTime).
|
||||
- Korrelationen nur erwähnen wenn der jeweilige r-Wert im Snapshot vorhanden und |r| > 0.4 ist. Falls ein Korrelationsfeld nicht im Snapshot vorhanden ist, keine Korrelation interpretieren oder erfinden. Vergleiche die aktuellen 7-Tage-Werte mit den Langzeit-Durchschnitten (avg90, allTime).
|
||||
- Nutze historicalComparison um aktuelle Veränderungen gegenüber der etablierten Baseline einzuordnen.
|
||||
- Falls Alter und Geschlecht bekannt sind, alters- und geschlechtsspezifische Risikobewertung anwenden.`;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ FACHSPEZIFISCH — MEDIKAMENTEN-ADHÄRENZ:
|
||||
- Verpasste Dosen: Konsequenzen differenzieren (Antihypertensiva-Rebound vs. Statine = weniger zeitkritisch).
|
||||
- Motivierende Gesprächsführung: Positive Formulierungen bevorzugen, Fortschritte anerkennen.
|
||||
- Vergleiche Perioden hoher Adhärenz (>90%) mit den zugehörigen Vitalwerten. Zeige konkret: "In Wochen mit >90% Einnahmetreue war der systolische RR X mmHg niedriger."
|
||||
- Nutze die vorberechneten Korrelationen (correlations) und historicalComparison um den Einfluss der Adhärenz auf Blutdruck und Puls zu quantifizieren.
|
||||
- Falls Stimmungsdaten verfügbar: Prüfe ob niedrige Adhärenz mit schlechterer Stimmung korreliert.`;
|
||||
- Korrelationen nur erwähnen wenn der r-Wert im Snapshot vorhanden und |r| > 0.4 ist. Falls das Feld nicht im Snapshot vorhanden ist, keine Korrelation interpretieren oder erfinden.
|
||||
- Nutze historicalComparison um den Einfluss der Adhärenz auf Blutdruck und Puls zu quantifizieren, sofern die Daten im Snapshot vorhanden sind.
|
||||
- Falls Stimmungsdaten verfügbar und Korrelation |r| > 0.4: Prüfe ob niedrige Adhärenz mit schlechterer Stimmung korreliert.`;
|
||||
}
|
||||
|
||||
export function getMedicationComplianceUserPrompt(snapshotJson: string, todayKey: string): string {
|
||||
|
||||
@@ -9,7 +9,7 @@ FACHSPEZIFISCH -- STIMMUNG/WOHLBEFINDEN:
|
||||
- Anhaltende Phasen: Mehr als 3 Tage unter 2.5 oder uber 4.5 sind auffallig.
|
||||
- Zusammenhange: Stimmung korreliert haufig mit Schlaf, Aktivitat, Blutdruck und Medikamenten-Compliance.
|
||||
- Tags: Falls Stimmungs-Tags vorhanden sind, prufe ob bestimmte Tags mit Stimmungslevels korrelieren.
|
||||
- Cross-Metrik: Falls Kontext zu Gewicht, Blutdruck oder Puls vorhanden ist und eine auffaellige Korrelation zeigt, kurz erwahnen.
|
||||
- Cross-Metrik: Korrelationen zu Gewicht, Blutdruck oder Puls nur erwähnen wenn der jeweilige r-Wert im Snapshot vorhanden und |r| > 0.4 ist. Falls das Feld nicht im Snapshot vorhanden ist, keine Korrelation interpretieren oder erfinden.
|
||||
- Erzwinge keine Querverweise wenn kein klares Muster erkennbar ist.`;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ FACHSPEZIFISCH — PULS/HERZFREQUENZ:
|
||||
- Erholung: Sinkender Ruhepuls über Wochen deutet auf verbesserte kardiovaskuläre Fitness hin.
|
||||
- Medikamenten-Einfluss: Betablocker senken die Herzfrequenz — Compliance-Korrelation prüfen.
|
||||
- Blutdruck-Korrelation: Puls × systolischen Druck = Doppelprodukt als Herzbelastungsindikator.
|
||||
- Stimmungs-Korrelation: Chronischer Stress kann Ruhepuls erhöhen.
|
||||
- Nutze die moodVsPulse-Korrelation um Stress-Einfluss auf den Ruhepuls einzuschätzen. Bei r > 0.3 (negative Korrelation = schlechtere Stimmung → höherer Puls) den Zusammenhang klinisch einordnen.
|
||||
- Nutze die pulseVsSystolic-Korrelation für die Bewertung der hämodynamischen Kopplung.
|
||||
- Stimmungs-Korrelation: Chronischer Stress kann Ruhepuls erhöhen. Nur erwähnen wenn moodVsPulse-Korrelation im Snapshot vorhanden und |r| > 0.4.
|
||||
- moodVsPulse-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4. Falls nicht vorhanden, keine Korrelation interpretieren.
|
||||
- pulseVsSystolic-Korrelation: Nur analysieren wenn im Snapshot vorhanden und |r| > 0.4. Bewertung der hämodynamischen Kopplung.
|
||||
- Vergleiche avg7 vs avg30 vs avg90 vs allTimeAvg um kurzfristige Abweichungen von der Langzeit-Baseline zu erkennen.
|
||||
- Nutze historicalComparison.pulse: Bei ≥5 bpm Veränderung gegenüber der Baseline klinisch bewerten.`;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ FACHSPEZIFISCH — GEWICHT:
|
||||
- Trend-Analyse: 7-Tage, 30-Tage und 90-Tage gleitende Durchschnitte vergleichen.
|
||||
- Plateau-Erkennung: Gewichtsveränderung < ±0.5 kg über > 14 Tage als Plateau identifizieren.
|
||||
- Realistische Zielprojektion: Maximal 0.5-1.0 kg/Woche als nachhaltiger Gewichtsverlust (DGE-Empfehlung).
|
||||
- Gewichts-Blutdruck-Korrelation: Pro kg Gewichtsreduktion ca. 1 mmHg systolische Senkung.
|
||||
- Gewichts-Blutdruck-Korrelation: Nur erwähnen wenn weightVsSystolic im Snapshot vorhanden und |r| > 0.4. Pro kg Gewichtsreduktion ca. 1 mmHg systolische Senkung.
|
||||
- Medikamenten-Einfluss: Gewichtsrelevante Medikamente identifizieren (z.B. Betablocker, Cortison).
|
||||
- Tageszeit-Schwankungen: Morgen- vs. Abendmessungen differenzieren (1-2 kg normal).
|
||||
- Vergleiche das aktuelle 7-Tage-Mittel (historicalComparison.weight.current7dAvg) mit dem 30-Tage-Baseline (previous30dAvg). Bei >2 kg Differenz: klinisch bewerten und Ursachen diskutieren.
|
||||
|
||||
@@ -22,10 +22,13 @@ export const insightDataQualitySchema = z.object({
|
||||
});
|
||||
|
||||
export const insightResultSchema = z.object({
|
||||
insightType: z.string().optional(),
|
||||
summary: z.string(),
|
||||
classification: z.enum(["optimal", "gut", "grenzwertig", "erhoht", "kritisch"]),
|
||||
classificationLabel: z.string().optional(),
|
||||
findings: z.array(insightFindingSchema),
|
||||
correlations: z.array(insightCorrelationSchema),
|
||||
primaryRecommendation: z.string().optional(),
|
||||
recommendations: z.array(z.string()),
|
||||
dataQuality: insightDataQualitySchema,
|
||||
disclaimer: z.string(),
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
pearsonCorrelation,
|
||||
type CorrelationResult,
|
||||
} from "@/lib/analytics/correlations";
|
||||
import { getMedicationCategories } from "@/lib/medication-category";
|
||||
|
||||
interface DataCoverage {
|
||||
count: number;
|
||||
@@ -49,6 +50,9 @@ export interface AggregatedFeatures {
|
||||
allTimeMaxDia: number | null;
|
||||
slopeSys30: number | null;
|
||||
slopeDia30: number | null;
|
||||
sdSys30: number | null;
|
||||
sdDia30: number | null;
|
||||
pulsePressure30: number | null;
|
||||
pctInTarget: number | null;
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
@@ -70,6 +74,7 @@ export interface AggregatedFeatures {
|
||||
coverage: DataCoverage;
|
||||
};
|
||||
mood?: {
|
||||
scale: string;
|
||||
avg7: number | null;
|
||||
avg30: number | null;
|
||||
latest: number | null;
|
||||
@@ -109,8 +114,10 @@ export interface AggregatedFeatures {
|
||||
medications?: Array<{
|
||||
name: string;
|
||||
dose: string;
|
||||
category: string;
|
||||
compliance7: number;
|
||||
compliance30: number;
|
||||
compliance90: number;
|
||||
streak: number;
|
||||
missedLast7: number;
|
||||
}>;
|
||||
@@ -134,6 +141,13 @@ export interface RawFeatures extends AggregatedFeatures {
|
||||
}>;
|
||||
}
|
||||
|
||||
function stdDev(values: number[]): number | null {
|
||||
if (values.length < 2) return null;
|
||||
const mean = values.reduce((s, v) => s + v, 0) / values.length;
|
||||
const variance = values.reduce((s, v) => s + (v - mean) ** 2, 0) / (values.length - 1);
|
||||
return Math.round(Math.sqrt(variance) * 10) / 10;
|
||||
}
|
||||
|
||||
function toDataPoints(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
): DataPoint[] {
|
||||
@@ -309,16 +323,19 @@ export async function extractFeatures(
|
||||
|
||||
let pctInTarget: number | null = null;
|
||||
if (bpTargets) {
|
||||
const sysInRange = sysData.filter(
|
||||
(m) => m.value >= bpTargets.sysLow && m.value <= bpTargets.sysHigh,
|
||||
).length;
|
||||
const diaInRange = diaData.filter(
|
||||
(m) => m.value >= bpTargets.diaLow && m.value <= bpTargets.diaHigh,
|
||||
).length;
|
||||
const total = sysData.length + diaData.length;
|
||||
if (total > 0) {
|
||||
pctInTarget = Math.round(((sysInRange + diaInRange) / total) * 100);
|
||||
const sysByTime = new Map(sysData.map(m => [m.measuredAt.getTime(), m.value]));
|
||||
let inTargetCount = 0;
|
||||
let pairedCount = 0;
|
||||
for (const dia of diaData) {
|
||||
const sysVal = sysByTime.get(dia.measuredAt.getTime());
|
||||
if (sysVal === undefined) continue;
|
||||
pairedCount++;
|
||||
if (sysVal >= bpTargets.sysLow && sysVal <= bpTargets.sysHigh &&
|
||||
dia.value >= bpTargets.diaLow && dia.value <= bpTargets.diaHigh) {
|
||||
inTargetCount++;
|
||||
}
|
||||
}
|
||||
pctInTarget = pairedCount > 0 ? Math.round((inTargetCount / pairedCount) * 100) : null;
|
||||
}
|
||||
|
||||
features.bloodPressure = {
|
||||
@@ -334,6 +351,22 @@ export async function extractFeatures(
|
||||
allTimeMaxDia: diaSummary?.count ? diaSummary.max : null,
|
||||
slopeSys30: sysSummary?.slope30?.slope ?? null,
|
||||
slopeDia30: diaSummary?.slope30?.slope ?? null,
|
||||
sdSys30: (() => {
|
||||
const fromMs = now - 30 * 24 * 60 * 60 * 1000;
|
||||
const vals = sysData.filter(m => m.measuredAt.getTime() >= fromMs).map(m => m.value);
|
||||
return stdDev(vals);
|
||||
})(),
|
||||
sdDia30: (() => {
|
||||
const fromMs = now - 30 * 24 * 60 * 60 * 1000;
|
||||
const vals = diaData.filter(m => m.measuredAt.getTime() >= fromMs).map(m => m.value);
|
||||
return stdDev(vals);
|
||||
})(),
|
||||
pulsePressure30: (() => {
|
||||
const avgSys = sysSummary?.avg30 ?? null;
|
||||
const avgDia = diaSummary?.avg30 ?? null;
|
||||
if (avgSys === null || avgDia === null) return null;
|
||||
return Math.round((avgSys - avgDia) * 10) / 10;
|
||||
})(),
|
||||
pctInTarget,
|
||||
coverage: computeCoverage(
|
||||
[...sysData, ...diaData].sort(
|
||||
@@ -425,6 +458,7 @@ export async function extractFeatures(
|
||||
: null;
|
||||
|
||||
features.mood = {
|
||||
scale: "1=LAUSIG, 2=SCHLECHT, 3=OKAY, 4=GUT, 5=SUPER_GUT",
|
||||
avg7: avg(last7),
|
||||
avg30: avg(last30),
|
||||
latest: moodEntries[moodEntries.length - 1].score,
|
||||
@@ -487,6 +521,10 @@ export async function extractFeatures(
|
||||
});
|
||||
|
||||
if (medications.length > 0) {
|
||||
const categoryMap = await getMedicationCategories(
|
||||
medications.map((med) => med.id),
|
||||
);
|
||||
|
||||
features.medications = [];
|
||||
for (const med of medications) {
|
||||
const events = await prisma.medicationIntakeEvent.findMany({
|
||||
@@ -500,13 +538,16 @@ export async function extractFeatures(
|
||||
}));
|
||||
const c7 = calculateCompliance(mapped, med.schedules, 7, med.createdAt);
|
||||
const c30 = calculateCompliance(mapped, med.schedules, 30, med.createdAt);
|
||||
const c90 = calculateCompliance(mapped, med.schedules, 90, med.createdAt);
|
||||
|
||||
features.medications.push({
|
||||
name: med.name,
|
||||
dose: med.dose,
|
||||
category: categoryMap[med.id] ?? "OTHER",
|
||||
compliance7: c7.rate,
|
||||
compliance30: c30.rate,
|
||||
streak: c7.streak,
|
||||
compliance90: c90.rate,
|
||||
streak: c30.streak,
|
||||
missedLast7: c7.missed,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user