mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
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:
1 parent
751448494b
commit
14cbc69a2d
10 files changed
+162
-28
No files matched your search
@@ -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}`;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user