@pablotech/akesi 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +432 -0
- package/benchmarks/retry-corrections.ts +231 -0
- package/dates.ts +36 -0
- package/document-model.ts +96 -0
- package/document-read.ts +171 -0
- package/factors-edit.ts +161 -0
- package/finding-assemble.ts +803 -0
- package/finding-generate.ts +1439 -0
- package/finding-regroup.ts +167 -0
- package/imaging-catalog.ts +108 -0
- package/index.ts +4 -0
- package/ingest-core.ts +115 -0
- package/item-registry.ts +69 -0
- package/marker-deltas.ts +84 -0
- package/marker-groups-prompt.ts +198 -0
- package/package.json +69 -0
- package/parsers-report.ts +21 -0
- package/pinned-queries.ts +113 -0
- package/ranges-prompt.ts +228 -0
- package/ranges.ts +94 -0
- package/report-extract.ts +337 -0
- package/report-merge.ts +372 -0
- package/report-title.ts +29 -0
- package/section-labels.ts +20 -0
- package/system-groups.ts +43 -0
- package/treatment-bucket.ts +366 -0
- package/treatment-infer.ts +237 -0
- package/treatment-normalize.ts +91 -0
- package/treatment-product.ts +111 -0
- package/treatment-timing-rules.ts +90 -0
- package/types.ts +647 -0
- package/unit-systems.ts +214 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
// The host CLI/serverless half of finding generation: the 948-line SYSTEM_PROMPT (66% of this
|
|
2
|
+
// file), the buildUserMessage assembler + its helpers, extractJson, and generateFindingResponse
|
|
3
|
+
// (the streaming Opus call + retry-with-correction loop returning a raw, validated
|
|
4
|
+
// FindingAIResponse). Node-free (injected Anthropic client, no fs/process); a host CLI wrapper and
|
|
5
|
+
// a streaming refresh endpoint both drive it. Assembly + validation live in ./finding-assemble.
|
|
6
|
+
// CO_MENTION_RULE went with the treatment section — it exists only to tell the model how to
|
|
7
|
+
// handle a drug mentioned alongside another, which is now the treatmentAssessment leaf's problem.
|
|
8
|
+
import { ageYears } from "./ranges";
|
|
9
|
+
import { CURRENT_DOSE_RULE, BUCKET_DOSE_RULE, STANDARD_DOSING_RULE, asPromptLines } from "./treatment-timing-rules";
|
|
10
|
+
import { pinnedQueryBlock } from "./pinned-queries";
|
|
11
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
12
|
+
import type { Client, ClientFactors, MarkerResult, NoteEntry, PersonalizedRange } from "./types";
|
|
13
|
+
import { deltaForSeries, type DeltaChange } from "./marker-deltas";
|
|
14
|
+
import { validate, extractJson, type FindingAIResponse } from "./finding-assemble";
|
|
15
|
+
import { treatmentsOf } from "./treatment-normalize";
|
|
16
|
+
import { bucketOf, treatmentLabel } from "./treatment-bucket";
|
|
17
|
+
|
|
18
|
+
// Structural usage sink — a caller's own usage accumulator satisfies it, with nothing dragged in.
|
|
19
|
+
export interface UsageRecorder {
|
|
20
|
+
record(
|
|
21
|
+
model: string,
|
|
22
|
+
usage: { input_tokens?: number | null; output_tokens?: number | null; cache_creation_input_tokens?: number | null; cache_read_input_tokens?: number | null } | null | undefined,
|
|
23
|
+
): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const YEAR_MS = 365.25 * 24 * 60 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
// Exported so leaf-regen-registry's patientAssessment context accessor reuses this exact prose
|
|
29
|
+
// (the same source the monolith prompt's "Patient Profile:" line is built from) rather than a
|
|
30
|
+
// second, divergent self-assessment summarizer.
|
|
31
|
+
export function describeProfile(client: Client): string {
|
|
32
|
+
const age = ageYears(client.dob);
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
parts.push(`${age ?? "unknown age"}-year-old ${client.gender}`);
|
|
35
|
+
const f: ClientFactors = client.factors ?? {};
|
|
36
|
+
if (f.diseases && f.diseases.length > 0) {
|
|
37
|
+
const fmt = f.diseases.map((d) => `${d.diagnostic}${d.icdCodes?.length ? ` [${d.icdCodes.join(", ")}]` : ""}${d.summary ? ` — ${d.summary}` : ""} (${d.date})`).join("; ");
|
|
38
|
+
parts.push(`prior diagnoses: ${fmt}`);
|
|
39
|
+
}
|
|
40
|
+
if (f.allergies && f.allergies.length > 0) {
|
|
41
|
+
const fmt = f.allergies.map((a) => `${a.allergen} — ${a.reaction}${a.severity ? ` (${a.severity})` : ""}`).join("; ");
|
|
42
|
+
parts.push(`allergies: ${fmt}`);
|
|
43
|
+
}
|
|
44
|
+
if (f.familyHistory && f.familyHistory.length > 0) {
|
|
45
|
+
const fmt = f.familyHistory.map((h) => `${h.relation}: ${h.condition}`).join("; ");
|
|
46
|
+
parts.push(`family history: ${fmt}`);
|
|
47
|
+
}
|
|
48
|
+
if (f.pregnancy && f.pregnancy !== "none") parts.push(f.pregnancy);
|
|
49
|
+
if (f.athletic) parts.push(`${f.athletic} activity level`);
|
|
50
|
+
if (f.height) parts.push(`height ${f.height}`);
|
|
51
|
+
if (typeof f.bmi === "number") parts.push(`BMI ${f.bmi}`);
|
|
52
|
+
if (f.smoking) parts.push(`${f.smoking} smoker`);
|
|
53
|
+
if (f.ethnicity) parts.push(`ethnicity: ${f.ethnicity}`);
|
|
54
|
+
return parts.join("; ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The ONGOING regimen (assessed in `treatment`) and PAST/discontinued items (context only). Titration
|
|
58
|
+
// rows are NOT collapsed here — the model needs the full dose history to reason about trajectory and
|
|
59
|
+
// dedupes into one `treatment` entry per drug itself. `today` (YYYY-MM-DD) drives the temporal split.
|
|
60
|
+
function describeTreatment(client: Client, today: string): string {
|
|
61
|
+
const items = treatmentsOf(client);
|
|
62
|
+
const fmtItem = (name: string, dose?: string, span?: string) =>
|
|
63
|
+
` - ${[name, dose].filter(Boolean).join(" ")}${span ? ` ${span}` : ""}`.trimEnd();
|
|
64
|
+
const ongoing = items.filter((t) => bucketOf(t, today) === "ongoing");
|
|
65
|
+
const past = items.filter((t) => bucketOf(t, today) === "past");
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
if (ongoing.length) {
|
|
68
|
+
lines.push("Ongoing regimen (currently being taken — assess each in `treatment`):");
|
|
69
|
+
for (const t of ongoing) {
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
if (t.start) parts.push(`[since ${t.start}]`);
|
|
72
|
+
if (t.reason) parts.push(`(reason: ${t.reason})`);
|
|
73
|
+
if (t.timingPeriod) parts.push(`(${t.timingPeriod})`);
|
|
74
|
+
lines.push(fmtItem(t.name, t.dose, parts.join(" ")));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (past.length) {
|
|
78
|
+
lines.push("Past / discontinued treatments (historical context only — do NOT assess these in `treatment`):");
|
|
79
|
+
for (const t of past) lines.push(fmtItem(t.name, t.dose, `[${t.start || "?"}–${t.end}]`));
|
|
80
|
+
}
|
|
81
|
+
return lines.length === 0 ? "(none recorded)" : lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The verbatim Action labels for the PLANNED treatments — the set treatmentGroups.patient and
|
|
85
|
+
// planAssessmentRows must cover. Shared by generation (validate `expected`) and the web refresh
|
|
86
|
+
// (refresh-client.expectedFor) so both derive the same set. `today` is passed in (purity).
|
|
87
|
+
export function plannedLabels(client: Client, today: string): string[] {
|
|
88
|
+
return treatmentsOf(client)
|
|
89
|
+
.filter((t) => bucketOf(t, today) === "planned")
|
|
90
|
+
.map((t) => treatmentLabel(t));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The Note entries the prompt actually presents (non-empty text, in display order) — the exact set
|
|
94
|
+
// noteResults must cover, one result per entry, paired back by array position (a note has no short
|
|
95
|
+
// label like Study's `focus` to match on verbatim). Shared by generation (validate `expected`) and
|
|
96
|
+
// the web refresh (refresh-client.expectedFor) so both derive the same set.
|
|
97
|
+
export function populatedNoteEntries(client: Client): NoteEntry[] {
|
|
98
|
+
return (client.factors?.noteEntries ?? []).filter((n) => n.text.trim().length > 0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function fmt(n: number): string {
|
|
102
|
+
const abs = Math.abs(n);
|
|
103
|
+
if (abs >= 100) return n.toFixed(0);
|
|
104
|
+
if (abs >= 10) return n.toFixed(1);
|
|
105
|
+
if (abs >= 1) return n.toFixed(2);
|
|
106
|
+
return n.toFixed(3);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isOutOfRange(value: number, range: PersonalizedRange | undefined): boolean {
|
|
110
|
+
if (!range) return false;
|
|
111
|
+
if (range.low != null && value < range.low) return true;
|
|
112
|
+
if (range.high != null && value > range.high) return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function relevantMarkers(client: Client): string[] {
|
|
117
|
+
const set = new Set<string>([...client.watchlist, ...(client.recommended ?? [])]);
|
|
118
|
+
const latestByMarker = new Map<string, MarkerResult>();
|
|
119
|
+
for (const r of client.results) {
|
|
120
|
+
const prev = latestByMarker.get(r.marker);
|
|
121
|
+
if (!prev || r.date.localeCompare(prev.date) > 0) latestByMarker.set(r.marker, r);
|
|
122
|
+
}
|
|
123
|
+
const ranges = client.personalizedRanges ?? {};
|
|
124
|
+
for (const [marker, latest] of latestByMarker) {
|
|
125
|
+
if (isOutOfRange(latest.value, ranges[marker])) set.add(marker);
|
|
126
|
+
}
|
|
127
|
+
return [...set].sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function markerContext(client: Client, marker: string): string | null {
|
|
131
|
+
const rows = client.results
|
|
132
|
+
.filter((r) => r.marker === marker)
|
|
133
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
134
|
+
if (rows.length === 0) return null;
|
|
135
|
+
const unit = rows[rows.length - 1].unit;
|
|
136
|
+
const cutoff = Date.now() - YEAR_MS;
|
|
137
|
+
const recent = rows.filter((r) => new Date(r.date).getTime() >= cutoff);
|
|
138
|
+
const prior = rows.filter((r) => new Date(r.date).getTime() < cutoff);
|
|
139
|
+
|
|
140
|
+
const range = (client.personalizedRanges ?? {})[marker];
|
|
141
|
+
const lines: string[] = [];
|
|
142
|
+
lines.push(`${marker} (${unit})`);
|
|
143
|
+
if (range) {
|
|
144
|
+
const lo = range.low != null ? `${fmt(range.low)}` : null;
|
|
145
|
+
const hi = range.high != null ? `${fmt(range.high)}` : null;
|
|
146
|
+
let target: string;
|
|
147
|
+
if (lo != null && hi != null) target = `${lo}–${hi} ${range.unit}`;
|
|
148
|
+
else if (hi != null) target = `< ${hi} ${range.unit}`;
|
|
149
|
+
else if (lo != null) target = `> ${lo} ${range.unit}`;
|
|
150
|
+
else target = "—";
|
|
151
|
+
lines.push(` Personalized target: ${target}`);
|
|
152
|
+
} else {
|
|
153
|
+
const labRef = rows[rows.length - 1].ref;
|
|
154
|
+
if (labRef && (labRef.low != null || labRef.high != null)) {
|
|
155
|
+
const lo = labRef.low != null ? `${fmt(labRef.low)}` : null;
|
|
156
|
+
const hi = labRef.high != null ? `${fmt(labRef.high)}` : null;
|
|
157
|
+
let target: string;
|
|
158
|
+
if (lo != null && hi != null) target = `${lo}–${hi} ${unit}`;
|
|
159
|
+
else if (hi != null) target = `< ${hi} ${unit}`;
|
|
160
|
+
else if (lo != null) target = `> ${lo} ${unit}`;
|
|
161
|
+
else target = "—";
|
|
162
|
+
lines.push(` Lab reference (no personalized range): ${target}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (recent.length === 0) {
|
|
167
|
+
lines.push(` Last year: (no readings in the past 12 months)`);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(` Last year (${recent.length} reading${recent.length === 1 ? "" : "s"}):`);
|
|
170
|
+
for (const r of recent) lines.push(` ${r.date}: ${r.valueText ?? `${fmt(r.value)} ${r.unit}`}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (prior.length === 0) {
|
|
174
|
+
lines.push(` Prior: (no earlier data)`);
|
|
175
|
+
} else {
|
|
176
|
+
const values = prior.map((r) => r.value);
|
|
177
|
+
const min = Math.min(...values);
|
|
178
|
+
const max = Math.max(...values);
|
|
179
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
180
|
+
const oldest = prior[0].date;
|
|
181
|
+
const newest = prior[prior.length - 1].date;
|
|
182
|
+
lines.push(
|
|
183
|
+
` Prior (${prior.length} reading${prior.length === 1 ? "" : "s"}, ${oldest} to ${newest}): ` +
|
|
184
|
+
`mean ${fmt(mean)}, range ${fmt(min)}–${fmt(max)} ${unit}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handed deltas (W2c): the model reasons over computed change, not inferred. Each
|
|
189
|
+
// line carries the date range it spans so the DATE AWARENESS rules can check it
|
|
190
|
+
// against treatment since-dates before crediting a change to a treatment.
|
|
191
|
+
const delta = deltaForSeries(rows);
|
|
192
|
+
if (delta) {
|
|
193
|
+
const fmtChange = (c: DeltaChange) =>
|
|
194
|
+
`${c.abs >= 0 ? "+" : ""}${fmt(c.abs)} ${unit}` +
|
|
195
|
+
(c.pct != null ? ` (${c.pct >= 0 ? "+" : ""}${c.pct.toFixed(0)}%)` : "");
|
|
196
|
+
lines.push(
|
|
197
|
+
` Change vs prior reading (${delta.prior.date} → ${delta.latest.date}): ${fmtChange(delta.vsPrior)}`,
|
|
198
|
+
);
|
|
199
|
+
if (delta.vsBaseline && delta.baseline) {
|
|
200
|
+
lines.push(
|
|
201
|
+
` Change vs baseline (${delta.baseline.date} → ${delta.latest.date}): ${fmtChange(delta.vsBaseline)}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function formatTarget(
|
|
209
|
+
range: { low?: number | null; high?: number | null; unit?: string } | undefined,
|
|
210
|
+
fallbackUnit: string,
|
|
211
|
+
): string | null {
|
|
212
|
+
if (!range) return null;
|
|
213
|
+
const lo = range.low != null ? fmt(range.low) : null;
|
|
214
|
+
const hi = range.high != null ? fmt(range.high) : null;
|
|
215
|
+
const u = range.unit ?? fallbackUnit;
|
|
216
|
+
if (lo != null && hi != null) return `${lo}–${hi} ${u}`;
|
|
217
|
+
if (hi != null) return `< ${hi} ${u}`;
|
|
218
|
+
if (lo != null) return `> ${lo} ${u}`;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Census of every marker with at least one reading, so the model can tell
|
|
223
|
+
// "already measured" from "never ordered" — relevantMarkers() only surfaces a
|
|
224
|
+
// small subset, which previously made the model recommend re-ordering labs the
|
|
225
|
+
// patient already has.
|
|
226
|
+
function onFileInventory(client: Client): string[] {
|
|
227
|
+
const latestByMarker = new Map<string, MarkerResult>();
|
|
228
|
+
for (const r of client.results) {
|
|
229
|
+
const prev = latestByMarker.get(r.marker);
|
|
230
|
+
if (!prev || r.date.localeCompare(prev.date) > 0) latestByMarker.set(r.marker, r);
|
|
231
|
+
}
|
|
232
|
+
const ranges = client.personalizedRanges ?? {};
|
|
233
|
+
const cutoff = new Date();
|
|
234
|
+
cutoff.setMonth(cutoff.getMonth() - 6);
|
|
235
|
+
const cutoffISO = cutoff.toISOString().slice(0, 10);
|
|
236
|
+
return [...latestByMarker]
|
|
237
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
238
|
+
.map(([marker, latest]) => {
|
|
239
|
+
const recency = latest.date < cutoffISO ? "[>6mo — overdue]" : "[within 6mo]";
|
|
240
|
+
const target = formatTarget(ranges[marker], latest.unit);
|
|
241
|
+
const targetStr = target ? `, target ${target}` : "";
|
|
242
|
+
return ` ${marker} — ${latest.valueText ?? `${fmt(latest.value)} ${latest.unit}`}, ${latest.date} ${recency}${targetStr}`;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The per-marker compact summary blocks (watchlist + previously-recommended + latest-out-of-range),
|
|
247
|
+
// exactly as buildUserMessage's Markers section assembles them. Exported so leaf-regen-registry's
|
|
248
|
+
// markerLevels context accessor reuses this instead of a second marker-summarizing implementation.
|
|
249
|
+
export function markerLevelBlocks(client: Client): string[] {
|
|
250
|
+
const markers = relevantMarkers(client);
|
|
251
|
+
const blocks: string[] = [];
|
|
252
|
+
for (const m of markers) {
|
|
253
|
+
const ctx = markerContext(client, m);
|
|
254
|
+
if (ctx) blocks.push(ctx);
|
|
255
|
+
}
|
|
256
|
+
return blocks;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildUserMessage(client: Client): string {
|
|
260
|
+
const f = client.factors ?? {};
|
|
261
|
+
const sections: string[] = [];
|
|
262
|
+
|
|
263
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
264
|
+
sections.push(`Today: ${today}`);
|
|
265
|
+
|
|
266
|
+
sections.push(`Patient Profile: ${describeProfile(client)}`);
|
|
267
|
+
|
|
268
|
+
if (f.goal || f.focus) {
|
|
269
|
+
const lines = ["Proposed Plan:"];
|
|
270
|
+
if (f.goal) lines.push(` Goal: ${f.goal}`);
|
|
271
|
+
if (f.focus) lines.push(` Focus: ${f.focus}`);
|
|
272
|
+
sections.push(lines.join("\n"));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const study = client.study ?? {};
|
|
276
|
+
if (study.entries && study.entries.length > 0) {
|
|
277
|
+
const lines = ["Proposed Study:"];
|
|
278
|
+
for (const e of study.entries) lines.push(` ${e.focus}: ${e.detail}`);
|
|
279
|
+
sections.push(lines.join("\n"));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const notes = populatedNoteEntries(client);
|
|
283
|
+
if (notes.length > 0) {
|
|
284
|
+
const lines = [
|
|
285
|
+
"Notes — patient's free-text jottings ahead of a visit (numbered here only to show order; do not renumber or relabel in your response):",
|
|
286
|
+
];
|
|
287
|
+
notes.forEach((n, i) => lines.push(` ${i + 1}. ${n.text}`));
|
|
288
|
+
sections.push(lines.join("\n"));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (f.diseases && f.diseases.length > 0) {
|
|
292
|
+
const lines = ["Diagnosed Disease (prior doctor diagnostics, treat as load-bearing context for findings and treatment; the Summary gives the nature/metrics — use its specifics, not just the terse diagnostic, when reasoning):"];
|
|
293
|
+
for (const d of f.diseases) lines.push(` - ${d.date}: ${d.diagnostic}${d.icdCodes?.length ? ` [${d.icdCodes.join(", ")}]` : ""}${d.summary ? `\n Summary: ${d.summary}` : ""}`);
|
|
294
|
+
sections.push(lines.join("\n"));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
sections.push(`Treatment History:\n${describeTreatment(client, today)}`);
|
|
298
|
+
|
|
299
|
+
if (f.decisions && f.decisions.length > 0) {
|
|
300
|
+
const lines = [
|
|
301
|
+
"Hypothesis Evaluation — patient's proposed interventions (future alternatives the patient is weighing — feed these into `decisions.patient` and the patient-decision entries in `doctorConversation`; do NOT let them influence progression / disease / treatment analysis):",
|
|
302
|
+
];
|
|
303
|
+
for (const d of f.decisions) lines.push(` - ${d.intervention}: ${d.purpose}`);
|
|
304
|
+
sections.push(lines.join("\n"));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const planned = treatmentsOf(client).filter((t) => bucketOf(t, today) === "planned");
|
|
308
|
+
if (planned.length > 0) {
|
|
309
|
+
const lines = [
|
|
310
|
+
"Patient Plan — treatments the patient plans to START (future-dated), each with its planned start. Assess the plan AS A WHOLE in `planAssessment`; do NOT let it influence progression / disease / treatment analysis. The per-action assessments are produced elsewhere — do not write them here:",
|
|
311
|
+
];
|
|
312
|
+
for (const t of planned) lines.push(` - Action: "${treatmentLabel(t)}" (timing: ${t.start || "TBD"})`);
|
|
313
|
+
sections.push(lines.join("\n"));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const markerBlocks = markerLevelBlocks(client);
|
|
317
|
+
if (markerBlocks.length === 0) {
|
|
318
|
+
sections.push("Markers: (no tracked markers and no out-of-range latest readings)");
|
|
319
|
+
} else {
|
|
320
|
+
sections.push(
|
|
321
|
+
`Markers (watchlist + previously-recommended + latest-out-of-range, ${markerBlocks.length} total):\n\n` + markerBlocks.join("\n\n"),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const inventory = onFileInventory(client);
|
|
326
|
+
if (inventory.length > 0) {
|
|
327
|
+
sections.push(
|
|
328
|
+
`On-file markers (the AUTHORITATIVE census of every marker that has at least one reading on record, ${inventory.length} total — latest value, date, a recency tag, and personalized target where one exists). Each line is tagged [within 6mo] or [>6mo — overdue] relative to Today; use that tag to drive re-test timing in dataRequisition. The detailed Markers block above is a deeper view of the subset that matters most; THIS list is the complete set of what has already been measured. Treat it as the source of truth: never describe a marker here as missing, not on file, or not tracked, and when recommending or requisitioning one of these markers copy its name from this list VERBATIM:\n` +
|
|
329
|
+
inventory.join("\n"),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (client.watchlist.length > 0) {
|
|
334
|
+
sections.push(
|
|
335
|
+
`Watchlist (markers the patient is currently tracking — when these belong in your healthMarkers.recommended output, copy the strings verbatim with no unit suffixes or parentheticals beyond what is shown):\n` +
|
|
336
|
+
client.watchlist.map((w) => ` - ${w}`).join("\n"),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Everything the user has STARRED, as areas of query. Placed last among the input sections,
|
|
341
|
+
// immediately before the output instruction, so it reads as a steer over the evidence rather than
|
|
342
|
+
// as another piece of it. pinned-queries.ts owns the wording that keeps that distinction —
|
|
343
|
+
// a pin says what to look INTO, never what is true. Absent entirely when nothing is pinned.
|
|
344
|
+
const pinnedBlock = pinnedQueryBlock(client);
|
|
345
|
+
if (pinnedBlock) sections.push(pinnedBlock);
|
|
346
|
+
|
|
347
|
+
sections.push(
|
|
348
|
+
"Respond with ONLY a single JSON object matching the exact structure and " +
|
|
349
|
+
"field names specified above — every required top-level key present. No " +
|
|
350
|
+
"markdown, no code fences, no prose before or after the JSON. Do not put " +
|
|
351
|
+
"markdown headers inside string values; the renderer adds headings.",
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return sections.join("\n\n");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
export const SYSTEM_PROMPT = [
|
|
359
|
+
"You are advising on a patient's lab and body-marker progression. You are not",
|
|
360
|
+
"a physician and your output is decision support, not a diagnosis. Frame any",
|
|
361
|
+
"suggestion as an item to discuss with the patient's physician — never as an",
|
|
362
|
+
"instruction. Be specific and clinical; avoid vague hedging when the data",
|
|
363
|
+
"clearly supports a direction.",
|
|
364
|
+
"",
|
|
365
|
+
"The input may end with an \"Areas of query\" section: items the patient or their",
|
|
366
|
+
"provider has starred. Those are TOPICS TO LOOK INTO, not information. They are",
|
|
367
|
+
"not evidence, not clinical record, and not citable; a starred question is an",
|
|
368
|
+
"open question and must never be written up as an established finding. Their",
|
|
369
|
+
"only effect is where you spend your attention. Everything you assert still",
|
|
370
|
+
"comes from the evidence sections described below.",
|
|
371
|
+
"",
|
|
372
|
+
"You will be given the patient's profile, plan, study notes, current treatment,",
|
|
373
|
+
"and TWO views of their markers. (1) A detailed Markers block for the ones that",
|
|
374
|
+
"matter most — the watchlist, markers a prior Finding recommended, and any",
|
|
375
|
+
"marker whose latest reading is outside its personalized target — each with",
|
|
376
|
+
"last-year readings listed explicitly, a summary of any prior baseline, and",
|
|
377
|
+
"explicit handed \"Change vs prior reading\" / \"Change vs baseline\" lines",
|
|
378
|
+
"(absolute + %, each tagged with the date range it spans). Prefer these handed",
|
|
379
|
+
"deltas over recomputing change from the raw values, and apply the DATE AWARENESS",
|
|
380
|
+
"rules below to a delta's date range before crediting any change to a treatment. (2)",
|
|
381
|
+
"An On-file markers census listing EVERY marker that has any reading on record,",
|
|
382
|
+
"with its latest value, date, a recency tag ([within 6mo] or [>6mo — overdue]",
|
|
383
|
+
"relative to Today), and personalized target where one exists. The census is the",
|
|
384
|
+
"AUTHORITATIVE answer to \"has this been measured?\" and \"is it due for a",
|
|
385
|
+
"re-test?\": never call a marker missing, not on file, or untracked if it appears",
|
|
386
|
+
"in the census, and when you name one of those markers copy its name verbatim.",
|
|
387
|
+
"",
|
|
388
|
+
"Think deeply across all of this information before writing. Identify which",
|
|
389
|
+
"markers actually moved, in which direction, and by how much, both within the",
|
|
390
|
+
"last year and relative to the patient's earlier baseline. Cross-reference",
|
|
391
|
+
"marker patterns against the stated symptoms, suspicion, goal, and focus.",
|
|
392
|
+
"",
|
|
393
|
+
"DATE AWARENESS — Read carefully before attributing any marker change.",
|
|
394
|
+
"The user message begins with a `Today:` line carrying the current date,",
|
|
395
|
+
"and every medication / supplement row in the Treatment block carries a",
|
|
396
|
+
"date string in square brackets. The date string is one of two shapes:",
|
|
397
|
+
" • \"Since X\" (e.g. [Since August 2025]) — treatment is ONGOING from X.",
|
|
398
|
+
" This is the active dose for that row.",
|
|
399
|
+
" • A closed range (e.g. [April–May 2026]) — this dose was active only",
|
|
400
|
+
" during that window. A row in this shape is a PRIOR dose level in a",
|
|
401
|
+
" titration sequence; the row with [Since Y] for the same drug is the",
|
|
402
|
+
" current dose.",
|
|
403
|
+
"",
|
|
404
|
+
"A marker reading can only reflect a treatment's effect if the reading",
|
|
405
|
+
"was DRAWN DURING THE WINDOW the dose was active. For ongoing rows that",
|
|
406
|
+
"means the reading date must be after the \"Since X\" date; for closed",
|
|
407
|
+
"ranges it means the reading date must fall inside the range.",
|
|
408
|
+
"",
|
|
409
|
+
"Before you write any sentence of the form \"X has produced effect Y\",",
|
|
410
|
+
"or \"the dose increase is showing up as Z\", check explicitly:",
|
|
411
|
+
" 1. Which row of the titration is the CURRENT dose?",
|
|
412
|
+
...asPromptLines(CURRENT_DOSE_RULE, " "),
|
|
413
|
+
...asPromptLines(BUCKET_DOSE_RULE, " "),
|
|
414
|
+
...asPromptLines(STANDARD_DOSING_RULE, " "),
|
|
415
|
+
" 2. What is the date the current dose started (the X in [Since X])?",
|
|
416
|
+
" 3. What is the date of the latest reading for the marker(s) you are",
|
|
417
|
+
" about to credit to the treatment?",
|
|
418
|
+
" 4. If the latest reading PRE-DATES the current dose's start, the",
|
|
419
|
+
" reading CANNOT reflect that current dose's effect — do not",
|
|
420
|
+
" attribute. Say so plainly instead: \"the latest [marker] reading",
|
|
421
|
+
" [date] pre-dates the [drug] titration to [dose] [since-date], so",
|
|
422
|
+
" the current dose has not been re-tested yet; recheck in 8–12",
|
|
423
|
+
" weeks\".",
|
|
424
|
+
" 5. If the latest reading is AFTER the current dose's start but by an",
|
|
425
|
+
" amount too short to expect an effect (e.g. < 2–4 weeks for most",
|
|
426
|
+
" lab markers, < 8 weeks for HbA1c, < 12 weeks for body composition),",
|
|
427
|
+
" say so plainly — the dose is too new to assess yet.",
|
|
428
|
+
" 6. When a reading falls within a closed-range row's window, you may",
|
|
429
|
+
" attribute the effect at THAT historical dose, not the current one.",
|
|
430
|
+
"",
|
|
431
|
+
"This rule applies everywhere a treatment effect is discussed:",
|
|
432
|
+
"progression.recent (this-window trajectory), treatment[i].assessment",
|
|
433
|
+
"(per-item efficacy), decisions.*.pros and decisions.*.recommendation",
|
|
434
|
+
"(when you are arguing whether to add or hold an intervention based on",
|
|
435
|
+
"what the current regimen has already achieved). Do not make up effects",
|
|
436
|
+
"that the timeline does not support.",
|
|
437
|
+
"",
|
|
438
|
+
"WRITING STYLE — Abbreviations.",
|
|
439
|
+
"Spell out every abbreviation on its FIRST appearance within each section,",
|
|
440
|
+
"with the abbreviation in parentheses, then use the abbreviation thereafter",
|
|
441
|
+
"within that same section. Examples of correct first use:",
|
|
442
|
+
' "Non-Alcoholic Fatty Liver Disease (NAFLD)... NAFLD often runs alongside..."',
|
|
443
|
+
' "Apolipoprotein B (ApoB) sits at 76 against a <60 target; ApoB-lowering..."',
|
|
444
|
+
' "Testosterone Replacement Therapy (TRT) raises Free T... TRT also suppresses..."',
|
|
445
|
+
"Treat each of the following as its own section for this rule (abbreviation",
|
|
446
|
+
"usage resets at each section boundary):",
|
|
447
|
+
" • progression.latest",
|
|
448
|
+
" • progression.recent",
|
|
449
|
+
" • progression.overall",
|
|
450
|
+
" • each disease[i].finding",
|
|
451
|
+
" • each treatment[i].assessment",
|
|
452
|
+
" • each decisions.patient[i].recommendation (and again separately for each",
|
|
453
|
+
" entry's pros/cons/alternatives bullets considered as one block)",
|
|
454
|
+
" • each decisions.ai[i].recommendation (same — bullets treated as one block)",
|
|
455
|
+
" • each doctorConversation[i] (questions list considered as one block)",
|
|
456
|
+
"This applies to disease abbreviations (NAFLD, MASLD, ASCVD, OSA, PCOS),",
|
|
457
|
+
"imaging shorthand (CAC, CAD-RADS, LAP), drug-class shorthand (GLP-1,",
|
|
458
|
+
"PCSK9, TRT, SGLT2i, SERM), and marker abbreviations (ApoB, HbA1c, DHEA-S,",
|
|
459
|
+
"IGF-1, LH, FSH, SHBG, ALT, AST, PSA, eGFR). It does NOT apply to commonly-",
|
|
460
|
+
"recognized abbreviations that no reader needs spelled out (HDL, LDL, BMI,",
|
|
461
|
+
"BP, mg/dL, ng/mL, IU). When in doubt, spell it out.",
|
|
462
|
+
"",
|
|
463
|
+
"COLLISION AVOIDANCE — No abbreviation may carry two meanings in the same",
|
|
464
|
+
"document. If two terms could share the same letters, only ONE of them is",
|
|
465
|
+
"permitted to be abbreviated anywhere in this report; the other must be",
|
|
466
|
+
"spelled out in full every single time, even on its tenth appearance.",
|
|
467
|
+
"",
|
|
468
|
+
"Specific reservations in THIS document:",
|
|
469
|
+
" • AI is RESERVED for \"Artificial Intelligence\" (the report frames its",
|
|
470
|
+
' algorithm-surfaced suggestions under the heading "AI Consideration").',
|
|
471
|
+
" NEVER use AI as an abbreviation for Aromatase Inhibitor — spell out",
|
|
472
|
+
" \"aromatase inhibitor\" (lowercase, full phrase) every time it appears,",
|
|
473
|
+
' in prose, in pros/cons bullets, in alternatives, in the',
|
|
474
|
+
' recommendation, and in doctorConversation questions. The same rule',
|
|
475
|
+
" applies if you would ever introduce a drug class abbreviated AI for",
|
|
476
|
+
" any other reason.",
|
|
477
|
+
" • LH is RESERVED for \"Luteinizing Hormone\". Spell out any other term",
|
|
478
|
+
" that would otherwise abbreviate to LH.",
|
|
479
|
+
" • FSH is RESERVED for \"Follicle-Stimulating Hormone\".",
|
|
480
|
+
" • TRT is RESERVED for \"Testosterone Replacement Therapy\". Spell out",
|
|
481
|
+
' any other "replacement therapy" you might discuss.',
|
|
482
|
+
" • CT is RESERVED for the imaging modality (\"computed tomography\").",
|
|
483
|
+
" • E2 is RESERVED for \"estradiol\".",
|
|
484
|
+
"",
|
|
485
|
+
"Before you write any abbreviation, run a quick check: would a reader",
|
|
486
|
+
"encountering this acronym here, after reading the rest of the report,",
|
|
487
|
+
"have to pause to figure out which of two things it means? If yes, spell",
|
|
488
|
+
"the long one out.",
|
|
489
|
+
"",
|
|
490
|
+
"Reply with strict JSON matching the requested schema. The six fields are:",
|
|
491
|
+
"",
|
|
492
|
+
"- progression: an object with three prose fields — latest, recent, overall.",
|
|
493
|
+
" All three address the patient as a whole (cardiometabolic, hormonal, body",
|
|
494
|
+
" composition, hepatic, etc.), not a per-marker bullet list. They differ in",
|
|
495
|
+
" temporal frame:",
|
|
496
|
+
"",
|
|
497
|
+
" progression.latest: 4–8 sentences. Describe where the patient stands RIGHT",
|
|
498
|
+
" NOW, based on each meaningful marker's single most recent reading vs its",
|
|
499
|
+
" personalized target. Cite specific values for the markers that materially",
|
|
500
|
+
' shape the snapshot ("ApoB latest 76 against a <60 target", "free T latest',
|
|
501
|
+
' 62 pg/mL against an 80–150 target"), weighted toward watchlist + out-of-',
|
|
502
|
+
" range markers. This is the \"what does the current picture look like\"",
|
|
503
|
+
" paragraph the patient would read first. Plain prose, no bullets.",
|
|
504
|
+
"",
|
|
505
|
+
" progression.recent: 4–8 sentences. Discuss the EVOLUTION across the last",
|
|
506
|
+
" 12 months — how the picture moved, not where it ended. Speak holistically",
|
|
507
|
+
" about the patient's health (atherogenic risk, glycemic control, hormonal",
|
|
508
|
+
" axis, body composition, hepatic load) and call out the wins and the",
|
|
509
|
+
" setbacks of this window. Tie the year's trajectory to any intervention",
|
|
510
|
+
" started or titrated in this window (drugs, supplements, behavioral",
|
|
511
|
+
" changes). Plain prose.",
|
|
512
|
+
"",
|
|
513
|
+
" progression.overall: 4–8 sentences. Place the Latest snapshot in the",
|
|
514
|
+
" CONTEXT OF THE FULL HISTORICAL DATASET. Where did the patient come from",
|
|
515
|
+
" across every reading on file, including data older than 12 months? Frame",
|
|
516
|
+
" it as progress vs regression over the life of the dataset: is the recent",
|
|
517
|
+
" picture a continuation of long-standing drift, a clear turnaround, or a",
|
|
518
|
+
" partial recovery that has not yet returned to a pre-incident baseline?",
|
|
519
|
+
" Reference the prior-baseline numbers from the Markers block to anchor the",
|
|
520
|
+
" comparison, and contextualize today against where this person started.",
|
|
521
|
+
" Plain prose.",
|
|
522
|
+
"",
|
|
523
|
+
"- disease: an array of { group, finding } entries, one per major marker",
|
|
524
|
+
" area, addressing each individually rather than as a single open paragraph.",
|
|
525
|
+
" Cover the standard longevity-clinic categories — Cardiovascular Risk,",
|
|
526
|
+
" Metabolic Health, Hormonal / Endocrine, Body Composition, Hepatic, Renal,",
|
|
527
|
+
" Inflammation — including every category that is plausibly relevant to",
|
|
528
|
+
" this patient (typically 5–8). Use these exact group names in title case so",
|
|
529
|
+
" they align with healthMarkers.recommended groups where they overlap.",
|
|
530
|
+
"",
|
|
531
|
+
" ORDER the array by clinical severity, highest first. Severity reflects the",
|
|
532
|
+
" strength of the finding(s) within that area: how far markers sit outside",
|
|
533
|
+
" personalized targets, how strongly the pattern points to a real disease",
|
|
534
|
+
" process vs an isolated value, and the magnitude of downstream risk (e.g.",
|
|
535
|
+
" ASCVD, end-organ damage, mortality contribution). Areas with no finding",
|
|
536
|
+
" identified sort to the bottom.",
|
|
537
|
+
"",
|
|
538
|
+
" Within the same severity tier, order acute before chronic — i.e. areas",
|
|
539
|
+
" where the picture is actively deteriorating or recently flipped out of",
|
|
540
|
+
" range come before areas where the abnormality has been stable for years.",
|
|
541
|
+
" An area with a sharp recent change beats an area with a long-standing",
|
|
542
|
+
" drift at the same severity level.",
|
|
543
|
+
"",
|
|
544
|
+
" For each entry:",
|
|
545
|
+
" group: the category name as above.",
|
|
546
|
+
" finding: 3–6 sentences of plain prose. The focus of this section is the",
|
|
547
|
+
" patient's HEALTH — what their situation actually means for them as a",
|
|
548
|
+
" person — not a list of marker values. Lead with the lay meaning, then",
|
|
549
|
+
" substantiate with metrics.",
|
|
550
|
+
"",
|
|
551
|
+
" Sentence 1 (and possibly 2) should explain in plain language what is",
|
|
552
|
+
" happening to the patient's body and what it means for them in",
|
|
553
|
+
" everyday terms — the kind of explanation a smart non-clinician would",
|
|
554
|
+
' take away from a good doctor visit. Examples: "Your cholesterol-',
|
|
555
|
+
' carrying particles are still loading the walls of your arteries faster',
|
|
556
|
+
' than your body clears them — exactly the long-running process behind',
|
|
557
|
+
' most heart attacks and strokes." Or: "Your testosterone is sitting at',
|
|
558
|
+
" the low end of a young man's range, which fits the low energy and",
|
|
559
|
+
' slow recovery you described." Make the meaning vivid and concrete.',
|
|
560
|
+
"",
|
|
561
|
+
" THEN substantiate with the specifics — name the suspected pattern",
|
|
562
|
+
' (e.g. "atherogenic dyslipidemia", "subclinical hypothyroidism",',
|
|
563
|
+
' "insulin resistance") and cite the markers, symptoms, or suspicion',
|
|
564
|
+
' that support it (e.g. "ApoB 76 against a <60 target", "free T 62',
|
|
565
|
+
' pg/mL against an 80–150 target"). When you use a technical phrase',
|
|
566
|
+
" like \"still atherogenic\" or \"baseline but still atherogenic\", pair",
|
|
567
|
+
' it immediately with the plain-language meaning ("still atherogenic —',
|
|
568
|
+
' the particles in your blood are still in the size and number range',
|
|
569
|
+
' that drives plaque buildup"). Be precise about what the technical',
|
|
570
|
+
" term means; do not leave it floating.",
|
|
571
|
+
"",
|
|
572
|
+
' Use "suggests", "consistent with", "raises the possibility of" —',
|
|
573
|
+
" never assert a diagnosis.",
|
|
574
|
+
"",
|
|
575
|
+
" If the data for this area shows no concern, do NOT omit the area —",
|
|
576
|
+
" lead with plain-language reassurance and substantiate. Example: \"The",
|
|
577
|
+
" filtering work your kidneys do is on track for your age and body",
|
|
578
|
+
" type. No finding identified — eGFR, creatinine, and BUN all sit",
|
|
579
|
+
' within their personalized targets." Or for an untracked area: "We',
|
|
580
|
+
" have no way to read this picture right now because the relevant labs",
|
|
581
|
+
' have not been drawn. No finding identified — no inflammatory markers',
|
|
582
|
+
' (hsCRP, ESR) are currently tracked." A short "no finding" entry is',
|
|
583
|
+
" fine; do not pad.",
|
|
584
|
+
"",
|
|
585
|
+
// `decisions` is the one section split down the middle: `ai` is this core's own
|
|
586
|
+
// aiHypothesis node, `patient` belongs to the hypothesisEvaluation leaf (whose mergeInto
|
|
587
|
+
// explicitly leaves `ai` alone). So the section stays and the patient half goes: emit `patient`
|
|
588
|
+
// as an empty array and let the leaf fill it, rather than writing entries the leaf overwrites.
|
|
589
|
+
"- decisions: an object with two fields, patient and ai. Both are arrays",
|
|
590
|
+
" of decision entries with the same shape. Every Rx-specific intervention",
|
|
591
|
+
" you would raise on your own goes in ai. ALWAYS emit patient as an EMPTY",
|
|
592
|
+
" ARRAY — the patient's own proposed interventions are answered elsewhere",
|
|
593
|
+
" and anything you put there is discarded.",
|
|
594
|
+
" Decisions did NOT influence your progression / disease / treatment",
|
|
595
|
+
" analysis above — those sections must stand on their own.",
|
|
596
|
+
"",
|
|
597
|
+
" decisions.ai: the COLLECTIVE, COMPLETE set of specific interventions",
|
|
598
|
+
" the Finding implies for this patient — the AI's own full recommended",
|
|
599
|
+
" Rx/procedure plan, NOT just net-new deltas. Include an entry EVEN IF",
|
|
600
|
+
" the patient already lists it under their Hypothesis or Plan, or is",
|
|
601
|
+
" already taking it; the value is a precise, complete recommendation set.",
|
|
602
|
+
" Constrain to SPECIFIC PRESCRIPTION MEDICATIONS, named evidence-based",
|
|
603
|
+
" supplements with a clear mechanistic role (e.g. methyl-B12 /",
|
|
604
|
+
" L-methylfolate for elevated homocysteine with low or low-normal B12),",
|
|
605
|
+
" or named Rx-equivalent procedures. Exclude generic lifestyle advice",
|
|
606
|
+
" (diet, sleep, exercise, weight loss) and vague \"supplements\".",
|
|
607
|
+
" decisions.ai is for THERAPEUTIC interventions ONLY. NEVER put a",
|
|
608
|
+
" diagnostic test, lab draw, imaging study, panel, or screening here",
|
|
609
|
+
" (e.g. an hsCRP or Lipoprotein(a) measurement, a repeat CTA, a sleep",
|
|
610
|
+
" study) — those are data to OBTAIN, not treatments to weigh, and belong",
|
|
611
|
+
" solely in dataRequisition. Litmus test: if you cannot name at least two",
|
|
612
|
+
" genuine cons AND two real alternative therapies for the SAME goal, it is",
|
|
613
|
+
" not a therapeutic decision — drop it (it is almost certainly a",
|
|
614
|
+
" requisition). Getting a number on file has no therapeutic cons or",
|
|
615
|
+
" alternatives, which is the tell.",
|
|
616
|
+
"",
|
|
617
|
+
" BE PRECISE and name the relationships between options. When a drug",
|
|
618
|
+
" class has a foundation + add-on structure, recommend the FOUNDATION",
|
|
619
|
+
" explicitly rather than assuming it: e.g. for residual ApoB, do NOT",
|
|
620
|
+
" propose a PCSK9 inhibitor \"atop a statin\" without recommending the",
|
|
621
|
+
" statin itself — recommend a specific statin (e.g. rosuvastatin, with a",
|
|
622
|
+
" muscle-sparing note where the patient's goals warrant), then ezetimibe,",
|
|
623
|
+
" then a PCSK9 inhibitor as escalation steps if the ApoB target is not",
|
|
624
|
+
" met. Name a specific agent and, where determinable, a starting dose.",
|
|
625
|
+
" Surface the obvious standard-of-care interventions the Finding implies",
|
|
626
|
+
" even when unglamorous. Note when an item is already in the patient's",
|
|
627
|
+
" regimen or plan (so it reads as confirmation, not a contradiction).",
|
|
628
|
+
" Skip pure dose-titration of an existing drug (that belongs in treatment",
|
|
629
|
+
" assessment). Aim for the complete set the Finding warrants — typically",
|
|
630
|
+
" 3–8 entries, up to 12 for a patient with many open studies.",
|
|
631
|
+
"",
|
|
632
|
+
" For each entry (patient or ai):",
|
|
633
|
+
" intervention: copy the intervention string verbatim from the user",
|
|
634
|
+
' message (e.g. "Testosterone Replacement Therapy", "Tesamorelin"),',
|
|
635
|
+
' or, for ai entries, a concrete drug-class label (e.g. "Statin or',
|
|
636
|
+
' PCSK9 inhibitor", "SGLT2 inhibitor", "Enclomiphene").',
|
|
637
|
+
" purpose: for patient entries, copy the patient's stated purpose",
|
|
638
|
+
' verbatim from the user message (e.g. "improved free T", "reduce',
|
|
639
|
+
' visceral adipose tissue"). For ai entries, a short clause (≤22',
|
|
640
|
+
" words) that LEADS WITH THE BENEFIT — the functional or clinical",
|
|
641
|
+
" outcome the patient actually cares about, tied to their goals or",
|
|
642
|
+
" symptoms where relevant (better overnight HRV and recovery, lower",
|
|
643
|
+
" long-term heart-attack / stroke risk, preserved fertility, more lean",
|
|
644
|
+
" mass for the masters-sport goal) — and THEN names the metric(s) by",
|
|
645
|
+
" which that benefit is measured. Do NOT give a bare metric move as the",
|
|
646
|
+
' whole purpose. e.g. NOT "lower homocysteine and raise Vitamin B12" but',
|
|
647
|
+
' "improve vascular and autonomic recovery (overnight HRV), measured by',
|
|
648
|
+
' homocysteine and Vitamin B12 normalizing"; NOT "lower ApoB to <55" but',
|
|
649
|
+
' "cut long-term heart-attack and stroke risk, measured by ApoB to <55".',
|
|
650
|
+
"",
|
|
651
|
+
" pros: an array of 3–6 short bullets (each ≤30 words) covering the",
|
|
652
|
+
" reasons to pursue this intervention for THIS patient given the",
|
|
653
|
+
" Finding above. Tie each pro to the patient's specific data when",
|
|
654
|
+
' relevant — their actual lab values, age, symptoms, goals, athletic',
|
|
655
|
+
" profile, etc. Cover the upside angles a thoughtful clinician would",
|
|
656
|
+
" raise (e.g. for TRT in a middle-aged man with low Free T and a",
|
|
657
|
+
' masters athletics goal: "lifts Free T from the bottom of the range',
|
|
658
|
+
' where symptoms tend to cluster", "consistent with the patient\'s',
|
|
659
|
+
' masters-sports performance goal", "addresses low-T fatigue and',
|
|
660
|
+
' recovery patterns").',
|
|
661
|
+
"",
|
|
662
|
+
" cons: an array of 3–6 short bullets (each ≤30 words) covering the",
|
|
663
|
+
" reasons NOT to pursue, or the risks/costs that come with it, tied",
|
|
664
|
+
" to THIS patient's profile and goals. Cover the full set a thoughtful",
|
|
665
|
+
" clinician would raise. For TRT specifically that means: impact on",
|
|
666
|
+
" fertility (testicular atrophy, suppressed spermatogenesis), E2",
|
|
667
|
+
" conversion / aromatization and the side effects that follow, drug",
|
|
668
|
+
" dependency / HPG-axis suppression that may be hard to reverse,",
|
|
669
|
+
" cardiovascular and hematocrit considerations, the lifelong",
|
|
670
|
+
" commitment, and whether the patient's age and lab basis (total T",
|
|
671
|
+
" vs Free T) really warrant it. Adapt to the actual intervention",
|
|
672
|
+
" under consideration — Tesamorelin's cons differ (IGF-1 / acromegaly",
|
|
673
|
+
" risk, glucose impact, injection burden, cost, regulatory status).",
|
|
674
|
+
"",
|
|
675
|
+
" alternatives: an array of 2–5 short bullets (each ≤40 words) naming",
|
|
676
|
+
" OTHER ways to pursue the same stated purpose, with a one-clause why.",
|
|
677
|
+
" For TRT targeting low Free T, alternatives include clomid /",
|
|
678
|
+
" enclomiphene (preserves fertility and HPG axis), hCG monotherapy,",
|
|
679
|
+
" addressing SHBG drivers (insulin resistance, fatty liver), weight",
|
|
680
|
+
" loss + sleep optimization, treating the underlying cause if",
|
|
681
|
+
" secondary hypogonadism (prolactin, pituitary). For Tesamorelin",
|
|
682
|
+
" targeting VAT, alternatives include caloric deficit + resistance",
|
|
683
|
+
" training, GLP-1 / GIP agonist titration, SGLT2 inhibitor in the",
|
|
684
|
+
" right context, sleep / cortisol optimization.",
|
|
685
|
+
"",
|
|
686
|
+
" recommendation: 3–6 sentences of plain prose. Synthesize: should the",
|
|
687
|
+
" patient pursue this, hold off, or pursue an alternative first?",
|
|
688
|
+
" Anchor your recommendation to THIS patient's specific data (their",
|
|
689
|
+
" age, the lab values that matter for this decision, fertility goals,",
|
|
690
|
+
" athletic goals, current treatment regimen). Make explicit what GATES",
|
|
691
|
+
" the action: name whether the data already on file is enough to act on",
|
|
692
|
+
" NOW, or whether a specific further reading / plan step must come first.",
|
|
693
|
+
" When the patient can act today, say so and point to the data that",
|
|
694
|
+
" licenses it (e.g. \"lipid markers — ApoB at 76 vs. a target of <55 —",
|
|
695
|
+
" already give you and your doctor enough data to act right now\"); when",
|
|
696
|
+
" it should wait, name the exact gate (the missing draw, the prior drug",
|
|
697
|
+
" that must be on board, the threshold a marker must cross). Be specific",
|
|
698
|
+
" about the conditions under which the answer changes (e.g. \"if fertility is",
|
|
699
|
+
" preserved as a near-term goal, start with enclomiphene rather than",
|
|
700
|
+
" direct TRT\", \"if Free T stays below X after 6 months of lifestyle",
|
|
701
|
+
' and weight loss, then TRT becomes more justifiable"). Use',
|
|
702
|
+
' "consider", "discuss with the prescribing physician", "could be',
|
|
703
|
+
' reasonable if" — never a hard directive.',
|
|
704
|
+
"",
|
|
705
|
+
" If the user message lists no decisions, decisions.patient is an empty",
|
|
706
|
+
" array; decisions.ai is independent and may still have entries if the",
|
|
707
|
+
" Finding motivates them.",
|
|
708
|
+
"",
|
|
709
|
+
"- doctorConversation: an array of { group, questions } entries. The list",
|
|
710
|
+
" has THREE parts in order: first the finding-based groups (one per",
|
|
711
|
+
" disease area, in disease's severity-then-acute order, group names",
|
|
712
|
+
" matching disease groups verbatim), then the patient-decision groups",
|
|
713
|
+
" (one per decisions.patient entry in the same order, group name matching",
|
|
714
|
+
" the intervention verbatim), then the AI-consideration groups (one per",
|
|
715
|
+
" decisions.ai entry in the same order, group name matching the",
|
|
716
|
+
" intervention verbatim).",
|
|
717
|
+
"",
|
|
718
|
+
" Total entries = disease.length + decisions.patient.length +",
|
|
719
|
+
" decisions.ai.length. Every disease group MUST appear; every patient",
|
|
720
|
+
" decision MUST appear; every AI consideration MUST appear. Do not",
|
|
721
|
+
" interleave the three parts.",
|
|
722
|
+
"",
|
|
723
|
+
" For each finding-based entry:",
|
|
724
|
+
" group: copy the corresponding disease entry's group string verbatim",
|
|
725
|
+
' (e.g. "Cardiovascular Risk", "Hormonal / Endocrine").',
|
|
726
|
+
" questions: an array of 2–4 short bullets, each phrased as a topic or",
|
|
727
|
+
" question the patient should literally raise at their next",
|
|
728
|
+
" appointment about THIS group. Conversational tone — write each as",
|
|
729
|
+
" if the patient is reading it off a list. Each bullet 5–25 words.",
|
|
730
|
+
" Within a group, questions may be about a finding itself, about",
|
|
731
|
+
" ongoing treatment for that finding (drugs/supplements the patient",
|
|
732
|
+
" currently takes), or about hypothetical/future treatment (something",
|
|
733
|
+
" to consider adding or changing). Mix is fine — they should cover",
|
|
734
|
+
" the patient's most useful angles for that area.",
|
|
735
|
+
"",
|
|
736
|
+
" For each decision-based entry (patient or AI):",
|
|
737
|
+
" group: copy the intervention name verbatim from the corresponding",
|
|
738
|
+
' decisions.patient or decisions.ai entry (e.g. "Testosterone',
|
|
739
|
+
' Replacement Therapy", "Tesamorelin", "Statin or PCSK9 inhibitor").',
|
|
740
|
+
" questions: an array of 2–4 short bullets phrased as topics or",
|
|
741
|
+
" questions the patient should literally raise about THIS decision.",
|
|
742
|
+
" These are ADDITIONAL questions specific to weighing the decision —",
|
|
743
|
+
" they complement, not duplicate, the finding-based questions above.",
|
|
744
|
+
" Pull directly from the pros/cons/alternatives/recommendation you",
|
|
745
|
+
" wrote for that decision in the decisions section. Examples for TRT:",
|
|
746
|
+
' "Ask if enclomiphene could raise Free T while preserving fertility',
|
|
747
|
+
' before committing to TRT."',
|
|
748
|
+
' "Discuss how E2 will be monitored and managed if we start TRT."',
|
|
749
|
+
' "Ask whether targeting SHBG drivers (weight, sleep) could lift Free',
|
|
750
|
+
' T enough without a prescription."',
|
|
751
|
+
" Conversational tone, 5–25 words each, plain language.",
|
|
752
|
+
"",
|
|
753
|
+
" Avoid obscure clinical terminology (no \"acromegaly\", \"subclinical",
|
|
754
|
+
' hypothyroidism", "aromatization", "atherogenic dyslipidemia") — use',
|
|
755
|
+
' plain language or commonly-known drug-class shorthand ("statin",',
|
|
756
|
+
' "PCSK9", "GLP-1", "estrogen blocker", "TRT", marker names like',
|
|
757
|
+
' "IGF-1" or "ApoB" are fine). Each bullet ties back to a specific',
|
|
758
|
+
" finding or treatment item above — these are the patient's takeaways",
|
|
759
|
+
" condensed, not new analysis.",
|
|
760
|
+
"",
|
|
761
|
+
" Good examples grouped under Cardiovascular Risk:",
|
|
762
|
+
' "Discuss whether a statin or PCSK9 inhibitor would help bring',
|
|
763
|
+
' ApoB to goal."',
|
|
764
|
+
' "Ask whether the current ezetimibe dose should change given the',
|
|
765
|
+
' residual ApoB gap."',
|
|
766
|
+
" Good examples grouped under Hormonal / Endocrine:",
|
|
767
|
+
' "Ask about high IGF-1 alongside low testosterone — does that',
|
|
768
|
+
' pattern mean anything?"',
|
|
769
|
+
' "Ask about the role an estrogen blocker could play if we start',
|
|
770
|
+
' TRT."',
|
|
771
|
+
"",
|
|
772
|
+
" Do not include a closing summary bullet; each item should stand on",
|
|
773
|
+
" its own. If an area genuinely has nothing to ask, you may emit a",
|
|
774
|
+
" single screening-style question rather than padding.",
|
|
775
|
+
"",
|
|
776
|
+
"- definitions: an array of { term, definition, group } entries forming the",
|
|
777
|
+
" comprehensive Abbreviations glossary at the end of the document. After",
|
|
778
|
+
" writing every other section, scan EVERYTHING you wrote — progression",
|
|
779
|
+
" (latest, recent, overall), disease, treatment, decisions.patient,",
|
|
780
|
+
" decisions.ai, doctorConversation, and healthMarkers.recommended",
|
|
781
|
+
" rationales — and include every abbreviation that appears anywhere.",
|
|
782
|
+
" Examples of what to include: disease abbreviations (\"NAFLD\", \"MASLD\",",
|
|
783
|
+
' "OSA", "ASCVD", "PCOS"), imaging shorthand ("CAC", "LAP", "CAD-RADS"),',
|
|
784
|
+
' drug-class shorthand ("GLP-1", "PCSK9", "TRT", "SGLT2i", "SERM"), and',
|
|
785
|
+
' marker abbreviations ("ApoB", "HbA1c", "DHEA-S", "LH", "FSH", "SHBG",',
|
|
786
|
+
' "IGF-1", "ALT", "AST", "PSA", "eGFR"). If you used a term anywhere in',
|
|
787
|
+
" the document — even just once, even inside a rationale string — it",
|
|
788
|
+
" must appear in this glossary.",
|
|
789
|
+
"",
|
|
790
|
+
" Each entry:",
|
|
791
|
+
' term: exactly as written in the sections ("NAFLD", "PCSK9", "ApoB"). Do',
|
|
792
|
+
" not include the expansion in the term itself.",
|
|
793
|
+
" definition: at most half a sentence (roughly 8–18 words). Two shapes:",
|
|
794
|
+
' • For disease/condition/imaging/drug-class abbreviations, expand and',
|
|
795
|
+
' give one brief plain-language meaning. Example: "Non-Alcoholic',
|
|
796
|
+
' Fatty Liver Disease — fat buildup in the liver not caused by',
|
|
797
|
+
' alcohol."',
|
|
798
|
+
" • For marker abbreviations, just expand the abbreviation plus one",
|
|
799
|
+
' short phrase (≤8 words) about what it represents. Example:',
|
|
800
|
+
' "Apolipoprotein B — the carrier protein on artery-clogging',
|
|
801
|
+
' cholesterol particles." Do NOT explain how the marker is measured',
|
|
802
|
+
" or interpreted; the rest of the report does that.",
|
|
803
|
+
" group: the body system this term most directly relates to — copied VERBATIM",
|
|
804
|
+
" from one of the disease[].group names above (e.g. \"Cardiovascular Risk\",",
|
|
805
|
+
' "Body Composition", "Hormonal / Endocrine"). Every definition group MUST',
|
|
806
|
+
" be one of the disease groups; assign each term to its closest-fitting",
|
|
807
|
+
" system even when the term itself isn't obviously about one — never omit",
|
|
808
|
+
" this field.",
|
|
809
|
+
"",
|
|
810
|
+
" Include each term only once, even if it appears in multiple sections.",
|
|
811
|
+
" Sort alphabetically by term. Skip plain-English words and full names",
|
|
812
|
+
" already written out (no need to define \"testosterone\" or \"triglycerides\"",
|
|
813
|
+
" if they were never abbreviated). If you used a term, you must define it",
|
|
814
|
+
" here. The earlier writing-style rule (spell out on first use in each",
|
|
815
|
+
" section) is in addition to, not a replacement for, this glossary.",
|
|
816
|
+
"",
|
|
817
|
+
' REQUIRED: include an entry for "AI" defined as "Artificial Intelligence',
|
|
818
|
+
' — the algorithm that surfaced the entries under \\"AI Consideration\\"',
|
|
819
|
+
' in the Decision section". This is required even if AI does not appear',
|
|
820
|
+
" in any of your prose, because the report itself uses AI in a section",
|
|
821
|
+
" heading and the reader needs to know what it means; the reservation in",
|
|
822
|
+
" the writing-style rules anchors the meaning here.",
|
|
823
|
+
"",
|
|
824
|
+
"- healthMarkers: an object with one field, recommended. This is the",
|
|
825
|
+
" clinic's evidence-based recommendation list — the markers an advanced-",
|
|
826
|
+
" practice longevity clinic (think Peter Attia's framing — cardiovascular",
|
|
827
|
+
" risk, metabolic health, hormone optimization, body composition, hepatic",
|
|
828
|
+
" load, inflammation, mineral/vitamin status) would recommend this",
|
|
829
|
+
" patient track given the Finding you just wrote.",
|
|
830
|
+
"",
|
|
831
|
+
" recommended: an array of groups. Each group is { group, markers } where:",
|
|
832
|
+
" group: a clinical-domain category name in title case, e.g. \"Metabolic",
|
|
833
|
+
' Health", "Cardiovascular Risk", "Hormonal / Endocrine", "Body',
|
|
834
|
+
' Composition", "Hepatic", "Renal", "Inflammation". Use the same group',
|
|
835
|
+
" names as the disease array above where the category exists. Each",
|
|
836
|
+
" group name must appear AT MOST ONCE — if multiple markers fit the",
|
|
837
|
+
" same domain, put them all in one entry's markers array, do not emit",
|
|
838
|
+
" a second group entry with the same name. Pick the most specific",
|
|
839
|
+
" domain for each marker (e.g. PSA → Hormonal / Endocrine or Renal,",
|
|
840
|
+
" not a second Cardiovascular Risk entry).",
|
|
841
|
+
" markers: array of { name, rationale }. Each rationale is one short",
|
|
842
|
+
" sentence (≤25 words) tying the marker back to the Finding (a",
|
|
843
|
+
" specific marker pattern, diagnosis, symptom, or risk lever named",
|
|
844
|
+
" above).",
|
|
845
|
+
" name: when the marker already appears in the On-file markers census,",
|
|
846
|
+
" copy its name from there VERBATIM (e.g. \"Gamma-Glutamyl Transferase",
|
|
847
|
+
" (GGT)\", \"Thyroid-Stimulating Hormone (TSH)\", \"Insulin (Fasting)\",",
|
|
848
|
+
" \"Ferritin\") — do NOT invent a shorthand alias (\"GGT\", \"TSH\",",
|
|
849
|
+
" \"Fasting insulin\"). A name that does not match the census cannot be",
|
|
850
|
+
" reconciled with the patient's existing readings, so it is treated as a",
|
|
851
|
+
" brand-new untracked marker. Coin a new name only for a marker that is",
|
|
852
|
+
" genuinely absent from the census (never measured, e.g. Lp(a)).",
|
|
853
|
+
"",
|
|
854
|
+
" IMPORTANT — this list is FINDING-DRIVEN, not a standard panel.",
|
|
855
|
+
"",
|
|
856
|
+
" Do NOT compile the typical markers of each clinical domain. There is no",
|
|
857
|
+
" obligation to populate every category, no obligation to include the",
|
|
858
|
+
" generic Metabolic Health panel (HbA1c, fasting glucose, fasting insulin,",
|
|
859
|
+
" lipid panel, …) just because Metabolic Health exists, no obligation to",
|
|
860
|
+
" include the generic Cardiovascular Risk panel (ApoB, Lp(a), hsCRP, …)",
|
|
861
|
+
" just because cardiovascular markers exist. We are not reinventing or",
|
|
862
|
+
" rediscovering what a standard blood test covers.",
|
|
863
|
+
"",
|
|
864
|
+
" Every marker on this list must be motivated by something specific you",
|
|
865
|
+
" wrote in the Finding — a marker pattern, diagnosis, symptom, treatment",
|
|
866
|
+
" gap, or risk lever named above. If you cannot point to the exact piece",
|
|
867
|
+
" of the Finding that motivates a marker, do not include it. The rationale",
|
|
868
|
+
" must name that anchor (e.g. \"residual ApoB 76 against <60 target — Lp(a)",
|
|
869
|
+
" distinguishes inherited risk from cleanable lipoprotein burden\"; \"low",
|
|
870
|
+
" Free T + low LH suggests secondary hypogonadism — Prolactin rules out a",
|
|
871
|
+
" pituitary driver\"). Vague rationales like \"useful for metabolic health\"",
|
|
872
|
+
" or \"part of a complete workup\" are disqualifying.",
|
|
873
|
+
"",
|
|
874
|
+
" ALSO make this list COMPREHENSIVE of the interventions under consideration",
|
|
875
|
+
" — both the AI Hypothesis (decisions.ai) and the Patient Hypothesis",
|
|
876
|
+
" (decisions.patient and the Patient Plan). For EVERY intervention proposed",
|
|
877
|
+
" or being weighed, include the markers needed to (a) safely WORK IT UP",
|
|
878
|
+
" before starting and (b) MONITOR response and safety after. A proposed",
|
|
879
|
+
" intervention IS a specific anchor, so these count as Finding-motivated and",
|
|
880
|
+
" are NOT the generic panels barred above; the rationale must name the",
|
|
881
|
+
" intervention (e.g. \"baseline before a Selective Estrogen Receptor",
|
|
882
|
+
" Modulator / enclomiphene — Thyroid-Stimulating Hormone (TSH) and Prolactin",
|
|
883
|
+
" rule out thyroid/pituitary drivers and set a pre-treatment baseline\";",
|
|
884
|
+
" \"statin safety monitoring — Alanine-aminotransferase (ALT, SGPT),",
|
|
885
|
+
" Aspartate-aminotransferase (AST, SGOT), Creatine Kinase\"). A hormonal",
|
|
886
|
+
" HPG-axis agent (TRT, enclomiphene, a SERM, hCG) implies Thyroid-Stimulating",
|
|
887
|
+
" Hormone (TSH), Prolactin, Estradiol, Luteinizing Hormone (LH),",
|
|
888
|
+
" Follicle-Stimulating Hormone (FSH), total and Free testosterone, SHBG, and",
|
|
889
|
+
" Hematocrit. Markers added for this reason enter recommended like any other",
|
|
890
|
+
" — they get a personalized range and appear on the Blood re-test schedule —",
|
|
891
|
+
" so the requisite-data set is complete for acting on the hypotheses, not",
|
|
892
|
+
" just the marker patterns.",
|
|
893
|
+
"",
|
|
894
|
+
" Apply this independent of the Watchlist:",
|
|
895
|
+
" • Include a watchlisted marker only if the Finding specifically",
|
|
896
|
+
" motivates ongoing focus on it; otherwise leave it out.",
|
|
897
|
+
" • Include a non-watchlisted marker only if the Finding specifically",
|
|
898
|
+
" motivates adding it.",
|
|
899
|
+
"",
|
|
900
|
+
" Do NOT restrict yourself to markers the patient already has data for —",
|
|
901
|
+
" if the Finding motivates ordering it, include it with rationale, even",
|
|
902
|
+
" when no data exists yet. Total list size scales with how many specific",
|
|
903
|
+
" anchors the Finding actually contains — often 3–10 markers; a sparse",
|
|
904
|
+
" Finding warrants a sparse list. Do not pad.",
|
|
905
|
+
"",
|
|
906
|
+
"- dataRequisition: the COMPLETE set of additional data this patient should",
|
|
907
|
+
" obtain to act on the Finding, grouped by body system THEN by modality. An",
|
|
908
|
+
" array of { type, group, items } entries (all three keys REQUIRED on every",
|
|
909
|
+
" entry — never omit `group`), where:",
|
|
910
|
+
" type is the data modality in Title Case — e.g. \"Blood\", \"Scan /",
|
|
911
|
+
" Imaging\", \"Screening / Procedure\", \"Functional / Wearable\";",
|
|
912
|
+
" group is the body system this cell informs — copied VERBATIM from one of",
|
|
913
|
+
" the disease[].group names above (e.g. \"Cardiovascular Risk\", \"Hormonal",
|
|
914
|
+
" / Endocrine\"). Every dataRequisition group MUST be one of the disease",
|
|
915
|
+
" groups. Emit ONE entry per (modality × body system): split each",
|
|
916
|
+
" modality's tests by the body system they inform, so the same `type` may",
|
|
917
|
+
" recur under different `group`s (a \"Blood\" entry for Cardiovascular Risk",
|
|
918
|
+
" AND a separate \"Blood\" entry for Hormonal / Endocrine);",
|
|
919
|
+
" items is an array of STRINGS, each one line naming the test/data and WHY in",
|
|
920
|
+
" one phrase joined by \" — \" (e.g. \"Repeat coronary artery calcium / CTA",
|
|
921
|
+
" — ~5 years since the 2021 scan\").",
|
|
922
|
+
" Worked example of ONE entry (note all three keys are present):",
|
|
923
|
+
" { \"type\": \"Blood\", \"group\": \"Cardiovascular Risk\", \"items\": [\"[New] Lipoprotein(a) — never measured; refine cardiovascular risk\"] }",
|
|
924
|
+
" Base it on the Finding (which already reflects Patient Assessment) AND",
|
|
925
|
+
" standard-of-care screening intervals judged against Today. Include, where",
|
|
926
|
+
" warranted:",
|
|
927
|
+
" • GROUP every recommended marker by HOW IT IS MEASURED, never by",
|
|
928
|
+
" convenience. Serum/plasma blood draws go in \"Blood\". DEXA / body-",
|
|
929
|
+
" composition markers — Visceral adipose tissue mass, Android % Fat,",
|
|
930
|
+
" Gynoid % Fat, Total % Fat, Lean mass index, Skeletal muscle mass, Body",
|
|
931
|
+
" fat mass, Weight — go in a separate \"Body Composition\" group (they are",
|
|
932
|
+
" read off a DEXA scan or scale, NOT a blood test; do NOT put them in",
|
|
933
|
+
" Blood). Wearable-derived signals go in \"Functional / Wearable\". The",
|
|
934
|
+
" Blood group AND each marker group is a STRICT VIEW of",
|
|
935
|
+
" healthMarkers.recommended and a re-test SCHEDULE: list every",
|
|
936
|
+
" recommended marker, in its modality group, with WHEN it should next be",
|
|
937
|
+
" measured. A marker may NOT appear unless it is in",
|
|
938
|
+
" healthMarkers.recommended.",
|
|
939
|
+
" TAG every marker item with exactly one of three states as a leading",
|
|
940
|
+
" bracket, so the reader separates them at a glance:",
|
|
941
|
+
" – \"[New] <marker> — never measured; …\": absent from the On-file",
|
|
942
|
+
" census (no prior reading) — draw a first baseline now;",
|
|
943
|
+
" – \"[Overdue] <marker> — last <date>; …\": a prior reading exists but",
|
|
944
|
+
" is past its expected re-test interval (census tag [>6mo — overdue],",
|
|
945
|
+
" or stale for acting on the Finding) — due NOW;",
|
|
946
|
+
" – \"[Due soon] <marker> — last <date>, by <when>; …\": a current",
|
|
947
|
+
" reading exists within interval (census tag [within 6mo]) but a",
|
|
948
|
+
" re-test falls within the next 6 months — event-anchored where the",
|
|
949
|
+
" Finding/Plan implies it (\"8–12 weeks after rosuvastatin starts\",",
|
|
950
|
+
" \"6–8 weeks after the DHEA titration\"), else the routine cadence.",
|
|
951
|
+
" HARD CEILING: no recommended marker may go more than 6 MONTHS without a",
|
|
952
|
+
" re-test, so every marker is [New], [Overdue], or [Due soon]. Do NOT",
|
|
953
|
+
" frame items as \"on file\" vs \"not on file\"; NEVER call a census marker",
|
|
954
|
+
" missing or untracked — the census proves it exists;",
|
|
955
|
+
" • follow-on imaging the Finding implies, with timing driven by Today",
|
|
956
|
+
" vs the date of the prior study (e.g. a repeat coronary artery",
|
|
957
|
+
" calcium / CTA given the years elapsed since the patient's prior",
|
|
958
|
+
" cardiac imaging in Diagnosed Disease);",
|
|
959
|
+
" • RE-SCAN every condition in Diagnosed Disease whose status is tracked",
|
|
960
|
+
" by imaging or a procedure and whose last study is stale relative to",
|
|
961
|
+
" Today, so the requisition UPDATES the prior finding rather than leaving",
|
|
962
|
+
" it frozen at diagnosis. Walk the Diagnosed Disease list and, for each",
|
|
963
|
+
" such condition with no more-recent equivalent study on file, requisition",
|
|
964
|
+
" the modality that re-stages it — e.g. hepatic steatosis / NAFLD →",
|
|
965
|
+
" liver ultrasound or MRI-PDFF with MR elastography / FibroScan (the",
|
|
966
|
+
" quantitative re-stage of steatosis and fibrosis), coronary plaque →",
|
|
967
|
+
" CAC / CTA. State the prior finding, its date, and the elapsed time in",
|
|
968
|
+
" the rationale, and where a Plan drug plausibly changed that organ (e.g.",
|
|
969
|
+
" Tirzepatide / weight loss on hepatic fat) anchor the timing so the",
|
|
970
|
+
" re-scan captures the treated state. Do NOT treat blood enzymes (ALT/AST)",
|
|
971
|
+
" as a substitute for the imaging re-stage of a structural diagnosis.",
|
|
972
|
+
" • age- and history-appropriate standard-of-care screenings that are",
|
|
973
|
+
" due or overdue (e.g. colonoscopy if none in ~10 years, DEXA, skin",
|
|
974
|
+
" check), judged against Today and the patient's age.",
|
|
975
|
+
" Each rationale is one short clause naming WHY — the marker pattern,",
|
|
976
|
+
" diagnosis, elapsed time, or guideline interval. Where the USEFUL timing of",
|
|
977
|
+
" a requisition depends on a step in the Patient Plan — a draw or scan whose",
|
|
978
|
+
" result only becomes meaningful once a planned drug has been started, dosed",
|
|
979
|
+
" to target, or been on board long enough — make that relative timing",
|
|
980
|
+
" EXPLICIT in the rationale, naming the plan step it hinges on (e.g. \"ideally",
|
|
981
|
+
" after the statin has been on board for 6+ months so the result reflects",
|
|
982
|
+
" the future treatment regime\", or \"draw 6–8 weeks after the Tirzepatide",
|
|
983
|
+
" titration to 10–15 mg lands\"). Do this only where it genuinely changes",
|
|
984
|
+
" WHEN to order; routine draws need no such clause. Group only the modalities",
|
|
985
|
+
" that have items; if a group would be empty, omit it. Use the patient's",
|
|
986
|
+
" actual dates and Today to reason about elapsed time; never invent a date.",
|
|
987
|
+
"",
|
|
988
|
+
"- criticalRatios: an array of 2–8 clinically meaningful RATIOS of two markers",
|
|
989
|
+
" that matter for THIS patient's challenges — the relationships a clinician",
|
|
990
|
+
" reads together rather than in isolation (e.g. Triglycerides : HDL for insulin",
|
|
991
|
+
" resistance, Total cholesterol : HDL or ApoB : ApoA1 for atherogenic balance,",
|
|
992
|
+
" Testosterone : Estradiol or DHEA-S : Cortisol for the endocrine axis, Omega",
|
|
993
|
+
" ratios for inflammation). Choose ratios grounded in the Diagnosed Disease,",
|
|
994
|
+
" the markers on file, and the Finding — do not pad with generic ones the data",
|
|
995
|
+
" does not motivate. Each entry has:",
|
|
996
|
+
" name: the ratio's display name, the two markers joined by \" : \" (e.g.",
|
|
997
|
+
" \"Triglycerides : HDL\").",
|
|
998
|
+
" numerator, denominator: the two component marker names, copied VERBATIM",
|
|
999
|
+
" from the On-file census (so the dashboard can find their readings). Both",
|
|
1000
|
+
" must be markers that actually appear in the census.",
|
|
1001
|
+
" unit: the ratio's unit label. For two markers in the SAME unit the ratio is",
|
|
1002
|
+
" dimensionless — use \"\" (empty string). Only set a unit when the ratio",
|
|
1003
|
+
" genuinely carries one.",
|
|
1004
|
+
" meaning: 1–2 sentences in plain language on what the ratio signifies and",
|
|
1005
|
+
" why it matters for this patient.",
|
|
1006
|
+
" generalLow / generalHigh: the population / guideline target band for the",
|
|
1007
|
+
" ratio (omit a bound that is open-ended — e.g. a \"lower is better\" ratio",
|
|
1008
|
+
" may have only generalHigh). generalExplanation: one clause naming the",
|
|
1009
|
+
" guideline basis.",
|
|
1010
|
+
" personalizedLow / personalizedHigh: the target band shifted for THIS",
|
|
1011
|
+
" patient's age, sex, diagnoses, and goal (often tighter than general).",
|
|
1012
|
+
" explanation: the rationale for the personalized target.",
|
|
1013
|
+
" Ground the bands in the actual numbers — if the patient's current ratio sits",
|
|
1014
|
+
" far outside a band you propose, re-check you have the right orientation and",
|
|
1015
|
+
" scale. Do NOT invent a ratio whose components are not both on file.",
|
|
1016
|
+
"",
|
|
1017
|
+
"- patternAntipattern: an object with two prose fields, pattern and",
|
|
1018
|
+
" antipattern, surfacing the clinical PATTERNS and ANTI-PATTERNS at play for",
|
|
1019
|
+
" THIS patient. It is patient-specific (rendered inside the AI Conclusion),",
|
|
1020
|
+
" and is DISTINCT from the report's static methodology Introduction — speak",
|
|
1021
|
+
" about the patient's own data, not the report's approach.",
|
|
1022
|
+
" pattern: 3–6 sentences naming the recognizable medical patterns this",
|
|
1023
|
+
" patient's data fits — the marker clusters, symptom-to-lab",
|
|
1024
|
+
" concordances, and treatment responses that line up with a known",
|
|
1025
|
+
" disease process or physiologic mechanism (e.g. atherogenic",
|
|
1026
|
+
" dyslipidemia, secondary hypogonadism, insulin resistance). Name each",
|
|
1027
|
+
" pattern and cite the specific data that places the patient in it.",
|
|
1028
|
+
" antipattern: 3–6 sentences naming the ANTI-PATTERNS — places where this",
|
|
1029
|
+
" patient's data BREAKS the expected pattern, or where the current",
|
|
1030
|
+
" approach risks a known reasoning/treatment pitfall: markers that",
|
|
1031
|
+
" should move together but don't, a treatment whose marker response",
|
|
1032
|
+
" contradicts expectation, a finding that does not fit the leading",
|
|
1033
|
+
" hypothesis, or a plan step that cuts against the data. Name each",
|
|
1034
|
+
" anti-pattern and the specific data that flags it.",
|
|
1035
|
+
" Both fields are ALWAYS required — one tight paragraph each.",
|
|
1036
|
+
"",
|
|
1037
|
+
"- clinicalSynthesis: an object with two required prose fields (adverse,",
|
|
1038
|
+
" favorable) and one OPTIONAL field (conditioning), rendered as the",
|
|
1039
|
+
" \"Clinical Synthesis\" section. This is the two-track TRAJECTORY narrative a",
|
|
1040
|
+
" clinician delivers — it SYNTHESIZES across findings already established",
|
|
1041
|
+
" elsewhere in this report (Diagnosed Disease, Health Finding, the handed",
|
|
1042
|
+
" marker deltas, Treatment History). It does NOT diagnose: introduce no new",
|
|
1043
|
+
" disease claim or alarm that those sections do not already support.",
|
|
1044
|
+
" VERBATIM ANCHORING (applies to adverse, favorable, and conditioning):",
|
|
1045
|
+
" whenever you state a change, comparison, or delta, quote the source's",
|
|
1046
|
+
" EXACT descriptor for BOTH endpoints in double quotes, word-for-word from",
|
|
1047
|
+
" the Diagnosed Disease summary or the marker reading it came from — e.g.",
|
|
1048
|
+
' "left atrial volume index 29 mL/m²" (2019) → "Moderately dilated left',
|
|
1049
|
+
' atrium, volume index 43 mL/m²" (2024)',
|
|
1050
|
+
" — and only THEN, if useful, add the derived figure (\"a ~14 mL/m²",
|
|
1051
|
+
" increase\"). Never lead with a derived number (an absolute change, a",
|
|
1052
|
+
" percent, a span) without the two quoted endpoints behind it; the reader",
|
|
1053
|
+
" must be able to find each quoted phrase verbatim in the source report.",
|
|
1054
|
+
" Do not invent or round a descriptor the source does not contain.",
|
|
1055
|
+
" adverse: 3–6 sentences on the forces working AGAINST the patient — the",
|
|
1056
|
+
" structural, heritable, or age-clock findings the patient cannot",
|
|
1057
|
+
" lifestyle their way out of (e.g. a rising CAC score, an enlarging",
|
|
1058
|
+
" aortic root, a coded comorbidity). Cite the specific datum behind each",
|
|
1059
|
+
" claim (a delta with its dates, a structural finding, an ICD",
|
|
1060
|
+
" comorbidity).",
|
|
1061
|
+
" favorable: 3–6 sentences on the gains working IN THE PATIENT'S FAVOR —",
|
|
1062
|
+
" the lifestyle- and treatment-driven improvements the data shows. Cite",
|
|
1063
|
+
" the specific marker delta or treatment response behind each, and honor",
|
|
1064
|
+
" DATE AWARENESS: only credit a treatment with a gain when the improving",
|
|
1065
|
+
" reading post-dates that treatment's start.",
|
|
1066
|
+
" conditioning: an OPTIONAL qualitative biological-vs-chronological read",
|
|
1067
|
+
" (a \"conditioning\" / loose heart-age-style observation), included ONLY",
|
|
1068
|
+
" where the data genuinely supports one. It must be explicitly",
|
|
1069
|
+
" qualitative and hedged — NEVER state a computed \"biological age = N\"",
|
|
1070
|
+
" as a clinical fact. If the data does not support such a read, return",
|
|
1071
|
+
' an EMPTY STRING "".',
|
|
1072
|
+
" If one track is genuinely thin, keep it short and honest — do NOT invent",
|
|
1073
|
+
" a counterweight to balance the other. adverse and favorable are always",
|
|
1074
|
+
" required.",
|
|
1075
|
+
"",
|
|
1076
|
+
"- planAssessment: a SHORT holistic assessment of the patient's PATIENT PLAN",
|
|
1077
|
+
" as a whole — the Action / Date table the patient supplied. Evaluate the",
|
|
1078
|
+
" plan in light of EVERYTHING ELSE in this report: their labs, diagnosed",
|
|
1079
|
+
" disease, treatment history, the Finding you wrote, the hypotheses and their",
|
|
1080
|
+
" evaluation. Cover cross-cutting issues: sequencing, gaps, redundancy, and",
|
|
1081
|
+
" how the plan interacts as a set with current treatments and markers. 1–2",
|
|
1082
|
+
" tight paragraphs. If NO Patient Plan was provided (the plan list is empty),",
|
|
1083
|
+
' return an EMPTY STRING "" — do not invent a plan to assess.',
|
|
1084
|
+
"",
|
|
1085
|
+
"- finalThoughts: your closing reflection on the ENTIRE report — the second",
|
|
1086
|
+
" sub-section of AI on Plan, titled \"Final Thoughts\". Step back and",
|
|
1087
|
+
" synthesize across everything: the labs, diagnosed disease, treatment",
|
|
1088
|
+
" history, the Finding, the hypotheses and their evaluation, and the",
|
|
1089
|
+
" Patient Plan. Name the one or two things that matter most, the biggest",
|
|
1090
|
+
" open question, and the single highest-leverage next move. This is ALWAYS",
|
|
1091
|
+
" required (it does not depend on a Patient Plan existing). 1–3 tight",
|
|
1092
|
+
" paragraphs.",
|
|
1093
|
+
"",
|
|
1094
|
+
"- basis: an ARRAY of { key, text } pairs declaring, for each printed",
|
|
1095
|
+
" section in the patient's PDF, WHAT KIND OF CONTENT the section carries",
|
|
1096
|
+
" — user-entered values OR LLM inference — and, for inference, WHICH",
|
|
1097
|
+
" OTHER SECTIONS fed the inference. This is a STRUCTURAL clarification,",
|
|
1098
|
+
" not a descriptive one. Do NOT name specific data values like \"age 54\"",
|
|
1099
|
+
" or \"Tirzepatide 6 mg\" — that is the data itself, not the basis.",
|
|
1100
|
+
"",
|
|
1101
|
+
" Two shapes for every basis text. Pick the right one per key:",
|
|
1102
|
+
"",
|
|
1103
|
+
" SHAPE A — user-entered sections (the section's values come straight",
|
|
1104
|
+
" from the patient's inputs and are rendered as-is in tables or",
|
|
1105
|
+
" definition lists; the LLM did NOT infer them). For these the basis is",
|
|
1106
|
+
" the literal four-word phrase:",
|
|
1107
|
+
" \"Based on user input.\"",
|
|
1108
|
+
" Do not add data, do not name the input fields, do not expand.",
|
|
1109
|
+
"",
|
|
1110
|
+
" SHAPE B — LLM-inferred sections (you wrote the contents by reasoning",
|
|
1111
|
+
" over the patient's data). Basis names the SECTIONS that fed the",
|
|
1112
|
+
" inference, joined as a comma-separated list:",
|
|
1113
|
+
" \"Based on Patient Profile, Proposed Study, Diagnosed Disease, and",
|
|
1114
|
+
" marker readings.\"",
|
|
1115
|
+
" Reference SECTIONS by their PDF heading name (Patient Profile,",
|
|
1116
|
+
" Proposed Plan, Proposed Study, Diagnosed Disease, Treatment History,",
|
|
1117
|
+
" Decision Support, Study Result, Health Finding, Treatment assessment,",
|
|
1118
|
+
" Doctor Conversation, Health Markers, marker readings) — not data values.",
|
|
1119
|
+
" 3 to 7 inputs is typical; if a section drew on truly just one input",
|
|
1120
|
+
" source, one is fine.",
|
|
1121
|
+
"",
|
|
1122
|
+
" Emit EXACTLY 29 entries, one per required key below. Key strings must",
|
|
1123
|
+
" match these tokens VERBATIM (camelCase, no spaces, no synonyms).",
|
|
1124
|
+
"",
|
|
1125
|
+
" When you refer to a section by name in a Shape B basis, use the EXACT",
|
|
1126
|
+
" Title Case heading shown in the PDF — never sentence case, never",
|
|
1127
|
+
" abbreviated — and APPEND an attribution tag in parentheses naming",
|
|
1128
|
+
" WHERE that section comes from. Two tag values are used:",
|
|
1129
|
+
" (user input) — the section is rendered from data the user entered.",
|
|
1130
|
+
" Applies to: Patient Profile, Proposed Plan, Proposed Study, Notes,",
|
|
1131
|
+
" Diagnosed Disease, Treatment History, and Hypothesis Evaluation when",
|
|
1132
|
+
" you are referring to the patient's listed interventions table.",
|
|
1133
|
+
" (AI) — the section's contents are LLM-inferred (you wrote them).",
|
|
1134
|
+
" Applies to: Reasoned Finding, Study Result, Note Result, Health Finding, Treatment",
|
|
1135
|
+
" Assessment, Pattern and Antipattern, Hypothesis Evaluation when you are referring to the",
|
|
1136
|
+
" Finding's analysis of those interventions, Doctor Conversation,",
|
|
1137
|
+
" Health Markers (the curated Recommended set), and Abbreviations.",
|
|
1138
|
+
"",
|
|
1139
|
+
" For raw lab data, write \"your lab data\" — no parens, no tag. It is",
|
|
1140
|
+
" the patient's measured values, neither a user-entered section nor an",
|
|
1141
|
+
" AI inference.",
|
|
1142
|
+
"",
|
|
1143
|
+
" Worked example showing the format:",
|
|
1144
|
+
' "Based on your lab data, Diagnosed Disease (user input), and',
|
|
1145
|
+
' Health Finding (AI)."',
|
|
1146
|
+
"",
|
|
1147
|
+
" All valid section headings (use these EXACT strings when naming them):",
|
|
1148
|
+
" \"Patient Assessment\" (the wrapper for the five user-entered profile /",
|
|
1149
|
+
" plan / study / disease / treatment sub-sections)",
|
|
1150
|
+
" \"Patient Profile\"",
|
|
1151
|
+
" \"Stated Objective\" (was Proposed Plan; the patient's stated goal /",
|
|
1152
|
+
" focus)",
|
|
1153
|
+
" \"Pursued Study\" (was Proposed Study; named investigations the",
|
|
1154
|
+
" patient is pursuing)",
|
|
1155
|
+
" \"Notes\" (the patient's free-text jottings ahead of a visit)",
|
|
1156
|
+
" \"Diagnosed Disease\"",
|
|
1157
|
+
" \"Treatment History\"",
|
|
1158
|
+
" \"Patient Hypothesis\" (a sub-section of Patient Assessment — the table",
|
|
1159
|
+
" of the patient's speculative future-alternative interventions)",
|
|
1160
|
+
" \"AI Hypothesis\" (top-level h3 — the LLM's COLLECTIVE, complete set of",
|
|
1161
|
+
" recommended interventions implied by the Finding, mirroring Patient",
|
|
1162
|
+
" Hypothesis structure; may restate items the patient already lists)",
|
|
1163
|
+
" \"Hypothesis Evaluation\" (top-level h3 — the LLM's per-intervention",
|
|
1164
|
+
" analysis (pros / cons / alternatives / recommendation) for both",
|
|
1165
|
+
" Patient Hypothesis and AI Hypothesis entries)",
|
|
1166
|
+
" \"AI Findings\" (the LLM analysis section — was Reasoned Finding)",
|
|
1167
|
+
" \"Health Progression\"",
|
|
1168
|
+
" \"Study Result\" (the per-Pursued-Study inference sub-section of AI",
|
|
1169
|
+
" Findings, sitting between Health Progression and Health Finding)",
|
|
1170
|
+
" \"Note Result\" (the per-Note inference sub-section of AI Findings,",
|
|
1171
|
+
" sitting between Study Result and Health Finding)",
|
|
1172
|
+
" \"Health Finding\" (was Possible Findings; the per-area disease analysis)",
|
|
1173
|
+
" \"Treatment Assessment\" (Title Case — capital A)",
|
|
1174
|
+
" \"Doctor Conversation\"",
|
|
1175
|
+
" \"Marker Levels\" (the h3 wrapping Blood / Scan / Watch / Other; the",
|
|
1176
|
+
" section carries BOTH raw user lab data AND AI-inferred personalized",
|
|
1177
|
+
" levels. When you reference Marker Levels in a basis line, use the",
|
|
1178
|
+
" attribution tag \"(raw user data and AI)\" — never just (AI) or (user",
|
|
1179
|
+
" input) alone, since both apply.)",
|
|
1180
|
+
" \"Performance to Markers\" (was Health Markers; the h3 summary tables",
|
|
1181
|
+
" of Watchlist + Recommended)",
|
|
1182
|
+
" \"Patient Plan\" (top-level h3 — a patient-entered table of concrete",
|
|
1183
|
+
" Action / Date steps the patient intends to take)",
|
|
1184
|
+
" \"AI Conclusion\" (was AI on Plan; top-level h3 with three sub-sections:",
|
|
1185
|
+
" \"Pattern and Antipattern\" — this patient's clinical patterns / anti-",
|
|
1186
|
+
" patterns — then \"On the Patient Plan\" — your inference on the Patient",
|
|
1187
|
+
" Plan — then \"Final Thoughts\" — your reflection on the whole report)",
|
|
1188
|
+
" \"Pattern and Antipattern\" (the patient-specific patterns / anti-patterns",
|
|
1189
|
+
" sub-section of AI Conclusion; distinct from the static methodology",
|
|
1190
|
+
" Introduction on page 1)",
|
|
1191
|
+
" \"Clinical Synthesis\" (the two-track trajectory synthesis section —",
|
|
1192
|
+
" adverse vs favorable forces plus an optional biological-age read)",
|
|
1193
|
+
" \"Critical Ratios\" (the section identifying clinically meaningful marker",
|
|
1194
|
+
" ratios — components, meaning, and target bands)",
|
|
1195
|
+
" \"Abbreviations\"",
|
|
1196
|
+
" \"your lab data\" (the actual measured marker values — RAW user input;",
|
|
1197
|
+
" no Title Case; distinct from \"Marker Levels\" which are the AI-",
|
|
1198
|
+
" inferred personalized ranges)",
|
|
1199
|
+
"",
|
|
1200
|
+
" USER-ENTERED keys (use Shape A — \"Based on user input.\"):",
|
|
1201
|
+
" patientAssessment (the h3 wrapping the five user-entered profile /",
|
|
1202
|
+
" objective / study / disease / treatment sub-sections)",
|
|
1203
|
+
" patientProfile",
|
|
1204
|
+
" statedObjective",
|
|
1205
|
+
" pursuedStudy",
|
|
1206
|
+
" pursuedNotes",
|
|
1207
|
+
" diagnosedDisease",
|
|
1208
|
+
" treatmentHistory",
|
|
1209
|
+
" correlationHistory (the h4 sub-section of Patient Assessment, sitting",
|
|
1210
|
+
" after Treatment History — a patient-entered table of observed",
|
|
1211
|
+
" Event/Date correlations between symptoms / clinical events and",
|
|
1212
|
+
" treatments or labs)",
|
|
1213
|
+
" patientHypothesis (a sub-section of Patient Assessment holding the",
|
|
1214
|
+
" patient's speculative future-alternative interventions table)",
|
|
1215
|
+
" patientPlan (a sub-section of Patient Assessment holding the patient-",
|
|
1216
|
+
" entered table of concrete Action / Date steps the patient intends",
|
|
1217
|
+
" to take)",
|
|
1218
|
+
"",
|
|
1219
|
+
" LLM-INFERRED keys (use Shape B — name the input sections with",
|
|
1220
|
+
" attribution tags):",
|
|
1221
|
+
" markerLevels (the per-source marker grid — Blood / Scan / Watch /",
|
|
1222
|
+
" Other, wrapped under the h3 Marker Levels): displays BOTH the",
|
|
1223
|
+
" patient's raw measured values (their input — \"your lab data\") AND",
|
|
1224
|
+
" the AI-inferred personalized target levels for each marker. The",
|
|
1225
|
+
" personalized levels themselves are derived from Patient Assessment",
|
|
1226
|
+
" — they are NOT derived from Patient Hypothesis or any AI section.",
|
|
1227
|
+
" The basis must distinguish these two sources explicitly: lab data",
|
|
1228
|
+
" is user input, the levels are AI.",
|
|
1229
|
+
' Example: "Based on your lab data (user input) and AI-inferred',
|
|
1230
|
+
' personalized levels from Patient Assessment (user input)."',
|
|
1231
|
+
" aiFindings (the h3 overall): full Patient Assessment + Marker",
|
|
1232
|
+
" Levels.",
|
|
1233
|
+
' Example: "Based on Patient Assessment (user input) and Marker',
|
|
1234
|
+
' Levels (raw user data and AI)."',
|
|
1235
|
+
// studyResults / noteResults / treatmentAssessment are gone from this list with their
|
|
1236
|
+
// sections: each leaf stamps its own basis at merge time (stampLeafBasis), which is what
|
|
1237
|
+
// LEAF_OWNED_BASIS_KEYS in finding-assemble.ts already assumes.
|
|
1238
|
+
" healthProgression, possibleFindings — these AI Findings sub-sections",
|
|
1239
|
+
" carry the same shape: Patient Assessment + Marker Levels. Use:",
|
|
1240
|
+
' "Based on Patient Assessment (user input) and Marker Levels (raw',
|
|
1241
|
+
' user data and AI)."',
|
|
1242
|
+
" (possibleFindings renders under the heading \"Health Finding\".)",
|
|
1243
|
+
" dataRequisition (the Data Requisition sub-section of Doctor",
|
|
1244
|
+
" Conversation — the additional data to obtain, grouped by modality):",
|
|
1245
|
+
" based on the Finding and standard-of-care intervals. Example:",
|
|
1246
|
+
' "Based on AI Findings (AI), Marker Levels (raw user data and AI),',
|
|
1247
|
+
' and standard-of-care screening intervals."',
|
|
1248
|
+
" doctorConversation (NOW a top-level h3 sitting after Hypothesis",
|
|
1249
|
+
" Evaluation in print order — was a subsection of AI Findings):",
|
|
1250
|
+
" condensed from EVERY prior top-level section. Reference them all.",
|
|
1251
|
+
' Example: "Based on Patient Assessment (user input), Marker Levels',
|
|
1252
|
+
' (raw user data and AI), Patient Hypothesis (user input), AI',
|
|
1253
|
+
' Findings (AI), AI Hypothesis (AI), and Hypothesis Evaluation (AI)."',
|
|
1254
|
+
" aiHypothesis (the top-level h3 holding the LLM's COLLECTIVE recommended",
|
|
1255
|
+
" interventions table): the LLM derives these from Patient Assessment,",
|
|
1256
|
+
" Marker Levels, and AI Findings — the complete set the Finding",
|
|
1257
|
+
" implies, which may overlap the patient's own Hypothesis.",
|
|
1258
|
+
' Example: "Based on Patient Assessment (user input), Marker Levels',
|
|
1259
|
+
' (raw user data and AI), and AI Findings (AI)."',
|
|
1260
|
+
" hypothesisEvaluation (the new top-level h3 doing the per-intervention",
|
|
1261
|
+
" analysis — pros / cons / alternatives / recommendation for both",
|
|
1262
|
+
" Patient Hypothesis and AI Hypothesis entries): based on ALL PRIOR",
|
|
1263
|
+
" TOP-LEVEL SECTIONS (Patient Assessment, Marker Levels, Patient",
|
|
1264
|
+
" Hypothesis, AI Findings, AI Hypothesis).",
|
|
1265
|
+
' Example: "Based on Patient Assessment (user input), Marker Levels',
|
|
1266
|
+
' (raw user data and AI), Patient Hypothesis (user input), AI',
|
|
1267
|
+
' Findings (AI), and AI Hypothesis (AI)."',
|
|
1268
|
+
" healthMarkers (the Performance to Markers section): the user's",
|
|
1269
|
+
" Watchlist and the Finding's recommended set.",
|
|
1270
|
+
" Example: \"Based on your Watchlist (user input) and the Finding's",
|
|
1271
|
+
" Recommended set (AI).\"",
|
|
1272
|
+
" patternAntipattern (the \"Pattern and Antipattern\" sub-section of AI",
|
|
1273
|
+
" Conclusion — this patient's own clinical patterns and anti-patterns):",
|
|
1274
|
+
" based on the Finding and the patient's data.",
|
|
1275
|
+
' Example: "Based on Patient Assessment (user input), Marker Levels',
|
|
1276
|
+
' (raw user data and AI), and AI Findings (AI)."',
|
|
1277
|
+
" clinicalSynthesis (the \"Clinical Synthesis\" two-track trajectory",
|
|
1278
|
+
" section — adverse vs favorable forces, optional biological-age read):",
|
|
1279
|
+
" synthesizes findings already established across the report.",
|
|
1280
|
+
' Example: "Based on Diagnosed Disease (user input), Health Finding',
|
|
1281
|
+
' (AI), Marker Levels (raw user data and AI), and Treatment History',
|
|
1282
|
+
' (user input)."',
|
|
1283
|
+
" criticalRatios (the \"Critical Ratios\" section — clinically meaningful",
|
|
1284
|
+
" marker ratios chosen for this patient): based on the markers on file and",
|
|
1285
|
+
" the Finding's disease analysis.",
|
|
1286
|
+
' Example: "Based on your lab data, Diagnosed Disease (user input), and',
|
|
1287
|
+
' Health Finding (AI)."',
|
|
1288
|
+
" aiOnPlan (the \"On the Patient Plan\" sub-section of AI Conclusion — your",
|
|
1289
|
+
" inference on the Patient Plan in light of everything else): based on",
|
|
1290
|
+
" Patient Plan plus all prior sections.",
|
|
1291
|
+
' Example: "Based on Patient Plan (user input), Patient Assessment',
|
|
1292
|
+
' (user input), Marker Levels (raw user data and AI), AI Findings',
|
|
1293
|
+
' (AI), and Hypothesis Evaluation (AI)."',
|
|
1294
|
+
" finalThoughts (the \"Final Thoughts\" sub-section of AI Conclusion — your",
|
|
1295
|
+
" closing reflection on the whole report): based on every section.",
|
|
1296
|
+
' Example: "Based on every section of this report (user input and',
|
|
1297
|
+
' AI)."',
|
|
1298
|
+
" abbreviations: every other section in the report.",
|
|
1299
|
+
' Example: "Based on every other section (user input and AI) in this',
|
|
1300
|
+
' report."',
|
|
1301
|
+
"",
|
|
1302
|
+
" Each text is ONE sentence ending with a period, no bullet lists, no",
|
|
1303
|
+
" semicolons, no markdown. Keep them short — user-entered keys are 4",
|
|
1304
|
+
" words exactly; LLM-inferred keys are typically 8–20 words.",
|
|
1305
|
+
].join("\n");
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* The retry suffix appended to the user message after one or more rejected attempts.
|
|
1311
|
+
*
|
|
1312
|
+
* Include EVERY prior rejection, not just the latest. A real run burned all six attempts because each
|
|
1313
|
+
* correction said "fix exactly this problem": attempt 4 failed on a duplicate marker group, 5 on a bad
|
|
1314
|
+
* dataRequisition group, 6 on a doctorConversation label — the model fixed each named problem and
|
|
1315
|
+
* broke a different one, and never once saw the accumulated list. Six full Opus generations, no usable
|
|
1316
|
+
* output.
|
|
1317
|
+
*
|
|
1318
|
+
* Exported so both a CLI path and a serverless refresh endpoint share one definition — a fix
|
|
1319
|
+
* to the "fix exactly this problem" singular wording once landed in only one of the two call sites,
|
|
1320
|
+
* so the browser path — the one patients and providers actually use — kept sending the exact
|
|
1321
|
+
* failure that had been measured and removed elsewhere. One definition now; the golden fixture
|
|
1322
|
+
* holds the wording.
|
|
1323
|
+
*
|
|
1324
|
+
* Empty string for no rejections, so callers concatenate unconditionally.
|
|
1325
|
+
*/
|
|
1326
|
+
export function correctionSuffix(priorRejections: string[]): string {
|
|
1327
|
+
if (priorRejections.length === 0) return "";
|
|
1328
|
+
return (
|
|
1329
|
+
`\n\n=== CORRECTIONS — ${priorRejections.length} previous attempt(s) were REJECTED ===\n` +
|
|
1330
|
+
priorRejections.map((r, i) => `${i + 1}. ${r}`).join("\n") +
|
|
1331
|
+
`\nRegenerate the COMPLETE JSON satisfying ALL of the above at once. Every one of these was a ` +
|
|
1332
|
+
`real rejection of one of your own attempts — fixing the last while reintroducing an earlier ` +
|
|
1333
|
+
`one fails again. Keep every other field valid.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// The streaming Opus call + retry-with-correction loop (was the head of generateFinding),
|
|
1338
|
+
// returning a raw, validated FindingAIResponse. Anthropic client injected; no Node fs/process. The
|
|
1339
|
+
// caller assembles it (assembleFinding) and stamps hashes.
|
|
1340
|
+
/**
|
|
1341
|
+
* The request knobs a Finding generation needs, in one place.
|
|
1342
|
+
*
|
|
1343
|
+
* Adaptive thinking is opt-in by model family, and the token budget is large enough that changing
|
|
1344
|
+
* it is a cost decision. Both were once duplicated in a caller, so one caller's Finding could
|
|
1345
|
+
* silently stop matching another's — which is the invariant this module exists to protect.
|
|
1346
|
+
*/
|
|
1347
|
+
export const FINDING_MAX_TOKENS = 128000;
|
|
1348
|
+
|
|
1349
|
+
export function findingRequestParams(model: string): {
|
|
1350
|
+
max_tokens: number;
|
|
1351
|
+
thinking?: { type: "adaptive" };
|
|
1352
|
+
} {
|
|
1353
|
+
// Returned as ONE spreadable object rather than separate pieces: `thinking` has to land as
|
|
1354
|
+
// `thinking: { type: "adaptive" }` in the request, and handing a caller the inner object invites
|
|
1355
|
+
// it to spread that instead — which type-checks (the request body is loosely typed) and silently
|
|
1356
|
+
// turns adaptive thinking off.
|
|
1357
|
+
return model.toLowerCase().includes("opus")
|
|
1358
|
+
? { max_tokens: FINDING_MAX_TOKENS, thinking: { type: "adaptive" } }
|
|
1359
|
+
: { max_tokens: FINDING_MAX_TOKENS };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
export async function generateFindingResponse(
|
|
1363
|
+
anthropic: Anthropic,
|
|
1364
|
+
client: Client,
|
|
1365
|
+
model: string,
|
|
1366
|
+
usage?: UsageRecorder,
|
|
1367
|
+
/** Called with the validation message each time an attempt is rejected and a correction retried. */
|
|
1368
|
+
onAttemptFailed?: (attempt: number, reason: string) => void,
|
|
1369
|
+
): Promise<FindingAIResponse> {
|
|
1370
|
+
const requestParams = findingRequestParams(model);
|
|
1371
|
+
// Include EVERY prior rejection, not just the latest. A real run burned all six attempts because each
|
|
1372
|
+
// correction said "fix exactly this problem": attempt 4 failed on a duplicate marker group, 5 on a
|
|
1373
|
+
// bad dataRequisition group, 6 on a doctorConversation label — the model fixed each named problem
|
|
1374
|
+
// and broke a different one, and never once saw the accumulated list. Six full Opus generations, no
|
|
1375
|
+
// usable output.
|
|
1376
|
+
const priorRejections: string[] = [];
|
|
1377
|
+
const oneAttempt = async (): Promise<FindingAIResponse> => {
|
|
1378
|
+
const userContent = buildUserMessage(client) + correctionSuffix(priorRejections);
|
|
1379
|
+
const response = await anthropic.messages
|
|
1380
|
+
.stream({
|
|
1381
|
+
model,
|
|
1382
|
+
...requestParams,
|
|
1383
|
+
system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
|
|
1384
|
+
messages: [{ role: "user", content: userContent }],
|
|
1385
|
+
})
|
|
1386
|
+
.finalMessage();
|
|
1387
|
+
usage?.record(model, response.usage);
|
|
1388
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
1389
|
+
if (!textBlock || textBlock.type !== "text") throw new Error("no text block in finding response");
|
|
1390
|
+
const raw = extractJson(textBlock.text);
|
|
1391
|
+
let candidate: FindingAIResponse;
|
|
1392
|
+
try {
|
|
1393
|
+
candidate = JSON.parse(raw);
|
|
1394
|
+
} catch (e) {
|
|
1395
|
+
const msg = (e as Error).message;
|
|
1396
|
+
const m = msg.match(/position (\d+)/);
|
|
1397
|
+
const ctx = m ? raw.slice(Math.max(0, +m[1] - 120), +m[1] + 60) : raw.slice(-180);
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`invalid JSON in finding response (stop_reason=${response.stop_reason}, len=${raw.length}, ` +
|
|
1400
|
+
`lastChar=${JSON.stringify(raw.slice(-1))}): ${msg}\n…context around failure: …${ctx}…`,
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const plannedActions = plannedLabels(client, new Date().toISOString().slice(0, 10));
|
|
1404
|
+
validate(candidate, {
|
|
1405
|
+
patient: [
|
|
1406
|
+
...(client.factors?.decisions ?? []).map((d) => d.intervention.trim()),
|
|
1407
|
+
...plannedActions,
|
|
1408
|
+
],
|
|
1409
|
+
planActions: plannedActions,
|
|
1410
|
+
noteIds: populatedNoteEntries(client).map((n) => n.id),
|
|
1411
|
+
});
|
|
1412
|
+
return candidate;
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
// Each attempt is a WHOLE Opus generation — roughly $5 and several minutes. Six of them bought
|
|
1416
|
+
// nothing on the run that motivated the accumulation above, so the ceiling is now three: enough for
|
|
1417
|
+
// the correction loop to work (most rejections clear on the second try), cheap enough that a
|
|
1418
|
+
// pathological run costs one Finding rather than six.
|
|
1419
|
+
const MAX_ATTEMPTS = 3;
|
|
1420
|
+
let parsed: FindingAIResponse | undefined;
|
|
1421
|
+
let lastErr: unknown;
|
|
1422
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS && !parsed; attempt++) {
|
|
1423
|
+
try {
|
|
1424
|
+
parsed = await oneAttempt();
|
|
1425
|
+
} catch (e) {
|
|
1426
|
+
lastErr = e;
|
|
1427
|
+
const correction = String((e as Error).message).slice(0, 600);
|
|
1428
|
+
if (!priorRejections.includes(correction)) priorRejections.push(correction);
|
|
1429
|
+
// A silent retry is indistinguishable from a hang. A live check once took 32 minutes on
|
|
1430
|
+
// this loop and only the token count revealed it had run three full generations; the reason for
|
|
1431
|
+
// each was discarded into `correction` and never surfaced. The message can name a treatment or
|
|
1432
|
+
// a study, so it is PHI-adjacent: this reports it to the CALLER, which decides where it may go
|
|
1433
|
+
// (a CLI prints it locally; a host web client sends the server a category, never the prose).
|
|
1434
|
+
onAttemptFailed?.(attempt, correction);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (!parsed) throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
1438
|
+
return parsed;
|
|
1439
|
+
}
|