---
title: "N1 Healthcare — CHR Workflow Architecture"
---

N1 Healthcare — CHR Workflow Architecture

## Overview

  The workflow-health-summary** generates a 1-page patient health summary via a 6-stage integrative medicine cascade.
  It analyzes biomarker data through deterministic scoring, enrichment, and derived marker computation, then uses two LLM calls to
  identify clinical patterns and produce structured clinical summaries. The final output is deterministically rendered to HTML via
  `chr-html`.

```text
Pipeline Stages

LLM Calls

Forge Libraries

Pydantic Models

```
**Philosophy:** This repo is *pure analysis logic*. No infrastructure, no templates, no API clients. All shared infra comes from forge-sentinel libs.

### Run Modes

##### Local Mode

JSON file &rarr; cascade &rarr; HTML. No credentials required.

`INPUT_FILE=tests/fixtures/patient-small.json uv run python bin/run.py`

##### Forge Mode

API fetch &rarr; cascade &rarr; HTML + PDF, with billing and cloud upload.

Requires `USER_ID`, `CHR_ID`, LiteLLM and N1 API credentials.

## Pipeline Flowchart

3 deterministic stages + 2 LLM calls + 1 render. The LLM receives the complete enriched picture and reasons about patterns across markers (integrative medicine approach).

```text
S0
Pre-filter
Deterministic

chr-data (reference) &bull; Pure scoring: deviation, trend, recency, density

S1
Context Assembly
Deterministic

chr-data (reference: optimal ranges, body systems, meds, unit conversion)

S2
Derived Markers
Deterministic

chr-data (DERIVED_REGISTRY, unit conversion)

S3
Pattern Recognition
LLM

chr-llm (allm_json_call, load_prompt) &bull; chr-langfuse (tracing)

S4
Clinical Summary
LLM

chr-llm (allm_json_call, load_prompt) &bull; chr-langfuse (tracing)

S5
Render
Deterministic

chr-html (html_document, build_report) &bull; chr-styles (CSS) &bull; chr-pdf (WeasyPrint)

```
## Stage Details

#### Pre-filter — Deterministic Scoring

Groups biomarkers by biomarker ID, scores each group, then selects top 50 unhealthy + top 50 healthy biomarker groups (100 total). Each selected group carries its full measurement history.

```text
Input
`dict[str, BiomarkerInput]`

Output
`Stage0Output`

```
##### Unhealthy Group Scoring (max 80)

- **worst_deviation (30):** Highest deviation across all measurements vs reference midpoint
- **trend (20):** Linear regression slope across full history (3+ data points required)
- **recency (10):** &le;2yr = 10, &le;5yr = 5, older = 0
- **data_richness (10):** &ge;5 pts = 10, &ge;3 = 7, 2 = 4, 1 = 1
- **oor_consistency (10):** Percentage of measurements that are abnormal

##### Healthy Group Scoring (max 80)

- **clinical_significance (30):** High-value markers (glucose, HbA1c, TSH, lipids, etc.) = 30; others = 10
- **stability (20):** Fraction of measurements that are normal &times; 20
- **recency (10):** Same as unhealthy
- **data_richness (10):** Same as unhealthy
- **system_coverage (10):** Bonus for underrepresented body systems

##### Hard Rules

- Severely abnormal groups (worst_deviation &ge; 20) always included
- Qualitative biomarkers excluded
- If one pool has fewer than 50, overflow fills from the other

```text
chr-data

```
#### Context Assembly — Functional Enrichment

Enriches scored biomarkers with functional/optimal ranges, body system classification, and medication interaction context. All lookups come from `chr-data`'s reference module.

```text
Input
`Stage0Output + PatientContext?`

Output
`ContextEnrichedInput`

```
##### Functional Classification

- **optimal:** Within both lab range and functional/optimal range
- **within_lab:** Within lab range, no optimal range defined
- **outside_optimal:** Within lab range but outside functional range
- **outside_lab:** Outside standard lab reference range
- **unknown:** Non-numeric value

##### Key Operations

- Unit conversion to conventional units via `convert_to_conventional()`
- Optimal range lookup via `get_optimal_range(canonical)`
- Body system mapping via `get_body_systems(canonical)`
- Medication interaction detection via `get_affected_markers(med_name)`
- Data quality assessment (recency, trend availability, system coverage)

```text
chr-data

```
#### Derived Markers — Computed Ratios & Indices

Computes clinically meaningful derived markers from available biomarkers. Uses `DERIVED_REGISTRY` from `chr-data` which contains formulas, parameter mappings, and interpretation logic.

```text
Input
`ContextEnrichedInput + PatientContext?`

Output
`DerivedMarkersOutput`

```
##### Example Derived Markers

- **TG/HDL Ratio:** Triglycerides / HDL — insulin resistance proxy
- **HOMA-IR:** (Glucose &times; Insulin) / 405 — insulin resistance
- **eGFR:** CKD-EPI formula (requires age + sex) — kidney function
- **NLR:** Neutrophils / Lymphocytes — inflammation marker

##### Key Operations

- Builds biomarker_id &rarr; numeric_value lookup (highest prefilter score wins)
- All values converted to conventional units before computation
- Demographics-dependent markers (eGFR) require age and sex from PatientContext
- Skipped markers tracked separately for transparency

```text
chr-data

```
#### Pattern Recognition — LLM Call #1

LLM identifies 2-6 clinical patterns across enriched biomarkers, providing root cause hypotheses, gap analysis, and cross-system connections. Thinks like a functional medicine doctor.

```text
Input
`ContextEnrichedInput + DerivedMarkersOutput`

Output
`PatternAnalysisOutput`

```
##### Prompt Template Variables

- {{enriched_biomarkers_json}} — Full enriched biomarker data
- {{derived_markers_json}} — Computed derived markers
- {{context_section}} — Patient context (optional)
- {{data_quality_json}} — Data quality summary

##### Post-LLM Validation

- Filter evidence IDs to only those present in input data
- Skip patterns with zero valid supporting evidence
- Downgrade confidence from "high" to "medium" when all evidence is stale (>2 years)
- Enforce 1-6 pattern count (fallback pattern if none survive validation)
- Severity floor: pattern can't be "monitor" if supporting biomarker has high deviation (&ge;60 score + outside_lab)
- Force trajectory to "insufficient_data" when no longitudinal data exists

```text
chr-llm
chr-langfuse

```
#### Clinical Summary — LLM Call #2

LLM produces the final structured summary: headline, key findings, recommendations, gap recommendations, and overall status. This is the content that gets rendered into the report.

```text
Input
`PatternAnalysisOutput + ContextEnrichedInput + DerivedMarkersOutput`

Output
`ClinicalSummaryOutput`

```
##### Prompt Template Variables

- {{patterns_json}} — Clinical patterns from S3
- {{enriched_biomarkers_json}} — Enriched biomarkers (compact)
- {{derived_markers_json}} — Derived markers
- {{context_section}} — Patient context (optional)

##### Post-LLM Validation

- Filter findings to valid measurement_ids only
- Force trend to "insufficient_data" when no longitudinal data
- Ensure critical pattern biomarkers appear in findings
- Cap findings: min 3, max 10 (pad with highest-scored biomarkers if needed)
- Cap recommendations: min 2, max 5 (add generic fallbacks if needed)
- Deterministic override: overall_status based on pattern severities (critical &rarr; attention_needed)
- Medication dedup: flag recommendations that suggest starting a medication the patient already takes

```text
chr-llm
chr-langfuse

```
#### Render — Deterministic HTML

Converts `ClinicalSummaryOutput` to a complete HTML document using `chr-html`'s component library and `chr-styles`'s CSS system. No LLM-generated HTML — the presentation layer is fully deterministic.

```text
Input
`ClinicalSummaryOutput`

Output
`HTML string &rarr; PDF via chr-pdf`

```
##### Render Components

- **Header:** Patient name, date, overall status badge
- **Headline:** Key finding + trajectory summary
- **Key Findings:** Severity dots, trend arrows, functional notes
- **Clinical Patterns:** Pattern cards with severity borders and root cause
- **Recommendations:** Category-tagged action items (excluding "test" category)
- **Recommended Tests:** Gap recommendations (test-specific)
- **Footer:** AI disclaimer + confidentiality notice

##### CSS Pipeline

- `html_document(body, report_type="single-page", override="health-summary")`
- Base CSS + single-page type CSS + health-summary override CSS layered
- PDF rendering via WeasyPrint through `chr-pdf`

```text
chr-html
chr-styles
chr-pdf
chr-document-manager

```
## Full Prompts

Both prompts use a system/human split loaded via `chr-llm`'s `load_prompt()`. The system message is everything above the `---` separator, the human template is everything below. Template variables are highlighted in amber.

```text
pattern_recognition.md — Stage 3 Prompt (78 lines)
S3 &bull; LLM

**Copy

```
You are an integrative medicine physician analyzing biomarker data. Your task is to identify clinical patterns — connections across biomarkers that reveal underlying health stories. Think like a functional medicine doctor: look at the whole picture, ask "why", and identify what's missing.

Always respond with valid JSON only. No markdown, no explanation, just JSON.
---
## Clinical Pattern Analysis

You are analyzing biomarker data for a patient health summary. The biomarkers have been enriched with functional/optimal ranges (tighter than standard lab ranges) and organized by body system. Derived markers (ratios, indices) have been computed where data permits.

### How to Think About This

1. **Look for clusters, not isolated values.** A single mildly elevated marker means little. Three markers in the same direction across two systems tells a story.
2. **Use functional ranges first.** A glucose of 95 is "normal" by lab standards but already suboptimal functionally. When both lab AND functional ranges are abnormal, that's more significant.
3. **Consider root causes.** Elevated glucose + elevated triglycerides + low HDL isn't three separate problems — it's insulin resistance. Name the root cause.
4. **Note what's missing.** If you see elevated inflammatory markers but no thyroid panel, that's a gap worth noting.
5. **Assess trajectory.** A value that's been worsening over 3 data points matters more than a single snapshot.
6. **Cross-system connections matter.** Thyroid dysfunction affects cholesterol. Insulin resistance drives inflammation. Connect the dots.

### Input Data

#### Enriched Biomarkers (with functional ranges and body systems)
{{enriched_biomarkers_json}}

#### Derived Markers (computed ratios and indices)
{{derived_markers_json}}

{{context_section}}

#### Data Quality Summary
{{data_quality_json}}

### Rules

