mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(insights): wire curated medical references into the live insight paths
The reference-grounding builder (buildSystemPromptWithReferences) and its SOURCES list shipped to no one: both live paths called the plain system prompt, so the model was asked to cite referenceId values it was never shown — and the parser rejects invented ids, so the citation machinery the schema and UI footnote already support stayed dark. Wire the builder into both consumers, passing the metric sections the snapshot actually carries (a weight-only run never lists BP guidance). Apply the fenced-JSON parse fallback here too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e6ef9f2a3e
commit
005f4ba2be
3 files changed
+63
-6
No files matched your search
@@ -20,7 +20,8 @@ import {
|
||||
buildUserPrompt,
|
||||
type ComparisonSnapshot,
|
||||
} from "@/lib/ai/prompts/insight-system-prompt";
|
||||
import { getStrictInsightsSystemPrompt } from "@/lib/ai/prompts/insight-generator";
|
||||
import { buildSystemPromptWithReferences } from "@/lib/ai/prompts/insight-generator";
|
||||
import { metricsFromPresentSections } from "@/lib/ai/medical-references";
|
||||
import { summarize, type DataPoint } from "@/lib/analytics/trends";
|
||||
import {
|
||||
resolveDashboardLayout,
|
||||
@@ -517,6 +518,18 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
userPrompt += buildDerivedBriefingPrompt(derivedBriefing, locale);
|
||||
}
|
||||
|
||||
// v1.12.7 (B5) — inject the curated SOURCES block for the metric sections
|
||||
// this briefing actually carries, so a normative claim can cite a real
|
||||
// `referenceId` the schema + UI footnote already support. Returns the plain
|
||||
// prompt unchanged when no applicable metric section is present.
|
||||
const referenceMetrics = metricsFromPresentSections({
|
||||
bloodPressure: features.bloodPressure != null,
|
||||
weight: features.weight != null,
|
||||
pulse: features.pulse != null,
|
||||
mood: features.mood != null,
|
||||
medication: (features.medications?.length ?? 0) > 0,
|
||||
});
|
||||
|
||||
let result;
|
||||
let workingProviderType: string;
|
||||
let fallbackHopCount = 0;
|
||||
@@ -536,7 +549,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// tolerates the strict shape via `insightResultSchema.safeParse`'s
|
||||
// soft fallback to `parsed` and `passthrough()` on the strict
|
||||
// schema, so switching the prompt does not break legacy callers.
|
||||
systemPrompt: getStrictInsightsSystemPrompt(locale),
|
||||
systemPrompt: buildSystemPromptWithReferences(locale, referenceMetrics),
|
||||
userPrompt,
|
||||
temperature: 0.3,
|
||||
maxTokens: 1500,
|
||||
|
||||
@@ -163,6 +163,31 @@ export function getMedicalReferenceById(
|
||||
return MEDICAL_REFERENCES.find((r) => r.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the set of feature/snapshot sections present for a generation onto
|
||||
* the reference metric buckets. Lets the insight consumers pick the
|
||||
* applicable curated SOURCES without re-encoding the bucket names.
|
||||
*
|
||||
* `present` carries one boolean per high-level section the snapshot
|
||||
* actually contains; absent / false sections drop their bucket so a
|
||||
* weight-only generation never lists ESH BP guidance.
|
||||
*/
|
||||
export function metricsFromPresentSections(present: {
|
||||
bloodPressure?: boolean;
|
||||
weight?: boolean;
|
||||
pulse?: boolean;
|
||||
mood?: boolean;
|
||||
medication?: boolean;
|
||||
}): MedicalReferenceMetric[] {
|
||||
const out: MedicalReferenceMetric[] = [];
|
||||
if (present.bloodPressure) out.push("bp");
|
||||
if (present.weight) out.push("weight");
|
||||
if (present.pulse) out.push("pulse");
|
||||
if (present.mood) out.push("mood");
|
||||
if (present.medication) out.push("medication");
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the references whose `metricApplicability` overlaps any of the
|
||||
* supplied metric buckets. Order preserved from the canonical bundle
|
||||
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
buildUserPrompt,
|
||||
type ComparisonSnapshot,
|
||||
} from "@/lib/ai/prompts/insight-system-prompt";
|
||||
import { getStrictInsightsSystemPrompt } from "@/lib/ai/prompts/insight-generator";
|
||||
import { buildSystemPromptWithReferences } from "@/lib/ai/prompts/insight-generator";
|
||||
import { metricsFromPresentSections } from "@/lib/ai/medical-references";
|
||||
import { summarize, type DataPoint } from "@/lib/analytics/trends";
|
||||
import {
|
||||
resolveDashboardLayout,
|
||||
@@ -52,7 +53,10 @@ import {
|
||||
enqueueStatusGeneration,
|
||||
type InsightStatusScope,
|
||||
} from "@/lib/jobs/insight-status-generate-shared";
|
||||
import { normalizeLocale } from "@/lib/insights/status-shared";
|
||||
import {
|
||||
normalizeLocale,
|
||||
stripJsonFences,
|
||||
} from "@/lib/insights/status-shared";
|
||||
import {
|
||||
isTimeoutStub,
|
||||
statusCacheAction,
|
||||
@@ -524,6 +528,18 @@ export async function generateComprehensiveInsight(
|
||||
userPrompt += buildGlp1PlateauPrompt(plateauContext, locale);
|
||||
}
|
||||
|
||||
// v1.12.7 (B5) — inject the curated SOURCES block for the metric sections
|
||||
// this generation actually carries, so a normative claim ("target < 140/90")
|
||||
// can cite a real `referenceId` the schema + UI footnote already support.
|
||||
// Returns the plain prompt unchanged when no applicable metric is present.
|
||||
const referenceMetrics = metricsFromPresentSections({
|
||||
bloodPressure: features.bloodPressure != null,
|
||||
weight: features.weight != null,
|
||||
pulse: features.pulse != null,
|
||||
mood: features.mood != null,
|
||||
medication: (features.medications?.length ?? 0) > 0,
|
||||
});
|
||||
|
||||
let result;
|
||||
let workingProviderType: string;
|
||||
try {
|
||||
@@ -531,7 +547,7 @@ export async function generateComprehensiveInsight(
|
||||
userId,
|
||||
providers: chain,
|
||||
params: {
|
||||
systemPrompt: getStrictInsightsSystemPrompt(locale),
|
||||
systemPrompt: buildSystemPromptWithReferences(locale, referenceMetrics),
|
||||
userPrompt,
|
||||
temperature: 0.3,
|
||||
maxTokens: 1500,
|
||||
@@ -548,7 +564,10 @@ export async function generateComprehensiveInsight(
|
||||
|
||||
let insights: InsightResult | Record<string, unknown>;
|
||||
try {
|
||||
const parsed = JSON.parse(result.content);
|
||||
// Anthropic + local have no native JSON mode, so a ```json-fenced or
|
||||
// sentence-prefixed reply would otherwise fail the whole generation.
|
||||
// Strip the fence before parsing; clean JSON passes through unchanged.
|
||||
const parsed = JSON.parse(stripJsonFences(result.content));
|
||||
const validated = insightResultSchema.safeParse(parsed);
|
||||
insights = validated.success ? validated.data : parsed;
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user