adithya9903 commited on
Commit
ad43b05
Β·
1 Parent(s): d110f58

feat: Initial dev

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitignore +4 -1
  2. openenv-polypharmacy/.dockerignore +8 -0
  3. openenv-polypharmacy/.env.example +3 -0
  4. openenv-polypharmacy/Dockerfile +21 -12
  5. openenv-polypharmacy/README.md +174 -113
  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/{src/polypharmacy_env.egg-info/requires.txt β†’ backend/requirements.txt} +3 -5
  10. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/__init__.py +0 -0
  11. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/api/__init__.py +0 -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/{src β†’ backend/src}/polypharmacy_env/baselines/__init__.py +0 -0
  17. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/heuristic_agent.py +0 -0
  18. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/random_agent.py +0 -0
  19. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/client.py +0 -0
  20. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/config.py +1 -1
  21. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/data_loader.py +0 -0
  22. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/ddi_simulator.py +0 -0
  23. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/env_core.py +0 -0
  24. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/graders.py +0 -0
  25. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/models.py +0 -0
  26. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/rewards.py +0 -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/{src β†’ backend/src}/polypharmacy_env/tasks.py +0 -0
  30. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/__init__.py +0 -0
  31. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/test_api.py +0 -0
  32. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/test_env_core.py +0 -0
  33. openenv-polypharmacy/data/processed/patients_polypharmacy.csv +29 -29
  34. openenv-polypharmacy/docker-compose.yml +35 -0
  35. openenv-polypharmacy/frontend/Dockerfile +12 -0
  36. openenv-polypharmacy/frontend/index.html +12 -0
  37. openenv-polypharmacy/frontend/package-lock.json +1677 -0
  38. openenv-polypharmacy/frontend/package.json +19 -0
  39. openenv-polypharmacy/frontend/src/App.jsx +371 -0
  40. openenv-polypharmacy/frontend/src/main.jsx +10 -0
  41. openenv-polypharmacy/frontend/src/styles.css +304 -0
  42. openenv-polypharmacy/frontend/vite.config.js +10 -0
  43. openenv-polypharmacy/inference.py +11 -15
  44. openenv-polypharmacy/openenv.yaml +1 -1
  45. openenv-polypharmacy/pyproject.toml +4 -3
  46. openenv-polypharmacy/requirements.txt +1 -8
  47. openenv-polypharmacy/scripts/dev_backend.sh +4 -0
  48. openenv-polypharmacy/scripts/dev_frontend.sh +5 -0
  49. openenv-polypharmacy/scripts/inference.py +0 -217
  50. openenv-polypharmacy/scripts/run_validation.sh +2 -2
.gitignore CHANGED
@@ -1,3 +1,6 @@
1
  venv/
2
  .DS_Store
3
- __pycache__/
 
 
 
 
1
  venv/
2
  .DS_Store
3
+ __pycache__/
4
+ node_modules/
5
+ dist/
6
+ .env
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/.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ GROQ_API_KEY=your_groq_api_key_here
2
+ GROQ_BASE_URL=https://api.groq.com/openai/v1
3
+ GROQ_MODEL_NAME=llama-3.3-70b-versatile
openenv-polypharmacy/Dockerfile CHANGED
@@ -1,30 +1,39 @@
 
 
 
 
 
 
 
1
  FROM python:3.11-slim
2
 
3
- # System deps
4
  RUN apt-get update && \
5
  apt-get install -y --no-install-recommends build-essential curl && \