1. Identify 2-6 clinical patterns. Each pattern must reference at least 1 biomarker by measurement_id.
2. For each pattern, provide:
   - A root cause hypothesis (what's driving this pattern?)
   - Supporting evidence (measurement_ids that support this pattern)
   - Contradicting evidence (measurement_ids that argue against, if any)
   - Trajectory assessment
   - Affected body systems
3. Include gap analysis: what tests are missing that would confirm or refute your hypotheses?
4. Note cross-system connections when they exist.
5. Use ONLY measurement_ids that exist in the input data. Do not invent biomarker IDs.
6. Severity should reflect the clinical significance of the entire pattern, not just individual values.
7. When derived markers are available (HOMA-IR, TG/HDL ratio, etc.), use them to strengthen or modify your pattern assessment.
8. When medication context is available, consider whether abnormal values are expected given the medication (e.g., B12 depletion on metformin is expected, not a new finding).

### Output Format

Return a JSON object:
```
{
  "patterns": [
```json
{
"name": "<pattern name, 3-8 words>",
"severity": "critical" | "significant" | "monitor",
"confidence": "high" | "medium" | "low",
"narrative": "<clinical explanation, max 80 words>",
"supporting_evidence": ["<measurement_id>", ...],
"contradicting_evidence": ["<measurement_id>", ...],
"root_cause_hypothesis": "<possible root cause, max 30 words>",
"body_systems": ["<system>", ...],
"trajectory": "worsening" | "improving" | "stable" | "insufficient_data"
}
```
  ],
  "gap_analysis": [
```json
{
"test_name": "<name of missing test>",
"reason": "<why valuable, max 25 words>",
"related_pattern": "<pattern name>",
"priority": "high" | "medium" | "low"
}
```
  ],
  "cross_system_connections": [
```text
"<brief description of a cross-system relationship>"
```
  ]
}
```

```text
clinical_summary.md — Stage 4 Prompt (98 lines)
S4 &bull; LLM

Copy

```
```
You are writing the final content for a 1-page patient health summary. You have clinical patterns, enriched biomarker data, and derived markers. Now produce the summary content that will be rendered into the report.

Always respond with valid JSON only. No markdown, no explanation, just JSON.
---
## Clinical Summary Generation

You are producing the final structured output for a patient health summary report. You have:
1. Clinical patterns identified by an integrative medicine analysis
2. Enriched biomarkers with functional/optimal ranges
3. Derived markers (computed ratios and indices)

### Input Data

#### Clinical Patterns
{{patterns_json}}

#### Enriched Biomarkers
{{enriched_biomarkers_json}}

#### Derived Markers
{{derived_markers_json}}

{{context_section}}

### Rules

#### Headline
- Single sentence, max 15 words.
- Reference the most clinically significant pattern, not just one biomarker.
- Tone: direct, informative, not alarmist.
- When patient context is available, acknowledge known diagnoses if they relate to the most significant findings.

#### Key Findings (3-10)
- Prioritize: critical first, then significant, then monitor.
- Each finding needs: biomarker name, value with unit, severity, trend, and context (max 15 words).
- Include a functional_note when the value is within lab range but outside functional/optimal range (e.g., "Outside optimal: <90 mg/dL").
- Reference the related pattern when applicable.
- When medications are known, include medication context (e.g., "LDL 145 on atorvastatin — above goal").
- Maximum 10 findings.

#### Recommendations (2-5)
- Categorize each: "test" (order a test), "lifestyle" (diet/exercise/sleep), "discuss" (talk to provider), "monitor" (retest/track), "urgent" (immediate action).
- Be specific. Not "improve diet" but "increase omega-3 intake to improve TG/HDL ratio."
- Include a brief rationale for each recommendation.
- CRITICAL: Do NOT suggest starting a medication the patient is already taking.
- Reference the pattern that drives this recommendation.

#### Gap Recommendations
- Tests that should be ordered to fill data gaps identified in the pattern analysis.
- Each with a reason and priority.

#### Overall Status
- "attention_needed": any critical pattern
- "monitoring_recommended": significant patterns but no critical
- "generally_healthy": only monitor-level patterns

#### Trajectory Summary
- One sentence summarizing the overall health trajectory based on pattern trajectories.

### Output Format

Return a JSON object:
```
{
  "headline": "<max 15 words>",
  "key_findings": [
```json
{
"measurement_id": "<from input>",
"display_name": "<clean biomarker name>",
"value": "<value with unit>",
"severity": "critical" | "significant" | "monitor",
"trend": "worsening" | "improving" | "stable" | "insufficient_data",
"context": "<1-line explanation, max 15 words>",
"functional_note": "<functional range note or empty string>",
"pattern_ref": "<pattern name if applicable, else null>"
}
```
  ],
  "recommendations": [
```json
{
"priority": 1-5,
"category": "test" | "lifestyle" | "discuss" | "monitor" | "urgent",
"action": "<specific recommendation, max 30 words>",
"rationale": "<brief rationale, max 20 words>",
"pattern_ref": "<related pattern name>"
}
```
  ],
  "gap_recommendations": [
```json
{
"test_name": "<test name>",
"reason": "<why recommended, max 25 words>",
"priority": "high" | "medium" | "low"
}
```
  ],
  "overall_status": "attention_needed" | "monitoring_recommended" | "generally_healthy",
  "trajectory_summary": "<overall trajectory, max 20 words>"
}
```

## Data Contracts

All Pydantic models live in `cascade/models.py`. Models are grouped by the stage that produces them.

### Input Models

  BiomarkerInput — Raw biomarker data

```text
Copy

```
class BiomarkerInput(BaseModel):
```text
"""Biomarker data matching workflow-functional's BiomarkerInsight schema."""

test_name: str
measurement_id: str
value: str
status: str
reference_range: str
unit: str | None = None
test_date: str | None = None
all_measurements: list[dict[str, Any]] | None = None
file_name: str | None = None
biomarker_id: str | None = None
record_id: str | None = None

```
  CascadeInput — Top-level pipeline input

```text
Copy

```
```
class CascadeInput(BaseModel):
```text
"""Top-level input to the cascade pipeline."""

patient_name: str = Field(description="Display name for the report header")
report_date: str = Field(description="Report date in human-readable format")
biomarkers: dict[str, BiomarkerInput] = Field(description="measurement_id -> BiomarkerInput")
context: PatientContext | None = Field(default=None)
```
```

  PatientContext — Non-biomarker clinical data

```text
Copy

```
```
class PatientContext(BaseModel):
```text
"""Non-biomarker patient data passed to LLM stages for clinical context."""

patient_name: str = "Patient"
age: int | None = None
gender: str | None = None
diagnoses: list[dict[str, Any]] = Field(default_factory=list)
medications: list[dict[str, Any]] = Field(default_factory=list)
procedures: list[dict[str, Any]] = Field(default_factory=list)
genetics: list[dict[str, Any]] = Field(default_factory=list)
report_date: str = ""
enabled_types: set[str] = Field(
default_factory=lambda: set(ALL_CONTEXT_TYPES)
)
```
```

### Stage 0 Output

  Stage0Output, ScoredBiomarker, ScoredCanonicalGroup

```text
Copy

```
```
class ScoredBiomarker(BaseModel):
```text
"""Biomarker with deterministic relevance score."""
biomarker: BiomarkerInput
total_score: float
components: dict[str, float] = Field(default_factory=dict)

```
class ScoredCanonicalGroup(BaseModel):
```text
"""Patient biomarker group with all measurements and group-level scoring."""
biomarker_id: str
representative: BiomarkerInput
all_members: list[BiomarkerInput]
pool: Literal["healthy", "unhealthy"]
total_score: float
components: dict[str, float] = Field(default_factory=dict)

```
class Stage0Output(BaseModel):
```text
"""Output of the deterministic pre-filter."""
scored_biomarkers: list[ScoredBiomarker]
canonical_groups: list[ScoredCanonicalGroup]
excluded_count: int
input_count: int
unhealthy_count: int = 0
healthy_count: int = 0
```
```

### Stage 1 Output

  ContextEnrichedInput, EnrichedBiomarker, DataQuality, OptimalRange, MedicationInteraction

```text
Copy

```
```
class OptimalRange(BaseModel):
```text
low: float | None = None
high: float | None = None
source: str = ""

```
class MedicationInteraction(BaseModel):
```text
medication_name: str
interaction_note: str = ""

```
class EnrichedBiomarker(BaseModel):
```text
"""Biomarker enriched with functional ranges, body systems, medication context."""
biomarker: BiomarkerInput
prefilter_score: float
numeric_value: float | None = None
lab_range_low: float | None = None
lab_range_high: float | None = None
optimal_range: OptimalRange | None = None
functional_status: str = "unknown"  # optimal|within_lab|outside_optimal|outside_lab|unknown
body_systems: list[str] = Field(default_factory=list)
medication_interactions: list[MedicationInteraction] = Field(default_factory=list)

```
class DataQuality(BaseModel):
```text
total_biomarkers: int
biomarkers_with_trends: int = 0
biomarkers_recent: int = 0
biomarkers_stale: int = 0
systems_covered: list[str] = Field(default_factory=list)
systems_missing: list[str] = Field(default_factory=list)
has_context: bool = False
has_medications: bool = False

```
class ContextEnrichedInput(BaseModel):
```text
"""Output of Stage 1: Context Assembly."""
enriched_biomarkers: list[EnrichedBiomarker]
by_system: dict[str, list[str]]
data_quality: DataQuality
patient_context: PatientContext | None = None
```
```

### Stage 2 Output

  DerivedMarkersOutput, DerivedMarker

```text
Copy

```
```
class DerivedMarker(BaseModel):
```text
"""A computed marker derived from multiple biomarkers."""
name: str
value: float
unit: str
status: str  # optimal|borderline|elevated|low
optimal_range_str: str
interpretation: str
source_biomarker_ids: list[str]

```
class DerivedMarkersOutput(BaseModel):
```text
"""Output of Stage 2: Derived Markers."""
computed: list[DerivedMarker] = Field(default_factory=list)
skipped: list[str] = Field(default_factory=list)
```
```

### Stage 3 Output

  PatternAnalysisOutput, ClinicalPattern, GapAnalysisItem

```text
Copy

```
```
class ClinicalPattern(BaseModel):
```text
"""A clinical pattern identified across multiple biomarkers."""
name: str
severity: Literal["critical", "significant", "monitor"]
confidence: Literal["high", "medium", "low"]
narrative: str  # max 80 words
supporting_evidence: list[str]  # measurement_ids, min 1
contradicting_evidence: list[str]
root_cause_hypothesis: str  # max 30 words
body_systems: list[str]
trajectory: Literal["worsening", "improving", "stable", "insufficient_data"]

```
class GapAnalysisItem(BaseModel):
```text
"""A missing lab test that would inform clinical reasoning."""
test_name: str
reason: str  # max 25 words
related_pattern: str
priority: Literal["high", "medium", "low"]

```
class PatternAnalysisOutput(BaseModel):
```text
"""Output of Stage 3: Pattern Recognition."""
patterns: list[ClinicalPattern]  # 1-6
gap_analysis: list[GapAnalysisItem]
cross_system_connections: list[str]
```
```

### Stage 4 Output

  ClinicalSummaryOutput, SummaryFinding, Recommendation, GapRecommendation

```text
Copy

```
```
class SummaryFinding(BaseModel):
```text
"""A key finding for the clinical summary."""
measurement_id: str
display_name: str
value: str
severity: Literal["critical", "significant", "monitor"]
trend: Literal["worsening", "improving", "stable", "insufficient_data"]
context: str  # max 15 words
functional_note: str
pattern_ref: str | None

```
class Recommendation(BaseModel):
```text
"""A prioritized recommendation."""
priority: int  # 1-5
category: Literal["test", "lifestyle", "discuss", "monitor", "urgent"]
action: str  # max 30 words
rationale: str  # max 20 words
pattern_ref: str

```
class GapRecommendation(BaseModel):
```text
"""A recommended test to fill a data gap."""
test_name: str
reason: str  # max 25 words
priority: Literal["high", "medium", "low"]

```
class ClinicalSummaryOutput(BaseModel):
```text
"""Output of Stage 4 — everything needed to render the report."""
headline: str  # max 15 words
key_findings: list[SummaryFinding]  # 1-10
recommendations: list[Recommendation]  # 2-5
gap_recommendations: list[GapRecommendation]
overall_status: Literal["attention_needed", "monitoring_recommended", "generally_healthy"]
trajectory_summary: str  # max 20 words
patterns: list[ClinicalPattern]
html: str = ""  # populated by render.py, not LLM
```
```

## Library Dependency Map

Forge-sentinel libraries used at each stage.  = deterministic stage,  = LLM stage.

```text
Library
S0
S1
S2
S3
S4
S5

chr-core

chr-data

chr-llm

chr-html

chr-styles

chr-frontend

chr-logging

chr-document-manager

chr-pdf

chr-langfuse

```
### Library Details

```text
chr-core
Exception hierarchy, CHRBaseSettings, PII sanitization. Transitive dependency via chr-llm.

chr-data
Async N1 API fetching, pagination, typed models, biomarker reference data (optimal ranges, body systems, medication markers, derived formulas, unit conversion).

chr-llm
LLM calls with billing, JSON parsing, token tracking. `allm_json_call()` for stages 3-4, `load_prompt()` for .md prompt files.

chr-html
HTML fragment/document builders. `html_document()`, severity dots, trend arrows, escaping utilities.

chr-styles
CSS loader (base + type + override). `get_report_css("single-page", "health-summary")`.

chr-frontend
Progress tracking, UI phases, user error messages. `ProgressReporter`, `UIPhase`, `BaseProgressTracker`.

chr-logging
Structured JSON logging, PII sanitization, context vars. `configure_logging()`, `bind_request_context()`.

chr-document-manager
S3/GCS upload, signed URLs, publish pipeline. `publish_report()` for HTML write + PDF render + cloud upload.

chr-pdf
HTML&rarr;PDF rendering, compression. `WeasyPrintRenderer` via `publish_report()`.

chr-langfuse
Langfuse LLM tracing (spans, cost, prompt visibility). `traced_stage()` wraps stages 3-4 LLM calls.

```
### Not Yet Integrated

```text
chr-verify
Hallucination detection (name matching, timeline validation). No verification of LLM output before render.

chr-checkpoint
File-based stage checkpointing, crash recovery. Restarts currently re-run all 6 stages.

chr-resilience
Retry decorators, error classification, OTEL setup. Not yet integrated.

chr-charts
Biomarker chart generation (Chart.js, matplotlib). No visualizations in reports currently.

```
## Configuration

Settings model from `config.py` using `pydantic-settings`. All settings can be overridden via environment variables.

  Settings Model

```text
Copy

```
class Settings(BaseSettings):
```json
# LLM
llm_model: str = "gemini-2.5-pro"
llm_temperature: float = 0.1
llm_max_retries: int = 2
llm_timeout: int = 180

# Service identity (for billing metadata)
service_name: str = "workflow-health-summary"

# Cascade tuning
prefilter_max_output: int = 100

# Progress reporting (forge mode)
n1_api_base_url: str = ""
n1_api_key: str = ""
sync_with_cloud: bool = True

# Storage (cloud upload via chr-document-manager)
bucket_name: str = ""
storage_provider: str = "s3"
aws_region: str = "us-east-2"
skip_uploads: bool = False

model_config = {"env_prefix": "", "env_file": ".env", "extra": "ignore"}

```
### Environment Variables

```text
Variable
Default
Description

`LLM_MODEL`
`gemini-2.5-pro`
LLM model for stages 3-4 (via LiteLLM)

`LLM_TEMPERATURE`
`0.1`
Low temperature for consistent clinical output

`LLM_MAX_RETRIES`
`2`
Retry count for failed LLM calls

`LLM_TIMEOUT`
`180`
Timeout in seconds per LLM call

`SERVICE_NAME`
`workflow-health-summary`
Service identity for billing metadata

`PREFILTER_MAX_OUTPUT`
`100`
Max biomarker groups selected (50 unhealthy + 50 healthy)

`N1_API_BASE_URL`
*empty*
N1 API base URL (forge mode)

`N1_API_KEY`
*empty*
N1 API key (forge mode)

`SYNC_WITH_CLOUD`
`true`
Enable cloud progress sync

`BUCKET_NAME`
*empty*
S3/GCS bucket for report upload

`STORAGE_PROVIDER`
`s3`
Cloud storage provider (s3 or gcs)

`AWS_REGION`
`us-east-2`
AWS region for S3

`SKIP_UPLOADS`
`false`
Skip cloud upload (local mode)

`INPUT_FILE`
*none*
Path to JSON patient file (local mode)

`USER_ID`
*none*
Patient user ID (forge mode)

`CHR_ID`
*none*
CHR session ID (forge mode)

```
## Overview

  The workflow-data-analysis** generates multi-page biomarker-focused Comprehensive Health Reports (CHR) — data visualizations
  with AI-powered summaries but *no clinical recommendations*. It processes patient biomarker groups through 8 analysis stages:
  chart discovery, chart rendering, clinical clustering, biomarker summaries, SBAR narrative, pattern & follow-up analysis, timeline analysis, and deterministic HTML assembly.

```text
Analysis Stages

7+
LLM Call Types

Forge Libraries

20+
Feature Flags

```
**Philosophy:** This repo is a *thin orchestrator*. Chart utilities, HTML builders, retry logic, data fetching, and styling all live in forge-sentinel shared libraries. WDA only does: LLM calls, chart generation, data mapping, and pipeline orchestration.

### Run Modes

##### Local Mode

CSV fixtures &rarr; charts &rarr; HTML. No API credentials required.

`uv run python -m src.reportgen.main` with `USE_LOCAL_DATA=true`

##### Staging / Production

N1 API fetch &rarr; charts + LLM summaries &rarr; HTML + PDF upload.

Requires `USER_ID`, `CHR_ID`, `N1_API_KEY`, `OPENAI_API_KEY`

## Pipeline Flowchart

3 deterministic + 5 LLM-powered stages. Deterministic data processing bookends the LLM calls. All LLM features have deterministic fallbacks for graceful degradation.

```text
S0
Chart Discovery
Deterministic

chr-data (EntityFetcher) &bull; chr-charts (classification, resolve_category)

S1
Chart Rendering
LLM + Fallback

chr-charts (validate_biomarker_value) &bull; Chart.js config generation or PNG download

S2
Smart Clustering
Deterministic

chr-charts (apply_smart_clustering, subgroups) &bull; Clinical priority ordering

S3
Biomarker Summaries
LLM

chr-llm (billing) &bull; Per-chart trend analysis + per-category chapter summaries

S4
SBAR Clinical Narrative
LLM

chr-llm &bull; Situation, Background, Assessment, Recommendations

S5
Pattern & Follow-Up Analysis
LLM

chr-llm &bull; Cross-biomarker patterns + follow-up test recommendations

S6
Timeline Analysis
Deterministic + LLM

Inflection point detection &bull; LLM narrative generation &bull; Hallucination validation

S7
Render
Deterministic

chr-html (component builders) &bull; chr-styles (CSS) &bull; chr-core (PII sanitization)

```
## Stage Details

#### Chart Discovery — Fetch, Filter & Classify

Fetches patient biomarker groups from the N1 API, filters by data richness, optionally classifies uncategorized biomarkers via LLM, and resolves each into a health category. Returns a prioritized list of chartable biomarkers.

```text
Input
`biomarkers[] + biomarkers_df`

Output
`List[ChartMetadata]`

```
##### Filtering

- Keep only biomarker groups with `member_count &ge; 2` (minimum for trend line)
- Filter out invalid biomarker_id values (NaN, None, empty)
- Validate biologically plausible values via `validate_biomarker_value()`
- When count exceeds `MAX_BIOMARKERS` (1000), prioritize by: member_count DESC &rarr; health_areas count DESC &rarr; name ASC

##### Category Resolution (fallback chain)

- 1. API `group_name` from patient biomarker metadata
- 2. LLM classification via `classify_biomarkers_with_llm()` (optional, uses fast model)
- 3. Infer from `health_areas` field
- 4. Heuristic name matching (`infer_category_from_name()`)
- 5. Fallback: &ldquo;Uncategorized&rdquo;

##### LLM Classification (Optional)

- Model: `classification_model` (default: gemini-3-flash-preview)
- Purpose: Batch-classify uncategorized biomarker names into health categories
- Returns `ClassificationResult` with cache + token usage
- Cached across calls to avoid re-classifying known biomarkers

```text
chr-data
chr-charts
chr-llm

```
#### Chart Rendering — PNG or Chart.js

Generates visual chart representations for each biomarker. Two rendering modes: download PNGs from the N1 chart API, or generate Chart.js configurations via LLM (with deterministic fallback). Chart.js mode produces interactive browser-rendered charts.

```json
Input
`ChartMetadata[] + biomarkers_df`

Output
`chart_paths{} (PNG) or chart_configs{} (Chart.js)`

```
##### PNG Mode (default)

- Download chart images from N1 chart API
- Skipped entirely when Chart.js mode is enabled
- Batch download with error handling per chart

##### Chart.js Mode (LLM-powered)

- LLM generates complete Chart.js v4 JSON configs with reference range annotations
- Deterministic fallback builds config from data when LLM unavailable or disabled
- Batched with semaphore (max 5 concurrent LLM calls)
- Deduplicates same-date readings by averaging (`_deduplicate_rows_by_date()`)
- Validates values via `validate_biomarker_value()` — filters biologically implausible data
- Fallback reference range parsing from row data when API returns null (`_extract_reference_range_from_rows()`)

##### Chart.js Output Format

- Line chart with date labels (Mon YYYY format)
- Green points (#22c55e) for normal values, red (#ef4444) for out-of-range
- Reference range shown as shaded annotation band
- Responsive with hover tooltips

```text
chr-charts
chr-llm
chr-resilience

```
#### Smart Clustering — Clinical Priority Ordering

Groups charts by health category, orders categories by clinical relevance (most abnormal biomarkers first), and assigns biomarkers to clinical sub-groups within each category. Ensures the most clinically significant data appears first in the report.

```json
Input
`ChartMetadata[] + biomarkers_df`

Output
`charts_by_category{} (ordered) + category_order[]`

```
##### Patient Profile Analysis

- `analyze_patient_profile(biomarkers_df)` — counts abnormal biomarkers per category
- Categories with more out-of-range values ranked higher
- Ensures most clinically relevant sections seen first

##### Category Ordering

- `smart_category_order()` — sort by abnormal count descending
- `sort_charts_within_category()` — prioritize within each category
- `sort_singletons_within_category()` — sort table rows for singleton biomarkers

##### Sub-Group Assignment

- 7 category definitions with keyword-matched sub-groups (e.g., Cardiovascular &rarr; Lipid Panel, Cardiac Markers, Blood Pressure)
- `get_subgroup_for_biomarker(name, category)` — case-insensitive partial match
- `group_charts_by_subgroup()` — ordered by clinical priority within category
- Unmapped biomarkers fall to &ldquo;Uncategorized&rdquo; sub-group

```text
chr-charts

```
#### Biomarker Summaries — Per-Chart & Per-Category

Generates per-biomarker trend summaries and per-category chapter summaries via LLM. Chart summaries provide individual biomarker analysis while chapter summaries give a holistic view of each health category. All calls are independently toggleable and run with semaphore-based batched concurrency (max 5 concurrent).

```json
Input
`biomarkers_df + charts_by_category{}`

Output
`chart_summaries{} + chapter_summaries{}`

```
##### Chart Summaries (`enable_chart_summaries`)

- Per-biomarker trend analysis: description, measurements, clinical meaning, action
- Extracts `[CONCLUSION:positive|neutral|negative]` sentiment tag
- Deterministic fallback if LLM fails
- Batched with semaphore (max 5 concurrent)

##### Chapter Summaries (`enable_chapter_summaries`)

- 3-4 sentence analysis per health category
- Covers: overall picture, key findings, patterns, clinical context
- Constrained to only reference biomarkers in the category

```text
chr-llm
chr-resilience

```
#### SBAR Clinical Narrative

Generates a structured SBAR (Situation, Background, Assessment, Recommendations) clinical narrative from the patient's biomarker data. Uses the medical communication standard adapted for health reports. All biomarker references validated against input data.

```json
Input
`biomarkers_df + charts_by_category{}`

Output
`SBARSummary (situation, background, assessment, recommendations)`

```
##### SBAR Framework

- Medical communication standard adapted for health reports

##### Structure

- **S**ituation: 1-2 sentences on current health status
- **B**ackground: 3-5 bullets with key vitals/labs (actual values)
- **A**ssessment: 1-2 sentence interpretation of findings
- **R**ecommendations: 3-5 action items starting with **bold verbs** (Discuss, Monitor, Schedule, Review)

##### Safety

- All biomarker references validated against input data
- No clinical diagnoses — observations only
- JSON response format with structured parsing

```text
chr-llm
chr-resilience

```
#### Pattern & Follow-Up Analysis

Discovers cross-biomarker patterns and generates follow-up test recommendations. Identifies multi-biomarker interactions, temporal trends, and cross-system connections. All biomarker names validated against input data to prevent hallucination.

```json
Input
`biomarkers_df + charts_by_category{}`

Output
`KeyPattern[] + FollowUpTest[]`

```
##### Key Patterns (`enable_key_patterns`)

- Cross-biomarker pattern discovery (max 10 patterns)
- Each pattern: name, description, connected biomarkers, causes, effects, actions, priority (high/medium/low), type (concern/healthy)
- All biomarker names validated against input data (hallucination prevention)

##### Category Patterns (`enable_category_patterns`)

- Per-category pattern subsets for inline display within category sections

##### Follow-Up Tests (`enable_follow_up_tests`)

- 3-5 recommended tests with urgency: priority / recommended / routine
- Related biomarkers must exist in patient data
- Each test: name, reason, related_biomarkers, urgency

```text
chr-llm
chr-resilience

```
#### Timeline Analysis — Inflection Detection + Narrative

Detects significant biomarker changes (inflection points) over time using deterministic algorithms, then generates an LLM-powered timeline narrative. Three-layer hallucination prevention validates all LLM output against actual patient data.

```text
Input
`biomarkers_df`

Output
`HealthEvent[] + timeline_narrative: str`

```
##### Inflection Point Detection (Deterministic)

- **Status changes** (normal &harr; abnormal): Require &ge; 25% value change to avoid minor fluctuations
- **Significant value changes** (same status): Require > 40% change
- Values validated via `validate_biomarker_value()` before analysis
- Signed `change_pct`: positive = increase, negative = decrease
- Events classified: *improvement*, *deterioration*, or *significant_change*

##### Event Selection

- Sort by absolute change_pct descending (most significant first)
- Take top N events (default 5, configurable via `timeline_max_events`)
- Re-sort chronologically for narrative coherence

##### LLM Narrative Generation

- Bullet-point format: overall trajectory + individual event descriptions
- Bold biomarker names and significant changes

##### Hallucination Prevention (3 layers)

- `_validate_timeline_narrative()`: All biomarker name mentions must exist in actual data
- `_validate_timeline_values()`: All percentage claims checked against real data (&pm;0.5% tolerance)
- `_sanitize_percentages()`: Replace hallucinated percentages with correct values from events

```text
chr-charts
chr-llm

```
#### Render — Deterministic HTML

Converts all generated content into a complete HTML report using `chr-html`'s component library and `chr-styles`'s CSS system. No LLM-generated HTML — the presentation layer is fully deterministic. Charts embedded as base64 data URIs (PNG) or inline Chart.js canvases. All LLM output PII-sanitized before rendering.

```text
Input
`All S0-S6 outputs + demographics`

Output
`HTML string &rarr; file`

```
##### Render Components (from chr-html)

- `html_document()`: Full HTML wrapper with head, body, report-type CSS
- `report_header_card()`: Title, patient age/gender, report date
- `category_section()`: Health area headers with category icons
- `chart_block()` / `chartjs_block()`: Chart + summary text
- `chart_grid()`: Multi-column chart layout
- `sbar_grid()`: SBAR display (Situation, Background, Assessment, Recommendations)
- `pattern_card()`: Key pattern cards with priority/type badges
- `follow_up_tests_grid()`: Follow-up test recommendation cards
- `timeline()`: Timeline events and narrative
- `critical_findings_strip()`: Out-of-range biomarker summary banner

##### CSS Pipeline

- `html_document(body, report_type="multi-page")`
- Base CSS + multi-page type CSS layered via `chr-styles`
- PII sanitization via `chr-core.pii.sanitize_report_content()`

```text
chr-html
chr-styles
chr-core

```
## Full Prompts

  Chart Summary Prompt Per Chart &bull; S3

```text
**Copy

```
Analyze the following biomarker data and provide a concise clinical summary.

**Description:** Brief technical definition of what this biomarker measures
**Measurements:** Trend (direction + brief explanation) followed by all data points
**Clinical Meaning:** 1-2 brief points about what the values indicate
**Action:** One specific sentence (action required OR "No action required")
[Sample Source]: Blood/Urine/Saliva/etc
[CONCLUSION:positive|neutral|negative] One-sentence summary

CRITICAL: Only reference biomarkers provided in the data below.
Do NOT hallucinate values, dates, or biomarker names.

  Chapter Summary Prompt Per Category &bull; S3

```text
Copy

```
```
Analyze the following biomarker data for the {category} health area.

Write a comprehensive summary (3-4 sentences, max {max_words} words) covering:
1. Overall health picture for this category
2. Key findings (abnormal, borderline, or optimal values)
3. Patterns and multi-biomarker interactions
4. Clinical context and what the results suggest

CRITICAL: Only reference biomarkers listed in the data below.
```

  SBAR Summary Prompt Global &bull; S4

```text
Copy

```
```
Generate a clinical SBAR summary from the biomarker data:

- SITUATION: 1-2 concise sentences on current health status
- BACKGROUND: 3-5 bullets with key vitals/labs (include actual values)
- ASSESSMENT: 1-2 sentence interpretation of findings
- RECOMMENDATIONS: 3-5 action bullets starting with **bold verb**
  (Examples: **Discuss**, **Monitor**, **Schedule**, **Review**)

Return ONLY valid JSON. No markdown wrapping.
```

  Key Pattern Discovery Prompt Global &bull; S5

```text
Copy

```
```
Analyze biomarker data to discover KEY PATTERNS a physician should know:
- Multi-biomarker interactions and correlations
- Temporal trends across measurements
- Cross-system connections

CRITICAL: ONLY reference biomarkers present in the patient's actual report.

Return JSON array of patterns:
[{
  "pattern_name": "3-8 word name",
  "pattern_discovered": "detailed description",
  "connected_biomarkers": ["Exact Biomarker Name", ...],
  "potential_causes": ["cause1", ...],
  "potential_effects": ["effect1", ...],
  "possible_actions": ["action1", ...],
  "priority": "high|medium|low",
  "pattern_type": "concern|healthy"
}]
```

  Follow-Up Tests Prompt Global &bull; S5

```text
Copy

```
```
Suggest 3-5 follow-up tests based on biomarker patterns.

CRITICAL: ONLY reference biomarkers from the patient's actual data.

For each test:
{
  "test_name": "Test Name",
  "reason": "Grounded in patient's specific findings",
  "related_biomarkers": ["Exact Biomarker Name"],
  "urgency": "priority|recommended|routine"
}

Return ONLY valid JSON array.
```

  Timeline Narrative Prompt Global &bull; S6

```text
Copy

```
```
CRITICAL: Response MUST be ONLY bullet points. No paragraphs.

Analyze chronological biomarker events and create bullet-point summary:
1. EVERY LINE must start with "- " (markdown bullet)
2. Highlight most significant health changes
3. Identify overall trajectory (improving/stable/declining/mixed)
4. Group related biomarker changes

FORMAT RULES:
- EVERY line MUST start with "- "
- NO paragraphs allowed
- Use **bold** for biomarker names or important changes
- Start with overall trajectory as first bullet

Chronological events: {events_text}
```

## Data Contracts

### Core Models

  ChartMetadata

```text
Copy

```
```
class ChartMetadata:
```text
id: str                          # biomarker_id
canonical_name: str
description: str
biomarker: str
chart_type: str = "line"
data_points: int = 0
units: str | None = None
category: str = "Uncategorized"    # resolved health category
health_areas: list[str] = []
member_count: int = 0             # data points in biomarker group
reference_range_min: float | None = None
reference_range_max: float | None = None
```
```

  HealthEvent (Timeline)

```text
Copy

```
```
class HealthEvent:
```text
date: str                       # "YYYY-MM" format
biomarker_name: str
biomarker_id: str
event_type: str                 # "improvement" | "deterioration" | "significant_change"
old_value: str
new_value: str
old_status: str                 # "normal" | "abnormal"
new_status: str
change_pct: float | None       # Signed: positive = increase, negative = decrease
reference_range: str
unit: str
```
```

  Summary Dataclasses

```text
Copy

```
```
class ChartSummary:
```text
biomarker_id: str
canonical_name: str
summary_text: str
conclusion: str                 # "positive" | "neutral" | "negative"
is_fallback: bool = False

```
class ChapterSummary:
```text
category: str
summary_text: str
biomarker_count: int
abnormal_count: int

```
class SBARSummary:
```text
situation: str
background: list[str]
assessment: str
recommendations: list[str]

```
class KeyPattern:
```text
pattern_name: str
pattern_discovered: str
connected_biomarkers: list[str]
potential_causes: list[str]
potential_effects: list[str]
possible_actions: list[str]
priority: str                   # "high" | "medium" | "low"
pattern_type: str               # "concern" | "healthy"

```
class FollowUpTest:
```text
test_name: str
reason: str
related_biomarkers: list[str]
urgency: str                    # "priority" | "recommended" | "routine"

```
class ChartJSConfig(BaseModel):
```text
biomarker_id: str
canonical_name: str
config_json: str               # Complete Chart.js config as JSON
is_fallback: bool = False
```
```

## Library Map

### Dependency Grid

```text
Library
S0
S1
S2
S3
S4
S5
S6
S7

chr-data

chr-charts

chr-llm

chr-resilience

chr-core

chr-html

chr-styles

chr-frontend

```
### Library Details

```text
chr-core
Exception hierarchy (`RetryableError`, `NonRetryableError`), `CHRBaseSettings`, PII sanitization for report content.

chr-data
Async N1 API fetching via `EntityFetcher`. Typed models: `Biomarker`, `Biomarker`, `UserProfile`. Local CSV loader for development.

chr-charts
LLM classification (`classify_biomarkers_with_llm`), smart clustering (`apply_smart_clustering`), sub-group definitions, value validation (`validate_biomarker_value`), reference range formatting.

chr-llm
LLM billing headers via `get_billing_headers()`, cost tracking (`CostTracker`), token usage recording, model pricing reference.

chr-resilience
Retry decorators (`@retry_api`, `@retry_llm`), error classification, progress helpers (`compute_adjusted_progress`, `trace_progress_stage`).

chr-html
HTML component builders: `html_document`, `chart_block`, `chartjs_block`, `sbar_grid`, `pattern_card`, `timeline`, `category_section`, `follow_up_tests_grid`.

chr-styles
CSS loader with report_type + override layering. Design tokens, responsive layouts, N1 brand colors.

chr-logging
Structured JSON logging, PII sanitization in logs, `update_log_stage()` for stage-based log context.

```
## Configuration

Settings model from `config.py` using `CHRBaseSettings` (forge). Over 20 feature flags control which analysis stages run and what appears in the report.

  Settings Model (key fields)

```text
Copy

```
class CloudSettings(CHRBaseSettings):
```text
# Charts (S0-S1)
enable_charts: bool = True
enable_tables: bool = False
enable_llm_charts: bool = False
use_deterministic_charts: bool = True
max_biomarkers: int = 1000
max_llm_chart_biomarkers: int = 15

# LLM Summaries (S3-S5)
enable_chart_summaries: bool = True
enable_chapter_summaries: bool = True
enable_key_patterns: bool = True
enable_category_patterns: bool = True
enable_sbar: bool = True
enable_follow_up_tests: bool = True

# Timeline (S6)
enable_timeline_summary: bool = True

# Models
summary_model: str = "gemini-2.5-pro"
classification_model: str = "gemini-3-flash-preview"
temperature: float = 0.2

# Safety
enable_pii_sanitization: bool = True
enable_llm_classification: bool = True

# Retry (per K8s pod, &times;3 with restarts)
retry_llm_max_attempts: int = 20
retry_api_max_attempts: int = 10

```
### Environment Variables

```text
Variable
Default
Description

`SUMMARY_MODEL`
`gemini-2.5-pro`
LLM model for all summary generation (S3-S6)

`CLASSIFICATION_MODEL`
`gemini-3-flash-preview`
Fast model for biomarker categorization (S0)

`TEMPERATURE`
`0.2`
LLM sampling temperature (lower = more deterministic)

`MAX_BIOMARKERS`
`1000`
Maximum charts to generate (0 = unlimited)

`ENABLE_LLM_CHARTS`
`false`
Use Chart.js instead of PNG charts (S1)

`ENABLE_CHARTS`
`true`
Include biomarker chart images in report

`ENABLE_SBAR`
`true`
Generate SBAR clinical summary (S4)

`ENABLE_KEY_PATTERNS`
`true`
Cross-biomarker pattern discovery (S5)

`ENABLE_FOLLOW_UP_TESTS`
`true`
Follow-up test recommendations (S5)

`ENABLE_TIMELINE_SUMMARY`
`true`
Timeline narrative from inflection points (S6)

`DEV_MODE`
`false`
Show patient biomarker IDs on charts

`USE_LOCAL_DATA`
`false`
Use local CSV files instead of N1 API

```
## Functional Workflow

```text
📋

```
#### Coming Soon

```text
Clinical report with recommendations. Python, LangGraph, LiteLLM. PDF output via Typst.

```
## Generative Sequential Workflow

```text
📄

```
#### Coming Soon

Detailed narrative report with sequential LLM stages. Python + LiteLLM, PDF via LaTeX.

## Generative Langroid Workflow

```text
🤖

```
#### Coming Soon

Agent-based workflow with lifestyle guides. Python, Langroid, LiteLLM.

## Claude Code Workflow

```text
💻

```
#### Coming Soon

CLI-driven report generation via Claude Code agent. Python.

## Overview

  The workflow-supplements-optimization** generates a 1-page supplement protocol via a 7-stage cascade.
  It analyzes biomarker data through deterministic pre-filtering, context enrichment, derived marker computation, and
  nutrient gap matching, then uses two LLM calls to select supplements with specific dosing and build a daily protocol.
  The final output is deterministically rendered to HTML via `chr-html`.

```text
Pipeline Stages

LLM Calls

Forge Libraries

20+
Pydantic Models

```
**Philosophy:** Deterministic first, LLM second. Safety screening (drug-supplement interactions, contraindications) is always deterministic — never delegated to an LLM. The LLM only selects from pre-validated candidates and generates patient-friendly text.

### Run Modes

##### Local Mode

JSON file &rarr; cascade &rarr; HTML. No credentials required.

`INPUT_FILE=tests/fixtures/patient.json uv run python bin/run.py`

##### Forge Mode

API fetch &rarr; cascade &rarr; HTML + PDF, with billing and cloud upload.

Requires `USER_ID`, `CHR_ID`, LiteLLM and N1 API credentials.

## Pipeline Flowchart

4 deterministic stages + 2 LLM calls + 1 render. Stages 0–2.5 are fully deterministic (scoring, enrichment, derived markers, nutrient gap matching). Stages 3–4 use LLM for supplement selection and protocol assembly. Stage 5 renders deterministically.

```text
S0
Pre-filter
Deterministic

Scoring: deviation, trend, recency, density, OOR consistency &bull; Top 50 unhealthy + 50 healthy

S1
Context Assembly
Deterministic

chr-data (optimal ranges, body systems, medication interactions, unit conversion)

S2
Derived Markers
Deterministic

chr-data (DERIVED_REGISTRY) &bull; HOMA-IR, eGFR, LDL:HDL, TG:HDL ratios

S2.5
Nutrient Gap Matching
Deterministic

41 biomarker&rarr;nutrient mappings &bull; ~30 drug-supplement + supplement-supplement interaction rules

S3
Supplement Analysis
LLM

chr-llm (allm_json_call, load_prompt) &bull; Selects 3-8 from pre-validated candidates &bull; Post-LLM validation + hallucination detection

S4
Protocol Assembly
LLM

chr-llm (allm_json_call, load_prompt) &bull; Daily schedule (4 slots) &bull; Safety notes &bull; Retest timeline

S5
Render
Deterministic

chr-html (html_document) &bull; chr-styles (single-page + supplements override) &bull; chr-pdf (WeasyPrint)

```
## Stage Details

#### Pre-filter — Deterministic Scoring

Groups biomarkers by biomarker ID, scores each group with 5 components (max 80 pts), then selects top 50 unhealthy + top 50 healthy biomarker groups. Severely abnormal groups are always included. Each selected group carries its full measurement history.

```text
Input
`dict[str, BiomarkerInput]`

Output
`Stage0Output`

```
##### Unhealthy Group Scoring (max 80)

- **worst_deviation (0-30):** Highest deviation across all measurements vs reference midpoint
- **temporal_trend (0-20):** Linear regression slope across full history (3+ data points required)
- **recency (0-10):** &le;2yr = 10, &le;5yr = 5, older = 0
- **data_richness (0-10):** &ge;5 pts = 10, &ge;3 = 7, 2 = 4, 1 = 1
- **oor_consistency (0-10):** Percentage of measurements that are out-of-range

##### Healthy Group Scoring (max 80)

- **clinical_significance (0-30):** High-value markers (glucose, HbA1c, TSH, lipids) = 30; others = 10
- **stability (0-20):** Fraction of measurements that are normal &times; 20
- **recency (0-10):** Same as unhealthy
- **data_richness (0-10):** Same as unhealthy
- **system_coverage (0-10):** Bonus for underrepresented body systems

##### Hard Rules

- Severely abnormal groups (worst_deviation &ge; 20) always included
- Qualitative biomarkers excluded
- If one pool has fewer than 50, overflow fills from the other

##### Key References

- `_HIGH_SIGNIFICANCE_MARKERS` — clinically important markers list
- `_TEST_NAME_ALIASES` — ~50 biomarker ID normalization rules
- `get_body_systems()` — body system classification from chr-data

```text
chr-data

```
#### Context Assembly — Functional Enrichment

Enriches scored biomarkers with functional/optimal ranges, body system classification, medication interaction context, and data quality assessment. All lookups come from `chr-data`'s reference module.

```text
Input
`Stage0Output + PatientContext?`

Output
`ContextEnrichedInput`

```
##### Functional Classification

- **outside_lab:** Outside standard lab reference range (highest priority)
- **outside_optimal:** Within lab range but outside functional/optimal range
- **within_optimal:** Within both lab and functional ranges

##### Key Operations

- Optimal range lookup via `get_optimal_range(canonical)`
- Unit conversion to conventional units via `convert_to_conventional()`
- Medication interaction detection via `get_affected_markers(med_name)`
- Body system mapping via `get_body_systems(canonical)`
- Data quality assessment (coverage, staleness, body systems)

```text
chr-data

```
#### Derived Markers — Computed Ratios & Indices

Computes clinically meaningful derived markers from available biomarkers. Uses `DERIVED_REGISTRY` from `chr-data` which contains formulas, parameter mappings, and interpretation logic.

```text
Input
`ContextEnrichedInput + PatientContext?`

Output
`DerivedMarkersOutput`

```
##### Example Derived Markers

- **HOMA-IR:** (Glucose &times; Insulin) / 405 — insulin resistance
- **eGFR:** CKD-EPI formula (requires age + sex) — kidney function
- **LDL:HDL Ratio:** LDL / HDL — cardiovascular risk proxy
- **TG:HDL Ratio:** Triglycerides / HDL — insulin resistance proxy

##### Key Operations

- Builds biomarker_id &rarr; numeric_value lookup (highest prefilter score wins)
- All values converted to conventional units before computation
- Demographics-dependent markers (eGFR) require age and sex from PatientContext
- Skipped markers tracked separately for transparency

```text
chr-data

2.5

```
#### Nutrient Gap Matching — Deterministic Candidate Selection

Maps enriched biomarkers to supplement candidates using 41 biomarker-to-nutrient rules, then screens all candidates against drug-supplement and supplement-supplement interaction databases. Contraindicated supplements are blocked before any LLM call.

```text
Input
`ContextEnrichedInput + DerivedMarkersOutput + PatientContext`

Output
`list[NutrientCandidate] + list[InteractionFlag]`

```
##### Biomarker &rarr; Nutrient Mapping

- 41 mappings in `BIOMARKER_NUTRIENT_MAP`
- Direct markers (vitamin D, B12, ferritin) map 1:1
- Indirect markers (homocysteine, CRP, TSH) map to multiple nutrients with relevance + evidence scores

##### Severity Classification

- **severe:** &ge;30% deviation from optimal midpoint
- **moderate:** 10-30% deviation
- **mild:** <10% deviation

##### Interaction Screening

- **~30 drug-supplement rules:** Levothyroxine+Iron, Metformin+B12, Warfarin+Omega-3, etc.
- **Supplement-supplement rules:** Iron+Calcium absorption, Zinc+Copper competition, etc.
- **Blocking logic:** `contraindicated` &rarr; blocked, diagnosis contraindications &rarr; blocked
- **Caution flags:** flagged with spacing/timing instructions, not blocked

```text
reference/nutrient_gaps.py
reference/supplement_specs.py
reference/interactions.py

```
#### Supplement Analysis — LLM Call #1

LLM selects 3-8 supplements from pre-validated candidates, assigns specific doses, and writes clinical rationales. Extensive post-LLM validation catches hallucinated supplements, enforces dose caps, and ensures safety.

```text
Input
`NutrientCandidates + ContextEnrichedInput + DerivedMarkersOutput + PatientContext`

Output
`SupplementGapAnalysisOutput`

```
##### LLM Selection

- Selects 3-8 supplements from pre-validated candidates that passed S2.5 screening
- Assigns specific doses based on biomarker severity, patient context, and supplement specs
- Writes per-supplement clinical rationale

##### Post-LLM Validation

- **Name resolution:** exact match &rarr; fuzzy match &rarr; alias lookup against SUPPLEMENT_SPECS
- **Blocking check:** re-verify against interaction database (LLM may hallucinate cleared supplements)
- **Deduplication:** merge duplicate supplement entries
- **Dose capping:** if LLM proposes dose above max_daily, cap to max_daily
- **Timing sanitization:** normalize timing values to known slots
- **Hallucination detection:** verify supplement names against ground truth in SUPPLEMENT_SPECS

##### Fallback

- If post-validation produces 0 valid supplements: pick top candidate from highest-severity gap deterministically

```text
chr-llm
chr-langfuse
chr-verify

```
#### Protocol Assembly — LLM Call #2

LLM builds the daily protocol: a 4-slot schedule, patient-friendly safety notes, retest timeline, and headline + summary. Extensive post-LLM validation backfills missing data and resolves timing conflicts deterministically.

```text
Input
`SupplementGapAnalysisOutput + InteractionFlags + PatientContext`

Output
`SupplementProtocolOutput`

```
##### Daily Schedule

- 4 time slots: morning, afternoon, evening, bedtime
- Per-slot instructions (e.g. &ldquo;take with fat for D3 absorption&rdquo;)

##### Post-LLM Validation

- **Auto-assign:** supplements missing from schedule are assigned to a slot based on supplement_specs timing
- **Conflict resolution:** Iron + Calcium never in same slot (absorption interference)
- **Safety note backfill:** interaction flags from S2.5 are added as safety notes if LLM omitted them
- **Retest backfill:** severe-gap biomarkers get retest entries if LLM omitted them
- **Headline hallucination check:** validated against supplement data; deterministic fallback if hallucinated

##### Overall Status (Deterministic)

- **actionable_gaps:** any severe gaps detected
- **minor_optimization:** moderate gaps only
- **no_significant_gaps:** no deficiencies found
- Status is always determined deterministically from gap data, never by LLM

```text
chr-llm
chr-langfuse
chr-verify

```
#### Render — Deterministic HTML

Converts `SupplementProtocolOutput` to a complete HTML document using `chr-html`'s component library and `chr-styles`'s CSS system. No LLM-generated HTML — the presentation layer is fully deterministic.

```text
Input
`SupplementProtocolOutput + patient_name + report_date`

Output
`HTML string &rarr; PDF via chr-pdf`

```
##### HTML Sections

- **Header:** Patient name, date, overall status badge
- **Headline + summary:** Protocol overview from S4
- **Daily schedule (hero):** 4 time-slot cards (morning/afternoon/evening/bedtime) with per-slot instructions
- **Supplement theme cards:** Individual supplement cards with dose, form, rationale, retest
- **Interaction warnings:** Amber callouts for flagged interactions
- **Safety notes:** base .callout (amber for important, neutral for informational)
- **Retest timeline:** base .data-table with biomarker, weeks, reason
- **Disclaimer + footer:** base .report-disclaimer + .report-footer

##### CSS Pipeline

- `html_document(body, report_type="single-page", override="supplements")`
- Base CSS + single-page type CSS + supplements override CSS layered
- PDF rendering via WeasyPrint through `chr-pdf`

##### Edge Case: No Gaps

- When pipeline finds no deficiencies: renders a simplified &ldquo;No Significant Gaps&rdquo; variant

```text
chr-html
chr-styles
chr-pdf
chr-document-manager

```
## Full Prompts

Both prompts use a system/human split loaded via `chr-llm`'s `load_prompt()`. The system message is everything above the `---` separator, the human template is everything below.

### Stage 3 — Supplement Gap Analysis

Instructs the LLM to select 3-8 supplements from pre-validated candidates, assign specific doses, and write clinical rationales for each selection.

Prompt template: `src/supplements_optimization/prompts/supplement_gap_analysis.md`

### Stage 4 — Protocol Assembly

Instructs the LLM to build a daily schedule (4 time slots), write patient-friendly safety notes, set a retest timeline, and compose the headline and summary.

Prompt template: `src/supplements_optimization/prompts/protocol_assembly.md`

## Data Contracts

All Pydantic v2 models live in `src/supplements_optimization/cascade/models.py`. Models are grouped by the stage that produces them.

  ModelStagePurposeKey Fields

```text
`BiomarkerInput`InputRaw biomarker measurementtest_name, value, unit, reference_range, biomarker_id
`PatientContext`InputPatient demographics + medicationspatient_name, age, gender, medications, diagnoses
`ScoredBiomarkerGroup`S0Biomarker group with scoringbiomarker_id, pool, total_score, components
`Stage0Output`S0Pre-filter resultscanonical_groups, excluded_count, unhealthy_count, healthy_count
`EnrichedBiomarker`S1Biomarker with functional contextfunctional_status, optimal_range, body_systems, medication_interactions
`ContextEnrichedInput`S1Full enriched datasetenriched_biomarkers, by_system, data_quality
`DerivedMarker`S2Computed ratio/indexname, value, unit, status, interpretation, source_biomarker_ids
`DerivedMarkersOutput`S2All derived markerscomputed, skipped
`NutrientGapMatch`S2.5Biomarker &rarr; nutrient matchbiomarker_id, nutrient, severity, deviation_pct
`NutrientCandidate`S2.5Pre-validated supplement candidatesupplement_name, gap_matches, priority_score, safety_status
`InteractionFlag`S2.5Drug/supplement interactionsupplement, interacts_with, severity, mechanism
`PrioritizedSupplement`S3LLM-selected supplementrank, supplement_name, specific_dose, form, rationale, retest_weeks
`SupplementGapAnalysisOutput`S3Analysis resultspriority_supplements, validation_notes
`DailyScheduleSlot`S4Time slot in scheduletime_of_day, supplements, instructions
`SafetyNote`S4Patient safety notecategory, severity, note
`RetestItem`S4Retest recommendationbiomarker_name, retest_weeks, reason
`SupplementProtocolOutput`S4Final protocol (render input)headline, protocol_summary, overall_status, daily_schedule, safety_notes, retest_timeline

```
## Library Dependency Map

Forge-sentinel libraries used at each stage.  = deterministic stage,  = LLM stage.

```text
Library
S0
S1
S2
S2.5
S3
S4
S5

chr-core

chr-data

chr-llm

chr-html

chr-styles

chr-frontend

chr-logging

chr-document-manager

chr-pdf

chr-langfuse

chr-verify

chr-resilience

```
### Library Details

```text
chr-core
Exception hierarchy, CHRBaseSettings, PII sanitization. Transitive dependency via chr-llm.

chr-data
Async N1 API fetching, pagination, typed models, biomarker reference data (optimal ranges, body systems, medication markers, derived formulas, unit conversion).

chr-llm
LLM calls with billing, JSON parsing, token tracking. `allm_json_call()` for stages 3-4, `load_prompt()` for .md prompt files.

chr-html
HTML fragment/document builders. `html_document()`, supplement theme cards, schedule slots, escaping utilities.

chr-styles
CSS loader (base + type + override). `get_report_css("single-page", "supplements")`.

chr-frontend
Progress tracking, UI phases, user error messages. `ProgressReporter`, `UIPhase`, `BaseProgressTracker`.

chr-logging
Structured JSON logging, PII sanitization, context vars. `configure_logging()`, `bind_request_context()`.

chr-document-manager
S3/GCS upload, signed URLs, publish pipeline. `publish_report()` for HTML write + PDF render + cloud upload.

chr-pdf
HTML&rarr;PDF rendering, compression. `WeasyPrintRenderer` via `publish_report()`.

chr-langfuse
Langfuse LLM tracing (spans, cost, prompt visibility). `traced_stage()` wraps stages 3-4 LLM calls.

chr-verify
Hallucination detection (name matching, supplement validation). Post-LLM verification in stages 3-4.

chr-resilience
Retry decorators, error classification, OTEL setup. LLM call retries in stages 3-4.

```
## Configuration

Settings model from `config.py` using `pydantic-settings`. All settings can be overridden via environment variables.

  Settings Model

```text
**Copy

```
class Settings(BaseSettings):
```json
# LLM
llm_model: str = "gemini-2.5-pro"
llm_temperature: float = 0.1
llm_max_retries: int = 2
llm_timeout: int = 180

# Service identity (for billing metadata)
service_name: str = "workflow-supplements-optimization"

# Cascade tuning
prefilter_max_output: int = 100
max_supplements: int = 8
deidentify_pii: bool = True

# Progress reporting (forge mode)
n1_api_base_url: str = ""
n1_api_key: str = ""
sync_with_cloud: bool = True

# Storage (cloud upload via chr-document-manager)
bucket_name: str = ""
storage_provider: str = "s3"
aws_region: str = "us-east-2"
skip_uploads: bool = False

model_config = {"env_prefix": "", "env_file": ".env", "extra": "ignore"}

```
### Environment Variables

```text
Variable
Default
Description

`LLM_MODEL`
`gemini-2.5-pro`
LLM model for stages 3-4 (via LiteLLM)

`LLM_TEMPERATURE`
`0.1`
Low temperature for consistent dosing

`LLM_MAX_RETRIES`
`2`
Retry count for failed LLM calls

`LLM_TIMEOUT`
`180`
Timeout in seconds per LLM call

`SERVICE_NAME`
`workflow-supplements-optimization`
Service identity for billing metadata

`PREFILTER_MAX_OUTPUT`
`100`
Max biomarker groups selected (50 unhealthy + 50 healthy)

`MAX_SUPPLEMENTS`
`8`
Maximum supplements in final protocol (3-8 range)

`DEIDENTIFY_PII`
`true`
Sanitize PII before LLM calls

`N1_API_BASE_URL`
*empty*
N1 API base URL (forge mode)

`N1_API_KEY`
*empty*
N1 API key (forge mode)

`SYNC_WITH_CLOUD`
`true`
Enable cloud progress sync

`BUCKET_NAME`
*empty*
S3/GCS bucket for report upload

`STORAGE_PROVIDER`
`s3`
Cloud storage provider (s3 or gcs)

`AWS_REGION`
`us-east-2`
AWS region for S3

`SKIP_UPLOADS`
`false`
Skip cloud upload (local mode)

`INPUT_FILE`
*none*
Path to JSON patient file (local mode)

`USER_ID`
*none*
Patient user ID (forge mode)

`CHR_ID`
*none*
CHR session ID (forge mode)

```
## Overview

  The workflow-predictive-aging** generates a multi-page predictive health report via a 7-stage integrative medicine cascade
  with 10/20/30-year biomarker projections. It extends the health-summary pipeline with deterministic trend analysis (Stage 2.5),
  predictive LLM prompts, and projection chart generation (Stage 4.5). The final output is a multi-page HTML/PDF with inline projection charts.

```text
Pipeline Stages

LLM Calls

Forge Libraries

25+
Pydantic Models

```
**Philosophy:** Same as health-summary — *pure analysis logic*. The key differentiator is Stage 2.5 (deterministic projections via linear regression + population norm blending) and Stage 4.5 (chart generation via `chr-charts`). Dual-mode: legacy prompts (single snapshot) or predictive prompts (trajectory-focused).

### Run Modes

##### Local Mode

JSON file &rarr; cascade &rarr; HTML + PDF. No credentials required.

`INPUT_FILE=tests/fixtures/patient-small.json uv run python bin/run.py`

##### Forge Mode

API fetch &rarr; cascade &rarr; HTML + PDF, with billing, PII de-identification, and cloud upload.

Requires `USER_ID`, `CHR_ID`, LiteLLM and N1 API credentials.

## Pipeline Flowchart

4 deterministic stages + 2 LLM calls + 1 chart generation + 1 render. Stages 2.5 and 4.5 are unique to predictive-aging (not present in health-summary). The LLM receives projections and reasons about aging trajectories.

```text
S0
Pre-filter
Deterministic

chr-data (reference) &bull; Pure scoring: deviation, trend, recency, density

S1
Context Assembly
Deterministic

chr-data (reference: optimal ranges, body systems, meds, unit conversion)

S2
Derived Markers
Deterministic

chr-data (DERIVED_REGISTRY, unit conversion)

S2.5
Projections
Deterministic

Linear regression + population norm blending (NHANES/Framingham/CKD-EPI) &bull; 16 clinical thresholds

S3
Pattern Recognition
LLM

chr-llm (allm_json_call, load_prompt) &bull; chr-langfuse (tracing) &bull; chr-resilience (retry) &bull; chr-verify

S4
Clinical Summary
LLM

chr-llm (allm_json_call, load_prompt) &bull; chr-langfuse (tracing) &bull; chr-resilience (retry) &bull; chr-verify

S4.5
Chart Generation
Deterministic

chr-charts (matplotlib &rarr; base64 PNG) &bull; Priority scoring: threshold crossings &times; 10 + severity

S5
Render
Deterministic

chr-html (multi-page) &bull; chr-styles (CSS) &bull; chr-pdf (WeasyPrint)

```
## Stage Details

#### Pre-filter — Deterministic Scoring

Groups biomarkers by biomarker ID, scores each group, then selects top 50 unhealthy + top 50 healthy biomarker groups (100 total). Same logic as health-summary.

```text
Input
`dict[str, BiomarkerInput]`

Output
`Stage0Output`

```
##### Scoring (same as health-summary)

- **worst_deviation (30):** Highest deviation across all measurements vs reference midpoint
- **trend (20):** Linear regression slope across full history (3+ data points required)
- **recency (10):** &le;2yr = 10, &le;5yr = 5, older = 0
- **data_richness (10):** &ge;5 pts = 10, &ge;3 = 7, 2 = 4, 1 = 1
- **oor_consistency (10):** Percentage of measurements that are abnormal

```text
chr-data

```
#### Context Assembly — Functional Enrichment

Enriches scored biomarkers with functional/optimal ranges, body system classification, and medication interaction context. Same logic as health-summary.

```text
Input
`Stage0Output + PatientContext?`

Output
`ContextEnrichedInput`

```
##### Key Operations

- Unit conversion to conventional units via `convert_to_conventional()`
- Optimal range lookup via `get_optimal_range(canonical)`
- Body system mapping via `get_body_systems(canonical)`
- Medication interaction detection via `get_affected_markers(med_name)`

```text
chr-data

```
#### Derived Markers — Computed Ratios & Indices

Computes clinically meaningful derived markers (HOMA-IR, TG/HDL, eGFR, NLR) from available biomarkers using `DERIVED_REGISTRY`. Same logic as health-summary.

```text
Input
`ContextEnrichedInput + PatientContext?`

Output
`DerivedMarkersOutput`

```
##### Example Derived Markers

- **HOMA-IR:** (Glucose &times; Insulin) / 405 — insulin resistance
- **TG/HDL Ratio:** Triglycerides / HDL — insulin resistance proxy
- **eGFR:** CKD-EPI formula (requires age + sex) — kidney function
- **NLR:** Neutrophils / Lymphocytes — inflammation marker

```text
chr-data

2.5

```
#### Projections — Trend Analysis & 10/20/30-Year Forecasting

Fully deterministic stage unique to predictive-aging. Computes blended trends (patient history + population norms) and projects each biomarker 10, 20, and 30 years into the future. Checks 16 clinical thresholds for disease-onset crossings.

```text
Input
`ContextEnrichedInput + DerivedMarkersOutput`

Output
`ProjectionOutput`

```
##### Blending Weights (Patient vs Population)

- **5+ data points:** 90% patient / 10% population
- **3-4 data points:** 70% patient / 30% population
- **2 data points:** 50% patient / 50% population
- **1 data point:** 20% patient / 80% population
- **0 data points:** 0% patient / 100% population norm

##### Population Norm Sources

- NHANES age-stratified means (general biomarkers)
- Framingham equations (cardiovascular markers)
- CKD-EPI age-related decline (kidney function)

##### Clinical Thresholds (16 rules)

- **Glucose:** &ge;100 pre-diabetic, &ge;126 diabetic
- **HbA1c:** &ge;5.7% pre-diabetic, &ge;6.5% diabetic
- **eGFR:** <60 CKD Stage 3, <30 CKD Stage 4
- **LDL:** &ge;160 high, &ge;190 very high
- **Triglycerides:** &ge;200 high, &ge;500 very high
- **TSH:** >4.5 hypothyroid, <0.4 hyperthyroid
- **ALT:** >56 elevated
- **Creatinine:** >1.3 elevated
- **Hemoglobin:** <12 anemia

##### Key Operations

- Linear regression on patient measurement history (slope units/year, r²)
- Blended slope = (patient_weight &times; patient_slope) + (pop_weight &times; pop_slope)
- projected_value = current + blended_slope &times; years
- Threshold crossing detection: current below threshold &rarr; projected above (or vice versa)
- System-level aggregation: trajectory = "declining" if risk worsens 10y&rarr;30y
- Max 50 biomarkers projected (configurable via `projection_max_biomarkers`)

```text
chr-data

```
#### Pattern Recognition — LLM Call #1

LLM identifies clinical patterns. In **predictive mode**, it uses projections to assess aging acceleration, threshold crossings, and projected severity at 10/20/30 years. Outputs `PredictivePatternOutput` with `AgingRiskAssessment` per pattern and an `aging_acceleration_score` (0–100). In **legacy mode**, it produces a standard single-snapshot `PatternAnalysisOutput`.

```text
Input (predictive)
`ContextEnrichedInput + DerivedMarkersOutput + ProjectionOutput`

Output (predictive)
`PredictivePatternOutput`

```
##### Predictive Mode Additions

- Per-pattern `AgingRiskAssessment`: risk narratives at 10y, 20y, 30y + acceleration/deceleration factors
- Projected severity at each horizon (critical/significant/monitor)
- Focus on trajectories over snapshots: where is each biomarker heading?
- Cross-system compounding: multiplicative, not additive risks
- `aging_acceleration_score` (0 = protective, 50 = on track, 100 = accelerated)

##### Post-LLM Validation

- Filter evidence IDs to only those present in input data
- Skip patterns with zero valid supporting evidence
- Enforce 1–4 pattern count
- Hallucination verification via `chr-verify`

```text
chr-llm
chr-langfuse
chr-resilience
chr-verify

```
#### Clinical Summary — LLM Call #2

LLM produces the final structured summary. In **predictive mode**, findings include projected values at 10/20/30 years, threshold alerts, and recommendations carry `time_sensitivity`. Outputs `PredictiveSummaryOutput` with `aging_outlook` (accelerated/on_track/favorable). In **legacy mode**, produces standard `ClinicalSummaryOutput`.

```text
Input (predictive)
`PredictivePatternOutput + ContextEnrichedInput + DerivedMarkersOutput + ProjectionOutput`

Output (predictive)
`PredictiveSummaryOutput`

```
##### Predictive Mode Additions

- `aging_outlook`: accelerated | on_track | favorable
- Findings (1–8) include `projected_10y`, `projected_20y`, `projected_30y` values
- `threshold_alert` when clinical threshold crossing is projected
- Recommendations (1–5) include `time_sensitivity`: immediate | within_1y | within_5y | long_term
- `impact_description` per recommendation (how it affects trajectory)
- `aging_acceleration_score` (0–100) echoed from S3
- Headline focuses on projected trajectory, not current snapshot

##### Post-LLM Validation

- Filter findings to valid measurement_ids only
- Cap findings: min 1, max 8
- Cap recommendations: min 1, max 5
- Hallucination verification via `chr-verify`
- Deterministic override: overall_status based on threshold crossings within 10y &rarr; attention_needed

```text
chr-llm
chr-langfuse
chr-resilience
chr-verify

4.5

```
#### Chart Generation — Projection Visualizations

Fully deterministic stage unique to predictive-aging. Selects key biomarkers for visualization via priority scoring, then renders projection charts using `chr-charts` (matplotlib &rarr; base64 PNG). Two chart types: finding charts (Page 1) and system charts (Page 2).

```text
Input
`PredictiveSummaryOutput + ProjectionOutput + ContextEnrichedInput`

Output
`list[ProjectionChart]`

```
##### Finding Charts (Page 1) — max 3

- Priority: findings with `threshold_alert` or severity &ge; significant
- Score = threshold_crossings &times; 10 + severity_weight
- Shows: historical data points + trend line + projected curve + threshold lines

##### System Charts (Page 2) — max 4

- One chart per declining system (trajectory = "declining")
- Picks worst biomarker per system: most threshold crossings, then highest risk at 30y
- Shows: historical data + projection + lab/optimal range bands + threshold lines

##### Render Pipeline

- Patient measurement history &rarr; (dates_as_fractional_years, values)
- chr-charts matplotlib renderer &rarr; base64 PNG
- Output: `ProjectionChart(biomarker_id, display_name, base64_png)`

```text
chr-charts
chr-data

```
#### Render — Multi-Page HTML

Converts `PredictiveSummaryOutput` + charts to a multi-page HTML document using `chr-html` and `chr-styles`. No LLM-generated HTML — the presentation layer is fully deterministic.

```text
Input
`PredictiveSummaryOutput + list[ProjectionChart]`

Output
`HTML string &rarr; PDF via chr-pdf`

```
##### Page Layout

- **Page 1 (Executive):** Headline, aging outlook, aging acceleration score, key findings with projected values, up to 3 finding charts
- **Page 2 (System Projections):** System-level trajectory summaries, up to 4 system charts, threshold crossing alerts
- **Page 3+ (Details):** Clinical patterns with aging risk assessments, recommendations with time sensitivity, gap recommendations

##### CSS Pipeline

- `html_document(body, report_type="multi-page", override="predictive-aging")`
- Base CSS + multi-page type CSS + predictive-aging override CSS layered
- Inline chart images (base64 PNG — no external dependencies)
- PDF rendering via WeasyPrint through `chr-pdf`

```text
chr-html
chr-styles
chr-pdf
chr-document-manager

```
## Full Prompts

Four prompts in dual-mode configuration: predictive (with projections) and legacy (snapshot only). Loaded via `chr-llm`'s `load_prompt()`. System message above `---`, human template below. Template variables in amber.

```text
predictive_patterns.md — Stage 3 Predictive Mode (aging trajectories)
S3 &bull; LLM

**Copy

```
You are an integrative medicine physician analyzing biomarker data with a focus on aging acceleration. Your task is to identify clinical patterns through the lens of trajectories, threshold crossings, and projected health outcomes at 10, 20, and 30 years.

Always respond with valid JSON only. No markdown, no explanation, just JSON.
---
## Predictive Pattern Analysis

You are analyzing biomarker data enriched with projections for a predictive aging report. The biomarkers have been enriched with functional/optimal ranges, organized by body system, and projected forward using trend analysis. Derived markers and clinical thresholds have been pre-computed.

### How to Think About This

1. **Focus on trajectories, not snapshots.** A glucose of 95 matters less than a glucose trending from 85 to 95 over 5 years with a projected crossing of 100 mg/dL in 8 years.
2. **Use projections as the anchor.** Where is each biomarker heading? Which clinical thresholds will be crossed?
3. **Identify aging acceleration.** Which patterns make biological aging faster than chronological aging?
4. **Consider threshold crossings.** A projected crossing from "normal" to "pre-diabetic" in 10 years is clinically actionable NOW.
5. **Assess risk at each horizon.** Current severity may be "monitor" but projected 20-year severity may be "critical".
6. **Cross-system compounding.** Insulin resistance + declining kidney function + rising inflammation is multiplicative, not additive.

### Input Data

#### Enriched Biomarkers (with functional ranges and body systems)
{{enriched_biomarkers_json}}

#### Derived Markers (computed ratios and indices)
{{derived_markers_json}}

#### Biomarker Projections (10/20/30-year trend analysis)
{{projections_json}}

#### System Projection Summaries
{{system_projections_json}}

{{context_section}}

#### Data Quality Summary
{{data_quality_json}}

### Rules

1. Identify 1-4 clinical patterns focused on aging acceleration.
2. For each pattern, provide:
   - Root cause hypothesis (max 30 words)
   - Supporting evidence (measurement_ids from input)
   - Current trajectory AND projected severity at 10y, 20y, 30y
   - Aging risk assessment: risk narratives at each horizon, acceleration and deceleration factors
   - Affected body systems
3. Include gap analysis: what tests would confirm or refute projections?
4. Note cross-system connections with compounding risk.
5. Use ONLY measurement_ids from input data.
6. Provide an aging_acceleration_score (0-100): 0 = protective, 50 = on track for age, 100 = maximally accelerated.

### Output Format

Return a JSON object:
```
{
  "patterns": [
```json
{
"name": "<3-8 words>",
"severity": "critical" | "significant" | "monitor",
"confidence": "high" | "medium" | "low",
"narrative": "<max 80 words, trajectory-focused>",
"supporting_evidence": ["<measurement_id>", ...],
"contradicting_evidence": ["<measurement_id>", ...],
"root_cause_hypothesis": "<max 30 words>",
"body_systems": ["<system>", ...],
"trajectory": "worsening" | "improving" | "stable" | "insufficient_data",
"projected_severity_10y": "critical" | "significant" | "monitor",
"projected_severity_20y": "critical" | "significant" | "monitor",
"projected_severity_30y": "critical" | "significant" | "monitor",
"aging_risk": {
"risk_10y": "<max 30 words>",
"risk_20y": "<max 30 words>",
"risk_30y": "<max 30 words>",
"acceleration_factors": ["<factor>", ...],
"deceleration_factors": ["<factor>", ...]
}
}
```
  ],
  "gap_analysis": [
```json
{
"test_name": "<name>",
"reason": "<max 25 words>",
"related_pattern": "<pattern name>",
"priority": "high" | "medium" | "low"
}
```
  ],
  "cross_system_connections": ["<description>", ...],
  "aging_acceleration_score": 0-100
}
```

```text
predictive_summary.md — Stage 4 Predictive Mode (multi-page, projected values)
S4 &bull; LLM

Copy

```
```
You are writing the final content for a multi-page predictive aging health report. You have clinical patterns with aging risk assessments, enriched biomarkers, derived markers, and 10/20/30-year projections. Produce the summary content for a report focused on projected health trajectory.

Always respond with valid JSON only. No markdown, no explanation, just JSON.
---
## Predictive Summary Generation

You are producing the final structured output for a predictive aging report. This report spans multiple pages and focuses on WHERE the patient's health is heading, not just where it is now.

### Input Data

#### Clinical Patterns (with aging risk assessments)
{{patterns_json}}

#### Enriched Biomarkers
{{enriched_biomarkers_json}}

#### Derived Markers
{{derived_markers_json}}

#### Biomarker Projections
{{projections_json}}

{{context_section}}

### Rules

#### Headline
- Max 15 words. Focus on PROJECTED trajectory.
- Example: "Metabolic markers project diabetes risk within 10 years without intervention"

#### Aging Outlook
- "accelerated": aging faster than chronological age (score > 65)
- "on_track": aging at expected rate (score 35-65)
- "favorable": aging slower than expected (score < 35)

#### Key Findings (1-8)
- Include current value AND projected values at 10y, 20y, 30y (from deterministic projections)
- Include threshold_alert when a clinical threshold crossing is projected
- Do NOT invent projected values — use only from projections input
- Prioritize findings with threshold crossings and high severity

#### Recommendations (1-5)
- Include time_sensitivity: "immediate" | "within_1y" | "within_5y" | "long_term"
- Include impact_description (max 25 words): how this affects the aging trajectory
- CRITICAL: Do NOT suggest medications the patient is already taking

#### Healthy Highlights (0-4)
- Biomarkers with favorable projected trajectories

#### Overall Status
- "attention_needed": threshold crossings within 10y
- "monitoring_recommended": threshold crossings within 20y
- "generally_healthy": no threshold crossings or only 30y+

#### Trajectory Summary
- Max 25 words. Overall aging trajectory summary.

### Output Format

Return a JSON object:
```
{
  "headline": "<max 15 words, trajectory-focused>",
  "aging_outlook": "accelerated" | "on_track" | "favorable",
  "aging_acceleration_score": 0-100,
  "key_findings": [
```json
{
"measurement_id": "<from input>",
"display_name": "<biomarker name>",
"value": "<current value with unit>",
"severity": "critical" | "significant" | "monitor",
"trend": "worsening" | "improving" | "stable" | "insufficient_data",
"context": "<max 15 words>",
"functional_note": "<or empty>",
"pattern_ref": "<pattern name or null>",
"projected_10y": "<projected value with unit>",
"projected_20y": "<projected value with unit>",
"projected_30y": "<projected value with unit>",
"threshold_alert": "<threshold label or empty>"
}
```
  ],
  "healthy_highlights": [
```json
{
"display_name": "<biomarker name>",
"value": "<value with unit>",
"note": "<max 12 words>"
}
```
  ],
  "recommendations": [
```json
{
"priority": 1-5,
"category": "test" | "lifestyle" | "discuss" | "monitor" | "urgent",
"action": "<max 30 words>",
"rationale": "<max 20 words>",
"pattern_ref": "<related pattern>",
"time_sensitivity": "immediate" | "within_1y" | "within_5y" | "long_term",
"impact_description": "<max 25 words>"
}
```
  ],
  "gap_recommendations": [
```json
{
"test_name": "<test>",
"reason": "<max 25 words>",
"priority": "high" | "medium" | "low"
}
```
  ],
  "overall_status": "attention_needed" | "monitoring_recommended" | "generally_healthy",
  "trajectory_summary": "<max 25 words>"
}
```
```

```text
pattern_recognition.md — Stage 3 Legacy Mode (single snapshot, no projections)
S3 &bull; LLM

Copy

```
```
You are an integrative medicine physician analyzing biomarker data. Your task is to identify clinical patterns — connections across biomarkers that reveal underlying health stories. Think like a functional medicine doctor: look at the whole picture, ask "why", and identify what's missing.

Always respond with valid JSON only. No markdown, no explanation, just JSON.
---
## Clinical Pattern Analysis

You are analyzing biomarker data for a patient health summary. The biomarkers have been enriched with functional/optimal ranges (tighter than standard lab ranges) and organized by body system. Derived markers (ratios, indices) have been computed where data permits.

### How to Think About This

1. **Look for clusters, not isolated values.** Three markers in the same direction across two systems tells a story.
2. **Use functional ranges first.** A glucose of 95 is "normal" by lab standards but already suboptimal functionally.
3. **Consider root causes.** Elevated glucose + elevated triglycerides + low HDL isn't three problems — it's insulin resistance.
4. **Note what's missing.** Elevated inflammatory markers but no thyroid panel is a gap worth noting.
5. **Assess trajectory.** A value worsening over 3 data points matters more than a single snapshot.

### Input Data

#### Enriched Biomarkers
{{enriched_biomarkers_json}}

#### Derived Markers
{{derived_markers_json}}

{{context_section}}

#### Data Quality Summary
{{data_quality_json}}

### Rules

1. Identify 1-4 clinical patterns. Each must reference &ge;1 biomarker by measurement_id.
2. Provide root cause hypothesis, supporting/contradicting evidence, trajectory, body systems.
3. Include gap analysis and cross-system connections.
4. Use ONLY measurement_ids from input data.

### Output Format

Return JSON: `{ "patterns": [...], "gap_analysis": [...], "cross_system_connections": [...] }`
```

```text
clinical_summary.md — Stage 4 Legacy Mode (single page, 6 findings max)
S4 &bull; LLM

Copy

```
```
You are writing the final content for a 1-page patient health summary. You have clinical patterns, enriched biomarker data, and derived markers. Produce the summary content that will be rendered into the report.

Always respond with valid JSON only. No markdown, no explanation, just JSON.
---
## Clinical Summary Generation

You are producing the final structured output for a patient health summary report. This is a single-page report — content must fit on one A4 page.

### Input Data

#### Clinical Patterns
{{patterns_json}}

#### Enriched Biomarkers
{{enriched_biomarkers_json}}

#### Derived Markers
{{derived_markers_json}}

{{context_section}}

### Rules

- **Headline** (max 15 words): most clinically significant finding
- **Key Findings** (1-6): severity, trend, context, functional_note, pattern_ref
- **Healthy Highlights** (0-4): biomarkers in good health
- **Recommendations** (1-3): category, rationale, pattern_ref. Do NOT suggest meds already being taken.
- **Gap Recommendations** (0-5): missing tests
- **Overall Status**: attention_needed | monitoring_recommended | generally_healthy
- **Trajectory Summary** (max 20 words)

### Output Format

Return JSON: `{ "headline": "...", "key_findings": [...], "healthy_highlights": [...], "recommendations": [...], "gap_recommendations": [...], "overall_status": "...", "trajectory_summary": "..." }`
```

## Data Contracts

All Pydantic models live in `cascade/models.py`. Models grouped by the stage that produces them. Shared models (Input, S0-S2) are identical to health-summary; predictive-specific models highlighted below.

### Input Models (shared)

  BiomarkerInput, CascadeInput, PatientContext — same as health-summary

```text
Copy

```
class BiomarkerInput(BaseModel):
```json
test_name: str
measurement_id: str
value: str
status: str
reference_range: str
unit: str | None = None
test_date: str | None = None
all_measurements: list[dict[str, Any]] | None = None
file_name: str | None = None
biomarker_id: str | None = None
record_id: str | None = None
additional_data: dict[str, Any] = {}
unit_mismatch: bool = False

```
class PatientContext(BaseModel):
```text
patient_name: str = "Patient"
age: int | None = None
gender: str | None = None
diagnoses: list[dict[str, Any]] = []
medications: list[dict[str, Any]] = []
procedures: list[dict[str, Any]] = []
genetics: list[dict[str, Any]] = []
report_date: str = ""
enabled_types: set[str]

```
### Stage 2.5 Output — Projections

  TrendAnalysis, TimeHorizonProjection, BiomarkerProjection, SystemProjectionSummary, ProjectionOutput

```text
Copy

```
```
class TrendAnalysis(BaseModel):
```text
"""Blended trend from patient data + population norms."""
slope: float  # annual rate of change in native units
direction: Literal["increasing", "decreasing", "stable"] = "stable"
confidence: Literal["high", "medium", "low"] = "low"
source: Literal["patient_data", "population_norm", "blended"] = "population_norm"
r_squared: float | None = None
data_points: int = 0

```
class TimeHorizonProjection(BaseModel):
```text
"""Single time-horizon projection for a biomarker."""
years: int  # 10, 20, or 30
projected_value: float
projected_status: Literal["optimal", "within_lab", "outside_optimal", "outside_lab", "unknown"] = "unknown"
risk_level: Literal["low", "moderate", "high", "critical"] = "low"
threshold_crossing: str = ""  # clinical threshold label if crossed, else ""

```
class BiomarkerProjection(BaseModel):
```text
"""Full projection for a single biomarker across all horizons."""
biomarker_id: str
display_name: str
current_value: float
unit: str
trend: TrendAnalysis
projections: list[TimeHorizonProjection] = []  # at 10, 20, 30 years
clinical_significance: str = ""

```
class SystemProjectionSummary(BaseModel):
```text
"""System-level aggregation of biomarker projections."""
system_name: str
trajectory: Literal["declining", "stable", "improving"] = "stable"
risk_10y: Literal["low", "moderate", "high", "critical"] = "low"
risk_20y: Literal["low", "moderate", "high", "critical"] = "low"
risk_30y: Literal["low", "moderate", "high", "critical"] = "low"
threshold_crossings: list[str] = []
biomarker_ids: list[str] = []

```
class ProjectionOutput(BaseModel):
```json
"""Output of Stage 2.5: Projections."""
biomarker_projections: list[BiomarkerProjection] = []
system_summaries: list[SystemProjectionSummary] = []
projection_horizons: list[int] = [10, 20, 30]
patient_age: int | None = None
data_source_summary: dict[str, int] = {}  # count by source type
```
```

### Stage 3 Output — Predictive Patterns

  AgingRiskAssessment, PredictivePattern, PredictivePatternOutput

```text
Copy

```
```
class AgingRiskAssessment(BaseModel):
```text
"""Per-pattern aging risk narratives at each projection horizon."""
risk_10y: str = ""  # max 30 words
risk_20y: str = ""  # max 30 words
risk_30y: str = ""  # max 30 words
acceleration_factors: list[str] = []
deceleration_factors: list[str] = []

```
class PredictivePattern(ClinicalPattern):
```text
"""Extends ClinicalPattern with aging risk projections."""
aging_risk: AgingRiskAssessment = AgingRiskAssessment()
projected_severity_10y: Literal["critical", "significant", "monitor"] = "monitor"
projected_severity_20y: Literal["critical", "significant", "monitor"] = "monitor"
projected_severity_30y: Literal["critical", "significant", "monitor"] = "monitor"

```
class PredictivePatternOutput(BaseModel):
```text
"""Output of Stage 3 in predictive mode."""
patterns: list[PredictivePattern]  # 1-4
gap_analysis: list[GapAnalysisItem] = []
cross_system_connections: list[str] = []
aging_acceleration_score: int = 50  # 0-100
```
```

### Stage 4 Output — Predictive Summary

  PredictiveFinding, PredictiveRecommendation, PredictiveSummaryOutput

```text
Copy

```
```
class PredictiveFinding(SummaryFinding):
```text
"""Extends SummaryFinding with projected values."""
projected_10y: str = ""
projected_20y: str = ""
projected_30y: str = ""
threshold_alert: str = ""

```
class PredictiveRecommendation(Recommendation):
```text
"""Extends Recommendation with time sensitivity."""
time_sensitivity: Literal["immediate", "within_1y", "within_5y", "long_term"] = "long_term"
impact_description: str = ""  # max 25 words

```
class PredictiveSummaryOutput(BaseModel):
```text
"""Output of Stage 4 in predictive mode — everything for multi-page render."""
headline: str  # max 15 words
aging_outlook: Literal["accelerated", "on_track", "favorable"] = "on_track"
key_findings: list[PredictiveFinding]  # 1-8
healthy_highlights: list[HealthyHighlight] = []  # up to 4
recommendations: list[PredictiveRecommendation]  # 1-5
gap_recommendations: list[GapRecommendation] = []  # 0-5
overall_status: Literal["attention_needed", "monitoring_recommended", "generally_healthy"]
trajectory_summary: str = ""  # max 25 words
patterns: list[PredictivePattern] = []
aging_acceleration_score: int = 50  # 0-100
html: str = ""  # populated by render, not LLM
```
```

### Stage 4.5 Output — Charts

  ProjectionChart (NamedTuple)

```text
Copy

```
```
class ProjectionChart(NamedTuple):
```text
"""A rendered projection chart for a single biomarker."""
biomarker_id: str
display_name: str
base64_png: str  # full data URI: data:image/png;base64,...
```
```

## Library Dependency Map

Forge-sentinel libraries used at each stage.  = deterministic stage,  = LLM stage,  = predictive-aging unique stage.

```text
Library
S0
S1
S2
S2.5
S3
S4
S4.5
S5

chr-core

chr-data

chr-llm

chr-charts

chr-html

chr-styles

chr-pdf

chr-verify

chr-resilience

chr-langfuse

chr-frontend

chr-logging

chr-document-manager

```
### Library Details

```text
chr-core
Exception hierarchy (RetryableError, NonRetryableError), CHRBaseSettings, PII sanitization. Transitive dependency via chr-llm.

chr-data
Async N1 API fetching, transforms, biomarker reference data (optimal ranges, body systems, medication markers, derived formulas, unit conversion). Used in S0-S2, S2.5 (population norms), S4.5 (historical data lookup).

chr-llm
LLM calls with billing, JSON parsing, token tracking. `allm_json_call()` for S3-S4, `load_prompt()` for .md prompt files. Auto-prefix bare model names with `openai/` when proxy enabled.

chr-charts
Chart generation via matplotlib. Renders projection charts with historical data points, trend lines, projected curves, lab/optimal range bands, and clinical threshold lines. Output: base64 PNG.

chr-html
HTML fragment/document builders. `html_document()` with multi-page support for predictive-aging.

chr-styles
CSS loader. `get_report_css("multi-page", "predictive-aging")`.

chr-pdf
HTML&rarr;PDF rendering, compression. `WeasyPrintRenderer` via `publish_report()`.

chr-verify
Hallucination detection: name matching, timeline validation, projected value verification. Integrated at S3-S4 post-LLM validation.

chr-resilience
Retry decorators (`@retry_llm`, `@retry_api`) on LLM calls in S3-S4. RetryBudget for cascading failure protection.

chr-langfuse
Langfuse LLM tracing (spans, cost, prompt visibility). `traced_stage()` wraps S3-S4 LLM calls.

chr-frontend
Progress tracking, UI phases. `ProgressReporter`, `UIPhase`. Used across all stages.

chr-document-manager
S3/GCS upload, signed URLs, publish pipeline. `publish_report()` for HTML + PDF + cloud upload.

```
## Configuration

Pydantic Settings in `src/predictive_aging/config.py`. All settings overridable via environment variables. Predictive-aging-specific fields highlighted.

  Settings Model

```text
Copy

```
class Settings(BaseSettings):
```json
# LLM
llm_model: str = "gemini-2.5-pro"
llm_temperature: float = 0.1
llm_timeout: int = 180

# Service identity
service_name: str = "workflow-predictive-aging"

# Cascade tuning
prefilter_max_output: int = 100

# Projection settings (predictive-aging specific)
projection_horizons: list[int] = [10, 20, 30]
projection_max_biomarkers: int = 50

# PII de-identification (predictive-aging specific)
deidentify_pii: bool = True

# Progress reporting (forge mode)
n1_api_base_url: str = ""
n1_api_key: str = ""
sync_with_cloud: bool = True

# Storage (cloud upload)
bucket_name: str = ""
storage_provider: str = "s3"
aws_region: str = "us-east-2"
skip_uploads: bool = False

model_config = {"env_prefix": "", "env_file": ".env", "extra": "ignore"}

```
### Environment Variables

```text
Variable
Default
Description

`LLM_MODEL`
`gemini-2.5-pro`
LLM model for stages 3-4 (via LiteLLM)

`LLM_TEMPERATURE`
`0.1`
Low temperature for consistent clinical output

`LLM_TIMEOUT`
`180`
Timeout in seconds per LLM call

`SERVICE_NAME`
`workflow-predictive-aging`
Service identity for billing metadata

`PREFILTER_MAX_OUTPUT`
`100`
Max biomarker groups selected (50 unhealthy + 50 healthy)

`PROJECTION_HORIZONS`
`[10, 20, 30]`
Years to project forward (predictive-aging specific)

`PROJECTION_MAX_BIOMARKERS`
`50`
Max biomarkers to project (predictive-aging specific)

`DEIDENTIFY_PII`
`true`
Replace patient names with pseudonyms (predictive-aging specific)

`N1_API_BASE_URL`
*empty*
N1 API base URL (forge mode)

`N1_API_KEY`
*empty*
N1 API key (forge mode)

`SYNC_WITH_CLOUD`
`true`
Enable cloud progress sync

`BUCKET_NAME`
*empty*
S3/GCS bucket for report upload

`STORAGE_PROVIDER`
`s3`
Cloud storage provider (s3 or gcs)

`AWS_REGION`
`us-east-2`
AWS region for S3

`SKIP_UPLOADS`
`false`
Skip cloud upload (local mode)

`INPUT_FILE`
*none*
Path to JSON patient file (local mode)

`USER_ID`
*none*
Patient user ID (forge mode)

`CHR_ID`
*none*
CHR session ID (forge mode)

```
## Overview

  Easy CHR** generates an interactive, multi-section health report from raw medical PDFs (lab results, imaging, clinical notes) via a **7-phase agentic pipeline**.
  Four phases use iterative LLM tool-calling loops (where the model explores data, forms hypotheses, and builds output incrementally), one phase uses a single LLM call, and the final rendering is fully deterministic (Nunjucks templates — no LLM involvement). The output is a self-contained HTML report with Chart.js visualisations and a 3D body-twin viewer.

```text
Pipeline Phases

Agentic Phases

32+
LLM Tools

25+
TypeScript Interfaces

```
**Philosophy:** This is a *deep agentic reasoning system*, not a simple prompt&rarr;response pipeline. Each phase uses an iterative tool-calling loop where the LLM decides which tool to call, receives results, reasons about them, and iterates — often for 25–50 cycles. External state (Maps, Sets) preserves work product through chat compression, so the agent can lose conversation memory but never its output.

### Run Modes

##### Local Mode

Upload PDFs via React UI &rarr; SSE-streamed pipeline &rarr; HTML report.

`POST /api/realm` with multipart files. Requires `GEMINI_API_KEY`.

##### Forge Mode (K8s Job)

N1 API data fetch &rarr; pipeline &rarr; HTML + S3 upload + signed URL.

Requires `USER_ID`, `CHR_ID`, N1 API + S3 credentials.

### Three Entry Points

##### `execute()`

Full pipeline: Raw PDFs &rarr; OCR extraction &rarr; all 7 phases. Used by HTTP upload path.

##### `executeWithExtractedContent()`

Skips Phase 1: Pre-extracted markdown from N1 API &rarr; Phases 2–7. Used by job runner.

## Pipeline Flowchart

4 agentic phases (iterative LLM tool-calling loops) + 1 single-shot LLM call + 1 OCR extraction + 1 deterministic render. Each agentic phase uses external state that **survives chat compression** — the agent can lose conversation history but never its work product.

```text
P1
Document Extraction
Deterministic + OCR

Gemini Vision &bull; PDF &rarr; Markdown with page numbers

P2
Agentic Medical Analysis
Agentic &bull; 50 iter

8+ tools &bull; Explore &rarr; Hypothesise &rarr; Cross-reference &rarr; Synthesise

P3
Agentic Research & Validation
Agentic &bull; 50 iter

5 tools &bull; Web search &rarr; Fetch primary sources &rarr; Record verdicts

P4
Agentic Data Structuring
Agentic &bull; 25 iter

8 tools &bull; Builds structured_data.json incrementally (SOURCE OF TRUTH)

P5
Agentic Validation
Agentic &bull; 15 iter

11+ tools &bull; Verification-focused: verify_value_exists, compare_date_ranges

P6
Organ Insights
Single LLM Call

Per-organ markdown &bull; Bridge between CHR and 3D body twin

P7
Deterministic HTML Render
Deterministic

Nunjucks + Tailwind (inline) + Chart.js + Cytoscape.js &bull; Milliseconds, 100% fidelity

```
### How Each Agentic Phase Works

Phases 2–5 all follow the same iterative tool-calling pattern. The LLM is given tools, decides which to call, receives results, and iterates until it calls a completion tool.

##### The Agentic Loop (shared pattern)

- **1. Send history + tools to LLM** via `generateContentStreaming()` (keeps LiteLLM proxy alive)
- **2. LLM responds** with tool calls (e.g. `search_data("platelets")`) or text
- **3. Execute each tool** against external state (Maps, Sets); add results to conversation
- **4. Check completion** — did LLM call the `complete_*` tool? If yes, validate and return
- **5. Check compression** — if tokens > 50% of 1M limit, compress oldest 70%, keep newest 30%
- **6. Repeat** until completion or max iterations reached

##### External State (survives compression)

- **Medical Analyst:** `currentAnalysis: Map<string, string>` — exploration notes and synthesis sections
- **Research Agent:** `findings: Map`, `searchesPerformed: Set`, `documentsRead: Set`
- **Data Structurer:** `currentJson: Map<string, unknown>` — the JSON being built section-by-section
- **Validator:** `validationIssues: Array` — accumulated issues

##### Why Streaming?

- Gemini 3 Pro thinking takes 2–3 minutes on complex function calls
- LiteLLM gateway has a ~5 minute TTFT (time-to-first-token) timeout
- `generateContentStreaming()` sends first token in seconds, preventing timeout cascade

## Stage Details

#### Document Extraction — PDF to Markdown

Converts raw PDFs into a single combined markdown file. Each document gets a `## [filename.pdf]` header. For N1 API sources, pre-extracted markdown is fetched directly (skipping OCR).

```text
Input
`PDF/TXT/CSV files *or* N1 API record IDs`

Output
`extracted.md (~460KB combined)`

```
##### How It Works

- **PDFs:** Gemini Vision OCR (`gemini-2.5-flash`) processes each page, adds page numbers
- **Text files:** Direct read with `## [FileName]` section markers
- **N1 API:** Pre-extracted markdown via `/records/{id}/markdown` (fast path, avoids OCR cost)
- **Mixed mode:** Combine pre-extracted + OCR for records with partial extraction
- All content combined into one `extracted.md` file

##### Challenges

- Multi-language content (English, Chinese, Malay) in same table
- Inconsistent units across labs (`g/dL` vs `g/L`, `mmol/L` vs `mg/dL`)
- OCR artefacts and formatting inconsistencies
- Reference ranges vary by lab provider

```text
@google/genai
gemini-2.5-flash

```
#### Agentic Medical Analysis — Explore, Hypothesise, Synthesise

An **Integrative Systems Physician** agent iteratively explores 17+ years of medical records. It reads documents oldest&rarr;newest, forms hypotheses, cross-references findings across body systems, and produces a comprehensive medical narrative.

```text
Input
`extracted.md (via tools) + patient question`

Output
`analysis.md (medical narrative)`

```
##### How the Agent Reasons (Two Modes)

- **MODE A — Exploration (Phases 1–4 internally):**                        Read documents oldest&rarr;newest via `read_document()`
- Write descriptive exploration notes (e.g. "OAT Panel Jul 2025", "Cross-System Pattern: Methylation &rarr; Coagulation")
- For every marker with &ge;2 occurrences: call `get_value_history()` to track trends
- Perform &ge;10 searches, write &ge;8 exploration notes

```text
**MODE B — Synthesis (Phase 5 internally):**

```
- Write final synthesis sections with `replace=true`
- Required: Executive Summary, System-by-System Analysis, Timeline, Root Cause Hypothesis, Causal Chain, Keystone Findings, Recommendations, Missing Data

##### Available Tools (8+)

- `list_documents()` — inventory of all source docs
- `read_document(name)` — full text of one document section
- `search_data(query)` — case-insensitive substring search with &pm;2 line context
- `get_value_history(marker)` — all historical values, chronologically sorted (for trend detection)
- `get_analysis()` / `get_section(name)` — read current progress
- `update_analysis(section, content, replace?)` — write to external state Map
- `get_date_range()` / `list_documents_by_year()` / `extract_timeline_events()` — temporal awareness
- `complete_analysis(summary, confidence)` — validates synthesis sections exist, signals done

##### Data Fidelity Rule

- Every extracted value must include: exact number + unit + reference range + flag
- Format: `TSH: 2.3 mIU/L (ref 0.4-4.0) *L`

```text
gemini-3-pro-preview
50 max iterations
chat compression

```
#### Agentic Research & Web Validation — Evidence Grounding

An **Agentic Medical Research Specialist** searches the web to ground claims from the analysis with authoritative external evidence. It searches iteratively, fetches and reads primary sources, and records honest verdicts (supported / unsupported / contested).

```text
Input
`analysis.md`

Output
`research.json (claims + sources)`

```
##### How the Agent Reasons

- **Survey:** Identify 12–20 claim candidates from analysis, prioritise by centrality and actionability
- **Search:** Multiple searches per claim with different formulations (mechanism, evidence quality, guideline position)
- **Read:** Fetch primary sources (papers, guidelines, institution pages) via `fetch_document()` — up to 8KB each
- **Record:** `record_finding(claim, verdict, confidence, evidence, sources)` with honest verdicts
- **Audit:** `get_research_state()` to check coverage before completing
- "Depth over speed" — `contested` and `unsupported` verdicts are valuable findings, not failures

##### Available Tools (5)

- `search_web(query)` — Gemini built-in web search; resolves Google redirect URLs
- `fetch_document(url)` — reads up to 8KB of content, strips HTML
- `record_finding(claim, verdict, confidence, evidence, sources)` — stores in external Findings Map
- `get_research_state()` — audit progress (claims covered, searches done, findings count)
- `complete_research(summary)` — validates substantive summary, minimum coverage

##### Source Classification

- `journal`: pubmed, ncbi, nejm, lancet
- `guideline`: who.int, cdc.gov, nih.gov
- `institution`: mayoclinic, clevelandclinic
- `education`: uptodate, medscape

```text
gemini-3-pro-preview
50 max iterations
web search

```
#### Agentic Data Structuring — Building the Source of Truth

A **Data Extraction Specialist** agent transforms the medical narrative and research findings into a single, precisely structured JSON object (`structured_data.json`). This JSON drives the deterministic HTML renderer — every field must be accurate and complete.

```text
Input
`analysis.md (inline) + research.json (inline) + extracted.md (tools)`

Output
`structured_data.json (25+ fields, Zod-validated)`

```
##### Source Priority (conflict resolution)

- **Priority 1 — Analysis** (inline): Primary for clinical reasoning, interpretations, causal chains
- **Priority 2 — Research** (inline): Citations, validated claims
- **Priority 3 — Raw Source** (via tools): Original values, dates, reference ranges

##### How JSON is Built Incrementally

- Agent reads analysis + research in context window
- Calls `update_json_section("executive_summary", '{...}')` for object sections
- Calls `append_to_section("data_gaps", '[{...}, {...}]')` for arrays (3–5 items/batch)
- Calls `search_source()` and `get_value_history()` to verify values from raw source
- Periodically calls `get_json_draft()` to check completeness
- Calls `complete_structuring()` when all required sections present

##### Mandatory Tool Calls for detailed_findings

- For EVERY body system: `search_source("[system] diagnosis")` + `search_source("[system] biomarker")`
- For EVERY biomarker trend: **MUST** call `get_value_history("[marker]")` — never write trend data from memory
- If `get_value_history()` returns <2 data points &rarr; exclude from `biomarker_trends[]` (charts need &ge;2)

##### Available Tools (8)

- `search_source(query)` — search extracted.md
- `get_value_history(marker)` — all historical values for trend building
- `get_date_range()` / `list_source_documents()` — orientation
- `update_json_section(section, data)` — set object fields in external Map
- `append_to_section(section, items)` — add to array fields (1–5 per call)
- `get_json_draft()` — review progress
- `complete_structuring(summary)` — validates required sections: executiveSummary, criticalFindings, timeline, diagnoses, systemsHealth

##### Post-Processing: Zod Validation

- Output validated through `report.zod.ts` with intelligent coercion
- `normalizeKeysToSnakeCase()` — camelCase &rarr; snake_case recursively
- `z.coerce.number()` — string "6.5" &rarr; number 6.5
- `.catch("neutral")` — invalid enum values default instead of failing
- `flatEventsToEras()` — flat event arrays &rarr; hierarchical era/group/findings
- Reference filtering — strips entries without `https://` URIs

```text
gemini-3-pro-preview
25 max iterations
zod

```
#### Agentic Validation — Verification-Focused QA

A **QA Specialist** agent verifies that `structured_data.json` accurately reflects the source data. Unlike the analyst which *explores*, the validator *verifies*. Tools return summaries and verification results (not full content) to prevent payload bloat.

```text
Input
`structured_data.json + extracted.md + analysis.md`

Output
`validation.md + corrected JSON (if needed)`

```
##### Verification Workflow (7 Steps)

- **1. Orientation:** `list_documents()` + `get_json_overview()` + `compare_date_ranges()`
- **2. Timeline check:** `find_missing_timeline_years()` &rarr; flag gaps as critical issues
- **3. Numeric check:** For each document &rarr; `get_document_summary()` &rarr; for each abnormal value &rarr; `verify_value_exists()`
- **4. Array check:** Verify counts in criticalFindings, diagnoses, trends
- **5. Qualitative check:** Search for medications, supplements, symptoms
- **6. PII check:** Any full name, DOB, SSN in JSON &rarr; critical issue
- **7. Complete:** `complete_validation(status)` — pass / pass_with_warnings / needs_revision

##### Primary Verification Tool

- `verify_value_exists(marker, expected_value?)` returns:           `{ existsInSource, existsInJson, sourceValue, jsonValue, match }`
- Combined check: is value in extracted.md AND in structured_data.json? Do they agree?

##### Correction Mechanism

- If `needs_revision`: surgical JSON patches via `deepMergeJsonPatch()`
- Faster and more precise than full regeneration

```text
gemini-3-pro-preview
15 max iterations
11+ tools

```
#### Organ Insights — Bridge to 3D Body Twin

Single LLM call that generates per-organ markdown from `structured_data.json`. This is the bridge between the HTML report and the 3D body twin visualisation.

```text
Input
`structured_data.json`

Output
`organ_insights.md &rarr; body-twin.json`

```
##### Per-Organ Sections

- `## Organ Name` headers with Status (critical/warning/stable/optimal), Confidence, Markers table
- Clinical findings, cross-organ connections, clinical implications
- `BodyTwinTransformer` parses markdown into typed `BodyTwinData` for Three.js viewer
- 14 body systems, per-organ health scores, cross-organ connection edges

```text
gemini-3-pro-preview
single call

```
#### Deterministic HTML Render — Nunjucks Templates

Pure data transformation — **no LLM calls**. Takes validated JSON, computes a section manifest (which sections have enough data to render), renders Nunjucks templates, and compiles Tailwind CSS inline. Output is a self-contained HTML file with no external CDN dependencies.

```text
Input
`StructuredReportParsed (Zod-validated) + organ_insights.md`

Output
`index.html (self-contained, interactive)`

```
##### Rendering Pipeline

- **1. Compute Section Manifest:** Boolean map of which sections render (threshold-based rules)
- **2. Nunjucks Templates:** `base.html.njk` + 13 section includes gated by manifest
- **3. Custom Filters:** `format_bold`, `format_citations`, `paragraphs`, `trend_chart_data`, `score_level`
- **4. Tailwind CSS:** PostCSS compiles used classes into inline `<style>` — no CDN dependency
- **5. Charts:** Chart.js initialised client-side from embedded `data-trend-chart` JSON attributes

##### Section Manifest Thresholds

- `conditions`: total_count &ge; 1
- `timeline`: eras.length &ge; 2
- `biological_story`: graph_edges &ge; 1 AND graph_nodes &ge; 2
- Treatment sub-tabs: each independently gated by array length > 0
- If manifest value is `false`: section completely absent (no heading, no empty state)

##### Report Sections (13 templates)

- Executive Summary (SBAR hero) &bull; Key Metrics (system scores) &bull; Conditions (priority groups)
- Key Visualisations (radar, gauge, donut, heatmap) &bull; Timeline (era-based) &bull; Biological Story (Cytoscape.js)
- Detailed Findings (tabbed per-system with sub-tabs) &bull; Treatment Plan &bull; Prognosis
- Long-Term Management &bull; Organ Insights &bull; Data Gaps &bull; References

```text
nunjucks
tailwindcss
postcss
chart.js
cytoscape.js

```
## How Data Flows: Concrete Example

A single platelet value flowing through all 7 phases illustrates how the system reasons about data, verifies it, and renders it.

```text
Platelet Value: End-to-End Journey Through 7 Phases
Example

**Copy

```
═══ PHASE 1: Extraction ═══════════════════════════════════════════════
Source PDF (20070912.pdf) is OCR'd by Gemini Vision:

## [20070912.pdf]
<table>...<td>Platelets</td><td>159</td><td>x10^9/L</td>...</table>

═══ PHASE 2: Medical Analysis (Agentic) ══════════════════════════════
Agent calls tools iteratively:

read_document("20070912.pdf")       → sees platelets: 159
get_value_history("Platelets")      → returns 8 values across 17 years
update_analysis("Hematologic Observations", "Platelets: 159 x10^9/L (2007)...")

Agent writes synthesis:
"Chronic mild thrombocytopenia (140-160 range) since 2007"

═══ PHASE 3: Research (Agentic) ══════════════════════════════════════
Agent searches web and reads primary sources:

search_web("chronic mild thrombocytopenia benign ethnic")
fetch_document("https://pubmed.ncbi.nlm.nih.gov/...")  → reads abstract
record_finding("Chronic mild thrombocytopenia", "supported", "high", ...)

═══ PHASE 4: Data Structuring (Agentic) ══════════════════════════════
Agent reads analysis inline, then verifies via tools:

search_source("platelets diagnosis")
→ Finds: "mild thrombocytopenia" in PBF reports from 2011, 2014, 2017

get_value_history("Platelet")
→ Returns 8 data points: 159(2007), 176.9(2009), 163(2010), 148(2011),
  172(2013), 149(2014), 141(2017), 138(2025)

update_json_section("detailed_findings", '{
  "systems": [{
```json
"name": "Hematologic", "score": 42, "level": "warning",
"biomarker_trends": [{
"name": "Platelets",
"data_points": [
{"date":"2007-09-12","value":159,"unit":"x10^9/L","status":"Low"},
...8 total...
],
"arrow": "↓", "trend": "Gradual decline over 17 years"
}]
```
  }]
}')

═══ PHASE 5: Validation (Agentic) ════════════════════════════════════

verify_value_exists("Platelets", "159")
→ { existsInSource: true, existsInJson: true, match: true }

═══ PHASE 6: Organ Insights ══════════════════════════════════════════
LLM generates per-organ markdown including:

## Bone Marrow
- Status: warning
- Markers: Platelets 138 x10^9/L (↓ trending)
- Cross-organ: Affects clotting cascade, possible splenic sequestration

═══ PHASE 7: Deterministic Render ════════════════════════════════════
Nunjucks template reads JSON, filter serialises for Chart.js:

{% for bt in sys.biomarker_trends | filter_min_points(2) %}
  <canvas data-trend-chart='{{ bt | trend_chart_data }}'></canvas>
{% endfor %}

→ Client-side JS initialises Chart.js line chart with reference range band

```text
How Contradictions are Handled
Reasoning

Copy

```
```
The pipeline handles contradictions at three levels:

── Level 1: Source-level (Phase 2) ─────────────────────────────────────
Same biomarker in different units across labs:
  Lab A:  Hemoglobin 14.6 g/dL
  Lab B:  Hemoglobin 146  g/L    (same value, different unit)

The analyst agent uses search_data() and get_value_history() to see
ALL values. The SKILL.md instructs: "Same biomarker reported in different
units across labs — always include the unit and reference range."

── Level 2: Analysis vs Source (Phase 4) ───────────────────────────────
Priority chain resolves conflicts:
  Priority 1: Analysis (trusted clinical interpretation)
  Priority 2: Research (validated citations)
  Priority 3: Raw Source (exact values via tools)

If analysis says "Platelets 140" but raw source says "Platelets 138":
→ Agent calls search_source("platelets") to verify
→ Uses the tool result (raw source) for exact value
→ Trusts analysis for clinical interpretation

── Level 3: JSON vs Source (Phase 5) ───────────────────────────────────
Validator cross-checks every value:
  verify_value_exists("Platelets", "140")
  → { existsInSource: true, sourceValue: "138",
```text
existsInJson: true, jsonValue: "140",
match: false }
```
  → report_issue("wrong_value", "warning", "Platelets: JSON says 140, source says 138")
  → Correction applied via deepMergeJsonPatch()
```

