justlabs-mcp-server 1.0.0 → 1.1.0

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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Basic tests for analysis modules
3
+ * Run with: npx ts-node src/__tests__/analysis.test.ts
4
+ */
5
+ export {};
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Basic tests for analysis modules
3
+ * Run with: npx ts-node src/__tests__/analysis.test.ts
4
+ */
5
+ import { classifyValue, checkCriticalValue, getReferenceRange, getApplicableRange } from "../ranges/questRanges.js";
6
+ import { analyzePanelResults } from "../analysis/analyzePanelResults.js";
7
+ import { suggestNextSteps } from "../analysis/suggestNextSteps.js";
8
+ import { generatePatientSummary } from "../analysis/generatePatientSummary.js";
9
+ let passed = 0;
10
+ let failed = 0;
11
+ function test(name, fn) {
12
+ try {
13
+ fn();
14
+ console.log(`✓ ${name}`);
15
+ passed++;
16
+ }
17
+ catch (e) {
18
+ console.log(`✗ ${name}`);
19
+ console.log(` Error: ${e.message}`);
20
+ failed++;
21
+ }
22
+ }
23
+ function expect(actual) {
24
+ return {
25
+ toBe(expected) {
26
+ if (actual !== expected) {
27
+ throw new Error(`Expected ${expected}, got ${actual}`);
28
+ }
29
+ },
30
+ toBeTruthy() {
31
+ if (!actual) {
32
+ throw new Error(`Expected truthy value, got ${actual}`);
33
+ }
34
+ },
35
+ toBeFalsy() {
36
+ if (actual) {
37
+ throw new Error(`Expected falsy value, got ${actual}`);
38
+ }
39
+ },
40
+ toContain(expected) {
41
+ if (typeof actual !== "string" || !actual.includes(expected)) {
42
+ throw new Error(`Expected "${actual}" to contain "${expected}"`);
43
+ }
44
+ },
45
+ };
46
+ }
47
+ console.log("\n=== Range Classification Tests ===\n");
48
+ test("classifies value below range as low", () => {
49
+ const result = classifyValue(0.3, { low: 0.45, high: 4.5, unit: "mIU/L" });
50
+ expect(result).toBe("low");
51
+ });
52
+ test("classifies value above range as high", () => {
53
+ const result = classifyValue(5.0, { low: 0.45, high: 4.5, unit: "mIU/L" });
54
+ expect(result).toBe("high");
55
+ });
56
+ test("classifies value within range as normal", () => {
57
+ const result = classifyValue(2.5, { low: 0.45, high: 4.5, unit: "mIU/L" });
58
+ expect(result).toBe("normal");
59
+ });
60
+ test("classifies value with only upper bound", () => {
61
+ const result = classifyValue(50, { high: 34, unit: "IU/mL" });
62
+ expect(result).toBe("high");
63
+ });
64
+ console.log("\n=== Critical Value Detection Tests ===\n");
65
+ test("detects critical high glucose", () => {
66
+ const result = checkCriticalValue("glucose", 450);
67
+ expect(result.isCritical).toBeTruthy();
68
+ });
69
+ test("does not flag normal glucose", () => {
70
+ const result = checkCriticalValue("glucose", 95);
71
+ expect(result.isCritical).toBeFalsy();
72
+ });
73
+ test("detects critical low potassium", () => {
74
+ const result = checkCriticalValue("potassium", 2.3);
75
+ expect(result.isCritical).toBeTruthy();
76
+ });
77
+ test("detects critical high TSH", () => {
78
+ const result = checkCriticalValue("tsh", 60);
79
+ expect(result.isCritical).toBeTruthy();
80
+ });
81
+ console.log("\n=== Sex-Specific Range Tests ===\n");
82
+ test("returns male-specific ferritin range", () => {
83
+ const biomarker = getReferenceRange("ferritin");
84
+ expect(biomarker).toBeTruthy();
85
+ const range = getApplicableRange(biomarker, "male");
86
+ expect(range.high).toBe(400);
87
+ });
88
+ test("returns female-specific ferritin range", () => {
89
+ const biomarker = getReferenceRange("ferritin");
90
+ const range = getApplicableRange(biomarker, "female");
91
+ expect(range.high).toBe(200);
92
+ });
93
+ console.log("\n=== Panel Analysis Tests ===\n");
94
+ test("analyzes fatigue panel with normal results", () => {
95
+ const analysis = analyzePanelResults({
96
+ panel_id: "fatigue_panel",
97
+ results: [
98
+ { biomarker_key: "tsh", value: 2.0, unit: "mIU/L" },
99
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
100
+ { biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
101
+ { biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
102
+ { biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
103
+ { biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
104
+ ],
105
+ });
106
+ expect(analysis.summary.overall_status).toBe("all_normal");
107
+ expect(analysis.red_flags_detected).toBeFalsy();
108
+ });
109
+ test("detects abnormal TSH in fatigue panel", () => {
110
+ const analysis = analyzePanelResults({
111
+ panel_id: "fatigue_panel",
112
+ results: [
113
+ { biomarker_key: "tsh", value: 8.0, unit: "mIU/L" },
114
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
115
+ { biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
116
+ { biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
117
+ { biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
118
+ { biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
119
+ ],
120
+ });
121
+ expect(analysis.summary.overall_status).toBe("some_outside_range");
122
+ expect(analysis.key_findings.length).toBe(1);
123
+ expect(analysis.key_findings[0].biomarker_key).toBe("tsh");
124
+ });
125
+ test("stops analysis on critical value", () => {
126
+ const analysis = analyzePanelResults({
127
+ panel_id: "fatigue_panel",
128
+ results: [
129
+ { biomarker_key: "tsh", value: 60, unit: "mIU/L" }, // Critical high
130
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
131
+ ],
132
+ });
133
+ expect(analysis.red_flags_detected).toBeTruthy();
134
+ expect(analysis.summary.overall_status).toBe("critical");
135
+ });
136
+ console.log("\n=== Next Steps Suggestion Tests ===\n");
137
+ test("suggests thyroid deep dive for abnormal TSH", () => {
138
+ const analysis = analyzePanelResults({
139
+ panel_id: "fatigue_panel",
140
+ results: [
141
+ { biomarker_key: "tsh", value: 8.0, unit: "mIU/L" },
142
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
143
+ { biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
144
+ { biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
145
+ { biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
146
+ { biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
147
+ ],
148
+ });
149
+ const nextSteps = suggestNextSteps("fatigue_panel", analysis);
150
+ expect(nextSteps.next_step.type).toBe("additional_testing");
151
+ if (nextSteps.next_step.type === "additional_testing") {
152
+ expect(nextSteps.next_step.recommended_panel_id).toBe("thyroid_deep_dive");
153
+ }
154
+ });
155
+ test("suggests clinician discussion for all normal results", () => {
156
+ const analysis = analyzePanelResults({
157
+ panel_id: "fatigue_panel",
158
+ results: [
159
+ { biomarker_key: "tsh", value: 2.0, unit: "mIU/L" },
160
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
161
+ { biomarker_key: "vitamin-b12", value: 500, unit: "pg/mL" },
162
+ { biomarker_key: "vitamin-d", value: 45, unit: "ng/mL" },
163
+ { biomarker_key: "hemoglobin", value: 14, unit: "g/dL" },
164
+ { biomarker_key: "wbc", value: 7.0, unit: "K/uL" },
165
+ ],
166
+ });
167
+ const nextSteps = suggestNextSteps("fatigue_panel", analysis);
168
+ expect(nextSteps.next_step.type).toBe("clinician_discussion");
169
+ });
170
+ console.log("\n=== Patient Summary Tests ===\n");
171
+ test("generates summary with disclaimer", () => {
172
+ const analysis = analyzePanelResults({
173
+ panel_id: "fatigue_panel",
174
+ results: [
175
+ { biomarker_key: "tsh", value: 2.0, unit: "mIU/L" },
176
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
177
+ ],
178
+ });
179
+ const nextSteps = suggestNextSteps("fatigue_panel", analysis);
180
+ const summary = generatePatientSummary("fatigue_panel", analysis, nextSteps);
181
+ expect(summary.patient_summary.disclaimer).toContain("educational purposes only");
182
+ expect(summary.patient_summary.requires_acknowledgment).toBeTruthy();
183
+ });
184
+ test("includes CTA only for additional testing", () => {
185
+ const analysis = analyzePanelResults({
186
+ panel_id: "fatigue_panel",
187
+ results: [
188
+ { biomarker_key: "tsh", value: 8.0, unit: "mIU/L" }, // Abnormal
189
+ { biomarker_key: "ferritin", value: 80, unit: "ng/mL" },
190
+ ],
191
+ });
192
+ const nextSteps = suggestNextSteps("fatigue_panel", analysis);
193
+ const summary = generatePatientSummary("fatigue_panel", analysis, nextSteps);
194
+ expect(summary.patient_summary.cta).toBeTruthy();
195
+ expect(summary.patient_summary.cta?.url).toContain("justlabs.health");
196
+ });
197
+ // Summary
198
+ console.log("\n=================================");
199
+ console.log(`Tests: ${passed} passed, ${failed} failed`);
200
+ console.log("=================================\n");
201
+ if (failed > 0) {
202
+ process.exit(1);
203
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Analyze panel results - educational context only, no diagnostic language
3
+ */
4
+ export interface ResultInput {
5
+ biomarker_key: string;
6
+ value: number;
7
+ unit: string;
8
+ }
9
+ export interface AnalysisInput {
10
+ panel_id: string;
11
+ results: ResultInput[];
12
+ sex_at_birth?: "male" | "female";
13
+ age?: number;
14
+ fasting?: boolean;
15
+ on_thyroid_meds?: boolean;
16
+ }
17
+ export interface Finding {
18
+ biomarker_key: string;
19
+ display_name: string;
20
+ value: number;
21
+ unit: string;
22
+ status: "low" | "high" | "normal" | "unknown";
23
+ reference_range: string;
24
+ educational_context: string;
25
+ note?: string;
26
+ }
27
+ export interface AnalysisOutput {
28
+ panel_id: string;
29
+ panel_name: string;
30
+ summary: {
31
+ overall_status: "all_normal" | "some_outside_range" | "needs_attention" | "critical";
32
+ headline: string;
33
+ };
34
+ key_findings: Finding[];
35
+ reassuring_findings: string[];
36
+ missing_required_markers: string[];
37
+ red_flags_detected: boolean;
38
+ red_flag_message: string | null;
39
+ disclaimer_acknowledged: boolean;
40
+ }
41
+ /**
42
+ * Analyze panel results with educational context
43
+ * Returns structured findings without diagnostic language
44
+ */
45
+ export declare function analyzePanelResults(input: AnalysisInput): AnalysisOutput;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Analyze panel results - educational context only, no diagnostic language
3
+ */
4
+ import { getReferenceRange, getApplicableRange, classifyValue, checkCriticalValue, formatReferenceRange, } from "../ranges/questRanges.js";
5
+ import { getPanel } from "../ranges/panels.js";
6
+ /**
7
+ * Analyze panel results with educational context
8
+ * Returns structured findings without diagnostic language
9
+ */
10
+ export function analyzePanelResults(input) {
11
+ const panel = getPanel(input.panel_id);
12
+ if (!panel) {
13
+ return {
14
+ panel_id: input.panel_id,
15
+ panel_name: "Unknown Panel",
16
+ summary: {
17
+ overall_status: "needs_attention",
18
+ headline: "Unable to analyze: panel not found",
19
+ },
20
+ key_findings: [],
21
+ reassuring_findings: [],
22
+ missing_required_markers: [],
23
+ red_flags_detected: false,
24
+ red_flag_message: null,
25
+ disclaimer_acknowledged: false,
26
+ };
27
+ }
28
+ // Check for critical values first
29
+ const criticalFindings = [];
30
+ for (const result of input.results) {
31
+ const critical = checkCriticalValue(result.biomarker_key, result.value);
32
+ if (critical.isCritical && critical.message) {
33
+ criticalFindings.push({
34
+ key: result.biomarker_key,
35
+ message: critical.message,
36
+ });
37
+ }
38
+ }
39
+ if (criticalFindings.length > 0) {
40
+ return {
41
+ panel_id: input.panel_id,
42
+ panel_name: panel.name,
43
+ summary: {
44
+ overall_status: "critical",
45
+ headline: "Important: Please contact a healthcare provider promptly",
46
+ },
47
+ key_findings: [],
48
+ reassuring_findings: [],
49
+ missing_required_markers: [],
50
+ red_flags_detected: true,
51
+ red_flag_message: `One or more values require prompt medical attention. ${criticalFindings.map((f) => f.message).join(" ")} Please contact your healthcare provider or seek medical care.`,
52
+ disclaimer_acknowledged: false,
53
+ };
54
+ }
55
+ // Check for missing required markers
56
+ const providedKeys = input.results.map((r) => r.biomarker_key.toLowerCase());
57
+ const missingMarkers = panel.requiredMarkers.filter((m) => !providedKeys.includes(m.toLowerCase()));
58
+ // Analyze each result
59
+ const keyFindings = [];
60
+ const reassuringFindings = [];
61
+ for (const result of input.results) {
62
+ const biomarkerRange = getReferenceRange(result.biomarker_key);
63
+ if (!biomarkerRange) {
64
+ keyFindings.push({
65
+ biomarker_key: result.biomarker_key,
66
+ display_name: result.biomarker_key,
67
+ value: result.value,
68
+ unit: result.unit,
69
+ status: "unknown",
70
+ reference_range: "Not available",
71
+ educational_context: "Reference range not available for this marker.",
72
+ });
73
+ continue;
74
+ }
75
+ const applicableRange = getApplicableRange(biomarkerRange, input.sex_at_birth);
76
+ const status = classifyValue(result.value, applicableRange);
77
+ const referenceRangeStr = formatReferenceRange(applicableRange);
78
+ // Build educational context based on status
79
+ let educationalContext = biomarkerRange.educationalContext;
80
+ let note;
81
+ if (status === "low" && biomarkerRange.lowContext) {
82
+ educationalContext = biomarkerRange.lowContext;
83
+ }
84
+ else if (status === "high" && biomarkerRange.highContext) {
85
+ educationalContext = biomarkerRange.highContext;
86
+ }
87
+ // Add medication notes if relevant
88
+ if (input.on_thyroid_meds && ["tsh", "free-t4", "free-t3"].includes(result.biomarker_key.toLowerCase())) {
89
+ note = "Note: Thyroid medication can affect these values. Results are best interpreted by your prescribing clinician.";
90
+ }
91
+ // Add fasting note if relevant
92
+ if (!input.fasting && ["glucose", "insulin", "triglycerides"].includes(result.biomarker_key.toLowerCase())) {
93
+ note = "Note: This marker is ideally measured after fasting. Non-fasting values may be higher.";
94
+ }
95
+ const finding = {
96
+ biomarker_key: result.biomarker_key,
97
+ display_name: biomarkerRange.displayName,
98
+ value: result.value,
99
+ unit: result.unit,
100
+ status,
101
+ reference_range: referenceRangeStr,
102
+ educational_context: educationalContext,
103
+ note,
104
+ };
105
+ if (status === "normal") {
106
+ reassuringFindings.push(`${biomarkerRange.displayName} is within the expected range.`);
107
+ }
108
+ else {
109
+ keyFindings.push(finding);
110
+ }
111
+ }
112
+ // Determine overall status
113
+ let overallStatus = "all_normal";
114
+ let headline = "Your results are within expected ranges.";
115
+ if (keyFindings.length > 0) {
116
+ overallStatus = "some_outside_range";
117
+ const outsideRangeCount = keyFindings.filter((f) => f.status !== "unknown").length;
118
+ headline = `${outsideRangeCount} marker${outsideRangeCount !== 1 ? "s" : ""} outside the typical range. See details below.`;
119
+ }
120
+ if (missingMarkers.length > 0) {
121
+ headline += ` Note: ${missingMarkers.length} expected marker${missingMarkers.length !== 1 ? "s" : ""} not provided.`;
122
+ }
123
+ return {
124
+ panel_id: input.panel_id,
125
+ panel_name: panel.name,
126
+ summary: {
127
+ overall_status: overallStatus,
128
+ headline,
129
+ },
130
+ key_findings: keyFindings,
131
+ reassuring_findings: reassuringFindings,
132
+ missing_required_markers: missingMarkers,
133
+ red_flags_detected: false,
134
+ red_flag_message: null,
135
+ disclaimer_acknowledged: false,
136
+ };
137
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Generate patient-friendly summary with required disclaimer
3
+ */
4
+ import { AnalysisOutput } from "./analyzePanelResults.js";
5
+ import { NextStepsOutput } from "./suggestNextSteps.js";
6
+ export interface PatientSummaryCTA {
7
+ label: string;
8
+ url: string;
9
+ }
10
+ export interface PatientSummary {
11
+ headline: string;
12
+ bullets: string[];
13
+ cta: PatientSummaryCTA | null;
14
+ disclaimer: string;
15
+ requires_acknowledgment: boolean;
16
+ }
17
+ export interface PatientSummaryOutput {
18
+ patient_summary: PatientSummary;
19
+ safety_warning: string | null;
20
+ }
21
+ /**
22
+ * Generate a patient-friendly summary
23
+ * Requires disclaimer acknowledgment before showing analysis
24
+ */
25
+ export declare function generatePatientSummary(panelId: string, analysis: AnalysisOutput, nextSteps: NextStepsOutput): PatientSummaryOutput;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Generate patient-friendly summary with required disclaimer
3
+ */
4
+ const DISCLAIMER = `This summary is for educational purposes only and is not medical advice. Reference ranges may vary by lab and individual factors. Always discuss your results with a qualified healthcare provider before making any health decisions.`;
5
+ const SAFETY_DISCLAIMER = `IMPORTANT: One or more of your results requires prompt medical attention. Please contact your healthcare provider or seek medical care before reviewing this summary further.`;
6
+ /**
7
+ * Generate a patient-friendly summary
8
+ * Requires disclaimer acknowledgment before showing analysis
9
+ */
10
+ export function generatePatientSummary(panelId, analysis, nextSteps) {
11
+ // Safety check first
12
+ if (analysis.red_flags_detected || nextSteps.safety.red_flags_detected) {
13
+ return {
14
+ patient_summary: {
15
+ headline: "Please contact a healthcare provider",
16
+ bullets: [
17
+ "One or more values in your results need prompt medical review.",
18
+ "Do not delay seeking care based on this summary.",
19
+ "Your healthcare provider can evaluate these findings properly.",
20
+ ],
21
+ cta: null,
22
+ disclaimer: DISCLAIMER,
23
+ requires_acknowledgment: true,
24
+ },
25
+ safety_warning: nextSteps.safety.message || SAFETY_DISCLAIMER,
26
+ };
27
+ }
28
+ // Build summary based on analysis and next steps
29
+ const bullets = [];
30
+ // Add headline context
31
+ if (analysis.summary.overall_status === "all_normal") {
32
+ bullets.push("All markers reviewed are within expected ranges.");
33
+ }
34
+ else if (analysis.key_findings.length > 0) {
35
+ bullets.push(`${analysis.key_findings.length} marker${analysis.key_findings.length !== 1 ? "s" : ""} outside the typical range.`);
36
+ }
37
+ // Add key findings (max 2)
38
+ const significantFindings = analysis.key_findings
39
+ .filter((f) => f.status === "low" || f.status === "high")
40
+ .slice(0, 2);
41
+ for (const finding of significantFindings) {
42
+ const direction = finding.status === "low" ? "below" : "above";
43
+ bullets.push(`${finding.display_name}: ${finding.value} ${finding.unit} (${direction} typical range of ${finding.reference_range})`);
44
+ }
45
+ // Add reassuring note if some things are normal
46
+ if (analysis.reassuring_findings.length > 0 && analysis.key_findings.length > 0) {
47
+ bullets.push(`${analysis.reassuring_findings.length} other marker${analysis.reassuring_findings.length !== 1 ? "s" : ""} within expected ranges.`);
48
+ }
49
+ // Add next step guidance
50
+ if (nextSteps.next_step.type === "additional_testing") {
51
+ bullets.push(`For more detail, consider: ${nextSteps.next_step.recommended_panel_name}`);
52
+ }
53
+ else if (nextSteps.next_step.type === "clinician_discussion") {
54
+ bullets.push("Consider discussing these results with your healthcare provider.");
55
+ }
56
+ // Ensure we have 3-5 bullets
57
+ if (bullets.length < 3) {
58
+ bullets.push("These results provide a snapshot of selected health markers.");
59
+ }
60
+ if (bullets.length < 3) {
61
+ bullets.push("A healthcare provider can help put these in context with your overall health.");
62
+ }
63
+ // Cap at 5 bullets
64
+ const finalBullets = bullets.slice(0, 5);
65
+ // Build CTA only for additional testing
66
+ let cta = null;
67
+ if (nextSteps.next_step.type === "additional_testing") {
68
+ cta = {
69
+ label: `Learn more: ${nextSteps.next_step.recommended_panel_name} ($${nextSteps.next_step.price})`,
70
+ url: nextSteps.next_step.order_url,
71
+ };
72
+ }
73
+ // Build headline
74
+ let headline;
75
+ if (analysis.summary.overall_status === "all_normal") {
76
+ headline = "Your results are within expected ranges";
77
+ }
78
+ else if (analysis.key_findings.length === 1) {
79
+ headline = "One marker to note in your results";
80
+ }
81
+ else {
82
+ headline = "A few markers to review in your results";
83
+ }
84
+ return {
85
+ patient_summary: {
86
+ headline,
87
+ bullets: finalBullets,
88
+ cta,
89
+ disclaimer: DISCLAIMER,
90
+ requires_acknowledgment: true,
91
+ },
92
+ safety_warning: null,
93
+ };
94
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Suggest next steps based on panel analysis
3
+ * Returns at most ONE follow-up panel, framed as "more detail" not diagnosis
4
+ */
5
+ import { AnalysisOutput } from "./analyzePanelResults.js";
6
+ export interface NextStepAdditionalTesting {
7
+ type: "additional_testing";
8
+ recommended_panel_id: string;
9
+ recommended_panel_name: string;
10
+ reason: string;
11
+ confidence: "suggested" | "may_be_helpful";
12
+ order_url: string;
13
+ price: number;
14
+ }
15
+ export interface NextStepNoTesting {
16
+ type: "no_additional_testing";
17
+ reason: string;
18
+ }
19
+ export interface NextStepClinicianDiscussion {
20
+ type: "clinician_discussion";
21
+ reason: string;
22
+ }
23
+ export type NextStep = NextStepAdditionalTesting | NextStepNoTesting | NextStepClinicianDiscussion;
24
+ export interface NextStepsOutput {
25
+ next_step: NextStep;
26
+ not_recommended: string[];
27
+ safety: {
28
+ red_flags_detected: boolean;
29
+ message: string | null;
30
+ };
31
+ clinician_guidance: string;
32
+ }
33
+ /**
34
+ * Suggest next steps based on analysis
35
+ * Conservative approach: one follow-up max, educational framing
36
+ */
37
+ export declare function suggestNextSteps(panelId: string, analysis: AnalysisOutput): NextStepsOutput;