pbanavara commited on
Commit
e894ebf
Β·
verified Β·
1 Parent(s): 9cc586f

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -97,7 +97,7 @@ file_report() β†’ KARS PASSED, reward=+15
97
  ```bash
98
  # Start the server
99
  conda activate openenv
100
- uvicorn server.app:app --host 0.0.0.0 --port 8000
101
  ```
102
 
103
  ```python
@@ -120,11 +120,27 @@ trajectories = run_episodes(
120
 
121
  ## Patients
122
 
123
- | ID | Condition | T1 GFR | T5 GFR | HbA1c T1β†’T5 | Notes |
124
- |----|-----------|--------|--------|-------------|-------|
125
- | P001 | CKD Stage 4 | 18.5 | 12.1 | 7.2β†’8.9 | Complete record |
126
- | P002 | Diabetic nephropathy | 11.0 | 8.3 | 9.1β†’10.2 | Antihypertensives, insulin |
127
- | P003 | CKD Stage 3 | 22.3 | 19.8 | null | HbA1c never recorded, inactive waitlist |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  ## KARS Required Fields
130
 
@@ -143,10 +159,12 @@ prana_env/
143
  β”œβ”€β”€ models.py # PranaAction, PranaObservation
144
  β”œβ”€β”€ test_agent.py # LLM agent RL loop (GPT-4o)
145
  β”œβ”€β”€ test_client.py # Smoke test client
 
146
  β”œβ”€β”€ data/
147
- β”‚ └── patient_db.json # Patient records with T1 snapshots and T5 values
 
148
  └── server/
149
- β”œβ”€β”€ app.py # FastAPI + WebSocket server
150
  β”œβ”€β”€ prana_env_environment.py # RL environment: actions, KARS validator, rewards
151
  └── Dockerfile
152
  ```
@@ -157,7 +175,7 @@ prana_env/
157
  from prana_env.client import PranaEnv
158
  from prana_env.models import PranaAction
159
 
160
- with PranaEnv(base_url="http://localhost:8000") as env:
161
  result = env.reset(patient_id="P001")
162
  print(result.observation.query_result)
163
 
 
97
  ```bash
98
  # Start the server
99
  conda activate openenv
100
+ uvicorn server.app:app --host 0.0.0.0 --port 7860
101
  ```
102
 
103
  ```python
 
120
 
121
  ## Patients
122
 
123
+ 50 procedurally generated patients (P001–P050) across CKD stages 3–5:
124
+
125
+ | ID | Condition | Notes |
126
+ |----|-----------|-------|
127
+ | P001 | CKD Stage 4 | Complete record, anchor patient |
128
+ | P002 | Diabetic nephropathy | Antihypertensives, insulin, anchor patient |
129
+ | P003 | CKD Stage 3 | HbA1c not recorded (non-diabetic), anchor patient |
130
+ | P004–P050 | CKD Stage 3/4/5 | Procedurally generated (seed=42) |
131
+
132
+ **Patient distribution:**
133
+ - CKD Stage 3: ~25% Β· Stage 4: ~50% Β· Stage 5: ~25%
134
+ - 60% diabetic β€” HbA1c present; non-diabetics have 85% chance of missing HbA1c
135
+ - ~10% of patients have an injected anomalous lab reading (>25% delta) for benchmark coverage
136
+
137
+ **All patients include distractor fields** (queryable but not KARS-required):
138
+ `cholesterol`, `bmi`, `albumin`, `hemoglobin`
139
+
140
+ To regenerate the patient database:
141
+ ```bash
142
+ python data/generate_patients.py
143
+ ```
144
 
145
  ## KARS Required Fields
146
 
 
159
  β”œβ”€β”€ models.py # PranaAction, PranaObservation
160
  β”œβ”€β”€ test_agent.py # LLM agent RL loop (GPT-4o)
161
  β”œβ”€β”€ test_client.py # Smoke test client
162
+ β”œβ”€β”€ prana_grpo_qwen3_8b_fp8.ipynb # GRPO fine-tuning notebook (Qwen3-8B FP8, H100)
163
  β”œβ”€β”€ data/
164
+ β”‚ β”œβ”€β”€ patient_db.json # 50 patients with T1 snapshots, T5 values, distractor fields
165
+ β”‚ └── generate_patients.py # Procedural patient generator (seed=42, CKD stage distributions)
166
  └── server/
167
+ β”œβ”€β”€ app.py # FastAPI + WebSocket server (port 7860)
168
  β”œβ”€β”€ prana_env_environment.py # RL environment: actions, KARS validator, rewards
169
  └── Dockerfile
170
  ```
 
175
  from prana_env.client import PranaEnv
176
  from prana_env.models import PranaAction
177
 
178
+ with PranaEnv(base_url="http://localhost:7860") as env:
179
  result = env.reset(patient_id="P001")
180
  print(result.observation.query_result)
181
 