```text
How Chat Compression Preserves Work
Infrastructure

Copy

```
```
When conversation exceeds 50% of 1M token limit:

1. Split history at a safe boundary (after user message, not mid-function)
2. Compress oldest ~70% with phase-specific prompt
3. LLM outputs a <state_snapshot> summarising progress
4. New history = [system prompt] + [snapshot] + [kept 30%]
5. Safety: reject if compressed &ge; original size

CRITICAL: External state is NEVER compressed:

  Phase 2  currentAnalysis: Map<string, string>     ✅ survives
  Phase 3  findings: Map, searches: Set              ✅ survives
  Phase 4  currentJson: Map<string, unknown>          ✅ survives
  Phase 5  validationIssues: Array                    ✅ survives

The agent can lose the memory of HOW it decided something,
but never the decision itself.

Phase-specific compression prompts:
  analyst   → Key findings, documents explored, analysis state
  researcher → Claims researched, searches performed
  structurer → JSON sections written, source lookups
  validator  → Verification results, issues found
```

## Data Contracts

All TypeScript interfaces live in `server/src/schemas/report.schema.ts`. Zod validation in `report.zod.ts`. Section visibility in `section-manifest.ts`.

### Root Schema

  StructuredReport — Top-level report structure (25+ fields)

```text
Copy

```
interface StructuredReport {
  executive_summary: ExecutiveSummary;          // SBAR format — always required
  key_metrics:       KeyMetricsDashboard;        // system scores — always required
  detailed_findings: DetailedFindings;           // per-system drilldown — always required
  conditions:            IdentifiedConditions | null;  // condition groups
  timeline:              MedicalTimeline | null;       // era-based timeline
  biological_story:      BiologicalStory | null;       // Cytoscape.js graph
  treatment_plan:        TreatmentPlan | null;         // actions, supplements, lifestyle
  prognosis:             Prognosis | null;             // outcome scenarios
  long_term_management:  LongTermManagement | null;    // checkpoints, monitoring
  data_gaps:             DataGap[] | null;             // missing tests
  references:            Reference[] | null;           // online sources only
  organ_insights:        string | null;               // injected by Phase 6
}

