feat(briefing): GLP1 plateau detection rule with clinical referral framing

Phase 9 of W4d GLP-1 full integration. New `detectGlp1Plateau()`
helper flags the clinical plateau pattern for GLP-1 receptor agonists:
≥21 days on a stable dose AND weight delta within ±0.5 kg over that
same window. When the rule fires, a SYSTEM CONTEXT block is appended
to the insight-generation user prompt with the named drug, current
dose, days-on-dose, and weight delta — and instructs the model to
emit a glp1_plateau keyFinding framed observationally.

GROUND RULE 14 (insight prompt) keeps the safety contract: the
finding NAMES the plateau and the conversation it should prompt
("worth mentioning at the next visit"), but NEVER recommends a dose
change. The plateau is detection, not prescription.

Detector returns `null` when no GLP-1 medication is active OR no
plateau condition is met, so 99% of users pay zero token cost and
never see a plateau-shaped section in their briefing.

The schema gains "glp1_plateau" in the keyFinding.sourceMetric enum
(landed earlier this phase) and the daily-briefing icon mapping
inherits the Pill icon for visual consistency with other
medication findings.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 13:52:31 +02:00
1 parent 8b79ed89c8
commit 6148a179ca
3 files changed
+221 -1

No files matched your search

+16 -1
View File
@@ -2,6 +2,10 @@ import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { extractFeatures } from "@/lib/insights/features";
import {
detectGlp1Plateau,
buildGlp1PlateauPrompt,
} from "@/lib/insights/glp1-plateau";
import {
buildUserPrompt,
type ComparisonSnapshot,
@@ -266,12 +270,23 @@ export const POST = apiHandler(async (request: NextRequest) => {
// narrative is default-on whenever the toggle is on per research §7
// Q4 — the pulldown is the single affordance.
const comparisonSnapshot = await buildComparisonSnapshotForUser(userId);
const userPrompt = buildUserPrompt(
// v1.4.25 W4d — GLP-1 plateau detector. Appends a SYSTEM CONTEXT
// block to the user prompt when the active GLP-1 medication has been
// on a stable dose for ≥21 days with weight delta within ±0.5 kg.
// The block instructs the model to emit a glp1_plateau keyFinding
// framed observationally (no dose recommendation per GROUND RULE 14).
// Returns null when no GLP-1 is active OR no plateau condition;
// 99% of users land in the null branch and pay zero token cost.
const plateauContext = await detectGlp1Plateau(userId);
let userPrompt = buildUserPrompt(
featuresJson,
dbUser?.insightsPrivacyMode ?? "aggregated",
locale,
comparisonSnapshot ?? undefined,
);
if (plateauContext) {
userPrompt += buildGlp1PlateauPrompt(plateauContext, locale);
}
let result;
let workingProviderType: string;
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import { buildGlp1PlateauPrompt } from "@/lib/insights/glp1-plateau";
describe("glp1-plateau", () => {
describe("buildGlp1PlateauPrompt()", () => {
it("renders the EN block with the named drug and dose", () => {
const prompt = buildGlp1PlateauPrompt(
{
drug: "Mounjaro",
doseValue: 7.5,
doseUnit: "mg",
doseSince: "2026-04-01",
daysOnDose: 30,
weightDeltaKg: -0.2,
readingsCount: 9,
},
"en",
);
expect(prompt).toContain("GLP-1 PLATEAU ACTIVE");
expect(prompt).toContain("Mounjaro 7.5 mg");
expect(prompt).toContain("2026-04-01");
expect(prompt).toContain("-0.2 kg");
expect(prompt).toContain("glp1_plateau");
expect(prompt).toContain("GROUND RULE 14");
expect(prompt).toContain("NEVER recommend a dose change");
});
it("renders the DE block with German framing", () => {
const prompt = buildGlp1PlateauPrompt(
{
drug: "Mounjaro",
doseValue: 7.5,
doseUnit: "mg",
doseSince: "2026-04-01",
daysOnDose: 30,
weightDeltaKg: -0.2,
readingsCount: 9,
},
"de",
);
expect(prompt).toContain("GLP-1-PLATEAU AKTIV");
expect(prompt).toContain("Mounjaro 7.5 mg");
expect(prompt).toContain("KEINE Dosis-Empfehlung");
expect(prompt).toContain("GRUNDREGEL 14");
});
it("computes week number from days on dose", () => {
const prompt = buildGlp1PlateauPrompt(
{
drug: "Mounjaro",
doseValue: 7.5,
doseUnit: "mg",
doseSince: "2026-04-01",
daysOnDose: 30,
weightDeltaKg: -0.2,
readingsCount: 9,
},
"en",
);
expect(prompt).toContain("week 4");
});
});
});
+138
View File
@@ -0,0 +1,138 @@
/**
* v1.4.25 W4d — GLP-1 plateau-detection rule.
*
* Server-side detector that flags weight plateau when a user on an
* active GLP-1 medication has been on a stable dose for ≥21 days AND
* their weight has not decreased by more than 0.5 kg in that same
* trailing window. The result is injected into the insight-generation
* user prompt so the LLM can include a plateau finding in the daily
* briefing — framed conservatively per GROUND RULE 14 of the insight
* prompt (defer dose decisions to the clinician, observational tone).
*
* Returns `null` when:
* - the user has no GLP-1 medications,
* - the current dose has been in place < 21 days,
* - weight has dropped by more than 0.5 kg in the same window,
* - or there are fewer than two weight readings to compare.
*/
import { prisma } from "@/lib/db";
const PLATEAU_WINDOW_DAYS = 21;
const PLATEAU_THRESHOLD_KG = 0.5;
export interface Glp1PlateauContext {
/** Display drug name ("Mounjaro", "Ozempic"). */
drug: string;
/** Current dose value (e.g. 7.5). */
doseValue: number;
/** Dose unit (e.g. "mg"). */
doseUnit: string;
/** ISO date the current dose started. */
doseSince: string;
/** Days the user has been on the current dose. */
daysOnDose: number;
/** Weight delta in kg over the trailing window (negative = loss). */
weightDeltaKg: number;
/** Number of weight readings considered. */
readingsCount: number;
}
export async function detectGlp1Plateau(
userId: string,
now: Date = new Date(),
): Promise<Glp1PlateauContext | null> {
const meds = await prisma.medication.findMany({
where: { userId, treatmentClass: "GLP1", active: true },
include: {
doseChanges: { orderBy: { effectiveFrom: "desc" }, take: 1 },
},
});
if (meds.length === 0) return null;
const med = meds[0];
const latestDose = med.doseChanges[0];
if (!latestDose) return null;
const daysOnDose = Math.floor(
(now.getTime() - latestDose.effectiveFrom.getTime()) /
(24 * 60 * 60 * 1000),
);
if (daysOnDose < PLATEAU_WINDOW_DAYS) return null;
const windowStart = new Date(now.getTime());
windowStart.setDate(windowStart.getDate() - PLATEAU_WINDOW_DAYS);
const weightRows = await prisma.measurement.findMany({
where: {
userId,
type: "WEIGHT",
measuredAt: { gte: windowStart },
},
orderBy: { measuredAt: "asc" },
select: { value: true, measuredAt: true },
});
if (weightRows.length < 2) return null;
const first = weightRows[0].value;
const last = weightRows[weightRows.length - 1].value;
const delta = last - first;
// Plateau condition: weight has NOT decreased by more than the
// threshold. Mild loss within the threshold still counts as a plateau
// because that's the clinically relevant pattern ("stuck at this
// dose"); a hard drop bigger than the threshold means the user is
// still responding.
if (delta < -PLATEAU_THRESHOLD_KG) return null;
// First word of the drug name as the display token. Falls back to
// the full name if the user typed something unusual.
const drug = med.name.trim().split(/[\s_]/)[0] || med.name.trim();
return {
drug,
doseValue: latestDose.doseValue,
doseUnit: latestDose.doseUnit,
doseSince: latestDose.effectiveFrom.toISOString().slice(0, 10),
daysOnDose,
weightDeltaKg: Math.round(delta * 10) / 10,
readingsCount: weightRows.length,
};
}
/**
* Render a Markdown-flavoured prompt block for the insight generator
* that names the plateau and tells the model how to frame the
* resulting finding (deferring to the clinician). Returned string is
* appended to the user prompt; the model's GROUND RULE 14 carries
* the dose-prescription refusal.
*/
export function buildGlp1PlateauPrompt(
ctx: Glp1PlateauContext,
locale: "en" | "de",
): string {
if (locale === "de") {
return `
SYSTEM CONTEXT — GLP-1-PLATEAU AKTIV
Der Nutzer nimmt aktuell ${ctx.drug} ${ctx.doseValue} ${ctx.doseUnit} (seit ${ctx.doseSince}, ${ctx.daysOnDose} Tage). In den letzten ${PLATEAU_WINDOW_DAYS} Tagen hat sich das Gewicht nur um ${ctx.weightDeltaKg} kg verändert (n=${ctx.readingsCount} Messungen). Das ist das klinische Plateau-Muster für GLP-1-Rezeptoragonisten.
Wenn du einen dailyBriefing-keyFinding zu diesem Plateau emittierst:
- sourceMetric: "glp1_plateau"
- tone: "info"
- detail: nenne den Wirkstoff und die aktuelle Dosis ("${ctx.drug} ${ctx.doseValue} ${ctx.doseUnit}, Woche ${Math.floor(ctx.daysOnDose / 7)}")
- Rahmen es als beobachtetes Muster, keine Empfehlung; weise auf das Gespräch mit der behandelnden Ärztin beim nächsten Termin hin
- KEINE Dosis-Empfehlung (GRUNDREGEL 14)`;
}
return `
SYSTEM CONTEXT — GLP-1 PLATEAU ACTIVE
The user is currently on ${ctx.drug} ${ctx.doseValue} ${ctx.doseUnit} (since ${ctx.doseSince}, ${ctx.daysOnDose} days). In the last ${PLATEAU_WINDOW_DAYS} days their weight has shifted by ${ctx.weightDeltaKg} kg (n=${ctx.readingsCount} readings). That is the clinical plateau pattern for GLP-1 receptor agonists.
If you emit a dailyBriefing keyFinding for this plateau:
- sourceMetric: "glp1_plateau"
- tone: "info"
- detail: name the drug and current dose ("${ctx.drug} ${ctx.doseValue} ${ctx.doseUnit}, week ${Math.floor(ctx.daysOnDose / 7)}")
- frame as observed pattern, not recommendation; mention conversation with the prescribing clinician at the next visit
- NEVER recommend a dose change (GROUND RULE 14)`;
}