6
  rm -rf /var/lib/apt/lists/*
7
 
8
  WORKDIR /app
9
 
10
- # Install Python deps first (layer caching)
11
- COPY requirements.txt .
12
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
13
 
14
- # Copy project
15
- COPY . .
16
 
17
- # Generate data if not present
18
- RUN python3 scripts/preprocess_data.py
19
 
20
- # Environment
21
  ENV PORT=7860
22
- ENV PYTHONPATH="/app/src:${PYTHONPATH}"
23
  ENV PYTHONUNBUFFERED=1
24
 
25
  EXPOSE 7860
26
 
27
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
28
  CMD curl -f http://localhost:7860/health || exit 1
29
 
30
- CMD ["uvicorn", "polypharmacy_env.api.server:app", "--host", "0.0.0.0", "--port", "7860"]
 
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/README.md CHANGED
@@ -1,184 +1,245 @@
1
  # PolypharmacyEnv
2
 
3
- An [OpenEnv](https://github.com/meta-pytorch/OpenEnv)-compliant reinforcement-learning environment that simulates **elderly polypharmacy medication review**. An RL agent acts as a clinical pharmacist assistant, identifying dangerous drug-drug interactions (DDIs), Beers-criteria violations, and proposing safe interventions.
4
 
5
- ---
6
-
7
- ## Motivation
8
-
9
- Polypharmacy (concurrent use of multiple medications) is extremely common in elderly patients (age >= 65) and carries significant risks:
10
-
11
- - **Drug-drug interactions** can cause adverse events, hospitalisation, and death.
12
- - **Beers-criteria violations** flag medications that are inappropriate or require dose adjustments in older adults.
13
- - Stopping critical medications (anticoagulants, insulin) without proper substitution can be equally dangerous.
14
-
15
- This environment lets RL and LLM-based agents learn to **balance risk reduction against regimen stability**.
16
 
17
  ---
18
 
19
- ## Action Space
20
-
21
- Each step, the agent sends a `PolypharmacyAction` with one of three action types:
22
 
23
- | `action_type` | Required fields | Description |
24
- |---|---|---|
25
- | `query_ddi` | `drug_id_1`, `drug_id_2` | Query the DDI database for an interaction between two drugs |
26
- | `propose_intervention` | `target_drug_id`, `intervention_type` | Propose changing a medication (`stop`, `dose_reduce`, `substitute`, `add_monitoring`) |
27
- | `finish_review` | β€” | End the review and trigger final grading |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- Optional fields: `proposed_new_drug_id`, `rationale`.
30
 
31
- ## Observation Space
32
 
33
- `PolypharmacyObservation` includes:
34
 
35
- - **Patient demographics**: `age`, `sex`, `conditions`, `eGFR_category`, `liver_function_category`
36
- - **Medications**: list of `MedicationEntry` (drug_id, name, class, dose, high-risk flags, Beers flags)
37
- - **History**: `interaction_queries` (past DDI query results), `interventions` (past actions)
38
- - **Budgets**: `remaining_query_budget`, `remaining_intervention_budget`
39
- - **Reward signals**: `shaped_reward`, `done`
40
 
41
- ## State
42
 
43
- `PolypharmacyState`: `episode_id`, `task_id`, `step_count`, `max_steps`, `num_query_actions`, `num_interventions`.
 
 
44
 
45
  ---
46
 
47
- ## Tasks
48
 
49
- | Task ID | Difficulty | Drugs | Query Budget | Intervention Budget | Max Steps | Description |
50
- |---|---|---|---|---|---|---|
51
- | `easy_screening` | Easy | 3-5 | 4 | 2 | 10 | One severe DDI, simple resolution |
52
- | `budgeted_screening` | Medium | 6-10 | 8 | 3 | 20 | Multiple DDIs + Beers issues, limited budgets |
53
- | `complex_tradeoff` | Hard | 10-15 | 12 | 5 | 30 | Critical drugs, trade-off between risk and regimen stability |
54
 
55
  ---
56
 
57
- ## Reward Structure
58
 
59
- **Per-step shaped rewards:**
60
 
61
- | Event | Reward |
62
- |---|---|
63
- | DDI query | -0.01 (cost) + 0.03 bonus if severe DDI discovered |
64
- | Successful intervention | +(previous_risk - new_risk) - 0.02 cost |
65
- | Invalid action | -0.10 penalty |
66
- | Timeout (max steps exceeded) | -0.20 penalty |
67
- | `finish_review` | + grader score (0.0 to 1.0) |
68
 
69
- **Terminal grader scoring:**
70
- - **Easy**: 50% risk reduction + 50% targeted intervention flag
71
- - **Medium**: 50% risk reduction + 30% intervention precision + 20% query efficiency
72
- - **Hard**: risk reduction - regimen disruption penalty - critical drug penalty
 
73
 
74
  ---
75
 
76
- ## Setup & Usage
 
 
77
 
78
- ### Install dependencies
79
 
80
  ```bash
81
- pip install -r requirements.txt
82
  ```
83
 
84
- ### Generate synthetic data
85
 
86
  ```bash
87
- python3 scripts/preprocess_data.py
 
 
88
  ```
89
 
90
- ### Run the API server locally
91
 
92
  ```bash
93
- PYTHONPATH=src uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860
94
  ```
95
 
96
- ### Run the heuristic baseline
 
 
97
 
98
  ```bash
99
- PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
100
  ```
101
 
102
- ### Run tests
103
 
104
  ```bash
105
- PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
106
  ```
107
 
108
- ### Run `inference.py` (LLM baseline)
 
 
 
 
 
 
 
 
 
109
 
110
  ```bash
111
- # Start the server first, then in another terminal:
112
- export OPENAI_API_KEY="sk-..."
113
- export MODEL_NAME="gpt-4.1"
114
- export POLYPHARMACY_ENV_URL="http://localhost:7860"
115
- python3 inference.py
116
  ```
117
 
118
- ### Docker
119
 
120
  ```bash
121
- docker build -t polypharmacy-env .
122
- docker run -p 7860:7860 polypharmacy-env
123
  ```
124
 
 
 
 
 
 
125
  ---
126
 
127
- ## Hugging Face Space
 
 
 
 
128
 
129
- This repo is ready for deployment as a HF Space:
 
 
130
 
131
- - **Space type**: `docker`
132
- - **Tag**: `openenv`
133
- - The container listens on port 7860 and exposes `/reset`, `/step`, `/state`, `/health`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  ---
136
 
137
- ## Baseline Scores
 
 
138
 
139
- ### Heuristic Agent (deterministic, rule-based)
 
 
 
 
 
140
 
141
- | Task | Avg Score | Avg Reward |
142
- |---|---|---|
143
- | `easy_screening` | ~0.96 | ~1.30 |
144
- | `budgeted_screening` | ~0.48 | ~0.45 |
145
- | `complex_tradeoff` | ~0.24 | ~0.11 |
146
 
147
- *(Scores vary by seed; run `scripts/run_validation.sh` for exact numbers.)*
148
 
149
  ---
150
 
151
- ## Project Structure
 
 
152
 
 
 
153
  ```
154
- openenv-polypharmacy/
155
- openenv.yaml # OpenEnv manifest
156
- Dockerfile # Container image
157
- inference.py # LLM baseline script
158
- requirements.txt
159
- pyproject.toml
160
- src/polypharmacy_env/
161
- config.py # Constants, task configs
162
- models.py # Pydantic action/observation/state models
163
- env_core.py # PolypharmacyEnv implementation
164
- tasks.py # Task selection utilities
165
- graders.py # Deterministic graders (3 difficulty levels)
166
- rewards.py # Reward shaping logic
167
- data_loader.py # CSV data loading
168
- ddi_simulator.py # Drug interaction lookup engine
169
- api/
170
- server.py # FastAPI HTTP server
171
- schemas.py # Request/response schemas
172
- baselines/
173
- heuristic_agent.py # Rule-based baseline
174
- random_agent.py # Random baseline
175
- tests/
176
- test_env_core.py
177
- test_api.py
178
- data/
179
- lookups/ # Drug metadata, DDI rules, Beers criteria CSVs
180
- processed/ # Synthetic patient episodes
181
- scripts/
182
- preprocess_data.py # Synthetic data generator
183
- run_validation.sh # Run tests + baseline
184
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/{src/polypharmacy_env.egg-info/requires.txt β†’ backend/requirements.txt} RENAMED
@@ -2,10 +2,8 @@ fastapi>=0.104.0
2
  uvicorn>=0.24.0
3
  pydantic>=2.0.0
4
  requests>=2.31.0
 
 
5
  openai>=1.0.0
6
-
7
- [dev]
8
  pytest>=7.0.0
9
- httpx>=0.25.0
10
- black
11
- isort
 
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/{src β†’ backend/src}/polypharmacy_env/__init__.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/api/__init__.py RENAMED
File without changes
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/{src β†’ backend/src}/polypharmacy_env/baselines/__init__.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/heuristic_agent.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/random_agent.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/client.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/config.py RENAMED
@@ -7,7 +7,7 @@ from pathlib import Path
7
  from typing import Dict
8
 
9
  # ── Paths ────────────────────────────────────────────────────────────────────
10
- PROJECT_ROOT = Path(__file__).resolve().parents[2] # openenv-polypharmacy/
11
  DATA_DIR = PROJECT_ROOT / "data"
12
  LOOKUPS_DIR = DATA_DIR / "lookups"
13
  PROCESSED_DIR = DATA_DIR / "processed"
 
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"
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/data_loader.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/ddi_simulator.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/env_core.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/graders.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/models.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/rewards.py RENAMED
File without changes
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/{src β†’ backend/src}/polypharmacy_env/tasks.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/__init__.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/test_api.py RENAMED
File without changes
openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/test_env_core.py RENAMED
File without changes
openenv-polypharmacy/data/processed/patients_polypharmacy.csv CHANGED
@@ -1,44 +1,44 @@
1
  episode_id,age,sex,conditions,eGFR_category,liver_function_category,medication_ids,baseline_risk_score,difficulty
2
- EP_0001,72,F,HTN,moderate,normal,DRUG_WARFARIN;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.264,easy
3
  EP_0002,67,M,OA;COPD;neuropathy,normal,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE,0.2833,easy
4
- EP_0003,73,F,HTN;HF,normal,normal,DRUG_IBUPROFEN;DRUG_WARFARIN;DRUG_FUROSEMIDE,0.2933,easy
5
- EP_0004,74,M,CKD,mild,impaired,DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_DIAZEPAM,0.3067,easy
6
  EP_0005,76,F,OA;neuropathy;CKD,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE,0.17,easy
7
- EP_0006,74,M,HTN;OA,normal,impaired,DRUG_IBUPROFEN;DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.44,easy
8
- EP_0007,90,M,BPH;OA,moderate,normal,DRUG_DIGOXIN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_AMIODARONE,0.16,easy
9
  EP_0008,77,F,CKD;OA;depression,mild,normal,DRUG_AMITRIPTYLINE;DRUG_IBUPROFEN;DRUG_SERTRALINE;DRUG_TRAMADOL;DRUG_FUROSEMIDE,0.17,easy
10
- EP_0009,67,M,COPD;GERD;BPH,mild,normal,DRUG_TRAMADOL;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN,0.205,easy
11
- EP_0010,75,M,dementia;HTN;depression,normal,impaired,DRUG_TRAMADOL;DRUG_DIAZEPAM;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE,0.4425,easy
12
- EP_0011,83,F,AF,moderate,normal,DRUG_TRAMADOL;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_ALPRAZOLAM,0.2275,easy
13
- EP_0012,71,F,HTN;GERD;depression,normal,normal,DRUG_LISINOPRIL;DRUG_FLUOXETINE;DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.254,easy
14
- EP_0013,70,F,HF;HTN;AF,mild,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_ALPRAZOLAM,0.3033,easy
15
  EP_0014,82,F,dementia,normal,normal,DRUG_DONEPEZIL;DRUG_NAPROXEN;DRUG_APIXABAN;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE,0.17,easy
16
  EP_0015,84,F,dementia;neuropathy,normal,normal,DRUG_DONEPEZIL;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_CELECOXIB;DRUG_TRAMADOL,0.17,easy
17
- EP_0016,83,M,HTN,normal,normal,DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_ALPRAZOLAM,0.3033,easy
18
- EP_0017,83,F,CKD,severe,normal,DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.2833,easy
19
- EP_0018,70,F,CKD;HF;HTN,mild,normal,DRUG_SPIRONOLACTONE;DRUG_ALPRAZOLAM;DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_METOPROLOL,0.182,easy
20
- EP_0019,84,M,DM;depression,normal,normal,DRUG_GLIPIZIDE;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_INSULIN_GLARGINE;DRUG_DIAZEPAM,0.448,easy
21
- EP_0020,90,F,neuropathy;BPH;AF,normal,normal,DRUG_WARFARIN;DRUG_NAPROXEN;DRUG_TAMSULOSIN,0.3,easy
22
- EP_0021,87,M,HTN;BPH;HF,normal,normal,DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE,0.2125,easy
23
- EP_0022,90,M,AF;GERD;DM,normal,impaired,DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_OMEPRAZOLE,0.2125,easy
24
- EP_0023,90,F,HF,normal,normal,DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_METOPROLOL,0.2833,easy
25
  EP_0024,71,F,OA,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_APIXABAN,0.17,easy
26
- EP_0025,71,M,COPD;AF;neuropathy,mild,normal,DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_NAPROXEN,0.3,easy
27
- EP_0026,88,M,GERD;dementia,severe,normal,DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE,0.2125,easy
28
  EP_0027,76,M,AF,normal,normal,DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_NAPROXEN,0.43,easy
29
  EP_0028,73,F,CKD,moderate,normal,DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL,0.17,easy
30
- EP_0029,70,F,CKD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_GABAPENTIN;DRUG_AMLODIPINE;DRUG_DIAZEPAM,0.184,easy
31
- EP_0030,87,F,dementia;HF;depression,normal,normal,DRUG_WARFARIN;DRUG_DONEPEZIL;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.27,easy
32
- EP_0031,69,M,HF,severe,normal,DRUG_WARFARIN;DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.36,easy
33
  EP_0032,89,F,neuropathy,mild,normal,DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_PREDNISONE;DRUG_TRAMADOL,0.2125,easy
34
  EP_0033,68,F,dementia,mild,impaired,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_SPIRONOLACTONE;DRUG_TRAMADOL;DRUG_ALPRAZOLAM,0.182,easy
35
- EP_0034,84,F,CKD;HF;HTN,moderate,normal,DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN;DRUG_AMIODARONE,0.2667,easy
36
- EP_0035,74,M,HTN;DM,normal,impaired,DRUG_IBUPROFEN;DRUG_GLIPIZIDE;DRUG_WARFARIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL,0.176,easy
37
- EP_0036,80,F,DM;neuropathy;HTN,severe,normal,DRUG_WARFARIN;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN,0.225,easy
38
- EP_0037,78,M,HF,normal,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_DIAZEPAM;DRUG_LISINOPRIL,0.23,easy
39
- EP_0038,89,F,HTN;AF,moderate,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_DIAZEPAM,0.3067,easy
40
  EP_0039,78,F,OA;depression,moderate,normal,DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_SERTRALINE,0.205,easy
41
- EP_0040,72,F,neuropathy;COPD;BPH,normal,normal,DRUG_TRAMADOL;DRUG_ALPRAZOLAM;DRUG_AMITRIPTYLINE;DRUG_TAMSULOSIN,0.44,easy
42
  EP_0041,89,F,AF;BPH;DM;HF;HTN,mild,normal,DRUG_GLIPIZIDE;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_INSULIN_GLARGINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN,0.1,medium
43
  EP_0042,66,F,HTN;AF;CKD,moderate,normal,DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_SERTRALINE,0.173,medium
44
  EP_0043,70,F,OA;HTN;dementia,moderate,normal,DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_LISINOPRIL;DRUG_METOPROLOL,0.0467,medium
 
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
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 CHANGED
@@ -1,15 +1,14 @@
1
  #!/usr/bin/env python3
2
  """Baseline LLM inference script for the PolypharmacyEnv.
3
 
4
- Uses the OpenAI Python client to drive an LLM agent through the
5
  PolypharmacyEnv HTTP API. Emits structured stdout logs in the
6
  [START], [STEP], [END] format required by the OpenEnv evaluation spec.
7
 
8
  Environment variables:
9
- OPENAI_API_KEY – required
10
- API_BASE_URL – LLM endpoint (default: https://api.openai.com/v1)
11
- MODEL_NAME – model to use (default: gpt-4.1)
12
- HF_TOKEN – HuggingFace token (optional)
13
  POLYPHARMACY_ENV_URL – environment HTTP base URL (default: http://localhost:7860)
14
  """
15
 
@@ -18,7 +17,6 @@ from __future__ import annotations
18
  import json
19
  import os
20
  import sys
21
- import time
22
  import uuid
23
  from typing import Any, Dict, List
24
 
@@ -27,10 +25,9 @@ from openai import OpenAI
27
 
28
  # ── Configuration ────────────────────────────────────────────────────────────
29
 
30
- API_KEY = os.environ.get("OPENAI_API_KEY", "")
31
- API_BASE = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
32
- MODEL = os.environ.get("MODEL_NAME", "gpt-4.1")
33
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
34
  ENV_URL = os.environ.get("POLYPHARMACY_ENV_URL", "http://localhost:7860")
35
 
36
  TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
@@ -119,16 +116,16 @@ def _summarise_obs(obs: Dict[str, Any]) -> str:
119
  def _ask_llm(obs_summary: str) -> Dict[str, Any]:
120
  """Call the LLM and parse a PolypharmacyAction JSON."""
121
  try:
122
- resp = client.chat.completions.create(
123
  model=MODEL,
124
  messages=[
125
  {"role": "system", "content": SYSTEM_PROMPT},
126
  {"role": "user", "content": obs_summary},
127
  ],
128
- temperature=0.2,
129
  max_tokens=256,
 
130
  )
131
- text = resp.choices[0].message.content or ""
132
  # Strip markdown fences if present
133
  text = text.strip()
134
  if text.startswith("```"):
@@ -146,7 +143,7 @@ def _ask_llm(obs_summary: str) -> Dict[str, Any]:
146
 
147
  def main() -> None:
148
  if not API_KEY:
149
- _err("OPENAI_API_KEY is required")
150
  sys.exit(1)
151
 
152
  run_id = str(uuid.uuid4())[:8]
@@ -159,7 +156,6 @@ def main() -> None:
159
  "run_id": run_id,
160
  "task_id": task_id,
161
  "model": MODEL,
162
- "api_base": API_BASE,
163
  "episodes": EPISODES_PER_TASK,
164
  })
165
 
 
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
 
 
17
  import json
18
  import os
19
  import sys
 
20
  import uuid
21
  from typing import Any, Dict, List
22
 
 
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"]
 
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("```"):
 
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]
 
156
  "run_id": run_id,
157
  "task_id": task_id,
158
  "model": MODEL,
 
159
  "episodes": EPISODES_PER_TASK,
160
  })
161
 
openenv-polypharmacy/openenv.yaml CHANGED
@@ -13,7 +13,7 @@ tags:
13
  - openenv
14
  type: space
15
  runtime: fastapi
16
- app: polypharmacy_env.api.server:app
17
  port: 7860
18
 
19
  tasks:
 
13
  - openenv
14
  type: space
15
  runtime: fastapi
16
+ app: backend.main:app
17
  port: 7860
18
 
19
  tasks:
openenv-polypharmacy/pyproject.toml CHANGED
@@ -13,6 +13,7 @@ dependencies = [
13
  "pydantic>=2.0.0",
14
  "requests>=2.31.0",
15
  "openai>=1.0.0",
 
16
  "openenv-core>=0.2.0",
17
  ]
18
 
@@ -25,11 +26,11 @@ dev = [
25
  ]
26
 
27
  [tool.setuptools.packages.find]
28
- where = ["src"]
29
 
30
  [tool.pytest.ini_options]
31
- testpaths = ["src/polypharmacy_env/tests"]
32
- pythonpath = ["src"]
33
 
34
  [tool.black]
35
  line-length = 99
 
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
 
 
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
openenv-polypharmacy/requirements.txt CHANGED
@@ -1,8 +1 @@
1
- fastapi>=0.104.0
2
- uvicorn>=0.24.0
3
- pydantic>=2.0.0
4
- requests>=2.31.0
5
- openai>=1.0.0
6
- httpx>=0.25.0
7
- openenv-core>=0.2.0
8
- pytest>=7.0.0
 
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
openenv-polypharmacy/scripts/dev_frontend.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd frontend
5
+ npm run dev
openenv-polypharmacy/scripts/inference.py DELETED
@@ -1,217 +0,0 @@
1
- #!/usr/bin/env python3
2
- """LLM-based inference agent for PolypharmacyEnv.
3
-
4
- Connects to a running OpenEnv server via WebSocket (using PolypharmacyClient)
5
- and runs an LLM agent that reviews a patient's medication regimen.
6
-
7
- Usage:
8
- # Start server first:
9
- # uvicorn polypharmacy_env.api.server:app --port 7860
10
-
11
- # Then run inference:
12
- python scripts/inference.py --task easy_screening --seed 42
13
- python scripts/inference.py --task budgeted_screening --model gpt-4o
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import argparse
19
- import asyncio
20
- import json
21
- import os
22
- import sys
23
- from typing import Any, Dict, List
24
-
25
- # Add src to path
26
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
27
-
28
- from polypharmacy_env.client import PolypharmacyClient
29
- from polypharmacy_env.models import PolypharmacyAction, PolypharmacyObservation
30
-
31
- try:
32
- from openai import OpenAI
33
- except ImportError:
34
- OpenAI = None # type: ignore[assignment, misc]
35
-
36
-
37
- def format_observation_for_llm(obs: PolypharmacyObservation) -> str:
38
- """Convert an observation to a human-readable prompt for the LLM."""
39
- lines = [
40
- f"Patient: {obs.age}yo {obs.sex}",
41
- f"Conditions: {', '.join(obs.conditions)}",
42
- f"eGFR: {obs.eGFR_category}, Liver: {obs.liver_function_category}",
43
- f"Step: {obs.step_index}",
44
- f"Query budget remaining: {obs.remaining_query_budget}",
45
- f"Intervention budget remaining: {obs.remaining_intervention_budget}",
46
- "",
47
- "Current Medications:",
48
- ]
49
- for med in obs.current_medications:
50
- flags = f" [BEERS: {', '.join(med.beers_flags)}]" if med.beers_flags else ""
51
- high_risk = " [HIGH RISK ELDERLY]" if med.is_high_risk_elderly else ""
52
- lines.append(
53
- f" - {med.drug_id} ({med.generic_name}) {med.atc_class} "
54
- f"{med.dose_mg}mg{high_risk}{flags}"
55
- )
56
-
57
- if obs.interaction_queries:
58
- lines.append("")
59
- lines.append("DDI Queries So Far:")
60
- for q in obs.interaction_queries:
61
- lines.append(
62
- f" - {q.drug_id_1} + {q.drug_id_2}: "
63
- f"severity={q.severity}, rec={q.recommendation}"
64
- )
65
-
66
- if obs.interventions:
67
- lines.append("")
68
- lines.append("Interventions So Far:")
69
- for iv in obs.interventions:
70
- lines.append(f" - {iv.action_type} {iv.target_drug_id}: {iv.rationale}")
71
-
72
- return "\n".join(lines)
73
-
74
-
75
- SYSTEM_PROMPT = """\
76
- You are a clinical pharmacist assistant reviewing an elderly patient's medication regimen.
77
-
78
- Your goal: identify dangerous drug-drug interactions and Beers Criteria violations,
79
- then propose safe interventions (stop, dose_reduce, substitute, add_monitoring) to
80
- reduce risk while preserving therapeutic coverage.
81
-
82
- Available actions (respond with JSON):
83
- 1. {"action_type": "query_ddi", "drug_id_1": "...", "drug_id_2": "..."}
84
- - Check for a drug-drug interaction between two medications.
85
- 2. {"action_type": "propose_intervention", "target_drug_id": "...", \
86
- "intervention_type": "stop|dose_reduce|substitute|add_monitoring", "rationale": "..."}
87
- - Propose a change to the regimen.
88
- 3. {"action_type": "finish_review"}
89
- - End the review and submit your final regimen.
90
-
91
- Strategy tips:
92
- - Query high-risk drug pairs first (especially those flagged as high-risk elderly or Beers).
93
- - Prioritise resolving severe DDIs over moderate ones.
94
- - Prefer substitution over stopping when possible.
95
- - Always provide a clinical rationale for interventions.
96
- - Finish the review when you've addressed all major issues or exhausted your budget.
97
-
98
- Respond with ONLY a valid JSON action object, no explanation outside the JSON.\
99
- """
100
-
101
-
102
- def parse_llm_action(text: str) -> PolypharmacyAction:
103
- """Parse an LLM response into a PolypharmacyAction."""
104
- text = text.strip()
105
- # Extract JSON from markdown code blocks if present
106
- if "```" in text:
107
- parts = text.split("```")
108
- for part in parts:
109
- part = part.strip()
110
- if part.startswith("json"):
111
- part = part[4:].strip()
112
- if part.startswith("{"):
113
- text = part
114
- break
115
-
116
- data = json.loads(text)
117
- return PolypharmacyAction(**data)
118
-
119
-
120
- async def run_llm_episode(
121
- base_url: str,
122
- task_id: str,
123
- seed: int,
124
- model: str,
125
- max_retries: int = 3,
126
- ) -> Dict[str, Any]:
127
- """Run a single episode with LLM agent via WebSocket."""
128
- if OpenAI is None:
129
- raise ImportError("openai package is required. Install with: pip install openai")
130
-
131
- llm = OpenAI()
132
- total_reward = 0.0
133
- steps = 0
134
- messages: List[Dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}]
135
-
136
- async with PolypharmacyClient(base_url=base_url) as client:
137
- result = await client.reset(task_id=task_id, seed=seed)
138
- obs = result.observation
139
-
140
- while not result.done:
141
- obs_text = format_observation_for_llm(obs)
142
- messages.append({"role": "user", "content": obs_text})
143
-
144
- # Call LLM
145
- action = None
146
- for attempt in range(max_retries):
147
- try:
148
- response = llm.chat.completions.create(
149
- model=model,
150
- messages=messages,
151
- temperature=0.0,
152
- max_tokens=256,
153
- )
154
- llm_text = response.choices[0].message.content or ""
155
- messages.append({"role": "assistant", "content": llm_text})
156
- action = parse_llm_action(llm_text)
157
- break
158
- except (json.JSONDecodeError, Exception) as e:
159
- if attempt == max_retries - 1:
160
- print(f" LLM parse failed after {max_retries} attempts: {e}")
161
- action = PolypharmacyAction(action_type="finish_review")
162
- else:
163
- messages.append({
164
- "role": "user",
165
- "content": f"Invalid JSON. Please respond with only a valid JSON action. Error: {e}",
166
- })
167
-
168
- assert action is not None
169
- result = await client.step(action)
170
- obs = result.observation
171
- total_reward += result.reward or 0.0
172
- steps += 1
173
-
174
- print(
175
- f" step={steps} action={action.action_type} "
176
- f"reward={result.reward:.4f} done={result.done}"
177
- )
178
-
179
- return {
180
- "task_id": task_id,
181
- "seed": seed,
182
- "total_reward": total_reward,
183
- "steps": steps,
184
- }
185
-
186
-
187
- async def amain(args: argparse.Namespace) -> None:
188
- results = []
189
- for seed in range(args.seed, args.seed + args.episodes):
190
- print(f"\n=== Episode: task={args.task} seed={seed} ===")
191
- result = await run_llm_episode(
192
- base_url=args.url,
193
- task_id=args.task,
194
- seed=seed,
195
- model=args.model,
196
- )
197
- results.append(result)
198
- print(f" => reward={result['total_reward']:.4f} steps={result['steps']}")
199
-
200
- if results:
201
- avg_reward = sum(r["total_reward"] for r in results) / len(results)
202
- print(f"\nAverage reward over {len(results)} episodes: {avg_reward:.4f}")
203
-
204
-
205
- def main() -> None:
206
- parser = argparse.ArgumentParser(description="Run LLM agent on PolypharmacyEnv")
207
- parser.add_argument("--url", default="ws://localhost:7860", help="Server URL")
208
- parser.add_argument("--task", default="budgeted_screening", help="Task ID")
209
- parser.add_argument("--seed", type=int, default=0, help="Starting seed")
210
- parser.add_argument("--episodes", type=int, default=1, help="Number of episodes")
211
- parser.add_argument("--model", default="gpt-4o", help="LLM model name")
212
- args = parser.parse_args()
213
- asyncio.run(amain(args))
214
-
215
-
216
- if __name__ == "__main__":
217
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv-polypharmacy/scripts/run_validation.sh CHANGED
@@ -5,11 +5,11 @@ set -euo pipefail
5
  cd "$(dirname "$0")/.."
6
 
7
  echo "=== Running unit tests ==="
8
- PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
9
 
10
  echo ""
11
  echo "=== Running heuristic baseline ==="
12
- PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
13
 
14
  echo ""
15
  echo "=== Validation complete ==="
 
5
  cd "$(dirname "$0")/.."
6
 
7
  echo "=== Running unit tests ==="
8
+ PYTHONPATH=backend/src python3 -m pytest backend/src/polypharmacy_env/tests/ -v
9
 
10
  echo ""
11
  echo "=== Running heuristic baseline ==="
12
+ PYTHONPATH=backend/src python3 -m polypharmacy_env.baselines.heuristic_agent
13
 
14
  echo ""
15
  echo "=== Validation complete ==="