### Key Nested Types

  ExecutiveSummary — SBAR clinical communication

```text
Copy

```
```
interface ExecutiveSummary {
  central_theme: string;              // single sentence, overall trajectory
  key_statistics: KeyStatistics;       // counts: conditions, diagnoses, biomarkers, date range
  situation: SBARBullet[];             // 3-5 bullets, &le;15 words each
  background: SBARBullet[];            // 3-5 bullets
  assessment: SBARBullet[];            // 3-5 bullets
  recommendation: SBARBullet[];        // 3-5 bullets
  narrative_summary: string;           // full narrative (multiple paragraphs)
  top_priority: string;               // single most urgent action
}

interface KeyStatistics {
  total_conditions: number;
  data_span_years: string;
  total_diagnoses: number;
  total_biomarkers: number;
  total_biomarkers_out_of_range: number;
  most_recent_assessment: string;
}
```

  DetailedFindings — Per-body-system drilldown (most complex section)

```text
Copy

```
```
interface DetailedFindings {
  systems: BodySystem[];
}

interface BodySystem {
  name: string;                              // "Hematologic", "Cardiovascular", etc.
  score: number;                             // 0-100 health score
  status: string;                            // "Concerning", "Fair", "Good"
  level: Level;                              // "critical" | "warning" | "neutral" | "positive"
  diagnoses: SystemDiagnosis[];              // diagnosis name, date, status, citation
  biomarkers: SystemBiomarker[];             // most recent value per marker
  biomarker_trends: BiomarkerTrend[];        // full history for Chart.js (&ge;2 points)
  clinical_interpretation: string;          // prose with **bold** and [citations]
}

interface BiomarkerTrend {
  name: string;
  data_points: TrendDataPoint[];             // [{date, value, unit, reference, status, citation}]
  status: string;                            // "Slowly declining", "Stable", etc.
  arrow: TrendArrow;                         // "→" | "~" | "↑" | "↑↑" | "↓" | "↓↓"
  level: Level;
  trend: string;                             // narrative description of trend
}
```

  BiologicalStory — Cytoscape.js disease cascade graph