data/generate_patients.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate procedural patient database for PRANA-Env and tau2 benchmark.
4
+
5
+ Produces 50 patients (P001-P050) across CKD stages 3-5 with:
6
+ - Stage-appropriate GFR / creatinine distributions
7
+ - Diabetic status driving HbA1c presence
8
+ - Systematic missing fields (non-diabetic β†’ null HbA1c, etc.)
9
+ - Distractor fields (cholesterol, bmi, albumin, hemoglobin)
10
+ - T1 snapshot with slightly better values (disease progression)
11
+ - Anomaly injection for ~10% of patients (for benchmark coverage)
12
+
13
+ Outputs:
14
+ - prana_env/data/patient_db.json (prana_env flat format)
15
+ - tau2-bench/data/tau2/domains/prana/db.json (tau2 LabResult format, preserves other DBs)
16
+ """
17
+
18
+ import json
19
+ import random
20
+ from pathlib import Path
21
+ from datetime import date, timedelta
22
+
23
+ SEED = 42
24
+ N_PATIENTS = 50
25
+ EPISODE_DATE = date(2026, 3, 7)
26
+ T1_NOMINAL_DATE = date(2025, 11, 7)
27
+ T5_MEAS_DATE = date(2026, 3, 1) # recent measurement date used in tau2 history
28
+
29
+ rng = random.Random(SEED)
30
+
31
+ # ── Clinical distributions ─────────────────────────────────────────────────────
32
+
33
+ CKD_STAGES = {
34
+ 3: {"gfr": (30, 59), "creatinine": (1.2, 2.5), "weight": 0.25},
35
+ 4: {"gfr": (15, 29), "creatinine": (2.5, 5.0), "weight": 0.50},
36
+ 5: {"gfr": (5, 14), "creatinine": (5.0, 9.5), "weight": 0.25},
37
+ }
38
+
39
+ BLOOD_TYPES = ["O+", "A+", "B+", "AB+", "O-", "A-", "B-", "AB-"]
40
+ BLOOD_WGTS = [0.38, 0.34, 0.09, 0.03, 0.07, 0.06, 0.02, 0.01]
41
+
42
+ FIRST_NAMES = ["James","Maria","David","Sarah","Michael","Linda","Robert","Patricia",
43
+ "William","Barbara","Richard","Susan","Joseph","Jessica","Thomas","Karen",
44
+ "Charles","Lisa","Christopher","Nancy","Daniel","Betty","Matthew","Margaret",
45
+ "Anthony","Sandra","Mark","Ashley","Donald","Dorothy","Steven","Kimberly",
46
+ "Paul","Emily","Andrew","Donna","Joshua","Michelle","Kenneth","Carol",
47
+ "Kevin","Amanda","Brian","Melissa","George","Deborah","Timothy","Stephanie",
48
+ "Ronald","Rebecca"]
49
+ LAST_NAMES = ["Smith","Johnson","Williams","Brown","Jones","Garcia","Miller","Davis",
50
+ "Rodriguez","Martinez","Hernandez","Lopez","Gonzalez","Wilson","Anderson",
51
+ "Thomas","Taylor","Moore","Jackson","Martin","Lee","Perez","Thompson",
52
+ "White","Harris","Sanchez","Clark","Ramirez","Lewis","Robinson","Walker",
53
+ "Young","Allen","King","Wright","Scott","Torres","Nguyen","Hill","Flores",
54
+ "Green","Adams","Nelson","Baker","Hall","Rivera","Campbell","Mitchell",
55
+ "Carter","Roberts"]
56
+
57
+ # ── Anomaly patients β€” fixed set for benchmark reproducibility ─────────────────
58
+ # These patients have an extra measurement close to filing that triggers >25% delta
59
+ ANOMALY_PATIENT_INDICES = {7, 12, 19, 26, 33} # 0-indexed within P004-P050
60
+
61
+
62
+ def pick_ckd_stage() -> int:
63
+ stages = list(CKD_STAGES.keys())
64
+ weights = [CKD_STAGES[s]["weight"] for s in stages]
65
+ return rng.choices(stages, weights=weights)[0]
66
+
67
+
68
+ def generate_patient(idx: int) -> dict:
69
+ """Return a patient dict in prana_env format."""
70
+ patient_id = f"P{idx:03d}"
71
+ stage = pick_ckd_stage()
72
+ cfg = CKD_STAGES[stage]
73
+
74
+ # T5 current values
75
+ gfr_t5 = round(rng.uniform(*cfg["gfr"]), 1)
76
+ creatinine_t5 = round(rng.uniform(*cfg["creatinine"]), 1)
77
+
78
+ diabetic = rng.random() < 0.60
79
+ hba1c_t5 = round(rng.uniform(6.5, 12.0), 1) if diabetic else None
80
+
81
+ # Missing field scenarios
82
+ # Non-diabetic patients: 85% chance HbA1c not measured
83
+ missing_hba1c = (not diabetic and rng.random() < 0.85) or (diabetic and rng.random() < 0.04)
84
+ missing_creatinine = rng.random() < 0.05
85
+ missing_blood_type = rng.random() < 0.03
86
+
87
+ blood_type = rng.choices(BLOOD_TYPES, weights=BLOOD_WGTS)[0] if not missing_blood_type else None
88
+ pra = round(rng.uniform(0, 80), 1)
89
+
90
+ # T1 values: disease was less advanced β€” GFR higher, creatinine lower
91
+ gfr_t1 = round(min(gfr_t5 * rng.uniform(1.15, 1.60), 60.0), 1)
92
+ creatinine_t1 = round(creatinine_t5 * rng.uniform(0.55, 0.85), 1)
93
+ hba1c_t1 = round(hba1c_t5 * rng.uniform(0.82, 0.96), 1) if hba1c_t5 is not None else None
94
+
95
+ # Distractor fields β€” present, queryable, not KARS-required
96
+ cholesterol = round(rng.uniform(140, 270), 1)
97
+ bmi = round(rng.uniform(18.5, 40.0), 1)
98
+ albumin = round(rng.uniform(2.0, 4.2), 2) # Low in CKD
99
+ hemoglobin = round(rng.uniform(7.5, 13.5), 1) # Anemia common in CKD
100
+
101
+ t1_snapshot: dict = {
102
+ "gfr": gfr_t1,
103
+ "creatinine": creatinine_t1 if not missing_creatinine else None,
104
+ "blood_type": blood_type,
105
+ "pra": pra,
106
+ "recorded_at": T1_NOMINAL_DATE.isoformat(),
107
+ }
108
+ if hba1c_t1 is not None and not missing_hba1c:
109
+ t1_snapshot["hba1c"] = hba1c_t1
110
+
111
+ patient: dict = {
112
+ "patient_id": patient_id,
113
+ "name": f"{rng.choice(FIRST_NAMES)} {rng.choice(LAST_NAMES)}",
114
+ "age": rng.randint(28, 72),
115
+ "ckd_stage": stage,
116
+ "gfr": gfr_t5,
117
+ "creatinine": creatinine_t5 if not missing_creatinine else None,
118
+ "blood_type": blood_type,
119
+ "pra": pra,
120
+ # Distractor fields
121
+ "cholesterol": cholesterol,
122
+ "bmi": bmi,
123
+ "albumin": albumin,
124
+ "hemoglobin": hemoglobin,
125
+ "t1_snapshot": t1_snapshot,
126
+ }
127
+ if hba1c_t5 is not None and not missing_hba1c:
128
+ patient["hba1c"] = hba1c_t5
129
+
130
+ return patient
131
+
132
+
133
+ def to_tau2_patient(p: dict, anomaly: bool = False) -> dict:
134
+ """Convert prana_env patient dict to tau2 LabResult format."""
135
+ pid = p["patient_id"]
136
+
137
+ def lab_history(t1_val, t5_val, anomaly_entry=None) -> list:
138
+ entries = []
139
+ if t1_val is not None:
140
+ entries.append({"value": t1_val, "recorded_at": T1_NOMINAL_DATE.isoformat()})
141
+ if anomaly_entry:
142
+ entries.append(anomaly_entry)
143
+ if t5_val is not None:
144
+ entries.append({"value": t5_val, "recorded_at": T5_MEAS_DATE.isoformat()})
145
+ return entries
146
+
147
+ snap = p.get("t1_snapshot", {})
148
+
149
+ # Anomaly: inject a second T5 measurement with >25% delta, 6 days before filing
150
+ gfr_anomaly = None
151
+ if anomaly and p.get("gfr") is not None:
152
+ anomaly_gfr = round(p["gfr"] * 0.55, 1) # 45% drop β€” clearly anomalous
153
+ gfr_anomaly = {"value": anomaly_gfr, "recorded_at": "2026-03-01"}
154
+
155
+ tau2: dict = {
156
+ "patient_id": pid,
157
+ "name": p["name"],
158
+ "age": p["age"],
159
+ "blood_type": p.get("blood_type"),
160
+ "pra": p.get("pra"),
161
+ "gfr": lab_history(snap.get("gfr"), p.get("gfr"), gfr_anomaly),
162
+ "creatinine": lab_history(snap.get("creatinine"), p.get("creatinine")),
163
+ "hba1c": lab_history(snap.get("hba1c"), p.get("hba1c")),
164
+ }
165
+ return tau2
166
+
167
+
168
+ # ── Main ──────────────────────────────────────────────────────────────────────
169
+
170
+ def main():
171
+ prana_env_root = Path(__file__).parent.parent
172
+ tau2_root = prana_env_root.parent / "tau2-bench"
173
+
174
+ # Load existing P001-P003 as anchors
175
+ existing_prana = json.loads((prana_env_root / "data" / "patient_db.json").read_text())
176
+ existing_patients = existing_prana["patients"] # P001, P002, P003
177
+
178
+ # Load existing tau2 db to preserve non-patient sections
179
+ tau2_db_path = tau2_root / "data" / "tau2" / "domains" / "prana" / "db.json"
180
+ existing_tau2 = json.loads(tau2_db_path.read_text())
181
+
182
+ # Generate P004-P050
183
+ new_prana_patients = {}
184
+ new_tau2_patients = {}
185
+
186
+ for idx in range(4, N_PATIENTS + 1):
187
+ p = generate_patient(idx)
188
+ pid = p["patient_id"]
189
+ is_anomaly = (idx - 4) in ANOMALY_PATIENT_INDICES
190
+
191
+ # Add distractor fields to existing P001-P003 if not present
192
+ new_prana_patients[pid] = p
193
+ new_tau2_patients[pid] = to_tau2_patient(p, anomaly=is_anomaly)
194
+
195
+ # Add distractor fields to existing P001-P003
196
+ distractor_defaults = {
197
+ "P001": {"cholesterol": 187.3, "bmi": 24.1, "albumin": 3.2, "hemoglobin": 10.8},
198
+ "P002": {"cholesterol": 214.6, "bmi": 27.3, "albumin": 2.8, "hemoglobin": 9.4},
199
+ "P003": {"cholesterol": 168.9, "bmi": 22.7, "albumin": 3.6, "hemoglobin": 11.2},
200
+ }
201
+ for pid, extras in distractor_defaults.items():
202
+ existing_patients[pid].update(extras)
203
+
204
+ # ── Write prana_env patient_db.json ───────────────────────────────────────
205
+ prana_out = {"patients": {**existing_patients, **new_prana_patients}}
206
+ out_path = prana_env_root / "data" / "patient_db.json"
207
+ out_path.write_text(json.dumps(prana_out, indent=2))
208
+ print(f"Wrote {len(prana_out['patients'])} patients β†’ {out_path}")
209
+
210
+ # ── Write tau2 db.json ─────────────────────────────────────────────────────
211
+ all_tau2_patients = {**existing_tau2["patient_db"], **new_tau2_patients}
212
+ existing_tau2["patient_db"] = all_tau2_patients
213
+ tau2_db_path.write_text(json.dumps(existing_tau2, indent=2))
214
+ print(f"Wrote {len(all_tau2_patients)} patients β†’ {tau2_db_path}")
215
+
216
+ # ── Summary ───────────────────────────────────────────────────────────────
217
+ stages = {}
218
+ missing_hba1c = missing_creatinine = missing_bt = anomaly_count = 0
219
+ for pid, p in prana_out["patients"].items():
220
+ s = p.get("ckd_stage", "?")
221
+ stages[s] = stages.get(s, 0) + 1
222
+ if p.get("hba1c") is None:
223
+ missing_hba1c += 1
224
+ if p.get("creatinine") is None:
225
+ missing_creatinine += 1
226
+ if p.get("blood_type") is None:
227
+ missing_bt += 1
228
+
229
+ print(f"\nSummary:")
230
+ print(f" CKD stages: {dict(sorted(stages.items()))}")
231
+ print(f" Missing HbA1c: {missing_hba1c}/{N_PATIENTS}")
232
+ print(f" Missing creatinine: {missing_creatinine}/{N_PATIENTS}")
233
+ print(f" Missing blood_type: {missing_bt}/{N_PATIENTS}")
234
+ print(f" Anomaly patients (tau2): {len(ANOMALY_PATIENT_INDICES)}")
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()
data/patient_db.json CHANGED
@@ -16,7 +16,11 @@
16
  "blood_type": "A+",
17
  "pra": 12,
18
  "recorded_at": "2025-11-07"
19
- }
 
 
 
 
20
  },
21
  "P002": {
22
  "patient_id": "P002",
@@ -34,7 +38,11 @@
34
  "blood_type": "O-",
35
  "pra": 45,
36
  "recorded_at": "2025-11-07"
37
- }
 
 
 
 
38
  },
39
  "P003": {
40
  "patient_id": "P003",
@@ -52,7 +60,1058 @@
52
  "blood_type": "B+",
53
  "pra": 8,
54
  "recorded_at": "2025-11-07"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
  }
57
  }
58
- }
 
16
  "blood_type": "A+",
17
  "pra": 12,
18
  "recorded_at": "2025-11-07"
19
+ },
20
+ "cholesterol": 187.3,
21
+ "bmi": 24.1,
22
+ "albumin": 3.2,
23
+ "hemoglobin": 10.8
24
  },
25
  "P002": {
26
  "patient_id": "P002",
 
38
  "blood_type": "O-",
39
  "pra": 45,
40
  "recorded_at": "2025-11-07"
41
+ },
42
+ "cholesterol": 214.6,
43
+ "bmi": 27.3,
44
+ "albumin": 2.8,
45
+ "hemoglobin": 9.4
46
  },
47
  "P003": {
48
  "patient_id": "P003",
 
60
  "blood_type": "B+",
61
  "pra": 8,
62
  "recorded_at": "2025-11-07"
63
+ },
64
+ "cholesterol": 168.9,
65
+ "bmi": 22.7,
66
+ "albumin": 3.6,
67
+ "hemoglobin": 11.2
68
+ },
69
+ "P004": {
70
+ "patient_id": "P004",
71
+ "name": "Michelle Moore",
72
+ "age": 28,
73
+ "ckd_stage": 4,
74
+ "gfr": 15.4,
75
+ "creatinine": 3.2,
76
+ "blood_type": "A+",
77
+ "pra": 2.4,
78
+ "cholesterol": 165.8,
79
+ "bmi": 32.5,
80
+ "albumin": 3.2,
81
+ "hemoglobin": 8.8,
82
+ "t1_snapshot": {
83
+ "gfr": 19.2,
84
+ "creatinine": 2.2,
85
+ "blood_type": "A+",
86
+ "pra": 2.4,
87
+ "recorded_at": "2025-11-07",
88
+ "hba1c": 8.7
89
+ },
90
+ "hba1c": 10.6
91
+ },
92
+ "P005": {
93
+ "patient_id": "P005",
94
+ "name": "Kevin Flores",
95
+ "age": 51,
96
+ "ckd_stage": 5,
97
+ "gfr": 6.4,
98
+ "creatinine": 6.9,
99
+ "blood_type": "O+",
100
+ "pra": 27.5,
101
+ "cholesterol": 156.2,
102
+ "bmi": 38.3,
103
+ "albumin": 2.17,
104
+ "hemoglobin": 9.3,
105
+ "t1_snapshot": {
106
+ "gfr": 8.1,
107
+ "creatinine": 3.9,
108
+ "blood_type": "O+",
109
+ "pra": 27.5,
110
+ "recorded_at": "2025-11-07",
111
+ "hba1c": 6.8
112
+ },
113
+ "hba1c": 7.7
114
+ },
115
+ "P006": {
116
+ "patient_id": "P006",
117
+ "name": "Richard Wright",
118
+ "age": 43,
119
+ "ckd_stage": 4,
120
+ "gfr": 24.9,
121
+ "creatinine": 2.6,
122
+ "blood_type": "O+",
123
+ "pra": 50.9,
124
+ "cholesterol": 174.7,
125
+ "bmi": 38.6,
126
+ "albumin": 3.43,
127
+ "hemoglobin": 11.2,
128
+ "t1_snapshot": {
129
+ "gfr": 32.7,
130
+ "creatinine": 1.7,
131
+ "blood_type": "O+",
132
+ "pra": 50.9,
133
+ "recorded_at": "2025-11-07",
134
+ "hba1c": 6.9
135
+ },
136
+ "hba1c": 8.1
137
+ },
138
+ "P007": {
139
+ "patient_id": "P007",
140
+ "name": "Daniel Wilson",
141
+ "age": 69,
142
+ "ckd_stage": 3,
143
+ "gfr": 41.0,
144
+ "creatinine": 2.5,
145
+ "blood_type": "B+",
146
+ "pra": 18.3,
147
+ "cholesterol": 174.8,
148
+ "bmi": 23.0,
149
+ "albumin": 4.07,
150
+ "hemoglobin": 12.8,
151
+ "t1_snapshot": {
152
+ "gfr": 47.7,
153
+ "creatinine": 1.6,
154
+ "blood_type": "B+",
155
+ "pra": 18.3,
156
+ "recorded_at": "2025-11-07"
157
+ }
158
+ },
159
+ "P008": {
160
+ "patient_id": "P008",
161
+ "name": "Richard Baker",
162
+ "age": 55,
163
+ "ckd_stage": 4,
164
+ "gfr": 27.4,
165
+ "creatinine": 4.1,
166
+ "blood_type": "A+",
167
+ "pra": 46.7,
168
+ "cholesterol": 204.2,
169
+ "bmi": 34.7,
170
+ "albumin": 3.89,
171
+ "hemoglobin": 8.4,
172
+ "t1_snapshot": {
173
+ "gfr": 36.0,
174
+ "creatinine": 3.5,
175
+ "blood_type": "A+",
176
+ "pra": 46.7,
177
+ "recorded_at": "2025-11-07",
178
+ "hba1c": 6.1
179
+ },
180
+ "hba1c": 7.3
181
+ },
182
+ "P009": {
183
+ "patient_id": "P009",
184
+ "name": "Timothy Taylor",
185
+ "age": 60,
186
+ "ckd_stage": 4,
187
+ "gfr": 20.4,
188
+ "creatinine": 4.0,
189
+ "blood_type": "O+",
190
+ "pra": 70.8,
191
+ "cholesterol": 178.2,
192
+ "bmi": 21.9,
193
+ "albumin": 2.01,
194
+ "hemoglobin": 11.8,
195
+ "t1_snapshot": {
196
+ "gfr": 30.4,
197
+ "creatinine": 3.1,
198
+ "blood_type": "O+",
199
+ "pra": 70.8,
200
+ "recorded_at": "2025-11-07",
201
+ "hba1c": 6.9
202
+ },
203
+ "hba1c": 7.9
204
+ },
205
+ "P010": {
206
+ "patient_id": "P010",
207
+ "name": "Margaret Martin",
208
+ "age": 43,
209
+ "ckd_stage": 5,
210
+ "gfr": 9.6,
211
+ "creatinine": 5.5,
212
+ "blood_type": "O+",
213
+ "pra": 12.9,
214
+ "cholesterol": 259.4,
215
+ "bmi": 31.4,
216
+ "albumin": 3.07,
217
+ "hemoglobin": 8.2,
218
+ "t1_snapshot": {
219
+ "gfr": 15.2,
220
+ "creatinine": 4.5,
221
+ "blood_type": "O+",
222
+ "pra": 12.9,
223
+ "recorded_at": "2025-11-07"
224
+ }
225
+ },
226
+ "P011": {
227
+ "patient_id": "P011",
228
+ "name": "Joseph Rivera",
229
+ "age": 47,
230
+ "ckd_stage": 3,
231
+ "gfr": 55.5,
232
+ "creatinine": 2.4,
233
+ "blood_type": "O+",
234
+ "pra": 38.0,
235
+ "cholesterol": 195.0,
236
+ "bmi": 23.1,
237
+ "albumin": 3.19,
238
+ "hemoglobin": 11.9,
239
+ "t1_snapshot": {
240
+ "gfr": 60.0,
241
+ "creatinine": 1.5,
242
+ "blood_type": "O+",
243
+ "pra": 38.0,
244
+ "recorded_at": "2025-11-07",
245
+ "hba1c": 8.7
246
+ },
247
+ "hba1c": 9.2
248
+ },
249
+ "P012": {
250
+ "patient_id": "P012",
251
+ "name": "Betty Jones",
252
+ "age": 60,
253
+ "ckd_stage": 4,
254
+ "gfr": 24.4,
255
+ "creatinine": 3.4,
256
+ "blood_type": "O+",
257
+ "pra": 44.3,
258
+ "cholesterol": 232.0,
259
+ "bmi": 19.8,
260
+ "albumin": 2.15,
261
+ "hemoglobin": 7.7,
262
+ "t1_snapshot": {
263
+ "gfr": 34.5,
264
+ "creatinine": 1.9,
265
+ "blood_type": "O+",
266
+ "pra": 44.3,
267
+ "recorded_at": "2025-11-07"
268
+ }
269
+ },
270
+ "P013": {
271
+ "patient_id": "P013",
272
+ "name": "Robert Brown",
273
+ "age": 53,
274
+ "ckd_stage": 3,
275
+ "gfr": 49.4,
276
+ "creatinine": 1.5,
277
+ "blood_type": "B+",
278
+ "pra": 15.2,
279
+ "cholesterol": 200.7,
280
+ "bmi": 34.2,
281
+ "albumin": 3.48,
282
+ "hemoglobin": 13.4,
283
+ "t1_snapshot": {
284
+ "gfr": 59.0,
285
+ "creatinine": 1.0,
286
+ "blood_type": "B+",
287
+ "pra": 15.2,
288
+ "recorded_at": "2025-11-07",
289
+ "hba1c": 10.2
290
+ },
291
+ "hba1c": 11.6
292
+ },
293
+ "P014": {
294
+ "patient_id": "P014",
295
+ "name": "Ronald Thomas",
296
+ "age": 38,
297
+ "ckd_stage": 4,
298
+ "gfr": 26.2,
299
+ "creatinine": 2.8,
300
+ "blood_type": "O-",
301
+ "pra": 6.0,
302
+ "cholesterol": 224.8,
303
+ "bmi": 30.1,
304
+ "albumin": 2.03,
305
+ "hemoglobin": 8.1,
306
+ "t1_snapshot": {
307
+ "gfr": 39.7,
308
+ "creatinine": 2.3,
309
+ "blood_type": "O-",
310
+ "pra": 6.0,
311
+ "recorded_at": "2025-11-07",
312
+ "hba1c": 7.8
313
+ },
314
+ "hba1c": 9.4
315
+ },
316
+ "P015": {
317
+ "patient_id": "P015",
318
+ "name": "Joseph Jackson",
319
+ "age": 41,
320
+ "ckd_stage": 4,
321
+ "gfr": 21.7,
322
+ "creatinine": null,
323
+ "blood_type": "A-",
324
+ "pra": 62.8,
325
+ "cholesterol": 235.0,
326
+ "bmi": 35.3,
327
+ "albumin": 3.46,
328
+ "hemoglobin": 10.4,
329
+ "t1_snapshot": {
330
+ "gfr": 27.7,
331
+ "creatinine": null,
332
+ "blood_type": "A-",
333
+ "pra": 62.8,
334
+ "recorded_at": "2025-11-07"
335
+ }
336
+ },
337
+ "P016": {
338
+ "patient_id": "P016",
339
+ "name": "Patricia Torres",
340
+ "age": 43,
341
+ "ckd_stage": 5,
342
+ "gfr": 10.2,
343
+ "creatinine": 7.4,
344
+ "blood_type": "O-",
345
+ "pra": 12.6,
346
+ "cholesterol": 164.2,
347
+ "bmi": 31.3,
348
+ "albumin": 3.49,
349
+ "hemoglobin": 8.9,
350
+ "t1_snapshot": {
351
+ "gfr": 16.1,
352
+ "creatinine": 4.2,
353
+ "blood_type": "O-",
354
+ "pra": 12.6,
355
+ "recorded_at": "2025-11-07"
356
+ }
357
+ },
358
+ "P017": {
359
+ "patient_id": "P017",
360
+ "name": "James Robinson",
361
+ "age": 67,
362
+ "ckd_stage": 4,
363
+ "gfr": 15.6,
364
+ "creatinine": 2.7,
365
+ "blood_type": "A+",
366
+ "pra": 25.1,
367
+ "cholesterol": 223.9,
368
+ "bmi": 28.3,
369
+ "albumin": 4.04,
370
+ "hemoglobin": 13.1,
371
+ "t1_snapshot": {
372
+ "gfr": 19.8,
373
+ "creatinine": 1.6,
374
+ "blood_type": "A+",
375
+ "pra": 25.1,
376
+ "recorded_at": "2025-11-07"
377
+ }
378
+ },
379
+ "P018": {
380
+ "patient_id": "P018",
381
+ "name": "Nancy Nelson",
382
+ "age": 34,
383
+ "ckd_stage": 5,
384
+ "gfr": 14.0,
385
+ "creatinine": 5.3,
386
+ "blood_type": "O+",
387
+ "pra": 12.6,
388
+ "cholesterol": 268.3,
389
+ "bmi": 32.6,
390
+ "albumin": 2.02,
391
+ "hemoglobin": 12.4,
392
+ "t1_snapshot": {
393
+ "gfr": 21.4,
394
+ "creatinine": 4.0,
395
+ "blood_type": "O+",
396
+ "pra": 12.6,
397
+ "recorded_at": "2025-11-07",
398
+ "hba1c": 7.2
399
+ },
400
+ "hba1c": 8.0
401
+ },
402
+ "P019": {
403
+ "patient_id": "P019",
404
+ "name": "James Perez",
405
+ "age": 36,
406
+ "ckd_stage": 5,
407
+ "gfr": 6.2,
408
+ "creatinine": 5.5,
409
+ "blood_type": "O+",
410
+ "pra": 50.7,
411
+ "cholesterol": 250.0,
412
+ "bmi": 20.5,
413
+ "albumin": 2.93,
414
+ "hemoglobin": 9.2,
415
+ "t1_snapshot": {
416
+ "gfr": 7.9,
417
+ "creatinine": 3.8,
418
+ "blood_type": "O+",
419
+ "pra": 50.7,
420
+ "recorded_at": "2025-11-07",
421
+ "hba1c": 9.0
422
+ },
423
+ "hba1c": 9.5
424
+ },
425
+ "P020": {
426
+ "patient_id": "P020",
427
+ "name": "David Thompson",
428
+ "age": 41,
429
+ "ckd_stage": 4,
430
+ "gfr": 18.7,
431
+ "creatinine": 4.4,
432
+ "blood_type": "O-",
433
+ "pra": 43.6,
434
+ "cholesterol": 156.6,
435
+ "bmi": 25.1,
436
+ "albumin": 3.98,
437
+ "hemoglobin": 12.3,
438
+ "t1_snapshot": {
439
+ "gfr": 28.5,
440
+ "creatinine": 3.2,
441
+ "blood_type": "O-",
442
+ "pra": 43.6,
443
+ "recorded_at": "2025-11-07"
444
+ }
445
+ },
446
+ "P021": {
447
+ "patient_id": "P021",
448
+ "name": "Brian Mitchell",
449
+ "age": 43,
450
+ "ckd_stage": 4,
451
+ "gfr": 24.3,
452
+ "creatinine": 3.4,
453
+ "blood_type": "O+",
454
+ "pra": 13.0,
455
+ "cholesterol": 163.3,
456
+ "bmi": 38.4,
457
+ "albumin": 3.72,
458
+ "hemoglobin": 10.0,
459
+ "t1_snapshot": {
460
+ "gfr": 36.7,
461
+ "creatinine": 2.1,
462
+ "blood_type": "O+",
463
+ "pra": 13.0,
464
+ "recorded_at": "2025-11-07",
465
+ "hba1c": 9.9
466
+ },
467
+ "hba1c": 11.3
468
+ },
469
+ "P022": {
470
+ "patient_id": "P022",
471
+ "name": "Rebecca Moore",
472
+ "age": 50,
473
+ "ckd_stage": 4,
474
+ "gfr": 26.0,
475
+ "creatinine": 2.8,
476
+ "blood_type": "A+",
477
+ "pra": 24.4,
478
+ "cholesterol": 143.1,
479
+ "bmi": 22.7,
480
+ "albumin": 2.72,
481
+ "hemoglobin": 12.7,
482
+ "t1_snapshot": {
483
+ "gfr": 39.2,
484
+ "creatinine": 1.7,
485
+ "blood_type": "A+",
486
+ "pra": 24.4,
487
+ "recorded_at": "2025-11-07"
488
+ }
489
+ },
490
+ "P023": {
491
+ "patient_id": "P023",
492
+ "name": "Joshua Gonzalez",
493
+ "age": 44,
494
+ "ckd_stage": 4,
495
+ "gfr": 20.6,
496
+ "creatinine": 5.0,
497
+ "blood_type": "A-",
498
+ "pra": 21.2,
499
+ "cholesterol": 180.8,
500
+ "bmi": 31.5,
501
+ "albumin": 3.13,
502
+ "hemoglobin": 9.8,
503
+ "t1_snapshot": {
504
+ "gfr": 24.7,
505
+ "creatinine": 3.4,
506
+ "blood_type": "A-",
507
+ "pra": 21.2,
508
+ "recorded_at": "2025-11-07",
509
+ "hba1c": 10.8
510
+ },
511
+ "hba1c": 11.7
512
+ },
513
+ "P024": {
514
+ "patient_id": "P024",
515
+ "name": "Paul Martin",
516
+ "age": 70,
517
+ "ckd_stage": 3,
518
+ "gfr": 42.6,
519
+ "creatinine": 1.9,
520
+ "blood_type": "O+",
521
+ "pra": 34.5,
522
+ "cholesterol": 221.0,
523
+ "bmi": 32.8,
524
+ "albumin": 2.27,
525
+ "hemoglobin": 12.9,
526
+ "t1_snapshot": {
527
+ "gfr": 60.0,
528
+ "creatinine": 1.6,
529
+ "blood_type": "O+",
530
+ "pra": 34.5,
531
+ "recorded_at": "2025-11-07"
532
+ }
533
+ },
534
+ "P025": {
535
+ "patient_id": "P025",
536
+ "name": "Donald Baker",
537
+ "age": 41,
538
+ "ckd_stage": 4,
539
+ "gfr": 20.6,
540
+ "creatinine": 3.2,
541
+ "blood_type": "A+",
542
+ "pra": 24.1,
543
+ "cholesterol": 195.9,
544
+ "bmi": 31.0,
545
+ "albumin": 3.44,
546
+ "hemoglobin": 10.3,
547
+ "t1_snapshot": {
548
+ "gfr": 28.8,
549
+ "creatinine": 1.8,
550
+ "blood_type": "A+",
551
+ "pra": 24.1,
552
+ "recorded_at": "2025-11-07",
553
+ "hba1c": 7.6
554
+ },
555
+ "hba1c": 8.8
556
+ },
557
+ "P026": {
558
+ "patient_id": "P026",
559
+ "name": "Steven Flores",
560
+ "age": 32,
561
+ "ckd_stage": 4,
562
+ "gfr": 26.1,
563
+ "creatinine": 4.9,
564
+ "blood_type": "A+",
565
+ "pra": 7.5,
566
+ "cholesterol": 180.4,
567
+ "bmi": 35.8,
568
+ "albumin": 2.32,
569
+ "hemoglobin": 7.8,
570
+ "t1_snapshot": {
571
+ "gfr": 41.2,
572
+ "creatinine": 3.0,
573
+ "blood_type": "A+",
574
+ "pra": 7.5,
575
+ "recorded_at": "2025-11-07"
576
+ }
577
+ },
578
+ "P027": {
579
+ "patient_id": "P027",
580
+ "name": "Dorothy Brown",
581
+ "age": 63,
582
+ "ckd_stage": 4,
583
+ "gfr": 27.4,
584
+ "creatinine": 3.9,
585
+ "blood_type": "A+",
586
+ "pra": 71.4,
587
+ "cholesterol": 241.2,
588
+ "bmi": 23.2,
589
+ "albumin": 3.77,
590
+ "hemoglobin": 11.7,
591
+ "t1_snapshot": {
592
+ "gfr": 42.1,
593
+ "creatinine": 3.2,
594
+ "blood_type": "A+",
595
+ "pra": 71.4,
596
+ "recorded_at": "2025-11-07"
597
+ }
598
+ },
599
+ "P028": {
600
+ "patient_id": "P028",
601
+ "name": "Stephanie Walker",
602
+ "age": 56,
603
+ "ckd_stage": 3,
604
+ "gfr": 54.6,
605
+ "creatinine": 1.8,
606
+ "blood_type": "A-",
607
+ "pra": 71.3,
608
+ "cholesterol": 205.6,
609
+ "bmi": 36.4,
610
+ "albumin": 3.21,
611
+ "hemoglobin": 12.9,
612
+ "t1_snapshot": {
613
+ "gfr": 60.0,
614
+ "creatinine": 1.4,
615
+ "blood_type": "A-",
616
+ "pra": 71.3,
617
+ "recorded_at": "2025-11-07"
618
+ }
619
+ },
620
+ "P029": {
621
+ "patient_id": "P029",
622
+ "name": "Deborah Wilson",
623
+ "age": 32,
624
+ "ckd_stage": 4,
625
+ "gfr": 18.5,
626
+ "creatinine": 4.1,
627
+ "blood_type": "O+",
628
+ "pra": 22.9,
629
+ "cholesterol": 210.2,
630
+ "bmi": 21.5,
631
+ "albumin": 2.51,
632
+ "hemoglobin": 11.7,
633
+ "t1_snapshot": {
634
+ "gfr": 23.5,
635
+ "creatinine": 2.6,
636
+ "blood_type": "O+",
637
+ "pra": 22.9,
638
+ "recorded_at": "2025-11-07"
639
+ }
640
+ },
641
+ "P030": {
642
+ "patient_id": "P030",
643
+ "name": "Andrew Mitchell",
644
+ "age": 62,
645
+ "ckd_stage": 4,
646
+ "gfr": 19.6,
647
+ "creatinine": 3.7,
648
+ "blood_type": "O+",
649
+ "pra": 70.5,
650
+ "cholesterol": 178.8,
651
+ "bmi": 26.9,
652
+ "albumin": 3.96,
653
+ "hemoglobin": 12.5,
654
+ "t1_snapshot": {
655
+ "gfr": 27.6,
656
+ "creatinine": 2.6,
657
+ "blood_type": "O+",
658
+ "pra": 70.5,
659
+ "recorded_at": "2025-11-07",
660
+ "hba1c": 10.6
661
+ },
662
+ "hba1c": 11.1
663
+ },
664
+ "P031": {
665
+ "patient_id": "P031",
666
+ "name": "Ashley Rodriguez",
667
+ "age": 57,
668
+ "ckd_stage": 5,
669
+ "gfr": 13.1,
670
+ "creatinine": 7.2,
671
+ "blood_type": "B+",
672
+ "pra": 67.2,
673
+ "cholesterol": 257.9,
674
+ "bmi": 31.2,
675
+ "albumin": 3.46,
676
+ "hemoglobin": 8.0,
677
+ "t1_snapshot": {
678
+ "gfr": 20.5,
679
+ "creatinine": 6.1,
680
+ "blood_type": "B+",
681
+ "pra": 67.2,
682
+ "recorded_at": "2025-11-07",
683
+ "hba1c": 8.2
684
+ },
685
+ "hba1c": 9.2
686
+ },
687
+ "P032": {
688
+ "patient_id": "P032",
689
+ "name": "David Carter",
690
+ "age": 29,
691
+ "ckd_stage": 3,
692
+ "gfr": 37.5,
693
+ "creatinine": 1.6,
694
+ "blood_type": "A+",
695
+ "pra": 66.8,
696
+ "cholesterol": 263.7,
697
+ "bmi": 23.3,
698
+ "albumin": 2.15,
699
+ "hemoglobin": 13.2,
700
+ "t1_snapshot": {
701
+ "gfr": 51.1,
702
+ "creatinine": 1.2,
703
+ "blood_type": "A+",
704
+ "pra": 66.8,
705
+ "recorded_at": "2025-11-07",
706
+ "hba1c": 6.9
707
+ },
708
+ "hba1c": 8.4
709
+ },
710
+ "P033": {
711
+ "patient_id": "P033",
712
+ "name": "Maria Martin",
713
+ "age": 64,
714
+ "ckd_stage": 5,
715
+ "gfr": 6.8,
716
+ "creatinine": 5.1,
717
+ "blood_type": "A+",
718
+ "pra": 61.4,
719
+ "cholesterol": 154.9,
720
+ "bmi": 36.1,
721
+ "albumin": 4.12,
722
+ "hemoglobin": 8.1,
723
+ "t1_snapshot": {
724
+ "gfr": 8.3,
725
+ "creatinine": 3.7,
726
+ "blood_type": "A+",
727
+ "pra": 61.4,
728
+ "recorded_at": "2025-11-07",
729
+ "hba1c": 6.7
730
+ },
731
+ "hba1c": 7.2
732
+ },
733
+ "P034": {
734
+ "patient_id": "P034",
735
+ "name": "Ashley Nelson",
736
+ "age": 51,
737
+ "ckd_stage": 4,
738
+ "gfr": 28.4,
739
+ "creatinine": 3.5,
740
+ "blood_type": "O+",
741
+ "pra": 61.8,
742
+ "cholesterol": 155.7,
743
+ "bmi": 39.7,
744
+ "albumin": 3.72,
745
+ "hemoglobin": 9.6,
746
+ "t1_snapshot": {
747
+ "gfr": 43.5,
748
+ "creatinine": 2.6,
749
+ "blood_type": "O+",
750
+ "pra": 61.8,
751
+ "recorded_at": "2025-11-07"
752
+ }
753
+ },
754
+ "P035": {
755
+ "patient_id": "P035",
756
+ "name": "Timothy Nguyen",
757
+ "age": 45,
758
+ "ckd_stage": 3,
759
+ "gfr": 48.8,
760
+ "creatinine": 1.2,
761
+ "blood_type": "A+",
762
+ "pra": 12.2,
763
+ "cholesterol": 220.1,
764
+ "bmi": 38.3,
765
+ "albumin": 3.7,
766
+ "hemoglobin": 10.3,
767
+ "t1_snapshot": {
768
+ "gfr": 60.0,
769
+ "creatinine": 0.8,
770
+ "blood_type": "A+",
771
+ "pra": 12.2,
772
+ "recorded_at": "2025-11-07",
773
+ "hba1c": 8.4
774
+ },
775
+ "hba1c": 9.2
776
+ },
777
+ "P036": {
778
+ "patient_id": "P036",
779
+ "name": "Donna Smith",
780
+ "age": 61,
781
+ "ckd_stage": 4,
782
+ "gfr": 18.4,
783
+ "creatinine": 4.8,
784
+ "blood_type": "O+",
785
+ "pra": 39.5,
786
+ "cholesterol": 173.6,
787
+ "bmi": 24.5,
788
+ "albumin": 3.31,
789
+ "hemoglobin": 12.8,
790
+ "t1_snapshot": {
791
+ "gfr": 23.9,
792
+ "creatinine": 3.3,
793
+ "blood_type": "O+",
794
+ "pra": 39.5,
795
+ "recorded_at": "2025-11-07",
796
+ "hba1c": 7.8
797
+ },
798
+ "hba1c": 9.0
799
+ },
800
+ "P037": {
801
+ "patient_id": "P037",
802
+ "name": "Stephanie Scott",
803
+ "age": 49,
804
+ "ckd_stage": 5,
805
+ "gfr": 5.8,
806
+ "creatinine": 8.2,
807
+ "blood_type": "B+",
808
+ "pra": 7.4,
809
+ "cholesterol": 215.6,
810
+ "bmi": 28.7,
811
+ "albumin": 3.17,
812
+ "hemoglobin": 10.1,
813
+ "t1_snapshot": {
814
+ "gfr": 7.2,
815
+ "creatinine": 6.2,
816
+ "blood_type": "B+",
817
+ "pra": 7.4,
818
+ "recorded_at": "2025-11-07",
819
+ "hba1c": 9.2
820
+ },
821
+ "hba1c": 10.7
822
+ },
823
+ "P038": {
824
+ "patient_id": "P038",
825
+ "name": "Christopher Anderson",
826
+ "age": 51,
827
+ "ckd_stage": 4,
828
+ "gfr": 21.4,
829
+ "creatinine": 3.3,
830
+ "blood_type": "A+",
831
+ "pra": 15.3,
832
+ "cholesterol": 238.8,
833
+ "bmi": 31.3,
834
+ "albumin": 4.16,
835
+ "hemoglobin": 12.5,
836
+ "t1_snapshot": {
837
+ "gfr": 31.7,
838
+ "creatinine": 2.1,
839
+ "blood_type": "A+",
840
+ "pra": 15.3,
841
+ "recorded_at": "2025-11-07",
842
+ "hba1c": 9.5
843
+ },
844
+ "hba1c": 10.5
845
+ },
846
+ "P039": {
847
+ "patient_id": "P039",
848
+ "name": "Steven Davis",
849
+ "age": 32,
850
+ "ckd_stage": 3,
851
+ "gfr": 30.4,
852
+ "creatinine": 1.9,
853
+ "blood_type": "O-",
854
+ "pra": 39.3,
855
+ "cholesterol": 197.3,
856
+ "bmi": 22.5,
857
+ "albumin": 2.11,
858
+ "hemoglobin": 13.1,
859
+ "t1_snapshot": {
860
+ "gfr": 46.9,
861
+ "creatinine": 1.4,
862
+ "blood_type": "O-",
863
+ "pra": 39.3,
864
+ "recorded_at": "2025-11-07",
865
+ "hba1c": 10.5
866
+ },
867
+ "hba1c": 11.9
868
+ },
869
+ "P040": {
870
+ "patient_id": "P040",
871
+ "name": "Ashley Martin",
872
+ "age": 64,
873
+ "ckd_stage": 4,
874
+ "gfr": 16.0,
875
+ "creatinine": 4.1,
876
+ "blood_type": "O+",
877
+ "pra": 61.2,
878
+ "cholesterol": 207.9,
879
+ "bmi": 28.2,
880
+ "albumin": 2.97,
881
+ "hemoglobin": 12.7,
882
+ "t1_snapshot": {
883
+ "gfr": 22.8,
884
+ "creatinine": 3.2,
885
+ "blood_type": "O+",
886
+ "pra": 61.2,
887
+ "recorded_at": "2025-11-07",
888
+ "hba1c": 6.2
889
+ },
890
+ "hba1c": 7.3
891
+ },
892
+ "P041": {
893
+ "patient_id": "P041",
894
+ "name": "Deborah Jackson",
895
+ "age": 72,
896
+ "ckd_stage": 4,
897
+ "gfr": 23.5,
898
+ "creatinine": 4.4,
899
+ "blood_type": "O+",
900
+ "pra": 13.9,
901
+ "cholesterol": 198.6,
902
+ "bmi": 31.3,
903
+ "albumin": 2.64,
904
+ "hemoglobin": 8.9,
905
+ "t1_snapshot": {
906
+ "gfr": 27.8,
907
+ "creatinine": 2.4,
908
+ "blood_type": "O+",
909
+ "pra": 13.9,
910
+ "recorded_at": "2025-11-07"
911
+ }
912
+ },
913
+ "P042": {
914
+ "patient_id": "P042",
915
+ "name": "Christopher Lewis",
916
+ "age": 35,
917
+ "ckd_stage": 5,
918
+ "gfr": 5.6,
919
+ "creatinine": 6.1,
920
+ "blood_type": "O+",
921
+ "pra": 18.0,
922
+ "cholesterol": 147.8,
923
+ "bmi": 35.5,
924
+ "albumin": 3.31,
925
+ "hemoglobin": 12.4,
926
+ "t1_snapshot": {
927
+ "gfr": 6.8,
928
+ "creatinine": 3.8,
929
+ "blood_type": "O+",
930
+ "pra": 18.0,
931
+ "recorded_at": "2025-11-07",
932
+ "hba1c": 9.2
933
+ },
934
+ "hba1c": 10.9
935
+ },
936
+ "P043": {
937
+ "patient_id": "P043",
938
+ "name": "Lisa Torres",
939
+ "age": 30,
940
+ "ckd_stage": 4,
941
+ "gfr": 19.3,
942
+ "creatinine": 3.5,
943
+ "blood_type": "B+",
944
+ "pra": 48.3,
945
+ "cholesterol": 251.9,
946
+ "bmi": 31.1,
947
+ "albumin": 2.05,
948
+ "hemoglobin": 12.1,
949
+ "t1_snapshot": {
950
+ "gfr": 22.4,
951
+ "creatinine": 2.2,
952
+ "blood_type": "B+",
953
+ "pra": 48.3,
954
+ "recorded_at": "2025-11-07",
955
+ "hba1c": 8.7
956
+ },
957
+ "hba1c": 9.5
958
+ },
959
+ "P044": {
960
+ "patient_id": "P044",
961
+ "name": "Sandra Carter",
962
+ "age": 63,
963
+ "ckd_stage": 5,
964
+ "gfr": 6.6,
965
+ "creatinine": 7.3,
966
+ "blood_type": "A-",
967
+ "pra": 37.6,
968
+ "cholesterol": 160.9,
969
+ "bmi": 27.4,
970
+ "albumin": 3.09,
971
+ "hemoglobin": 11.5,
972
+ "t1_snapshot": {
973
+ "gfr": 8.8,
974
+ "creatinine": 4.7,
975
+ "blood_type": "A-",
976
+ "pra": 37.6,
977
+ "recorded_at": "2025-11-07",
978
+ "hba1c": 6.7
979
+ },
980
+ "hba1c": 8.0
981
+ },
982
+ "P045": {
983
+ "patient_id": "P045",
984
+ "name": "Lisa Scott",
985
+ "age": 36,
986
+ "ckd_stage": 3,
987
+ "gfr": 32.6,
988
+ "creatinine": 1.5,
989
+ "blood_type": null,
990
+ "pra": 69.5,
991
+ "cholesterol": 220.9,
992
+ "bmi": 29.2,
993
+ "albumin": 2.97,
994
+ "hemoglobin": 7.8,
995
+ "t1_snapshot": {
996
+ "gfr": 44.3,
997
+ "creatinine": 0.8,
998
+ "blood_type": null,
999
+ "pra": 69.5,
1000
+ "recorded_at": "2025-11-07",
1001
+ "hba1c": 9.6
1002
+ },
1003
+ "hba1c": 10.7
1004
+ },
1005
+ "P046": {
1006
+ "patient_id": "P046",
1007
+ "name": "Deborah Jackson",
1008
+ "age": 60,
1009
+ "ckd_stage": 5,
1010
+ "gfr": 8.9,
1011
+ "creatinine": 8.1,
1012
+ "blood_type": "A+",
1013
+ "pra": 44.2,
1014
+ "cholesterol": 200.0,
1015
+ "bmi": 21.0,
1016
+ "albumin": 3.83,
1017
+ "hemoglobin": 10.5,
1018
+ "t1_snapshot": {
1019
+ "gfr": 10.6,
1020
+ "creatinine": 6.9,
1021
+ "blood_type": "A+",
1022
+ "pra": 44.2,
1023
+ "recorded_at": "2025-11-07",
1024
+ "hba1c": 11.3
1025
+ },
1026
+ "hba1c": 11.9
1027
+ },
1028
+ "P047": {
1029
+ "patient_id": "P047",
1030
+ "name": "Timothy Martin",
1031
+ "age": 65,
1032
+ "ckd_stage": 4,
1033
+ "gfr": 20.8,
1034
+ "creatinine": 3.7,
1035
+ "blood_type": "B+",
1036
+ "pra": 10.9,
1037
+ "cholesterol": 184.2,
1038
+ "bmi": 35.4,
1039
+ "albumin": 2.59,
1040
+ "hemoglobin": 7.5,
1041
+ "t1_snapshot": {
1042
+ "gfr": 24.6,
1043
+ "creatinine": 2.9,
1044
+ "blood_type": "B+",
1045
+ "pra": 10.9,
1046
+ "recorded_at": "2025-11-07",
1047
+ "hba1c": 8.5
1048
+ },
1049
+ "hba1c": 9.0
1050
+ },
1051
+ "P048": {
1052
+ "patient_id": "P048",
1053
+ "name": "Anthony Harris",
1054
+ "age": 70,
1055
+ "ckd_stage": 4,
1056
+ "gfr": 24.2,
1057
+ "creatinine": 4.7,
1058
+ "blood_type": "A-",
1059
+ "pra": 69.6,
1060
+ "cholesterol": 251.3,
1061
+ "bmi": 27.3,
1062
+ "albumin": 2.7,
1063
+ "hemoglobin": 10.3,
1064
+ "t1_snapshot": {
1065
+ "gfr": 38.5,
1066
+ "creatinine": 2.9,
1067
+ "blood_type": "A-",
1068
+ "pra": 69.6,
1069
+ "recorded_at": "2025-11-07",
1070
+ "hba1c": 8.0
1071
+ },
1072
+ "hba1c": 9.2
1073
+ },
1074
+ "P049": {
1075
+ "patient_id": "P049",
1076
+ "name": "Michael Walker",
1077
+ "age": 44,
1078
+ "ckd_stage": 5,
1079
+ "gfr": 10.9,
1080
+ "creatinine": 5.7,
1081
+ "blood_type": "O+",
1082
+ "pra": 67.6,
1083
+ "cholesterol": 142.0,
1084
+ "bmi": 21.6,
1085
+ "albumin": 3.92,
1086
+ "hemoglobin": 13.3,
1087
+ "t1_snapshot": {
1088
+ "gfr": 13.0,
1089
+ "creatinine": 4.7,
1090
+ "blood_type": "O+",
1091
+ "pra": 67.6,
1092
+ "recorded_at": "2025-11-07"
1093
+ }
1094
+ },
1095
+ "P050": {
1096
+ "patient_id": "P050",
1097
+ "name": "Thomas Mitchell",
1098
+ "age": 33,
1099
+ "ckd_stage": 4,
1100
+ "gfr": 24.7,
1101
+ "creatinine": 4.1,
1102
+ "blood_type": "O+",
1103
+ "pra": 57.5,
1104
+ "cholesterol": 144.7,
1105
+ "bmi": 20.0,
1106
+ "albumin": 3.39,
1107
+ "hemoglobin": 13.0,
1108
+ "t1_snapshot": {
1109
+ "gfr": 36.8,
1110
+ "creatinine": 3.3,
1111
+ "blood_type": "O+",
1112
+ "pra": 57.5,
1113
+ "recorded_at": "2025-11-07"
1114
  }
1115
  }
1116
  }
1117
+ }
prana_grpo_qwen3_8b_fp8.ipynb ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# PRANA-Env: Reinforcement Learning with Qwen3-8B FP8\n",
8
+ "\n",
9
+ "Fine-tune **Qwen3-8B** using **GRPO + FP8** on the PRANA kidney transplant administration environment.\n",
10
+ "\n",
11
+ "The agent must:\n",
12
+ "1. Query fragmented clinical datastores\n",
13
+ "2. Detect stale lab values (90-day KARS recency window)\n",
14
+ "3. Detect anomalous measurements (>25% change within 14 days)\n",
15
+ "4. File a complete KARS-compliant SRTR report\n",
16
+ "\n",
17
+ "Reward signal comes from the deterministic KARS validator in prana_env.\n",
18
+ "\n",
19
+ "**Hardware**: H100 required for FP8.\n",
20
+ "\n",
21
+ "**Baseline**: Qwen3-8B untuned scores **0.71 Pass@1** on temporal/anomaly tasks. \n",
22
+ "**Target**: β‰₯ 0.90 Pass@1 after GRPO fine-tuning."
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "markdown",
27
+ "metadata": {},
28
+ "source": [
29
+ "## 1. Installation"
30
+ ]
31
+ },
32
+ {
33
+ "cell_type": "code",
34
+ "execution_count": null,
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": "import os\nos.environ['UNSLOTH_VLLM_STANDBY'] = '1'\n\nfrom unsloth import FastLanguageModel\nimport torch\n\nmax_seq_length = 2048 # Multi-turn clinical dialogue\nlora_rank = 32 # From official Qwen3-8B FP8 notebook\n\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n model_name = 'unsloth/Qwen3-8B-FP8',\n max_seq_length = max_seq_length,\n load_in_4bit = False, # FP8, not 4bit\n fast_inference = True, # vLLM fast inference for GRPO rollouts\n max_lora_rank = lora_rank,\n load_in_fp8 = True, # FP8 training on H100\n)\n\nmodel = FastLanguageModel.get_peft_model(\n model,\n r = lora_rank,\n target_modules = [\n 'q_proj', 'k_proj', 'v_proj', 'o_proj',\n 'gate_proj', 'up_proj', 'down_proj',\n ],\n lora_alpha = lora_rank * 2,\n use_gradient_checkpointing = 'unsloth',\n random_state = 3407,\n)"
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "metadata": {},
43
+ "outputs": [],
44
+ "source": [
45
+ "%%capture\n",
46
+ "# Clone prana_env and install dependencies\n",
47
+ "!git clone https://github.com/pbanavara/prana_env.git\n",
48
+ "!pip install -q fastapi uvicorn websockets pydantic openenv requests\n",
49
+ "%cd prana_env\n",
50
+ "!pip install -q -e .\n",
51
+ "\n",
52
+ "import sys, os\n",
53
+ "sys.path.insert(0, '.')\n",
54
+ "working_directory = os.path.abspath('.')"
55
+ ]
56
+ },
57
+ {
58
+ "cell_type": "markdown",
59
+ "metadata": {},
60
+ "source": [
61
+ "## 2. Load Qwen3-8B with FP8 + LoRA"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": null,
67
+ "metadata": {},
68
+ "outputs": [],
69
+ "source": [
70
+ "import os\n",
71
+ "os.environ['UNSLOTH_VLLM_STANDBY'] = '1'\n",
72
+ "\n",
73
+ "from unsloth import FastLanguageModel\n",
74
+ "import torch\n",
75
+ "\n",
76
+ "max_seq_length = 2048 # Multi-turn clinical dialogue\n",
77
+ "lora_rank = 32 # From official Qwen3-8B FP8 notebook\n",
78
+ "\n",
79
+ "model, tokenizer = FastLanguageModel.from_pretrained(\n",
80
+ " model_name = 'unsloth/Qwen3-8B',\n",
81
+ " max_seq_length = max_seq_length,\n",
82
+ " load_in_4bit = False, # FP8, not 4bit\n",
83
+ " fast_inference = True, # vLLM fast inference for GRPO rollouts\n",
84
+ " max_lora_rank = lora_rank,\n",
85
+ " load_in_fp8 = True, # FP8 training on H100\n",
86
+ ")\n",
87
+ "\n",
88
+ "model = FastLanguageModel.get_peft_model(\n",
89
+ " model,\n",
90
+ " r = lora_rank,\n",
91
+ " target_modules = [\n",
92
+ " 'q_proj', 'k_proj', 'v_proj', 'o_proj',\n",
93
+ " 'gate_proj', 'up_proj', 'down_proj',\n",
94
+ " ],\n",
95
+ " lora_alpha = lora_rank * 2,\n",
96
+ " use_gradient_checkpointing = 'unsloth',\n",
97
+ " random_state = 3407,\n",
98
+ ")"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "markdown",
103
+ "metadata": {},
104
+ "source": [
105
+ "## 3. Launch prana_env server\n",
106
+ "\n",
107
+ "Start the FastAPI + WebSocket server as a local subprocess."
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "code",
112
+ "execution_count": null,
113
+ "metadata": {},
114
+ "outputs": [],
115
+ "source": "import subprocess, time, requests\n\nPRANA_PORT = 7860\nPRANA_BASE_URL = f'http://localhost:{PRANA_PORT}'\n_server_proc = None\n\ndef launch_prana_server():\n global _server_proc\n if _server_proc is not None:\n try:\n requests.get(f'{PRANA_BASE_URL}/health', timeout=2)\n return\n except Exception:\n _server_proc.kill()\n _server_proc = None\n\n _server_proc = subprocess.Popen(\n ['uvicorn', 'server.app:app', '--host', '0.0.0.0', '--port', str(PRANA_PORT)],\n cwd=working_directory,\n stdout=subprocess.DEVNULL,\n stderr=subprocess.DEVNULL,\n )\n for _ in range(20):\n try:\n requests.get(f'{PRANA_BASE_URL}/health', timeout=2)\n print(f'prana_env server ready at {PRANA_BASE_URL}')\n return\n except Exception:\n time.sleep(1)\n raise RuntimeError('prana_env server failed to start')\n\nlaunch_prana_server()"
116
+ },
117
+ {
118
+ "cell_type": "markdown",
119
+ "metadata": {},
120
+ "source": [
121
+ "## 4. PRANA-Env client helpers"
122
+ ]
123
+ },
124
+ {
125
+ "cell_type": "code",
126
+ "execution_count": null,
127
+ "metadata": {},
128
+ "outputs": [],
129
+ "source": "import random\nfrom prana_env.client import PranaEnv\nfrom prana_env.models import PranaAction\n\nPATIENTS = [f'P{i:03d}' for i in range(1, 51)] # P001-P050\n\ndef run_episode(action_sequence: list[dict], patient_id: str) -> tuple[float, str]:\n \"\"\"\n Execute a list of parsed actions against prana_env.\n Returns (cumulative_reward, 'PASSED'|'FAILED'|'INCOMPLETE').\n \"\"\"\n launch_prana_server()\n cumulative_reward = 0.0\n kars_result = 'INCOMPLETE'\n\n with PranaEnv(base_url=PRANA_BASE_URL) as env:\n env.reset(patient_id=patient_id)\n for action_dict in action_sequence:\n try:\n action = PranaAction(**action_dict)\n result = env.step(action)\n cumulative_reward += result.reward\n if result.done:\n kars_result = result.observation.kars_result or 'FAILED'\n break\n except Exception:\n cumulative_reward -= 1.0\n continue\n\n return cumulative_reward, kars_result"
130
+ },
131
+ {
132
+ "cell_type": "markdown",
133
+ "metadata": {},
134
+ "source": [
135
+ "## 5. Action parser"
136
+ ]
137
+ },
138
+ {
139
+ "cell_type": "code",
140
+ "execution_count": null,
141
+ "metadata": {},
142
+ "outputs": [],
143
+ "source": [
144
+ "import json, re\n",
145
+ "\n",
146
+ "def extract_actions(response: str) -> list[dict]:\n",
147
+ " \"\"\"\n",
148
+ " Extract a JSON array of actions from the model response.\n",
149
+ " Model is instructed to output actions inside ```json ... ``` blocks.\n",
150
+ " \"\"\"\n",
151
+ " match = re.search(r'```json\\s*(\\[.*?\\])\\s*```', response, re.DOTALL)\n",
152
+ " if not match:\n",
153
+ " match = re.search(r'(\\[\\s*\\{.*?\\}\\s*\\])', response, re.DOTALL)\n",
154
+ " if not match:\n",
155
+ " return []\n",
156
+ " try:\n",
157
+ " return json.loads(match.group(1))\n",
158
+ " except json.JSONDecodeError:\n",
159
+ " return []"
160
+ ]
161
+ },
162
+ {
163
+ "cell_type": "markdown",
164
+ "metadata": {},
165
+ "source": [
166
+ "## 6. GRPO prompt"
167
+ ]
168
+ },
169
+ {
170
+ "cell_type": "code",
171
+ "execution_count": null,
172
+ "metadata": {},
173
+ "outputs": [],
174
+ "source": [
175
+ "SYSTEM_PROMPT = \"\"\"\n",
176
+ "You are a clinical administrative agent for a kidney transplant center.\n",
177
+ "Your task is to file a KARS-compliant SRTR report for a patient.\n",
178
+ "\n",
179
+ "Today's date is 2026-03-07 (filing date T5).\n",
180
+ "The patient has a record from approximately 4 months ago (T1). Some fields may be stale.\n",
181
+ "\n",
182
+ "KARS Required Fields:\n",
183
+ "- hba1c, gfr, creatinine (PatientDB) β€” time-sensitive, must be within 90 days of filing\n",
184
+ "- blood_type (PatientDB) β€” stable, no recency requirement\n",
185
+ "\n",
186
+ "OPTN Clinical Integrity Policy:\n",
187
+ "- If two measurements of the same field within 14 days differ by >25%, do NOT file.\n",
188
+ " Communicate the anomaly and recommend a confirmatory test.\n",
189
+ "\n",
190
+ "Actions available:\n",
191
+ "- query_db: {action_type: query_db, target: PatientDB, field: <field>, patient_id: <id>}\n",
192
+ "- record_value: {action_type: record_value, field: <field>, value: <value>}\n",
193
+ "- file_report: {action_type: file_report}\n",
194
+ "\n",
195
+ "Output your complete action plan as a JSON array inside ```json ... ``` tags.\n",
196
+ "Reason step by step before outputting actions.\n",
197
+ "\"\"\".strip()\n",
198
+ "\n",
199
+ "USER_PROMPT_TEMPLATE = \"\"\"\n",
200
+ "File a KARS-compliant SRTR report for patient {patient_id}.\n",
201
+ "The T1 snapshot from ~4 months ago is pre-loaded in the record.\n",
202
+ "Check which fields are stale or anomalous, re-query only what is needed, and file.\n",
203
+ "\"\"\".strip()\n",
204
+ "\n",
205
+ "def make_prompt(patient_id: str) -> list[dict]:\n",
206
+ " return [\n",
207
+ " {'role': 'system', 'content': SYSTEM_PROMPT},\n",
208
+ " {'role': 'user', 'content': USER_PROMPT_TEMPLATE.format(patient_id=patient_id)},\n",
209
+ " ]"
210
+ ]
211
+ },
212
+ {
213
+ "cell_type": "markdown",
214
+ "metadata": {},
215
+ "source": [
216
+ "## 7. Reward functions"
217
+ ]
218
+ },
219
+ {
220
+ "cell_type": "code",
221
+ "execution_count": null,
222
+ "metadata": {},
223
+ "outputs": [],
224
+ "source": "def actions_parseable(completions, **kwargs):\n \"\"\"Reward 1.0 if model outputs a parseable action list, -1.0 otherwise.\"\"\"\n scores = []\n for completion in completions:\n response = completion[0]['content']\n actions = extract_actions(response)\n scores.append(1.0 if len(actions) > 0 else -1.0)\n return scores\n\n\ndef kars_reward(completions, prompts, **kwargs):\n \"\"\"\n Execute the action sequence in prana_env and return the KARS reward.\n prana_env reward scale:\n +15 KARS PASSED first attempt\n +10 KARS PASSED after correction\n -1 re-query of already-fresh field\n -5 KARS FAILED\n -10 unrecoverable (3 attempts)\n Normalized to [-1, 1] for GRPO stability.\n \"\"\"\n scores = []\n for completion, prompt in zip(completions, prompts):\n response = completion[0]['content']\n actions = extract_actions(response)\n\n if not actions:\n scores.append(-1.0)\n continue\n\n # Extract patient_id from user message (P001-P050)\n patient_id = 'P001'\n for msg in prompt:\n if msg['role'] == 'user':\n m = re.search(r'P\\d{3}', msg['content'])\n if m:\n patient_id = m.group(0)\n\n # Inject patient_id into query_db actions if missing\n for a in actions:\n if a.get('action_type') == 'query_db' and 'patient_id' not in a:\n a['patient_id'] = patient_id\n\n try:\n raw_reward, kars_result = run_episode(actions, patient_id)\n normalized = max(-1.0, min(1.0, raw_reward / 15.0))\n scores.append(normalized)\n print(f'[KARS] patient={patient_id} result={kars_result} raw={raw_reward:.1f} norm={normalized:.2f}')\n except Exception as e:\n print(f'[KARS] error: {e}')\n scores.append(-1.0)\n\n return scores"
225
+ },
226
+ {
227
+ "cell_type": "markdown",
228
+ "metadata": {},
229
+ "source": [
230
+ "## 8. Dataset"
231
+ ]
232
+ },
233
+ {
234
+ "cell_type": "code",
235
+ "execution_count": null,
236
+ "metadata": {},
237
+ "outputs": [],
238
+ "source": "from datasets import Dataset\n\n# 1000 episodes cycling through all 50 patients\nrecords = []\nfor i in range(1000):\n pid = PATIENTS[i % len(PATIENTS)]\n records.append({\n 'prompt': make_prompt(pid),\n 'answer': 0,\n })\n\ndataset = Dataset.from_list(records)\n\nmaximum_length = len(tokenizer.apply_chat_template(\n make_prompt('P001'),\n add_generation_prompt=True,\n))\nprint(f'Prompt token length: {maximum_length}')\ndataset[0]"
239
+ },
240
+ {
241
+ "cell_type": "markdown",
242
+ "metadata": {},
243
+ "source": [
244
+ "## 9. GRPO Training"
245
+ ]
246
+ },
247
+ {
248
+ "cell_type": "code",
249
+ "execution_count": null,
250
+ "metadata": {},
251
+ "outputs": [],
252
+ "source": [
253
+ "max_prompt_length = maximum_length + 1\n",
254
+ "max_completion_length = max_seq_length - max_prompt_length\n",
255
+ "\n",
256
+ "from vllm import SamplingParams\n",
257
+ "vllm_sampling_params = SamplingParams(\n",
258
+ " min_p = 0.1,\n",
259
+ " top_p = 1.0,\n",
260
+ " top_k = -1,\n",
261
+ " seed = 3407,\n",
262
+ " stop = [tokenizer.eos_token],\n",
263
+ " include_stop_str_in_output = True,\n",
264
+ ")\n",
265
+ "\n",
266
+ "from trl import GRPOConfig, GRPOTrainer\n",
267
+ "\n",
268
+ "training_args = GRPOConfig(\n",
269
+ " vllm_sampling_params = vllm_sampling_params,\n",
270
+ " temperature = 1.0,\n",
271
+ " learning_rate = 5e-6,\n",
272
+ " weight_decay = 0.01,\n",
273
+ " warmup_ratio = 0.1,\n",
274
+ " lr_scheduler_type = 'linear',\n",
275
+ " optim = 'adamw_8bit',\n",
276
+ " logging_steps = 1,\n",
277
+ " per_device_train_batch_size = 4,\n",
278
+ " gradient_accumulation_steps = 1,\n",
279
+ " num_generations = 4, # Increase to 8 if VRAM allows\n",
280
+ " max_prompt_length = max_prompt_length,\n",
281
+ " max_completion_length = max_completion_length,\n",
282
+ " max_steps = 600,\n",
283
+ " save_steps = 100,\n",
284
+ " report_to = 'none',\n",
285
+ " output_dir = 'outputs',\n",
286
+ ")\n",
287
+ "\n",
288
+ "trainer = GRPOTrainer(\n",
289
+ " model = model,\n",
290
+ " processing_class = tokenizer,\n",
291
+ " reward_funcs = [\n",
292
+ " actions_parseable,\n",
293
+ " kars_reward,\n",
294
+ " ],\n",
295
+ " args = training_args,\n",
296
+ " train_dataset = dataset,\n",
297
+ ")"
298
+ ]
299
+ },
300
+ {
301
+ "cell_type": "code",
302
+ "execution_count": null,
303
+ "metadata": {},
304
+ "outputs": [],
305
+ "source": [
306
+ "trainer.train()"
307
+ ]
308
+ },
309
+ {
310
+ "cell_type": "markdown",
311
+ "metadata": {},
312
+ "source": [
313
+ "## 10. Inference β€” test the fine-tuned model"
314
+ ]
315
+ },
316
+ {
317
+ "cell_type": "code",
318
+ "execution_count": null,
319
+ "metadata": {},
320
+ "outputs": [],
321
+ "source": [
322
+ "from transformers import TextStreamer\n",
323
+ "\n",
324
+ "test_patient = 'P002' # anomalous GFR/creatinine β€” hardest case\n",
325
+ "text = tokenizer.apply_chat_template(\n",
326
+ " make_prompt(test_patient),\n",
327
+ " tokenize=False,\n",
328
+ " add_generation_prompt=True,\n",
329
+ ")\n",
330
+ "\n",
331
+ "_ = model.generate(\n",
332
+ " **tokenizer(text, return_tensors='pt').to('cuda'),\n",
333
+ " temperature=1.0,\n",
334
+ " max_new_tokens=1024,\n",
335
+ " streamer=TextStreamer(tokenizer, skip_prompt=False),\n",
336
+ ")"
337
+ ]
338
+ },
339
+ {
340
+ "cell_type": "markdown",
341
+ "metadata": {},
342
+ "source": [
343
+ "## 11. Save model"
344
+ ]
345
+ },
346
+ {
347
+ "cell_type": "code",
348
+ "execution_count": null,
349
+ "metadata": {},
350
+ "outputs": [],
351
+ "source": [
352
+ "model.save_pretrained('prana_qwen3_8b_lora')\n",
353
+ "tokenizer.save_pretrained('prana_qwen3_8b_lora')\n",
354
+ "\n",
355
+ "# Push to Hub (optional)\n",
356
+ "if False:\n",
357
+ " model.push_to_hub_merged(\n",
358
+ " 'pbanavara/prana-qwen3-8b-grpo',\n",
359
+ " tokenizer,\n",
360
+ " save_method='merged_16bit',\n",
361
+ " token='hf_...',\n",
362
+ " )"
363
+ ]
364
+ }
365
+ ],
366
+ "metadata": {
367
+ "kernelspec": {
368
+ "display_name": "Python 3",
369
+ "language": "python",
370
+ "name": "python3"
371
+ },
372
+ "language_info": {
373
+ "name": "python",
374
+ "version": "3.11.0"
375
+ }
376
+ },
377
+ "nbformat": 4,
378
+ "nbformat_minor": 4
379
+ }
server/app.py CHANGED
@@ -19,10 +19,10 @@ Endpoints:
19
 
20
  Usage:
21
  # Development (with auto-reload):
22
- uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
 
24
  # Production:
25
- uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
 
27
  # Or run directly:
28
  python -m server.app
@@ -50,7 +50,7 @@ app = create_app(
50
  )
51
 
52
 
53
- def main(host: str = "0.0.0.0", port: int = 8000):
54
  """
