diff --git a/src/lib/ai/prompts/base-system.ts b/src/lib/ai/prompts/base-system.ts index 8271c6071..0ec607cc1 100644 --- a/src/lib/ai/prompts/base-system.ts +++ b/src/lib/ai/prompts/base-system.ts @@ -32,6 +32,17 @@ KORRELATIONSANALYSE: - r < 0.4: schwache/keine Korrelation — nicht erwähnen - Korrelation ≠ Kausalität: immer als "Zusammenhang" formulieren, nicht als "Ursache" +ERWEITERTE METRIKEN: +- ratePressureProduct.rpp30: Puls × systolischer RR. Normal: 7.000-10.000. > 12.000: erhöhter myokardialer Sauerstoffbedarf. + * Wenn beide steigen ("Double Jeopardy"): höchstes Risiko + * Wenn nur Puls steigt (RR stabil): Stress/Dekonditionierung + * Wenn nur RR steigt (Puls stabil): Gefäßwiderstand +- bodyCompositionDivergence.flag: Gewicht stabil + Körperfett steigt = stille Muskelmasse-Abnahme (sarkopenische Adipositas-Frühzeichen). +- moodAdherenceRisk: Stimmung ≤ 2.5 über 7 Tage + fallend = Adhärenz-Einbruch in den nächsten 5 Tagen wahrscheinlich. Proaktiv ansprechen. +- seasonalVariation: Winter-Sommer-Differenz des systolischen RR. > 5 mmHg ist physiologisch normal. Den User beruhigen — dies ist keine Verschlechterung. +- sleep: Zielwert ≥ 7h/Nacht (ESC). < 6h: Risikofaktor für Hypertonie und Gewichtszunahme. +- activity: WHO-Ziel ≥ 8.000 Schritte/Tag. + HISTORISCHER VERGLEICH: - Vergleiche aktuelle 7-Tage-Werte mit dem 30-Tage-Durchschnitt der Vorperiode - Klinisch relevante Veränderungen benennen: diff --git a/src/lib/ai/prompts/blood-pressure.ts b/src/lib/ai/prompts/blood-pressure.ts index 9a6ae0f59..f54697984 100644 --- a/src/lib/ai/prompts/blood-pressure.ts +++ b/src/lib/ai/prompts/blood-pressure.ts @@ -21,7 +21,15 @@ FACHSPEZIFISCH — BLUTDRUCK: - 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.`; +- Nutze historicalComparison.systolic und historicalComparison.diastolic: Bei ≥5 mmHg systolischer bzw. ≥3 mmHg diastolischer Veränderung klinisch bewerten. +- Morgen-Risikoleiter (J-HOP Studie): + * Morgen-RR 135-144: Schlaganfall-HR 2.45 + * Morgen-RR 145-154: HR 2.80 + * Morgen-RR 155-164: HR 3.58 + * Morgen-RR ≥ 165: HR 6.52 +- Rate-Pressure Product: Wenn ratePressureProduct.rpp30 > 12.000: "Erhöhter kardialer Sauerstoffbedarf" mit assessment "warning" bewerten. +- Saisonale Variation: Falls seasonalVariation vorhanden und delta > 5 mmHg: "Physiologisch normale saisonale Schwankung — kein Grund zur Sorge, ggf. Winter-Dosisanpassung besprechen." +- Salz-Signal: Akuter Gewichtsanstieg ≥ 1 kg in 3 Tagen + systolischer Anstieg ≥ 5 mmHg = mögliche erhöhte Natriumzufuhr.`; } export function getBloodPressureUserPrompt(snapshotJson: string, todayKey: string): string { diff --git a/src/lib/ai/prompts/general-status.ts b/src/lib/ai/prompts/general-status.ts index 9ed0e8fec..663b270ed 100644 --- a/src/lib/ai/prompts/general-status.ts +++ b/src/lib/ai/prompts/general-status.ts @@ -13,7 +13,12 @@ FACHSPEZIFISCH — GESAMTBEWERTUNG: - Positive Entwicklungen explizit hervorheben — Motivation ist therapeutisch relevant. - 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.`; +- Falls Alter und Geschlecht bekannt sind, alters- und geschlechtsspezifische Risikobewertung anwenden. +- Schlaf: Falls sleep-Daten vorhanden, Schlafqualität in die Gesamtbewertung einbeziehen. < 6h/Nacht als Risikofaktor für Hypertonie und Gewichtszunahme benennen. +- Aktivität: Falls activity-Daten vorhanden, Schrittzahl bewerten (WHO-Ziel ≥ 8.000/Tag). Zusammenhang mit Puls- und Gewichtstrend herstellen. +- Rate-Pressure Product: Falls ratePressureProduct vorhanden, als Indikator für kardiale Belastung in die Risikostratifizierung einbeziehen. > 12.000 = erhöhter myokardialer Sauerstoffbedarf. +- Body-Composition-Divergenz: Falls bodyCompositionDivergence.flag = true, als Frühzeichen für sarkopenische Adipositas in die Gesamtbewertung aufnehmen. +- Saisonale Variation: Falls seasonalVariation vorhanden, saisonale Blutdruckschwankungen kontextualisieren und ggf. beruhigend einordnen.`; } export function getGeneralStatusUserPrompt(snapshotJson: string, todayKey: string): string { diff --git a/src/lib/ai/prompts/medication-compliance.ts b/src/lib/ai/prompts/medication-compliance.ts index a5a0f60b1..e9e582ac5 100644 --- a/src/lib/ai/prompts/medication-compliance.ts +++ b/src/lib/ai/prompts/medication-compliance.ts @@ -19,7 +19,9 @@ FACHSPEZIFISCH — MEDIKAMENTEN-ADHÄRENZ: - 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." - 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.`; +- Falls Stimmungsdaten verfügbar und Korrelation |r| > 0.4: Prüfe ob niedrige Adhärenz mit schlechterer Stimmung korreliert. +- Chronotherapie-Hinweis: Falls ein Blutdruck-Medikament vorhanden ist UND Compliance > 90% ABER BP nicht im Zielbereich: "Einnahme-Zeitpunkt mit dem Arzt besprechen — abendliche Einnahme kann bei einigen Patienten die nächtliche Blutdruckkontrolle verbessern." +- Mood-Adhärenz-Risiko: Falls moodAdherenceRisk = true: "Deine Stimmung war in den letzten Tagen niedrig. Erfahrungsgemäß kann das die Einnahmetreue in den kommenden Tagen beeinflussen. Tipp: Lege die Medikamente abends schon bereit."`; } export function getMedicationComplianceUserPrompt(snapshotJson: string, todayKey: string): string { diff --git a/src/lib/ai/prompts/pulse.ts b/src/lib/ai/prompts/pulse.ts index f55c08de5..104af9c51 100644 --- a/src/lib/ai/prompts/pulse.ts +++ b/src/lib/ai/prompts/pulse.ts @@ -18,7 +18,13 @@ FACHSPEZIFISCH — PULS/HERZFREQUENZ: - 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.`; +- Nutze historicalComparison.pulse: Bei ≥5 bpm Veränderung gegenüber der Baseline klinisch bewerten. +- Fitness-Interpretation: + * slope30 < -0.2 bpm/Tag (≈ -6 bpm/Monat): Positives Fitness-Signal, kardiovaskuläre Verbesserung + * slope30 > +0.2 bpm/Tag (≈ +6 bpm/Monat) OHNE Stimmungsabfall: Mögliche Dekonditionierung + * slope30 > +0.2 bpm/Tag MIT moodVsPulse r > 0.4: Stressbedingte Pulserhöhung +- Risikoband 80-100 bpm: Technisch "normal", aber erhöhtes CV-Risiko (+6-9% Mortalität pro 10 bpm, Meta-Analyse). +- Resting HR > 90 bpm: Unabhängiger Mortalitätsprädiktor — als eigenes Finding mit assessment "warning" erfassen.`; } export function getPulseUserPrompt(snapshotJson: string, todayKey: string): string { diff --git a/src/lib/ai/prompts/weight.ts b/src/lib/ai/prompts/weight.ts index 890dee87e..55f425be7 100644 --- a/src/lib/ai/prompts/weight.ts +++ b/src/lib/ai/prompts/weight.ts @@ -14,7 +14,14 @@ FACHSPEZIFISCH — GEWICHT: - 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. - Plateau-Definition: <0.5 kg Veränderung über >14 Tage (prüfe ob avg7 ≈ avg30 ≈ avg90). -- Nutze allTimeMin/allTimeMax/allTimeAvg um den aktuellen Wert historisch einzuordnen.`; +- Nutze allTimeMin/allTimeMax/allTimeAvg um den aktuellen Wert historisch einzuordnen. +- Gewichtsmeilensteine (klinisch signifikant): + * 5% Verlust vom Startgewicht (allTimeMax): Metabolischer Benefit, KV-Risikoreduktion + * 10% Verlust: Erheblicher klinischer Nutzen + * Wenn erreicht: Explizit anerkennen und klinischen Kontext geben +- Zu schneller Verlust: > 1 kg/Woche über > 2 Wochen = Risiko für Muskelmasseverlust, nicht nachhaltig +- Body-Composition-Divergenz: Falls bodyCompositionDivergence.flag = true: "Gewicht stabil, aber Körperfettanteil steigend — möglicher Muskelmasseverlust. Krafttraining empfehlen." +- Schlaf-Gewicht-Verbindung: Falls sleep.avg7 < 6h UND weight.slope30 > 0: "Schlafmangel stört metabolische Regulation und kann Gewichtszunahme begünstigen."`; } export function getWeightUserPrompt(snapshotJson: string, todayKey: string): string { diff --git a/src/lib/insights/features.ts b/src/lib/insights/features.ts index ec59a367c..974875b7c 100644 --- a/src/lib/insights/features.ts +++ b/src/lib/insights/features.ts @@ -82,12 +82,44 @@ export interface AggregatedFeatures { totalEntries: number; coverage: DataCoverage; }; + sleep?: { + avg7: number | null; + avg30: number | null; + latest: number | null; + coverage: DataCoverage; + }; + activity?: { + avg7: number | null; + avg30: number | null; + latest: number | null; + coverage: DataCoverage; + }; + ratePressureProduct?: { + rpp7: number | null; + rpp30: number | null; + risk: 'normal' | 'elevated' | null; + }; + bodyCompositionDivergence?: { + weightStable: boolean; + bodyFatRising: boolean; + flag: boolean; + }; + moodAdherenceRisk?: boolean; + seasonalVariation?: { + winterAvgSys: number | null; + summerAvgSys: number | null; + delta: number | null; + significance: 'normal' | 'elevated' | null; + }; correlations?: { weightVsSystolic: CorrelationResult | null; weightVsDiastolic: CorrelationResult | null; pulseVsSystolic: CorrelationResult | null; moodVsPulse: CorrelationResult | null; moodVsSystolic: CorrelationResult | null; + moodVsWeight: CorrelationResult | null; + sleepVsPulse: CorrelationResult | null; + sleepVsSystolic: CorrelationResult | null; }; historicalComparison?: { weight?: { @@ -406,6 +438,30 @@ export async function extractFeatures( }; } + // Sleep Duration + const sleepData = byType("SLEEP_DURATION"); + if (sleepData.length > 0) { + const summary = summarize(toDataPoints(sleepData)); + features.sleep = { + avg7: summary.avg7, + avg30: summary.avg30, + latest: summary.latest, + coverage: computeCoverage(sleepData, now), + }; + } + + // Activity Steps + const activityData = byType("ACTIVITY_STEPS"); + if (activityData.length > 0) { + const summary = summarize(toDataPoints(activityData)); + features.activity = { + avg7: summary.avg7, + avg30: summary.avg30, + latest: summary.latest, + coverage: computeCoverage(activityData, now), + }; + } + // Mood const moodEntries = await prisma.moodEntry.findMany({ where: { userId }, @@ -491,14 +547,73 @@ export async function extractFeatures( const computeCorr = (a: DataPoint[], b: DataPoint[]) => pearsonCorrelation(pairByTimestamp(a, b)); + const sleepPoints = toDataPoints(sleepData); + features.correlations = { weightVsSystolic: computeCorr(weightPoints, sysPoints), weightVsDiastolic: computeCorr(weightPoints, diaPoints), pulseVsSystolic: computeCorr(pulsePoints, sysPoints), moodVsPulse: computeCorr(moodPoints, pulsePoints), moodVsSystolic: computeCorr(moodPoints, sysPoints), + moodVsWeight: computeCorr(moodPoints, weightPoints), + sleepVsPulse: sleepData.length > 0 ? computeCorr(sleepPoints, pulsePoints) : null, + sleepVsSystolic: sleepData.length > 0 ? computeCorr(sleepPoints, sysPoints) : null, }; + // Rate-Pressure Product (RPP) — myocardial oxygen demand indicator + if (features.pulse && features.bloodPressure) { + const rpp7 = + features.pulse.avg7 !== null && features.bloodPressure.avgSys30 !== null + ? Math.round(features.pulse.avg7 * (avgInWindow(sysData, now, 7) ?? features.bloodPressure.avgSys30)) + : null; + const rpp30 = + features.pulse.avg30 !== null && features.bloodPressure.avgSys30 !== null + ? Math.round(features.pulse.avg30 * features.bloodPressure.avgSys30) + : null; + const rppRef = rpp30 ?? rpp7; + features.ratePressureProduct = { + rpp7, + rpp30, + risk: rppRef !== null ? (rppRef > 12000 ? 'elevated' : 'normal') : null, + }; + } + + // Body Composition Divergence + if (features.weight && features.bodyFat) { + const weightStable = features.weight.slope30 !== null && Math.abs(features.weight.slope30) < 0.01; + const bodyFatRising = features.bodyFat.slope30 !== null && features.bodyFat.slope30 > 0; + features.bodyCompositionDivergence = { + weightStable, + bodyFatRising, + flag: weightStable && bodyFatRising, + }; + } + + // Mood-Adherence Risk Flag + if (features.mood && features.medications && features.medications.length > 0) { + features.moodAdherenceRisk = + features.mood.avg7 !== null && + features.mood.avg7 <= 2.5 && + features.mood.trend30 === 'declining'; + } + + // Seasonal BP Variation (only if > 180 days of data) + if (features.context.dataSpanDays > 180 && sysData.length > 0) { + const winterMonths = [11, 0, 1]; // Dec, Jan, Feb (0-indexed) + const summerMonths = [5, 6, 7]; // Jun, Jul, Aug + const winterVals = sysData.filter(m => winterMonths.includes(m.measuredAt.getMonth())).map(m => m.value); + const summerVals = sysData.filter(m => summerMonths.includes(m.measuredAt.getMonth())).map(m => m.value); + const winterAvg = winterVals.length > 0 ? Math.round((winterVals.reduce((s, v) => s + v, 0) / winterVals.length) * 10) / 10 : null; + const summerAvg = summerVals.length > 0 ? Math.round((summerVals.reduce((s, v) => s + v, 0) / summerVals.length) * 10) / 10 : null; + const delta = winterAvg !== null && summerAvg !== null ? Math.round((winterAvg - summerAvg) * 10) / 10 : null; + features.seasonalVariation = { + winterAvgSys: winterAvg, + summerAvgSys: summerAvg, + delta, + significance: delta !== null ? (Math.abs(delta) > 5 ? 'elevated' : 'normal') : null, + }; + } + // Historical comparison: current 7d avg vs previous 30d avg (days 7-37) features.historicalComparison = {}; if (weightData.length > 0) {