```text
Copy

```
```
interface BiologicalStory {
  central_pathophysiology: string;
  graph_nodes: GraphNode[];               // tier 0 = root driver, tier 5 = end-stage
  graph_edges: GraphEdge[];               // directed relationships with mechanism text
}

interface GraphNode {
  id: string;
  label: string;
  tier: 0 | 1 | 2 | 3 | 4 | 5;
  node_type: "root" | "critical" | "warning" | "neutral";
}

interface GraphEdge {
  source: string;                          // node id
  target: string;                          // node id
  affect: "Drives" | "Worsens" | "Allows" | "Impairs" | "Creates";
  mechanism: string;                       // "Insulin resistance → Increased hepatic VLDL"
  bidirectional: boolean;
}
```

  SectionManifest — Deterministic rendering visibility

```text
Copy

```
```
interface SectionManifest {
  // Always present
  executive_summary: true;
  key_metrics: true;
  detailed_findings: true;

  // Conditional (threshold-based)
  conditions: boolean;                      // total_count &ge; 1
  key_visualizations: boolean;               // any systems data exists
  timeline: boolean;                         // eras.length &ge; 2
  biological_story: boolean;                 // edges &ge; 1 AND nodes &ge; 2

  // Treatment plan sub-tabs (each independent)
  treatment_plan: boolean;
  treatment_plan_immediate: boolean;
  treatment_plan_supplements: boolean;
  treatment_plan_lifestyle: boolean;
  treatment_plan_monitoring: boolean;
  treatment_plan_doctor_questions: boolean;

  // Long-term management sub-tabs
  long_term_management: boolean;
  long_term_checkpoints: boolean;
  long_term_monitoring: boolean;
  long_term_vaccinations: boolean;
  long_term_screenings: boolean;

  prognosis: boolean;
  organ_insights: boolean;
  data_gaps: boolean;
  references: boolean;
}
```

