adithya9903 commited on
Commit
2043afa
·
1 Parent(s): eba6899

Set up repository for Hugging Face Spaces deployment.

Browse files

Add the project files and strengthen .gitignore rules so Python, Node, build artifacts, and env files are excluded from version control.

Made-with: Cursor

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +30 -2
  2. openenv-polypharmacy/.dockerignore +8 -0
  3. openenv-polypharmacy/Dockerfile +39 -0
  4. openenv-polypharmacy/PROMPT.md +571 -0
  5. openenv-polypharmacy/README.md +245 -0
  6. openenv-polypharmacy/backend/Dockerfile +28 -0
  7. openenv-polypharmacy/backend/__init__.py +1 -0
  8. openenv-polypharmacy/backend/main.py +15 -0
  9. openenv-polypharmacy/backend/requirements.txt +9 -0
  10. openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py +11 -0
  11. openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py +1 -0
  12. openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py +63 -0
  13. openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py +1 -0
  14. openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py +35 -0
  15. openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py +6 -0
  16. openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py +1 -0
  17. openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py +197 -0
  18. openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py +53 -0
  19. openenv-polypharmacy/backend/src/polypharmacy_env/client.py +49 -0
  20. openenv-polypharmacy/backend/src/polypharmacy_env/config.py +79 -0
  21. openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py +142 -0
  22. openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py +115 -0
  23. openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py +416 -0
  24. openenv-polypharmacy/backend/src/polypharmacy_env/graders.py +98 -0
  25. openenv-polypharmacy/backend/src/polypharmacy_env/models.py +111 -0
  26. openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py +92 -0
  27. openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py +1 -0
  28. openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py +246 -0
  29. openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py +47 -0
  30. openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py +1 -0
  31. openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py +146 -0
  32. openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py +192 -0
  33. openenv-polypharmacy/data/lookups/beers_criteria.csv +16 -0
  34. openenv-polypharmacy/data/lookups/ddi_rules.csv +25 -0
  35. openenv-polypharmacy/data/lookups/drug_metadata.csv +34 -0
  36. openenv-polypharmacy/data/processed/patients_polypharmacy.csv +121 -0
  37. openenv-polypharmacy/docker-compose.yml +35 -0
  38. openenv-polypharmacy/frontend/Dockerfile +12 -0
  39. openenv-polypharmacy/frontend/index.html +12 -0
  40. openenv-polypharmacy/frontend/package-lock.json +1677 -0
  41. openenv-polypharmacy/frontend/package.json +19 -0
  42. openenv-polypharmacy/frontend/src/App.jsx +371 -0
  43. openenv-polypharmacy/frontend/src/main.jsx +10 -0
  44. openenv-polypharmacy/frontend/src/styles.css +304 -0
  45. openenv-polypharmacy/frontend/vite.config.js +10 -0
  46. openenv-polypharmacy/inference.py +210 -0
  47. openenv-polypharmacy/openenv.yaml +30 -0
  48. openenv-polypharmacy/pyproject.toml +41 -0
  49. openenv-polypharmacy/requirements.txt +1 -0
  50. openenv-polypharmacy/scripts/dev_backend.sh +4 -0
.gitignore CHANGED
@@ -1,6 +1,34 @@
 
1
  venv/
 
 
2
  .env
 
 
3
  __pycache__/
4
- *.pyc
5
- frontend/node_modules/
 
 
 
 
 
 
 
6
  frontend/dist/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Python ---
2
  venv/
3
+ .venv/
4
+ env/
5
  .env
6
+ .env.*
7
+ *.py[cod]
8
  __pycache__/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ coverage.xml
14
+
15
+ # --- Node / frontend ---
16
+ node_modules/
17
+ **/node_modules/
18
  frontend/dist/
