TheJackBright commited on
Commit
b42dbeb
·
1 Parent(s): 2f3edd0

Version 1 - VK

Browse files
Files changed (36) hide show
  1. openenv-polypharmacy/Dockerfile +30 -0
  2. openenv-polypharmacy/PROMPT.md +571 -0
  3. openenv-polypharmacy/README.md +184 -0
  4. openenv-polypharmacy/data/lookups/beers_criteria.csv +16 -0
  5. openenv-polypharmacy/data/lookups/ddi_rules.csv +25 -0
  6. openenv-polypharmacy/data/lookups/drug_metadata.csv +34 -0
  7. openenv-polypharmacy/data/processed/patients_polypharmacy.csv +121 -0
  8. openenv-polypharmacy/inference.py +214 -0
  9. openenv-polypharmacy/openenv.yaml +30 -0
  10. openenv-polypharmacy/pyproject.toml +39 -0
  11. openenv-polypharmacy/requirements.txt +7 -0
  12. openenv-polypharmacy/scripts/preprocess_data.py +301 -0
  13. openenv-polypharmacy/scripts/run_validation.sh +15 -0
  14. openenv-polypharmacy/src/polypharmacy_env.egg-info/PKG-INFO +15 -0
  15. openenv-polypharmacy/src/polypharmacy_env.egg-info/SOURCES.txt +25 -0
  16. openenv-polypharmacy/src/polypharmacy_env.egg-info/dependency_links.txt +1 -0
  17. openenv-polypharmacy/src/polypharmacy_env.egg-info/requires.txt +11 -0
  18. openenv-polypharmacy/src/polypharmacy_env.egg-info/top_level.txt +1 -0
  19. openenv-polypharmacy/src/polypharmacy_env/__init__.py +1 -0
  20. openenv-polypharmacy/src/polypharmacy_env/api/__init__.py +1 -0
  21. openenv-polypharmacy/src/polypharmacy_env/api/schemas.py +36 -0
  22. openenv-polypharmacy/src/polypharmacy_env/api/server.py +67 -0
  23. openenv-polypharmacy/src/polypharmacy_env/baselines/__init__.py +1 -0
  24. openenv-polypharmacy/src/polypharmacy_env/baselines/heuristic_agent.py +204 -0
  25. openenv-polypharmacy/src/polypharmacy_env/baselines/random_agent.py +54 -0
  26. openenv-polypharmacy/src/polypharmacy_env/config.py +79 -0
  27. openenv-polypharmacy/src/polypharmacy_env/data_loader.py +142 -0
  28. openenv-polypharmacy/src/polypharmacy_env/ddi_simulator.py +115 -0
  29. openenv-polypharmacy/src/polypharmacy_env/env_core.py +413 -0
  30. openenv-polypharmacy/src/polypharmacy_env/graders.py +98 -0
  31. openenv-polypharmacy/src/polypharmacy_env/models.py +103 -0
  32. openenv-polypharmacy/src/polypharmacy_env/rewards.py +92 -0
  33. openenv-polypharmacy/src/polypharmacy_env/tasks.py +47 -0
  34. openenv-polypharmacy/src/polypharmacy_env/tests/__init__.py +1 -0
  35. openenv-polypharmacy/src/polypharmacy_env/tests/test_api.py +73 -0
  36. openenv-polypharmacy/src/polypharmacy_env/tests/test_env_core.py +162 -0