## Technology & Dependency Map

Dependencies used at each phase.  = deterministic,  = LLM-powered.

```text
Technology
P1
P2
P3
P4
P5
P6
P7

@google/genai

genai-factory (streaming)

chat-compression

zod (report.zod.ts)

nunjucks

tailwindcss + postcss

section-manifest

langfuse

fastify

S3 / local storage

```
### Key Technology Roles

```text
@google/genai
Gemini AI SDK for function calling and streaming. All LLM interactions go through this.

genai-factory.ts
`generateContentStreaming()` keeps LiteLLM proxy alive during 2-3 minute thinking. `createGoogleGenAI()` injects billing headers.

chat-compression
Phase-specific history compression. Preserves external state (Maps, Sets), compresses oldest 70% of conversation when token threshold exceeded.

zod + report.zod.ts
Runtime schema validation with intelligent coercion (string→number, camelCase→snake_case, flat events→eras). Graceful degradation via `.catch()` defaults.

nunjucks
Template engine for deterministic HTML rendering. 13 section templates with 10+ custom filters (format_bold, format_citations, trend_chart_data, etc.).

tailwindcss + postcss
Compiles CSS inline from used utility classes. Zero CDN dependency — output is fully self-contained.

section-manifest.ts
Single source of truth for which sections render. Threshold-based boolean map prevents empty sections from appearing.

langfuse
LLM observability: trace spans per phase, generation-level token tracking, cost attribution per user via billing headers.

fastify
HTTP server for local mode. SSE streaming of pipeline events to React frontend.

@aws-sdk/client-s3
Production storage for reports. Two-tier: scoped storage (pipeline intermediates) + base storage (final HTML with signed URLs).

```
## Configuration