19
+ **/dist/
20
+ npm-debug.log*
21
+ yarn-debug.log*
22
+ yarn-error.log*
23
+ pnpm-debug.log*
24
+
25
+ # --- Build / temp ---
26
+ *.log
27
+ *.tmp
28
+ *.swp
29
+ .DS_Store
30
+
31
+ # --- Project-specific nested paths ---
32
+ openenv-polypharmacy/frontend/node_modules/
33
+ openenv-polypharmacy/frontend/dist/
34
+ openenv-polypharmacy/.pytest_cache/
openenv-polypharmacy/.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ **/__pycache__/
4
+ **/.pytest_cache/
5
+ **/.DS_Store
6
+ .env
7
+ frontend/node_modules
8
+ frontend/dist
openenv-polypharmacy/Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS frontend-builder
2
+ WORKDIR /app/frontend
3
+ COPY frontend/package*.json ./
4
+ RUN npm ci
5
+ COPY frontend/ ./
6
+ RUN npm run build
7
+
8
+ FROM python:3.11-slim
9
+
10
+ RUN apt-get update && \
11
+ apt-get install -y --no-install-recommends build-essential curl && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ WORKDIR /app
15
+
16
+ COPY backend/requirements.txt /app/backend/requirements.txt
17
+ RUN pip install --no-cache-dir -r /app/backend/requirements.txt
18
+
19
+ COPY backend /app/backend
20
+ COPY data /app/data
21
+ COPY scripts /app/scripts
22
+ COPY openenv.yaml /app/openenv.yaml
23
+ COPY .env.example /app/.env.example
24
+ COPY inference.py /app/inference.py
25
+
26
+ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
27
+
28
+ RUN python3 /app/scripts/preprocess_data.py
29
+
30
+ ENV PORT=7860
31
+ ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
32
+ ENV PYTHONUNBUFFERED=1
33
+
34
+ EXPOSE 7860
35
+
36
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
37
+ CMD curl -f http://localhost:7860/health || exit 1
38
+
39
+ CMD ["sh", "-c", "uvicorn backend.main:app --host 0.0.0.0 --port ${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,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PolypharmacyEnv
2
+
3
+ Monorepo for an OpenEnv-compatible medication safety environment with:
4
+
5
+ - a FastAPI backend (`backend/`)
6
+ - a React frontend (`frontend/`)
7
+ - data assets (`data/`)
8
+ - utility scripts (`scripts/`)
9
+
10
+ ---
11
+
12
+ ## Repository Structure
13
+
14
+ ```text
15
+ openenv-polypharmacy/
16
+ backend/
17
+ main.py # ASGI entrypoint (uvicorn target)
18
+ requirements.txt # Backend dependencies
19
+ Dockerfile # Backend container
20
+ src/polypharmacy_env/ # Python package source
21
+ api/
22
+ app.py # FastAPI/OpenEnv app assembly
23
+ server.py # Compatibility import wrapper
24
+ routes/agent.py # /agent/suggest route
25
+ services/
26
+ groq_agent.py # Groq-based action suggestion logic
27
+ env_core.py # OpenEnv environment core
28
+ models.py # Action/observation/state models
29
+ data_loader.py # CSV loading
30
+ ddi_simulator.py # DDI and Beers lookups
31
+ rewards.py # Reward shaping
32
+ graders.py # Task graders
33
+ tasks.py # Task/episode selection
34
+ tests/ # Backend tests
35
+ frontend/
36
+ src/ # React UI code
37
+ package.json
38
+ Dockerfile # Frontend container
39
+ data/
40
+ lookups/ # drug_metadata.csv, ddi_rules.csv, beers_criteria.csv
41
+ processed/ # patients_polypharmacy.csv
42
+ scripts/
43
+ preprocess_data.py # Synthetic data generation
44
+ dev_backend.sh # Local backend run helper
45
+ dev_frontend.sh # Local frontend run helper
46
+ run_validation.sh # Tests + baseline validation
47
+ docker-compose.yml # Full stack orchestration
48
+ openenv.yaml # OpenEnv manifest
49
+ inference.py # Optional CLI inference baseline
50
+ .env.example # Environment template
51
+ ```
52
+
53
+ ---
54
+
55
+ ## What It Does
56
+
57
+ The environment simulates elderly polypharmacy review. Agent actions:
58
+
59
+ - `query_ddi`
60
+ - `propose_intervention`
61
+ - `finish_review`
62
+
63
+ Supported tasks:
64
+
65
+ - `easy_screening`
66
+ - `budgeted_screening`
67
+ - `complex_tradeoff`
68
+
69
+ ---
70
+
71
+ ## Prerequisites
72
+
73
+ - Python 3.10+
74
+ - Node.js 18+ (or 20+ recommended)
75
+ - npm
76
+ - Docker + Docker Compose (optional, for containerized run)
77
+
78
+ ---
79
+
80
+ ## Environment Setup
81
+
82
+ Create `.env`:
83
+
84
+ ```bash
85
+ cp .env.example .env
86
+ ```
87
+
88
+ Set values:
89
+
90
+ - `GROQ_API_KEY=...` (required)
91
+ - `GROQ_BASE_URL=https://api.groq.com/openai/v1` (recommended)
92
+ - `GROQ_MODEL_NAME=llama-3.3-70b-versatile` (recommended)
93
+
94
+ ---
95
+
96
+ ## Local Run (Recommended During Development)
97
+
98
+ ### 1) Install dependencies
99
+
100
+ Backend:
101
+
102
+ ```bash
103
+ pip install -r backend/requirements.txt
104
+ ```
105
+
106
+ Frontend:
107
+
108
+ ```bash
109
+ cd frontend
110
+ npm install
111
+ cd ..
112
+ ```
113
+
114
+ ### 2) Generate/update synthetic data (if needed)
115
+
116
+ ```bash
117
+ python scripts/preprocess_data.py
118
+ ```
119
+
120
+ ### 3) Start services in two terminals
121
+
122
+ Terminal A:
123
+
124
+ ```bash
125
+ ./scripts/dev_backend.sh
126
+ ```
127
+
128
+ Terminal B:
129
+
130
+ ```bash
131
+ ./scripts/dev_frontend.sh
132
+ ```
133
+
134
+ ### 4) Open app
135
+
136
+ - Frontend: [http://localhost:5173](http://localhost:5173)
137
+ - Backend health: [http://localhost:7860/health](http://localhost:7860/health)
138
+
139
+ ---
140
+
141
+ ## Docker Run
142
+
143
+ Run both services:
144
+
145
+ ```bash
146
+ docker compose up --build
147
+ ```
148
+
149
+ Stop:
150
+
151
+ ```bash
152
+ docker compose down
153
+ ```
154
+
155
+ Ports:
156
+
157
+ - backend: `7860`
158
+ - frontend: `5173`
159
+
160
+ ---
161
+
162
+ ## Hugging Face Spaces Deployment (Docker)
163
+
164
+ This repo now includes a **root `Dockerfile`** that builds frontend + backend into one container, so Spaces can host both API and UI together.
165
+
166
+ ### 1) Create a new Space
167
+
168
+ - Go to [Hugging Face Spaces](https://huggingface.co/new-space)
169
+ - Choose **Docker** SDK
170
+ - Create the Space
171
+
172
+ ### 2) Add Space secrets/variables
173
+
174
+ In Space Settings -> Variables and Secrets:
175
+
176
+ - Secret: `GROQ_API_KEY`
177
+ - Variable: `GROQ_BASE_URL=https://api.groq.com/openai/v1`
178
+ - Variable: `GROQ_MODEL_NAME=llama-3.3-70b-versatile`
179
+
180
+ ### 3) Push this repository to the Space
181
+
182
+ Commit and push all files, including root `Dockerfile`.
183
+
184
+ ### 4) Verify after build
185
+
186
+ - Space root URL loads the React UI
187
+ - `/health` returns healthy status
188
+ - OpenEnv endpoints are available (`/reset`, `/step`, `/state`, `/schema`)
189
+
190
+ Notes:
191
+
192
+ - Container reads `PORT` (defaults to `7860`) which is Space-friendly.
193
+ - Frontend static assets are served by FastAPI from `frontend/dist`.
194
+
195
+ ---
196
+
197
+ ## API Endpoints
198
+
199
+ OpenEnv/health:
200
+
201
+ - `POST /reset`
202
+ - `POST /step`
203
+ - `GET /state`
204
+ - `GET /health`
205
+ - `GET /schema`
206
+ - `WS /ws` (stateful session)
207
+
208
+ AI helper:
209
+
210
+ - `POST /agent/suggest`
211
+
212
+ ---
213
+
214
+ ## Testing
215
+
216
+ Run backend tests:
217
+
218
+ ```bash
219
+ python -m pytest backend/src/polypharmacy_env/tests -v
220
+ ```
221
+
222
+ Or run validation script:
223
+
224
+ ```bash
225
+ ./scripts/run_validation.sh
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Notes
231
+
232
+ - OpenEnv HTTP reset/step is stateless; multi-step episode continuity should use websocket (`/ws`).
233
+ - The frontend uses websocket for episode continuity and HTTP for AI suggestion.
234
+ - AI behavior includes rule-based guardrails to avoid repetitive low-value loops.
235
+
236
+ ---
237
+
238
+ ## Troubleshooting
239
+
240
+ - `ModuleNotFoundError: polypharmacy_env`
241
+ - Start backend using `./scripts/dev_backend.sh` from repo root.
242
+ - `/agent/suggest` fails
243
+ - Check `.env` keys and restart backend.
244
+ - UI state looks stale
245
+ - Hard refresh browser and click `Reset Episode`.
openenv-polypharmacy/backend/Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y --no-install-recommends build-essential curl && \
5
+ rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+
9
+ COPY backend/requirements.txt /app/backend/requirements.txt
10
+ RUN pip install --no-cache-dir -r /app/backend/requirements.txt
11
+
12
+ COPY backend /app/backend
13
+ COPY data /app/data
14
+ COPY scripts /app/scripts
15
+ COPY .env.example /app/.env.example
16
+
17
+ RUN python3 /app/scripts/preprocess_data.py
18
+
19
+ ENV PORT=7860
20
+ ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
21
+ ENV PYTHONUNBUFFERED=1
22
+
23
+ EXPOSE 7860
24
+
25
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
26
+ CMD curl -f http://localhost:7860/health || exit 1
27
+
28
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
openenv-polypharmacy/backend/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Backend entrypoint package for monorepo structure."""
openenv-polypharmacy/backend/main.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ASGI entrypoint for backend service in monorepo layout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ BACKEND_DIR = Path(__file__).resolve().parent
9
+ SRC = BACKEND_DIR / "src"
10
+ if str(SRC) not in sys.path:
11
+ sys.path.insert(0, str(SRC))
12
+
13
+ from polypharmacy_env.api.app import app # noqa: E402
14
+
15
+ __all__ = ["app"]
openenv-polypharmacy/backend/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.0
2
+ uvicorn>=0.24.0
3
+ pydantic>=2.0.0
4
+ requests>=2.31.0
5
+ httpx>=0.25.0
6
+ openenv-core>=0.2.0
7
+ openai>=1.0.0
8
+ python-dotenv>=1.0.0
9
+ pytest>=7.0.0
openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PolypharmacyEnv – an OpenEnv environment for elderly polypharmacy safety."""
2
+
3
+ from .client import PolypharmacyClient
4
+ from .models import PolypharmacyAction, PolypharmacyObservation, PolypharmacyState
5
+
6
+ __all__ = [
7
+ "PolypharmacyClient",
8
+ "PolypharmacyAction",
9
+ "PolypharmacyObservation",
10
+ "PolypharmacyState",
11
+ ]
openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API package."""
openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI app factory for PolypharmacyEnv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+ from fastapi import HTTPException
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.staticfiles import StaticFiles
11
+ from openenv.core.env_server.http_server import create_app
12
+ from starlette.responses import FileResponse
13
+
14
+ from ..env_core import PolypharmacyEnv
15
+ from ..models import PolypharmacyAction, PolypharmacyObservation
16
+ from .routes.agent import router as agent_router
17
+
18
+ load_dotenv()
19
+
20
+
21
+ class SPAStaticFiles(StaticFiles):
22
+ """Serve SPA index for unknown frontend routes."""
23
+
24
+ async def get_response(self, path: str, scope):
25
+ response = await super().get_response(path, scope)
26
+ if response.status_code != 404:
27
+ return response
28
+ index_path = Path(self.directory) / "index.html"
29
+ if index_path.exists():
30
+ return FileResponse(index_path)
31
+ raise HTTPException(status_code=404, detail="Not Found")
32
+
33
+
34
+ def create_polypharmacy_app():
35
+ app = create_app(
36
+ PolypharmacyEnv,
37
+ PolypharmacyAction,
38
+ PolypharmacyObservation,
39
+ env_name="polypharmacy_env",
40
+ )
41
+
42
+ app.add_middleware(
43
+ CORSMiddleware,
44
+ allow_origins=[
45
+ "http://localhost:5173",
46
+ "http://127.0.0.1:5173",
47
+ ],
48
+ allow_credentials=True,
49
+ allow_methods=["*"],
50
+ allow_headers=["*"],
51
+ )
52
+ app.include_router(agent_router)
53
+
54
+ # In Docker Space deployment, serve built frontend from same container.
55
+ project_root = Path(__file__).resolve().parents[4]
56
+ frontend_dist = project_root / "frontend" / "dist"
57
+ if frontend_dist.exists():
58
+ app.mount("/", SPAStaticFiles(directory=frontend_dist, html=True), name="frontend")
59
+
60
+ return app
61
+
62
+
63
+ app = create_polypharmacy_app()
openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API route modules."""
openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent suggestion API routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from pydantic import BaseModel, Field
7
+
8
+ from ...models import PolypharmacyAction, PolypharmacyObservation
9
+ from ...services.groq_agent import suggest_action_from_observation
10
+
11
+ router = APIRouter(prefix="/agent", tags=["agent"])
12
+
13
+
14
+ class AgentSuggestRequest(BaseModel):
15
+ observation: PolypharmacyObservation
16
+ model_name: str | None = None
17
+
18
+
19
+ class AgentSuggestResponse(BaseModel):
20
+ action: PolypharmacyAction
21
+ source: str = Field(default="groq")
22
+
23
+
24
+ @router.post("/suggest", response_model=AgentSuggestResponse)
25
+ def suggest_agent_action(payload: AgentSuggestRequest) -> AgentSuggestResponse:
26
+ """Return a model-suggested action for the current observation."""
27
+ try:
28
+ action = suggest_action_from_observation(
29
+ payload.observation, model_name=payload.model_name
30
+ )
31
+ except ValueError as exc:
32
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
33
+ except Exception as exc:
34
+ raise HTTPException(status_code=500, detail=f"Model call failed: {exc}") from exc
35
+ return AgentSuggestResponse(action=action)
openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Backward-compatible app import path.
2
+
3
+ Use `polypharmacy_env.api.app:app` for the main app module.
4
+ """
5
+
6
+ from .app import app
openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Baseline agents."""
openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(seed=seed, task_id=task_id)
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
+ obs = env.step(action)
56
+ reward = obs.reward or 0.0
57
+ total_reward += reward
58
+ steps += 1
59
+
60
+ if obs.done:
61
+ grader_score = obs.metadata.get("grader_score", 0.0)
62
+ return total_reward, grader_score, steps
63
+
64
+ # Track severity from metadata
65
+ ddi_info = obs.metadata.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
+ intervened: set[str] = set()
74
+
75
+ def _try_intervene(
76
+ target: str,
77
+ rationale: str,
78
+ ) -> Tuple[bool, PolypharmacyObservation]:
79
+ """Try substitute then stop. Returns (done, obs)."""
80
+ nonlocal total_reward, steps
81
+ # Try substitute first
82
+ act = PolypharmacyAction(
83
+ action_type="propose_intervention",
84
+ target_drug_id=target,
85
+ intervention_type="substitute",
86
+ rationale=rationale,
87
+ )
88
+ obs_new = env.step(act)
89
+ total_reward += obs_new.reward or 0.0
90
+ steps += 1
91
+
92
+ if obs_new.done:
93
+ return True, obs_new
94
+
95
+ # If substitute failed, try stop
96
+ if obs_new.metadata.get("warning"):
97
+ if obs_new.remaining_intervention_budget <= 0:
98
+ return False, obs_new
99
+ act2 = PolypharmacyAction(
100
+ action_type="propose_intervention",
101
+ target_drug_id=target,
102
+ intervention_type="stop",
103
+ rationale=f"No substitute; {rationale}",
104
+ )
105
+ obs_new = env.step(act2)
106
+ total_reward += obs_new.reward or 0.0
107
+ steps += 1
108
+ if obs_new.done:
109
+ return True, obs_new
110
+
111
+ return False, obs_new
112
+
113
+ # Intervene on severe pairs
114
+ for a, b in severe_pairs:
115
+ if obs.remaining_intervention_budget <= 0:
116
+ break
117
+ target = b if a in intervened else a
118
+ if target in intervened:
119
+ target = b
120
+ if target in intervened:
121
+ continue
122
+ intervened.add(target)
123
+
124
+ done, obs = _try_intervene(target, f"Severe DDI between {a} and {b}")
125
+ if done:
126
+ grader_score = obs.metadata.get("grader_score", 0.0)
127
+ return total_reward, grader_score, steps
128
+
129
+ # Phase 2b: Intervene on moderate DDI drugs
130
+ for a, b in moderate_pairs:
131
+ if obs.remaining_intervention_budget <= 0:
132
+ break
133
+ target = b if a in intervened else a
134
+ if target in intervened:
135
+ target = b
136
+ if target in intervened:
137
+ continue
138
+ intervened.add(target)
139
+
140
+ done, obs = _try_intervene(target, f"Moderate DDI between {a} and {b}")
141
+ if done:
142
+ grader_score = obs.metadata.get("grader_score", 0.0)
143
+ return total_reward, grader_score, steps
144
+
145
+ # Phase 3: Address Beers-flagged "avoid" drugs
146
+ for med in meds_sorted:
147
+ if obs.remaining_intervention_budget <= 0:
148
+ break
149
+ if med.drug_id in intervened:
150
+ continue
151
+ if not med.beers_flags:
152
+ continue
153
+ if any("avoid" in f for f in med.beers_flags):
154
+ intervened.add(med.drug_id)
155
+ done, obs = _try_intervene(
156
+ med.drug_id, f"Beers criteria: {', '.join(med.beers_flags)}"
157
+ )
158
+ if done:
159
+ grader_score = obs.metadata.get("grader_score", 0.0)
160
+ return total_reward, grader_score, steps
161
+
162
+ # Phase 4: Finish
163
+ action = PolypharmacyAction(action_type="finish_review")
164
+ obs = env.step(action)
165
+ total_reward += obs.reward or 0.0
166
+ steps += 1
167
+ grader_score = obs.metadata.get("grader_score", 0.0)
168
+
169
+ return total_reward, grader_score, steps
170
+
171
+
172
+ def run_heuristic_baseline(
173
+ n_episodes: int = 5,
174
+ task_ids: List[str] | None = None,
175
+ ) -> None:
176
+ """Run the heuristic agent across tasks and print results."""
177
+ if task_ids is None:
178
+ task_ids = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
179
+
180
+ env = PolypharmacyEnv()
181
+
182
+ for tid in task_ids:
183
+ scores: list[float] = []
184
+ rewards: list[float] = []
185
+ for i in range(n_episodes):
186
+ total_r, score, steps = run_heuristic_episode(env, task_id=tid, seed=i)
187
+ scores.append(score)
188
+ rewards.append(total_r)
189
+ print(f" [{tid}] ep={i} steps={steps} reward={total_r:.4f} score={score:.4f}")
190
+
191
+ avg_s = sum(scores) / len(scores) if scores else 0.0
192
+ avg_r = sum(rewards) / len(rewards) if rewards else 0.0
193
+ print(f" [{tid}] avg_score={avg_s:.4f} avg_reward={avg_r:.4f}\n")
194
+
195
+
196
+ if __name__ == "__main__":
197
+ run_heuristic_baseline()
openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ obs = env.step(action)
47
+ total_reward += obs.reward or 0.0
48
+ steps += 1
49
+ if obs.done:
50
+ grader_score = obs.metadata.get("grader_score", 0.0)
51
+ break
52
+
53
+ return total_reward, grader_score, steps
openenv-polypharmacy/backend/src/polypharmacy_env/client.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenEnv client for PolypharmacyEnv.
2
+
3
+ Provides a typed async/sync client for interacting with a PolypharmacyEnv
4
+ server via WebSocket, following the OpenEnv EnvClient pattern.
5
+
6
+ Example (async):
7
+ >>> async with PolypharmacyClient(base_url="ws://localhost:8000") as env:
8
+ ... result = await env.reset(task_id="easy_screening")
9
+ ... result = await env.step(PolypharmacyAction(action_type="finish_review"))
10
+
11
+ Example (sync):
12
+ >>> with PolypharmacyClient(base_url="ws://localhost:8000").sync() as env:
13
+ ... result = env.reset(task_id="easy_screening")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Dict
19
+
20
+ from openenv.core.client_types import StepResult
21
+ from openenv.core.env_client import EnvClient
22
+
23
+ from .models import PolypharmacyAction, PolypharmacyObservation, PolypharmacyState
24
+
25
+
26
+ class PolypharmacyClient(
27
+ EnvClient[PolypharmacyAction, PolypharmacyObservation, PolypharmacyState]
28
+ ):
29
+ """Typed OpenEnv client for the PolypharmacyEnv environment."""
30
+
31
+ def _step_payload(self, action: PolypharmacyAction) -> Dict[str, Any]:
32
+ """Convert a PolypharmacyAction to the JSON payload for the server."""
33
+ return action.model_dump(exclude_none=True)
34
+
35
+ def _parse_result(
36
+ self, payload: Dict[str, Any]
37
+ ) -> StepResult[PolypharmacyObservation]:
38
+ """Parse a server response into a StepResult with typed observation."""
39
+ obs_data = payload.get("observation", payload)
40
+ obs = PolypharmacyObservation.model_validate(obs_data)
41
+ return StepResult(
42
+ observation=obs,
43
+ reward=payload.get("reward"),
44
+ done=payload.get("done", False),
45
+ )
46
+
47
+ def _parse_state(self, payload: Dict[str, Any]) -> PolypharmacyState:
48
+ """Parse a server state response into a typed PolypharmacyState."""
49
+ return PolypharmacyState.model_validate(payload)
openenv-polypharmacy/backend/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[3] # 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/backend/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/backend/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/backend/src/polypharmacy_env/env_core.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 openenv.core.env_server.interfaces import Environment
10
+
11
+ from .config import CRITICAL_DRUG_IDS, TaskConfig
12
+ from .data_loader import PatientEpisode
13
+ from .ddi_simulator import DDISimulator
14
+ from .graders import (
15
+ grade_budgeted_screening,
16
+ grade_complex_tradeoff,
17
+ grade_easy_screening,
18
+ )
19
+ from .models import (
20
+ InteractionQueryRecord,
21
+ InterventionRecord,
22
+ MedicationEntry,
23
+ PolypharmacyAction,
24
+ PolypharmacyObservation,
25
+ PolypharmacyState,
26
+ )
27
+ from .rewards import compute_regimen_risk, compute_shaped_reward
28
+ from .tasks import get_task_config, sample_episode
29
+
30
+
31
+ class PolypharmacyEnv(
32
+ Environment[PolypharmacyAction, PolypharmacyObservation, PolypharmacyState]
33
+ ):
34
+ """OpenEnv-compliant environment for elderly polypharmacy medication review.
35
+
36
+ Extends openenv.core.env_server.interfaces.Environment with typed
37
+ Action/Observation/State generics.
38
+ """
39
+
40
+ def __init__(self) -> None:
41
+ super().__init__()
42
+ self._sim = DDISimulator()
43
+ self._task_cfg: Optional[TaskConfig] = None
44
+ self._episode: Optional[PatientEpisode] = None
45
+ self._medications: List[MedicationEntry] = []
46
+ self._interaction_queries: List[InteractionQueryRecord] = []
47
+ self._interventions: List[InterventionRecord] = []
48
+ self._risk_deltas: List[float] = [] # per-intervention risk improvement
49
+ self._step_count: int = 0
50
+ self._done: bool = True
51
+ self._baseline_risk: float = 0.0
52
+ self._current_risk: float = 0.0
53
+ self._remaining_query_budget: int = 0
54
+ self._remaining_intervention_budget: int = 0
55
+ self._severe_moderate_discovered: int = 0
56
+ self._total_drug_changes: int = 0
57
+ self._critical_stopped_without_sub: int = 0
58
+ self._last_reward: float = 0.0
59
+
60
+ # ── reset ────────────────────────────────────────────────────────────────
61
+
62
+ def reset(
63
+ self,
64
+ seed: Optional[int] = None,
65
+ episode_id: Optional[str] = None,
66
+ **kwargs: Any,
67
+ ) -> PolypharmacyObservation:
68
+ task_id = kwargs.get("task_id", None)
69
+ self._task_cfg = get_task_config(task_id)
70
+ self._episode = sample_episode(task_id, seed=seed, episode_id=episode_id)
71
+
72
+ # Build medication list
73
+ self._medications = []
74
+ for did in self._episode.medication_ids:
75
+ meta = self._sim.get_drug_meta(did)
76
+ if meta is None:
77
+ continue
78
+ flags = self._sim.get_beers_flags(did, self._episode.conditions)
79
+ self._medications.append(MedicationEntry(
80
+ drug_id=did,
81
+ generic_name=meta.generic_name,
82
+ atc_class=meta.atc_class,
83
+ dose_mg=meta.default_dose_mg,
84
+ is_high_risk_elderly=meta.is_high_risk_elderly,
85
+ beers_flags=flags,
86
+ ))
87
+
88
+ self._interaction_queries = []
89
+ self._interventions = []
90
+ self._risk_deltas = []
91
+ self._step_count = 0
92
+ self._done = False
93
+ self._remaining_query_budget = self._task_cfg.query_budget
94
+ self._remaining_intervention_budget = self._task_cfg.intervention_budget
95
+ self._severe_moderate_discovered = 0
96
+ self._total_drug_changes = 0
97
+ self._critical_stopped_without_sub = 0
98
+ self._last_reward = 0.0
99
+
100
+ # Compute baseline risk
101
+ self._baseline_risk = self._compute_risk()
102
+ self._current_risk = self._baseline_risk
103
+
104
+ return self._make_observation()
105
+
106
+ # ── step ─────────────────────────────────────────────────────────────────
107
+
108
+ def step(
109
+ self,
110
+ action: PolypharmacyAction,
111
+ timeout_s: Optional[float] = None,
112
+ **kwargs: Any,
113
+ ) -> PolypharmacyObservation:
114
+ if self._done:
115
+ return self._make_observation()
116
+
117
+ assert self._task_cfg is not None
118
+ assert self._episode is not None
119
+
120
+ reward = 0.0
121
+ info: Dict[str, Any] = {}
122
+
123
+ # Validate basic action structure
124
+ valid, err = self._validate_action(action)
125
+ if not valid:
126
+ reward = compute_shaped_reward(
127
+ self._current_risk, self._current_risk,
128
+ action.action_type, is_invalid=True,
129
+ )
130
+ info["error"] = err
131
+ self._step_count += 1
132
+ return self._check_timeout_and_build_obs(reward, info)
133
+
134
+ if action.action_type == "query_ddi":
135
+ reward, info = self._handle_query(action)
136
+
137
+ elif action.action_type == "propose_intervention":
138
+ reward, info = self._handle_intervention(action)
139
+
140
+ elif action.action_type == "finish_review":
141
+ self._done = True
142
+ score = self._run_grader()
143
+ reward = score # terminal bonus
144
+ info["grader_score"] = score
145
+
146
+ self._step_count += 1
147
+ return self._check_timeout_and_build_obs(reward, info)
148
+
149
+ # ── state property ───────────────────────────────────────────────────────
150
+
151
+ @property
152
+ def state(self) -> PolypharmacyState:
153
+ return PolypharmacyState(
154
+ episode_id=self._episode.episode_id if self._episode else None,
155
+ step_count=self._step_count,
156
+ task_id=self._task_cfg.task_id if self._task_cfg else "",
157
+ max_steps=self._task_cfg.max_steps if self._task_cfg else 0,
158
+ num_query_actions=len(self._interaction_queries),
159
+ num_interventions=len(self._interventions),
160
+ )
161
+
162
+ # ── Internal helpers ─────────────────────────────────────────────────────
163
+
164
+ def _compute_risk(self) -> float:
165
+ drug_ids = [m.drug_id for m in self._medications]
166
+ return compute_regimen_risk(
167
+ drug_ids,
168
+ self._episode.conditions if self._episode else [],
169
+ self._sim.ddi_rules,
170
+ self._sim.beers_criteria,
171
+ self._sim.drug_metadata,
172
+ )
173
+
174
+ def _validate_action(self, action: PolypharmacyAction) -> Tuple[bool, str]:
175
+ if action.action_type == "query_ddi":
176
+ if not action.drug_id_1 or not action.drug_id_2:
177
+ return False, "query_ddi requires drug_id_1 and drug_id_2"
178
+ elif action.action_type == "propose_intervention":
179
+ if not action.target_drug_id:
180
+ return False, "propose_intervention requires target_drug_id"
181
+ if action.intervention_type in (None, "none"):
182
+ return False, "propose_intervention requires a valid intervention_type"
183
+ return True, ""
184
+
185
+ def _handle_query(self, action: PolypharmacyAction) -> Tuple[float, Dict[str, Any]]:
186
+ info: Dict[str, Any] = {}
187
+ assert action.drug_id_1 and action.drug_id_2
188
+
189
+ if self._remaining_query_budget <= 0:
190
+ reward = compute_shaped_reward(
191
+ self._current_risk, self._current_risk,
192
+ "query_ddi", is_invalid=True,
193
+ )
194
+ info["error"] = "Query budget exhausted"
195
+ return reward, info
196
+
197
+ result = self._sim.lookup_ddi(action.drug_id_1, action.drug_id_2)
198
+ self._remaining_query_budget -= 1
199
+
200
+ self._interaction_queries.append(InteractionQueryRecord(
201
+ drug_id_1=action.drug_id_1,
202
+ drug_id_2=action.drug_id_2,
203
+ severity=result.severity,
204
+ recommendation=result.recommendation,
205
+ risk_score=result.base_risk_score,
206
+ step_index=self._step_count,
207
+ ))
208
+
209
+ discovered_severe = result.severity in ("severe", "moderate")
210
+ if discovered_severe:
211
+ self._severe_moderate_discovered += 1
212
+
213
+ reward = compute_shaped_reward(
214
+ self._current_risk, self._current_risk,
215
+ "query_ddi",
216
+ discovered_severe=(result.severity == "severe"),
217
+ )
218
+ info["ddi_result"] = {
219
+ "severity": result.severity,
220
+ "recommendation": result.recommendation,
221
+ "risk_score": result.base_risk_score,
222
+ }
223
+ return reward, info
224
+
225
+ def _handle_intervention(self, action: PolypharmacyAction) -> Tuple[float, Dict[str, Any]]:
226
+ info: Dict[str, Any] = {}
227
+ assert action.target_drug_id
228
+ assert action.intervention_type and action.intervention_type != "none"
229
+
230
+ if self._remaining_intervention_budget <= 0:
231
+ reward = compute_shaped_reward(
232
+ self._current_risk, self._current_risk,
233
+ "propose_intervention", is_invalid=True,
234
+ )
235
+ info["error"] = "Intervention budget exhausted"
236
+ return reward, info
237
+
238
+ # Find target medication
239
+ target_idx: Optional[int] = None
240
+ for i, m in enumerate(self._medications):
241
+ if m.drug_id == action.target_drug_id:
242
+ target_idx = i
243
+ break
244
+
245
+ if target_idx is None:
246
+ reward = compute_shaped_reward(
247
+ self._current_risk, self._current_risk,
248
+ "propose_intervention", is_invalid=True,
249
+ )
250
+ info["error"] = f"Drug {action.target_drug_id} not in current medications"
251
+ return reward, info
252
+
253
+ previous_risk = self._current_risk
254
+ target_med = self._medications[target_idx]
255
+
256
+ if action.intervention_type == "stop":
257
+ self._medications.pop(target_idx)
258
+ self._total_drug_changes += 1
259
+ if action.target_drug_id in CRITICAL_DRUG_IDS:
260
+ self._critical_stopped_without_sub += 1
261
+
262
+ elif action.intervention_type == "dose_reduce":
263
+ meta = self._sim.get_drug_meta(action.target_drug_id)
264
+ if meta:
265
+ new_dose = max(meta.min_dose_mg, target_med.dose_mg * 0.5)
266
+ self._medications[target_idx] = target_med.model_copy(
267
+ update={"dose_mg": new_dose}
268
+ )
269
+
270
+ elif action.intervention_type == "substitute":
271
+ new_drug_id = action.proposed_new_drug_id
272
+ if not new_drug_id:
273
+ # Auto-find substitute
274
+ current_ids = [m.drug_id for m in self._medications]
275
+ new_drug_id = self._sim.find_substitute(action.target_drug_id, current_ids)
276
+ if new_drug_id:
277
+ new_meta = self._sim.get_drug_meta(new_drug_id)
278
+ if new_meta:
279
+ flags = self._sim.get_beers_flags(
280
+ new_drug_id,
281
+ self._episode.conditions if self._episode else [],
282
+ )
283
+ self._medications[target_idx] = MedicationEntry(
284
+ drug_id=new_drug_id,
285
+ generic_name=new_meta.generic_name,
286
+ atc_class=new_meta.atc_class,
287
+ dose_mg=new_meta.default_dose_mg,
288
+ is_high_risk_elderly=new_meta.is_high_risk_elderly,
289
+ beers_flags=flags,
290
+ )
291
+ self._total_drug_changes += 1
292
+ # If critical drug was substituted, don't penalise
293
+ if action.target_drug_id in CRITICAL_DRUG_IDS:
294
+ pass # substitution is acceptable
295
+ else:
296
+ info["warning"] = f"Substitute {new_drug_id} not found in metadata"
297
+ # Don't consume budget for a failed substitute
298
+ self._remaining_intervention_budget += 1
299
+ else:
300
+ info["warning"] = "No suitable substitute found"
301
+ # Don't consume budget for a failed substitute
302
+ self._remaining_intervention_budget += 1
303
+
304
+ elif action.intervention_type == "add_monitoring":
305
+ # Tag in metadata but don't change regimen
306
+ self._medications[target_idx] = target_med.model_copy(
307
+ update={"beers_flags": target_med.beers_flags + ["monitored"]}
308
+ )
309
+
310
+ self._remaining_intervention_budget -= 1
311
+ self._current_risk = self._compute_risk()
312
+ risk_delta = previous_risk - self._current_risk
313
+ self._risk_deltas.append(risk_delta)
314
+
315
+ self._interventions.append(InterventionRecord(
316
+ target_drug_id=action.target_drug_id,
317
+ action_type=action.intervention_type,
318
+ proposed_new_drug_id=action.proposed_new_drug_id,
319
+ rationale=action.rationale or "",
320
+ step_index=self._step_count,
321
+ ))
322
+
323
+ reward = compute_shaped_reward(previous_risk, self._current_risk, "propose_intervention")
324
+ info["risk_delta"] = risk_delta
325
+ return reward, info
326
+
327
+ def _run_grader(self) -> float:
328
+ assert self._task_cfg is not None
329
+ tid = self._task_cfg.task_id
330
+
331
+ if tid == "easy_screening":
332
+ severe_pairs = self._get_severe_pairs()
333
+ return grade_easy_screening(
334
+ self._baseline_risk,
335
+ self._current_risk,
336
+ self._interventions,
337
+ severe_pairs,
338
+ )
339
+ elif tid == "budgeted_screening":
340
+ return grade_budgeted_screening(
341
+ self._baseline_risk,
342
+ self._current_risk,
343
+ self._interventions,
344
+ self._risk_deltas,
345
+ len(self._interaction_queries),
346
+ self._severe_moderate_discovered,
347
+ )
348
+ elif tid == "complex_tradeoff":
349
+ return grade_complex_tradeoff(
350
+ self._baseline_risk,
351
+ self._current_risk,
352
+ self._interventions,
353
+ self._total_drug_changes,
354
+ self._critical_stopped_without_sub,
355
+ )
356
+ return 0.0
357
+
358
+ def _get_severe_pairs(self) -> List[Tuple[str, str]]:
359
+ """Return all severe DDI pairs present in the *initial* medication list."""
360
+ if not self._episode:
361
+ return []
362
+ pairs: List[Tuple[str, str]] = []
363
+ med_ids = self._episode.medication_ids
364
+ for a, b in combinations(sorted(set(med_ids)), 2):
365
+ key = (a, b) if a < b else (b, a)
366
+ rule = self._sim.ddi_rules.get(key)
367
+ if rule and rule.severity == "severe":
368
+ pairs.append(key)
369
+ return pairs
370
+
371
+ def _check_timeout_and_build_obs(
372
+ self, reward: float, info: Dict[str, Any]
373
+ ) -> PolypharmacyObservation:
374
+ assert self._task_cfg is not None
375
+
376
+ if not self._done and self._step_count >= self._task_cfg.max_steps:
377
+ self._done = True
378
+ timeout_penalty = compute_shaped_reward(
379
+ self._current_risk, self._current_risk,
380
+ "finish_review", is_timeout=True,
381
+ )
382
+ score = self._run_grader()
383
+ reward += timeout_penalty + score
384
+ info["timeout"] = True
385
+ info["grader_score"] = score
386
+
387
+ self._last_reward = reward
388
+ info["current_risk"] = self._current_risk
389
+ info["baseline_risk"] = self._baseline_risk
390
+
391
+ return self._make_observation(reward=reward, info=info)
392
+
393
+ def _make_observation(
394
+ self, reward: float = 0.0, info: Optional[Dict[str, Any]] = None,
395
+ ) -> PolypharmacyObservation:
396
+ ep = self._episode
397
+ cfg = self._task_cfg
398
+ return PolypharmacyObservation(
399
+ episode_id=ep.episode_id if ep else "",
400
+ task_id=cfg.task_id if cfg else "budgeted_screening",
401
+ age=ep.age if ep else 65,
402
+ sex=ep.sex if ep else "M",
403
+ conditions=ep.conditions if ep else [],
404
+ eGFR_category=ep.eGFR_category if ep else "normal",
405
+ liver_function_category=ep.liver_function_category if ep else "normal",
406
+ current_medications=deepcopy(self._medications),
407
+ interaction_queries=deepcopy(self._interaction_queries),
408
+ interventions=deepcopy(self._interventions),
409
+ step_index=self._step_count,
410
+ remaining_query_budget=self._remaining_query_budget,
411
+ remaining_intervention_budget=self._remaining_intervention_budget,
412
+ shaped_reward=reward,
413
+ done=self._done,
414
+ reward=reward,
415
+ metadata=info or {},
416
+ )
openenv-polypharmacy/backend/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/backend/src/polypharmacy_env/models.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from openenv.core.env_server.types import (
14
+ Action as OpenEnvAction,
15
+ Observation as OpenEnvObservation,
16
+ State as OpenEnvState,
17
+ )
18
+
19
+
20
+ # ── Auxiliary models ─────────────────────────────────────────────────────────
21
+
22
+ class MedicationEntry(BaseModel):
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ drug_id: str
26
+ generic_name: str
27
+ atc_class: str
28
+ dose_mg: float
29
+ frequency: str = "qd"
30
+ route: str = "po"
31
+ is_high_risk_elderly: bool = False
32
+ beers_flags: List[str] = Field(default_factory=list)
33
+
34
+
35
+ class InteractionQueryRecord(BaseModel):
36
+ model_config = ConfigDict(extra="forbid")
37
+
38
+ drug_id_1: str
39
+ drug_id_2: str
40
+ severity: Optional[str] = None
41
+ recommendation: Optional[str] = None
42
+ risk_score: Optional[float] = None
43
+ step_index: int = 0
44
+
45
+
46
+ class InterventionRecord(BaseModel):
47
+ model_config = ConfigDict(extra="forbid")
48
+
49
+ target_drug_id: str
50
+ action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]
51
+ proposed_new_drug_id: Optional[str] = None
52
+ rationale: str = ""
53
+ step_index: int = 0
54
+
55
+
56
+ # ── OpenEnv wire models ─────────────────────────────────────────────────────
57
+
58
+ class PolypharmacyAction(OpenEnvAction):
59
+ """Action sent by the agent each step.
60
+
61
+ Extends openenv.core.env_server.types.Action.
62
+ """
63
+
64
+ action_type: Literal["query_ddi", "propose_intervention", "finish_review"]
65
+ drug_id_1: Optional[str] = None
66
+ drug_id_2: Optional[str] = None
67
+ target_drug_id: Optional[str] = None
68
+ intervention_type: Optional[
69
+ Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"]
70
+ ] = None
71
+ proposed_new_drug_id: Optional[str] = None
72
+ rationale: Optional[str] = None
73
+
74
+
75
+ class PolypharmacyObservation(OpenEnvObservation):
76
+ """Observation returned to the agent.
77
+
78
+ Extends openenv.core.env_server.types.Observation which provides:
79
+ - done: bool
80
+ - reward: float | None
81
+ - metadata: Dict[str, Any]
82
+ """
83
+
84
+ episode_id: str = ""
85
+ task_id: str = "budgeted_screening"
86
+ age: int = 65
87
+ sex: str = "M"
88
+ conditions: List[str] = Field(default_factory=list)
89
+ eGFR_category: str = "normal"
90
+ liver_function_category: str = "normal"
91
+ current_medications: List[MedicationEntry] = Field(default_factory=list)
92
+ interaction_queries: List[InteractionQueryRecord] = Field(default_factory=list)
93
+ interventions: List[InterventionRecord] = Field(default_factory=list)
94
+ step_index: int = 0
95
+ remaining_query_budget: int = 0
96
+ remaining_intervention_budget: int = 0
97
+ shaped_reward: float = 0.0
98
+
99
+
100
+ class PolypharmacyState(OpenEnvState):
101
+ """Compact state snapshot for the /state endpoint.
102
+
103
+ Extends openenv.core.env_server.types.State which provides:
104
+ - episode_id: str | None
105
+ - step_count: int
106
+ """
107
+
108
+ task_id: str = ""
109
+ max_steps: int = 0
110
+ num_query_actions: int = 0
111
+ num_interventions: int = 0
openenv-polypharmacy/backend/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/backend/src/polypharmacy_env/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Service layer for external integrations."""
openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Groq-powered action suggester for PolypharmacyEnv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Any
8
+
9
+ from openai import OpenAI
10
+
11
+ from ..models import PolypharmacyAction, PolypharmacyObservation
12
+
13
+ DEFAULT_MODEL = "llama-3.1-8b-instant"
14
+ FALLBACK_MODELS = [
15
+ "llama-3.1-8b-instant",
16
+ "llama-3.3-70b-versatile",
17
+ "gemma2-9b-it",
18
+ ]
19
+ CRITICAL_DRUG_IDS = {"DRUG_WARFARIN", "DRUG_INSULIN_GLARGINE", "DRUG_DIGOXIN"}
20
+
21
+ SYSTEM_PROMPT = """You are a clinical medication safety assistant.
22
+ Return exactly one JSON object describing the next action.
23
+ Allowed output schema:
24
+ {
25
+ "action_type": "query_ddi" | "propose_intervention" | "finish_review",
26
+ "drug_id_1": "optional",
27
+ "drug_id_2": "optional",
28
+ "target_drug_id": "optional",
29
+ "intervention_type": "stop|dose_reduce|substitute|add_monitoring|none",
30
+ "proposed_new_drug_id": "optional",
31
+ "rationale": "optional"
32
+ }
33
+ No markdown fences. No extra text.
34
+ Do NOT use finish_review early. First, gather evidence with query_ddi and/or
35
+ perform at least one meaningful intervention when needed.
36
+ """
37
+
38
+
39
+ def _obs_to_prompt(obs: PolypharmacyObservation) -> str:
40
+ meds = ", ".join(m.drug_id for m in obs.current_medications)
41
+ conds = ", ".join(obs.conditions)
42
+ return (
43
+ f"Task: {obs.task_id}\n"
44
+ f"Age: {obs.age}, sex: {obs.sex}\n"
45
+ f"Conditions: {conds}\n"
46
+ f"Medications: {meds}\n"
47
+ f"Query budget: {obs.remaining_query_budget}\n"
48
+ f"Intervention budget: {obs.remaining_intervention_budget}\n"
49
+ f"Step index: {obs.step_index}\n"
50
+ "Choose the single safest, most useful next action."
51
+ )
52
+
53
+
54
+ def _parse_action(text: str) -> PolypharmacyAction:
55
+ raw = text.strip()
56
+ if raw.startswith("```"):
57
+ raw = raw.split("\n", 1)[-1]
58
+ if raw.endswith("```"):
59
+ raw = raw.rsplit("```", 1)[0]
60
+ raw = raw.strip()
61
+ payload: dict[str, Any] = json.loads(raw)
62
+ return PolypharmacyAction.model_validate(payload)
63
+
64
+
65
+ def _fallback_query_action(obs: PolypharmacyObservation) -> PolypharmacyAction:
66
+ meds = [m.drug_id for m in obs.current_medications]
67
+ if len(meds) >= 2 and obs.remaining_query_budget > 0:
68
+ return PolypharmacyAction(
69
+ action_type="query_ddi",
70
+ drug_id_1=meds[0],
71
+ drug_id_2=meds[1],
72
+ )
73
+ return PolypharmacyAction(action_type="finish_review")
74
+
75
+
76
+ def _norm_pair(a: str, b: str) -> tuple[str, str]:
77
+ return (a, b) if a < b else (b, a)
78
+
79
+
80
+ def _pick_unseen_query_pair(obs: PolypharmacyObservation) -> tuple[str, str] | None:
81
+ meds = [m.drug_id for m in obs.current_medications]
82
+ if len(meds) < 2 or obs.remaining_query_budget <= 0:
83
+ return None
84
+
85
+ seen = {
86
+ _norm_pair(q.drug_id_1, q.drug_id_2)
87
+ for q in obs.interaction_queries
88
+ }
89
+ # Prioritize pairs containing high-risk drugs.
90
+ high_risk = [m.drug_id for m in obs.current_medications if m.is_high_risk_elderly]
91
+ ordered = high_risk + [m for m in meds if m not in set(high_risk)]
92
+
93
+ for i in range(len(ordered)):
94
+ for j in range(i + 1, len(ordered)):
95
+ p = _norm_pair(ordered[i], ordered[j])
96
+ if p not in seen:
97
+ return p
98
+ return None
99
+
100
+
101
+ def _pick_intervention_target(obs: PolypharmacyObservation) -> str | None:
102
+ if obs.remaining_intervention_budget <= 0:
103
+ return None
104
+ med_set = {m.drug_id for m in obs.current_medications}
105
+
106
+ # Use latest discovered severe/moderate query as intervention target.
107
+ for q in reversed(obs.interaction_queries):
108
+ if q.severity in ("severe", "moderate"):
109
+ m1 = next((m for m in obs.current_medications if m.drug_id == q.drug_id_1), None)
110
+ m2 = next((m for m in obs.current_medications if m.drug_id == q.drug_id_2), None)
111
+ candidates = [m for m in (m1, m2) if m is not None]
112
+ if not candidates:
113
+ continue
114
+ # Prefer non-critical risky drugs first.
115
+ candidates.sort(
116
+ key=lambda m: (
117
+ m.drug_id in CRITICAL_DRUG_IDS,
118
+ 0 if any("avoid" in f for f in m.beers_flags) else 1,
119
+ 0 if m.is_high_risk_elderly else 1,
120
+ )
121
+ )
122
+ return candidates[0].drug_id
123
+
124
+ # Fallback: if no severe/moderate discovered, still intervene on obviously
125
+ # risky medications (Beers/high-risk flags) when budgets permit.
126
+ risky = sorted(
127
+ obs.current_medications,
128
+ key=lambda m: (
129
+ 0 if any("avoid" in f for f in m.beers_flags) else 1,
130
+ 0 if m.is_high_risk_elderly else 1,
131
+ 1 if m.drug_id in CRITICAL_DRUG_IDS else 0,
132
+ ),
133
+ )
134
+ for med in risky:
135
+ if any("avoid" in f for f in med.beers_flags) or med.is_high_risk_elderly:
136
+ return med.drug_id
137
+ return None
138
+
139
+
140
+ def _rule_based_action(obs: PolypharmacyObservation) -> PolypharmacyAction | None:
141
+ # If we already discovered significant risk, intervene before more querying.
142
+ target = _pick_intervention_target(obs)
143
+ if target and (
144
+ obs.step_index >= 1
145
+ and (
146
+ obs.remaining_query_budget <= 2
147
+ or len(obs.interaction_queries) >= 4
148
+ or any(q.severity in ("severe", "moderate") for q in obs.interaction_queries)
149
+ )
150
+ ):
151
+ intervention = "stop"
152
+ rationale = "Remove likely contributor to discovered interaction risk"
153
+ if target in CRITICAL_DRUG_IDS:
154
+ # Avoid blunt stop for critical meds.
155
+ intervention = "dose_reduce"
156
+ rationale = "Critical medication: prefer dose reduction over abrupt stop"
157
+ return PolypharmacyAction(
158
+ action_type="propose_intervention",
159
+ target_drug_id=target,
160
+ intervention_type=intervention,
161
+ rationale=rationale,
162
+ )
163
+
164
+ pair = _pick_unseen_query_pair(obs)
165
+ if pair:
166
+ return PolypharmacyAction(
167
+ action_type="query_ddi",
168
+ drug_id_1=pair[0],
169
+ drug_id_2=pair[1],
170
+ )
171
+
172
+ if obs.remaining_intervention_budget > 0:
173
+ # Final fallback before finish: at least one safety action.
174
+ target = _pick_intervention_target(obs)
175
+ if target:
176
+ return PolypharmacyAction(
177
+ action_type="propose_intervention",
178
+ target_drug_id=target,
179
+ intervention_type="dose_reduce"
180
+ if target in CRITICAL_DRUG_IDS
181
+ else "stop",
182
+ rationale="Fallback intervention when query options are exhausted",
183
+ )
184
+
185
+ if obs.step_index >= 3:
186
+ return PolypharmacyAction(action_type="finish_review")
187
+ return None
188
+
189
+
190
+ def _postprocess_action(
191
+ obs: PolypharmacyObservation, action: PolypharmacyAction
192
+ ) -> PolypharmacyAction:
193
+ # First apply deterministic guardrails to avoid repetitive loops.
194
+ ruled = _rule_based_action(obs)
195
+ if ruled is not None:
196
+ return ruled
197
+
198
+ # Guardrail: prevent useless immediate finish actions.
199
+ if action.action_type == "finish_review":
200
+ if obs.step_index < 2 and obs.remaining_query_budget > 0:
201
+ return _fallback_query_action(obs)
202
+ if len(obs.interaction_queries) == 0 and obs.remaining_query_budget > 0:
203
+ return _fallback_query_action(obs)
204
+ return action
205
+
206
+
207
+ def suggest_action_from_observation(
208
+ observation: PolypharmacyObservation,
209
+ model_name: str | None = None,
210
+ ) -> PolypharmacyAction:
211
+ """Use Groq chat completions to suggest a valid action."""
212
+ api_key = os.getenv("GROQ_API_KEY", "").strip()
213
+ if not api_key:
214
+ raise ValueError("GROQ_API_KEY is missing. Add it to your .env file.")
215
+
216
+ base_url = os.getenv("GROQ_BASE_URL", "https://api.groq.com/openai/v1").strip()
217
+ model = (model_name or os.getenv("GROQ_MODEL_NAME", DEFAULT_MODEL)).strip()
218
+ client = OpenAI(api_key=api_key, base_url=base_url)
219
+
220
+ user_prompt = _obs_to_prompt(observation)
221
+ tried: list[tuple[str, str]] = []
222
+ candidates: list[str] = [model] + [m for m in FALLBACK_MODELS if m != model]
223
+
224
+ for candidate in candidates:
225
+ try:
226
+ resp = client.chat.completions.create(
227
+ model=candidate,
228
+ messages=[
229
+ {"role": "system", "content": SYSTEM_PROMPT},
230
+ {"role": "user", "content": user_prompt},
231
+ ],
232
+ temperature=0.2,
233
+ max_tokens=220,
234
+ )
235
+ generated = (resp.choices[0].message.content or "").strip()
236
+ parsed = _parse_action(generated)
237
+ return _postprocess_action(observation, parsed)
238
+ except Exception as exc:
239
+ tried.append((candidate, str(exc)))
240
+
241
+ tried_txt = " | ".join(f"{m}: {err}" for m, err in tried)
242
+ raise ValueError(
243
+ "No Groq model worked. Try one of: "
244
+ "llama-3.3-70b-versatile, llama-3.1-8b-instant, gemma2-9b-it. "
245
+ f"Errors: {tried_txt}"
246
+ )
openenv-polypharmacy/backend/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/backend/src/polypharmacy_env/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Tests."""
openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the FastAPI HTTP server (OpenEnv create_app endpoints).
2
+
3
+ OpenEnv HTTP endpoints are *stateless*: each /reset and /step creates a
4
+ fresh environment instance. Multi-step sessions only work via WebSocket.
5
+ These tests validate single-call behaviour and schema contracts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import pytest
13
+ from fastapi.testclient import TestClient
14
+
15
+ from polypharmacy_env.api.server import app
16
+
17
+
18
+ @pytest.fixture
19
+ def client() -> TestClient:
20
+ return TestClient(app)
21
+
22
+
23
+ class TestHealth:
24
+ def test_health(self, client: TestClient) -> None:
25
+ resp = client.get("/health")
26
+ assert resp.status_code == 200
27
+ data = resp.json()
28
+ assert data["status"] == "healthy"
29
+
30
+
31
+ class TestReset:
32
+ def test_reset_default(self, client: TestClient) -> None:
33
+ resp = client.post("/reset", json={})
34
+ assert resp.status_code == 200
35
+ data = resp.json()
36
+ assert "observation" in data
37
+ assert data["done"] is False
38
+
39
+ def test_reset_with_task(self, client: TestClient) -> None:
40
+ resp = client.post("/reset", json={"task_id": "easy_screening"})
41
+ assert resp.status_code == 200
42
+ obs = resp.json()["observation"]
43
+ assert obs["task_id"] == "easy_screening"
44
+
45
+ def test_reset_observation_has_medications(self, client: TestClient) -> None:
46
+ resp = client.post("/reset", json={"task_id": "easy_screening", "seed": 42})
47
+ assert resp.status_code == 200
48
+ obs = resp.json()["observation"]
49
+ assert len(obs["current_medications"]) >= 3
50
+
51
+
52
+ class TestStep:
53
+ """Test /step endpoint – each call is independent (stateless)."""
54
+
55
+ def test_step_finish(self, client: TestClient) -> None:
56
+ resp = client.post(
57
+ "/step",
58
+ json={"action": {"action_type": "finish_review"}},
59
+ )
60
+ assert resp.status_code == 200
61
+ data = resp.json()
62
+ assert "observation" in data
63
+
64
+ def test_invalid_action_422(self, client: TestClient) -> None:
65
+ resp = client.post(
66
+ "/step",
67
+ json={"action": {"action_type": "invalid_type"}},
68
+ )
69
+ assert resp.status_code == 422
70
+
71
+
72
+ class TestSchema:
73
+ def test_schema(self, client: TestClient) -> None:
74
+ resp = client.get("/schema")
75
+ assert resp.status_code == 200
76
+ data = resp.json()
77
+ # OpenEnv schema endpoint returns keys: action, observation, state
78
+ assert "action" in data
79
+ assert "observation" in data
80
+
81
+
82
+ class TestWebSocketSession:
83
+ """Test multi-step sessions through the /ws WebSocket endpoint.
84
+
85
+ OpenEnv WS protocol:
86
+ Send: {"type": "reset", "data": {"task_id": "...", "seed": ...}}
87
+ Recv: {"type": "observation", "data": {"observation": {...}, "reward": ..., "done": ...}}
88
+ Send: {"type": "step", "data": {"action_type": "...", ...}}
89
+ Recv: {"type": "observation", "data": {"observation": {...}, ...}}
90
+ Send: {"type": "state"}
91
+ Recv: {"type": "state", "data": {...state fields...}}
92
+ """
93
+
94
+ def test_ws_reset_step_finish(self, client: TestClient) -> None:
95
+ with client.websocket_connect("/ws") as ws:
96
+ # Reset
97
+ ws.send_json({
98
+ "type": "reset",
99
+ "data": {"task_id": "easy_screening", "seed": 42},
100
+ })
101
+ reset_resp = ws.receive_json()
102
+ assert reset_resp["type"] == "observation"
103
+ reset_data = reset_resp["data"]
104
+ assert reset_data["done"] is False
105
+ obs = reset_data["observation"]
106
+ assert obs["task_id"] == "easy_screening"
107
+ meds = obs["current_medications"]
108
+ assert len(meds) >= 3
109
+
110
+ # Step – query DDI
111
+ if len(meds) >= 2:
112
+ ws.send_json({
113
+ "type": "step",
114
+ "data": {
115
+ "action_type": "query_ddi",
116
+ "drug_id_1": meds[0]["drug_id"],
117
+ "drug_id_2": meds[1]["drug_id"],
118
+ },
119
+ })
120
+ step_resp = ws.receive_json()
121
+ assert step_resp["type"] == "observation"
122
+ assert step_resp["data"]["done"] is False
123
+
124
+ # Finish
125
+ ws.send_json({
126
+ "type": "step",
127
+ "data": {"action_type": "finish_review"},
128
+ })
129
+ finish_resp = ws.receive_json()
130
+ assert finish_resp["type"] == "observation"
131
+ assert finish_resp["data"]["done"] is True
132
+
133
+ def test_ws_state(self, client: TestClient) -> None:
134
+ with client.websocket_connect("/ws") as ws:
135
+ ws.send_json({
136
+ "type": "reset",
137
+ "data": {"task_id": "easy_screening", "seed": 0},
138
+ })
139
+ ws.receive_json() # consume reset response
140
+
141
+ ws.send_json({"type": "state"})
142
+ state_resp = ws.receive_json()
143
+ assert state_resp["type"] == "state"
144
+ state_data = state_resp["data"]
145
+ assert state_data["step_count"] == 0
146
+ assert state_data["task_id"] == "easy_screening"
openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the PolypharmacyEnv core."""
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 (
9
+ PolypharmacyAction,
10
+ PolypharmacyObservation,
11
+ PolypharmacyState,
12
+ )
13
+
14
+
15
+ class TestReset:
16
+ def test_reset_returns_observation(self) -> None:
17
+ env = PolypharmacyEnv()
18
+ obs = env.reset(task_id="easy_screening", seed=42)
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) -> None:
25
+ env = PolypharmacyEnv()
26
+ obs = env.reset(task_id="budgeted_screening", seed=0)
27
+ assert obs.remaining_query_budget == 8
28
+ assert obs.remaining_intervention_budget == 3
29
+ assert len(obs.current_medications) >= 6
30
+
31
+ def test_reset_hard(self) -> None:
32
+ env = PolypharmacyEnv()
33
+ obs = env.reset(task_id="complex_tradeoff", seed=0)
34
+ assert obs.remaining_query_budget == 12
35
+ assert obs.remaining_intervention_budget == 5
36
+ assert len(obs.current_medications) >= 10
37
+
38
+ def test_default_task(self) -> None:
39
+ env = PolypharmacyEnv()
40
+ obs = env.reset(seed=0)
41
+ assert obs.task_id == "budgeted_screening"
42
+
43
+
44
+ class TestStep:
45
+ def test_query_ddi(self) -> None:
46
+ env = PolypharmacyEnv()
47
+ obs = env.reset(task_id="easy_screening", seed=42)
48
+ meds = obs.current_medications
49
+ assert len(meds) >= 2
50
+
51
+ action = PolypharmacyAction(
52
+ action_type="query_ddi",
53
+ drug_id_1=meds[0].drug_id,
54
+ drug_id_2=meds[1].drug_id,
55
+ )
56
+ obs = env.step(action)
57
+ assert isinstance(obs, PolypharmacyObservation)
58
+ assert obs.done is False
59
+ assert obs.step_index == 1
60
+ assert len(obs.interaction_queries) == 1
61
+
62
+ def test_invalid_action_penalised(self) -> None:
63
+ env = PolypharmacyEnv()
64
+ env.reset(task_id="easy_screening", seed=42)
65
+
66
+ action = PolypharmacyAction(
67
+ action_type="propose_intervention",
68
+ target_drug_id=None,
69
+ intervention_type=None,
70
+ )
71
+ obs = env.step(action)
72
+ assert obs.reward is not None
73
+ assert obs.reward < 0 # penalty
74
+
75
+ def test_finish_review(self) -> None:
76
+ env = PolypharmacyEnv()
77
+ env.reset(task_id="easy_screening", seed=42)
78
+
79
+ action = PolypharmacyAction(action_type="finish_review")
80
+ obs = env.step(action)
81
+ assert obs.done is True
82
+ assert "grader_score" in obs.metadata
83
+ score = obs.metadata["grader_score"]
84
+ assert 0.0 <= score <= 1.0
85
+
86
+ def test_intervention_stop(self) -> None:
87
+ env = PolypharmacyEnv()
88
+ obs = env.reset(task_id="easy_screening", seed=42)
89
+ target = obs.current_medications[0].drug_id
90
+ n_meds = len(obs.current_medications)
91
+
92
+ action = PolypharmacyAction(
93
+ action_type="propose_intervention",
94
+ target_drug_id=target,
95
+ intervention_type="stop",
96
+ rationale="test stop",
97
+ )
98
+ obs = env.step(action)
99
+ assert len(obs.current_medications) == n_meds - 1
100
+
101
+ def test_budget_exhaustion(self) -> None:
102
+ env = PolypharmacyEnv()
103
+ obs = env.reset(task_id="easy_screening", seed=42)
104
+ meds = obs.current_medications
105
+
106
+ # Exhaust query budget (4 for easy)
107
+ for i in range(4):
108
+ a_idx = i % len(meds)
109
+ b_idx = (i + 1) % len(meds)
110
+ action = PolypharmacyAction(
111
+ action_type="query_ddi",
112
+ drug_id_1=meds[a_idx].drug_id,
113
+ drug_id_2=meds[b_idx].drug_id,
114
+ )
115
+ obs = env.step(action)
116
+ if obs.done:
117
+ break
118
+
119
+ if not obs.done:
120
+ assert obs.remaining_query_budget == 0
121
+ # Trying another query should be penalised
122
+ action = PolypharmacyAction(
123
+ action_type="query_ddi",
124
+ drug_id_1=meds[0].drug_id,
125
+ drug_id_2=meds[1].drug_id,
126
+ )
127
+ obs = env.step(action)
128
+ assert obs.reward is not None
129
+ assert obs.reward < 0
130
+
131
+ def test_max_steps_timeout(self) -> None:
132
+ env = PolypharmacyEnv()
133
+ obs = env.reset(task_id="easy_screening", seed=42) # max_steps=10
134
+ meds = obs.current_medications
135
+
136
+ # Keep querying until timeout
137
+ for i in range(15):
138
+ if obs.done:
139
+ break
140
+ a = meds[i % len(meds)].drug_id
141
+ b = meds[(i + 1) % len(meds)].drug_id
142
+ action = PolypharmacyAction(
143
+ action_type="query_ddi",
144
+ drug_id_1=a,
145
+ drug_id_2=b,
146
+ )
147
+ obs = env.step(action)
148
+
149
+ assert obs.done is True
150
+
151
+
152
+ class TestState:
153
+ def test_state_after_reset(self) -> None:
154
+ env = PolypharmacyEnv()
155
+ env.reset(task_id="easy_screening", seed=42)
156
+ st = env.state
157
+ assert isinstance(st, PolypharmacyState)
158
+ assert st.step_count == 0
159
+ assert st.episode_id is not None
160
+
161
+
162
+ class TestGraderDeterminism:
163
+ def test_same_trajectory_same_score(self) -> None:
164
+ scores = []
165
+ for _ in range(3):
166
+ env = PolypharmacyEnv()
167
+ env.reset(task_id="easy_screening", seed=99)
168
+ obs = env.step(PolypharmacyAction(action_type="finish_review"))
169
+ scores.append(obs.metadata.get("grader_score", 0.0))
170
+ assert all(s == scores[0] for s in scores)
171
+
172
+ def test_intervention_changes_score(self) -> None:
173
+ # No intervention
174
+ env = PolypharmacyEnv()
175
+ env.reset(task_id="budgeted_screening", seed=42)
176
+ obs = env.step(PolypharmacyAction(action_type="finish_review"))
177
+ score_noop = obs.metadata.get("grader_score", 0.0)
178
+
179
+ # With intervention
180
+ env2 = PolypharmacyEnv()
181
+ obs_init2 = env2.reset(task_id="budgeted_screening", seed=42)
182
+ if obs_init2.current_medications:
183
+ env2.step(PolypharmacyAction(
184
+ action_type="propose_intervention",
185
+ target_drug_id=obs_init2.current_medications[0].drug_id,
186
+ intervention_type="stop",
187
+ rationale="test",
188
+ ))
189
+ obs2 = env2.step(PolypharmacyAction(action_type="finish_review"))
190
+ score_act = obs2.metadata.get("grader_score", 0.0)
191
+
192
+ assert score_noop != score_act
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_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,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_FUROSEMIDE;DRUG_FLUOXETINE;DRUG_TRAMADOL,0.2733,easy
5
+ EP_0004,74,M,CKD,mild,impaired,DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.2833,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_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
8
+ EP_0007,90,M,BPH;OA,moderate,normal,DRUG_WARFARIN;DRUG_NAPROXEN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN,0.225,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_WARFARIN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN,0.22,easy
11
+ EP_0010,75,M,dementia;HTN;depression,normal,impaired,DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE,0.2833,easy
12
+ EP_0011,83,F,AF,moderate,normal,DRUG_ALPRAZOLAM;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_TRAMADOL,0.2275,easy
13
+ EP_0012,71,F,HTN;GERD;depression,normal,normal,DRUG_DIAZEPAM;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_TRAMADOL,0.348,easy
14
+ EP_0013,70,F,HF;HTN;AF,mild,normal,DRUG_ALPRAZOLAM;DRUG_FUROSEMIDE;DRUG_TRAMADOL,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_ALPRAZOLAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.3033,easy
18
+ EP_0017,83,F,CKD,severe,normal,DRUG_DIAZEPAM;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.3067,easy
19
+ EP_0018,70,F,CKD;HF;HTN,mild,normal,DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_ALPRAZOLAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.182,easy
20
+ EP_0019,84,M,DM;depression,normal,normal,DRUG_INSULIN_GLARGINE;DRUG_FLUOXETINE;DRUG_AMITRIPTYLINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL,0.434,easy
21
+ EP_0020,90,F,neuropathy;BPH;AF,normal,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_AMIODARONE;DRUG_TAMSULOSIN,0.2,easy
22
+ EP_0021,87,M,HTN;BPH;HF,normal,normal,DRUG_SPIRONOLACTONE;DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_AMLODIPINE,0.2125,easy
23
+ EP_0022,90,M,AF;GERD;DM,normal,impaired,DRUG_OMEPRAZOLE;DRUG_DIAZEPAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.23,easy
24
+ EP_0023,90,F,HF,normal,normal,DRUG_DIAZEPAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.3067,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_WARFARIN;DRUG_DIGOXIN;DRUG_AMIODARONE;DRUG_GABAPENTIN,0.2,easy
27
+ EP_0026,88,M,GERD;dementia,severe,normal,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_APIXABAN;DRUG_NAPROXEN,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_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_TRAMADOL,0.17,easy
31
+ EP_0030,87,F,dementia;HF;depression,normal,normal,DRUG_DIGOXIN;DRUG_FLUOXETINE;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
32
+ EP_0031,69,M,HF,severe,normal,DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.426,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_WARFARIN;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_HYDROCHLOROTHIAZIDE,0.225,easy
36
+ EP_0035,74,M,HTN;DM,normal,impaired,DRUG_FLUOXETINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_GLIPIZIDE;DRUG_METOPROLOL;DRUG_TRAMADOL,0.164,easy
37
+ EP_0036,80,F,DM;neuropathy;HTN,severe,normal,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_AMIODARONE;DRUG_AMITRIPTYLINE,0.2,easy
38
+ EP_0037,78,M,HF,normal,normal,DRUG_LISINOPRIL;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.2125,easy
39
+ EP_0038,89,F,HTN;AF,moderate,normal,DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.2833,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_ALPRAZOLAM;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;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/docker-compose.yml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.9"
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: .
7
+ dockerfile: backend/Dockerfile
8
+ container_name: polypharmacy-backend
9
+ env_file:
10
+ - .env
11
+ ports:
12
+ - "7860:7860"
13
+ volumes:
14
+ - ./backend/src:/app/backend/src
15
+ - ./data:/app/data
16
+ - ./scripts:/app/scripts
17
+ - ./backend:/app/backend
18
+ healthcheck:
19
+ test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
20
+ interval: 20s
21
+ timeout: 5s
22
+ retries: 5
23
+
24
+ frontend:
25
+ build:
26
+ context: .
27
+ dockerfile: frontend/Dockerfile
28
+ container_name: polypharmacy-frontend
29
+ depends_on:
30
+ - backend
31
+ ports:
32
+ - "5173:5173"
33
+ volumes:
34
+ - ./frontend:/app
35
+ - /app/node_modules
openenv-polypharmacy/frontend/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY frontend/package*.json ./
6
+ RUN npm ci
7
+
8
+ COPY frontend/ ./
9
+
10
+ EXPOSE 5173
11
+
12
+ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
openenv-polypharmacy/frontend/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Polypharmacy Control Center</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
openenv-polypharmacy/frontend/package-lock.json ADDED
@@ -0,0 +1,1677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "polypharmacy-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "polypharmacy-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1"
13
+ },
14
+ "devDependencies": {
15
+ "@vitejs/plugin-react": "^4.3.1",
16
+ "vite": "^5.4.2"
17
+ }
18
+ },
19
+ "node_modules/@babel/code-frame": {
20
+ "version": "7.29.0",
21
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
22
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
23
+ "dev": true,
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@babel/helper-validator-identifier": "^7.28.5",
27
+ "js-tokens": "^4.0.0",
28
+ "picocolors": "^1.1.1"
29
+ },
30
+ "engines": {
31
+ "node": ">=6.9.0"
32
+ }
33
+ },
34
+ "node_modules/@babel/compat-data": {
35
+ "version": "7.29.0",
36
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
37
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
38
+ "dev": true,
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": ">=6.9.0"
42
+ }
43
+ },
44
+ "node_modules/@babel/core": {
45
+ "version": "7.29.0",
46
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
47
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
48
+ "dev": true,
49
+ "license": "MIT",
50
+ "dependencies": {
51
+ "@babel/code-frame": "^7.29.0",
52
+ "@babel/generator": "^7.29.0",
53
+ "@babel/helper-compilation-targets": "^7.28.6",
54
+ "@babel/helper-module-transforms": "^7.28.6",
55
+ "@babel/helpers": "^7.28.6",
56
+ "@babel/parser": "^7.29.0",
57
+ "@babel/template": "^7.28.6",
58
+ "@babel/traverse": "^7.29.0",
59
+ "@babel/types": "^7.29.0",
60
+ "@jridgewell/remapping": "^2.3.5",
61
+ "convert-source-map": "^2.0.0",
62
+ "debug": "^4.1.0",
63
+ "gensync": "^1.0.0-beta.2",
64
+ "json5": "^2.2.3",
65
+ "semver": "^6.3.1"
66
+ },
67
+ "engines": {
68
+ "node": ">=6.9.0"
69
+ },
70
+ "funding": {
71
+ "type": "opencollective",
72
+ "url": "https://opencollective.com/babel"
73
+ }
74
+ },
75
+ "node_modules/@babel/generator": {
76
+ "version": "7.29.1",
77
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
78
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
79
+ "dev": true,
80
+ "license": "MIT",
81
+ "dependencies": {
82
+ "@babel/parser": "^7.29.0",
83
+ "@babel/types": "^7.29.0",
84
+ "@jridgewell/gen-mapping": "^0.3.12",
85
+ "@jridgewell/trace-mapping": "^0.3.28",
86
+ "jsesc": "^3.0.2"
87
+ },
88
+ "engines": {
89
+ "node": ">=6.9.0"
90
+ }
91
+ },
92
+ "node_modules/@babel/helper-compilation-targets": {
93
+ "version": "7.28.6",
94
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
95
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
96
+ "dev": true,
97
+ "license": "MIT",
98
+ "dependencies": {
99
+ "@babel/compat-data": "^7.28.6",
100
+ "@babel/helper-validator-option": "^7.27.1",
101
+ "browserslist": "^4.24.0",
102
+ "lru-cache": "^5.1.1",
103
+ "semver": "^6.3.1"
104
+ },
105
+ "engines": {
106
+ "node": ">=6.9.0"
107
+ }
108
+ },
109
+ "node_modules/@babel/helper-globals": {
110
+ "version": "7.28.0",
111
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
112
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
113
+ "dev": true,
114
+ "license": "MIT",
115
+ "engines": {
116
+ "node": ">=6.9.0"
117
+ }
118
+ },
119
+ "node_modules/@babel/helper-module-imports": {
120
+ "version": "7.28.6",
121
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
122
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
123
+ "dev": true,
124
+ "license": "MIT",
125
+ "dependencies": {
126
+ "@babel/traverse": "^7.28.6",
127
+ "@babel/types": "^7.28.6"
128
+ },
129
+ "engines": {
130
+ "node": ">=6.9.0"
131
+ }
132
+ },
133
+ "node_modules/@babel/helper-module-transforms": {
134
+ "version": "7.28.6",
135
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
136
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
137
+ "dev": true,
138
+ "license": "MIT",
139
+ "dependencies": {
140
+ "@babel/helper-module-imports": "^7.28.6",
141
+ "@babel/helper-validator-identifier": "^7.28.5",
142
+ "@babel/traverse": "^7.28.6"
143
+ },
144
+ "engines": {
145
+ "node": ">=6.9.0"
146
+ },
147
+ "peerDependencies": {
148
+ "@babel/core": "^7.0.0"
149
+ }
150
+ },
151
+ "node_modules/@babel/helper-plugin-utils": {
152
+ "version": "7.28.6",
153
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
154
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
155
+ "dev": true,
156
+ "license": "MIT",
157
+ "engines": {
158
+ "node": ">=6.9.0"
159
+ }
160
+ },
161
+ "node_modules/@babel/helper-string-parser": {
162
+ "version": "7.27.1",
163
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
164
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
165
+ "dev": true,
166
+ "license": "MIT",
167
+ "engines": {
168
+ "node": ">=6.9.0"
169
+ }
170
+ },
171
+ "node_modules/@babel/helper-validator-identifier": {
172
+ "version": "7.28.5",
173
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
174
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
175
+ "dev": true,
176
+ "license": "MIT",
177
+ "engines": {
178
+ "node": ">=6.9.0"
179
+ }
180
+ },
181
+ "node_modules/@babel/helper-validator-option": {
182
+ "version": "7.27.1",
183
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
184
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
185
+ "dev": true,
186
+ "license": "MIT",
187
+ "engines": {
188
+ "node": ">=6.9.0"
189
+ }
190
+ },
191
+ "node_modules/@babel/helpers": {
192
+ "version": "7.29.2",
193
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
194
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
195
+ "dev": true,
196
+ "license": "MIT",
197
+ "dependencies": {
198
+ "@babel/template": "^7.28.6",
199
+ "@babel/types": "^7.29.0"
200
+ },
201
+ "engines": {
202
+ "node": ">=6.9.0"
203
+ }
204
+ },
205
+ "node_modules/@babel/parser": {
206
+ "version": "7.29.2",
207
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
208
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
209
+ "dev": true,
210
+ "license": "MIT",
211
+ "dependencies": {
212
+ "@babel/types": "^7.29.0"
213
+ },
214
+ "bin": {
215
+ "parser": "bin/babel-parser.js"
216
+ },
217
+ "engines": {
218
+ "node": ">=6.0.0"
219
+ }
220
+ },
221
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
222
+ "version": "7.27.1",
223
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
224
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
225
+ "dev": true,
226
+ "license": "MIT",
227
+ "dependencies": {
228
+ "@babel/helper-plugin-utils": "^7.27.1"
229
+ },
230
+ "engines": {
231
+ "node": ">=6.9.0"
232
+ },
233
+ "peerDependencies": {
234
+ "@babel/core": "^7.0.0-0"
235
+ }
236
+ },
237
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
238
+ "version": "7.27.1",
239
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
240
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
241
+ "dev": true,
242
+ "license": "MIT",
243
+ "dependencies": {
244
+ "@babel/helper-plugin-utils": "^7.27.1"
245
+ },
246
+ "engines": {
247
+ "node": ">=6.9.0"
248
+ },
249
+ "peerDependencies": {
250
+ "@babel/core": "^7.0.0-0"
251
+ }
252
+ },
253
+ "node_modules/@babel/template": {
254
+ "version": "7.28.6",
255
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
256
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
257
+ "dev": true,
258
+ "license": "MIT",
259
+ "dependencies": {
260
+ "@babel/code-frame": "^7.28.6",
261
+ "@babel/parser": "^7.28.6",
262
+ "@babel/types": "^7.28.6"
263
+ },
264
+ "engines": {
265
+ "node": ">=6.9.0"
266
+ }
267
+ },
268
+ "node_modules/@babel/traverse": {
269
+ "version": "7.29.0",
270
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
271
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
272
+ "dev": true,
273
+ "license": "MIT",
274
+ "dependencies": {
275
+ "@babel/code-frame": "^7.29.0",
276
+ "@babel/generator": "^7.29.0",
277
+ "@babel/helper-globals": "^7.28.0",
278
+ "@babel/parser": "^7.29.0",
279
+ "@babel/template": "^7.28.6",
280
+ "@babel/types": "^7.29.0",
281
+ "debug": "^4.3.1"
282
+ },
283
+ "engines": {
284
+ "node": ">=6.9.0"
285
+ }
286
+ },
287
+ "node_modules/@babel/types": {
288
+ "version": "7.29.0",
289
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
290
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
291
+ "dev": true,
292
+ "license": "MIT",
293
+ "dependencies": {
294
+ "@babel/helper-string-parser": "^7.27.1",
295
+ "@babel/helper-validator-identifier": "^7.28.5"
296
+ },
297
+ "engines": {
298
+ "node": ">=6.9.0"
299
+ }
300
+ },
301
+ "node_modules/@esbuild/aix-ppc64": {
302
+ "version": "0.21.5",
303
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
304
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
305
+ "cpu": [
306
+ "ppc64"
307
+ ],
308
+ "dev": true,
309
+ "license": "MIT",
310
+ "optional": true,
311
+ "os": [
312
+ "aix"
313
+ ],
314
+ "engines": {
315
+ "node": ">=12"
316
+ }
317
+ },
318
+ "node_modules/@esbuild/android-arm": {
319
+ "version": "0.21.5",
320
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
321
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
322
+ "cpu": [
323
+ "arm"
324
+ ],
325
+ "dev": true,
326
+ "license": "MIT",
327
+ "optional": true,
328
+ "os": [
329
+ "android"
330
+ ],
331
+ "engines": {
332
+ "node": ">=12"
333
+ }
334
+ },
335
+ "node_modules/@esbuild/android-arm64": {
336
+ "version": "0.21.5",
337
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
338
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
339
+ "cpu": [
340
+ "arm64"
341
+ ],
342
+ "dev": true,
343
+ "license": "MIT",
344
+ "optional": true,
345
+ "os": [
346
+ "android"
347
+ ],
348
+ "engines": {
349
+ "node": ">=12"
350
+ }
351
+ },
352
+ "node_modules/@esbuild/android-x64": {
353
+ "version": "0.21.5",
354
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
355
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
356
+ "cpu": [
357
+ "x64"
358
+ ],
359
+ "dev": true,
360
+ "license": "MIT",
361
+ "optional": true,
362
+ "os": [
363
+ "android"
364
+ ],
365
+ "engines": {
366
+ "node": ">=12"
367
+ }
368
+ },
369
+ "node_modules/@esbuild/darwin-arm64": {
370
+ "version": "0.21.5",
371
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
372
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
373
+ "cpu": [
374
+ "arm64"
375
+ ],
376
+ "dev": true,
377
+ "license": "MIT",
378
+ "optional": true,
379
+ "os": [
380
+ "darwin"
381
+ ],
382
+ "engines": {
383
+ "node": ">=12"
384
+ }
385
+ },
386
+ "node_modules/@esbuild/darwin-x64": {
387
+ "version": "0.21.5",
388
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
389
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
390
+ "cpu": [
391
+ "x64"
392
+ ],
393
+ "dev": true,
394
+ "license": "MIT",
395
+ "optional": true,
396
+ "os": [
397
+ "darwin"
398
+ ],
399
+ "engines": {
400
+ "node": ">=12"
401
+ }
402
+ },
403
+ "node_modules/@esbuild/freebsd-arm64": {
404
+ "version": "0.21.5",
405
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
406
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
407
+ "cpu": [
408
+ "arm64"
409
+ ],
410
+ "dev": true,
411
+ "license": "MIT",
412
+ "optional": true,
413
+ "os": [
414
+ "freebsd"
415
+ ],
416
+ "engines": {
417
+ "node": ">=12"
418
+ }
419
+ },
420
+ "node_modules/@esbuild/freebsd-x64": {
421
+ "version": "0.21.5",
422
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
423
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
424
+ "cpu": [
425
+ "x64"
426
+ ],
427
+ "dev": true,
428
+ "license": "MIT",
429
+ "optional": true,
430
+ "os": [
431
+ "freebsd"
432
+ ],
433
+ "engines": {
434
+ "node": ">=12"
435
+ }
436
+ },
437
+ "node_modules/@esbuild/linux-arm": {
438
+ "version": "0.21.5",
439
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
440
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
441
+ "cpu": [
442
+ "arm"
443
+ ],
444
+ "dev": true,
445
+ "license": "MIT",
446
+ "optional": true,
447
+ "os": [
448
+ "linux"
449
+ ],
450
+ "engines": {
451
+ "node": ">=12"
452
+ }
453
+ },
454
+ "node_modules/@esbuild/linux-arm64": {
455
+ "version": "0.21.5",
456
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
457
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
458
+ "cpu": [
459
+ "arm64"
460
+ ],
461
+ "dev": true,
462
+ "license": "MIT",
463
+ "optional": true,
464
+ "os": [
465
+ "linux"
466
+ ],
467
+ "engines": {
468
+ "node": ">=12"
469
+ }
470
+ },
471
+ "node_modules/@esbuild/linux-ia32": {
472
+ "version": "0.21.5",
473
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
474
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
475
+ "cpu": [
476
+ "ia32"
477
+ ],
478
+ "dev": true,
479
+ "license": "MIT",
480
+ "optional": true,
481
+ "os": [
482
+ "linux"
483
+ ],
484
+ "engines": {
485
+ "node": ">=12"
486
+ }
487
+ },
488
+ "node_modules/@esbuild/linux-loong64": {
489
+ "version": "0.21.5",
490
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
491
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
492
+ "cpu": [
493
+ "loong64"
494
+ ],
495
+ "dev": true,
496
+ "license": "MIT",
497
+ "optional": true,
498
+ "os": [
499
+ "linux"
500
+ ],
501
+ "engines": {
502
+ "node": ">=12"
503
+ }
504
+ },
505
+ "node_modules/@esbuild/linux-mips64el": {
506
+ "version": "0.21.5",
507
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
508
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
509
+ "cpu": [
510
+ "mips64el"
511
+ ],
512
+ "dev": true,
513
+ "license": "MIT",
514
+ "optional": true,
515
+ "os": [
516
+ "linux"
517
+ ],
518
+ "engines": {
519
+ "node": ">=12"
520
+ }
521
+ },
522
+ "node_modules/@esbuild/linux-ppc64": {
523
+ "version": "0.21.5",
524
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
525
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
526
+ "cpu": [
527
+ "ppc64"
528
+ ],
529
+ "dev": true,
530
+ "license": "MIT",
531
+ "optional": true,
532
+ "os": [
533
+ "linux"
534
+ ],
535
+ "engines": {
536
+ "node": ">=12"
537
+ }
538
+ },
539
+ "node_modules/@esbuild/linux-riscv64": {
540
+ "version": "0.21.5",
541
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
542
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
543
+ "cpu": [
544
+ "riscv64"
545
+ ],
546
+ "dev": true,
547
+ "license": "MIT",
548
+ "optional": true,
549
+ "os": [
550
+ "linux"
551
+ ],
552
+ "engines": {
553
+ "node": ">=12"
554
+ }
555
+ },
556
+ "node_modules/@esbuild/linux-s390x": {
557
+ "version": "0.21.5",
558
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
559
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
560
+ "cpu": [
561
+ "s390x"
562
+ ],
563
+ "dev": true,
564
+ "license": "MIT",
565
+ "optional": true,
566
+ "os": [
567
+ "linux"
568
+ ],
569
+ "engines": {
570
+ "node": ">=12"
571
+ }
572
+ },
573
+ "node_modules/@esbuild/linux-x64": {
574
+ "version": "0.21.5",
575
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
576
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
577
+ "cpu": [
578
+ "x64"
579
+ ],
580
+ "dev": true,
581
+ "license": "MIT",
582
+ "optional": true,
583
+ "os": [
584
+ "linux"
585
+ ],
586
+ "engines": {
587
+ "node": ">=12"
588
+ }
589
+ },
590
+ "node_modules/@esbuild/netbsd-x64": {
591
+ "version": "0.21.5",
592
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
593
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
594
+ "cpu": [
595
+ "x64"
596
+ ],
597
+ "dev": true,
598
+ "license": "MIT",
599
+ "optional": true,
600
+ "os": [
601
+ "netbsd"
602
+ ],
603
+ "engines": {
604
+ "node": ">=12"
605
+ }
606
+ },
607
+ "node_modules/@esbuild/openbsd-x64": {
608
+ "version": "0.21.5",
609
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
610
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
611
+ "cpu": [
612
+ "x64"
613
+ ],
614
+ "dev": true,
615
+ "license": "MIT",
616
+ "optional": true,
617
+ "os": [
618
+ "openbsd"
619
+ ],
620
+ "engines": {
621
+ "node": ">=12"
622
+ }
623
+ },
624
+ "node_modules/@esbuild/sunos-x64": {
625
+ "version": "0.21.5",
626
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
627
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
628
+ "cpu": [
629
+ "x64"
630
+ ],
631
+ "dev": true,
632
+ "license": "MIT",
633
+ "optional": true,
634
+ "os": [
635
+ "sunos"
636
+ ],
637
+ "engines": {
638
+ "node": ">=12"
639
+ }
640
+ },
641
+ "node_modules/@esbuild/win32-arm64": {
642
+ "version": "0.21.5",
643
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
644
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
645
+ "cpu": [
646
+ "arm64"
647
+ ],
648
+ "dev": true,
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "win32"
653
+ ],
654
+ "engines": {
655
+ "node": ">=12"
656
+ }
657
+ },
658
+ "node_modules/@esbuild/win32-ia32": {
659
+ "version": "0.21.5",
660
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
661
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
662
+ "cpu": [
663
+ "ia32"
664
+ ],
665
+ "dev": true,
666
+ "license": "MIT",
667
+ "optional": true,
668
+ "os": [
669
+ "win32"
670
+ ],
671
+ "engines": {
672
+ "node": ">=12"
673
+ }
674
+ },
675
+ "node_modules/@esbuild/win32-x64": {
676
+ "version": "0.21.5",
677
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
678
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
679
+ "cpu": [
680
+ "x64"
681
+ ],
682
+ "dev": true,
683
+ "license": "MIT",
684
+ "optional": true,
685
+ "os": [
686
+ "win32"
687
+ ],
688
+ "engines": {
689
+ "node": ">=12"
690
+ }
691
+ },
692
+ "node_modules/@jridgewell/gen-mapping": {
693
+ "version": "0.3.13",
694
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
695
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
696
+ "dev": true,
697
+ "license": "MIT",
698
+ "dependencies": {
699
+ "@jridgewell/sourcemap-codec": "^1.5.0",
700
+ "@jridgewell/trace-mapping": "^0.3.24"
701
+ }
702
+ },
703
+ "node_modules/@jridgewell/remapping": {
704
+ "version": "2.3.5",
705
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
706
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
707
+ "dev": true,
708
+ "license": "MIT",
709
+ "dependencies": {
710
+ "@jridgewell/gen-mapping": "^0.3.5",
711
+ "@jridgewell/trace-mapping": "^0.3.24"
712
+ }
713
+ },
714
+ "node_modules/@jridgewell/resolve-uri": {
715
+ "version": "3.1.2",
716
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
717
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
718
+ "dev": true,
719
+ "license": "MIT",
720
+ "engines": {
721
+ "node": ">=6.0.0"
722
+ }
723
+ },
724
+ "node_modules/@jridgewell/sourcemap-codec": {
725
+ "version": "1.5.5",
726
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
727
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
728
+ "dev": true,
729
+ "license": "MIT"
730
+ },
731
+ "node_modules/@jridgewell/trace-mapping": {
732
+ "version": "0.3.31",
733
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
734
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
735
+ "dev": true,
736
+ "license": "MIT",
737
+ "dependencies": {
738
+ "@jridgewell/resolve-uri": "^3.1.0",
739
+ "@jridgewell/sourcemap-codec": "^1.4.14"
740
+ }
741
+ },
742
+ "node_modules/@rolldown/pluginutils": {
743
+ "version": "1.0.0-beta.27",
744
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
745
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
746
+ "dev": true,
747
+ "license": "MIT"
748
+ },
749
+ "node_modules/@rollup/rollup-android-arm-eabi": {
750
+ "version": "4.60.1",
751
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
752
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
753
+ "cpu": [
754
+ "arm"
755
+ ],
756
+ "dev": true,
757
+ "license": "MIT",
758
+ "optional": true,
759
+ "os": [
760
+ "android"
761
+ ]
762
+ },
763
+ "node_modules/@rollup/rollup-android-arm64": {
764
+ "version": "4.60.1",
765
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
766
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
767
+ "cpu": [
768
+ "arm64"
769
+ ],
770
+ "dev": true,
771
+ "license": "MIT",
772
+ "optional": true,
773
+ "os": [
774
+ "android"
775
+ ]
776
+ },
777
+ "node_modules/@rollup/rollup-darwin-arm64": {
778
+ "version": "4.60.1",
779
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
780
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
781
+ "cpu": [
782
+ "arm64"
783
+ ],
784
+ "dev": true,
785
+ "license": "MIT",
786
+ "optional": true,
787
+ "os": [
788
+ "darwin"
789
+ ]
790
+ },
791
+ "node_modules/@rollup/rollup-darwin-x64": {
792
+ "version": "4.60.1",
793
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
794
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
795
+ "cpu": [
796
+ "x64"
797
+ ],
798
+ "dev": true,
799
+ "license": "MIT",
800
+ "optional": true,
801
+ "os": [
802
+ "darwin"
803
+ ]
804
+ },
805
+ "node_modules/@rollup/rollup-freebsd-arm64": {
806
+ "version": "4.60.1",
807
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
808
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
809
+ "cpu": [
810
+ "arm64"
811
+ ],
812
+ "dev": true,
813
+ "license": "MIT",
814
+ "optional": true,
815
+ "os": [
816
+ "freebsd"
817
+ ]
818
+ },
819
+ "node_modules/@rollup/rollup-freebsd-x64": {
820
+ "version": "4.60.1",
821
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
822
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
823
+ "cpu": [
824
+ "x64"
825
+ ],
826
+ "dev": true,
827
+ "license": "MIT",
828
+ "optional": true,
829
+ "os": [
830
+ "freebsd"
831
+ ]
832
+ },
833
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
834
+ "version": "4.60.1",
835
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
836
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
837
+ "cpu": [
838
+ "arm"
839
+ ],
840
+ "dev": true,
841
+ "license": "MIT",
842
+ "optional": true,
843
+ "os": [
844
+ "linux"
845
+ ]
846
+ },
847
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
848
+ "version": "4.60.1",
849
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
850
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
851
+ "cpu": [
852
+ "arm"
853
+ ],
854
+ "dev": true,
855
+ "license": "MIT",
856
+ "optional": true,
857
+ "os": [
858
+ "linux"
859
+ ]
860
+ },
861
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
862
+ "version": "4.60.1",
863
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
864
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
865
+ "cpu": [
866
+ "arm64"
867
+ ],
868
+ "dev": true,
869
+ "license": "MIT",
870
+ "optional": true,
871
+ "os": [
872
+ "linux"
873
+ ]
874
+ },
875
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
876
+ "version": "4.60.1",
877
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
878
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
879
+ "cpu": [
880
+ "arm64"
881
+ ],
882
+ "dev": true,
883
+ "license": "MIT",
884
+ "optional": true,
885
+ "os": [
886
+ "linux"
887
+ ]
888
+ },
889
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
890
+ "version": "4.60.1",
891
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
892
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
893
+ "cpu": [
894
+ "loong64"
895
+ ],
896
+ "dev": true,
897
+ "license": "MIT",
898
+ "optional": true,
899
+ "os": [
900
+ "linux"
901
+ ]
902
+ },
903
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
904
+ "version": "4.60.1",
905
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
906
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
907
+ "cpu": [
908
+ "loong64"
909
+ ],
910
+ "dev": true,
911
+ "license": "MIT",
912
+ "optional": true,
913
+ "os": [
914
+ "linux"
915
+ ]
916
+ },
917
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
918
+ "version": "4.60.1",
919
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
920
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
921
+ "cpu": [
922
+ "ppc64"
923
+ ],
924
+ "dev": true,
925
+ "license": "MIT",
926
+ "optional": true,
927
+ "os": [
928
+ "linux"
929
+ ]
930
+ },
931
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
932
+ "version": "4.60.1",
933
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
934
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
935
+ "cpu": [
936
+ "ppc64"
937
+ ],
938
+ "dev": true,
939
+ "license": "MIT",
940
+ "optional": true,
941
+ "os": [
942
+ "linux"
943
+ ]
944
+ },
945
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
946
+ "version": "4.60.1",
947
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
948
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
949
+ "cpu": [
950
+ "riscv64"
951
+ ],
952
+ "dev": true,
953
+ "license": "MIT",
954
+ "optional": true,
955
+ "os": [
956
+ "linux"
957
+ ]
958
+ },
959
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
960
+ "version": "4.60.1",
961
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
962
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
963
+ "cpu": [
964
+ "riscv64"
965
+ ],
966
+ "dev": true,
967
+ "license": "MIT",
968
+ "optional": true,
969
+ "os": [
970
+ "linux"
971
+ ]
972
+ },
973
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
974
+ "version": "4.60.1",
975
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
976
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
977
+ "cpu": [
978
+ "s390x"
979
+ ],
980
+ "dev": true,
981
+ "license": "MIT",
982
+ "optional": true,
983
+ "os": [
984
+ "linux"
985
+ ]
986
+ },
987
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
988
+ "version": "4.60.1",
989
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
990
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
991
+ "cpu": [
992
+ "x64"
993
+ ],
994
+ "dev": true,
995
+ "license": "MIT",
996
+ "optional": true,
997
+ "os": [
998
+ "linux"
999
+ ]
1000
+ },
1001
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1002
+ "version": "4.60.1",
1003
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
1004
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
1005
+ "cpu": [
1006
+ "x64"
1007
+ ],
1008
+ "dev": true,
1009
+ "license": "MIT",
1010
+ "optional": true,
1011
+ "os": [
1012
+ "linux"
1013
+ ]
1014
+ },
1015
+ "node_modules/@rollup/rollup-openbsd-x64": {
1016
+ "version": "4.60.1",
1017
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
1018
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
1019
+ "cpu": [
1020
+ "x64"
1021
+ ],
1022
+ "dev": true,
1023
+ "license": "MIT",
1024
+ "optional": true,
1025
+ "os": [
1026
+ "openbsd"
1027
+ ]
1028
+ },
1029
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1030
+ "version": "4.60.1",
1031
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
1032
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
1033
+ "cpu": [
1034
+ "arm64"
1035
+ ],
1036
+ "dev": true,
1037
+ "license": "MIT",
1038
+ "optional": true,
1039
+ "os": [
1040
+ "openharmony"
1041
+ ]
1042
+ },
1043
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1044
+ "version": "4.60.1",
1045
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
1046
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
1047
+ "cpu": [
1048
+ "arm64"
1049
+ ],
1050
+ "dev": true,
1051
+ "license": "MIT",
1052
+ "optional": true,
1053
+ "os": [
1054
+ "win32"
1055
+ ]
1056
+ },
1057
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1058
+ "version": "4.60.1",
1059
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
1060
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
1061
+ "cpu": [
1062
+ "ia32"
1063
+ ],
1064
+ "dev": true,
1065
+ "license": "MIT",
1066
+ "optional": true,
1067
+ "os": [
1068
+ "win32"
1069
+ ]
1070
+ },
1071
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1072
+ "version": "4.60.1",
1073
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
1074
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
1075
+ "cpu": [
1076
+ "x64"
1077
+ ],
1078
+ "dev": true,
1079
+ "license": "MIT",
1080
+ "optional": true,
1081
+ "os": [
1082
+ "win32"
1083
+ ]
1084
+ },
1085
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1086
+ "version": "4.60.1",
1087
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
1088
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
1089
+ "cpu": [
1090
+ "x64"
1091
+ ],
1092
+ "dev": true,
1093
+ "license": "MIT",
1094
+ "optional": true,
1095
+ "os": [
1096
+ "win32"
1097
+ ]
1098
+ },
1099
+ "node_modules/@types/babel__core": {
1100
+ "version": "7.20.5",
1101
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1102
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1103
+ "dev": true,
1104
+ "license": "MIT",
1105
+ "dependencies": {
1106
+ "@babel/parser": "^7.20.7",
1107
+ "@babel/types": "^7.20.7",
1108
+ "@types/babel__generator": "*",
1109
+ "@types/babel__template": "*",
1110
+ "@types/babel__traverse": "*"
1111
+ }
1112
+ },
1113
+ "node_modules/@types/babel__generator": {
1114
+ "version": "7.27.0",
1115
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1116
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1117
+ "dev": true,
1118
+ "license": "MIT",
1119
+ "dependencies": {
1120
+ "@babel/types": "^7.0.0"
1121
+ }
1122
+ },
1123
+ "node_modules/@types/babel__template": {
1124
+ "version": "7.4.4",
1125
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1126
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1127
+ "dev": true,
1128
+ "license": "MIT",
1129
+ "dependencies": {
1130
+ "@babel/parser": "^7.1.0",
1131
+ "@babel/types": "^7.0.0"
1132
+ }
1133
+ },
1134
+ "node_modules/@types/babel__traverse": {
1135
+ "version": "7.28.0",
1136
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1137
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1138
+ "dev": true,
1139
+ "license": "MIT",
1140
+ "dependencies": {
1141
+ "@babel/types": "^7.28.2"
1142
+ }
1143
+ },
1144
+ "node_modules/@types/estree": {
1145
+ "version": "1.0.8",
1146
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1147
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1148
+ "dev": true,
1149
+ "license": "MIT"
1150
+ },
1151
+ "node_modules/@vitejs/plugin-react": {
1152
+ "version": "4.7.0",
1153
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1154
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1155
+ "dev": true,
1156
+ "license": "MIT",
1157
+ "dependencies": {
1158
+ "@babel/core": "^7.28.0",
1159
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1160
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1161
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1162
+ "@types/babel__core": "^7.20.5",
1163
+ "react-refresh": "^0.17.0"
1164
+ },
1165
+ "engines": {
1166
+ "node": "^14.18.0 || >=16.0.0"
1167
+ },
1168
+ "peerDependencies": {
1169
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1170
+ }
1171
+ },
1172
+ "node_modules/baseline-browser-mapping": {
1173
+ "version": "2.10.16",
1174
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
1175
+ "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
1176
+ "dev": true,
1177
+ "license": "Apache-2.0",
1178
+ "bin": {
1179
+ "baseline-browser-mapping": "dist/cli.cjs"
1180
+ },
1181
+ "engines": {
1182
+ "node": ">=6.0.0"
1183
+ }
1184
+ },
1185
+ "node_modules/browserslist": {
1186
+ "version": "4.28.2",
1187
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1188
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1189
+ "dev": true,
1190
+ "funding": [
1191
+ {
1192
+ "type": "opencollective",
1193
+ "url": "https://opencollective.com/browserslist"
1194
+ },
1195
+ {
1196
+ "type": "tidelift",
1197
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1198
+ },
1199
+ {
1200
+ "type": "github",
1201
+ "url": "https://github.com/sponsors/ai"
1202
+ }
1203
+ ],
1204
+ "license": "MIT",
1205
+ "dependencies": {
1206
+ "baseline-browser-mapping": "^2.10.12",
1207
+ "caniuse-lite": "^1.0.30001782",
1208
+ "electron-to-chromium": "^1.5.328",
1209
+ "node-releases": "^2.0.36",
1210
+ "update-browserslist-db": "^1.2.3"
1211
+ },
1212
+ "bin": {
1213
+ "browserslist": "cli.js"
1214
+ },
1215
+ "engines": {
1216
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1217
+ }
1218
+ },
1219
+ "node_modules/caniuse-lite": {
1220
+ "version": "1.0.30001786",
1221
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
1222
+ "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==",
1223
+ "dev": true,
1224
+ "funding": [
1225
+ {
1226
+ "type": "opencollective",
1227
+ "url": "https://opencollective.com/browserslist"
1228
+ },
1229
+ {
1230
+ "type": "tidelift",
1231
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1232
+ },
1233
+ {
1234
+ "type": "github",
1235
+ "url": "https://github.com/sponsors/ai"
1236
+ }
1237
+ ],
1238
+ "license": "CC-BY-4.0"
1239
+ },
1240
+ "node_modules/convert-source-map": {
1241
+ "version": "2.0.0",
1242
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1243
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1244
+ "dev": true,
1245
+ "license": "MIT"
1246
+ },
1247
+ "node_modules/debug": {
1248
+ "version": "4.4.3",
1249
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1250
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1251
+ "dev": true,
1252
+ "license": "MIT",
1253
+ "dependencies": {
1254
+ "ms": "^2.1.3"
1255
+ },
1256
+ "engines": {
1257
+ "node": ">=6.0"
1258
+ },
1259
+ "peerDependenciesMeta": {
1260
+ "supports-color": {
1261
+ "optional": true
1262
+ }
1263
+ }
1264
+ },
1265
+ "node_modules/electron-to-chromium": {
1266
+ "version": "1.5.331",
1267
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
1268
+ "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
1269
+ "dev": true,
1270
+ "license": "ISC"
1271
+ },
1272
+ "node_modules/esbuild": {
1273
+ "version": "0.21.5",
1274
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1275
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1276
+ "dev": true,
1277
+ "hasInstallScript": true,
1278
+ "license": "MIT",
1279
+ "bin": {
1280
+ "esbuild": "bin/esbuild"
1281
+ },
1282
+ "engines": {
1283
+ "node": ">=12"
1284
+ },
1285
+ "optionalDependencies": {
1286
+ "@esbuild/aix-ppc64": "0.21.5",
1287
+ "@esbuild/android-arm": "0.21.5",
1288
+ "@esbuild/android-arm64": "0.21.5",
1289
+ "@esbuild/android-x64": "0.21.5",
1290
+ "@esbuild/darwin-arm64": "0.21.5",
1291
+ "@esbuild/darwin-x64": "0.21.5",
1292
+ "@esbuild/freebsd-arm64": "0.21.5",
1293
+ "@esbuild/freebsd-x64": "0.21.5",
1294
+ "@esbuild/linux-arm": "0.21.5",
1295
+ "@esbuild/linux-arm64": "0.21.5",
1296
+ "@esbuild/linux-ia32": "0.21.5",
1297
+ "@esbuild/linux-loong64": "0.21.5",
1298
+ "@esbuild/linux-mips64el": "0.21.5",
1299
+ "@esbuild/linux-ppc64": "0.21.5",
1300
+ "@esbuild/linux-riscv64": "0.21.5",
1301
+ "@esbuild/linux-s390x": "0.21.5",
1302
+ "@esbuild/linux-x64": "0.21.5",
1303
+ "@esbuild/netbsd-x64": "0.21.5",
1304
+ "@esbuild/openbsd-x64": "0.21.5",
1305
+ "@esbuild/sunos-x64": "0.21.5",
1306
+ "@esbuild/win32-arm64": "0.21.5",
1307
+ "@esbuild/win32-ia32": "0.21.5",
1308
+ "@esbuild/win32-x64": "0.21.5"
1309
+ }
1310
+ },
1311
+ "node_modules/escalade": {
1312
+ "version": "3.2.0",
1313
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1314
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1315
+ "dev": true,
1316
+ "license": "MIT",
1317
+ "engines": {
1318
+ "node": ">=6"
1319
+ }
1320
+ },
1321
+ "node_modules/fsevents": {
1322
+ "version": "2.3.3",
1323
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1324
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1325
+ "dev": true,
1326
+ "hasInstallScript": true,
1327
+ "license": "MIT",
1328
+ "optional": true,
1329
+ "os": [
1330
+ "darwin"
1331
+ ],
1332
+ "engines": {
1333
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1334
+ }
1335
+ },
1336
+ "node_modules/gensync": {
1337
+ "version": "1.0.0-beta.2",
1338
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1339
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1340
+ "dev": true,
1341
+ "license": "MIT",
1342
+ "engines": {
1343
+ "node": ">=6.9.0"
1344
+ }
1345
+ },
1346
+ "node_modules/js-tokens": {
1347
+ "version": "4.0.0",
1348
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1349
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1350
+ "license": "MIT"
1351
+ },
1352
+ "node_modules/jsesc": {
1353
+ "version": "3.1.0",
1354
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1355
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1356
+ "dev": true,
1357
+ "license": "MIT",
1358
+ "bin": {
1359
+ "jsesc": "bin/jsesc"
1360
+ },
1361
+ "engines": {
1362
+ "node": ">=6"
1363
+ }
1364
+ },
1365
+ "node_modules/json5": {
1366
+ "version": "2.2.3",
1367
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1368
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1369
+ "dev": true,
1370
+ "license": "MIT",
1371
+ "bin": {
1372
+ "json5": "lib/cli.js"
1373
+ },
1374
+ "engines": {
1375
+ "node": ">=6"
1376
+ }
1377
+ },
1378
+ "node_modules/loose-envify": {
1379
+ "version": "1.4.0",
1380
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1381
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1382
+ "license": "MIT",
1383
+ "dependencies": {
1384
+ "js-tokens": "^3.0.0 || ^4.0.0"
1385
+ },
1386
+ "bin": {
1387
+ "loose-envify": "cli.js"
1388
+ }
1389
+ },
1390
+ "node_modules/lru-cache": {
1391
+ "version": "5.1.1",
1392
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1393
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1394
+ "dev": true,
1395
+ "license": "ISC",
1396
+ "dependencies": {
1397
+ "yallist": "^3.0.2"
1398
+ }
1399
+ },
1400
+ "node_modules/ms": {
1401
+ "version": "2.1.3",
1402
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1403
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1404
+ "dev": true,
1405
+ "license": "MIT"
1406
+ },
1407
+ "node_modules/nanoid": {
1408
+ "version": "3.3.11",
1409
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1410
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1411
+ "dev": true,
1412
+ "funding": [
1413
+ {
1414
+ "type": "github",
1415
+ "url": "https://github.com/sponsors/ai"
1416
+ }
1417
+ ],
1418
+ "license": "MIT",
1419
+ "bin": {
1420
+ "nanoid": "bin/nanoid.cjs"
1421
+ },
1422
+ "engines": {
1423
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1424
+ }
1425
+ },
1426
+ "node_modules/node-releases": {
1427
+ "version": "2.0.37",
1428
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
1429
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
1430
+ "dev": true,
1431
+ "license": "MIT"
1432
+ },
1433
+ "node_modules/picocolors": {
1434
+ "version": "1.1.1",
1435
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1436
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1437
+ "dev": true,
1438
+ "license": "ISC"
1439
+ },
1440
+ "node_modules/postcss": {
1441
+ "version": "8.5.8",
1442
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
1443
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
1444
+ "dev": true,
1445
+ "funding": [
1446
+ {
1447
+ "type": "opencollective",
1448
+ "url": "https://opencollective.com/postcss/"
1449
+ },
1450
+ {
1451
+ "type": "tidelift",
1452
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1453
+ },
1454
+ {
1455
+ "type": "github",
1456
+ "url": "https://github.com/sponsors/ai"
1457
+ }
1458
+ ],
1459
+ "license": "MIT",
1460
+ "dependencies": {
1461
+ "nanoid": "^3.3.11",
1462
+ "picocolors": "^1.1.1",
1463
+ "source-map-js": "^1.2.1"
1464
+ },
1465
+ "engines": {
1466
+ "node": "^10 || ^12 || >=14"
1467
+ }
1468
+ },
1469
+ "node_modules/react": {
1470
+ "version": "18.3.1",
1471
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1472
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1473
+ "license": "MIT",
1474
+ "dependencies": {
1475
+ "loose-envify": "^1.1.0"
1476
+ },
1477
+ "engines": {
1478
+ "node": ">=0.10.0"
1479
+ }
1480
+ },
1481
+ "node_modules/react-dom": {
1482
+ "version": "18.3.1",
1483
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1484
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1485
+ "license": "MIT",
1486
+ "dependencies": {
1487
+ "loose-envify": "^1.1.0",
1488
+ "scheduler": "^0.23.2"
1489
+ },
1490
+ "peerDependencies": {
1491
+ "react": "^18.3.1"
1492
+ }
1493
+ },
1494
+ "node_modules/react-refresh": {
1495
+ "version": "0.17.0",
1496
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1497
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1498
+ "dev": true,
1499
+ "license": "MIT",
1500
+ "engines": {
1501
+ "node": ">=0.10.0"
1502
+ }
1503
+ },
1504
+ "node_modules/rollup": {
1505
+ "version": "4.60.1",
1506
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
1507
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
1508
+ "dev": true,
1509
+ "license": "MIT",
1510
+ "dependencies": {
1511
+ "@types/estree": "1.0.8"
1512
+ },
1513
+ "bin": {
1514
+ "rollup": "dist/bin/rollup"
1515
+ },
1516
+ "engines": {
1517
+ "node": ">=18.0.0",
1518
+ "npm": ">=8.0.0"
1519
+ },
1520
+ "optionalDependencies": {
1521
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
1522
+ "@rollup/rollup-android-arm64": "4.60.1",
1523
+ "@rollup/rollup-darwin-arm64": "4.60.1",
1524
+ "@rollup/rollup-darwin-x64": "4.60.1",
1525
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
1526
+ "@rollup/rollup-freebsd-x64": "4.60.1",
1527
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
1528
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
1529
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
1530
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
1531
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
1532
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
1533
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
1534
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
1535
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
1536
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
1537
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
1538
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
1539
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
1540
+ "@rollup/rollup-openbsd-x64": "4.60.1",
1541
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
1542
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
1543
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
1544
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
1545
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
1546
+ "fsevents": "~2.3.2"
1547
+ }
1548
+ },
1549
+ "node_modules/scheduler": {
1550
+ "version": "0.23.2",
1551
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1552
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1553
+ "license": "MIT",
1554
+ "dependencies": {
1555
+ "loose-envify": "^1.1.0"
1556
+ }
1557
+ },
1558
+ "node_modules/semver": {
1559
+ "version": "6.3.1",
1560
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1561
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1562
+ "dev": true,
1563
+ "license": "ISC",
1564
+ "bin": {
1565
+ "semver": "bin/semver.js"
1566
+ }
1567
+ },
1568
+ "node_modules/source-map-js": {
1569
+ "version": "1.2.1",
1570
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1571
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1572
+ "dev": true,
1573
+ "license": "BSD-3-Clause",
1574
+ "engines": {
1575
+ "node": ">=0.10.0"
1576
+ }
1577
+ },
1578
+ "node_modules/update-browserslist-db": {
1579
+ "version": "1.2.3",
1580
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1581
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1582
+ "dev": true,
1583
+ "funding": [
1584
+ {
1585
+ "type": "opencollective",
1586
+ "url": "https://opencollective.com/browserslist"
1587
+ },
1588
+ {
1589
+ "type": "tidelift",
1590
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1591
+ },
1592
+ {
1593
+ "type": "github",
1594
+ "url": "https://github.com/sponsors/ai"
1595
+ }
1596
+ ],
1597
+ "license": "MIT",
1598
+ "dependencies": {
1599
+ "escalade": "^3.2.0",
1600
+ "picocolors": "^1.1.1"
1601
+ },
1602
+ "bin": {
1603
+ "update-browserslist-db": "cli.js"
1604
+ },
1605
+ "peerDependencies": {
1606
+ "browserslist": ">= 4.21.0"
1607
+ }
1608
+ },
1609
+ "node_modules/vite": {
1610
+ "version": "5.4.21",
1611
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1612
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1613
+ "dev": true,
1614
+ "license": "MIT",
1615
+ "dependencies": {
1616
+ "esbuild": "^0.21.3",
1617
+ "postcss": "^8.4.43",
1618
+ "rollup": "^4.20.0"
1619
+ },
1620
+ "bin": {
1621
+ "vite": "bin/vite.js"
1622
+ },
1623
+ "engines": {
1624
+ "node": "^18.0.0 || >=20.0.0"
1625
+ },
1626
+ "funding": {
1627
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1628
+ },
1629
+ "optionalDependencies": {
1630
+ "fsevents": "~2.3.3"
1631
+ },
1632
+ "peerDependencies": {
1633
+ "@types/node": "^18.0.0 || >=20.0.0",
1634
+ "less": "*",
1635
+ "lightningcss": "^1.21.0",
1636
+ "sass": "*",
1637
+ "sass-embedded": "*",
1638
+ "stylus": "*",
1639
+ "sugarss": "*",
1640
+ "terser": "^5.4.0"
1641
+ },
1642
+ "peerDependenciesMeta": {
1643
+ "@types/node": {
1644
+ "optional": true
1645
+ },
1646
+ "less": {
1647
+ "optional": true
1648
+ },
1649
+ "lightningcss": {
1650
+ "optional": true
1651
+ },
1652
+ "sass": {
1653
+ "optional": true
1654
+ },
1655
+ "sass-embedded": {
1656
+ "optional": true
1657
+ },
1658
+ "stylus": {
1659
+ "optional": true
1660
+ },
1661
+ "sugarss": {
1662
+ "optional": true
1663
+ },
1664
+ "terser": {
1665
+ "optional": true
1666
+ }
1667
+ }
1668
+ },
1669
+ "node_modules/yallist": {
1670
+ "version": "3.1.1",
1671
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1672
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1673
+ "dev": true,
1674
+ "license": "ISC"
1675
+ }
1676
+ }
1677
+ }
openenv-polypharmacy/frontend/package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "polypharmacy-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview --port 4173"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.3.1",
17
+ "vite": "^5.4.2"
18
+ }
19
+ }
openenv-polypharmacy/frontend/src/App.jsx ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ const API_BASE = "http://localhost:7860";
4
+ const WS_URL = "ws://localhost:7860/ws";
5
+ const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
6
+
7
+ async function apiPost(path, body) {
8
+ const res = await fetch(`${API_BASE}${path}`, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify(body),
12
+ });
13
+ if (!res.ok) {
14
+ const msg = await res.text();
15
+ throw new Error(msg || `HTTP ${res.status}`);
16
+ }
17
+ return res.json();
18
+ }
19
+
20
+ export default function App() {
21
+ const [taskId, setTaskId] = useState("budgeted_screening");
22
+ const [obs, setObs] = useState(null);
23
+ const [log, setLog] = useState([]);
24
+ const [loading, setLoading] = useState(false);
25
+ const [action, setAction] = useState({
26
+ action_type: "query_ddi",
27
+ drug_id_1: "",
28
+ drug_id_2: "",
29
+ target_drug_id: "",
30
+ intervention_type: "stop",
31
+ proposed_new_drug_id: "",
32
+ rationale: "",
33
+ });
34
+
35
+ const medIds = useMemo(
36
+ () => (obs?.current_medications || []).map((m) => m.drug_id),
37
+ [obs]
38
+ );
39
+ const hasValidEpisode = Boolean(obs?.episode_id) && (obs?.current_medications?.length || 0) > 0;
40
+ const isDone = Boolean(obs?.done);
41
+ const finalScore =
42
+ typeof obs?.metadata?.grader_score === "number" ? obs.metadata.grader_score : null;
43
+ const noBudgetsLeft =
44
+ hasValidEpisode &&
45
+ (obs?.remaining_query_budget ?? 0) <= 0 &&
46
+ (obs?.remaining_intervention_budget ?? 0) <= 0;
47
+ const wsRef = useRef(null);
48
+ const pendingRef = useRef([]);
49
+
50
+ const wsEnsure = async () => {
51
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
52
+ if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) {
53
+ await new Promise((r) => setTimeout(r, 80));
54
+ return wsEnsure();
55
+ }
56
+
57
+ const ws = new WebSocket(WS_URL);
58
+ wsRef.current = ws;
59
+
60
+ ws.onmessage = (evt) => {
61
+ try {
62
+ const msg = JSON.parse(evt.data);
63
+ const pending = pendingRef.current.shift();
64
+ if (pending) pending.resolve(msg);
65
+ } catch (e) {
66
+ const pending = pendingRef.current.shift();
67
+ if (pending) pending.reject(e);
68
+ }
69
+ };
70
+ ws.onerror = (err) => {
71
+ const pending = pendingRef.current.shift();
72
+ if (pending) pending.reject(err);
73
+ };
74
+ ws.onclose = () => {
75
+ wsRef.current = null;
76
+ };
77
+
78
+ await new Promise((resolve, reject) => {
79
+ const t = setTimeout(() => reject(new Error("WebSocket connect timeout")), 2500);
80
+ ws.onopen = () => {
81
+ clearTimeout(t);
82
+ resolve();
83
+ };
84
+ });
85
+ return ws;
86
+ };
87
+
88
+ const wsSend = async (type, data) => {
89
+ const ws = await wsEnsure();
90
+ return await new Promise((resolve, reject) => {
91
+ pendingRef.current.push({ resolve, reject });
92
+ ws.send(JSON.stringify({ type, data }));
93
+ });
94
+ };
95
+
96
+ useEffect(() => {
97
+ return () => {
98
+ try {
99
+ wsRef.current?.close();
100
+ } catch {
101
+ // ignore
102
+ }
103
+ };
104
+ }, []);
105
+
106
+ const appendLog = (text) => {
107
+ setLog((prev) => [`${new Date().toLocaleTimeString()} ${text}`, ...prev].slice(0, 20));
108
+ };
109
+
110
+ const normalizeObsFromWs = (packetData) => {
111
+ const observation = packetData?.observation || {};
112
+ const mergedMetadata = {
113
+ ...(observation?.metadata || {}),
114
+ ...(packetData?.info || {}),
115
+ };
116
+ return {
117
+ ...observation,
118
+ done: Boolean(packetData?.done ?? observation?.done ?? false),
119
+ reward: packetData?.reward ?? observation?.reward ?? null,
120
+ metadata: mergedMetadata,
121
+ };
122
+ };
123
+
124
+ const handleReset = async () => {
125
+ setLoading(true);
126
+ try {
127
+ const msg = await wsSend("reset", { task_id: taskId });
128
+ const data = msg?.data || {};
129
+ const normalized = normalizeObsFromWs(data);
130
+ setObs(normalized);
131
+ const ids = (normalized?.current_medications || []).map((m) => m.drug_id);
132
+ setAction((prev) => ({
133
+ ...prev,
134
+ drug_id_1: ids[0] || "",
135
+ drug_id_2: ids[1] || "",
136
+ target_drug_id: ids[0] || "",
137
+ }));
138
+ appendLog(`Reset task=${taskId}`);
139
+ } catch (err) {
140
+ appendLog(`Reset failed: ${err.message}`);
141
+ } finally {
142
+ setLoading(false);
143
+ }
144
+ };
145
+
146
+ const buildActionPayload = () => {
147
+ if (noBudgetsLeft) {
148
+ return { action_type: "finish_review" };
149
+ }
150
+ if (action.action_type === "query_ddi") {
151
+ return {
152
+ action_type: "query_ddi",
153
+ drug_id_1: action.drug_id_1,
154
+ drug_id_2: action.drug_id_2,
155
+ };
156
+ }
157
+ if (action.action_type === "propose_intervention") {
158
+ return {
159
+ action_type: "propose_intervention",
160
+ target_drug_id: action.target_drug_id,
161
+ intervention_type: action.intervention_type,
162
+ proposed_new_drug_id: action.proposed_new_drug_id || undefined,
163
+ rationale: action.rationale || undefined,
164
+ };
165
+ }
166
+ return { action_type: "finish_review" };
167
+ };
168
+
169
+ const isActionValid = () => {
170
+ if (!hasValidEpisode) return false;
171
+ if (isDone) return false;
172
+ if (noBudgetsLeft) return true;
173
+ if (action.action_type === "query_ddi") {
174
+ return Boolean(action.drug_id_1 && action.drug_id_2);
175
+ }
176
+ if (action.action_type === "propose_intervention") {
177
+ return Boolean(action.target_drug_id && action.intervention_type);
178
+ }
179
+ return true;
180
+ };
181
+
182
+ const handleStep = async (overrideAction = null) => {
183
+ if (!hasValidEpisode) {
184
+ appendLog("Run Reset Episode before stepping.");
185
+ return;
186
+ }
187
+ setLoading(true);
188
+ try {
189
+ const payload = overrideAction || buildActionPayload();
190
+ const msg = await wsSend("step", payload);
191
+ const data = msg?.data || {};
192
+ const normalized = normalizeObsFromWs(data);
193
+ setObs(normalized);
194
+ appendLog(`Step: ${payload.action_type} -> reward=${data.reward ?? 0}`);
195
+ } catch (err) {
196
+ appendLog(`Step failed: ${err.message}`);
197
+ } finally {
198
+ setLoading(false);
199
+ }
200
+ };
201
+
202
+ const askAi = async () => {
203
+ if (!hasValidEpisode) {
204
+ appendLog("Run Reset Episode before asking AI.");
205
+ return;
206
+ }
207
+ setLoading(true);
208
+ try {
209
+ const data = await apiPost("/agent/suggest", { observation: obs });
210
+ appendLog(`AI suggestion: ${data.action.action_type}`);
211
+ await handleStep(data.action);
212
+ } catch (err) {
213
+ appendLog(`AI suggestion failed: ${err.message}`);
214
+ } finally {
215
+ setLoading(false);
216
+ }
217
+ };
218
+
219
+ return (
220
+ <div className="shell">
221
+ <div className="bg-orb orb-a" />
222
+ <div className="bg-orb orb-b" />
223
+
224
+ <div className="container">
225
+ <header className="topbar glass">
226
+ <div className="title-wrap">
227
+ <h1>Polypharmacy Control Center</h1>
228
+ </div>
229
+ <div className={`status-chip ${hasValidEpisode ? "live" : "idle"}`}>
230
+ {hasValidEpisode ? "Session Live" : "Waiting for reset"}
231
+ </div>
232
+ <div className="actions">
233
+ <select value={taskId} onChange={(e) => setTaskId(e.target.value)}>
234
+ {TASKS.map((t) => (
235
+ <option key={t} value={t}>
236
+ {t}
237
+ </option>
238
+ ))}
239
+ </select>
240
+ <button onClick={handleReset} disabled={loading}>
241
+ Reset Episode
242
+ </button>
243
+ <button className="secondary" onClick={askAi} disabled={!hasValidEpisode || isDone || loading}>
244
+ Ask AI + Auto Step
245
+ </button>
246
+ </div>
247
+ </header>
248
+
249
+ <main className="layout">
250
+ <section className="panel glass panel-wide">
251
+ <h2>Episode</h2>
252
+ {hasValidEpisode ? (
253
+ <div className="kpi-grid">
254
+ <div><span>Episode</span><strong>{obs.episode_id}</strong></div>
255
+ <div><span>Task</span><strong>{obs.task_id}</strong></div>
256
+ <div><span>Age / Sex</span><strong>{obs.age} / {obs.sex}</strong></div>
257
+ <div><span>Step</span><strong>{obs.step_index}</strong></div>
258
+ <div><span>Query budget</span><strong>{obs.remaining_query_budget}</strong></div>
259
+ <div><span>Intervention budget</span><strong>{obs.remaining_intervention_budget}</strong></div>
260
+ </div>
261
+ ) : (
262
+ <p className="muted">Start with Reset Episode. Until then, step actions are blocked.</p>
263
+ )}
264
+ {noBudgetsLeft && (
265
+ <p className="muted budget-note">Query and intervention budgets are exhausted. Finish review to get final score.</p>
266
+ )}
267
+ {isDone && (
268
+ <p className="muted budget-note">
269
+ Episode complete
270
+ {finalScore !== null ? ` • final score: ${finalScore.toFixed(3)}` : ""}.
271
+ Click Reset Episode to start a new case.
272
+ </p>
273
+ )}
274
+ </section>
275
+
276
+ <section className="panel glass">
277
+ <h2>Action Console</h2>
278
+ <div className="action-row">
279
+ <label>Action type</label>
280
+ <select
281
+ value={action.action_type}
282
+ onChange={(e) => setAction((a) => ({ ...a, action_type: e.target.value }))}
283
+ >
284
+ <option value="query_ddi">query_ddi</option>
285
+ <option value="propose_intervention">propose_intervention</option>
286
+ <option value="finish_review">finish_review</option>
287
+ </select>
288
+ </div>
289
+
290
+ {action.action_type === "query_ddi" && (
291
+ <div className="stack stack-two">
292
+ <input
293
+ placeholder="drug_id_1"
294
+ value={action.drug_id_1}
295
+ onChange={(e) => setAction((a) => ({ ...a, drug_id_1: e.target.value }))}
296
+ />
297
+ <input
298
+ placeholder="drug_id_2"
299
+ value={action.drug_id_2}
300
+ onChange={(e) => setAction((a) => ({ ...a, drug_id_2: e.target.value }))}
301
+ />
302
+ </div>
303
+ )}
304
+
305
+ {action.action_type === "propose_intervention" && (
306
+ <div className="stack">
307
+ <select
308
+ value={action.target_drug_id}
309
+ onChange={(e) => setAction((a) => ({ ...a, target_drug_id: e.target.value }))}
310
+ >
311
+ <option value="">Select target drug</option>
312
+ {medIds.map((id) => (
313
+ <option key={id} value={id}>
314
+ {id}
315
+ </option>
316
+ ))}
317
+ </select>
318
+ <select
319
+ value={action.intervention_type}
320
+ onChange={(e) => setAction((a) => ({ ...a, intervention_type: e.target.value }))}
321
+ >
322
+ <option value="stop">stop</option>
323
+ <option value="dose_reduce">dose_reduce</option>
324
+ <option value="substitute">substitute</option>
325
+ <option value="add_monitoring">add_monitoring</option>
326
+ </select>
327
+ <input
328
+ placeholder="proposed_new_drug_id (optional)"
329
+ value={action.proposed_new_drug_id}
330
+ onChange={(e) =>
331
+ setAction((a) => ({ ...a, proposed_new_drug_id: e.target.value }))
332
+ }
333
+ />
334
+ <input
335
+ placeholder="rationale (optional)"
336
+ value={action.rationale}
337
+ onChange={(e) => setAction((a) => ({ ...a, rationale: e.target.value }))}
338
+ />
339
+ </div>
340
+ )}
341
+ <button onClick={() => handleStep()} disabled={!isActionValid() || loading}>
342
+ {noBudgetsLeft ? "Finish Review" : "Submit Step"}
343
+ </button>
344
+ </section>
345
+
346
+ <section className="panel glass">
347
+ <h2>Current Medications</h2>
348
+ <div className="med-grid">
349
+ {(obs?.current_medications || []).map((m) => (
350
+ <div key={m.drug_id} className="med-card">
351
+ <strong>{m.drug_id}</strong>
352
+ <p>{m.generic_name}</p>
353
+ <small>{m.dose_mg} mg • {m.atc_class}</small>
354
+ </div>
355
+ ))}
356
+ </div>
357
+ </section>
358
+
359
+ <section className="panel glass">
360
+ <h2>Event Log</h2>
361
+ <div className="logs">
362
+ {log.map((line, idx) => (
363
+ <div key={idx}>{line}</div>
364
+ ))}
365
+ </div>
366
+ </section>
367
+ </main>
368
+ </div>
369
+ </div>
370
+ );
371
+ }
openenv-polypharmacy/frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+ import "./styles.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
openenv-polypharmacy/frontend/src/styles.css ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #eef5ff;
3
+ --panel: rgba(255, 255, 255, 0.82);
4
+ --panel-solid: #ffffff;
5
+ --text: #0b2445;
6
+ --muted: #5b7596;
7
+ --primary: #1f8bff;
8
+ --primary-2: #69beff;
9
+ --accent: #0dd3ff;
10
+ --border: rgba(93, 156, 219, 0.22);
11
+ --shadow: 0 20px 50px rgba(25, 83, 143, 0.12);
12
+ --shadow-strong: 0 20px 42px rgba(31, 112, 182, 0.24);
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ font-family: "Inter", "SF Pro Text", "Segoe UI", sans-serif;
22
+ background:
23
+ radial-gradient(circle at 8% 0%, #cce7ff 0%, rgba(204, 231, 255, 0) 42%),
24
+ radial-gradient(circle at 92% 100%, #d5efff 0%, rgba(213, 239, 255, 0) 42%),
25
+ var(--bg);
26
+ color: var(--text);
27
+ }
28
+
29
+ .shell {
30
+ min-height: 100vh;
31
+ position: relative;
32
+ padding: 28px 22px;
33
+ overflow: hidden;
34
+ }
35
+
36
+ .container {
37
+ width: min(1300px, 100%);
38
+ margin: 0 auto;
39
+ position: relative;
40
+ z-index: 1;
41
+ }
42
+
43
+ .bg-orb {
44
+ position: absolute;
45
+ border-radius: 50%;
46
+ filter: blur(18px);
47
+ opacity: 0.9;
48
+ }
49
+ .orb-a {
50
+ width: 420px;
51
+ height: 420px;
52
+ right: -120px;
53
+ top: -100px;
54
+ background: radial-gradient(circle, rgba(72, 168, 255, 0.5), rgba(72, 168, 255, 0.1));
55
+ }
56
+ .orb-b {
57
+ width: 360px;
58
+ height: 360px;
59
+ left: -100px;
60
+ bottom: -120px;
61
+ background: radial-gradient(circle, rgba(110, 200, 255, 0.4), rgba(141, 205, 255, 0.06));
62
+ }
63
+
64
+ .glass {
65
+ backdrop-filter: blur(12px);
66
+ border: 1px solid var(--border);
67
+ background: var(--panel);
68
+ box-shadow: var(--shadow);
69
+ }
70
+
71
+ .topbar {
72
+ border-radius: 20px;
73
+ padding: 18px;
74
+ display: grid;
75
+ grid-template-columns: 1.2fr auto 1fr;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ gap: 12px;
79
+ }
80
+
81
+ .title-wrap h1 {
82
+ margin: 0;
83
+ font-size: clamp(1.1rem, 1.5vw, 1.45rem);
84
+ letter-spacing: 0.01em;
85
+ }
86
+
87
+ .title-wrap p {
88
+ margin: 4px 0 0;
89
+ color: var(--muted);
90
+ font-size: 0.92rem;
91
+ }
92
+
93
+ .status-chip {
94
+ justify-self: center;
95
+ border-radius: 999px;
96
+ padding: 7px 12px;
97
+ font-size: 0.76rem;
98
+ font-weight: 700;
99
+ letter-spacing: 0.04em;
100
+ text-transform: uppercase;
101
+ border: 1px solid transparent;
102
+ }
103
+
104
+ .status-chip.live {
105
+ color: #0d6a3f;
106
+ background: rgba(130, 245, 195, 0.18);
107
+ border-color: rgba(70, 199, 142, 0.3);
108
+ }
109
+
110
+ .status-chip.idle {
111
+ color: #24527f;
112
+ background: rgba(114, 194, 255, 0.18);
113
+ border-color: rgba(62, 152, 223, 0.28);
114
+ }
115
+
116
+ .actions {
117
+ display: flex;
118
+ justify-content: flex-end;
119
+ gap: 10px;
120
+ flex-wrap: wrap;
121
+ }
122
+
123
+ button,
124
+ select,
125
+ input {
126
+ border: 1px solid var(--border);
127
+ border-radius: 12px;
128
+ padding: 10px 13px;
129
+ font-size: 0.92rem;
130
+ background: #fff;
131
+ color: var(--text);
132
+ min-height: 42px;
133
+ }
134
+
135
+ button {
136
+ cursor: pointer;
137
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-2) 78%, var(--accent) 100%);
138
+ color: #fff;
139
+ border: none;
140
+ font-weight: 700;
141
+ box-shadow: var(--shadow-strong);
142
+ transition: transform 120ms ease, filter 120ms ease;
143
+ }
144
+
145
+ button:hover {
146
+ transform: translateY(-1px);
147
+ filter: brightness(1.02);
148
+ }
149
+
150
+ button.secondary {
151
+ background: linear-gradient(135deg, #68c2ff, #9dd9ff);
152
+ }
153
+
154
+ button:disabled {
155
+ opacity: 0.58;
156
+ cursor: not-allowed;
157
+ transform: none;
158
+ }
159
+
160
+ .layout {
161
+ margin-top: 18px;
162
+ display: grid;
163
+ gap: 16px;
164
+ grid-template-columns: 1.15fr 0.85fr;
165
+ align-items: start;
166
+ }
167
+
168
+ .panel {
169
+ border-radius: 18px;
170
+ padding: 18px;
171
+ }
172
+
173
+ .panel-wide {
174
+ grid-column: 1 / -1;
175
+ }
176
+
177
+ .panel h2 {
178
+ margin: 0 0 12px;
179
+ font-size: 1rem;
180
+ letter-spacing: 0.01em;
181
+ }
182
+
183
+ .kpi-grid {
184
+ display: grid;
185
+ grid-template-columns: repeat(3, minmax(0, 1fr));
186
+ gap: 12px;
187
+ }
188
+
189
+ .kpi-grid div {
190
+ background: rgba(255, 255, 255, 0.9);
191
+ border: 1px solid var(--border);
192
+ border-radius: 14px;
193
+ padding: 12px;
194
+ }
195
+
196
+ .kpi-grid span {
197
+ display: block;
198
+ font-size: 0.74rem;
199
+ color: var(--muted);
200
+ margin-bottom: 4px;
201
+ }
202
+
203
+ .kpi-grid strong {
204
+ font-size: 1.05rem;
205
+ }
206
+
207
+ .action-row,
208
+ .stack {
209
+ display: grid;
210
+ gap: 10px;
211
+ margin-bottom: 12px;
212
+ }
213
+
214
+ .stack-two {
215
+ grid-template-columns: repeat(2, minmax(0, 1fr));
216
+ }
217
+
218
+ .med-grid {
219
+ display: grid;
220
+ grid-template-columns: repeat(3, minmax(0, 1fr));
221
+ gap: 10px;
222
+ max-height: 420px;
223
+ overflow: auto;
224
+ padding-right: 2px;
225
+ }
226
+
227
+ .med-card {
228
+ border: 1px solid var(--border);
229
+ border-radius: 14px;
230
+ padding: 12px;
231
+ background: var(--panel-solid);
232
+ transition: transform 120ms ease, box-shadow 120ms ease;
233
+ }
234
+
235
+ .med-card:hover {
236
+ transform: translateY(-1px);
237
+ box-shadow: 0 10px 25px rgba(44, 105, 165, 0.12);
238
+ }
239
+
240
+ .med-card p {
241
+ margin: 6px 0 4px;
242
+ color: var(--muted);
243
+ text-transform: capitalize;
244
+ }
245
+
246
+ .logs {
247
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
248
+ font-size: 0.85rem;
249
+ max-height: 300px;
250
+ overflow: auto;
251
+ display: grid;
252
+ gap: 6px;
253
+ padding-right: 2px;
254
+ }
255
+
256
+ .logs div {
257
+ background: rgba(255, 255, 255, 0.78);
258
+ border: 1px solid var(--border);
259
+ border-radius: 10px;
260
+ padding: 8px 10px;
261
+ }
262
+
263
+ .muted {
264
+ color: var(--muted);
265
+ margin: 0;
266
+ }
267
+
268
+ .budget-note {
269
+ margin-top: 10px;
270
+ padding: 10px 12px;
271
+ border: 1px solid var(--border);
272
+ border-radius: 12px;
273
+ background: rgba(255, 255, 255, 0.78);
274
+ }
275
+
276
+ @media (max-width: 1120px) {
277
+ .layout {
278
+ grid-template-columns: 1fr;
279
+ }
280
+
281
+ .topbar {
282
+ grid-template-columns: 1fr;
283
+ }
284
+
285
+ .status-chip {
286
+ justify-self: start;
287
+ }
288
+
289
+ .actions {
290
+ justify-content: flex-start;
291
+ }
292
+ }
293
+
294
+ @media (max-width: 760px) {
295
+ .shell {
296
+ padding: 18px 12px;
297
+ }
298
+
299
+ .kpi-grid,
300
+ .med-grid,
301
+ .stack-two {
302
+ grid-template-columns: 1fr;
303
+ }
304
+ }
openenv-polypharmacy/frontend/vite.config.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ host: "0.0.0.0",
9
+ },
10
+ });
openenv-polypharmacy/inference.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Baseline LLM inference script for the PolypharmacyEnv.
3
+
4
+ Uses Groq's OpenAI-compatible Chat Completions API 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
+ GROQ_API_KEY – required
10
+ GROQ_BASE_URL – optional (default: https://api.groq.com/openai/v1)
11
+ GROQ_MODEL_NAME – model to use (default: llama-3.1-8b-instant)
12
+ POLYPHARMACY_ENV_URL – environment HTTP base URL (default: http://localhost:7860)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ import uuid
21
+ from typing import Any, Dict, List
22
+
23
+ import requests
24
+ from openai import OpenAI
25
+
26
+ # ── Configuration ────────────────────────────────────────────────────────────
27
+
28
+ MODEL = os.environ.get("GROQ_MODEL_NAME", "llama-3.1-8b-instant")
29
+ API_KEY = os.environ.get("GROQ_API_KEY", "")
30
+ API_BASE = os.environ.get("GROQ_BASE_URL", "https://api.groq.com/openai/v1")
31
+ ENV_URL = os.environ.get("POLYPHARMACY_ENV_URL", "http://localhost:7860")
32
+
33
+ TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
34
+ EPISODES_PER_TASK = 5
35
+
36
+ client = OpenAI(api_key=API_KEY, base_url=API_BASE)
37
+
38
+ # ── Logging helpers ──────────────────────────────────────────────────────────
39
+
40
+ def _log(tag: str, payload: Dict[str, Any]) -> None:
41
+ print(f"[{tag}] {json.dumps(payload, default=str)}", flush=True)
42
+
43
+
44
+ def _err(msg: str) -> None:
45
+ print(msg, file=sys.stderr, flush=True)
46
+
47
+
48
+ # ── Environment HTTP helpers ─────────────────────────────────────────────────
49
+
50
+ def env_reset(task_id: str) -> Dict[str, Any]:
51
+ resp = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id}, timeout=30)
52
+ resp.raise_for_status()
53
+ return resp.json()
54
+
55
+
56
+ def env_step(action: Dict[str, Any]) -> Dict[str, Any]:
57
+ resp = requests.post(f"{ENV_URL}/step", json={"action": action}, timeout=30)
58
+ resp.raise_for_status()
59
+ return resp.json()
60
+
61
+
62
+ # ── Observation → prompt ─────────────────────────────────────────────────────
63
+
64
+ SYSTEM_PROMPT = """\
65
+ You are a clinical pharmacist AI assistant reviewing an elderly patient's medication regimen.
66
+ You must reduce drug-interaction risk and address Beers-criteria violations while minimising
67
+ unnecessary medication changes.
68
+
69
+ Available actions (respond with STRICT JSON, no extra text):
70
+ 1. Query a drug pair for interactions:
71
+ {"action_type": "query_ddi", "drug_id_1": "...", "drug_id_2": "..."}
72
+
73
+ 2. Propose an intervention:
74
+ {"action_type": "propose_intervention", "target_drug_id": "...",
75
+ "intervention_type": "stop|dose_reduce|substitute|add_monitoring",
76
+ "proposed_new_drug_id": "...(optional)", "rationale": "..."}
77
+
78
+ 3. Finish the review:
79
+ {"action_type": "finish_review"}
80
+
81
+ Respond with EXACTLY ONE JSON object per turn. No markdown, no explanation outside JSON.
82
+ """
83
+
84
+
85
+ def _summarise_obs(obs: Dict[str, Any]) -> str:
86
+ meds = obs.get("current_medications", [])
87
+ med_summary = "; ".join(
88
+ f"{m['drug_id']}({m['generic_name']},{m['dose_mg']}mg)"
89
+ for m in meds
90
+ )
91
+ queries = obs.get("interaction_queries", [])
92
+ q_summary = "; ".join(
93
+ f"{q['drug_id_1']}+{q['drug_id_2']}={q.get('severity','?')}"
94
+ for q in queries
95
+ )
96
+ interventions = obs.get("interventions", [])
97
+ iv_summary = "; ".join(
98
+ f"{iv['action_type']}({iv['target_drug_id']})"
99
+ for iv in interventions
100
+ )
101
+ return (
102
+ f"Patient: age={obs.get('age')}, sex={obs.get('sex')}, "
103
+ f"conditions={obs.get('conditions')}, "
104
+ f"eGFR={obs.get('eGFR_category')}, liver={obs.get('liver_function_category')}\n"
105
+ f"Medications: {med_summary}\n"
106
+ f"Queries so far: {q_summary or 'none'}\n"
107
+ f"Interventions so far: {iv_summary or 'none'}\n"
108
+ f"Remaining query budget: {obs.get('remaining_query_budget')}\n"
109
+ f"Remaining intervention budget: {obs.get('remaining_intervention_budget')}\n"
110
+ f"Step: {obs.get('step_index')}"
111
+ )
112
+
113
+
114
+ # ── LLM call ─────────────────────────────────────────────────────────────────
115
+
116
+ def _ask_llm(obs_summary: str) -> Dict[str, Any]:
117
+ """Call the LLM and parse a PolypharmacyAction JSON."""
118
+ try:
119
+ chat_resp = client.chat.completions.create(
120
+ model=MODEL,
121
+ messages=[
122
+ {"role": "system", "content": SYSTEM_PROMPT},
123
+ {"role": "user", "content": obs_summary},
124
+ ],
125
+ max_tokens=256,
126
+ temperature=0.2,
127
+ )
128
+ text = (chat_resp.choices[0].message.content or "").strip()
129
+ # Strip markdown fences if present
130
+ text = text.strip()
131
+ if text.startswith("```"):
132
+ text = text.split("\n", 1)[-1]
133
+ if text.endswith("```"):
134
+ text = text.rsplit("```", 1)[0]
135
+ text = text.strip()
136
+ return json.loads(text)
137
+ except Exception as e:
138
+ _err(f"LLM parse error: {e}")
139
+ return {"action_type": "finish_review"}
140
+
141
+
142
+ # ── Main loop ────────────────────────────────────────────────────────────────
143
+
144
+ def main() -> None:
145
+ if not API_KEY:
146
+ _err("GROQ_API_KEY is required")
147
+ sys.exit(1)
148
+
149
+ run_id = str(uuid.uuid4())[:8]
150
+
151
+ for task_id in TASKS:
152
+ task_scores: List[float] = []
153
+ task_rewards: List[float] = []
154
+
155
+ _log("START", {
156
+ "run_id": run_id,
157
+ "task_id": task_id,
158
+ "model": MODEL,
159
+ "episodes": EPISODES_PER_TASK,
160
+ })
161
+
162
+ for ep_idx in range(EPISODES_PER_TASK):
163
+ reset_resp = env_reset(task_id)
164
+ obs = reset_resp["observation"]
165
+ done = reset_resp.get("done", False)
166
+ episode_id = obs.get("episode_id", f"ep_{ep_idx}")
167
+ total_reward = 0.0
168
+ step_idx = 0
169
+
170
+ while not done:
171
+ obs_summary = _summarise_obs(obs)
172
+ action_payload = _ask_llm(obs_summary)
173
+
174
+ step_resp = env_step(action_payload)
175
+ obs = step_resp["observation"]
176
+ reward = step_resp.get("reward", 0.0)
177
+ done = step_resp.get("done", False)
178
+ total_reward += reward
179
+
180
+ _log("STEP", {
181
+ "run_id": run_id,
182
+ "task_id": task_id,
183
+ "episode_id": episode_id,
184
+ "step_index": step_idx,
185
+ "observation_summary": obs_summary[:200],
186
+ "action_payload": action_payload,
187
+ "reward": reward,
188
+ "done": done,
189
+ })
190
+
191
+ step_idx += 1
192
+
193
+ grader_score = step_resp.get("info", {}).get("grader_score", 0.0)
194
+ task_scores.append(grader_score)
195
+ task_rewards.append(total_reward)
196
+
197
+ _log("END", {
198
+ "run_id": run_id,
199
+ "task_id": task_id,
200
+ "episodes": EPISODES_PER_TASK,
201
+ "avg_grader_score": sum(task_scores) / max(len(task_scores), 1),
202
+ "avg_total_reward": sum(task_rewards) / max(len(task_rewards), 1),
203
+ "per_episode_scores": task_scores,
204
+ })
205
+
206
+ _err("Inference complete.")
207
+
208
+
209
+ if __name__ == "__main__":
210
+ 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: backend.main: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,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "python-dotenv>=1.0.0",
17
+ "openenv-core>=0.2.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=7.0.0",
23
+ "httpx>=0.25.0",
24
+ "black",
25
+ "isort",
26
+ ]
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["backend/src"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["backend/src/polypharmacy_env/tests"]
33
+ pythonpath = ["backend/src"]
34
+
35
+ [tool.black]
36
+ line-length = 99
37
+ target-version = ["py310"]
38
+
39
+ [tool.isort]
40
+ profile = "black"
41
+ line_length = 99
openenv-polypharmacy/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ -r backend/requirements.txt
openenv-polypharmacy/scripts/dev_backend.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ uvicorn backend.main:app --reload --host 0.0.0.0 --port 7860