feat(insights): wire previous-analysis memory into the five remaining generators (#130)

Extends the v1.4 AI insights memory pattern established in PR #129
(general-status, blood-pressure-status) to the rest of the insight
surfaces. Each generator now reads its previous cached narrative via
`getPreviousInsightContext()` and injects the formatted block into the
user prompt so the model can call out improvements / regressions
relative to the user's last analysis instead of restating the status
from scratch every day.

Scopes wired in this commit:
- weight-status
- pulse-status
- bmi-status
- mood-status
- medication-compliance-status

No UI changes are needed — the insights card already renders the
comparison phrases the model emits. No schema change either; the
helper reads from the existing audit-log cache rows each generator
already writes to.

Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandMarc-André Bombeck authored and GitHub committed 2026-05-08 12:41:24 +02:00
1 parent 751448494b
commit 14cbc69a2d
10 files changed
+162 -28

No files matched your search

+7 -2
View File
@@ -40,15 +40,20 @@ export function getBmiUserPrompt(
snapshotJson: string,
todayKey: string,
locale: Locale,
previousContextBlock?: string,
): string {
const ctxBlock =
previousContextBlock && previousContextBlock.trim().length > 0
? `\n\n${previousContextBlock}\n`
: "";
if (locale === "en") {
return `Date: ${todayKey} (Europe/Berlin)
Analyse the BMI trajectory taking age, sex and weight trend into account. Place the WHO classification.
Analyse the BMI trajectory taking age, sex and weight trend into account. Place the WHO classification.${ctxBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey} (Europe/Berlin)
Analysiere den BMI-Verlauf unter Berücksichtigung von Alter, Geschlecht und Gewichtstrend. Ordne die Klassifikation nach WHO ein.
Analysiere den BMI-Verlauf unter Berücksichtigung von Alter, Geschlecht und Gewichtstrend. Ordne die Klassifikation nach WHO ein.${ctxBlock}
${snapshotJson}`;
}
+7 -2
View File
@@ -52,17 +52,22 @@ export function getMedicationComplianceUserPrompt(
snapshotJson: string,
todayKey: string,
locale: Locale,
previousContextBlock?: string,
): string {
const ctxBlock =
previousContextBlock && previousContextBlock.trim().length > 0
? `\n\n${previousContextBlock}\n`
: "";
if (locale === "en") {
return `Date: ${todayKey} (Europe/Berlin)
Analyse medication adherence with focus on patterns, effectiveness correlation and concrete suggestions for improvement.
Use the correlation data and historical comparison to back up the link between adherence and vital signs.
Use the correlation data and historical comparison to back up the link between adherence and vital signs.${ctxBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey} (Europe/Berlin)
Analysiere die Medikamenten-Einnahmetreue mit Fokus auf Muster, Wirksamkeitskorrelation und konkrete Verbesserungsvorschläge.
Nutze die Korrelationsdaten und den historischen Vergleich um den Zusammenhang zwischen Adhärenz und Vitalwerten zu belegen.
Nutze die Korrelationsdaten und den historischen Vergleich um den Zusammenhang zwischen Adhärenz und Vitalwerten zu belegen.${ctxBlock}
${snapshotJson}`;
}
+7 -2
View File
@@ -30,15 +30,20 @@ export function getMoodUserPrompt(
snapshotJson: string,
todayKey: string,
locale: Locale,
previousContextBlock?: string,
): string {
const ctxBlock =
previousContextBlock && previousContextBlock.trim().length > 0
? `\n\n${previousContextBlock}\n`
: "";
if (locale === "en") {
return `Date: ${todayKey} (Europe/Berlin)
Analyse the mood data with focus on trend, stability and links to other health metrics.
Analyse the mood data with focus on trend, stability and links to other health metrics.${ctxBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey} (Europe/Berlin)
Analysiere die Stimmungsdaten mit Fokus auf Trend, Stabilitat und Zusammenhange mit anderen Gesundheitsmetriken.
Analysiere die Stimmungsdaten mit Fokus auf Trend, Stabilitat und Zusammenhange mit anderen Gesundheitsmetriken.${ctxBlock}
${snapshotJson}`;
}
+7 -2
View File
@@ -58,17 +58,22 @@ export function getPulseUserPrompt(
snapshotJson: string,
todayKey: string,
locale: Locale,
previousContextBlock?: string,
): string {
const ctxBlock =
previousContextBlock && previousContextBlock.trim().length > 0
? `\n\n${previousContextBlock}\n`
: "";
if (locale === "en") {
return `Date: ${todayKey} (Europe/Berlin)
Analyse the pulse / heart-rate data with focus on resting-pulse trend, variability and links to medication and mood.
Use the precomputed correlations and historical comparison for a sound analysis.
Use the precomputed correlations and historical comparison for a sound analysis.${ctxBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey} (Europe/Berlin)
Analysiere die Puls-/Herzfrequenzdaten mit Fokus auf Ruhepulstrend, Variabilität und Zusammenhänge mit Medikation und Stimmung.
Nutze die vorberechneten Korrelationen und den historischen Vergleich für eine fundierte Analyse.
Nutze die vorberechneten Korrelationen und den historischen Vergleich für eine fundierte Analyse.${ctxBlock}
${snapshotJson}`;
}
+7 -2
View File
@@ -52,17 +52,22 @@ export function getWeightUserPrompt(
snapshotJson: string,
todayKey: string,
locale: Locale,
previousContextBlock?: string,
): string {
const ctxBlock =
previousContextBlock && previousContextBlock.trim().length > 0
? `\n\n${previousContextBlock}\n`
: "";
if (locale === "en") {
return `Date: ${todayKey} (Europe/Berlin)
Analyse the weight trajectory with focus on trends, BMI classification and links to other vital signs.
Use the temporal layers (avg7, avg30, avg90, allTime) and the historical comparison for a nuanced assessment.
Use the temporal layers (avg7, avg30, avg90, allTime) and the historical comparison for a nuanced assessment.${ctxBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey} (Europe/Berlin)
Analysiere die Gewichtsentwicklung mit Fokus auf Trends, BMI-Klassifikation und Zusammenhang mit anderen Vitalwerten.
Nutze die temporalen Schichten (avg7, avg30, avg90, allTime) und den historischen Vergleich für eine differenzierte Bewertung.
Nutze die temporalen Schichten (avg7, avg30, avg90, allTime) und den historischen Vergleich für eine differenzierte Bewertung.${ctxBlock}
${snapshotJson}`;
}
+21 -1
View File
@@ -3,6 +3,10 @@ import { resolveProvider } from "@/lib/ai/provider";
import { getBmiSystemPrompt, getBmiUserPrompt } from "@/lib/ai/prompts/bmi";
import { classifyBMI } from "@/lib/analytics/classifications";
import { getNoKeyBmiStatusText } from "@/lib/insights/no-key-fallbacks";
import {
formatPreviousContextForPrompt,
getPreviousInsightContext,
} from "@/lib/insights/memory";
const BMI_STATUS_POINTS = 30;
@@ -238,9 +242,25 @@ export async function generateBmiStatusForUser(
const snapshotJson = JSON.stringify(snapshot, null, 2);
const previousContext = await getPreviousInsightContext(
userId,
"bmi-status",
locale,
12,
);
const previousContextBlock = formatPreviousContextForPrompt(
previousContext,
locale,
);
const result = await provider.generateCompletion({
systemPrompt: getBmiSystemPrompt(locale),
userPrompt: getBmiUserPrompt(snapshotJson, todayKey, locale),
userPrompt: getBmiUserPrompt(
snapshotJson,
todayKey,
locale,
previousContextBlock,
),
temperature: 0.3,
maxTokens: 1000,
});
@@ -1,10 +1,17 @@
import { prisma } from "@/lib/db";
import { resolveProvider } from "@/lib/ai/provider";
import { getMedicationComplianceSystemPrompt, getMedicationComplianceUserPrompt } from "@/lib/ai/prompts/medication-compliance";
import {
getMedicationComplianceSystemPrompt,
getMedicationComplianceUserPrompt,
} from "@/lib/ai/prompts/medication-compliance";
import { calculateCompliance } from "@/lib/analytics/compliance";
import { getMedicationCategories } from "@/lib/medication-category";
import { sanitizeForPrompt } from "@/lib/insights/sanitize";
import { getNoKeyMedicationComplianceStatusText } from "@/lib/insights/no-key-fallbacks";
import {
formatPreviousContextForPrompt,
getPreviousInsightContext,
} from "@/lib/insights/memory";
const MEDICATION_COMPLIANCE_STATUS_POINTS = 30;
@@ -291,9 +298,25 @@ export async function generateMedicationComplianceStatusForUser(
const snapshotJson = JSON.stringify(snapshot, null, 2);
const previousContext = await getPreviousInsightContext(
userId,
"medication-compliance-status",
locale,
12,
);
const previousContextBlock = formatPreviousContextForPrompt(
previousContext,
locale,
);
const result = await provider.generateCompletion({
systemPrompt: getMedicationComplianceSystemPrompt(locale),
userPrompt: getMedicationComplianceUserPrompt(snapshotJson, todayKey, locale),
userPrompt: getMedicationComplianceUserPrompt(
snapshotJson,
todayKey,
locale,
previousContextBlock,
),
temperature: 0.3,
maxTokens: 1000,
});
+27 -10
View File
@@ -2,6 +2,10 @@ import { prisma } from "@/lib/db";
import { resolveProvider } from "@/lib/ai/provider";
import { getMoodSystemPrompt, getMoodUserPrompt } from "@/lib/ai/prompts/mood";
import { getNoKeyMoodStatusText } from "@/lib/insights/no-key-fallbacks";
import {
formatPreviousContextForPrompt,
getPreviousInsightContext,
} from "@/lib/insights/memory";
import {
pearsonCorrelation,
type PairedPoint,
@@ -212,8 +216,7 @@ export async function generateMoodStatusForUser(
? null
: round(
(moodSeries.filter(
(entry) =>
entry.value >= greenMin && entry.value <= greenMax,
(entry) => entry.value >= greenMin && entry.value <= greenMax,
).length /
moodSeries.length) *
100,
@@ -226,9 +229,7 @@ export async function generateMoodStatusForUser(
const oldestEntry = entries.length > 0 ? entries[0].moodLoggedAt : null;
const newestEntry =
entries.length > 0
? entries[entries.length - 1].moodLoggedAt
: null;
entries.length > 0 ? entries[entries.length - 1].moodLoggedAt : null;
const totalSpanDays =
oldestEntry && newestEntry
? Math.round(
@@ -237,9 +238,7 @@ export async function generateMoodStatusForUser(
)
: 0;
const newestEntryDaysAgo = newestEntry
? Math.round(
(Date.now() - newestEntry.getTime()) / (24 * 60 * 60 * 1000),
)
? Math.round((Date.now() - newestEntry.getTime()) / (24 * 60 * 60 * 1000))
: null;
// Fetch cross-metric context for enrichment
@@ -335,7 +334,9 @@ export async function generateMoodStatusForUser(
tags: tagSummary.length > 0 ? tagSummary : null,
},
crossMetricContext:
weightSeries.length >= 3 || sysSeries.length >= 3 || pulseSeries.length >= 3
weightSeries.length >= 3 ||
sysSeries.length >= 3 ||
pulseSeries.length >= 3
? {
weight:
weightSeries.length >= 3
@@ -364,9 +365,25 @@ export async function generateMoodStatusForUser(
const snapshotJson = JSON.stringify(snapshot, null, 2);
const previousContext = await getPreviousInsightContext(
userId,
"mood-status",
locale,
12,
);
const previousContextBlock = formatPreviousContextForPrompt(
previousContext,
locale,
);
const result = await provider.generateCompletion({
systemPrompt: getMoodSystemPrompt(locale),
userPrompt: getMoodUserPrompt(snapshotJson, todayKey, locale),
userPrompt: getMoodUserPrompt(
snapshotJson,
todayKey,
locale,
previousContextBlock,
),
temperature: 0.3,
maxTokens: 1000,
});
+25 -2
View File
@@ -1,6 +1,13 @@
import { prisma } from "@/lib/db";
import { resolveProvider } from "@/lib/ai/provider";
import { getPulseSystemPrompt, getPulseUserPrompt } from "@/lib/ai/prompts/pulse";
import {
getPulseSystemPrompt,
getPulseUserPrompt,
} from "@/lib/ai/prompts/pulse";
import {
formatPreviousContextForPrompt,
getPreviousInsightContext,
} from "@/lib/insights/memory";
import {
getAgeFromDateOfBirth,
getPersonalizedPulseTarget,
@@ -286,9 +293,25 @@ export async function generatePulseStatusForUser(
const snapshotJson = JSON.stringify(snapshot, null, 2);
const previousContext = await getPreviousInsightContext(
userId,
"pulse-status",
locale,
12,
);
const previousContextBlock = formatPreviousContextForPrompt(
previousContext,
locale,
);
const result = await provider.generateCompletion({
systemPrompt: getPulseSystemPrompt(locale),
userPrompt: getPulseUserPrompt(snapshotJson, todayKey, locale),
userPrompt: getPulseUserPrompt(
snapshotJson,
todayKey,
locale,
previousContextBlock,
),
temperature: 0.3,
maxTokens: 1000,
});
+29 -3
View File
@@ -1,11 +1,18 @@
import { prisma } from "@/lib/db";
import { resolveProvider } from "@/lib/ai/provider";
import { getWeightSystemPrompt, getWeightUserPrompt } from "@/lib/ai/prompts/weight";
import {
getWeightSystemPrompt,
getWeightUserPrompt,
} from "@/lib/ai/prompts/weight";
import {
pearsonCorrelation,
type PairedPoint,
} from "@/lib/analytics/correlations";
import { getNoKeyWeightStatusText } from "@/lib/insights/no-key-fallbacks";
import {
formatPreviousContextForPrompt,
getPreviousInsightContext,
} from "@/lib/insights/memory";
const WEIGHT_STATUS_POINTS = 30;
@@ -341,7 +348,10 @@ export async function generateWeightStatusForUser(
latest: dailyMoodSeries.at(-1)?.value ?? null,
series: dailyMoodSeries.slice(-10),
moodVsWeightCorrelation: (() => {
const moodVsWeightPairs = pairDailySeries(dailyMoodSeries, weightSeries);
const moodVsWeightPairs = pairDailySeries(
dailyMoodSeries,
weightSeries,
);
return pearsonCorrelation(moodVsWeightPairs);
})(),
}
@@ -350,9 +360,25 @@ export async function generateWeightStatusForUser(
const snapshotJson = JSON.stringify(snapshot, null, 2);
const previousContext = await getPreviousInsightContext(
userId,
"weight-status",
locale,
12,
);
const previousContextBlock = formatPreviousContextForPrompt(
previousContext,
locale,
);
const result = await provider.generateCompletion({
systemPrompt: getWeightSystemPrompt(locale),
userPrompt: getWeightUserPrompt(snapshotJson, todayKey, locale),
userPrompt: getWeightUserPrompt(
snapshotJson,
todayKey,
locale,
previousContextBlock,
),
temperature: 0.3,
maxTokens: 1000,
});