Settings from `server/src/config.ts`. Uses a defaults-only policy** — environment variable model overrides are intentionally ignored for consistency.

  REALM_CONFIG — Full Configuration

```text
**Copy

```
const REALM_CONFIG = {
  models: {
```text
markdown:     'gemini-2.5-flash',       // Phase 1: OCR extraction
intermediate: 'gemini-3-pro-preview',   // Phases 2-6: agentic reasoning
html:         'gemini-3-flash-preview', // (legacy, not used in Phase 7)
doctor:       'gemini-3-pro-preview',   // Primary model for all agents
```
  },
  agenticLoop: {
```text
maxIterations:   10,                     // Default (overridden per-phase: 25-50)
enableWebSearch: true,                    // Phase 3 toggle
```
  },
  retry: {
```json
llm:    { maxRetries: 8,  baseMultiplier: 10,  maxWait: 600 },  // seconds
api:    { maxRetries: 3,  baseMultiplier: 5,   maxWait: 120 },
vision: { maxRetries: 5,  baseMultiplier: 5,   maxWait: 180 },
```
  },
  throttle: {
```json
pdfExtraction: { maxConcurrent: 4,  delayMs: 100 },
webSearch:     { maxConcurrent: 3,  delayMs: 250 },
llm:           { maxConcurrent: 3,  delayMs: 150 },
```
  },
  compression: {
```text
threshold:        0.5,                   // Trigger at 50% of token limit
preserveFraction: 0.3,                   // Keep newest 30%
tokenLimit:       1048576,               // 1M tokens (gemini-3-pro-preview)
```
  },
};

### Environment Variables

```text
Variable
Default
Description

`GEMINI_API_KEY`
*required*
Gemini API key (or LiteLLM gateway key)

`GOOGLE_GEMINI_BASE_URL`
*empty*
Custom base URL for LiteLLM gateway routing

`MAX_AGENTIC_ITERATIONS`
`10`
Default max iterations for agentic loops (overridden per-phase)

`ENABLE_WEB_SEARCH`
`true`
Toggle Phase 3 (research) on/off

`COMPRESSION_THRESHOLD`
`0.5`
Trigger chat compression at this fraction of token limit

`COMPRESSION_PRESERVE`
`0.3`
Fraction of recent history to preserve during compression

`LLM_RETRY_MAX_WAIT_SECONDS`
`600`
Maximum wait time for LLM retry backoff

`REGEN_HTML`
`false`
Skip entire pipeline, re-render from existing structured_data.json

`OBSERVABILITY_ENABLED`
`false`
Enable Langfuse tracing for LLM calls

`USER_ID`
*none*
Patient user ID (forge/job-runner mode)

`CHR_ID`
*none*
CHR session ID (forge/job-runner mode)

`N1_API_BASE_URL`
*empty*
N1 API base URL for data fetching

`BUCKET_NAME`
*empty*
S3 bucket for report upload (production)

```
### Infrastructure

##### Deployment

```text
Docker &rarr; Kubernetes ephemeral job. Multi-arch builds (native ARM64 + AMD64). Separate ECR repos for prod (`n1-prod/`) and staging (`n1-staging/`).

```
##### Storage

Two-tier: `scopedStorage` (pipeline intermediates) + `baseStorage` (final HTML with signed URLs). S3 for production, local filesystem for development.

## Overview

  workflow-abc** generates interactive, multi-section Comprehensive Health Reports via an **8-stage multi-agent pipeline** built on LangGraph and DeepAgents.
  Five stages use iterative LLM tool-calling loops with specialized DeepAgents (explore, timeline, analyze, assess, synthesize), one stage uses LLM-based structured extraction, and two stages are deterministic (data preparation and reference linking). The output is a self-contained HTML report with Chart.js visualisations, Cytoscape.js disease-cascade graphs, and interactive tooltips.

```text
Pipeline Stages

Specialized Agents

Shared Subagents

10+
Report Sections

```
**Philosophy:** This is an *orchestrator-driven multi-agent system* using the evaluator–optimizer pattern. Each agent operates in an iterative tool-calling loop with dedicated subagents for data exploration and medical research. Agents communicate via a guarded virtual filesystem — each writes markdown reports that downstream agents read, ensuring clean separation of concerns and full auditability. The analyze stage uses a fan-out/fan-in pattern where per-condition workers run in parallel.

### Agent Architecture Patterns

##### Evaluator–Optimizer Loop

Each agent's output is validated by a dedicated **Validator agent** (Claude Opus). If validation fails, the agent receives structured feedback and retries. Configurable iterations (default: 1).

##### Fan-Out / Fan-In (Analyze)

The **analyze** stage spawns parallel workers via LangGraph's `Send()` API — one worker per identified condition (up to 15). Workers run concurrently with configurable max concurrency (default: 4). Partial failures are tracked separately.

### Output Report Sections

##### 10 Interactive Sections

Executive Summary &bull; Key Metrics Dashboard &bull; Identified Conditions &bull; Medical Timeline &bull; The Biological Story (Cytoscape.js graph) &bull; Detailed Findings by System &bull; Treatment Plan (8 tabs) &bull; Prognosis &bull; Long-Term Outlook &bull; References

##### Interactive Features

Abbreviation tooltips &bull; Citation hover cards &bull; Expandable condition cards &bull; Tabbed treatment plan &bull; System health score bars &bull; Biomarker trend charts (Chart.js) &bull; Disease cascade network graph (Cytoscape.js) &bull; Print-optimized layout

## Pipeline Flowchart

5 agentic stages (iterative LLM tool-calling with DeepAgents) + 1 LLM-based extraction + 2 deterministic stages. The **analyze** and **assess** stages run in parallel after **explore**, then **synthesize** merges all outputs.

```text
S1
Data Preparation
Deterministic

N1 API &rarr; Clean &rarr; Categorise &rarr; Markdown + JSON files

S2
Timeline
Agentic &bull; Claude Sonnet

Pre-generated template &rarr; Agent edits Section 1 in-place &bull; data_explorer + research subagents

S3
Explore
Agentic &bull; Claude Sonnet

Data exploration &rarr; Topic identification &rarr; Severity/actionability ranking

S4a
Analyze
Orchestrator–Worker &bull; Claude Sonnet

Fan-out: Per-condition deep-dive &bull; Up to 15 parallel workers

S4b
Assess
Agentic &bull; Claude Sonnet

Per-body-system scoring &bull; Biomarker trends &bull; Clinical interpretation

S5
Synthesize
Agentic &bull; Claude Sonnet

Cross-condition synthesis &bull; Medication reconciliation &bull; Cascade effects &bull; Treatment plan

S6
Render HTML
LLM Extraction + Deterministic

Parse report &rarr; LLM-based structured extraction (9 schemas) &rarr; Jinja2 template render

S7
Update References
Deterministic

Refresh record URLs &rarr; Append citation tables &rarr; Re-render HTML with reference tooltips

```
### How Each Agent Works (DeepAgents Pattern)

All agentic stages (S2–S5) follow the DeepAgents iterative tool-calling pattern, with shared middleware for patient context injection, retry handling, truncation recovery, and tool output limits.

##### The Agent Loop (shared pattern)

- **1. Inject patient context** via PatientContextMiddleware (name, DOB, age, gender, data file paths)
- **2. Send task + tools to LLM** with extended thinking enabled (budget: 4K–8K tokens)
- **3. LLM responds** with tool calls (e.g. `search_biomarkers("platelets")`) or text
- **4. Execute tools** against guarded filesystem; results added to conversation
- **5. Delegate to subagents** when needed (data_explorer for file navigation, research for web search)
- **6. Check completion** — agent writes final report to workspace filesystem
- **7. Validate** (optional) — Validator agent (Claude Opus) checks output quality; re-run on failure
- **8. Extract structured output** — Pydantic schema extraction from agent messages

##### Middleware Stack (per agent)

- **PatientContextMiddleware:** Injects patient demographics + data file overview into first message
- **RetryMiddleware:** Exponential backoff with jitter (2s initial, 60s max, 4&times; multiplier, 20 retries) for transient errors
- **TruncationHandlerMiddleware:** Detects `max_tokens` truncation, clears incomplete tool calls, injects iterative writing guidance
- **ToolOutputLimitMiddleware:** Prevents context explosion from large tool outputs

##### Two Subagent Types

- **Data Explorer** (Claude Haiku): Lightweight dict-spec subagent. Navigates patient files, extracts specific values, writes summaries to files for context preservation.
- **Research** (Claude Haiku): Lightweight dict-spec subagent. Web search + URL fetch with automatic citation tracking ([w1], [w2], etc.). Source hierarchy: Guidelines > Peer-reviewed > Clinical references.

## Stage Details

#### Data Preparation — API Fetch & Categorisation

Fetches patient data from the N1 API, runs data cleaners, generates a clinical index, and writes structured markdown + JSON files to the workspace. This is the **only stage that contacts external APIs for patient data**.

```text
Input
`USER_ID + CHR_ID &rarr; N1 API (profiles, diagnoses, procedures, biomarkers)`

Output
`profile.md, diagnoses.md, procedures.md, biomarkers.md, metadata.yaml`

```
##### Pipeline

- Fetch patient records from N1 API (authenticated)
- Run data cleaners for validation and normalisation
- Generate clinical index with categorical groupings
- Create markdown and JSON output files in workspace
- Initialise PatientContext (name, DOB, age, gender)
- Populate DataFiles with all generated file paths

#### Timeline — Medical History Chronology

Builds a comprehensive medical timeline. **Unique pattern:** sections 2–5 are programmatically pre-generated from structured data, then the agent reads the full template and edits Section 1 (narrative overview) in-place.

```text
Input
`Patient data files (diagnoses, biomarkers, procedures, profile)`

Output
`/timeline/report.md`

```
##### Key Characteristics