55
  Entry point for direct execution via uv run or python -m.
56
 
@@ -76,6 +76,6 @@ if __name__ == "__main__":
76
  import argparse
77
 
78
  parser = argparse.ArgumentParser()
79
- parser.add_argument("--port", type=int, default=8000)
80
  args = parser.parse_args()
81
  main(port=args.port)
 
19
 
20
  Usage:
21
  # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 7860
23
 
24
  # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 7860 --workers 4
26
 
27
  # Or run directly:
28
  python -m server.app
 
50
  )
51
 
52
 
53
+ def main(host: str = "0.0.0.0", port: int = 7860):
54
  """
55
  Entry point for direct execution via uv run or python -m.
56
 
 
76
  import argparse
77
 
78
  parser = argparse.ArgumentParser()
79
+ parser.add_argument("--port", type=int, default=7860)
80
  args = parser.parse_args()
81
  main(port=args.port)
server/prana_env_environment.py CHANGED
@@ -327,35 +327,19 @@ class PranaEnvironment(Environment):
327
  history.append((anom_date, self._injected_anomaly["value"]))
328
 
329
  history.append((EPISODE_DATE, float(t5_value)))
330
- history.sort(key=lambda x: x[0])
331
 
332
- lines = []
333
- for i, (d, v) in enumerate(history):
334
- suffix = " ← latest" if i == len(history) - 1 else ""
335
- lines.append(f" {v} (recorded: {d}){suffix}")
336
 
337
- result = (
 
 
338
  f"{field} measurement history for {patient_id} "
339
  f"(filing date: {EPISODE_DATE}):\n" + "\n".join(lines)
340
  )
341
 
342
- # Check for anomaly between consecutive entries within window
343
- for i in range(len(history) - 1):
344
- d1, v1 = history[i]
345
- d2, v2 = history[i + 1]
346
- days_apart = (d2 - d1).days
347
- if days_apart <= ANOMALY_WINDOW_DAYS and v1 > 0:
348
- change = abs(v2 - v1) / v1
349
- if change >= ANOMALY_THRESHOLD:
350
- pct = round(change * 100, 1)
351
- result += (
352
- f"\n⚠️ ANOMALY DETECTED: {v1} ({d1}) β†’ {v2} ({d2}), "
353
- f"{days_apart} days apart, {pct}% delta. "
354
- f"Recommend confirmatory test before filing."
355
- )
356
-
357
- return result
358
-
359
  def _handle_record_value(self, action: PranaAction) -> PranaObservation:
360
  field = (action.field or "").lower()
361
  value = action.value
 
327
  history.append((anom_date, self._injected_anomaly["value"]))
328
 
329
  history.append((EPISODE_DATE, float(t5_value)))
 
330
 
331
+ # Shuffle deterministically by (patient_id, field) β€” agent must sort by date.
332
+ # No ← latest pointer, no anomaly flag β€” matches tau2 benchmark behaviour.
333
+ rng = random.Random(hash((patient_id, field)) & 0xFFFFFFFF)
334
+ rng.shuffle(history)
335
 
336
+ lines = [f" {v} (recorded: {d})" for d, v in history]
337
+
338
+ return (
339
  f"{field} measurement history for {patient_id} "
340
  f"(filing date: {EPISODE_DATE}):\n" + "\n".join(lines)
341
  )
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  def _handle_record_value(self, action: PranaAction) -> PranaObservation:
344
  field = (action.field or "").lower()
345
  value = action.value