openenv-polypharmacy/Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # System deps
4
+ RUN apt-get update && \
5
+ apt-get install -y --no-install-recommends build-essential curl && \
6
+ rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ # Install Python deps first (layer caching)
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy project
15
+ COPY . .
16
+
17
+ # Generate data if not present
18
+ RUN python3 scripts/preprocess_data.py
19
+
20
+ # Environment
21
+ ENV PORT=7860
22
+ ENV PYTHONPATH="/app/src:${PYTHONPATH}"
23
+ ENV PYTHONUNBUFFERED=1
24
+
25
+ EXPOSE 7860
26
+
27
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
28
+ CMD curl -f http://localhost:7860/health || exit 1
29
+
30
+ CMD ["uvicorn", "polypharmacy_env.api.server:app", "--host", "0.0.0.0", "--port", "7860"]
openenv-polypharmacy/PROMPT.md ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are an expert Python backend, ML, and infrastructure engineer.
2
+ Your task is to implement a complete, production-ready OpenEnv environment called **PolypharmacyEnv** for training and evaluating agentic RL policies that act as an "elderly polypharmacy safety agent" (clinical pharmacist assistant).
3
+
4
+ The deliverable MUST satisfy all of the following:
5
+ - Fully compliant with the OpenEnv spec (typed models, `step()` / `reset()` / `state()`, `openenv.yaml`, HTTP server, Dockerfile).
6
+ - Simulates a realistic healthcare workflow around elderly polypharmacy and dangerous drug combinations.
7
+ - Defines at least **3 tasks** (easy → medium → hard) with deterministic agent graders producing scores in (0.0, 1.0).
8
+ - Provides shaped rewards over the trajectory (not just sparse terminal rewards).
9
+ - Includes a baseline LLM-based inference script `inference.py` in the repo root, following the evaluation requirements:
10
+ - Uses the OpenAI Python client.
11
+ - Reads `OPENAI_API_KEY`, `API_BASE_URL`, `MODEL_NAME`, and `HF_TOKEN` from the environment.
12
+ - Emits structured stdout logs in the exact `[START]`, `[STEP]`, `[END]` format from the OpenEnv sample inference script.
13
+ - Is containerized and deployable as a **Hugging Face Space** tagged with `openenv` that responds to OpenEnv-style `reset` / `step` / `state` HTTP calls.
14
+
15
+ Implement everything described below.
16
+
17
+ =================================================
18
+ 1. Repository and folder structure
19
+ =================================================
20
+
21
+ Create a Python package repository with this structure (names are important unless clearly labeled as examples):
22
+
23
+ - `openenv-polypharmacy/`
24
+ - `openenv.yaml`
25
+ - `README.md`
26
+ - `requirements.txt`
27
+ - `Dockerfile`
28
+ - `inference.py` # baseline LLM agent per spec
29
+ - `pyproject.toml` or `setup.cfg` (optional but recommended)
30
+ - `src/`
31
+ - `polypharmacy_env/`
32
+ - `__init__.py`
33
+ - `config.py`
34
+ - `models.py` # Action, Observation, State, helper models
35
+ - `env_core.py` # PolypharmacyEnv implementation
36
+ - `tasks.py` # task setup utilities
37
+ - `graders.py` # deterministic graders for each task
38
+ - `rewards.py` # reward shaping logic
39
+ - `data_loader.py` # load/preprocess patient and lookup data
40
+ - `ddi_simulator.py` # local DDI / guideline simulator
41
+ - `api/`
42
+ - `__init__.py`
43
+ - `schemas.py` # HTTP request/response schemas
44
+ - `server.py` # FastAPI app exposing OpenEnv endpoints
45
+ - `baselines/`
46
+ - `__init__.py`
47
+ - `heuristic_agent.py` # simple rule-based baseline agent
48
+ - `random_agent.py` # trivial random baseline (optional)
49
+ - `tests/`
50
+ - `__init__.py`
51
+ - `test_env_core.py`
52
+ - `test_api.py`
53
+ - `data/`
54
+ - `raw/` # placeholder for real/synthetic source data
55
+ - `processed/`
56
+ - `lookups/`
57
+ - `ddi_rules.csv`
58
+ - `beers_criteria.csv`
59
+ - `drug_metadata.csv`
60
+ - `scripts/`
61
+ - `preprocess_data.py`
62
+ - `run_validation.sh` # optional; runs OpenEnv validator, tests, etc.
63
+
64
+ Use Python 3.10+ with full type hints, and keep the code black/isort-compatible.
65
+
66
+ =================================================
67
+ 2. Domain, data, and clinical abstraction
68
+ =================================================
69
+
70
+ 2.1. Core scenario
71
+
72
+ Model an elderly patient (age ≥ 65) with:
73
+ - Demographics: age, sex.
74
+ - Comorbidities: e.g., hypertension, diabetes, heart failure, CKD, dementia.
75
+ - Basic labs: kidney function (eGFR category), liver function category.
76
+ - A current medication list (polypharmacy, e.g., 3–15 drugs depending on task).
77
+
78
+ Each **episode** is one medication-review session where the agent:
79
+ - Observes patient info and current meds.
80
+ - Optionally **queries** a DDI/guideline tool for specific drug pairs.
81
+ - Proposes **interventions**:
82
+ - `stop`: discontinue a drug.
83
+ - `dose_reduce`: lower dose of a drug.
84
+ - `substitute`: swap to a safer alternative.
85
+ - `add_monitoring`: keep the drug but flag extra monitoring.
86
+ - Calls `finish_review` when it decides the regimen is acceptable or budgets are exhausted.
87
+
88
+ No external PHI, EHRs, or online APIs: all data is **synthetic** or de-identified and local to the container (CSV files).
89
+
90
+ 2.2. Data files and CSV schemas
91
+
92
+ Implement local CSVs under `data/lookups/`:
93
+
94
+ **`drug_metadata.csv`**
95
+ - `drug_id` (string; unique key)
96
+ - `generic_name` (string)
97
+ - `atc_class` (string)
98
+ - `is_high_risk_elderly` (0/1)
99
+ - `default_dose_mg` (float)
100
+ - `min_dose_mg` (float)
101
+ - `max_dose_mg` (float)
102
+
103
+ **`beers_criteria.csv`**
104
+ - `drug_id` (string)
105
+ - `criterion_type` (enum string: `avoid`, `caution`, `dose_adjust`, `avoid_in_condition`)
106
+ - `condition` (nullable string; e.g., `CKD`, `dementia`)
107
+ - `rationale` (brief text)
108
+
109
+ **`ddi_rules.csv`**
110
+ - `drug_id_1` (string; normalized so `drug_id_1 < drug_id_2` lexicographically)
111
+ - `drug_id_2` (string)
112
+ - `severity` (enum string: `mild`, `moderate`, `severe`)
113
+ - `mechanism` (short text)
114
+ - `recommendation` (enum string: `avoid_combination`, `monitor_closely`, `dose_adjust`, `no_action`)
115
+ - `base_risk_score` (float in [0.0, 1.0])
116
+
117
+ Implement a synthetic patient-episode dataset under `data/processed/`:
118
+
119
+ **`patients_polypharmacy.csv`**
120
+ - `episode_id` (string)
121
+ - `age` (int)
122
+ - `sex` (enum: `M`, `F`, `O`)
123
+ - `conditions` (semicolon-separated; e.g., `HTN;DM;CKD`)
124
+ - `eGFR_category` (enum: `normal`, `mild`, `moderate`, `severe`)
125
+ - `liver_function_category` (enum: `normal`, `impaired`)
126
+ - `medication_ids` (semicolon-separated list of `drug_id`)
127
+ - `baseline_risk_score` (float in [0.0, 1.0])
128
+
129
+ 2.3. Preprocessing script
130
+
131
+ In `scripts/preprocess_data.py`:
132
+ - If real data is not provided, procedurally generate synthetic but plausible data using:
133
+ - Random combinations of conditions and drugs constrained by simple rules (e.g., CKD + renally-cleared drugs).
134
+ - Controlled distribution of high-risk DDIs and Beers violations.
135
+ - Explicitly tag episodes as easy/medium/hard (e.g., via number of drugs, number/severity of DDIs, and number of Beers issues).
136
+ - Save `patients_polypharmacy.csv` ready for the environment to consume.
137
+
138
+ =================================================
139
+ 3. OpenEnv models and environment implementation
140
+ =================================================
141
+
142
+ 3.1. Models
143
+
144
+ In `models.py`, define dataclasses or Pydantic models that extend the appropriate OpenEnv base types (`Action`, `Observation`, `State`) and are JSON-compatible.
145
+
146
+ Auxiliary models:
147
+
148
+ **`MedicationEntry`**
149
+ - `drug_id: str`
150
+ - `generic_name: str`
151
+ - `atc_class: str`
152
+ - `dose_mg: float`
153
+ - `frequency: str` # e.g., `qd`, `bid`
154
+ - `route: str` # e.g., `po`
155
+ - `is_high_risk_elderly: bool`
156
+ - `beers_flags: list[str]` # e.g., `["avoid", "dose_adjust_CKD"]`
157
+
158
+ **`InteractionQueryRecord`**
159
+ - `drug_id_1: str`
160
+ - `drug_id_2: str`
161
+ - `severity: str | None`
162
+ - `recommendation: str | None`
163
+ - `risk_score: float | None`
164
+ - `step_index: int`
165
+
166
+ **`InterventionRecord`**
167
+ - `target_drug_id: str`
168
+ - `action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]`
169
+ - `proposed_new_drug_id: str | None`
170
+ - `rationale: str`
171
+ - `step_index: int`
172
+
173
+ Core wire models:
174
+
175
+ **`PolypharmacyObservation`** (extends OpenEnv `Observation`)
176
+ - `episode_id: str`
177
+ - `task_id: Literal["easy_screening", "budgeted_screening", "complex_tradeoff"]`
178
+ - `age: int`
179
+ - `sex: str`
180
+ - `conditions: list[str]`
181
+ - `eGFR_category: str`
182
+ - `liver_function_category: str`
183
+ - `current_medications: list[MedicationEntry]`
184
+ - `interaction_queries: list[InteractionQueryRecord]`
185
+ - `interventions: list[InterventionRecord]`
186
+ - `step_index: int`
187
+ - `remaining_query_budget: int`
188
+ - `remaining_intervention_budget: int`
189
+ - `shaped_reward: float` # reward from last step
190
+ - `done: bool`
191
+
192
+ **`PolypharmacyAction`** (extends OpenEnv `Action`)
193
+ - `action_type: Literal["query_ddi", "propose_intervention", "finish_review"]`
194
+ - `drug_id_1: str | None` # for DDI queries or some interventions
195
+ - `drug_id_2: str | None` # for DDI queries
196
+ - `target_drug_id: str | None` # for interventions
197
+ - `intervention_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"] | None`
198
+ - `proposed_new_drug_id: str | None`
199
+ - `rationale: str | None`
200
+
201
+ **`PolypharmacyState`** (extends OpenEnv `State`)
202
+ - `episode_id: str`
203
+ - `task_id: str`
204
+ - `step_count: int`
205
+ - `max_steps: int`
206
+ - `num_query_actions: int`
207
+ - `num_interventions: int`
208
+
209
+ 3.2. Environment core
210
+
211
+ In `env_core.py`, implement `PolypharmacyEnv` extending the appropriate OpenEnv environment base class. It must implement:
212
+
213
+ **`reset(task_id: str | None = None) -> PolypharmacyObservation`**
214
+ - If `task_id` is `None`, default to medium (`budgeted_screening`).
215
+ - Sample an episode from `patients_polypharmacy.csv` filtered by difficulty.
216
+ - Initialize:
217
+ - `episode_id`
218
+ - `step_count = 0`
219
+ - task-specific budgets (query, interventions, max_steps)
220
+ - baseline regime and risk
221
+ - empty `interaction_queries` and `interventions`
222
+ - Return the initial `PolypharmacyObservation` with:
223
+ - `step_index = 0`
224
+ - `shaped_reward = 0.0`
225
+ - `done = False`
226
+
227
+ **`step(action: PolypharmacyAction) -> dict`**
228
+ - Validate the action; if invalid:
229
+ - Apply a negative reward.
230
+ - Do not modify regimen, but log error in `info`.
231
+ - If `action_type == "query_ddi"`:
232
+ - If query budget exhausted, apply penalty and do not query.
233
+ - Else:
234
+ - Use `ddi_simulator.lookup_ddi(drug_id_1, drug_id_2)` to get severity, recommendation, base_risk_score.
235
+ - Append an `InteractionQueryRecord`.
236
+ - Apply a small negative reward for query cost.
237
+ - If `action_type == "propose_intervention"`:
238
+ - If intervention budget exhausted, apply penalty and ignore change.
239
+ - Else:
240
+ - Update `current_medications` according to `intervention_type`:
241
+ - `stop`: remove medication.
242
+ - `dose_reduce`: adjust dose downward within [min_dose_mg, default_dose_mg].
243
+ - `substitute`: replace with a safer alternative from same `atc_class`.
244
+ - `add_monitoring`: keep drug but tag in internal state.
245
+ - Append an `InterventionRecord`.
246
+ - Recompute current regimen risk using the risk model (see 3.3).
247
+ - Compute shaped reward = (previous_risk - new_risk) - small intervention cost.
248
+ - If `action_type == "finish_review"`:
249
+ - Mark `done = True`.
250
+ - Call the task’s grader to get episode-level score in [0.0, 1.0].
251
+ - Add this as a terminal bonus to the current step reward.
252
+
253
+ - In all cases:
254
+ - Increment `step_count`.
255
+ - Check `max_steps`; if exceeded, auto-terminate:
256
+ - `done = True`
257
+ - apply time-out penalty
258
+ - call grader with current trajectory for a final score if appropriate.
259
+ - Construct next `PolypharmacyObservation` with updated fields.
260
+ - Return a dict:
261
+ - `observation`: `PolypharmacyObservation`
262
+ - `reward`: float shaped reward for this step
263
+ - `done`: bool
264
+ - `info`: dict with fields like `current_risk`, `baseline_risk`, `grader_score_if_terminal`, and debug flags.
265
+
266
+ **`state` property**
267
+ - Returns `PolypharmacyState` reflecting the current internal state.
268
+
269
+ 3.3. DDI simulator and risk model
270
+
271
+ In `ddi_simulator.py`:
272
+ - Load `ddi_rules.csv` once via `data_loader`.
273
+ - Implement `lookup_ddi(drug_id_1, drug_id_2) -> tuple[severity, recommendation, base_risk_score]`:
274
+ - Normalize the pair ordering.
275
+ - Look up row; if missing, return:
276
+ - severity = `"none"`
277
+ - recommendation = `"no_action"`
278
+ - base_risk_score = 0.0
279
+
280
+ In `rewards.py` (or a dedicated module), implement:
281
+ - `compute_regimen_risk(current_drug_ids, patient_context, ddi_rules, beers_rules, drug_metadata) -> float`
282
+ - Aggregate contributions from:
283
+ - Beers violations (weighted by `criterion_type` and relevant conditions).
284
+ - DDI base risk scores for all present drug pairs.
285
+ - High-risk elderly drugs.
286
+ - Normalize and clip to [0.0, 1.0].
287
+
288
+ Use this function to compute:
289
+ - `baseline_risk` at episode start.
290
+ - Risk after each intervention step.
291
+
292
+ Also implement:
293
+ - `compute_shaped_reward(previous_risk, new_risk, action, context, partial_metrics) -> float`
294
+ - Positive component: `previous_risk - new_risk`.
295
+ - Negative components: per-query cost, per-intervention cost, invalid-action penalty, time-out penalty.
296
+
297
+ =================================================
298
+ 4. Tasks and graders (3 difficulty levels)
299
+ =================================================
300
+
301
+ Define three task IDs and semantics in `tasks.py` and `graders.py`:
302
+
303
+ Task IDs:
304
+ - `easy_screening`
305
+ - `budgeted_screening`
306
+ - `complex_tradeoff`
307
+
308
+ 4.1. `easy_screening` (easy)
309
+
310
+ - Small regimen: 3–5 drugs.
311
+ - Exactly one **severe** DDI pair and possibly one simple Beers violation.
312
+ - Budgets:
313
+ - query_budget ≈ 4
314
+ - intervention_budget ≈ 2
315
+ - max_steps ≈ 10
316
+
317
+ Grader:
318
+ - Input: full trajectory, baseline risk, final risk, list of interventions.
319
+ - Compute:
320
+ - `risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, ε)` (normalized).
321
+ - `targeted_intervention_flag = 1.0` if at least one intervention affects one of the drugs in the known severe DDI pair, else 0.0.
322
+ - Score:
323
+ - `score = 0.5 * risk_reduction + 0.5 * targeted_intervention_flag`
324
+ - Clip to [0.0, 1.0].
325
+
326
+ 4.2. `budgeted_screening` (medium)
327
+
328
+ - Medium regimen: 6–10 drugs.
329
+ - Multiple DDIs (mild/moderate/severe) and multiple Beers issues.
330
+ - Budgets:
331
+ - query_budget ≈ 8
332
+ - intervention_budget ≈ 3
333
+ - max_steps ≈ 20
334
+
335
+ Grader:
336
+ - Compute:
337
+ - `risk_reduction_score` as normalized risk drop.
338
+ - `intervention_precision_score` = fraction of interventions that actually reduce risk or fix guideline violations.
339
+ - `query_efficiency_score` = (number of severe/moderate DDIs discovered) / (number of queries used), normalized.
340
+ - Weighted score, for example:
341
+ - `score = 0.5 * risk_reduction_score + 0.3 * intervention_precision_score + 0.2 * query_efficiency_score`
342
+ - Clip to [0.0, 1.0].
343
+
344
+ 4.3. `complex_tradeoff` (hard)
345
+
346
+ - Larger regimen: 10–15 drugs.
347
+ - Some drugs are **clinically critical** (e.g., anticoagulants, insulin analogues) and encoded as such in `drug_metadata` or a small internal map.
348
+ - Episodes contain:
349
+ - multiple DDIs and Beers issues, including ones involving critical drugs.
350
+ - safer substitutes for some risky drugs.
351
+
352
+ Budgets:
353
+ - query_budget ≈ 12
354
+ - intervention_budget ≈ 5
355
+ - max_steps ≈ 30
356
+
357
+ Grader adds a **regimen disruption penalty** component:
358
+ - Metrics:
359
+ - `risk_reduction_score` (as above).
360
+ - `critical_drug_penalty` = penalty if a critical drug is stopped without substitution to another suitable agent.
361
+ - `total_drug_changes` = number of drugs stopped or substituted.
362
+ - `regimen_disruption_penalty` derived from `total_drug_changes` and `critical_drug_penalty`.
363
+
364
+ Example scoring:
365
+ - `base = risk_reduction_score`
366
+ - `penalty = α * regimen_disruption_penalty`
367
+ - `score = clamp(base - penalty, 0.0, 1.0)`
368
+
369
+ 4.4. Reward shaping
370
+
371
+ In `rewards.py`, define a consistent shaping scheme:
372
+ - On each query:
373
+ - Small negative reward (e.g., −0.01) plus any small bonus if it discovers a severe DDI, if desired.
374
+ - On each intervention:
375
+ - Reward ≈ (previous_risk - new_risk) − small intervention cost.
376
+ - On invalid actions:
377
+ - Larger negative reward (e.g., −0.1) and no state change.
378
+ - On `finish_review`:
379
+ - Add the task-level `score` ∈ [0.0, 1.0] from the corresponding grader to that step’s shaped reward.
380
+
381
+ Ensure the sum of step rewards per episode remains in a reasonable numeric range (e.g., roughly -5 to +5) while still allowing meaningful differentiation by graders.
382
+
383
+ =================================================
384
+ 5. HTTP API server and openenv.yaml
385
+ =================================================
386
+
387
+ 5.1. HTTP server (FastAPI)
388
+
389
+ In `api/server.py`:
390
+ - Implement a FastAPI app that maintains a `PolypharmacyEnv` instance (or a multiplexing scheme if needed).
391
+ - Endpoints:
392
+ - `POST /reset`:
393
+ - Request body: may include `task_id` (string).
394
+ - Response: serialized `PolypharmacyObservation`.
395
+ - `POST /step`:
396
+ - Request body: serialized `PolypharmacyAction`.
397
+ - Response: dict with:
398
+ - `observation`: `PolypharmacyObservation`
399
+ - `reward`: float
400
+ - `done`: bool
401
+ - `info`: dict
402
+ - `GET /state`:
403
+ - Response: `PolypharmacyState`.
404
+
405
+ Provide a module-level `app = FastAPI(...)` object for use with uvicorn and Hugging Face Spaces. Ensure the JSON schema is consistent with OpenEnv clients (simple, flat JSON for observation/action/state).
406
+
407
+ 5.2. `openenv.yaml`
408
+
409
+ At repo root, define `openenv.yaml` consistent with the latest OpenEnv spec. At minimum, include:
410
+ - `name`: `polypharmacy_env`
411
+ - `version`: e.g., `0.1.0`
412
+ - `description`: human-readable description.
413
+ - `author`: your details.
414
+ - `tags`: e.g., `["healthcare", "polypharmacy", "openenv"]`
415
+ - `tasks`:
416
+ - One entry per task:
417
+ - `id`: `"easy_screening"` / `"budgeted_screening"` / `"complex_tradeoff"`
418
+ - `description`: one-line description
419
+ - `difficulty`: `"easy"`, `"medium"`, `"hard"`
420
+
421
+ Ensure `openenv validate` (or equivalent validator) passes once implemented.
422
+
423
+ =================================================
424
+ 6. Baseline heuristic (non-LLM) agent
425
+ =================================================
426
+
427
+ In `baselines/heuristic_agent.py`, implement a simple, deterministic baseline agent that:
428
+
429
+ For each episode:
430
+ - Iterates through all unordered medication pairs within query budget:
431
+ - Calls `query_ddi` via the environment for each pair until the query budget is exhausted or all pairs are examined.
432
+ - Records severe and moderate interactions.
433
+ - After querying:
434
+ - For each severe DDI pair:
435
+ - Try `substitute` one of the drugs using `drug_metadata`:
436
+ - Prefer substitute within same `atc_class` that:
437
+ - is not marked high-risk elderly.
438
+ - does not participate in known severe DDIs with the rest of the regimen.
439
+ - If no substitute exists, propose `stop` for the higher-risk drug.
440
+ - Respect intervention budget limits.
441
+ - Finally, call `finish_review`.
442
+
443
+ This baseline should be callable as a simple Python function that interacts with `PolypharmacyEnv` directly (without HTTP).
444
+
445
+ =================================================
446
+ 7. Baseline LLM inference script (inference.py)
447
+ =================================================
448
+
449
+ At repo root, create `inference.py` that:
450
+
451
+ 7.1. Uses the OpenAI Python client
452
+
453
+ - Import and configure the official OpenAI Python client.
454
+ - Read environment variables:
455
+ - `OPENAI_API_KEY` (required).
456
+ - `API_BASE_URL` (base URL for LLM; default to OpenAI standard if not set).
457
+ - `MODEL_NAME` (e.g., `gpt-4.1` or similar).
458
+ - `HF_TOKEN` (if needed for HF auth; do not hardcode).
459
+ - Read `POLYPHARMACY_ENV_URL` (or similar) for the environment’s HTTP base URL.
460
+
461
+ 7.2. Implements the required logging format
462
+
463
+ - For each **run** across all tasks:
464
+ - Emit a `[START]` line with a JSON payload exactly matching the evaluation specification:
465
+ - Fields such as `run_id`, `task_id`, `model`, etc., in the same order and naming as the sample OpenEnv inference script.
466
+ - For each **step** in an episode:
467
+ - Emit a `[STEP]` line with JSON fields including:
468
+ - `run_id`
469
+ - `task_id`
470
+ - `episode_id`
471
+ - `step_index`
472
+ - `observation_summary` (brief, machine-readable summary)
473
+ - `action_payload` (the action sent to the env)
474
+ - `reward`
475
+ - `done`
476
+ - After finishing an episode for a task:
477
+ - Emit an `[END]` line summarizing:
478
+ - `run_id`
479
+ - `task_id`
480
+ - per-episode statistics (e.g., total reward, grader score from last step’s `info`).
481
+ - The stdout format MUST follow the sample exactly:
482
+ - Same tags: `[START]`, `[STEP]`, `[END]`.
483
+ - Same JSON field names and ordering as the provided reference.
484
+ - No extra prints except these structured logs (and necessary error messages to stderr).
485
+
486
+ 7.3. LLM agent loop
487
+
488
+ - For each task (`easy_screening`, `budgeted_screening`, `complex_tradeoff`):
489
+ - Run a fixed small number of episodes (e.g., 5–10 per task) for baseline scoring.
490
+ - For each episode:
491
+ - Call `/reset` with the task id.
492
+ - At each step:
493
+ - Summarize the observation into a concise prompt for the LLM:
494
+ - Include age, sex, conditions, high-risk flags, budgets, and a compressed view of meds and previous actions.
495
+ - Ask the model to output a **strict JSON** representing `PolypharmacyAction` fields.
496
+ - Parse and validate the JSON; if invalid, fall back to a safe default (e.g., `finish_review` or a no-op) and penalize in evaluation.
497
+ - Send this action to `/step` and log `[STEP]`.
498
+ - End when `done=True` or max_steps is reached.
499
+ - At the end, print aggregate scores per task and overall.
500
+
501
+ Make sure runtime < 20 minutes and that the script can run within 2 vCPUs and 8 GB RAM.
502
+
503
+ =================================================
504
+ 8. Dockerfile and Hugging Face Space
505
+ =================================================
506
+
507
+ 8.1. Dockerfile
508
+
509
+ Create a `Dockerfile` that:
510
+ - Starts from a slim Python image (e.g., `python:3.11-slim`).
511
+ - Installs system dependencies as needed (e.g., `build-essential`, `curl`).
512
+ - Copies the project into the container.
513
+ - Installs Python dependencies from `requirements.txt`.
514
+ - Sets appropriate environment variables for the app (e.g., `PORT=7860`).
515
+ - Exposes port 7860.
516
+ - Uses a `CMD` or `ENTRYPOINT` that runs the FastAPI server, for example:
517
+ - `uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860`
518
+
519
+ 8.2. Hugging Face Space
520
+
521
+ Ensure the repository is ready to be used as a Hugging Face Space:
522
+ - Space type: `docker`.
523
+ - Tag: `openenv`.
524
+ - On container start, the server must listen on the correct port and respond to:
525
+ - `POST /reset`
526
+ - `POST /step`
527
+ - `GET /state`
528
+ - The environment must start cleanly with `docker build` + `docker run` locally.
529
+
530
+ =================================================
531
+ 9. README and documentation
532
+ =================================================
533
+
534
+ In `README.md`, include:
535
+
536
+ - **Environment description & motivation**:
537
+ - What PolypharmacyEnv simulates.
538
+ - Why elderly polypharmacy safety matters.
539
+ - **Action and observation spaces**:
540
+ - Describe `PolypharmacyAction`, `PolypharmacyObservation`, and `PolypharmacyState` fields and semantics.
541
+ - **Task descriptions**:
542
+ - `easy_screening`, `budgeted_screening`, `complex_tradeoff`, their difficulty and goals.
543
+ - **Reward structure**:
544
+ - Summarize shaping and terminal rewards.
545
+ - **Setup & usage**:
546
+ - How to install dependencies.
547
+ - How to run the API server locally (uvicorn command).
548
+ - How to run the heuristic baseline.
549
+ - How to run `inference.py` with environment variables.
550
+ - **Baseline scores**:
551
+ - Document reproducible baseline scores for each task (heuristic agent, and LLM baseline if available).
552
+
553
+ =================================================
554
+ 10. Validation and quality gates
555
+ =================================================
556
+
557
+ - Ensure:
558
+ - `openenv.yaml` and the HTTP server pass the OpenEnv validation script.
559
+ - `docker build` and `docker run` work without errors.
560
+ - `inference.py` completes under 20 minutes, within 2 vCPUs / 8 GB RAM.
561
+ - All graders:
562
+ - Are deterministic.
563
+ - Return scores strictly in [0.0, 1.0].
564
+ - No grader returns a constant score irrespective of behavior.
565
+
566
+ Aim for clean, well-structured, well-documented code with clear separation of concerns between:
567
+ - Data loading,
568
+ - Environment state & dynamics,
569
+ - Reward/grade logic,
570
+ - HTTP serving,
571
+ - Baseline agents and inference.
openenv-polypharmacy/README.md ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PolypharmacyEnv
2
+
3
+ An [OpenEnv](https://github.com/meta-pytorch/OpenEnv)-compliant reinforcement-learning environment that simulates **elderly polypharmacy medication review**. An RL agent acts as a clinical pharmacist assistant, identifying dangerous drug-drug interactions (DDIs), Beers-criteria violations, and proposing safe interventions.
4
+
5
+ ---
6
+
7
+ ## Motivation
8
+
9
+ Polypharmacy (concurrent use of multiple medications) is extremely common in elderly patients (age >= 65) and carries significant risks:
10
+
11
+ - **Drug-drug interactions** can cause adverse events, hospitalisation, and death.
12
+ - **Beers-criteria violations** flag medications that are inappropriate or require dose adjustments in older adults.
13
+ - Stopping critical medications (anticoagulants, insulin) without proper substitution can be equally dangerous.
14
+
15
+ This environment lets RL and LLM-based agents learn to **balance risk reduction against regimen stability**.
16
+
17
+ ---
18
+
19
+ ## Action Space
20
+
21
+ Each step, the agent sends a `PolypharmacyAction` with one of three action types:
22
+
23
+ | `action_type` | Required fields | Description |
24
+ |---|---|---|
25
+ | `query_ddi` | `drug_id_1`, `drug_id_2` | Query the DDI database for an interaction between two drugs |
26
+ | `propose_intervention` | `target_drug_id`, `intervention_type` | Propose changing a medication (`stop`, `dose_reduce`, `substitute`, `add_monitoring`) |
27
+ | `finish_review` | — | End the review and trigger final grading |
28
+
29
+ Optional fields: `proposed_new_drug_id`, `rationale`.
30
+
31
+ ## Observation Space
32
+
33
+ `PolypharmacyObservation` includes:
34
+
35
+ - **Patient demographics**: `age`, `sex`, `conditions`, `eGFR_category`, `liver_function_category`
36
+ - **Medications**: list of `MedicationEntry` (drug_id, name, class, dose, high-risk flags, Beers flags)
37
+ - **History**: `interaction_queries` (past DDI query results), `interventions` (past actions)
38
+ - **Budgets**: `remaining_query_budget`, `remaining_intervention_budget`
39
+ - **Reward signals**: `shaped_reward`, `done`
40
+
41
+ ## State
42
+
43
+ `PolypharmacyState`: `episode_id`, `task_id`, `step_count`, `max_steps`, `num_query_actions`, `num_interventions`.
44
+
45
+ ---
46
+
47
+ ## Tasks
48
+
49
+ | Task ID | Difficulty | Drugs | Query Budget | Intervention Budget | Max Steps | Description |
50
+ |---|---|---|---|---|---|---|
51
+ | `easy_screening` | Easy | 3-5 | 4 | 2 | 10 | One severe DDI, simple resolution |
52
+ | `budgeted_screening` | Medium | 6-10 | 8 | 3 | 20 | Multiple DDIs + Beers issues, limited budgets |
53
+ | `complex_tradeoff` | Hard | 10-15 | 12 | 5 | 30 | Critical drugs, trade-off between risk and regimen stability |
54
+
55
+ ---
56
+
57
+ ## Reward Structure
58
+
59
+ **Per-step shaped rewards:**
60
+
61
+ | Event | Reward |
62
+ |---|---|
63
+ | DDI query | -0.01 (cost) + 0.03 bonus if severe DDI discovered |
64
+ | Successful intervention | +(previous_risk - new_risk) - 0.02 cost |
65
+ | Invalid action | -0.10 penalty |
66
+ | Timeout (max steps exceeded) | -0.20 penalty |
67
+ | `finish_review` | + grader score (0.0 to 1.0) |
68
+
69
+ **Terminal grader scoring:**
70
+ - **Easy**: 50% risk reduction + 50% targeted intervention flag
71
+ - **Medium**: 50% risk reduction + 30% intervention precision + 20% query efficiency
72
+ - **Hard**: risk reduction - regimen disruption penalty - critical drug penalty
73
+
74
+ ---
75
+
76
+ ## Setup & Usage
77
+
78
+ ### Install dependencies
79
+
80
+ ```bash
81
+ pip install -r requirements.txt
82
+ ```
83
+
84
+ ### Generate synthetic data
85
+
86
+ ```bash
87
+ python3 scripts/preprocess_data.py
88
+ ```
89
+
90
+ ### Run the API server locally
91
+
92
+ ```bash
93
+ PYTHONPATH=src uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860
94
+ ```
95
+
96
+ ### Run the heuristic baseline
97
+
98
+ ```bash
99
+ PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
100
+ ```
101
+
102
+ ### Run tests
103
+
104
+ ```bash
105
+ PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
106
+ ```
107
+
108
+ ### Run `inference.py` (LLM baseline)
109
+
110
+ ```bash
111
+ # Start the server first, then in another terminal:
112
+ export OPENAI_API_KEY="sk-..."
113
+ export MODEL_NAME="gpt-4.1"
114
+ export POLYPHARMACY_ENV_URL="http://localhost:7860"
115
+ python3 inference.py
116
+ ```
117
+
118
+ ### Docker
119
+
120
+ ```bash
121
+ docker build -t polypharmacy-env .
122
+ docker run -p 7860:7860 polypharmacy-env
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Hugging Face Space
128
+
129
+ This repo is ready for deployment as a HF Space:
130
+
131
+ - **Space type**: `docker`
132
+ - **Tag**: `openenv`
133
+ - The container listens on port 7860 and exposes `/reset`, `/step`, `/state`, `/health`.
134
+
135
+ ---
136
+
137
+ ## Baseline Scores
138
+
139
+ ### Heuristic Agent (deterministic, rule-based)
140
+
141
+ | Task | Avg Score | Avg Reward |
142
+ |---|---|---|
143
+ | `easy_screening` | ~0.96 | ~1.30 |
144
+ | `budgeted_screening` | ~0.48 | ~0.45 |
145
+ | `complex_tradeoff` | ~0.24 | ~0.11 |
146
+
147
+ *(Scores vary by seed; run `scripts/run_validation.sh` for exact numbers.)*
148
+
149
+ ---
150
+
151
+ ## Project Structure
152
+
153
+ ```
154
+ openenv-polypharmacy/
155
+ openenv.yaml # OpenEnv manifest
156
+ Dockerfile # Container image
157
+ inference.py # LLM baseline script
158
+ requirements.txt
159
+ pyproject.toml
160
+ src/polypharmacy_env/
161
+ config.py # Constants, task configs
162
+ models.py # Pydantic action/observation/state models
163
+ env_core.py # PolypharmacyEnv implementation
164
+ tasks.py # Task selection utilities
165
+ graders.py # Deterministic graders (3 difficulty levels)
166
+ rewards.py # Reward shaping logic
167
+ data_loader.py # CSV data loading
168
+ ddi_simulator.py # Drug interaction lookup engine
169
+ api/
170
+ server.py # FastAPI HTTP server
171
+ schemas.py # Request/response schemas
172
+ baselines/
173
+ heuristic_agent.py # Rule-based baseline
174
+ random_agent.py # Random baseline
175
+ tests/
176
+ test_env_core.py
177
+ test_api.py
178
+ data/
179
+ lookups/ # Drug metadata, DDI rules, Beers criteria CSVs
180
+ processed/ # Synthetic patient episodes
181
+ scripts/
182
+ preprocess_data.py # Synthetic data generator
183
+ run_validation.sh # Run tests + baseline
184
+ ```
openenv-polypharmacy/data/lookups/beers_criteria.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ drug_id,criterion_type,condition,rationale
2
+ DRUG_DIAZEPAM,avoid,,"Long-acting benzodiazepine: falls, fractures, cognitive impairment in elderly"
3
+ DRUG_ALPRAZOLAM,avoid,,"Benzodiazepine: falls, fractures, cognitive impairment in elderly"
4
+ DRUG_AMITRIPTYLINE,avoid,,"Strongly anticholinergic TCA: sedation, confusion, urinary retention in elderly"
5
+ DRUG_GLIPIZIDE,caution,,Sulfonylurea: hypoglycemia risk higher in elderly
6
+ DRUG_NAPROXEN,avoid,CKD,"NSAID contraindicated in CKD – renal deterioration, fluid retention"
7
+ DRUG_IBUPROFEN,avoid,CKD,"NSAID contraindicated in CKD – renal deterioration, fluid retention"
8
+ DRUG_NAPROXEN,caution,,NSAID: GI bleeding and renal risk in elderly
9
+ DRUG_IBUPROFEN,caution,,NSAID: GI bleeding and renal risk in elderly
10
+ DRUG_DIGOXIN,dose_adjust,,Avoid doses > 0.125 mg/day in elderly – toxicity risk
11
+ DRUG_TRAMADOL,avoid,,"Opioid: CNS depression, falls, constipation in elderly"
12
+ DRUG_METFORMIN,dose_adjust,CKD,Reduce dose or avoid if eGFR < 30 – lactic acidosis risk
13
+ DRUG_INSULIN_GLARGINE,caution,,Tight glycemic control increases hypoglycemia risk in elderly
14
+ DRUG_PREDNISONE,avoid_in_condition,DM,Corticosteroid worsens glycemic control in diabetes
15
+ DRUG_DONEPEZIL,avoid_in_condition,dementia,"Limited benefit, GI side effects; reassess regularly"
16
+ DRUG_CIPROFLOXACIN,caution,,"Fluoroquinolone: tendon rupture, QT prolongation risk in elderly"
openenv-polypharmacy/data/lookups/ddi_rules.csv ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ drug_id_1,drug_id_2,severity,mechanism,recommendation,base_risk_score
2
+ DRUG_NAPROXEN,DRUG_WARFARIN,severe,Increased bleeding risk – NSAID inhibits platelet + anticoagulant,avoid_combination,0.9
3
+ DRUG_IBUPROFEN,DRUG_WARFARIN,severe,Increased bleeding risk – NSAID + anticoagulant synergy,avoid_combination,0.88
4
+ DRUG_ASPIRIN,DRUG_WARFARIN,moderate,Additive antiplatelet + anticoagulant bleeding risk,monitor_closely,0.55
5
+ DRUG_FLUOXETINE,DRUG_WARFARIN,moderate,SSRI increases serotonin and may potentiate bleeding,monitor_closely,0.45
6
+ DRUG_CIPROFLOXACIN,DRUG_WARFARIN,moderate,CYP1A2 inhibition raises warfarin levels,dose_adjust,0.5
7
+ DRUG_APIXABAN,DRUG_NAPROXEN,severe,DOAC + NSAID – high bleeding risk,avoid_combination,0.85
8
+ DRUG_APIXABAN,DRUG_ASPIRIN,moderate,Additive bleeding risk with antiplatelet,monitor_closely,0.5
9
+ DRUG_AMIODARONE,DRUG_DIGOXIN,severe,Amiodarone increases digoxin levels – toxicity risk,dose_adjust,0.8
10
+ DRUG_DIGOXIN,DRUG_SPIRONOLACTONE,moderate,Spironolactone may raise digoxin levels,monitor_closely,0.4
11
+ DRUG_CIPROFLOXACIN,DRUG_METFORMIN,moderate,Fluoroquinolone may cause dysglycemia with metformin,monitor_closely,0.35
12
+ DRUG_DIAZEPAM,DRUG_TRAMADOL,severe,CNS depression – benzodiazepine + opioid,avoid_combination,0.92
13
+ DRUG_ALPRAZOLAM,DRUG_TRAMADOL,severe,CNS depression – benzodiazepine + opioid,avoid_combination,0.91
14
+ DRUG_LISINOPRIL,DRUG_SPIRONOLACTONE,moderate,Hyperkalemia risk – ACE-I + K-sparing diuretic,monitor_closely,0.48
15
+ DRUG_LISINOPRIL,DRUG_NAPROXEN,moderate,"NSAID reduces ACE-I efficacy, renal risk",monitor_closely,0.42
16
+ DRUG_AMLODIPINE,DRUG_SIMVASTATIN,moderate,CYP3A4 interaction increases statin exposure,dose_adjust,0.38
17
+ DRUG_ATORVASTATIN,DRUG_CIPROFLOXACIN,mild,Minor CYP interaction raising statin levels,no_action,0.15
18
+ DRUG_CLOPIDOGREL,DRUG_OMEPRAZOLE,moderate,PPI reduces clopidogrel activation via CYP2C19,dose_adjust,0.45
19
+ DRUG_GLIPIZIDE,DRUG_INSULIN_GLARGINE,moderate,Additive hypoglycemia risk,monitor_closely,0.5
20
+ DRUG_FLUOXETINE,DRUG_TRAMADOL,severe,Serotonin syndrome risk – SSRI + serotonergic opioid,avoid_combination,0.82
21
+ DRUG_AMITRIPTYLINE,DRUG_TRAMADOL,severe,Serotonin syndrome + CNS depression,avoid_combination,0.85
22
+ DRUG_DIGOXIN,DRUG_METOPROLOL,moderate,Additive bradycardia,monitor_closely,0.4
23
+ DRUG_DIGOXIN,DRUG_FUROSEMIDE,moderate,Loop diuretic causes hypokalemia increasing digoxin toxicity risk,monitor_closely,0.45
24
+ DRUG_NAPROXEN,DRUG_PREDNISONE,moderate,GI bleeding risk – corticosteroid + NSAID,monitor_closely,0.5
25
+ DRUG_PREDNISONE,DRUG_WARFARIN,mild,Corticosteroid may alter INR,monitor_closely,0.25
openenv-polypharmacy/data/lookups/drug_metadata.csv ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ drug_id,generic_name,atc_class,is_high_risk_elderly,default_dose_mg,min_dose_mg,max_dose_mg
2
+ DRUG_WARFARIN,warfarin,B01AA,1,5.0,1.0,10.0
3
+ DRUG_APIXABAN,apixaban,B01AF,1,5.0,2.5,10.0
4
+ DRUG_METFORMIN,metformin,A10BA,0,1000,500,2000
5
+ DRUG_GLIPIZIDE,glipizide,A10BB,1,5.0,2.5,20.0
6
+ DRUG_LISINOPRIL,lisinopril,C09AA,0,10.0,2.5,40.0
7
+ DRUG_AMLODIPINE,amlodipine,C08CA,0,5.0,2.5,10.0
8
+ DRUG_METOPROLOL,metoprolol,C07AB,0,50.0,25.0,200.0
9
+ DRUG_DIGOXIN,digoxin,C01AA,1,0.25,0.0625,0.5
10
+ DRUG_FUROSEMIDE,furosemide,C03CA,0,40.0,20.0,160.0
11
+ DRUG_SPIRONOLACTONE,spironolactone,C03DA,0,25.0,12.5,50.0
12
+ DRUG_ATORVASTATIN,atorvastatin,C10AA,0,20.0,10.0,80.0
13
+ DRUG_SIMVASTATIN,simvastatin,C10AA,0,20.0,10.0,40.0
14
+ DRUG_OMEPRAZOLE,omeprazole,A02BC,0,20.0,10.0,40.0
15
+ DRUG_DIAZEPAM,diazepam,N05BA,1,5.0,2.0,10.0
16
+ DRUG_ALPRAZOLAM,alprazolam,N05BA,1,0.5,0.25,2.0
17
+ DRUG_AMITRIPTYLINE,amitriptyline,N06AA,1,25.0,10.0,75.0
18
+ DRUG_INSULIN_GLARGINE,insulin glargine,A10AE,1,20.0,10.0,60.0
19
+ DRUG_PREDNISONE,prednisone,H02AB,0,10.0,5.0,60.0
20
+ DRUG_NAPROXEN,naproxen,M01AE,1,500,250,1000
21
+ DRUG_IBUPROFEN,ibuprofen,M01AE,1,400,200,800
22
+ DRUG_CLOPIDOGREL,clopidogrel,B01AC,0,75.0,75.0,75.0
23
+ DRUG_ASPIRIN,aspirin,B01AC,0,81.0,81.0,325.0
24
+ DRUG_HYDROCHLOROTHIAZIDE,HCTZ,C03AA,0,25.0,12.5,50.0
25
+ DRUG_DONEPEZIL,donepezil,N06DA,0,5.0,5.0,10.0
26
+ DRUG_GABAPENTIN,gabapentin,N03AX,0,300,100,1200
27
+ DRUG_TRAMADOL,tramadol,N02AX,1,50.0,25.0,200.0
28
+ DRUG_FLUOXETINE,fluoxetine,N06AB,0,20.0,10.0,60.0
29
+ DRUG_SERTRALINE,sertraline,N06AB,0,50.0,25.0,200.0
30
+ DRUG_CIPROFLOXACIN,ciprofloxacin,J01MA,0,500,250,750
31
+ DRUG_TAMSULOSIN,tamsulosin,G04CA,0,0.4,0.4,0.8
32
+ DRUG_CELECOXIB,celecoxib,M01AE,0,200,100,400
33
+ DRUG_NORTRIPTYLINE,nortriptyline,N06AA,0,25.0,10.0,75.0
34
+ DRUG_LOSARTAN,losartan,C09AA,0,50.0,25.0,100.0
openenv-polypharmacy/data/processed/patients_polypharmacy.csv ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ episode_id,age,sex,conditions,eGFR_category,liver_function_category,medication_ids,baseline_risk_score,difficulty
2
+ EP_0001,72,F,HTN,moderate,normal,DRUG_WARFARIN;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.264,easy
3
+ EP_0002,67,M,OA;COPD;neuropathy,normal,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE,0.2833,easy
4
+ EP_0003,73,F,HTN;HF,normal,normal,DRUG_IBUPROFEN;DRUG_WARFARIN;DRUG_FUROSEMIDE,0.2933,easy
5
+ EP_0004,74,M,CKD,mild,impaired,DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_DIAZEPAM,0.3067,easy
6
+ EP_0005,76,F,OA;neuropathy;CKD,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE,0.17,easy
7
+ EP_0006,74,M,HTN;OA,normal,impaired,DRUG_IBUPROFEN;DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.44,easy
8
+ EP_0007,90,M,BPH;OA,moderate,normal,DRUG_DIGOXIN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_AMIODARONE,0.16,easy
9
+ EP_0008,77,F,CKD;OA;depression,mild,normal,DRUG_AMITRIPTYLINE;DRUG_IBUPROFEN;DRUG_SERTRALINE;DRUG_TRAMADOL;DRUG_FUROSEMIDE,0.17,easy
10
+ EP_0009,67,M,COPD;GERD;BPH,mild,normal,DRUG_TRAMADOL;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN,0.205,easy
11
+ EP_0010,75,M,dementia;HTN;depression,normal,impaired,DRUG_TRAMADOL;DRUG_DIAZEPAM;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE,0.4425,easy
12
+ EP_0011,83,F,AF,moderate,normal,DRUG_TRAMADOL;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_ALPRAZOLAM,0.2275,easy
13
+ EP_0012,71,F,HTN;GERD;depression,normal,normal,DRUG_LISINOPRIL;DRUG_FLUOXETINE;DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.254,easy
14
+ EP_0013,70,F,HF;HTN;AF,mild,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_ALPRAZOLAM,0.3033,easy
15
+ EP_0014,82,F,dementia,normal,normal,DRUG_DONEPEZIL;DRUG_NAPROXEN;DRUG_APIXABAN;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE,0.17,easy
16
+ EP_0015,84,F,dementia;neuropathy,normal,normal,DRUG_DONEPEZIL;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_CELECOXIB;DRUG_TRAMADOL,0.17,easy
17
+ EP_0016,83,M,HTN,normal,normal,DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_ALPRAZOLAM,0.3033,easy
18
+ EP_0017,83,F,CKD,severe,normal,DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.2833,easy
19
+ EP_0018,70,F,CKD;HF;HTN,mild,normal,DRUG_SPIRONOLACTONE;DRUG_ALPRAZOLAM;DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_METOPROLOL,0.182,easy
20
+ EP_0019,84,M,DM;depression,normal,normal,DRUG_GLIPIZIDE;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_INSULIN_GLARGINE;DRUG_DIAZEPAM,0.448,easy
21
+ EP_0020,90,F,neuropathy;BPH;AF,normal,normal,DRUG_WARFARIN;DRUG_NAPROXEN;DRUG_TAMSULOSIN,0.3,easy
22
+ EP_0021,87,M,HTN;BPH;HF,normal,normal,DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE,0.2125,easy
23
+ EP_0022,90,M,AF;GERD;DM,normal,impaired,DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_OMEPRAZOLE,0.2125,easy
24
+ EP_0023,90,F,HF,normal,normal,DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_METOPROLOL,0.2833,easy
25
+ EP_0024,71,F,OA,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_APIXABAN,0.17,easy
26
+ EP_0025,71,M,COPD;AF;neuropathy,mild,normal,DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_NAPROXEN,0.3,easy
27
+ EP_0026,88,M,GERD;dementia,severe,normal,DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE,0.2125,easy
28
+ EP_0027,76,M,AF,normal,normal,DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_NAPROXEN,0.43,easy
29
+ EP_0028,73,F,CKD,moderate,normal,DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL,0.17,easy
30
+ EP_0029,70,F,CKD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_GABAPENTIN;DRUG_AMLODIPINE;DRUG_DIAZEPAM,0.184,easy
31
+ EP_0030,87,F,dementia;HF;depression,normal,normal,DRUG_WARFARIN;DRUG_DONEPEZIL;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.27,easy
32
+ EP_0031,69,M,HF,severe,normal,DRUG_WARFARIN;DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.36,easy
33
+ EP_0032,89,F,neuropathy,mild,normal,DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_PREDNISONE;DRUG_TRAMADOL,0.2125,easy
34
+ EP_0033,68,F,dementia,mild,impaired,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_SPIRONOLACTONE;DRUG_TRAMADOL;DRUG_ALPRAZOLAM,0.182,easy
35
+ EP_0034,84,F,CKD;HF;HTN,moderate,normal,DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN;DRUG_AMIODARONE,0.2667,easy
36
+ EP_0035,74,M,HTN;DM,normal,impaired,DRUG_IBUPROFEN;DRUG_GLIPIZIDE;DRUG_WARFARIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL,0.176,easy
37
+ EP_0036,80,F,DM;neuropathy;HTN,severe,normal,DRUG_WARFARIN;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN,0.225,easy
38
+ EP_0037,78,M,HF,normal,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_DIAZEPAM;DRUG_LISINOPRIL,0.23,easy
39
+ EP_0038,89,F,HTN;AF,moderate,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_DIAZEPAM,0.3067,easy
40
+ EP_0039,78,F,OA;depression,moderate,normal,DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_SERTRALINE,0.205,easy
41
+ EP_0040,72,F,neuropathy;COPD;BPH,normal,normal,DRUG_TRAMADOL;DRUG_ALPRAZOLAM;DRUG_AMITRIPTYLINE;DRUG_TAMSULOSIN,0.44,easy
42
+ EP_0041,89,F,AF;BPH;DM;HF;HTN,mild,normal,DRUG_GLIPIZIDE;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_INSULIN_GLARGINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN,0.1,medium
43
+ EP_0042,66,F,HTN;AF;CKD,moderate,normal,DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_SERTRALINE,0.173,medium
44
+ EP_0043,70,F,OA;HTN;dementia,moderate,normal,DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_LISINOPRIL;DRUG_METOPROLOL,0.0467,medium
45
+ EP_0044,77,M,HF;HTN;GERD;COPD;neuropathy,normal,normal,DRUG_OMEPRAZOLE;DRUG_AMLODIPINE;DRUG_PREDNISONE;DRUG_LISINOPRIL;DRUG_SPIRONOLACTONE;DRUG_METOPROLOL;DRUG_GABAPENTIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN,0.1422,medium
46
+ EP_0045,78,M,CKD;depression;dementia;GERD;OA,severe,normal,DRUG_FLUOXETINE;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE,0.167,medium
47
+ EP_0046,82,M,BPH;DM;CKD;dementia;HF,moderate,normal,DRUG_GLIPIZIDE;DRUG_INSULIN_GLARGINE;DRUG_METOPROLOL;DRUG_METFORMIN;DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_TAMSULOSIN;DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_LISINOPRIL,0.178,medium
48
+ EP_0047,83,F,depression;HTN;BPH;neuropathy;AF,normal,impaired,DRUG_SERTRALINE;DRUG_GABAPENTIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL;DRUG_LISINOPRIL;DRUG_APIXABAN;DRUG_TAMSULOSIN;DRUG_AMLODIPINE;DRUG_FUROSEMIDE,0.0,medium
49
+ EP_0048,85,F,AF;DM;OA,severe,impaired,DRUG_WARFARIN;DRUG_GLIPIZIDE;DRUG_INSULIN_GLARGINE;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_DIGOXIN;DRUG_METFORMIN;DRUG_IBUPROFEN;DRUG_METOPROLOL,0.268,medium
50
+ EP_0049,65,F,BPH;COPD;neuropathy,normal,normal,DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_PREDNISONE;DRUG_WARFARIN;DRUG_FLUOXETINE;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.2963,medium
51
+ EP_0050,86,M,dementia;depression;OA;neuropathy,mild,impaired,DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_SERTRALINE;DRUG_NAPROXEN;DRUG_DONEPEZIL;DRUG_IBUPROFEN;DRUG_TRAMADOL,0.1214,medium
52
+ EP_0051,90,M,OA;HF;HTN;DM,normal,normal,DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_METFORMIN;DRUG_NAPROXEN,0.085,medium
53
+ EP_0052,70,M,AF;depression;GERD,moderate,normal,DRUG_FLUOXETINE;DRUG_METOPROLOL;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE;DRUG_OMEPRAZOLE;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_DIGOXIN,0.1063,medium
54
+ EP_0053,65,F,HF;DM;GERD;neuropathy;BPH,moderate,impaired,DRUG_INSULIN_GLARGINE;DRUG_GLIPIZIDE;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_SPIRONOLACTONE;DRUG_AMITRIPTYLINE;DRUG_LISINOPRIL;DRUG_GABAPENTIN,0.098,medium
55
+ EP_0054,82,F,OA;neuropathy;AF;DM,mild,normal,DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_APIXABAN;DRUG_INSULIN_GLARGINE;DRUG_IBUPROFEN;DRUG_NAPROXEN,0.1417,medium
56
+ EP_0055,74,M,GERD;HTN;CKD,moderate,normal,DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_SPIRONOLACTONE;DRUG_INSULIN_GLARGINE,0.06,medium
57
+ EP_0056,67,F,HTN;GERD;COPD;AF,moderate,normal,DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_OMEPRAZOLE;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_AMLODIPINE,0.1222,medium
58
+ EP_0057,74,F,DM;HTN;BPH,normal,normal,DRUG_INSULIN_GLARGINE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_GLIPIZIDE;DRUG_METFORMIN;DRUG_FUROSEMIDE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN,0.05,medium
59
+ EP_0058,90,F,AF;OA;BPH,moderate,normal,DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_IBUPROFEN;DRUG_NAPROXEN;DRUG_TAMSULOSIN;DRUG_TRAMADOL;DRUG_APIXABAN;DRUG_CLOPIDOGREL,0.303,medium
60
+ EP_0059,85,F,BPH;HTN;depression;dementia;COPD,mild,normal,DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_SERTRALINE;DRUG_FLUOXETINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_AMITRIPTYLINE,0.0,medium
61
+ EP_0060,80,F,HF;CKD;neuropathy;HTN,moderate,normal,DRUG_AMLODIPINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_SPIRONOLACTONE;DRUG_GABAPENTIN;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_AMITRIPTYLINE;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_ATORVASTATIN,0.173,medium
62
+ EP_0061,90,M,GERD;BPH;HF;HTN;CKD,mild,normal,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_TAMSULOSIN;DRUG_FUROSEMIDE,0.1417,medium
63
+ EP_0062,65,M,neuropathy;COPD;GERD;BPH;AF,moderate,normal,DRUG_PREDNISONE;DRUG_TAMSULOSIN;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_OMEPRAZOLE;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_WARFARIN;DRUG_APIXABAN,0.0722,medium
64
+ EP_0063,76,M,depression;COPD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE;DRUG_FLUOXETINE;DRUG_SERTRALINE;DRUG_GABAPENTIN,0.2386,medium
65
+ EP_0064,88,M,BPH;GERD;COPD,mild,normal,DRUG_OMEPRAZOLE;DRUG_PREDNISONE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_SIMVASTATIN;DRUG_AMLODIPINE;DRUG_ATORVASTATIN;DRUG_ALPRAZOLAM,0.0867,medium
66
+ EP_0065,75,M,HTN;HF;AF,mild,normal,DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN;DRUG_SPIRONOLACTONE;DRUG_DIGOXIN,0.1786,medium
67
+ EP_0066,66,M,HF;dementia;GERD;OA;DM,moderate,normal,DRUG_INSULIN_GLARGINE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_METFORMIN;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE,0.0563,medium
68
+ EP_0067,70,F,CKD;HTN;AF;HF,moderate,normal,DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_AMLODIPINE,0.0,medium
69
+ EP_0068,85,M,depression;dementia;neuropathy;HF,normal,impaired,DRUG_FLUOXETINE;DRUG_SERTRALINE;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_FUROSEMIDE,0.173,medium
70
+ EP_0069,74,F,OA;CKD;AF,mild,normal,DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_AMLODIPINE;DRUG_APIXABAN;DRUG_METOPROLOL;DRUG_TRAMADOL;DRUG_DIGOXIN,0.1889,medium
71
+ EP_0070,75,F,dementia;GERD;COPD;OA,mild,normal,DRUG_DONEPEZIL;DRUG_PREDNISONE;DRUG_OMEPRAZOLE;DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_GABAPENTIN,0.0,medium
72
+ EP_0071,68,M,BPH;DM;COPD;neuropathy,normal,normal,DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_METFORMIN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_PREDNISONE;DRUG_GLIPIZIDE,0.0714,medium
73
+ EP_0072,92,F,CKD;BPH;COPD;AF,normal,normal,DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_APIXABAN,0.1214,medium
74
+ EP_0073,88,F,OA;GERD;HTN;depression,mild,normal,DRUG_AMITRIPTYLINE;DRUG_OMEPRAZOLE;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_SERTRALINE,0.0525,medium
75
+ EP_0074,80,F,neuropathy;OA;CKD;depression,mild,normal,DRUG_AMLODIPINE;DRUG_SERTRALINE;DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN,0.1214,medium
76
+ EP_0075,68,F,dementia;AF;COPD;HTN;neuropathy,mild,normal,DRUG_GABAPENTIN;DRUG_AMLODIPINE;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_DONEPEZIL;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_LISINOPRIL,0.0944,medium
77
+ EP_0076,71,M,HF;DM;dementia,severe,normal,DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_INSULIN_GLARGINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_METFORMIN;DRUG_GLIPIZIDE;DRUG_FUROSEMIDE,0.2287,medium
78
+ EP_0077,75,F,AF;BPH;dementia,mild,impaired,DRUG_METOPROLOL;DRUG_TAMSULOSIN;DRUG_APIXABAN;DRUG_DONEPEZIL;DRUG_DIGOXIN;DRUG_WARFARIN;DRUG_SPIRONOLACTONE,0.1143,medium
79
+ EP_0078,81,F,OA;depression;DM;neuropathy;CKD,normal,normal,DRUG_GLIPIZIDE;DRUG_NAPROXEN;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_SERTRALINE;DRUG_INSULIN_GLARGINE;DRUG_IBUPROFEN,0.0714,medium
80
+ EP_0079,74,F,DM;OA;GERD;CKD,mild,impaired,DRUG_GLIPIZIDE;DRUG_AMLODIPINE;DRUG_INSULIN_GLARGINE;DRUG_METFORMIN;DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_NAPROXEN,0.0714,medium
81
+ EP_0080,72,M,GERD;HF;OA;CKD,normal,normal,DRUG_AMLODIPINE;DRUG_TRAMADOL;DRUG_LISINOPRIL;DRUG_OMEPRAZOLE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_IBUPROFEN,0.1063,medium
82
+ EP_0081,84,M,neuropathy;CKD;depression;OA,normal,impaired,DRUG_FLUOXETINE;DRUG_AMLODIPINE;DRUG_IBUPROFEN;DRUG_NAPROXEN;DRUG_SERTRALINE;DRUG_GABAPENTIN;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_SIMVASTATIN;DRUG_SPIRONOLACTONE;DRUG_DIGOXIN;DRUG_WARFARIN,0.395,hard
83
+ EP_0082,75,M,OA;COPD;neuropathy;CKD;GERD;HTN;depression,severe,impaired,DRUG_SERTRALINE;DRUG_FUROSEMIDE;DRUG_METOPROLOL;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_OMEPRAZOLE;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_PREDNISONE;DRUG_DIGOXIN,0.2293,hard
84
+ EP_0083,82,F,DM;dementia;OA;HF;neuropathy;COPD,moderate,normal,DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_PREDNISONE;DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_WARFARIN,0.2931,hard
85
+ EP_0084,80,F,CKD;neuropathy;COPD;BPH;dementia;HTN;OA,moderate,normal,DRUG_GABAPENTIN;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_PREDNISONE;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_TAMSULOSIN;DRUG_DIGOXIN;DRUG_WARFARIN,0.2767,hard
86
+ EP_0085,79,F,OA;DM;CKD;GERD;BPH;HF;neuropathy,severe,normal,DRUG_OMEPRAZOLE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE;DRUG_METFORMIN;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_GABAPENTIN;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.2008,hard
87
+ EP_0086,82,M,AF;DM;GERD;COPD;OA,moderate,impaired,DRUG_PREDNISONE;DRUG_METFORMIN;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_OMEPRAZOLE;DRUG_INSULIN_GLARGINE;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_GLIPIZIDE;DRUG_METOPROLOL;DRUG_TRAMADOL;DRUG_IBUPROFEN;DRUG_LOSARTAN,0.3057,hard
88
+ EP_0087,90,M,HTN;GERD;DM;AF;CKD,mild,impaired,DRUG_APIXABAN;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_OMEPRAZOLE;DRUG_WARFARIN;DRUG_METOPROLOL;DRUG_INSULIN_GLARGINE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_HYDROCHLOROTHIAZIDE,0.05,hard
89
+ EP_0088,86,F,HF;AF;COPD;HTN;OA;GERD,normal,impaired,DRUG_PREDNISONE;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_APIXABAN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_SPIRONOLACTONE;DRUG_NAPROXEN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_TRAMADOL;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE,0.3687,hard
90
+ EP_0089,75,M,DM;CKD;HTN;HF;BPH;neuropathy;GERD,mild,normal,DRUG_HYDROCHLOROTHIAZIDE;DRUG_AMLODIPINE;DRUG_METFORMIN;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_LISINOPRIL;DRUG_AMITRIPTYLINE;DRUG_GLIPIZIDE;DRUG_GABAPENTIN;DRUG_TAMSULOSIN;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.0417,hard
91
+ EP_0090,87,M,AF;depression;DM;COPD;OA,mild,normal,DRUG_NAPROXEN;DRUG_FLUOXETINE;DRUG_APIXABAN;DRUG_SERTRALINE;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE;DRUG_TRAMADOL;DRUG_PREDNISONE;DRUG_GLIPIZIDE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_METFORMIN;DRUG_METOPROLOL;DRUG_AMITRIPTYLINE,0.4267,hard
92
+ EP_0091,83,M,OA;dementia;GERD;depression;DM;HF,severe,normal,DRUG_GLIPIZIDE;DRUG_DIGOXIN;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_METFORMIN;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_WARFARIN,0.2843,hard
93
+ EP_0092,78,F,CKD;OA;AF;COPD;depression,severe,normal,DRUG_METOPROLOL;DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE;DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_IBUPROFEN;DRUG_DIGOXIN;DRUG_WARFARIN;DRUG_FUROSEMIDE;DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_PREDNISONE,0.4536,hard
94
+ EP_0093,93,F,HTN;DM;BPH;OA;dementia,severe,impaired,DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_METFORMIN;DRUG_FUROSEMIDE;DRUG_INSULIN_GLARGINE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_METOPROLOL;DRUG_GLIPIZIDE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_DIGOXIN,0.1125,hard
95
+ EP_0094,94,F,BPH;GERD;COPD;HF,moderate,impaired,DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_TAMSULOSIN;DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_WARFARIN,0.198,hard
96
+ EP_0095,90,M,HF;neuropathy;COPD;BPH;dementia;DM;CKD,normal,normal,DRUG_GABAPENTIN;DRUG_METFORMIN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_PREDNISONE;DRUG_SPIRONOLACTONE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_SERTRALINE,0.1487,hard
97
+ EP_0096,85,F,DM;COPD;HTN;CKD;depression;dementia,severe,impaired,DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_PREDNISONE;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_DIGOXIN,0.0773,hard
98
+ EP_0097,70,M,BPH;COPD;neuropathy;CKD;GERD;depression,severe,normal,DRUG_PREDNISONE;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_GABAPENTIN;DRUG_TAMSULOSIN;DRUG_FUROSEMIDE;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_IBUPROFEN;DRUG_LOSARTAN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_WARFARIN,0.145,hard
99
+ EP_0098,81,F,COPD;depression;GERD;BPH;OA,severe,impaired,DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_NAPROXEN;DRUG_SERTRALINE;DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_TAMSULOSIN;DRUG_PREDNISONE;DRUG_AMITRIPTYLINE;DRUG_NORTRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_LOSARTAN;DRUG_HYDROCHLOROTHIAZIDE,0.1447,hard
100
+ EP_0099,91,M,neuropathy;OA;AF;CKD,mild,normal,DRUG_TRAMADOL;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_SIMVASTATIN;DRUG_FLUOXETINE;DRUG_INSULIN_GLARGINE,0.4271,hard
101
+ EP_0100,90,F,DM;OA;HTN;HF,mild,impaired,DRUG_INSULIN_GLARGINE;DRUG_SPIRONOLACTONE;DRUG_METFORMIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_DIGOXIN;DRUG_GLIPIZIDE;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE,0.2333,hard
102
+ EP_0101,88,M,BPH;neuropathy;dementia;OA;CKD;DM;HF,severe,impaired,DRUG_AMLODIPINE;DRUG_TAMSULOSIN;DRUG_TRAMADOL;DRUG_GLIPIZIDE;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE;DRUG_LISINOPRIL;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_METFORMIN;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_METOPROLOL;DRUG_GABAPENTIN;DRUG_NAPROXEN,0.2333,hard
103
+ EP_0102,74,M,BPH;HF;dementia;CKD;DM;GERD,moderate,normal,DRUG_OMEPRAZOLE;DRUG_LISINOPRIL;DRUG_INSULIN_GLARGINE;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_DONEPEZIL;DRUG_SPIRONOLACTONE;DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_METFORMIN;DRUG_TAMSULOSIN;DRUG_WARFARIN,0.1715,hard
104
+ EP_0103,70,F,AF;GERD;depression;CKD,mild,impaired,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_APIXABAN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_AMITRIPTYLINE;DRUG_LOSARTAN;DRUG_GLIPIZIDE;DRUG_DIAZEPAM;DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE,0.2593,hard
105
+ EP_0104,84,F,OA;DM;dementia;AF;GERD;COPD;BPH,mild,normal,DRUG_METOPROLOL;DRUG_DONEPEZIL;DRUG_METFORMIN;DRUG_GABAPENTIN;DRUG_APIXABAN;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN;DRUG_NAPROXEN;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_IBUPROFEN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_WARFARIN,0.2521,hard
106
+ EP_0105,88,F,neuropathy;BPH;HTN;COPD;DM,severe,impaired,DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_PREDNISONE;DRUG_GABAPENTIN;DRUG_INSULIN_GLARGINE;DRUG_METFORMIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_WARFARIN,0.0208,hard
107
+ EP_0106,73,M,HTN;DM;CKD;OA;depression,normal,normal,DRUG_LISINOPRIL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE;DRUG_SERTRALINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METFORMIN;DRUG_FLUOXETINE;DRUG_INSULIN_GLARGINE;DRUG_AMLODIPINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_WARFARIN,0.3443,hard
108
+ EP_0107,82,M,HTN;GERD;BPH;depression;AF,mild,impaired,DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_SERTRALINE;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_DIGOXIN;DRUG_APIXABAN;DRUG_ATORVASTATIN;DRUG_CLOPIDOGREL,0.1167,hard
109
+ EP_0108,72,F,DM;dementia;GERD;BPH;neuropathy;OA;depression,severe,normal,DRUG_FLUOXETINE;DRUG_GABAPENTIN;DRUG_TAMSULOSIN;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_METFORMIN;DRUG_SERTRALINE;DRUG_IBUPROFEN;DRUG_INSULIN_GLARGINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_NORTRIPTYLINE;DRUG_WARFARIN,0.2933,hard
110
+ EP_0109,91,M,COPD;dementia;HF;OA;HTN;DM,mild,normal,DRUG_DONEPEZIL;DRUG_GLIPIZIDE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE;DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_METFORMIN;DRUG_WARFARIN,0.2864,hard
111
+ EP_0110,84,M,HF;GERD;dementia;CKD;COPD,severe,impaired,DRUG_DONEPEZIL;DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_PREDNISONE;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_AMITRIPTYLINE;DRUG_ASPIRIN;DRUG_NORTRIPTYLINE;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.1807,hard
112
+ EP_0111,91,F,dementia;CKD;DM;AF;BPH,normal,impaired,DRUG_GLIPIZIDE;DRUG_DONEPEZIL;DRUG_TAMSULOSIN;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE;DRUG_CELECOXIB;DRUG_AMITRIPTYLINE;DRUG_ATORVASTATIN;DRUG_IBUPROFEN,0.1487,hard
113
+ EP_0112,87,F,AF;HF;CKD;neuropathy;HTN;depression,moderate,impaired,DRUG_HYDROCHLOROTHIAZIDE;DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_FLUOXETINE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE;DRUG_METOPROLOL;DRUG_AMITRIPTYLINE;DRUG_APIXABAN;DRUG_SERTRALINE;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE,0.1308,hard
114
+ EP_0113,92,M,OA;HF;COPD;dementia;neuropathy;CKD,moderate,normal,DRUG_AMLODIPINE;DRUG_DONEPEZIL;DRUG_LISINOPRIL;DRUG_GABAPENTIN;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_AMITRIPTYLINE;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.3023,hard
115
+ EP_0114,72,M,depression;COPD;neuropathy;dementia;AF,moderate,normal,DRUG_DIGOXIN;DRUG_APIXABAN;DRUG_SERTRALINE;DRUG_METOPROLOL;DRUG_FLUOXETINE;DRUG_DONEPEZIL;DRUG_AMITRIPTYLINE;DRUG_PREDNISONE;DRUG_WARFARIN;DRUG_GABAPENTIN;DRUG_LISINOPRIL;DRUG_NORTRIPTYLINE;DRUG_AMLODIPINE,0.0846,hard
116
+ EP_0115,75,M,HTN;OA;dementia;HF;depression;CKD;AF,mild,normal,DRUG_WARFARIN;DRUG_SERTRALINE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE;DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_AMITRIPTYLINE;DRUG_DIGOXIN,0.3233,hard
117
+ EP_0116,76,F,OA;depression;neuropathy;HF,severe,impaired,DRUG_FLUOXETINE;DRUG_NAPROXEN;DRUG_FUROSEMIDE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_LISINOPRIL;DRUG_GABAPENTIN;DRUG_DIGOXIN;DRUG_IBUPROFEN;DRUG_SERTRALINE;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.4321,hard
118
+ EP_0117,85,M,depression;GERD;neuropathy;HTN,normal,normal,DRUG_GABAPENTIN;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_METOPROLOL;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_SPIRONOLACTONE;DRUG_IBUPROFEN;DRUG_NAPROXEN;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.2087,hard
119
+ EP_0118,70,F,DM;depression;HTN;HF;AF;neuropathy;CKD,severe,impaired,DRUG_FUROSEMIDE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE;DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_GLIPIZIDE;DRUG_WARFARIN;DRUG_METOPROLOL;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN,0.1833,hard
120
+ EP_0119,86,F,AF;HTN;HF;OA;dementia,normal,impaired,DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_DONEPEZIL;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE,0.2364,hard
121
+ EP_0120,94,F,HF;COPD;dementia;HTN;CKD,mild,normal,DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN;DRUG_LISINOPRIL;DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_WARFARIN,0.165,hard
openenv-polypharmacy/inference.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Baseline LLM inference script for the PolypharmacyEnv.
3
+
4
+ Uses the OpenAI Python client to drive an LLM agent through the
5
+ PolypharmacyEnv HTTP API. Emits structured stdout logs in the
6
+ [START], [STEP], [END] format required by the OpenEnv evaluation spec.
7
+
8
+ Environment variables:
9
+ OPENAI_API_KEY – required
10
+ API_BASE_URL – LLM endpoint (default: https://api.openai.com/v1)
11
+ MODEL_NAME – model to use (default: gpt-4.1)
12
+ HF_TOKEN – HuggingFace token (optional)
13
+ POLYPHARMACY_ENV_URL – environment HTTP base URL (default: http://localhost:7860)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ import time
22
+ import uuid
23
+ from typing import Any, Dict, List
24
+
25
+ import requests
26
+ from openai import OpenAI
27
+
28
+ # ── Configuration ────────────────────────────────────────────────────────────
29
+
30
+ API_KEY = os.environ.get("OPENAI_API_KEY", "")
31
+ API_BASE = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
32
+ MODEL = os.environ.get("MODEL_NAME", "gpt-4.1")
33
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
34
+ ENV_URL = os.environ.get("POLYPHARMACY_ENV_URL", "http://localhost:7860")
35
+
36
+ TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
37
+ EPISODES_PER_TASK = 5
38
+
39
+ client = OpenAI(api_key=API_KEY, base_url=API_BASE)
40
+
41
+ # ── Logging helpers ──────────────────────────────────────────────────────────
42
+
43
+ def _log(tag: str, payload: Dict[str, Any]) -> None:
44
+ print(f"[{tag}] {json.dumps(payload, default=str)}", flush=True)
45
+
46
+
47
+ def _err(msg: str) -> None:
48
+ print(msg, file=sys.stderr, flush=True)
49
+
50
+
51
+ # ── Environment HTTP helpers ─────────────────────────────────────────────────
52
+
53
+ def env_reset(task_id: str) -> Dict[str, Any]:
54
+ resp = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id}, timeout=30)
55
+ resp.raise_for_status()
56
+ return resp.json()
57
+
58
+
59
+ def env_step(action: Dict[str, Any]) -> Dict[str, Any]:
60
+ resp = requests.post(f"{ENV_URL}/step", json={"action": action}, timeout=30)
61
+ resp.raise_for_status()
62
+ return resp.json()
63
+
64
+
65
+ # ── Observation → prompt ─────────────────────────────────────────────────────
66
+
67
+ SYSTEM_PROMPT = """\
68
+ You are a clinical pharmacist AI assistant reviewing an elderly patient's medication regimen.
69
+ You must reduce drug-interaction risk and address Beers-criteria violations while minimising
70
+ unnecessary medication changes.
71
+
72
+ Available actions (respond with STRICT JSON, no extra text):
73
+ 1. Query a drug pair for interactions:
74
+ {"action_type": "query_ddi", "drug_id_1": "...", "drug_id_2": "..."}
75
+
76
+ 2. Propose an intervention:
77
+ {"action_type": "propose_intervention", "target_drug_id": "...",
78
+ "intervention_type": "stop|dose_reduce|substitute|add_monitoring",
79
+ "proposed_new_drug_id": "...(optional)", "rationale": "..."}
80
+
81
+ 3. Finish the review:
82
+ {"action_type": "finish_review"}
83
+
84
+ Respond with EXACTLY ONE JSON object per turn. No markdown, no explanation outside JSON.
85
+ """
86
+
87
+
88
+ def _summarise_obs(obs: Dict[str, Any]) -> str:
89
+ meds = obs.get("current_medications", [])
90
+ med_summary = "; ".join(
91
+ f"{m['drug_id']}({m['generic_name']},{m['dose_mg']}mg)"
92
+ for m in meds
93
+ )
94
+ queries = obs.get("interaction_queries", [])
95
+ q_summary = "; ".join(
96
+ f"{q['drug_id_1']}+{q['drug_id_2']}={q.get('severity','?')}"
97
+ for q in queries
98
+ )
99
+ interventions = obs.get("interventions", [])
100
+ iv_summary = "; ".join(
101
+ f"{iv['action_type']}({iv['target_drug_id']})"
102
+ for iv in interventions
103
+ )
104
+ return (
105
+ f"Patient: age={obs.get('age')}, sex={obs.get('sex')}, "
106
+ f"conditions={obs.get('conditions')}, "
107
+ f"eGFR={obs.get('eGFR_category')}, liver={obs.get('liver_function_category')}\n"
108
+ f"Medications: {med_summary}\n"
109
+ f"Queries so far: {q_summary or 'none'}\n"
110
+ f"Interventions so far: {iv_summary or 'none'}\n"
111
+ f"Remaining query budget: {obs.get('remaining_query_budget')}\n"
112
+ f"Remaining intervention budget: {obs.get('remaining_intervention_budget')}\n"
113
+ f"Step: {obs.get('step_index')}"
114
+ )
115
+
116
+
117
+ # ── LLM call ─────────────────────────────────────────────────────────────────
118
+
119
+ def _ask_llm(obs_summary: str) -> Dict[str, Any]:
120
+ """Call the LLM and parse a PolypharmacyAction JSON."""
121
+ try:
122
+ resp = client.chat.completions.create(
123
+ model=MODEL,
124
+ messages=[
125
+ {"role": "system", "content": SYSTEM_PROMPT},
126
+ {"role": "user", "content": obs_summary},
127
+ ],
128
+ temperature=0.2,
129
+ max_tokens=256,
130
+ )
131
+ text = resp.choices[0].message.content or ""
132
+ # Strip markdown fences if present
133
+ text = text.strip()
134
+ if text.startswith("```"):
135
+ text = text.split("\n", 1)[-1]
136
+ if text.endswith("```"):
137
+ text = text.rsplit("```", 1)[0]
138
+ text = text.strip()
139
+ return json.loads(text)
140
+ except Exception as e:
141
+ _err(f"LLM parse error: {e}")
142
+ return {"action_type": "finish_review"}
143
+
144
+
145
+ # ── Main loop ────────────────────────────────────────────────────────────────
146
+
147
+ def main() -> None:
148
+ if not API_KEY:
149
+ _err("OPENAI_API_KEY is required")
150
+ sys.exit(1)
151
+
152
+ run_id = str(uuid.uuid4())[:8]
153
+
154
+ for task_id in TASKS:
155
+ task_scores: List[float] = []
156
+ task_rewards: List[float] = []
157
+
158
+ _log("START", {
159
+ "run_id": run_id,
160
+ "task_id": task_id,
161
+ "model": MODEL,
162
+ "api_base": API_BASE,
163
+ "episodes": EPISODES_PER_TASK,
164
+ })
165
+
166
+ for ep_idx in range(EPISODES_PER_TASK):
167
+ reset_resp = env_reset(task_id)
168
+ obs = reset_resp["observation"]
169
+ done = reset_resp.get("done", False)
170
+ episode_id = obs.get("episode_id", f"ep_{ep_idx}")
171
+ total_reward = 0.0
172
+ step_idx = 0
173
+
174
+ while not done:
175
+ obs_summary = _summarise_obs(obs)
176
+ action_payload = _ask_llm(obs_summary)
177
+
178
+ step_resp = env_step(action_payload)
179
+ obs = step_resp["observation"]
180
+ reward = step_resp.get("reward", 0.0)
181
+ done = step_resp.get("done", False)
182
+ total_reward += reward
183
+
184
+ _log("STEP", {
185
+ "run_id": run_id,
186
+ "task_id": task_id,
187
+ "episode_id": episode_id,
188
+ "step_index": step_idx,
189
+ "observation_summary": obs_summary[:200],
190
+ "action_payload": action_payload,
191
+ "reward": reward,
192
+ "done": done,
193
+ })
194
+
195
+ step_idx += 1
196
+
197
+ grader_score = step_resp.get("info", {}).get("grader_score", 0.0)
198
+ task_scores.append(grader_score)
199
+ task_rewards.append(total_reward)
200
+
201
+ _log("END", {
202
+ "run_id": run_id,
203
+ "task_id": task_id,
204
+ "episodes": EPISODES_PER_TASK,
205
+ "avg_grader_score": sum(task_scores) / max(len(task_scores), 1),
206
+ "avg_total_reward": sum(task_rewards) / max(len(task_rewards), 1),
207
+ "per_episode_scores": task_scores,
208
+ })
209
+
210
+ _err("Inference complete.")
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()
openenv-polypharmacy/openenv.yaml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: polypharmacy_env
3
+ version: "0.1.0"
4
+ description: >
5
+ An OpenEnv environment that simulates elderly polypharmacy medication review.
6
+ An RL agent acts as a clinical pharmacist assistant, identifying dangerous
7
+ drug–drug interactions, Beers-criteria violations, and proposing safe
8
+ interventions (stop, dose-reduce, substitute, monitor).
9
+ author: "PolypharmacyEnv Team"
10
+ tags:
11
+ - healthcare
12
+ - polypharmacy
13
+ - openenv
14
+ type: space
15
+ runtime: fastapi
16
+ app: src.polypharmacy_env.api.server:app
17
+ port: 7860
18
+
19
+ tasks:
20
+ - id: easy_screening
21
+ description: "Small regimen (3-5 drugs) with one severe DDI. Identify and resolve it."
22
+ difficulty: easy
23
+
24
+ - id: budgeted_screening
25
+ description: "Medium regimen (6-10 drugs) with multiple DDIs and Beers issues under query/intervention budgets."
26
+ difficulty: medium
27
+
28
+ - id: complex_tradeoff
29
+ description: "Large regimen (10-15 drugs) including critical drugs. Balance risk reduction against regimen disruption."
30
+ difficulty: hard
openenv-polypharmacy/pyproject.toml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "polypharmacy-env"
7
+ version = "0.1.0"
8
+ description = "OpenEnv environment for elderly polypharmacy medication-review safety"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "fastapi>=0.104.0",
12
+ "uvicorn>=0.24.0",
13
+ "pydantic>=2.0.0",
14
+ "requests>=2.31.0",
15
+ "openai>=1.0.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=7.0.0",
21
+ "httpx>=0.25.0",
22
+ "black",
23
+ "isort",
24
+ ]
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["src/polypharmacy_env/tests"]
31
+ pythonpath = ["src"]
32
+
33
+ [tool.black]
34
+ line-length = 99
35
+ target-version = ["py310"]
36
+
37
+ [tool.isort]
38
+ profile = "black"
39
+ line_length = 99
openenv-polypharmacy/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.0
2
+ uvicorn>=0.24.0
3
+ pydantic>=2.0.0
4
+ requests>=2.31.0
5
+ openai>=1.0.0
6
+ httpx>=0.25.0
7
+ pytest>=7.0.0
openenv-polypharmacy/scripts/preprocess_data.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic data generator for the PolypharmacyEnv.
2
+
3
+ Generates:
4
+ - data/lookups/drug_metadata.csv
5
+ - data/lookups/ddi_rules.csv
6
+ - data/lookups/beers_criteria.csv
7
+ - data/processed/patients_polypharmacy.csv
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import csv
13
+ import random
14
+ import sys
15
+ from itertools import combinations
16
+ from pathlib import Path
17
+
18
+ ROOT = Path(__file__).resolve().parents[1]
19
+ LOOKUPS = ROOT / "data" / "lookups"
20
+ PROCESSED = ROOT / "data" / "processed"
21
+
22
+ # ── Drug catalogue ───────────────────────────────────────────────────────────
23
+
24
+ DRUGS = [
25
+ # drug_id, generic_name, atc_class, high_risk, default, min, max
26
+ ("DRUG_WARFARIN", "warfarin", "B01AA", 1, 5.0, 1.0, 10.0),
27
+ ("DRUG_APIXABAN", "apixaban", "B01AF", 1, 5.0, 2.5, 10.0),
28
+ ("DRUG_METFORMIN", "metformin", "A10BA", 0, 1000, 500, 2000),
29
+ ("DRUG_GLIPIZIDE", "glipizide", "A10BB", 1, 5.0, 2.5, 20.0),
30
+ ("DRUG_LISINOPRIL", "lisinopril", "C09AA", 0, 10.0, 2.5, 40.0),
31
+ ("DRUG_AMLODIPINE", "amlodipine", "C08CA", 0, 5.0, 2.5, 10.0),
32
+ ("DRUG_METOPROLOL", "metoprolol", "C07AB", 0, 50.0, 25.0,200.0),
33
+ ("DRUG_DIGOXIN", "digoxin", "C01AA", 1, 0.25, 0.0625,0.5),
34
+ ("DRUG_FUROSEMIDE", "furosemide", "C03CA", 0, 40.0, 20.0,160.0),
35
+ ("DRUG_SPIRONOLACTONE", "spironolactone", "C03DA", 0, 25.0, 12.5, 50.0),
36
+ ("DRUG_ATORVASTATIN", "atorvastatin", "C10AA", 0, 20.0, 10.0, 80.0),
37
+ ("DRUG_SIMVASTATIN", "simvastatin", "C10AA", 0, 20.0, 10.0, 40.0),
38
+ ("DRUG_OMEPRAZOLE", "omeprazole", "A02BC", 0, 20.0, 10.0, 40.0),
39
+ ("DRUG_DIAZEPAM", "diazepam", "N05BA", 1, 5.0, 2.0, 10.0),
40
+ ("DRUG_ALPRAZOLAM", "alprazolam", "N05BA", 1, 0.5, 0.25, 2.0),
41
+ ("DRUG_AMITRIPTYLINE", "amitriptyline", "N06AA", 1, 25.0, 10.0, 75.0),
42
+ ("DRUG_INSULIN_GLARGINE","insulin glargine", "A10AE", 1, 20.0, 10.0, 60.0),
43
+ ("DRUG_PREDNISONE", "prednisone", "H02AB", 0, 10.0, 5.0, 60.0),
44
+ ("DRUG_NAPROXEN", "naproxen", "M01AE", 1, 500, 250, 1000),
45
+ ("DRUG_IBUPROFEN", "ibuprofen", "M01AE", 1, 400, 200, 800),
46
+ ("DRUG_CLOPIDOGREL", "clopidogrel", "B01AC", 0, 75.0, 75.0, 75.0),
47
+ ("DRUG_ASPIRIN", "aspirin", "B01AC", 0, 81.0, 81.0, 325.0),
48
+ ("DRUG_HYDROCHLOROTHIAZIDE","HCTZ", "C03AA", 0, 25.0, 12.5, 50.0),
49
+ ("DRUG_DONEPEZIL", "donepezil", "N06DA", 0, 5.0, 5.0, 10.0),
50
+ ("DRUG_GABAPENTIN", "gabapentin", "N03AX", 0, 300, 100, 1200),
51
+ ("DRUG_TRAMADOL", "tramadol", "N02AX", 1, 50.0, 25.0, 200.0),
52
+ ("DRUG_FLUOXETINE", "fluoxetine", "N06AB", 0, 20.0, 10.0, 60.0),
53
+ ("DRUG_SERTRALINE", "sertraline", "N06AB", 0, 50.0, 25.0, 200.0),
54
+ ("DRUG_CIPROFLOXACIN", "ciprofloxacin", "J01MA", 0, 500, 250, 750),
55
+ ("DRUG_TAMSULOSIN", "tamsulosin", "G04CA", 0, 0.4, 0.4, 0.8),
56
+ ("DRUG_CELECOXIB", "celecoxib", "M01AE", 0, 200, 100, 400),
57
+ ("DRUG_NORTRIPTYLINE", "nortriptyline", "N06AA", 0, 25.0, 10.0, 75.0),
58
+ ("DRUG_LOSARTAN", "losartan", "C09AA", 0, 50.0, 25.0, 100.0),
59
+ ]
60
+
61
+ # ── DDI rules ────────────────────────────────────────────────────────────────
62
+
63
+ DDI_PAIRS: list[tuple[str, str, str, str, str, float]] = [
64
+ # id1, id2, severity, mechanism, recommendation, base_risk_score
65
+ ("DRUG_WARFARIN", "DRUG_NAPROXEN", "severe", "Increased bleeding risk – NSAID inhibits platelet + anticoagulant", "avoid_combination", 0.90),
66
+ ("DRUG_WARFARIN", "DRUG_IBUPROFEN", "severe", "Increased bleeding risk – NSAID + anticoagulant synergy", "avoid_combination", 0.88),
67
+ ("DRUG_WARFARIN", "DRUG_ASPIRIN", "moderate", "Additive antiplatelet + anticoagulant bleeding risk", "monitor_closely", 0.55),
68
+ ("DRUG_WARFARIN", "DRUG_FLUOXETINE", "moderate", "SSRI increases serotonin and may potentiate bleeding", "monitor_closely", 0.45),
69
+ ("DRUG_WARFARIN", "DRUG_CIPROFLOXACIN","moderate","CYP1A2 inhibition raises warfarin levels", "dose_adjust", 0.50),
70
+ ("DRUG_APIXABAN", "DRUG_NAPROXEN", "severe", "DOAC + NSAID – high bleeding risk", "avoid_combination", 0.85),
71
+ ("DRUG_APIXABAN", "DRUG_ASPIRIN", "moderate", "Additive bleeding risk with antiplatelet", "monitor_closely", 0.50),
72
+ ("DRUG_DIGOXIN", "DRUG_AMIODARONE", "severe", "Amiodarone increases digoxin levels – toxicity risk", "dose_adjust", 0.80),
73
+ ("DRUG_DIGOXIN", "DRUG_SPIRONOLACTONE","moderate","Spironolactone may raise digoxin levels", "monitor_closely", 0.40),
74
+ ("DRUG_METFORMIN", "DRUG_CIPROFLOXACIN","moderate","Fluoroquinolone may cause dysglycemia with metformin", "monitor_closely", 0.35),
75
+ ("DRUG_DIAZEPAM", "DRUG_TRAMADOL", "severe", "CNS depression – benzodiazepine + opioid", "avoid_combination", 0.92),
76
+ ("DRUG_ALPRAZOLAM", "DRUG_TRAMADOL", "severe", "CNS depression – benzodiazepine + opioid", "avoid_combination", 0.91),
77
+ ("DRUG_LISINOPRIL", "DRUG_SPIRONOLACTONE","moderate","Hyperkalemia risk – ACE-I + K-sparing diuretic", "monitor_closely", 0.48),
78
+ ("DRUG_LISINOPRIL", "DRUG_NAPROXEN", "moderate", "NSAID reduces ACE-I efficacy, renal risk", "monitor_closely", 0.42),
79
+ ("DRUG_SIMVASTATIN","DRUG_AMLODIPINE", "moderate", "CYP3A4 interaction increases statin exposure", "dose_adjust", 0.38),
80
+ ("DRUG_ATORVASTATIN","DRUG_CIPROFLOXACIN","mild", "Minor CYP interaction raising statin levels", "no_action", 0.15),
81
+ ("DRUG_CLOPIDOGREL","DRUG_OMEPRAZOLE", "moderate", "PPI reduces clopidogrel activation via CYP2C19", "dose_adjust", 0.45),
82
+ ("DRUG_INSULIN_GLARGINE","DRUG_GLIPIZIDE","moderate","Additive hypoglycemia risk", "monitor_closely", 0.50),
83
+ ("DRUG_FLUOXETINE", "DRUG_TRAMADOL", "severe", "Serotonin syndrome risk – SSRI + serotonergic opioid", "avoid_combination", 0.82),
84
+ ("DRUG_AMITRIPTYLINE","DRUG_TRAMADOL", "severe", "Serotonin syndrome + CNS depression", "avoid_combination", 0.85),
85
+ ("DRUG_METOPROLOL", "DRUG_DIGOXIN", "moderate", "Additive bradycardia", "monitor_closely", 0.40),
86
+ ("DRUG_FUROSEMIDE", "DRUG_DIGOXIN", "moderate", "Loop diuretic causes hypokalemia increasing digoxin toxicity risk", "monitor_closely", 0.45),
87
+ ("DRUG_PREDNISONE", "DRUG_NAPROXEN", "moderate", "GI bleeding risk – corticosteroid + NSAID", "monitor_closely", 0.50),
88
+ ("DRUG_PREDNISONE", "DRUG_WARFARIN", "mild", "Corticosteroid may alter INR", "monitor_closely", 0.25),
89
+ ]
90
+
91
+ # ── Beers criteria ───────────────────────────────────────────────────────────
92
+
93
+ BEERS_ENTRIES: list[tuple[str, str, str | None, str]] = [
94
+ # drug_id, criterion_type, condition, rationale
95
+ ("DRUG_DIAZEPAM", "avoid", None, "Long-acting benzodiazepine: falls, fractures, cognitive impairment in elderly"),
96
+ ("DRUG_ALPRAZOLAM", "avoid", None, "Benzodiazepine: falls, fractures, cognitive impairment in elderly"),
97
+ ("DRUG_AMITRIPTYLINE", "avoid", None, "Strongly anticholinergic TCA: sedation, confusion, urinary retention in elderly"),
98
+ ("DRUG_GLIPIZIDE", "caution", None, "Sulfonylurea: hypoglycemia risk higher in elderly"),
99
+ ("DRUG_NAPROXEN", "avoid", "CKD", "NSAID contraindicated in CKD – renal deterioration, fluid retention"),
100
+ ("DRUG_IBUPROFEN", "avoid", "CKD", "NSAID contraindicated in CKD – renal deterioration, fluid retention"),
101
+ ("DRUG_NAPROXEN", "caution", None, "NSAID: GI bleeding and renal risk in elderly"),
102
+ ("DRUG_IBUPROFEN", "caution", None, "NSAID: GI bleeding and renal risk in elderly"),
103
+ ("DRUG_DIGOXIN", "dose_adjust", None, "Avoid doses > 0.125 mg/day in elderly – toxicity risk"),
104
+ ("DRUG_TRAMADOL", "avoid", None, "Opioid: CNS depression, falls, constipation in elderly"),
105
+ ("DRUG_METFORMIN", "dose_adjust", "CKD", "Reduce dose or avoid if eGFR < 30 – lactic acidosis risk"),
106
+ ("DRUG_INSULIN_GLARGINE","caution", None, "Tight glycemic control increases hypoglycemia risk in elderly"),
107
+ ("DRUG_PREDNISONE", "avoid_in_condition", "DM", "Corticosteroid worsens glycemic control in diabetes"),
108
+ ("DRUG_DONEPEZIL", "avoid_in_condition", "dementia", "Limited benefit, GI side effects; reassess regularly"),
109
+ ("DRUG_CIPROFLOXACIN", "caution", None, "Fluoroquinolone: tendon rupture, QT prolongation risk in elderly"),
110
+ ]
111
+
112
+ # ── Conditions pool & constraints ────────────────────────────────────────────
113
+
114
+ ALL_CONDITIONS = ["HTN", "DM", "HF", "CKD", "AF", "COPD", "OA", "depression", "dementia", "GERD", "BPH", "neuropathy"]
115
+ EGFR_CATS = ["normal", "mild", "moderate", "severe"]
116
+ LIVER_CATS = ["normal", "impaired"]
117
+
118
+ # Drugs that make clinical sense per condition
119
+ CONDITION_DRUG_MAP: dict[str, list[str]] = {
120
+ "HTN": ["DRUG_LISINOPRIL", "DRUG_AMLODIPINE", "DRUG_METOPROLOL", "DRUG_HYDROCHLOROTHIAZIDE", "DRUG_FUROSEMIDE"],
121
+ "DM": ["DRUG_METFORMIN", "DRUG_GLIPIZIDE", "DRUG_INSULIN_GLARGINE"],
122
+ "HF": ["DRUG_FUROSEMIDE", "DRUG_SPIRONOLACTONE", "DRUG_METOPROLOL", "DRUG_LISINOPRIL", "DRUG_DIGOXIN"],
123
+ "CKD": ["DRUG_FUROSEMIDE", "DRUG_AMLODIPINE"],
124
+ "AF": ["DRUG_WARFARIN", "DRUG_APIXABAN", "DRUG_METOPROLOL", "DRUG_DIGOXIN"],
125
+ "COPD": ["DRUG_PREDNISONE"],
126
+ "OA": ["DRUG_NAPROXEN", "DRUG_IBUPROFEN", "DRUG_TRAMADOL", "DRUG_GABAPENTIN"],
127
+ "depression": ["DRUG_FLUOXETINE", "DRUG_SERTRALINE", "DRUG_AMITRIPTYLINE"],
128
+ "dementia": ["DRUG_DONEPEZIL"],
129
+ "GERD": ["DRUG_OMEPRAZOLE"],
130
+ "BPH": ["DRUG_TAMSULOSIN"],
131
+ "neuropathy": ["DRUG_GABAPENTIN", "DRUG_AMITRIPTYLINE"],
132
+ }
133
+
134
+
135
+ def _normalise_pair(a: str, b: str) -> tuple[str, str]:
136
+ return (a, b) if a < b else (b, a)
137
+
138
+
139
+ def _gen_drug_metadata(out: Path) -> None:
140
+ out.parent.mkdir(parents=True, exist_ok=True)
141
+ with open(out, "w", newline="") as f:
142
+ w = csv.writer(f)
143
+ w.writerow(["drug_id", "generic_name", "atc_class", "is_high_risk_elderly",
144
+ "default_dose_mg", "min_dose_mg", "max_dose_mg"])
145
+ for row in DRUGS:
146
+ w.writerow(row)
147
+
148
+
149
+ def _gen_ddi_rules(out: Path) -> None:
150
+ out.parent.mkdir(parents=True, exist_ok=True)
151
+ with open(out, "w", newline="") as f:
152
+ w = csv.writer(f)
153
+ w.writerow(["drug_id_1", "drug_id_2", "severity", "mechanism",
154
+ "recommendation", "base_risk_score"])
155
+ for pair in DDI_PAIRS:
156
+ a, b = _normalise_pair(pair[0], pair[1])
157
+ w.writerow([a, b, pair[2], pair[3], pair[4], pair[5]])
158
+
159
+
160
+ def _gen_beers(out: Path) -> None:
161
+ out.parent.mkdir(parents=True, exist_ok=True)
162
+ with open(out, "w", newline="") as f:
163
+ w = csv.writer(f)
164
+ w.writerow(["drug_id", "criterion_type", "condition", "rationale"])
165
+ for row in BEERS_ENTRIES:
166
+ w.writerow([row[0], row[1], row[2] or "", row[3]])
167
+
168
+
169
+ def _gen_patients(out: Path, n_easy: int = 40, n_med: int = 40, n_hard: int = 40) -> None:
170
+ """Generate synthetic patient episodes tagged by difficulty."""
171
+ out.parent.mkdir(parents=True, exist_ok=True)
172
+ rng = random.Random(42)
173
+ drug_ids = [d[0] for d in DRUGS]
174
+
175
+ # Build severity lookup for quick reference
176
+ severe_pairs: set[tuple[str, str]] = set()
177
+ for pair in DDI_PAIRS:
178
+ if pair[2] == "severe":
179
+ severe_pairs.add(_normalise_pair(pair[0], pair[1]))
180
+
181
+ rows: list[list[str]] = []
182
+ ep_counter = 0
183
+
184
+ def _pick_conditions(n: int) -> list[str]:
185
+ return rng.sample(ALL_CONDITIONS, min(n, len(ALL_CONDITIONS)))
186
+
187
+ def _drugs_for_conditions(conds: list[str], target_n: int) -> list[str]:
188
+ pool: list[str] = []
189
+ for c in conds:
190
+ pool.extend(CONDITION_DRUG_MAP.get(c, []))
191
+ pool = list(dict.fromkeys(pool)) # deduplicate preserving order
192
+ rng.shuffle(pool)
193
+ selected = pool[:target_n]
194
+ # Pad with random drugs if needed
195
+ remaining = [d for d in drug_ids if d not in selected]
196
+ while len(selected) < target_n and remaining:
197
+ pick = rng.choice(remaining)
198
+ remaining.remove(pick)
199
+ selected.append(pick)
200
+ return selected
201
+
202
+ def _count_severe(meds: list[str]) -> int:
203
+ count = 0
204
+ for a, b in combinations(meds, 2):
205
+ if _normalise_pair(a, b) in severe_pairs:
206
+ count += 1
207
+ return count
208
+
209
+ def _baseline_risk(meds: list[str]) -> float:
210
+ risk = 0.0
211
+ for pair in DDI_PAIRS:
212
+ a, b = _normalise_pair(pair[0], pair[1])
213
+ if a in meds and b in meds:
214
+ risk += pair[5]
215
+ return min(risk / max(len(meds), 1), 1.0)
216
+
217
+ # Easy episodes: 3-5 drugs, exactly 1 severe DDI
218
+ for _ in range(n_easy):
219
+ ep_counter += 1
220
+ n_drugs = rng.randint(3, 5)
221
+ conds = _pick_conditions(rng.randint(1, 3))
222
+ # Ensure at least one severe DDI pair is present
223
+ for attempt in range(50):
224
+ meds = _drugs_for_conditions(conds, n_drugs)
225
+ if _count_severe(meds) >= 1:
226
+ break
227
+ else:
228
+ # Force a known severe pair
229
+ sp = rng.choice(list(severe_pairs))
230
+ meds = list(set(meds[:n_drugs - 2]) | {sp[0], sp[1]})[:n_drugs]
231
+
232
+ age = rng.randint(65, 90)
233
+ sex = rng.choice(["M", "F"])
234
+ egfr = rng.choices(EGFR_CATS, weights=[4, 3, 2, 1])[0]
235
+ liver = rng.choices(LIVER_CATS, weights=[8, 2])[0]
236
+ br = round(_baseline_risk(meds), 4)
237
+ rows.append([
238
+ f"EP_{ep_counter:04d}", str(age), sex, ";".join(conds),
239
+ egfr, liver, ";".join(meds), str(br), "easy",
240
+ ])
241
+
242
+ # Medium episodes: 6-10 drugs, multiple DDIs
243
+ for _ in range(n_med):
244
+ ep_counter += 1
245
+ n_drugs = rng.randint(6, 10)
246
+ conds = _pick_conditions(rng.randint(3, 5))
247
+ meds = _drugs_for_conditions(conds, n_drugs)
248
+ age = rng.randint(65, 92)
249
+ sex = rng.choice(["M", "F"])
250
+ egfr = rng.choices(EGFR_CATS, weights=[3, 3, 3, 1])[0]
251
+ liver = rng.choices(LIVER_CATS, weights=[7, 3])[0]
252
+ br = round(_baseline_risk(meds), 4)
253
+ rows.append([
254
+ f"EP_{ep_counter:04d}", str(age), sex, ";".join(conds),
255
+ egfr, liver, ";".join(meds), str(br), "medium",
256
+ ])
257
+
258
+ # Hard episodes: 10-15 drugs, many issues, include critical drugs
259
+ for _ in range(n_hard):
260
+ ep_counter += 1
261
+ n_drugs = rng.randint(10, 15)
262
+ conds = _pick_conditions(rng.randint(4, 7))
263
+ meds = _drugs_for_conditions(conds, n_drugs)
264
+ # Ensure some critical drugs are present
265
+ critical = ["DRUG_WARFARIN", "DRUG_INSULIN_GLARGINE", "DRUG_DIGOXIN"]
266
+ for cd in rng.sample(critical, min(2, len(critical))):
267
+ if cd not in meds and len(meds) < 15:
268
+ meds.append(cd)
269
+ age = rng.randint(70, 95)
270
+ sex = rng.choice(["M", "F"])
271
+ egfr = rng.choices(EGFR_CATS, weights=[2, 2, 3, 3])[0]
272
+ liver = rng.choices(LIVER_CATS, weights=[6, 4])[0]
273
+ br = round(_baseline_risk(meds), 4)
274
+ rows.append([
275
+ f"EP_{ep_counter:04d}", str(age), sex, ";".join(conds),
276
+ egfr, liver, ";".join(meds), str(br), "hard",
277
+ ])
278
+
279
+ with open(out, "w", newline="") as f:
280
+ w = csv.writer(f)
281
+ w.writerow(["episode_id", "age", "sex", "conditions", "eGFR_category",
282
+ "liver_function_category", "medication_ids",
283
+ "baseline_risk_score", "difficulty"])
284
+ for r in rows:
285
+ w.writerow(r)
286
+
287
+
288
+ def main() -> None:
289
+ print("Generating drug_metadata.csv …")
290
+ _gen_drug_metadata(LOOKUPS / "drug_metadata.csv")
291
+ print("Generating ddi_rules.csv …")
292
+ _gen_ddi_rules(LOOKUPS / "ddi_rules.csv")
293
+ print("Generating beers_criteria.csv …")
294
+ _gen_beers(LOOKUPS / "beers_criteria.csv")
295
+ print("Generating patients_polypharmacy.csv …")
296
+ _gen_patients(PROCESSED / "patients_polypharmacy.csv")
297
+ print("Done.")
298
+
299
+
300
+ if __name__ == "__main__":
301
+ main()
openenv-polypharmacy/scripts/run_validation.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Run validation: tests, server smoke test, and heuristic baseline
3
+ set -euo pipefail
4
+
5
+ cd "$(dirname "$0")/.."
6
+
7
+ echo "=== Running unit tests ==="
8
+ PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
9
+
10
+ echo ""
11
+ echo "=== Running heuristic baseline ==="
12
+ PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
13
+
14
+ echo ""
15
+ echo "=== Validation complete ==="
openenv-polypharmacy/src/polypharmacy_env.egg-info/PKG-INFO ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: polypharmacy-env
3
+ Version: 0.1.0
4
+ Summary: OpenEnv environment for elderly polypharmacy medication-review safety
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: fastapi>=0.104.0
7
+ Requires-Dist: uvicorn>=0.24.0
8
+ Requires-Dist: pydantic>=2.0.0
9
+ Requires-Dist: requests>=2.31.0
10
+ Requires-Dist: openai>=1.0.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
13
+ Requires-Dist: httpx>=0.25.0; extra == "dev"
14
+ Requires-Dist: black; extra == "dev"
15
+ Requires-Dist: isort; extra == "dev"
openenv-polypharmacy/src/polypharmacy_env.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ pyproject.toml
3
+ src/polypharmacy_env/__init__.py
4
+ src/polypharmacy_env/config.py
5
+ src/polypharmacy_env/data_loader.py
6
+ src/polypharmacy_env/ddi_simulator.py
7
+ src/polypharmacy_env/env_core.py
8
+ src/polypharmacy_env/graders.py
9
+ src/polypharmacy_env/models.py
10
+ src/polypharmacy_env/rewards.py
11
+ src/polypharmacy_env/tasks.py
12
+ src/polypharmacy_env.egg-info/PKG-INFO
13
+ src/polypharmacy_env.egg-info/SOURCES.txt
14
+ src/polypharmacy_env.egg-info/dependency_links.txt
15
+ src/polypharmacy_env.egg-info/requires.txt
16
+ src/polypharmacy_env.egg-info/top_level.txt
17
+ src/polypharmacy_env/api/__init__.py
18
+ src/polypharmacy_env/api/schemas.py
19
+ src/polypharmacy_env/api/server.py
20
+ src/polypharmacy_env/baselines/__init__.py
21
+ src/polypharmacy_env/baselines/heuristic_agent.py
22
+ src/polypharmacy_env/baselines/random_agent.py
23
+ src/polypharmacy_env/tests/__init__.py
24
+ src/polypharmacy_env/tests/test_api.py
25
+ src/polypharmacy_env/tests/test_env_core.py
openenv-polypharmacy/src/polypharmacy_env.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
openenv-polypharmacy/src/polypharmacy_env.egg-info/requires.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.0
2
+ uvicorn>=0.24.0
3
+ pydantic>=2.0.0
4
+ requests>=2.31.0
5
+ openai>=1.0.0
6
+
7
+ [dev]
8
+ pytest>=7.0.0
9
+ httpx>=0.25.0
10
+ black
11
+ isort
openenv-polypharmacy/src/polypharmacy_env.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ polypharmacy_env
openenv-polypharmacy/src/polypharmacy_env/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PolypharmacyEnv – an OpenEnv environment for elderly polypharmacy safety."""
openenv-polypharmacy/src/polypharmacy_env/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API package."""
openenv-polypharmacy/src/polypharmacy_env/api/schemas.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP request/response schemas for the OpenEnv-compliant API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ResetRequest(BaseModel):
11
+ task_id: Optional[str] = None
12
+ seed: Optional[int] = Field(default=None, ge=0)
13
+ episode_id: Optional[str] = Field(default=None, max_length=255)
14
+
15
+
16
+ class StepRequest(BaseModel):
17
+ action: Dict[str, Any]
18
+ timeout_s: Optional[float] = Field(default=None, gt=0)
19
+ request_id: Optional[str] = Field(default=None, max_length=255)
20
+
21
+
22
+ class ResetResponse(BaseModel):
23
+ observation: Dict[str, Any]
24
+ reward: Optional[float] = None
25
+ done: bool = False
26
+
27
+
28
+ class StepResponse(BaseModel):
29
+ observation: Dict[str, Any]
30
+ reward: Optional[float] = None
31
+ done: bool = False
32
+ info: Dict[str, Any] = Field(default_factory=dict)
33
+
34
+
35
+ class HealthResponse(BaseModel):
36
+ status: str = "healthy"
openenv-polypharmacy/src/polypharmacy_env/api/server.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI server exposing the PolypharmacyEnv via OpenEnv HTTP endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import FastAPI, HTTPException
6
+
7
+ from ..env_core import PolypharmacyEnv
8
+ from ..models import PolypharmacyAction, PolypharmacyState
9
+ from .schemas import (
10
+ HealthResponse,
11
+ ResetRequest,
12
+ ResetResponse,
13
+ StepRequest,
14
+ StepResponse,
15
+ )
16
+
17
+ app = FastAPI(
18
+ title="PolypharmacyEnv",
19
+ description="OpenEnv environment for elderly polypharmacy medication-review safety.",
20
+ version="0.1.0",
21
+ )
22
+
23
+ # Module-level environment instance (single-session for simplicity)
24
+ _env = PolypharmacyEnv()
25
+
26
+
27
+ @app.post("/reset", response_model=ResetResponse)
28
+ def reset(req: ResetRequest | None = None) -> ResetResponse:
29
+ """Reset the environment and start a new episode."""
30
+ task_id = req.task_id if req else None
31
+ seed = req.seed if req else None
32
+ episode_id = req.episode_id if req else None
33
+
34
+ obs = _env.reset(task_id=task_id, seed=seed, episode_id=episode_id)
35
+ return ResetResponse(
36
+ observation=obs.model_dump(),
37
+ reward=0.0,
38
+ done=False,
39
+ )
40
+
41
+
42
+ @app.post("/step", response_model=StepResponse)
43
+ def step(req: StepRequest) -> StepResponse:
44
+ """Execute one step in the environment."""
45
+ try:
46
+ action = PolypharmacyAction(**req.action)
47
+ except Exception as e:
48
+ raise HTTPException(status_code=422, detail=f"Invalid action: {e}")
49
+
50
+ result = _env.step(action)
51
+ return StepResponse(
52
+ observation=result["observation"],
53
+ reward=result["reward"],
54
+ done=result["done"],
55
+ info=result["info"],
56
+ )
57
+
58
+
59
+ @app.get("/state", response_model=PolypharmacyState)
60
+ def state() -> PolypharmacyState:
61
+ """Return the current environment state snapshot."""
62
+ return _env.state
63
+
64
+
65
+ @app.get("/health", response_model=HealthResponse)
66
+ def health() -> HealthResponse:
67
+ return HealthResponse(status="healthy")
openenv-polypharmacy/src/polypharmacy_env/baselines/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Baseline agents."""
openenv-polypharmacy/src/polypharmacy_env/baselines/heuristic_agent.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic heuristic baseline agent for PolypharmacyEnv.
2
+
3
+ Strategy:
4
+ 1. Query all unordered medication pairs for DDIs (within budget),
5
+ prioritising high-risk elderly drugs first.
6
+ 2. For each severe DDI found, attempt substitution or stop.
7
+ 3. For each moderate DDI found, attempt substitution or stop.
8
+ 4. For remaining budget, address Beers-flagged "avoid" drugs.
9
+ 5. Call finish_review.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from itertools import combinations
15
+ from typing import List, Tuple
16
+
17
+ from ..env_core import PolypharmacyEnv
18
+ from ..models import PolypharmacyAction, PolypharmacyObservation
19
+
20
+
21
+ def run_heuristic_episode(
22
+ env: PolypharmacyEnv,
23
+ task_id: str = "budgeted_screening",
24
+ seed: int | None = None,
25
+ ) -> Tuple[float, float, int]:
26
+ """Run one episode with the heuristic agent.
27
+
28
+ Returns (total_reward, grader_score, steps).
29
+ """
30
+ obs = env.reset(task_id=task_id, seed=seed)
31
+ total_reward = 0.0
32
+ grader_score = 0.0
33
+ steps = 0
34
+
35
+ # Phase 1: Query DDIs between medication pairs, prioritising high-risk drugs
36
+ meds = obs.current_medications
37
+ # Sort: high-risk elderly drugs first, then by Beers flag count
38
+ meds_sorted = sorted(
39
+ meds,
40
+ key=lambda m: (not m.is_high_risk_elderly, -len(m.beers_flags), m.drug_id),
41
+ )
42
+ med_ids = [m.drug_id for m in meds_sorted]
43
+ pairs: List[Tuple[str, str]] = list(combinations(med_ids, 2))
44
+ severe_pairs: List[Tuple[str, str]] = []
45
+ moderate_pairs: List[Tuple[str, str]] = []
46
+
47
+ for a, b in pairs:
48
+ if obs.remaining_query_budget <= 0:
49
+ break
50
+ action = PolypharmacyAction(
51
+ action_type="query_ddi",
52
+ drug_id_1=a,
53
+ drug_id_2=b,
54
+ )
55
+ result = env.step(action)
56
+ obs = PolypharmacyObservation(**result["observation"])
57
+ total_reward += result["reward"]
58
+ steps += 1
59
+
60
+ if result["done"]:
61
+ grader_score = result["info"].get("grader_score", 0.0)
62
+ return total_reward, grader_score, steps
63
+
64
+ # Track severity
65
+ ddi_info = result["info"].get("ddi_result", {})
66
+ sev = ddi_info.get("severity", "none")
67
+ if sev == "severe":
68
+ severe_pairs.append((a, b))
69
+ elif sev == "moderate":
70
+ moderate_pairs.append((a, b))
71
+
72
+ # Phase 2: Intervene on severe DDI drugs first
73
+ current_ids = [m.drug_id for m in obs.current_medications]
74
+ intervened: set[str] = set()
75
+
76
+ def _try_intervene(
77
+ target: str,
78
+ rationale: str,
79
+ ) -> Tuple[bool, float, PolypharmacyObservation, int]:
80
+ """Try substitute then stop. Returns (success, total_reward, obs, steps)."""
81
+ nonlocal total_reward, steps
82
+ # Try substitute first
83
+ act = PolypharmacyAction(
84
+ action_type="propose_intervention",
85
+ target_drug_id=target,
86
+ intervention_type="substitute",
87
+ rationale=rationale,
88
+ )
89
+ res = env.step(act)
90
+ obs_new = PolypharmacyObservation(**res["observation"])
91
+ total_reward += res["reward"]
92
+ steps += 1
93
+
94
+ if res["done"]:
95
+ return True, total_reward, obs_new, steps
96
+
97
+ # If substitute failed, try stop
98
+ if res["info"].get("warning"):
99
+ if obs_new.remaining_intervention_budget <= 0:
100
+ return False, total_reward, obs_new, steps
101
+ act2 = PolypharmacyAction(
102
+ action_type="propose_intervention",
103
+ target_drug_id=target,
104
+ intervention_type="stop",
105
+ rationale=f"No substitute; {rationale}",
106
+ )
107
+ res2 = env.step(act2)
108
+ obs_new = PolypharmacyObservation(**res2["observation"])
109
+ total_reward += res2["reward"]
110
+ steps += 1
111
+ if res2["done"]:
112
+ return True, total_reward, obs_new, steps
113
+
114
+ return False, total_reward, obs_new, steps
115
+
116
+ # Intervene on severe pairs
117
+ for a, b in severe_pairs:
118
+ if obs.remaining_intervention_budget <= 0:
119
+ break
120
+ # Pick the drug to intervene on (prefer the one not yet intervened)
121
+ target = b if a in intervened else a
122
+ if target in intervened:
123
+ target = b
124
+ if target in intervened:
125
+ continue
126
+ intervened.add(target)
127
+
128
+ done, total_reward, obs, steps = _try_intervene(
129
+ target, f"Severe DDI between {a} and {b}"
130
+ )
131
+ if done:
132
+ grader_score = env._run_grader() if not done else 0.0
133
+ # grader_score was already computed in step
134
+ return total_reward, result["info"].get("grader_score", 0.0), steps
135
+
136
+ # Phase 2b: Intervene on moderate DDI drugs
137
+ for a, b in moderate_pairs:
138
+ if obs.remaining_intervention_budget <= 0:
139
+ break
140
+ target = b if a in intervened else a
141
+ if target in intervened:
142
+ target = b
143
+ if target in intervened:
144
+ continue
145
+ intervened.add(target)
146
+
147
+ done, total_reward, obs, steps = _try_intervene(
148
+ target, f"Moderate DDI between {a} and {b}"
149
+ )
150
+ if done:
151
+ return total_reward, result["info"].get("grader_score", 0.0), steps
152
+
153
+ # Phase 3: Address Beers-flagged "avoid" drugs
154
+ for med in meds_sorted:
155
+ if obs.remaining_intervention_budget <= 0:
156
+ break
157
+ if med.drug_id in intervened:
158
+ continue
159
+ if not med.beers_flags:
160
+ continue
161
+ if any("avoid" in f for f in med.beers_flags):
162
+ intervened.add(med.drug_id)
163
+ done, total_reward, obs, steps = _try_intervene(
164
+ med.drug_id, f"Beers criteria: {', '.join(med.beers_flags)}"
165
+ )
166
+ if done:
167
+ return total_reward, result["info"].get("grader_score", 0.0), steps
168
+
169
+ # Phase 4: Finish
170
+ action = PolypharmacyAction(action_type="finish_review")
171
+ result = env.step(action)
172
+ total_reward += result["reward"]
173
+ steps += 1
174
+ grader_score = result["info"].get("grader_score", 0.0)
175
+
176
+ return total_reward, grader_score, steps
177
+
178
+
179
+ def run_heuristic_baseline(
180
+ n_episodes: int = 5,
181
+ task_ids: List[str] | None = None,
182
+ ) -> None:
183
+ """Run the heuristic agent across tasks and print results."""
184
+ if task_ids is None:
185
+ task_ids = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
186
+
187
+ env = PolypharmacyEnv()
188
+
189
+ for tid in task_ids:
190
+ scores: list[float] = []
191
+ rewards: list[float] = []
192
+ for i in range(n_episodes):
193
+ total_r, score, steps = run_heuristic_episode(env, task_id=tid, seed=i)
194
+ scores.append(score)
195
+ rewards.append(total_r)
196
+ print(f" [{tid}] ep={i} steps={steps} reward={total_r:.4f} score={score:.4f}")
197
+
198
+ avg_s = sum(scores) / len(scores) if scores else 0.0
199
+ avg_r = sum(rewards) / len(rewards) if rewards else 0.0
200
+ print(f" [{tid}] avg_score={avg_s:.4f} avg_reward={avg_r:.4f}\n")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ run_heuristic_baseline()
openenv-polypharmacy/src/polypharmacy_env/baselines/random_agent.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Trivial random baseline agent for PolypharmacyEnv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import List, Tuple
7
+
8
+ from ..env_core import PolypharmacyEnv
9
+ from ..models import PolypharmacyAction, PolypharmacyObservation
10
+
11
+
12
+ def run_random_episode(
13
+ env: PolypharmacyEnv,
14
+ task_id: str = "budgeted_screening",
15
+ seed: int | None = None,
16
+ ) -> Tuple[float, float, int]:
17
+ rng = random.Random(seed)
18
+ obs = env.reset(task_id=task_id, seed=seed)
19
+ total_reward = 0.0
20
+ grader_score = 0.0
21
+ steps = 0
22
+
23
+ while not obs.done:
24
+ med_ids = [m.drug_id for m in obs.current_medications]
25
+ choice = rng.choice(["query_ddi", "propose_intervention", "finish_review"])
26
+
27
+ if choice == "query_ddi" and len(med_ids) >= 2 and obs.remaining_query_budget > 0:
28
+ pair = rng.sample(med_ids, 2)
29
+ action = PolypharmacyAction(
30
+ action_type="query_ddi",
31
+ drug_id_1=pair[0],
32
+ drug_id_2=pair[1],
33
+ )
34
+ elif choice == "propose_intervention" and med_ids and obs.remaining_intervention_budget > 0:
35
+ target = rng.choice(med_ids)
36
+ itype = rng.choice(["stop", "dose_reduce", "substitute", "add_monitoring"])
37
+ action = PolypharmacyAction(
38
+ action_type="propose_intervention",
39
+ target_drug_id=target,
40
+ intervention_type=itype,
41
+ rationale="random",
42
+ )
43
+ else:
44
+ action = PolypharmacyAction(action_type="finish_review")
45
+
46
+ result = env.step(action)
47
+ obs = PolypharmacyObservation(**result["observation"])
48
+ total_reward += result["reward"]
49
+ steps += 1
50
+ if result["done"]:
51
+ grader_score = result["info"].get("grader_score", 0.0)
52
+ break
53
+
54
+ return total_reward, grader_score, steps
openenv-polypharmacy/src/polypharmacy_env/config.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Environment configuration constants and task parameter definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Dict
8
+
9
+ # ── Paths ────────────────────────────────────────────────────────────────────
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[2] # openenv-polypharmacy/
11
+ DATA_DIR = PROJECT_ROOT / "data"
12
+ LOOKUPS_DIR = DATA_DIR / "lookups"
13
+ PROCESSED_DIR = DATA_DIR / "processed"
14
+
15
+ DDI_RULES_CSV = LOOKUPS_DIR / "ddi_rules.csv"
16
+ BEERS_CRITERIA_CSV = LOOKUPS_DIR / "beers_criteria.csv"
17
+ DRUG_METADATA_CSV = LOOKUPS_DIR / "drug_metadata.csv"
18
+ PATIENTS_CSV = PROCESSED_DIR / "patients_polypharmacy.csv"
19
+
20
+ # ── Reward hyper-parameters ──────────────────────────────────────────────────
21
+ QUERY_COST: float = 0.01
22
+ INTERVENTION_COST: float = 0.02
23
+ INVALID_ACTION_PENALTY: float = 0.10
24
+ TIMEOUT_PENALTY: float = 0.20
25
+ SEVERE_DDI_DISCOVERY_BONUS: float = 0.03
26
+
27
+ # ── Task parameters ─────────────────────────────────────────────────────────
28
+
29
+ @dataclass(frozen=True)
30
+ class TaskConfig:
31
+ task_id: str
32
+ difficulty: str
33
+ min_drugs: int
34
+ max_drugs: int
35
+ query_budget: int
36
+ intervention_budget: int
37
+ max_steps: int
38
+
39
+
40
+ TASK_CONFIGS: Dict[str, TaskConfig] = {
41
+ "easy_screening": TaskConfig(
42
+ task_id="easy_screening",
43
+ difficulty="easy",
44
+ min_drugs=3,
45
+ max_drugs=5,
46
+ query_budget=4,
47
+ intervention_budget=2,
48
+ max_steps=10,
49
+ ),
50
+ "budgeted_screening": TaskConfig(
51
+ task_id="budgeted_screening",
52
+ difficulty="medium",
53
+ min_drugs=6,
54
+ max_drugs=10,
55
+ query_budget=8,
56
+ intervention_budget=3,
57
+ max_steps=20,
58
+ ),
59
+ "complex_tradeoff": TaskConfig(
60
+ task_id="complex_tradeoff",
61
+ difficulty="hard",
62
+ min_drugs=10,
63
+ max_drugs=15,
64
+ query_budget=12,
65
+ intervention_budget=5,
66
+ max_steps=30,
67
+ ),
68
+ }
69
+
70
+ DEFAULT_TASK = "budgeted_screening"
71
+
72
+ # ── Critical drugs (must not be stopped without substitution) ────────────────
73
+ CRITICAL_DRUG_IDS: set[str] = {
74
+ "DRUG_WARFARIN",
75
+ "DRUG_APIXABAN",
76
+ "DRUG_INSULIN_GLARGINE",
77
+ "DRUG_METOPROLOL",
78
+ "DRUG_DIGOXIN",
79
+ }
openenv-polypharmacy/src/polypharmacy_env/data_loader.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load and cache CSV lookup data for the PolypharmacyEnv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ from dataclasses import dataclass, field
7
+ from functools import lru_cache
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Tuple
10
+
11
+ from .config import (
12
+ BEERS_CRITERIA_CSV,
13
+ DDI_RULES_CSV,
14
+ DRUG_METADATA_CSV,
15
+ PATIENTS_CSV,
16
+ )
17
+
18
+
19
+ # ── Row-level data classes ───────────────────────────────────────────────────
20
+
21
+ @dataclass(frozen=True)
22
+ class DrugMeta:
23
+ drug_id: str
24
+ generic_name: str
25
+ atc_class: str
26
+ is_high_risk_elderly: bool
27
+ default_dose_mg: float
28
+ min_dose_mg: float
29
+ max_dose_mg: float
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class DDIRule:
34
+ drug_id_1: str
35
+ drug_id_2: str
36
+ severity: str
37
+ mechanism: str
38
+ recommendation: str
39
+ base_risk_score: float
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class BeersCriterion:
44
+ drug_id: str
45
+ criterion_type: str # avoid | caution | dose_adjust | avoid_in_condition
46
+ condition: Optional[str]
47
+ rationale: str
48
+
49
+
50
+ @dataclass
51
+ class PatientEpisode:
52
+ episode_id: str
53
+ age: int
54
+ sex: str
55
+ conditions: List[str]
56
+ eGFR_category: str
57
+ liver_function_category: str
58
+ medication_ids: List[str]
59
+ baseline_risk_score: float
60
+ difficulty: str
61
+
62
+
63
+ # ── Loaders (cached) ────────────────────────────────────────────────────────
64
+
65
+ def _read_csv(path: Path) -> List[Dict[str, str]]:
66
+ with open(path, newline="") as f:
67
+ return list(csv.DictReader(f))
68
+
69
+
70
+ @lru_cache(maxsize=1)
71
+ def load_drug_metadata(path: Path = DRUG_METADATA_CSV) -> Dict[str, DrugMeta]:
72
+ out: Dict[str, DrugMeta] = {}
73
+ for row in _read_csv(path):
74
+ dm = DrugMeta(
75
+ drug_id=row["drug_id"],
76
+ generic_name=row["generic_name"],
77
+ atc_class=row["atc_class"],
78
+ is_high_risk_elderly=row["is_high_risk_elderly"] == "1",
79
+ default_dose_mg=float(row["default_dose_mg"]),
80
+ min_dose_mg=float(row["min_dose_mg"]),
81
+ max_dose_mg=float(row["max_dose_mg"]),
82
+ )
83
+ out[dm.drug_id] = dm
84
+ return out
85
+
86
+
87
+ def _normalise_pair(a: str, b: str) -> Tuple[str, str]:
88
+ return (a, b) if a < b else (b, a)
89
+
90
+
91
+ @lru_cache(maxsize=1)
92
+ def load_ddi_rules(path: Path = DDI_RULES_CSV) -> Dict[Tuple[str, str], DDIRule]:
93
+ out: Dict[Tuple[str, str], DDIRule] = {}
94
+ for row in _read_csv(path):
95
+ key = _normalise_pair(row["drug_id_1"], row["drug_id_2"])
96
+ out[key] = DDIRule(
97
+ drug_id_1=key[0],
98
+ drug_id_2=key[1],
99
+ severity=row["severity"],
100
+ mechanism=row["mechanism"],
101
+ recommendation=row["recommendation"],
102
+ base_risk_score=float(row["base_risk_score"]),
103
+ )
104
+ return out
105
+
106
+
107
+ @lru_cache(maxsize=1)
108
+ def load_beers_criteria(path: Path = BEERS_CRITERIA_CSV) -> List[BeersCriterion]:
109
+ out: List[BeersCriterion] = []
110
+ for row in _read_csv(path):
111
+ cond = row["condition"].strip() or None
112
+ out.append(BeersCriterion(
113
+ drug_id=row["drug_id"],
114
+ criterion_type=row["criterion_type"],
115
+ condition=cond,
116
+ rationale=row["rationale"],
117
+ ))
118
+ return out
119
+
120
+
121
+ def load_patients(
122
+ path: Path = PATIENTS_CSV,
123
+ difficulty: Optional[str] = None,
124
+ ) -> List[PatientEpisode]:
125
+ rows = _read_csv(path)
126
+ eps: List[PatientEpisode] = []
127
+ for row in rows:
128
+ d = row.get("difficulty", "medium")
129
+ if difficulty and d != difficulty:
130
+ continue
131
+ eps.append(PatientEpisode(
132
+ episode_id=row["episode_id"],
133
+ age=int(row["age"]),
134
+ sex=row["sex"],
135
+ conditions=[c.strip() for c in row["conditions"].split(";") if c.strip()],
136
+ eGFR_category=row["eGFR_category"],
137
+ liver_function_category=row["liver_function_category"],
138
+ medication_ids=[m.strip() for m in row["medication_ids"].split(";") if m.strip()],
139
+ baseline_risk_score=float(row["baseline_risk_score"]),
140
+ difficulty=d,
141
+ ))
142
+ return eps
openenv-polypharmacy/src/polypharmacy_env/ddi_simulator.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local DDI and guideline simulation using CSV lookup data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ from .data_loader import (
9
+ BeersCriterion,
10
+ DDIRule,
11
+ DrugMeta,
12
+ load_beers_criteria,
13
+ load_ddi_rules,
14
+ load_drug_metadata,
15
+ )
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DDIResult:
20
+ severity: str
21
+ recommendation: str
22
+ base_risk_score: float
23
+
24
+
25
+ _NO_INTERACTION = DDIResult(severity="none", recommendation="no_action", base_risk_score=0.0)
26
+
27
+
28
+ class DDISimulator:
29
+ """Provides drug–drug interaction and Beers-criteria lookups."""
30
+
31
+ def __init__(self) -> None:
32
+ self._ddi_rules: Dict[Tuple[str, str], DDIRule] = load_ddi_rules()
33
+ self._drug_meta: Dict[str, DrugMeta] = load_drug_metadata()
34
+ self._beers: List[BeersCriterion] = load_beers_criteria()
35
+
36
+ @staticmethod
37
+ def _normalise_pair(a: str, b: str) -> Tuple[str, str]:
38
+ return (a, b) if a < b else (b, a)
39
+
40
+ def lookup_ddi(self, drug_id_1: str, drug_id_2: str) -> DDIResult:
41
+ key = self._normalise_pair(drug_id_1, drug_id_2)
42
+ rule = self._ddi_rules.get(key)
43
+ if rule is None:
44
+ return _NO_INTERACTION
45
+ return DDIResult(
46
+ severity=rule.severity,
47
+ recommendation=rule.recommendation,
48
+ base_risk_score=rule.base_risk_score,
49
+ )
50
+
51
+ def get_beers_flags(
52
+ self,
53
+ drug_id: str,
54
+ patient_conditions: List[str],
55
+ ) -> List[str]:
56
+ """Return list of Beers flags applicable to *drug_id* given patient conditions."""
57
+ flags: List[str] = []
58
+ for bc in self._beers:
59
+ if bc.drug_id != drug_id:
60
+ continue
61
+ if bc.condition is None:
62
+ flags.append(bc.criterion_type)
63
+ elif bc.condition in patient_conditions:
64
+ flags.append(f"{bc.criterion_type}_{bc.condition}")
65
+ return flags
66
+
67
+ def get_drug_meta(self, drug_id: str) -> Optional[DrugMeta]:
68
+ return self._drug_meta.get(drug_id)
69
+
70
+ def find_substitute(
71
+ self,
72
+ drug_id: str,
73
+ current_drug_ids: List[str],
74
+ ) -> Optional[str]:
75
+ """Find a safer same-class substitute not already in the regimen."""
76
+ meta = self._drug_meta.get(drug_id)
77
+ if meta is None:
78
+ return None
79
+ candidates = [
80
+ dm
81
+ for dm in self._drug_meta.values()
82
+ if (
83
+ dm.atc_class == meta.atc_class
84
+ and dm.drug_id != drug_id
85
+ and dm.drug_id not in current_drug_ids
86
+ and not dm.is_high_risk_elderly
87
+ )
88
+ ]
89
+ if not candidates:
90
+ return None
91
+ # Pick the candidate with fewest severe DDIs with current regimen
92
+ def _severe_count(cand: DrugMeta) -> int:
93
+ count = 0
94
+ for did in current_drug_ids:
95
+ if did == drug_id:
96
+ continue
97
+ r = self.lookup_ddi(cand.drug_id, did)
98
+ if r.severity == "severe":
99
+ count += 1
100
+ return count
101
+
102
+ candidates.sort(key=lambda c: (_severe_count(c), c.drug_id))
103
+ return candidates[0].drug_id
104
+
105
+ @property
106
+ def drug_metadata(self) -> Dict[str, DrugMeta]:
107
+ return self._drug_meta
108
+
109
+ @property
110
+ def ddi_rules(self) -> Dict[Tuple[str, str], DDIRule]:
111
+ return self._ddi_rules
112
+
113
+ @property
114
+ def beers_criteria(self) -> List[BeersCriterion]:
115
+ return self._beers
openenv-polypharmacy/src/polypharmacy_env/env_core.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PolypharmacyEnv – core environment implementing OpenEnv step / reset / state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from itertools import combinations
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ from .config import CRITICAL_DRUG_IDS, TaskConfig
10
+ from .data_loader import PatientEpisode
11
+ from .ddi_simulator import DDISimulator
12
+ from .graders import (
13
+ grade_budgeted_screening,
14
+ grade_complex_tradeoff,
15
+ grade_easy_screening,
16
+ )
17
+ from .models import (
18
+ InteractionQueryRecord,
19
+ InterventionRecord,
20
+ MedicationEntry,
21
+ PolypharmacyAction,
22
+ PolypharmacyObservation,
23
+ PolypharmacyState,
24
+ )
25
+ from .rewards import compute_regimen_risk, compute_shaped_reward
26
+ from .tasks import get_task_config, sample_episode
27
+
28
+
29
+ class PolypharmacyEnv:
30
+ """OpenEnv-compliant environment for elderly polypharmacy medication review."""
31
+
32
+ def __init__(self) -> None:
33
+ self._sim = DDISimulator()
34
+ self._task_cfg: Optional[TaskConfig] = None
35
+ self._episode: Optional[PatientEpisode] = None
36
+ self._medications: List[MedicationEntry] = []
37
+ self._interaction_queries: List[InteractionQueryRecord] = []
38
+ self._interventions: List[InterventionRecord] = []
39
+ self._risk_deltas: List[float] = [] # per-intervention risk improvement
40
+ self._step_count: int = 0
41
+ self._done: bool = True
42
+ self._baseline_risk: float = 0.0
43
+ self._current_risk: float = 0.0
44
+ self._remaining_query_budget: int = 0
45
+ self._remaining_intervention_budget: int = 0
46
+ self._severe_moderate_discovered: int = 0
47
+ self._total_drug_changes: int = 0
48
+ self._critical_stopped_without_sub: int = 0
49
+ self._last_reward: float = 0.0
50
+
51
+ # ── reset ────────────────────────────────────────────────────────────────
52
+
53
+ def reset(
54
+ self,
55
+ task_id: Optional[str] = None,
56
+ seed: Optional[int] = None,
57
+ episode_id: Optional[str] = None,
58
+ ) -> PolypharmacyObservation:
59
+ self._task_cfg = get_task_config(task_id)
60
+ self._episode = sample_episode(task_id, seed=seed, episode_id=episode_id)
61
+
62
+ # Build medication list
63
+ self._medications = []
64
+ for did in self._episode.medication_ids:
65
+ meta = self._sim.get_drug_meta(did)
66
+ if meta is None:
67
+ continue
68
+ flags = self._sim.get_beers_flags(did, self._episode.conditions)
69
+ self._medications.append(MedicationEntry(
70
+ drug_id=did,
71
+ generic_name=meta.generic_name,
72
+ atc_class=meta.atc_class,
73
+ dose_mg=meta.default_dose_mg,
74
+ is_high_risk_elderly=meta.is_high_risk_elderly,
75
+ beers_flags=flags,
76
+ ))
77
+
78
+ self._interaction_queries = []
79
+ self._interventions = []
80
+ self._risk_deltas = []
81
+ self._step_count = 0
82
+ self._done = False
83
+ self._remaining_query_budget = self._task_cfg.query_budget
84
+ self._remaining_intervention_budget = self._task_cfg.intervention_budget
85
+ self._severe_moderate_discovered = 0
86
+ self._total_drug_changes = 0
87
+ self._critical_stopped_without_sub = 0
88
+ self._last_reward = 0.0
89
+
90
+ # Compute baseline risk
91
+ self._baseline_risk = self._compute_risk()
92
+ self._current_risk = self._baseline_risk
93
+
94
+ return self._make_observation()
95
+
96
+ # ── step ─────────────────────────────────────────────────────────────────
97
+
98
+ def step(self, action: PolypharmacyAction) -> Dict[str, Any]:
99
+ if self._done:
100
+ return self._terminal_response("Episode already finished.")
101
+
102
+ assert self._task_cfg is not None
103
+ assert self._episode is not None
104
+
105
+ reward = 0.0
106
+ info: Dict[str, Any] = {}
107
+
108
+ # Validate basic action structure
109
+ valid, err = self._validate_action(action)
110
+ if not valid:
111
+ reward = compute_shaped_reward(
112
+ self._current_risk, self._current_risk,
113
+ action.action_type, is_invalid=True,
114
+ )
115
+ info["error"] = err
116
+ self._step_count += 1
117
+ return self._check_timeout_and_respond(reward, info)
118
+
119
+ if action.action_type == "query_ddi":
120
+ reward, info = self._handle_query(action)
121
+
122
+ elif action.action_type == "propose_intervention":
123
+ reward, info = self._handle_intervention(action)
124
+
125
+ elif action.action_type == "finish_review":
126
+ self._done = True
127
+ score = self._run_grader()
128
+ reward = score # terminal bonus
129
+ info["grader_score"] = score
130
+
131
+ self._step_count += 1
132
+ return self._check_timeout_and_respond(reward, info)
133
+
134
+ # ── state property ───────────────────────────────────────────────────────
135
+
136
+ @property
137
+ def state(self) -> PolypharmacyState:
138
+ return PolypharmacyState(
139
+ episode_id=self._episode.episode_id if self._episode else None,
140
+ task_id=self._task_cfg.task_id if self._task_cfg else "",
141
+ step_count=self._step_count,
142
+ max_steps=self._task_cfg.max_steps if self._task_cfg else 0,
143
+ num_query_actions=len(self._interaction_queries),
144
+ num_interventions=len(self._interventions),
145
+ )
146
+
147
+ # ── Internal helpers ─────────────────────────────────────────────────────
148
+
149
+ def _compute_risk(self) -> float:
150
+ drug_ids = [m.drug_id for m in self._medications]
151
+ return compute_regimen_risk(
152
+ drug_ids,
153
+ self._episode.conditions if self._episode else [],
154
+ self._sim.ddi_rules,
155
+ self._sim.beers_criteria,
156
+ self._sim.drug_metadata,
157
+ )
158
+
159
+ def _validate_action(self, action: PolypharmacyAction) -> Tuple[bool, str]:
160
+ if action.action_type == "query_ddi":
161
+ if not action.drug_id_1 or not action.drug_id_2:
162
+ return False, "query_ddi requires drug_id_1 and drug_id_2"
163
+ elif action.action_type == "propose_intervention":
164
+ if not action.target_drug_id:
165
+ return False, "propose_intervention requires target_drug_id"
166
+ if action.intervention_type in (None, "none"):
167
+ return False, "propose_intervention requires a valid intervention_type"
168
+ return True, ""
169
+
170
+ def _handle_query(self, action: PolypharmacyAction) -> Tuple[float, Dict[str, Any]]:
171
+ info: Dict[str, Any] = {}
172
+ assert action.drug_id_1 and action.drug_id_2
173
+
174
+ if self._remaining_query_budget <= 0:
175
+ reward = compute_shaped_reward(
176
+ self._current_risk, self._current_risk,
177
+ "query_ddi", is_invalid=True,
178
+ )
179
+ info["error"] = "Query budget exhausted"
180
+ return reward, info
181
+
182
+ result = self._sim.lookup_ddi(action.drug_id_1, action.drug_id_2)
183
+ self._remaining_query_budget -= 1
184
+
185
+ self._interaction_queries.append(InteractionQueryRecord(
186
+ drug_id_1=action.drug_id_1,
187
+ drug_id_2=action.drug_id_2,
188
+ severity=result.severity,
189
+ recommendation=result.recommendation,
190
+ risk_score=result.base_risk_score,
191
+ step_index=self._step_count,
192
+ ))
193
+
194
+ discovered_severe = result.severity in ("severe", "moderate")
195
+ if discovered_severe:
196
+ self._severe_moderate_discovered += 1
197
+
198
+ reward = compute_shaped_reward(
199
+ self._current_risk, self._current_risk,
200
+ "query_ddi",
201
+ discovered_severe=(result.severity == "severe"),
202
+ )
203
+ info["ddi_result"] = {
204
+ "severity": result.severity,
205
+ "recommendation": result.recommendation,
206
+ "risk_score": result.base_risk_score,
207
+ }
208
+ return reward, info
209
+
210
+ def _handle_intervention(self, action: PolypharmacyAction) -> Tuple[float, Dict[str, Any]]:
211
+ info: Dict[str, Any] = {}
212
+ assert action.target_drug_id
213
+ assert action.intervention_type and action.intervention_type != "none"
214
+
215
+ if self._remaining_intervention_budget <= 0:
216
+ reward = compute_shaped_reward(
217
+ self._current_risk, self._current_risk,
218
+ "propose_intervention", is_invalid=True,
219
+ )
220
+ info["error"] = "Intervention budget exhausted"
221
+ return reward, info
222
+
223
+ # Find target medication
224
+ target_idx: Optional[int] = None
225
+ for i, m in enumerate(self._medications):
226
+ if m.drug_id == action.target_drug_id:
227
+ target_idx = i
228
+ break
229
+
230
+ if target_idx is None:
231
+ reward = compute_shaped_reward(
232
+ self._current_risk, self._current_risk,
233
+ "propose_intervention", is_invalid=True,
234
+ )
235
+ info["error"] = f"Drug {action.target_drug_id} not in current medications"
236
+ return reward, info
237
+
238
+ previous_risk = self._current_risk
239
+ target_med = self._medications[target_idx]
240
+
241
+ if action.intervention_type == "stop":
242
+ self._medications.pop(target_idx)
243
+ self._total_drug_changes += 1
244
+ if action.target_drug_id in CRITICAL_DRUG_IDS:
245
+ self._critical_stopped_without_sub += 1
246
+
247
+ elif action.intervention_type == "dose_reduce":
248
+ meta = self._sim.get_drug_meta(action.target_drug_id)
249
+ if meta:
250
+ new_dose = max(meta.min_dose_mg, target_med.dose_mg * 0.5)
251
+ self._medications[target_idx] = target_med.model_copy(
252
+ update={"dose_mg": new_dose}
253
+ )
254
+
255
+ elif action.intervention_type == "substitute":
256
+ new_drug_id = action.proposed_new_drug_id
257
+ if not new_drug_id:
258
+ # Auto-find substitute
259
+ current_ids = [m.drug_id for m in self._medications]
260
+ new_drug_id = self._sim.find_substitute(action.target_drug_id, current_ids)
261
+ if new_drug_id:
262
+ new_meta = self._sim.get_drug_meta(new_drug_id)
263
+ if new_meta:
264
+ flags = self._sim.get_beers_flags(
265
+ new_drug_id,
266
+ self._episode.conditions if self._episode else [],
267
+ )
268
+ self._medications[target_idx] = MedicationEntry(
269
+ drug_id=new_drug_id,
270
+ generic_name=new_meta.generic_name,
271
+ atc_class=new_meta.atc_class,
272
+ dose_mg=new_meta.default_dose_mg,
273
+ is_high_risk_elderly=new_meta.is_high_risk_elderly,
274
+ beers_flags=flags,
275
+ )
276
+ self._total_drug_changes += 1
277
+ # If critical drug was substituted, don't penalise
278
+ if action.target_drug_id in CRITICAL_DRUG_IDS:
279
+ pass # substitution is acceptable
280
+ else:
281
+ info["warning"] = f"Substitute {new_drug_id} not found in metadata"
282
+ # Don't consume budget for a failed substitute
283
+ self._remaining_intervention_budget += 1
284
+ else:
285
+ info["warning"] = "No suitable substitute found"
286
+ # Don't consume budget for a failed substitute
287
+ self._remaining_intervention_budget += 1
288
+
289
+ elif action.intervention_type == "add_monitoring":
290
+ # Tag in metadata but don't change regimen
291
+ self._medications[target_idx] = target_med.model_copy(
292
+ update={"beers_flags": target_med.beers_flags + ["monitored"]}
293
+ )
294
+
295
+ self._remaining_intervention_budget -= 1
296
+ self._current_risk = self._compute_risk()
297
+ risk_delta = previous_risk - self._current_risk
298
+ self._risk_deltas.append(risk_delta)
299
+
300
+ self._interventions.append(InterventionRecord(
301
+ target_drug_id=action.target_drug_id,
302
+ action_type=action.intervention_type,
303
+ proposed_new_drug_id=action.proposed_new_drug_id,
304
+ rationale=action.rationale or "",
305
+ step_index=self._step_count,
306
+ ))
307
+
308
+ reward = compute_shaped_reward(previous_risk, self._current_risk, "propose_intervention")
309
+ info["risk_delta"] = risk_delta
310
+ return reward, info
311
+
312
+ def _run_grader(self) -> float:
313
+ assert self._task_cfg is not None
314
+ tid = self._task_cfg.task_id
315
+
316
+ if tid == "easy_screening":
317
+ severe_pairs = self._get_severe_pairs()
318
+ return grade_easy_screening(
319
+ self._baseline_risk,
320
+ self._current_risk,
321
+ self._interventions,
322
+ severe_pairs,
323
+ )
324
+ elif tid == "budgeted_screening":
325
+ return grade_budgeted_screening(
326
+ self._baseline_risk,
327
+ self._current_risk,
328
+ self._interventions,
329
+ self._risk_deltas,
330
+ len(self._interaction_queries),
331
+ self._severe_moderate_discovered,
332
+ )
333
+ elif tid == "complex_tradeoff":
334
+ return grade_complex_tradeoff(
335
+ self._baseline_risk,
336
+ self._current_risk,
337
+ self._interventions,
338
+ self._total_drug_changes,
339
+ self._critical_stopped_without_sub,
340
+ )
341
+ return 0.0
342
+
343
+ def _get_severe_pairs(self) -> List[Tuple[str, str]]:
344
+ """Return all severe DDI pairs present in the *initial* medication list."""
345
+ if not self._episode:
346
+ return []
347
+ pairs: List[Tuple[str, str]] = []
348
+ med_ids = self._episode.medication_ids
349
+ for a, b in combinations(sorted(set(med_ids)), 2):
350
+ key = (a, b) if a < b else (b, a)
351
+ rule = self._sim.ddi_rules.get(key)
352
+ if rule and rule.severity == "severe":
353
+ pairs.append(key)
354
+ return pairs
355
+
356
+ def _check_timeout_and_respond(
357
+ self, reward: float, info: Dict[str, Any]
358
+ ) -> Dict[str, Any]:
359
+ assert self._task_cfg is not None
360
+
361
+ if not self._done and self._step_count >= self._task_cfg.max_steps:
362
+ self._done = True
363
+ timeout_penalty = compute_shaped_reward(
364
+ self._current_risk, self._current_risk,
365
+ "finish_review", is_timeout=True,
366
+ )
367
+ score = self._run_grader()
368
+ reward += timeout_penalty + score
369
+ info["timeout"] = True
370
+ info["grader_score"] = score
371
+
372
+ self._last_reward = reward
373
+ info["current_risk"] = self._current_risk
374
+ info["baseline_risk"] = self._baseline_risk
375
+
376
+ obs = self._make_observation(reward=reward)
377
+ return {
378
+ "observation": obs.model_dump(),
379
+ "reward": reward,
380
+ "done": self._done,
381
+ "info": info,
382
+ }
383
+
384
+ def _terminal_response(self, msg: str) -> Dict[str, Any]:
385
+ obs = self._make_observation()
386
+ return {
387
+ "observation": obs.model_dump(),
388
+ "reward": 0.0,
389
+ "done": True,
390
+ "info": {"error": msg},
391
+ }
392
+
393
+ def _make_observation(self, reward: float = 0.0) -> PolypharmacyObservation:
394
+ ep = self._episode
395
+ cfg = self._task_cfg
396
+ return PolypharmacyObservation(
397
+ episode_id=ep.episode_id if ep else "",
398
+ task_id=cfg.task_id if cfg else "budgeted_screening",
399
+ age=ep.age if ep else 65,
400
+ sex=ep.sex if ep else "M",
401
+ conditions=ep.conditions if ep else [],
402
+ eGFR_category=ep.eGFR_category if ep else "normal",
403
+ liver_function_category=ep.liver_function_category if ep else "normal",
404
+ current_medications=deepcopy(self._medications),
405
+ interaction_queries=deepcopy(self._interaction_queries),
406
+ interventions=deepcopy(self._interventions),
407
+ step_index=self._step_count,
408
+ remaining_query_budget=self._remaining_query_budget,
409
+ remaining_intervention_budget=self._remaining_intervention_budget,
410
+ shaped_reward=reward,
411
+ done=self._done,
412
+ reward=reward,
413
+ )
openenv-polypharmacy/src/polypharmacy_env/graders.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic graders for the three PolypharmacyEnv task difficulties."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import combinations
6
+ from typing import Dict, List, Tuple
7
+
8
+ from .data_loader import DDIRule
9
+ from .config import CRITICAL_DRUG_IDS
10
+ from .models import InterventionRecord
11
+
12
+
13
+ _EPS = 1e-8
14
+
15
+
16
+ def _clip(x: float) -> float:
17
+ return max(0.0, min(x, 1.0))
18
+
19
+
20
+ # ── Easy: easy_screening ─────────────────────────────────────────────────────
21
+
22
+ def grade_easy_screening(
23
+ baseline_risk: float,
24
+ final_risk: float,
25
+ interventions: List[InterventionRecord],
26
+ severe_ddi_drug_ids: List[Tuple[str, str]],
27
+ ) -> float:
28
+ """Score ∈ [0, 1] for the easy task.
29
+
30
+ 50 % risk reduction + 50 % targeted-intervention flag.
31
+ """
32
+ risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, _EPS)
33
+
34
+ targeted = 0.0
35
+ severe_drugs = set()
36
+ for a, b in severe_ddi_drug_ids:
37
+ severe_drugs.add(a)
38
+ severe_drugs.add(b)
39
+ for iv in interventions:
40
+ if iv.target_drug_id in severe_drugs:
41
+ targeted = 1.0
42
+ break
43
+
44
+ return _clip(0.5 * risk_reduction + 0.5 * targeted)
45
+
46
+
47
+ # ── Medium: budgeted_screening ───────────────────────────────────────────────
48
+
49
+ def grade_budgeted_screening(
50
+ baseline_risk: float,
51
+ final_risk: float,
52
+ interventions: List[InterventionRecord],
53
+ risk_deltas: List[float],
54
+ num_queries: int,
55
+ severe_moderate_discovered: int,
56
+ ) -> float:
57
+ """Score ∈ [0, 1] for the medium task.
58
+
59
+ 50 % risk reduction + 30 % intervention precision + 20 % query efficiency.
60
+ """
61
+ risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, _EPS)
62
+
63
+ # Intervention precision: fraction of interventions that reduced risk
64
+ if interventions:
65
+ good = sum(1 for d in risk_deltas if d > 0)
66
+ precision = good / len(interventions)
67
+ else:
68
+ precision = 0.0
69
+
70
+ # Query efficiency
71
+ if num_queries > 0:
72
+ query_eff = min(severe_moderate_discovered / num_queries, 1.0)
73
+ else:
74
+ query_eff = 0.0
75
+
76
+ return _clip(0.5 * risk_reduction + 0.3 * precision + 0.2 * query_eff)
77
+
78
+
79
+ # ── Hard: complex_tradeoff ───────────────────────────────────────────────────
80
+
81
+ def grade_complex_tradeoff(
82
+ baseline_risk: float,
83
+ final_risk: float,
84
+ interventions: List[InterventionRecord],
85
+ total_drug_changes: int,
86
+ critical_drugs_stopped_without_sub: int,
87
+ ) -> float:
88
+ """Score ∈ [0, 1] for the hard task.
89
+
90
+ Base = risk reduction; penalty for regimen disruption and critical-drug stops.
91
+ """
92
+ risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, _EPS)
93
+
94
+ # Regimen disruption: penalise excessive changes
95
+ disruption = 0.05 * total_drug_changes
96
+ critical_penalty = 0.20 * critical_drugs_stopped_without_sub
97
+
98
+ return _clip(risk_reduction - disruption - critical_penalty)
openenv-polypharmacy/src/polypharmacy_env/models.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic models for the PolypharmacyEnv environment.
2
+
3
+ Extends OpenEnv base types (Action, Observation, State) and defines
4
+ auxiliary records for medications, interactions, and interventions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List, Literal, Optional
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+
14
+ # ── Auxiliary models ─────────────────────────────────────────────────────────
15
+
16
+ class MedicationEntry(BaseModel):
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+ drug_id: str
20
+ generic_name: str
21
+ atc_class: str
22
+ dose_mg: float
23
+ frequency: str = "qd"
24
+ route: str = "po"
25
+ is_high_risk_elderly: bool = False
26
+ beers_flags: List[str] = Field(default_factory=list)
27
+
28
+
29
+ class InteractionQueryRecord(BaseModel):
30
+ model_config = ConfigDict(extra="forbid")
31
+
32
+ drug_id_1: str
33
+ drug_id_2: str
34
+ severity: Optional[str] = None
35
+ recommendation: Optional[str] = None
36
+ risk_score: Optional[float] = None
37
+ step_index: int = 0
38
+
39
+
40
+ class InterventionRecord(BaseModel):
41
+ model_config = ConfigDict(extra="forbid")
42
+
43
+ target_drug_id: str
44
+ action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]
45
+ proposed_new_drug_id: Optional[str] = None
46
+ rationale: str = ""
47
+ step_index: int = 0
48
+
49
+
50
+ # ── OpenEnv wire models ─────────────────────────────────────────────────────
51
+
52
+ class PolypharmacyAction(BaseModel):
53
+ """Action sent by the agent each step."""
54
+
55
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
56
+
57
+ action_type: Literal["query_ddi", "propose_intervention", "finish_review"]
58
+ drug_id_1: Optional[str] = None
59
+ drug_id_2: Optional[str] = None
60
+ target_drug_id: Optional[str] = None
61
+ intervention_type: Optional[
62
+ Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"]
63
+ ] = None
64
+ proposed_new_drug_id: Optional[str] = None
65
+ rationale: Optional[str] = None
66
+ metadata: Dict[str, Any] = Field(default_factory=dict)
67
+
68
+
69
+ class PolypharmacyObservation(BaseModel):
70
+ """Observation returned to the agent."""
71
+
72
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
73
+
74
+ episode_id: str = ""
75
+ task_id: str = "budgeted_screening"
76
+ age: int = 65
77
+ sex: str = "M"
78
+ conditions: List[str] = Field(default_factory=list)
79
+ eGFR_category: str = "normal"
80
+ liver_function_category: str = "normal"
81
+ current_medications: List[MedicationEntry] = Field(default_factory=list)
82
+ interaction_queries: List[InteractionQueryRecord] = Field(default_factory=list)
83
+ interventions: List[InterventionRecord] = Field(default_factory=list)
84
+ step_index: int = 0
85
+ remaining_query_budget: int = 0
86
+ remaining_intervention_budget: int = 0
87
+ shaped_reward: float = 0.0
88
+ done: bool = False
89
+ reward: Optional[float] = None
90
+ metadata: Dict[str, Any] = Field(default_factory=dict)
91
+
92
+
93
+ class PolypharmacyState(BaseModel):
94
+ """Compact state snapshot for the /state endpoint."""
95
+
96
+ model_config = ConfigDict(extra="allow", validate_assignment=True)
97
+
98
+ episode_id: Optional[str] = None
99
+ task_id: str = ""
100
+ step_count: int = Field(default=0, ge=0)
101
+ max_steps: int = 0
102
+ num_query_actions: int = 0
103
+ num_interventions: int = 0
openenv-polypharmacy/src/polypharmacy_env/rewards.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reward shaping and regimen-risk computation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import combinations
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ from .config import (
9
+ INTERVENTION_COST,
10
+ INVALID_ACTION_PENALTY,
11
+ QUERY_COST,
12
+ SEVERE_DDI_DISCOVERY_BONUS,
13
+ TIMEOUT_PENALTY,
14
+ )
15
+ from .data_loader import BeersCriterion, DDIRule, DrugMeta
16
+
17
+
18
+ def compute_regimen_risk(
19
+ current_drug_ids: List[str],
20
+ patient_conditions: List[str],
21
+ ddi_rules: Dict[Tuple[str, str], DDIRule],
22
+ beers_criteria: List[BeersCriterion],
23
+ drug_metadata: Dict[str, DrugMeta],
24
+ ) -> float:
25
+ """Compute an aggregate risk score for the current medication regimen.
26
+
27
+ Returns a float clipped to [0.0, 1.0].
28
+ """
29
+ if not current_drug_ids:
30
+ return 0.0
31
+
32
+ risk = 0.0
33
+ drug_set = set(current_drug_ids)
34
+
35
+ # 1. DDI pairwise risk
36
+ for a, b in combinations(sorted(drug_set), 2):
37
+ key = (a, b) if a < b else (b, a)
38
+ rule = ddi_rules.get(key)
39
+ if rule is not None:
40
+ risk += rule.base_risk_score
41
+
42
+ # 2. Beers violations
43
+ beers_weight = {"avoid": 0.25, "caution": 0.10, "dose_adjust": 0.08, "avoid_in_condition": 0.20}
44
+ for bc in beers_criteria:
45
+ if bc.drug_id not in drug_set:
46
+ continue
47
+ if bc.condition is None:
48
+ risk += beers_weight.get(bc.criterion_type, 0.05)
49
+ elif bc.condition in patient_conditions:
50
+ risk += beers_weight.get(bc.criterion_type, 0.05)
51
+
52
+ # 3. High-risk elderly drugs
53
+ for did in drug_set:
54
+ dm = drug_metadata.get(did)
55
+ if dm and dm.is_high_risk_elderly:
56
+ risk += 0.05
57
+
58
+ # Normalise by regimen size to keep score comparable across difficulties
59
+ risk /= max(len(drug_set), 1)
60
+ return min(max(risk, 0.0), 1.0)
61
+
62
+
63
+ def compute_shaped_reward(
64
+ previous_risk: float,
65
+ new_risk: float,
66
+ action_type: str,
67
+ *,
68
+ is_invalid: bool = False,
69
+ is_timeout: bool = False,
70
+ discovered_severe: bool = False,
71
+ ) -> float:
72
+ """Compute the step-level shaped reward."""
73
+ reward = 0.0
74
+
75
+ if is_invalid:
76
+ return -INVALID_ACTION_PENALTY
77
+
78
+ if is_timeout:
79
+ return -TIMEOUT_PENALTY
80
+
81
+ if action_type == "query_ddi":
82
+ reward -= QUERY_COST
83
+ if discovered_severe:
84
+ reward += SEVERE_DDI_DISCOVERY_BONUS
85
+
86
+ elif action_type == "propose_intervention":
87
+ reward += (previous_risk - new_risk)
88
+ reward -= INTERVENTION_COST
89
+
90
+ # finish_review terminal bonus is added by the caller after grading
91
+
92
+ return reward
openenv-polypharmacy/src/polypharmacy_env/tasks.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task setup utilities: select episodes and configure budgets per difficulty."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Optional
7
+
8
+ from .config import DEFAULT_TASK, TASK_CONFIGS, TaskConfig
9
+ from .data_loader import PatientEpisode, load_patients
10
+
11
+
12
+ # Map OpenEnv difficulty labels to the CSV difficulty tags
13
+ _DIFFICULTY_MAP = {
14
+ "easy_screening": "easy",
15
+ "budgeted_screening": "medium",
16
+ "complex_tradeoff": "hard",
17
+ }
18
+
19
+
20
+ def get_task_config(task_id: Optional[str] = None) -> TaskConfig:
21
+ tid = task_id or DEFAULT_TASK
22
+ cfg = TASK_CONFIGS.get(tid)
23
+ if cfg is None:
24
+ raise ValueError(f"Unknown task_id {tid!r}. Choose from {list(TASK_CONFIGS)}")
25
+ return cfg
26
+
27
+
28
+ def sample_episode(
29
+ task_id: Optional[str] = None,
30
+ seed: Optional[int] = None,
31
+ episode_id: Optional[str] = None,
32
+ ) -> PatientEpisode:
33
+ """Return a single patient episode appropriate for *task_id*."""
34
+ tid = task_id or DEFAULT_TASK
35
+ difficulty = _DIFFICULTY_MAP.get(tid, "medium")
36
+ episodes = load_patients(difficulty=difficulty)
37
+ if not episodes:
38
+ raise RuntimeError(f"No episodes found for difficulty={difficulty!r}")
39
+
40
+ if episode_id:
41
+ for ep in episodes:
42
+ if ep.episode_id == episode_id:
43
+ return ep
44
+ raise ValueError(f"Episode {episode_id!r} not found for difficulty={difficulty!r}")
45
+
46
+ rng = random.Random(seed)
47
+ return rng.choice(episodes)
openenv-polypharmacy/src/polypharmacy_env/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Tests."""
openenv-polypharmacy/src/polypharmacy_env/tests/test_api.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the FastAPI HTTP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from fastapi.testclient import TestClient
7
+
8
+ from polypharmacy_env.api.server import app
9
+
10
+
11
+ @pytest.fixture
12
+ def client() -> TestClient:
13
+ return TestClient(app)
14
+
15
+
16
+ class TestHealth:
17
+ def test_health(self, client: TestClient) -> None:
18
+ resp = client.get("/health")
19
+ assert resp.status_code == 200
20
+ assert resp.json()["status"] == "healthy"
21
+
22
+
23
+ class TestReset:
24
+ def test_reset_default(self, client: TestClient) -> None:
25
+ resp = client.post("/reset", json={})
26
+ assert resp.status_code == 200
27
+ data = resp.json()
28
+ assert "observation" in data
29
+ assert data["done"] is False
30
+
31
+ def test_reset_with_task(self, client: TestClient) -> None:
32
+ resp = client.post("/reset", json={"task_id": "easy_screening"})
33
+ assert resp.status_code == 200
34
+ obs = resp.json()["observation"]
35
+ assert obs["task_id"] == "easy_screening"
36
+
37
+
38
+ class TestStep:
39
+ def test_step_finish(self, client: TestClient) -> None:
40
+ client.post("/reset", json={"task_id": "easy_screening"})
41
+ resp = client.post("/step", json={"action": {"action_type": "finish_review"}})
42
+ assert resp.status_code == 200
43
+ data = resp.json()
44
+ assert data["done"] is True
45
+ assert "info" in data
46
+
47
+ def test_step_query(self, client: TestClient) -> None:
48
+ reset_resp = client.post("/reset", json={"task_id": "easy_screening", "seed": 0})
49
+ obs = reset_resp.json()["observation"]
50
+ meds = obs["current_medications"]
51
+ if len(meds) >= 2:
52
+ action = {
53
+ "action_type": "query_ddi",
54
+ "drug_id_1": meds[0]["drug_id"],
55
+ "drug_id_2": meds[1]["drug_id"],
56
+ }
57
+ resp = client.post("/step", json={"action": action})
58
+ assert resp.status_code == 200
59
+
60
+ def test_invalid_action(self, client: TestClient) -> None:
61
+ client.post("/reset", json={"task_id": "easy_screening"})
62
+ resp = client.post("/step", json={"action": {"action_type": "invalid_type"}})
63
+ assert resp.status_code == 422
64
+
65
+
66
+ class TestState:
67
+ def test_state(self, client: TestClient) -> None:
68
+ client.post("/reset", json={"task_id": "easy_screening"})
69
+ resp = client.get("/state")
70
+ assert resp.status_code == 200
71
+ data = resp.json()
72
+ assert "step_count" in data
73
+ assert data["task_id"] == "easy_screening"
openenv-polypharmacy/src/polypharmacy_env/tests/test_env_core.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for PolypharmacyEnv core logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from polypharmacy_env.env_core import PolypharmacyEnv
8
+ from polypharmacy_env.models import PolypharmacyAction, PolypharmacyObservation
9
+
10
+
11
+ @pytest.fixture
12
+ def env() -> PolypharmacyEnv:
13
+ return PolypharmacyEnv()
14
+
15
+
16
+ class TestReset:
17
+ def test_reset_returns_observation(self, env: PolypharmacyEnv) -> None:
18
+ obs = env.reset(task_id="easy_screening", seed=0)
19
+ assert isinstance(obs, PolypharmacyObservation)
20
+ assert obs.done is False
21
+ assert obs.step_index == 0
22
+ assert len(obs.current_medications) >= 3
23
+
24
+ def test_reset_medium(self, env: PolypharmacyEnv) -> None:
25
+ obs = env.reset(task_id="budgeted_screening", seed=1)
26
+ assert obs.remaining_query_budget == 8
27
+ assert obs.remaining_intervention_budget == 3
28
+
29
+ def test_reset_hard(self, env: PolypharmacyEnv) -> None:
30
+ obs = env.reset(task_id="complex_tradeoff", seed=2)
31
+ assert obs.remaining_query_budget == 12
32
+ assert obs.remaining_intervention_budget == 5
33
+
34
+ def test_default_task(self, env: PolypharmacyEnv) -> None:
35
+ obs = env.reset()
36
+ assert obs.task_id == "budgeted_screening"
37
+
38
+
39
+ class TestStep:
40
+ def test_query_ddi(self, env: PolypharmacyEnv) -> None:
41
+ obs = env.reset(task_id="easy_screening", seed=0)
42
+ meds = obs.current_medications
43
+ if len(meds) >= 2:
44
+ action = PolypharmacyAction(
45
+ action_type="query_ddi",
46
+ drug_id_1=meds[0].drug_id,
47
+ drug_id_2=meds[1].drug_id,
48
+ )
49
+ result = env.step(action)
50
+ assert "observation" in result
51
+ assert "reward" in result
52
+ assert result["done"] is False or result["done"] is True
53
+
54
+ def test_invalid_action_penalised(self, env: PolypharmacyEnv) -> None:
55
+ env.reset(task_id="easy_screening", seed=0)
56
+ action = PolypharmacyAction(
57
+ action_type="query_ddi",
58
+ drug_id_1=None,
59
+ drug_id_2=None,
60
+ )
61
+ result = env.step(action)
62
+ assert result["reward"] < 0
63
+
64
+ def test_finish_review(self, env: PolypharmacyEnv) -> None:
65
+ env.reset(task_id="easy_screening", seed=0)
66
+ action = PolypharmacyAction(action_type="finish_review")
67
+ result = env.step(action)
68
+ assert result["done"] is True
69
+ assert "grader_score" in result["info"]
70
+ score = result["info"]["grader_score"]
71
+ assert 0.0 <= score <= 1.0
72
+
73
+ def test_intervention_stop(self, env: PolypharmacyEnv) -> None:
74
+ obs = env.reset(task_id="easy_screening", seed=0)
75
+ if obs.current_medications:
76
+ target = obs.current_medications[0].drug_id
77
+ action = PolypharmacyAction(
78
+ action_type="propose_intervention",
79
+ target_drug_id=target,
80
+ intervention_type="stop",
81
+ rationale="test",
82
+ )
83
+ result = env.step(action)
84
+ new_obs = PolypharmacyObservation(**result["observation"])
85
+ drug_ids = [m.drug_id for m in new_obs.current_medications]
86
+ assert target not in drug_ids
87
+
88
+ def test_budget_exhaustion(self, env: PolypharmacyEnv) -> None:
89
+ obs = env.reset(task_id="easy_screening", seed=0)
90
+ # Exhaust query budget
91
+ meds = obs.current_medications
92
+ for _ in range(obs.remaining_query_budget + 1):
93
+ if len(meds) >= 2:
94
+ action = PolypharmacyAction(
95
+ action_type="query_ddi",
96
+ drug_id_1=meds[0].drug_id,
97
+ drug_id_2=meds[1].drug_id,
98
+ )
99
+ result = env.step(action)
100
+ if result["done"]:
101
+ break
102
+
103
+ def test_max_steps_timeout(self, env: PolypharmacyEnv) -> None:
104
+ obs = env.reset(task_id="easy_screening", seed=0)
105
+ meds = obs.current_medications
106
+ if len(meds) < 2:
107
+ return
108
+ for _ in range(20): # more than max_steps=10
109
+ action = PolypharmacyAction(
110
+ action_type="query_ddi",
111
+ drug_id_1=meds[0].drug_id,
112
+ drug_id_2=meds[1].drug_id,
113
+ )
114
+ result = env.step(action)
115
+ if result["done"]:
116
+ assert "grader_score" in result["info"] or "timeout" in result["info"]
117
+ break
118
+
119
+
120
+ class TestState:
121
+ def test_state_after_reset(self, env: PolypharmacyEnv) -> None:
122
+ env.reset(task_id="easy_screening", seed=0)
123
+ st = env.state
124
+ assert st.step_count == 0
125
+ assert st.task_id == "easy_screening"
126
+ assert st.episode_id is not None
127
+
128
+
129
+ class TestGraderDeterminism:
130
+ def test_same_trajectory_same_score(self, env: PolypharmacyEnv) -> None:
131
+ """Run the same trajectory twice; grader must return the same score."""
132
+ scores = []
133
+ for _ in range(2):
134
+ env.reset(task_id="easy_screening", seed=42)
135
+ action = PolypharmacyAction(action_type="finish_review")
136
+ result = env.step(action)
137
+ scores.append(result["info"]["grader_score"])
138
+ assert scores[0] == scores[1]
139
+
140
+ def test_intervention_changes_score(self, env: PolypharmacyEnv) -> None:
141
+ """A meaningful intervention should change the grader score vs. no-op."""
142
+ # Score with no intervention
143
+ env.reset(task_id="easy_screening", seed=42)
144
+ r1 = env.step(PolypharmacyAction(action_type="finish_review"))
145
+ score_noop = r1["info"]["grader_score"]
146
+
147
+ # Score after stopping a high-risk drug
148
+ obs = env.reset(task_id="easy_screening", seed=42)
149
+ high_risk = [m for m in obs.current_medications if m.is_high_risk_elderly]
150
+ if high_risk:
151
+ env.step(PolypharmacyAction(
152
+ action_type="propose_intervention",
153
+ target_drug_id=high_risk[0].drug_id,
154
+ intervention_type="stop",
155
+ rationale="test",
156
+ ))
157
+ r2 = env.step(PolypharmacyAction(action_type="finish_review"))
158
+ score_with = r2["info"]["grader_score"]
159
+ # Scores should differ (not necessarily larger, depending on the drug)
160
+ # At minimum, grader is not constant
161
+ assert isinstance(score_with, float)
162
+ assert 0.0 <= score_with <= 1.0