- **Model:** Claude Sonnet (extended thinking: 6K budget)
- **Subagents:** data_explorer, research
- **Middleware:** ToolOutputLimitMiddleware, RetryMiddleware
- **Validation:** Optional evaluator–optimizer loop
- **Report:** 5 sections (1 agent-authored, 4 programmatic)

#### Explore — Data Exploration & Topic Discovery

Explores all patient data to identify condition groups, ranked by severity and actionability. This stage determines the topics that the analyze stage will deep-dive into. Focuses on **discovery and organisation**, not treatment recommendations.

```text
Input
`Timeline report + raw patient data files`

Output
`/explore/summary.md + TopicIdentification[] (structured)`

```
##### Key Characteristics

- **Model:** Claude Sonnet (extended thinking: 6K budget)
- **Subagents:** data_explorer, research
- **Middleware:** ToolOutputLimitMiddleware, TruncationHandlerMiddleware, RetryMiddleware
- **Validation:** Optional evaluator–optimizer loop with structured extraction
- **Structured output:** TopicIdentification with topic_name, severity, actionability, conditions_included, key_biomarkers

```text
4a

```
#### Analyze — Per-Condition Deep Analysis (Orchestrator–Worker)

Uses the **orchestrator–worker** pattern with LangGraph's `Send()` API. The orchestrator receives identified topics from explore, then spawns isolated workers — one per condition — running in parallel with configurable concurrency.

```json
Input
`TopicIdentification[] + timeline + explore reports`

Output
`/analyze/{topic_slug}/report.md (per condition)`

```
##### Key Characteristics

- **Model:** Claude Sonnet (extended thinking: 6K budget)
- **Pattern:** Fan-out via `Send()` &rarr; parallel workers &rarr; fan-in with report collection
- **Max workers:** Configurable (default: 4 concurrent, up to 15 conditions)
- **Error handling:** Partial failures tracked separately; other workers continue
- **Per-worker:** Isolated state + optional validation loop
- **Subagents:** data_explorer, research (per worker)

```text
4b

```
#### Assess — System Health Scoring (runs parallel with Analyze)

Scores each body system (0–100) with evidence-based assessment, biomarker trend analysis, and clinical interpretation. Runs **in parallel** with analyze after explore completes.

```text
Input
`Timeline report + explore report + raw patient data`

Output
`/assess/report.md`

```
##### Key Characteristics

- **Model:** Claude Sonnet (extended thinking: 6K budget)
- **Subagents:** data_explorer, research
- **Tools:** Direct biomarker/diagnosis lookups, clinical guideline verification
- **Middleware:** ToolOutputLimitMiddleware, TruncationHandlerMiddleware, RetryMiddleware
- **Validation:** Optional evaluator–optimizer loop
- **Output:** Per-system health scores with evidence and clinical interpretation

#### Synthesize — Unified Cross-Condition Report

Merges all upstream outputs into a single comprehensive health report. Waits for **both** analyze and assess to complete. Produces the executive summary, medication reconciliation, polypharmacy assessment, cascade effects analysis, and treatment plan.

```text
Input
`Explore + assess + analyze reports (all upstream outputs)`

Output
`/synthesize/report.md`

```
##### Key Characteristics

- **Model:** Claude Sonnet (extended thinking: 6K budget)
- **Subagents:** data_explorer, research
- **Content:** Executive summary, medication reconciliation, polypharmacy assessment, cascade effects, treatment plan, prognosis
- **Middleware:** ToolOutputLimitMiddleware, TruncationHandlerMiddleware, RetryMiddleware
- **Validation:** Optional evaluator–optimizer loop

#### Render HTML — Structured Extraction & Template Rendering

Two-phase rendering: first, an LLM extracts structured data from the synthesize report using 9 Pydantic schemas; then, a Jinja2 template renders the final interactive HTML report deterministically from the extracted JSON.

```text
Input
`Synthesize report + explore + assess reports`

Output
`/render/synthesize.html + /render/extract/*.json`

```
##### Extraction Schemas (9 total)

- **AbbreviationList:** Medical abbreviation definitions for interactive tooltips
- **Header:** Report title, patient info, generation date
- **Sections 1–8:** Timeline overview, system health scores, identified conditions, condition analysis, medication reconciliation, treatment plan, prognosis, health recommendations

##### Rendering Pipeline

- Parse synthesize report markdown into sections (remapped to final numbering)
- Extract additional context from explore and assess reports
- Run LLM-based extraction per section (Gemini Flash) &rarr; save JSON to `/render/extract/*.json`
- Apply abbreviation postprocessor for interactive tooltips
- Render Jinja2 HTML template with extracted data

#### Update References — Citation Linking & URL Refresh

Refreshes N1 API record URLs (preventing expiry), appends source-linked reference tables to all markdown reports, and re-renders the HTML with reference tooltips and citation data.

```text
Input
`All upstream reports + rendered HTML`

Output
`Enhanced reports with &sect; References + re-rendered HTML with tooltips`

```
##### Pipeline

- Refresh record URLs from N1 API (ensure signed URLs not expired)
- Scan all report files (timeline, explore, analyze, assess, synthesize)
- Append source-linked reference tables to each report
- Re-render HTML with reference tables and tooltip data

## How Data Flows Between Agents

Agents communicate via a **guarded virtual filesystem**. Each agent writes markdown reports to its dedicated directory; downstream agents read from upstream directories via the data_explorer subagent.

```text
File-Based Inter-Agent Communication
Architecture

Copy

```
═══ WORKSPACE FILESYSTEM LAYOUT ═══════════════════════════════════════
agent_environment/{user_id}/{YYYY-MM-DD_HH-MM-SS}/

/data/
  profile.md                    # Patient demographics
  diagnoses.md                  # All diagnoses with IDs (d1-dN)
  procedures.md                 # All procedures with IDs (p1-pN)
  biomarkers.md       # Biomarker groups with IDs (cb1-cbN)
  metadata.yaml                 # Clinical index & categorical groupings

/timeline/
  report.md                     # 5-section medical timeline

/explore/
  summary.md                    # Topic identification & severity ranking

/analyze/
  {topic-slug-1}/report.md      # Per-condition deep analysis
  {topic-slug-2}/report.md
  ...up to 15 conditions

/assess/
  report.md                     # Per-body-system health scores

/synthesize/
  report.md                     # Unified cross-condition report

/render/
  extract/                      # Per-section JSON extraction
```text
abbreviations.json
header.json
section_1.json ... section_8.json
```
  synthesize.html               # Final interactive report

```text
Inter-Agent Reading Patterns
Data Flow

Copy

```
```
═══ WHO READS WHAT ════════════════════════════════════════════════════

timeline   reads: /data/*                         # Raw patient data only
explore    reads: /data/* + /timeline/report.md     # Data + timeline context
analyze    reads: /data/* + /timeline/ + /explore/   # All upstream (per worker)
assess     reads: /data/* + /timeline/ + /explore/   # Parallel with analyze
synthesize reads: /explore/ + /assess/ + /analyze/*  # All agent outputs
render_htmlreads: /synthesize/ + /explore/ + /assess/ # For extraction

═══ STATE CHANNELS (LangGraph Reducers) ═══════════════════════════════

# Shared fields (take-last reducer):
workspace_root: str
patient_context: PatientContext   # user_id, name, dob, age, gender
data_files: DataFiles             # Paths to all input data files

# Per-agent message channels (add-messages reducer):
explore_messages: list[BaseMessage]
timeline_messages: list[BaseMessage]
assess_messages: list[BaseMessage]
synthesize_messages: list[BaseMessage]

# Output paths:
timeline_report_path: str | None
explore_report_path: str | None
identified_topics: list[TopicIdentification] | None
analyze_report_paths: list[str]        # add reducer (accumulates)
analyze_errors: list[dict]              # add reducer (tracks failures)
assess_report_path: str | None
synthesize_report_path: str | None
render_html_path: str | None            # Final output
```

```text
Biomarker Value: End-to-End Journey Through Agents
Example

Copy

```
```
═══ STAGE 1: Data Preparation ═════════════════════════════════════════
N1 API returns patient biomarker data:

biomarkers.md:
  cb42 | Platelets | Hematologic
```text
b97: 2024-01-15 → 138 x10^9/L (Low)   ref: 150-400
b64: 2023-06-22 → 141 x10^9/L (Low)   ref: 150-400
b33: 2021-11-10 → 149 x10^9/L (Low)   ref: 150-400

```
═══ STAGE 2: Timeline ═════════════════════════════════════════════════
Agent reads pre-generated sections 2-5, edits section 1 in-place:

Section 1 (agent-authored): "Chronic mild thrombocytopenia (138-149 range)
present since earliest records, with gradual downward trend..."

═══ STAGE 3: Explore ══════════════════════════════════════════════════
Agent calls patient data tools:

search_biomarkers(category="Hematologic", out_of_range_only=True)
→ Returns platelets, WBC, and other out-of-range hematologic markers

Agent identifies topic:
TopicIdentification(
  topic_name="Hematologic Dysfunction",
  severity="Moderate",
  actionability="High",
  conditions_included=["Thrombocytopenia", "Anemia"],
  key_biomarkers=["cb42", "cb15"]
)

═══ STAGE 4a: Analyze (Worker) ════════════════════════════════════════
Worker spawned for "hematologic-dysfunction" topic:

search_biomarkers(ids=["cb42"], include_details=True)
→ Full platelet history with reference ranges and dates

Research subagent:
web_search("chronic mild thrombocytopenia differential diagnosis")
→ Returns [w1]: UpToDate, [w2]: Blood journal review

Writes: /analyze/hematologic-dysfunction/report.md

═══ STAGE 4b: Assess (parallel) ═══════════════════════════════════════
Agent scores Hematologic system:

search_biomarkers(category="Hematologic")
search_diagnoses(system="Hematologic")

Score: 42/100 | Level: warning | Status: "Concerning"
Biomarker trends: Platelets ↓ (gradual decline over 3 years)

═══ STAGE 5: Synthesize ═══════════════════════════════════════════════
Agent reads all upstream reports via data_explorer:

data_explorer: read /assess/report.md → gets Hematologic score: 42
data_explorer: read /analyze/hematologic-dysfunction/report.md → gets deep analysis

Produces unified section with cross-condition context:
"Chronic thrombocytopenia (platelets trending ↓ from 149 to 138)
interacts with metabolic dysfunction via..."

═══ STAGE 6: Render HTML ══════════════════════════════════════════════
LLM extracts structured data from synthesize report:

section_2.json (Key Metrics Dashboard):
  { systems: [{ name: "Hematologic", score: 42, level: "warning", ... }] }

Jinja2 renders Chart.js trend chart + score bar + tooltip

═══ STAGE 7: Update References ════════════════════════════════════════
Appends to all reports:

## References
| ID   | Type      | Name       | Link |
|------|-----------|------------|------|
| cb42 | Biomarker | Platelets  | [View Record](https://api.n1.care/...) |
| [w1] | Web       | UpToDate   | [View Source](https://uptodate.com/...) |
```

## Data Contracts

All data contracts are defined as Pydantic models in Python. Key schemas are in `src/healthcare_agent/workflow/subgraphs/*/schemas.py` and `src/healthcare_agent/workflow/state.py`.

### Core State

  HealthcareGraphState — Main workflow state (TypedDict)

```text
Copy

```
class HealthcareGraphState(TypedDict):
```text
# Shared fields (take-last reducer)
workspace_root: str
patient_context: PatientContext
data_files: DataFiles

# Per-agent message channels (add-messages reducer)
explore_messages: list[BaseMessage]
timeline_messages: list[BaseMessage]
assess_messages: list[BaseMessage]
synthesize_messages: list[BaseMessage]

# Stage outputs
timeline_report_path: str | None
explore_report_path: str | None
identified_topics: list[TopicIdentification] | None
analyze_report_paths: list[str]        # add reducer
analyze_errors: list[dict]              # add reducer
assess_report_path: str | None
synthesize_report_path: str | None
render_html_path: str | None            # Final output

```
### Key Domain Models

  TopicIdentification — Explore output schema

```text
Copy

```
```
class TopicIdentification(BaseModel):
```text
topic_name: str                     # "Metabolic Syndrome"
topic_slug: str                     # "metabolic-syndrome"
conditions_included: list[str]     # ["Type 2 Diabetes", "Dyslipidemia"]
severity: Literal["High", "Moderate", "Low"]
actionability: Literal["High", "Moderate", "Low"]
rationale: str                      # With citations [d5], [cb42]
key_biomarkers: list[str]           # ["cb42", "cb15"]
```
```

  PatientContext & DataFiles — Shared context injected into all agents

```text
Copy

```
```
class PatientContext(TypedDict):
```text
user_id: str
patient_name: str
dob: str
age: int
gender: str
today_date: str

```
class DataFiles(TypedDict):
```text
profile: str                       # /data/profile.md
diagnoses: str                     # /data/diagnoses.md
procedures: str                    # /data/procedures.md
biomarkers: str                    # /data/biomarkers.md
metadata: str                      # /data/metadata.yaml
```
```

  HTML Extraction Schemas — 9 Pydantic models for structured rendering

```text
Copy

```
```
# 9 extraction schemas used in Stage 6 (Render HTML):
class AbbreviationList(BaseModel):     # Interactive tooltip definitions
class Header(BaseModel):                # Title, patient info, date
class Section1(BaseModel):              # Timeline overview
class Section2(BaseModel):              # System health scores (from assess)
class Section3(BaseModel):              # Identified conditions
class Section4(BaseModel):              # Condition analysis detail
class Section5(BaseModel):              # Biological story (Cytoscape graph)
class Section6(BaseModel):              # Medication reconciliation
class Section7(BaseModel):              # Treatment plan
class Section8(BaseModel):              # Prognosis + long-term outlook
```

## Technology & Dependency Map

Dependencies used at each stage.  = deterministic,  = LLM-powered.

```text
Technology
S1
S2
S3
S4
S5
S6
S7

LangGraph

DeepAgents

LangChain

Anthropic Claude (Sonnet)

Anthropic Claude (Opus)

Anthropic Claude (Haiku)

Gemini (extraction)

Pydantic

Jinja2

N1 API Client

Langfuse

S3 / local storage

```
### Key Technology Roles

```text
LangGraph
State graph orchestration. Manages workflow node execution, fan-out/fan-in parallelisation via `Send()`, and custom reducers for state merging.

DeepAgents
Multi-turn agent framework with tool calling and subagent delegation. Provides the iterative loop pattern and middleware injection points.

LangChain
Model abstraction layer (`ChatAnthropic`, `ChatOpenAI`, `ChatDeepSeek`). Provides `BaseTool` interface and message types for agent communication.

Anthropic Claude
Primary LLM family. Opus for validation (8K thinking budget), Sonnet for main agents (6K thinking budget), Haiku for subagents (4K thinking budget). All use extended thinking with interleaved-thinking beta.

Gemini (via LiteLLM)
Used for structured data extraction in render_html stage. Flash variant for fast extraction, Pro variant for categorisation with thinking.

Pydantic
Schema validation for all structured outputs (TopicIdentification, HTML extraction schemas, settings). `BaseSettings` for environment configuration.

Jinja2
Deterministic HTML template rendering. Converts extracted JSON into interactive report with Chart.js, Cytoscape.js, and tooltip data.

structlog
Structured logging for all pipeline stages. Machine-readable log events with context injection.

Langfuse
LLM observability: trace spans per stage, generation-level token tracking, cost attribution. Disabled by default.

N1 API Client
Authenticated access to patient records (profiles, diagnoses, procedures, biomarkers). Used in data_preparation and update_references stages.

```
### Agent &harr; Model Assignment

```text
Agent
Model
Thinking Budget
Purpose

`explore`
Claude Sonnet
6,000 tokens
Data exploration & topic discovery

`timeline`
Claude Sonnet
6,000 tokens
Medical history chronology

`analyze`
Claude Sonnet
6,000 tokens
Per-condition deep analysis

`assess`
Claude Sonnet
6,000 tokens
System health scoring

`synthesize`
Claude Sonnet
6,000 tokens
Cross-condition synthesis

`validator`
Claude Opus
8,000 tokens
Quality validation (evaluator)

`data_explorer`
Claude Haiku
4,000 tokens
File navigation subagent

`research`
Claude Haiku
4,000 tokens
Web research subagent

`extraction`
Gemini Flash
—
Structured data extraction

```
## Configuration

All settings from `src/settings.py` via Pydantic `BaseSettings`. Agent-level config in `src/healthcare_agent/config.py`.

  SharedConfig — Agent Pipeline Configuration

```text
Copy

```
class SharedConfig:
```json
workspace_root: Path               # agent_environment/{user_id}/{timestamp}/
workspace_override: str | None     # Reuse existing workspace on reruns
require_validation: bool = True    # Enable validator agent
max_validation_iterations: int = 1 # Retry failed validation N times
max_analyze_workers: int = 4       # Parallel analyze workers (fan-out)
max_conditions: int = 15           # Limit conditions analysed
model_overrides: dict[str, str]    # {"explore": "claude-opus", ...}

```
# Features:
# - Lazy initialisation: workspace created on first access
# - Lazy model caching: models created on demand, cached per-agent
# - Guarded file backend: virtual filesystem with guardrails
# - Per-agent model override support via model_overrides dict

### Environment Variables

```text
Variable
Default
Description

`USER_ID`
*required*
Patient user ID for API data fetch

`CHR_ID`
*required*
CHR session ID for progress tracking

`OPENAI_API_KEY`
*required*
LiteLLM proxy API key (routes to Claude/Gemini)

`OPENAI_BASE_URL`
*required*
LiteLLM proxy base URL

`N1_API_KEY`
*required*
N1 API authentication key

`N1_API_BASE_URL`
*required*
N1 API base URL for data fetching

`BUCKET_NAME`
*empty*
S3 bucket for report upload (production)

`REQUIRE_VALIDATION`
`true`
Enable/disable validator agent

`MAX_VALIDATION_ITERATIONS`
`1`
Max validation retry loops per agent

`MAX_ANALYZE_WORKERS`
`4`
Max concurrent analyze workers (fan-out)

`MAX_CONDITIONS`
`15`
Limit conditions for analyze stage

`MODEL_CLAUDE_OPUS`
`claude-4.6-opus`
Claude Opus model version

`MODEL_CLAUDE_SONNET`
`claude-4.6-sonnet`
Claude Sonnet model version

`MODEL_CLAUDE_HAIKU`
`claude-4.5-haiku`
Claude Haiku model version

`MODEL_GEMINI_PRO`
`gemini-3-pro-preview`
Gemini Pro model version

`MODEL_GEMINI_FLASH`
`gemini-3-flash-preview`
Gemini Flash model version

`CHR_FILENAME`
`HEALTH_REPORT.html`
Output HTML filename

`IS_DEVELOPMENT`
`false`
Development mode flag

`LANGFUSE_ENABLED`
`false`
Enable Langfuse LLM observability

```
### Infrastructure

##### Deployment

```text
Docker &rarr; Kubernetes ephemeral job. Python 3.11+ with `uv` package manager. LangGraph Studio for local development (`uv run langgraph dev`).

```
##### Storage

Guarded virtual filesystem (in-memory or disk) for pipeline intermediates. S3 for production report storage with signed URLs. Workspace path: `agent_environment/{user_id}/{timestamp}/`

##### Retry & Resilience

Exponential backoff: 2s initial, 60s max, 4&times; multiplier, full jitter. 20 retries for transient errors. Retryable status codes: 429, 500, 502, 503, 504, 524, 529.

##### Observability

Structured logging (structlog) + optional Langfuse tracing. Progress tracking via N1 API stages: DATA_PREP, TIMELINE, EXPLORE, ANALYZE, ASSESS, SYNTHESIZE, RENDER, REFERENCES.