Adithya Vedhamani commited on
Commit
f629743
Β·
unverified Β·
2 Parent(s): d110f58b451b97

Merge pull request #1 from Vishwa-docs/adi

Browse files

Feat: React UI Implementation and Backend FastAPI Refactoring

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .dockerignore +12 -0
  2. .gitattributes +35 -0
  3. .gitignore +33 -1
  4. .gitignore copy +35 -0
  5. Dockerfile +39 -0
  6. README.MD +245 -0
  7. openenv-polypharmacy/.dockerignore +8 -0
  8. openenv-polypharmacy/.env.example +3 -0
  9. openenv-polypharmacy/Dockerfile +21 -12
  10. openenv-polypharmacy/PROMPT.md +0 -571
  11. openenv-polypharmacy/README.md +0 -184
  12. openenv-polypharmacy/backend/Dockerfile +28 -0
  13. openenv-polypharmacy/backend/__init__.py +1 -0
  14. openenv-polypharmacy/backend/main.py +15 -0
  15. openenv-polypharmacy/{src/polypharmacy_env.egg-info/requires.txt β†’ backend/requirements.txt} +3 -5
  16. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/__init__.py +0 -0
  17. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/api/__init__.py +0 -0
  18. openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py +63 -0
  19. openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py +1 -0
  20. openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py +35 -0
  21. openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py +6 -0
  22. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/__init__.py +0 -0
  23. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/heuristic_agent.py +0 -0
  24. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/baselines/random_agent.py +0 -0
  25. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/client.py +0 -0
  26. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/config.py +1 -1
  27. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/data_loader.py +0 -0
  28. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/ddi_simulator.py +0 -0
  29. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/env_core.py +0 -0
  30. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/graders.py +0 -0
  31. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/models.py +0 -0
  32. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/rewards.py +0 -0
  33. openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py +1 -0
  34. openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py +246 -0
  35. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tasks.py +0 -0
  36. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/__init__.py +0 -0
  37. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/test_api.py +0 -0
  38. openenv-polypharmacy/{src β†’ backend/src}/polypharmacy_env/tests/test_env_core.py +0 -0
  39. openenv-polypharmacy/data/processed/patients_polypharmacy.csv +29 -29
  40. openenv-polypharmacy/docker-compose.yml +35 -0
  41. openenv-polypharmacy/frontend/Dockerfile +12 -0
  42. openenv-polypharmacy/frontend/index.html +12 -0
  43. openenv-polypharmacy/frontend/package-lock.json +1677 -0
  44. openenv-polypharmacy/frontend/package.json +19 -0
  45. openenv-polypharmacy/frontend/src/App.jsx +387 -0
  46. openenv-polypharmacy/frontend/src/main.jsx +10 -0
  47. openenv-polypharmacy/frontend/src/styles.css +383 -0
  48. openenv-polypharmacy/frontend/vite.config.js +10 -0
  49. openenv-polypharmacy/inference.py +11 -15
  50. openenv-polypharmacy/openenv.yaml +1 -1
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ **/__pycache__
4
+ **/*.pyc
5
+ **/.pytest_cache
6
+ **/.mypy_cache
7
+ **/.ruff_cache
8
+ **/node_modules
9
+ **/dist
10
+ **/.env
11
+ **/.env.*
12
+ !openenv-polypharmacy/.env.example
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,3 +1,35 @@
 
1
  venv/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  .DS_Store
3
- __pycache__/
 
 
 
 
 
1
+ # --- Python ---
2
  venv/
3
+ .venv/
4
+ env/
5
+ .env
6
+ .env.*
7
+ !openenv-polypharmacy/.env.example
8
+ *.py[cod]
9
+ __pycache__/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ coverage.xml
15
+
16
+ # --- Node / frontend ---
17
+ node_modules/
18
+ **/node_modules/
19
+ frontend/dist/
20
+ **/dist/
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ pnpm-debug.log*
25
+
26
+ # --- Build / temp ---
27
+ *.log
28
+ *.tmp
29
+ *.swp
30
  .DS_Store
31
+
32
+ # --- Project-specific nested paths ---
33
+ openenv-polypharmacy/frontend/node_modules/
34
+ openenv-polypharmacy/frontend/dist/
35
+ openenv-polypharmacy/.pytest_cache/
.gitignore copy ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Python ---
2
+ venv/
3
+ .venv/
4
+ env/
5
+ .env
6
+ .env.*
7
+ !openenv-polypharmacy/.env.example
8
+ *.py[cod]
9
+ __pycache__/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ coverage.xml
15
+
16
+ # --- Node / frontend ---
17
+ node_modules/
18
+ **/node_modules/
19
+ frontend/dist/
20
+ **/dist/
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ pnpm-debug.log*
25
+
26
+ # --- Build / temp ---
27
+ *.log
28
+ *.tmp
29
+ *.swp
30
+ .DS_Store
31
+
32
+ # --- Project-specific nested paths ---
33
+ openenv-polypharmacy/frontend/node_modules/
34
+ openenv-polypharmacy/frontend/dist/
35
+ openenv-polypharmacy/.pytest_cache/
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS frontend-builder
2
+ WORKDIR /app/frontend
3
+ COPY openenv-polypharmacy/frontend/package*.json ./
4
+ RUN npm ci
5
+ COPY openenv-polypharmacy/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 openenv-polypharmacy/backend/requirements.txt /app/backend/requirements.txt
17
+ RUN pip install --no-cache-dir -r /app/backend/requirements.txt
18
+
19
+ COPY openenv-polypharmacy/backend /app/backend
20
+ COPY openenv-polypharmacy/data /app/data
21
+ COPY openenv-polypharmacy/scripts /app/scripts
22
+ COPY openenv-polypharmacy/openenv.yaml /app/openenv.yaml
23
+ COPY openenv-polypharmacy/.env.example /app/.env.example
24
+ COPY openenv-polypharmacy/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}"]
README.MD ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PolypharmacyEnv
2
+
3
+ Monorepo for an OpenEnv-compatible medication safety environment with:
4
+
5
+ - a FastAPI backend (`backend/`)
6
+ - a React frontend (`frontend/`)
7
+ - data assets (`data/`)
8
+ - utility scripts (`scripts/`)
9
+
10
+ ---
11
+
12
+ ## Repository Structure
13
+
14
+ ```text
15
+ openenv-polypharmacy/
16
+ backend/
17
+ main.py # ASGI entrypoint (uvicorn target)
18
+ requirements.txt # Backend dependencies
19
+ Dockerfile # Backend container
20
+ src/polypharmacy_env/ # Python package source
21
+ api/
22
+ app.py # FastAPI/OpenEnv app assembly
23
+ server.py # Compatibility import wrapper
24
+ routes/agent.py # /agent/suggest route
25
+ services/
26
+ groq_agent.py # Groq-based action suggestion logic
27
+ env_core.py # OpenEnv environment core
28
+ models.py # Action/observation/state models
29
+ data_loader.py # CSV loading
30
+ ddi_simulator.py # DDI and Beers lookups
31
+ rewards.py # Reward shaping
32
+ graders.py # Task graders
33
+ tasks.py # Task/episode selection
34
+ tests/ # Backend tests
35
+ frontend/
36
+ src/ # React UI code
37
+ package.json
38
+ Dockerfile # Frontend container
39
+ data/
40
+ lookups/ # drug_metadata.csv, ddi_rules.csv, beers_criteria.csv
41
+ processed/ # patients_polypharmacy.csv
42
+ scripts/
43
+ preprocess_data.py # Synthetic data generation
44
+ dev_backend.sh # Local backend run helper
45
+ dev_frontend.sh # Local frontend run helper
46
+ run_validation.sh # Tests + baseline validation
47
+ docker-compose.yml # Full stack orchestration
48
+ openenv.yaml # OpenEnv manifest
49
+ inference.py # Optional CLI inference baseline
50
+ .env.example # Environment template
51
+ ```
52
+
53
+ ---
54
+
55
+ ## What It Does
56
+
57
+ The environment simulates elderly polypharmacy review. Agent actions:
58
+
59
+ - `query_ddi`
60
+ - `propose_intervention`
61
+ - `finish_review`
62
+
63
+ Supported tasks:
64
+
65
+ - `easy_screening`
66
+ - `budgeted_screening`
67
+ - `complex_tradeoff`
68
+
69
+ ---
70
+
71
+ ## Prerequisites
72
+
73
+ - Python 3.10+
74
+ - Node.js 18+ (or 20+ recommended)
75
+ - npm
76
+ - Docker + Docker Compose (optional, for containerized run)
77
+
78
+ ---
79
+
80
+ ## Environment Setup
81
+
82
+ Create `.env`:
83
+
84
+ ```bash
85
+ cp .env.example .env
86
+ ```
87
+
88
+ Set values:
89
+
90
+ - `GROQ_API_KEY=...` (required)
91
+ - `GROQ_BASE_URL=https://api.groq.com/openai/v1` (recommended)
92
+ - `GROQ_MODEL_NAME=llama-3.3-70b-versatile` (recommended)
93
+
94
+ ---
95
+
96
+ ## Local Run (Recommended During Development)
97
+
98
+ ### 1) Install dependencies
99
+
100
+ Backend:
101
+
102
+ ```bash
103
+ pip install -r backend/requirements.txt
104
+ ```
105
+
106
+ Frontend:
107
+
108
+ ```bash
109
+ cd frontend
110
+ npm install
111
+ cd ..
112
+ ```
113
+
114
+ ### 2) Generate/update synthetic data (if needed)
115
+
116
+ ```bash
117
+ python scripts/preprocess_data.py
118
+ ```
119
+
120
+ ### 3) Start services in two terminals
121
+
122
+ Terminal A:
123
+
124
+ ```bash
125
+ ./scripts/dev_backend.sh
126
+ ```
127
+
128
+ Terminal B:
129
+
130
+ ```bash
131
+ ./scripts/dev_frontend.sh
132
+ ```
133
+
134
+ ### 4) Open app
135
+
136
+ - Frontend: [http://localhost:5173](http://localhost:5173)
137
+ - Backend health: [http://localhost:7860/health](http://localhost:7860/health)
138
+
139
+ ---
140
+
141
+ ## Docker Run
142
+
143
+ Run both services:
144
+
145
+ ```bash
146
+ docker compose up --build
147
+ ```
148
+
149
+ Stop:
150
+
151
+ ```bash
152
+ docker compose down
153
+ ```
154
+
155
+ Ports:
156
+
157
+ - backend: `7860`
158
+ - frontend: `5173`
159
+
160
+ ---
161
+
162
+ ## Hugging Face Spaces Deployment (Docker)
163
+
164
+ This repo now includes a **root `Dockerfile`** that builds frontend + backend into one container, so Spaces can host both API and UI together.
165
+
166
+ ### 1) Create a new Space
167
+
168
+ - Go to [Hugging Face Spaces](https://huggingface.co/new-space)
169
+ - Choose **Docker** SDK
170
+ - Create the Space
171
+
172
+ ### 2) Add Space secrets/variables
173
+
174
+ In Space Settings -> Variables and Secrets:
175
+
176
+ - Secret: `GROQ_API_KEY`
177
+ - Variable: `GROQ_BASE_URL=https://api.groq.com/openai/v1`
178
+ - Variable: `GROQ_MODEL_NAME=llama-3.3-70b-versatile`
179
+
180
+ ### 3) Push this repository to the Space
181
+
182
+ Commit and push all files, including root `Dockerfile`.
183
+
184
+ ### 4) Verify after build
185
+
186
+ - Space root URL loads the React UI
187
+ - `/health` returns healthy status
188
+ - OpenEnv endpoints are available (`/reset`, `/step`, `/state`, `/schema`)
189
+
190
+ Notes:
191
+
192
+ - Container reads `PORT` (defaults to `7860`) which is Space-friendly.
193
+ - Frontend static assets are served by FastAPI from `frontend/dist`.
194
+
195
+ ---
196
+
197
+ ## API Endpoints
198
+
199
+ OpenEnv/health:
200
+
201
+ - `POST /reset`
202
+ - `POST /step`
203
+ - `GET /state`
204
+ - `GET /health`
205
+ - `GET /schema`
206
+ - `WS /ws` (stateful session)
207
+
208
+ AI helper:
209
+
210
+ - `POST /agent/suggest`
211
+
212
+ ---
213
+
214
+ ## Testing
215
+
216
+ Run backend tests:
217
+
218
+ ```bash
219
+ python -m pytest backend/src/polypharmacy_env/tests -v
220
+ ```
221
+
222
+ Or run validation script:
223
+
224
+ ```bash
225
+ ./scripts/run_validation.sh
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Notes
231
+
232
+ - OpenEnv HTTP reset/step is stateless; multi-step episode continuity should use websocket (`/ws`).
233
+ - The frontend uses websocket for episode continuity and HTTP for AI suggestion.
234
+ - AI behavior includes rule-based guardrails to avoid repetitive low-value loops.
235
+
236
+ ---
237
+
238
+ ## Troubleshooting
239
+
240
+ - `ModuleNotFoundError: polypharmacy_env`
241
+ - Start backend using `./scripts/dev_backend.sh` from repo root.
242
+ - `/agent/suggest` fails
243
+ - Check `.env` keys and restart backend.
244
+ - UI state looks stale
245
+ - Hard refresh browser and click `Reset Episode`.
openenv-polypharmacy/.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/PROMPT.md DELETED
@@ -1,571 +0,0 @@
1
- You are an expert Python backend, ML, and infrastructure engineer.
2
- Your task is to implement a complete, production-ready OpenEnv environment called **PolypharmacyEnv** for training and evaluating agentic RL policies that act as an "elderly polypharmacy safety agent" (clinical pharmacist assistant).
3
-
4
- The deliverable MUST satisfy all of the following:
5
- - Fully compliant with the OpenEnv spec (typed models, `step()` / `reset()` / `state()`, `openenv.yaml`, HTTP server, Dockerfile).
6
- - Simulates a realistic healthcare workflow around elderly polypharmacy and dangerous drug combinations.
7
- - Defines at least **3 tasks** (easy β†’ medium β†’ hard) with deterministic agent graders producing scores in (0.0, 1.0).
8
- - Provides shaped rewards over the trajectory (not just sparse terminal rewards).
9
- - Includes a baseline LLM-based inference script `inference.py` in the repo root, following the evaluation requirements:
10
- - Uses the OpenAI Python client.
11
- - Reads `OPENAI_API_KEY`, `API_BASE_URL`, `MODEL_NAME`, and `HF_TOKEN` from the environment.
12
- - Emits structured stdout logs in the exact `[START]`, `[STEP]`, `[END]` format from the OpenEnv sample inference script.
13
- - Is containerized and deployable as a **Hugging Face Space** tagged with `openenv` that responds to OpenEnv-style `reset` / `step` / `state` HTTP calls.
14
-
15
- Implement everything described below.
16
-
17
- =================================================
18
- 1. Repository and folder structure
19
- =================================================
20
-
21
- Create a Python package repository with this structure (names are important unless clearly labeled as examples):
22
-
23
- - `openenv-polypharmacy/`
24
- - `openenv.yaml`
25
- - `README.md`
26
- - `requirements.txt`
27
- - `Dockerfile`
28
- - `inference.py` # baseline LLM agent per spec
29
- - `pyproject.toml` or `setup.cfg` (optional but recommended)
30
- - `src/`
31
- - `polypharmacy_env/`
32
- - `__init__.py`
33
- - `config.py`
34
- - `models.py` # Action, Observation, State, helper models
35
- - `env_core.py` # PolypharmacyEnv implementation
36
- - `tasks.py` # task setup utilities
37
- - `graders.py` # deterministic graders for each task
38
- - `rewards.py` # reward shaping logic
39
- - `data_loader.py` # load/preprocess patient and lookup data
40
- - `ddi_simulator.py` # local DDI / guideline simulator
41
- - `api/`
42
- - `__init__.py`
43
- - `schemas.py` # HTTP request/response schemas
44
- - `server.py` # FastAPI app exposing OpenEnv endpoints
45
- - `baselines/`
46
- - `__init__.py`
47
- - `heuristic_agent.py` # simple rule-based baseline agent
48
- - `random_agent.py` # trivial random baseline (optional)
49
- - `tests/`
50
- - `__init__.py`
51
- - `test_env_core.py`
52
- - `test_api.py`
53
- - `data/`
54
- - `raw/` # placeholder for real/synthetic source data
55
- - `processed/`
56
- - `lookups/`
57
- - `ddi_rules.csv`
58
- - `beers_criteria.csv`
59
- - `drug_metadata.csv`
60
- - `scripts/`
61
- - `preprocess_data.py`
62
- - `run_validation.sh` # optional; runs OpenEnv validator, tests, etc.
63
-
64
- Use Python 3.10+ with full type hints, and keep the code black/isort-compatible.
65
-
66
- =================================================
67
- 2. Domain, data, and clinical abstraction
68
- =================================================
69
-
70
- 2.1. Core scenario
71
-
72
- Model an elderly patient (age β‰₯ 65) with:
73
- - Demographics: age, sex.
74
- - Comorbidities: e.g., hypertension, diabetes, heart failure, CKD, dementia.
75
- - Basic labs: kidney function (eGFR category), liver function category.
76
- - A current medication list (polypharmacy, e.g., 3–15 drugs depending on task).
77
-
78
- Each **episode** is one medication-review session where the agent:
79
- - Observes patient info and current meds.
80
- - Optionally **queries** a DDI/guideline tool for specific drug pairs.
81
- - Proposes **interventions**:
82
- - `stop`: discontinue a drug.
83
- - `dose_reduce`: lower dose of a drug.
84
- - `substitute`: swap to a safer alternative.
85
- - `add_monitoring`: keep the drug but flag extra monitoring.
86
- - Calls `finish_review` when it decides the regimen is acceptable or budgets are exhausted.
87
-
88
- No external PHI, EHRs, or online APIs: all data is **synthetic** or de-identified and local to the container (CSV files).
89
-
90
- 2.2. Data files and CSV schemas
91
-
92
- Implement local CSVs under `data/lookups/`:
93
-
94
- **`drug_metadata.csv`**
95
- - `drug_id` (string; unique key)
96
- - `generic_name` (string)
97
- - `atc_class` (string)
98
- - `is_high_risk_elderly` (0/1)
99
- - `default_dose_mg` (float)
100
- - `min_dose_mg` (float)
101
- - `max_dose_mg` (float)
102
-
103
- **`beers_criteria.csv`**
104
- - `drug_id` (string)
105
- - `criterion_type` (enum string: `avoid`, `caution`, `dose_adjust`, `avoid_in_condition`)
106
- - `condition` (nullable string; e.g., `CKD`, `dementia`)
107
- - `rationale` (brief text)
108
-
109
- **`ddi_rules.csv`**
110
- - `drug_id_1` (string; normalized so `drug_id_1 < drug_id_2` lexicographically)
111
- - `drug_id_2` (string)
112
- - `severity` (enum string: `mild`, `moderate`, `severe`)
113
- - `mechanism` (short text)
114
- - `recommendation` (enum string: `avoid_combination`, `monitor_closely`, `dose_adjust`, `no_action`)
115
- - `base_risk_score` (float in [0.0, 1.0])
116
-
117
- Implement a synthetic patient-episode dataset under `data/processed/`:
118
-
119
- **`patients_polypharmacy.csv`**
120
- - `episode_id` (string)
121
- - `age` (int)
122
- - `sex` (enum: `M`, `F`, `O`)
123
- - `conditions` (semicolon-separated; e.g., `HTN;DM;CKD`)
124
- - `eGFR_category` (enum: `normal`, `mild`, `moderate`, `severe`)
125
- - `liver_function_category` (enum: `normal`, `impaired`)
126
- - `medication_ids` (semicolon-separated list of `drug_id`)
127
- - `baseline_risk_score` (float in [0.0, 1.0])
128
-
129
- 2.3. Preprocessing script
130
-
131
- In `scripts/preprocess_data.py`:
132
- - If real data is not provided, procedurally generate synthetic but plausible data using:
133
- - Random combinations of conditions and drugs constrained by simple rules (e.g., CKD + renally-cleared drugs).
134
- - Controlled distribution of high-risk DDIs and Beers violations.
135
- - Explicitly tag episodes as easy/medium/hard (e.g., via number of drugs, number/severity of DDIs, and number of Beers issues).
136
- - Save `patients_polypharmacy.csv` ready for the environment to consume.
137
-
138
- =================================================
139
- 3. OpenEnv models and environment implementation
140
- =================================================
141
-
142
- 3.1. Models
143
-
144
- In `models.py`, define dataclasses or Pydantic models that extend the appropriate OpenEnv base types (`Action`, `Observation`, `State`) and are JSON-compatible.
145
-
146
- Auxiliary models:
147
-
148
- **`MedicationEntry`**
149
- - `drug_id: str`
150
- - `generic_name: str`
151
- - `atc_class: str`
152
- - `dose_mg: float`
153
- - `frequency: str` # e.g., `qd`, `bid`
154
- - `route: str` # e.g., `po`
155
- - `is_high_risk_elderly: bool`
156
- - `beers_flags: list[str]` # e.g., `["avoid", "dose_adjust_CKD"]`
157
-
158
- **`InteractionQueryRecord`**
159
- - `drug_id_1: str`
160
- - `drug_id_2: str`
161
- - `severity: str | None`
162
- - `recommendation: str | None`
163
- - `risk_score: float | None`
164
- - `step_index: int`
165
-
166
- **`InterventionRecord`**
167
- - `target_drug_id: str`
168
- - `action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]`
169
- - `proposed_new_drug_id: str | None`
170
- - `rationale: str`
171
- - `step_index: int`
172
-
173
- Core wire models:
174
-
175
- **`PolypharmacyObservation`** (extends OpenEnv `Observation`)
176
- - `episode_id: str`
177
- - `task_id: Literal["easy_screening", "budgeted_screening", "complex_tradeoff"]`
178
- - `age: int`
179
- - `sex: str`
180
- - `conditions: list[str]`
181
- - `eGFR_category: str`
182
- - `liver_function_category: str`
183
- - `current_medications: list[MedicationEntry]`
184
- - `interaction_queries: list[InteractionQueryRecord]`
185
- - `interventions: list[InterventionRecord]`
186
- - `step_index: int`
187
- - `remaining_query_budget: int`
188
- - `remaining_intervention_budget: int`
189
- - `shaped_reward: float` # reward from last step
190
- - `done: bool`
191
-
192
- **`PolypharmacyAction`** (extends OpenEnv `Action`)
193
- - `action_type: Literal["query_ddi", "propose_intervention", "finish_review"]`
194
- - `drug_id_1: str | None` # for DDI queries or some interventions
195
- - `drug_id_2: str | None` # for DDI queries
196
- - `target_drug_id: str | None` # for interventions
197
- - `intervention_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"] | None`
198
- - `proposed_new_drug_id: str | None`
199
- - `rationale: str | None`
200
-
201
- **`PolypharmacyState`** (extends OpenEnv `State`)
202
- - `episode_id: str`
203
- - `task_id: str`
204
- - `step_count: int`
205
- - `max_steps: int`
206
- - `num_query_actions: int`
207
- - `num_interventions: int`
208
-
209
- 3.2. Environment core
210
-
211
- In `env_core.py`, implement `PolypharmacyEnv` extending the appropriate OpenEnv environment base class. It must implement:
212
-
213
- **`reset(task_id: str | None = None) -> PolypharmacyObservation`**
214
- - If `task_id` is `None`, default to medium (`budgeted_screening`).
215
- - Sample an episode from `patients_polypharmacy.csv` filtered by difficulty.
216
- - Initialize:
217
- - `episode_id`
218
- - `step_count = 0`
219
- - task-specific budgets (query, interventions, max_steps)
220
- - baseline regime and risk
221
- - empty `interaction_queries` and `interventions`
222
- - Return the initial `PolypharmacyObservation` with:
223
- - `step_index = 0`
224
- - `shaped_reward = 0.0`
225
- - `done = False`
226
-
227
- **`step(action: PolypharmacyAction) -> dict`**
228
- - Validate the action; if invalid:
229
- - Apply a negative reward.
230
- - Do not modify regimen, but log error in `info`.
231
- - If `action_type == "query_ddi"`:
232
- - If query budget exhausted, apply penalty and do not query.
233
- - Else:
234
- - Use `ddi_simulator.lookup_ddi(drug_id_1, drug_id_2)` to get severity, recommendation, base_risk_score.
235
- - Append an `InteractionQueryRecord`.
236
- - Apply a small negative reward for query cost.
237
- - If `action_type == "propose_intervention"`:
238
- - If intervention budget exhausted, apply penalty and ignore change.
239
- - Else:
240
- - Update `current_medications` according to `intervention_type`:
241
- - `stop`: remove medication.
242
- - `dose_reduce`: adjust dose downward within [min_dose_mg, default_dose_mg].
243
- - `substitute`: replace with a safer alternative from same `atc_class`.
244
- - `add_monitoring`: keep drug but tag in internal state.
245
- - Append an `InterventionRecord`.
246
- - Recompute current regimen risk using the risk model (see 3.3).
247
- - Compute shaped reward = (previous_risk - new_risk) - small intervention cost.
248
- - If `action_type == "finish_review"`:
249
- - Mark `done = True`.
250
- - Call the task’s grader to get episode-level score in [0.0, 1.0].
251
- - Add this as a terminal bonus to the current step reward.
252
-
253
- - In all cases:
254
- - Increment `step_count`.
255
- - Check `max_steps`; if exceeded, auto-terminate:
256
- - `done = True`
257
- - apply time-out penalty
258
- - call grader with current trajectory for a final score if appropriate.
259
- - Construct next `PolypharmacyObservation` with updated fields.
260
- - Return a dict:
261
- - `observation`: `PolypharmacyObservation`
262
- - `reward`: float shaped reward for this step
263
- - `done`: bool
264
- - `info`: dict with fields like `current_risk`, `baseline_risk`, `grader_score_if_terminal`, and debug flags.
265
-
266
- **`state` property**
267
- - Returns `PolypharmacyState` reflecting the current internal state.
268
-
269
- 3.3. DDI simulator and risk model
270
-
271
- In `ddi_simulator.py`:
272
- - Load `ddi_rules.csv` once via `data_loader`.
273
- - Implement `lookup_ddi(drug_id_1, drug_id_2) -> tuple[severity, recommendation, base_risk_score]`:
274
- - Normalize the pair ordering.
275
- - Look up row; if missing, return:
276
- - severity = `"none"`
277
- - recommendation = `"no_action"`
278
- - base_risk_score = 0.0
279
-
280
- In `rewards.py` (or a dedicated module), implement:
281
- - `compute_regimen_risk(current_drug_ids, patient_context, ddi_rules, beers_rules, drug_metadata) -> float`
282
- - Aggregate contributions from:
283
- - Beers violations (weighted by `criterion_type` and relevant conditions).
284
- - DDI base risk scores for all present drug pairs.
285
- - High-risk elderly drugs.
286
- - Normalize and clip to [0.0, 1.0].
287
-
288
- Use this function to compute:
289
- - `baseline_risk` at episode start.
290
- - Risk after each intervention step.
291
-
292
- Also implement:
293
- - `compute_shaped_reward(previous_risk, new_risk, action, context, partial_metrics) -> float`
294
- - Positive component: `previous_risk - new_risk`.
295
- - Negative components: per-query cost, per-intervention cost, invalid-action penalty, time-out penalty.
296
-
297
- =================================================
298
- 4. Tasks and graders (3 difficulty levels)
299
- =================================================
300
-
301
- Define three task IDs and semantics in `tasks.py` and `graders.py`:
302
-
303
- Task IDs:
304
- - `easy_screening`
305
- - `budgeted_screening`
306
- - `complex_tradeoff`
307
-
308
- 4.1. `easy_screening` (easy)
309
-
310
- - Small regimen: 3–5 drugs.
311
- - Exactly one **severe** DDI pair and possibly one simple Beers violation.
312
- - Budgets:
313
- - query_budget β‰ˆ 4
314
- - intervention_budget β‰ˆ 2
315
- - max_steps β‰ˆ 10
316
-
317
- Grader:
318
- - Input: full trajectory, baseline risk, final risk, list of interventions.
319
- - Compute:
320
- - `risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, Ξ΅)` (normalized).
321
- - `targeted_intervention_flag = 1.0` if at least one intervention affects one of the drugs in the known severe DDI pair, else 0.0.
322
- - Score:
323
- - `score = 0.5 * risk_reduction + 0.5 * targeted_intervention_flag`
324
- - Clip to [0.0, 1.0].
325
-
326
- 4.2. `budgeted_screening` (medium)
327
-
328
- - Medium regimen: 6–10 drugs.
329
- - Multiple DDIs (mild/moderate/severe) and multiple Beers issues.
330
- - Budgets:
331
- - query_budget β‰ˆ 8
332
- - intervention_budget β‰ˆ 3
333
- - max_steps β‰ˆ 20
334
-
335
- Grader:
336
- - Compute:
337
- - `risk_reduction_score` as normalized risk drop.
338
- - `intervention_precision_score` = fraction of interventions that actually reduce risk or fix guideline violations.
339
- - `query_efficiency_score` = (number of severe/moderate DDIs discovered) / (number of queries used), normalized.
340
- - Weighted score, for example:
341
- - `score = 0.5 * risk_reduction_score + 0.3 * intervention_precision_score + 0.2 * query_efficiency_score`
342
- - Clip to [0.0, 1.0].
343
-
344
- 4.3. `complex_tradeoff` (hard)
345
-
346
- - Larger regimen: 10–15 drugs.
347
- - Some drugs are **clinically critical** (e.g., anticoagulants, insulin analogues) and encoded as such in `drug_metadata` or a small internal map.
348
- - Episodes contain:
349
- - multiple DDIs and Beers issues, including ones involving critical drugs.
350
- - safer substitutes for some risky drugs.
351
-
352
- Budgets:
353
- - query_budget β‰ˆ 12
354
- - intervention_budget β‰ˆ 5
355
- - max_steps β‰ˆ 30
356
-
357
- Grader adds a **regimen disruption penalty** component:
358
- - Metrics:
359
- - `risk_reduction_score` (as above).
360
- - `critical_drug_penalty` = penalty if a critical drug is stopped without substitution to another suitable agent.
361
- - `total_drug_changes` = number of drugs stopped or substituted.
362
- - `regimen_disruption_penalty` derived from `total_drug_changes` and `critical_drug_penalty`.
363
-
364
- Example scoring:
365
- - `base = risk_reduction_score`
366
- - `penalty = Ξ± * regimen_disruption_penalty`
367
- - `score = clamp(base - penalty, 0.0, 1.0)`
368
-
369
- 4.4. Reward shaping
370
-
371
- In `rewards.py`, define a consistent shaping scheme:
372
- - On each query:
373
- - Small negative reward (e.g., βˆ’0.01) plus any small bonus if it discovers a severe DDI, if desired.
374
- - On each intervention:
375
- - Reward β‰ˆ (previous_risk - new_risk) βˆ’ small intervention cost.
376
- - On invalid actions:
377
- - Larger negative reward (e.g., βˆ’0.1) and no state change.
378
- - On `finish_review`:
379
- - Add the task-level `score` ∈ [0.0, 1.0] from the corresponding grader to that step’s shaped reward.
380
-
381
- Ensure the sum of step rewards per episode remains in a reasonable numeric range (e.g., roughly -5 to +5) while still allowing meaningful differentiation by graders.
382
-
383
- =================================================
384
- 5. HTTP API server and openenv.yaml
385
- =================================================
386
-
387
- 5.1. HTTP server (FastAPI)
388
-
389
- In `api/server.py`:
390
- - Implement a FastAPI app that maintains a `PolypharmacyEnv` instance (or a multiplexing scheme if needed).
391
- - Endpoints:
392
- - `POST /reset`:
393
- - Request body: may include `task_id` (string).
394
- - Response: serialized `PolypharmacyObservation`.
395
- - `POST /step`:
396
- - Request body: serialized `PolypharmacyAction`.
397
- - Response: dict with:
398
- - `observation`: `PolypharmacyObservation`
399
- - `reward`: float
400
- - `done`: bool
401
- - `info`: dict
402
- - `GET /state`:
403
- - Response: `PolypharmacyState`.
404
-
405
- Provide a module-level `app = FastAPI(...)` object for use with uvicorn and Hugging Face Spaces. Ensure the JSON schema is consistent with OpenEnv clients (simple, flat JSON for observation/action/state).
406
-
407
- 5.2. `openenv.yaml`
408
-
409
- At repo root, define `openenv.yaml` consistent with the latest OpenEnv spec. At minimum, include:
410
- - `name`: `polypharmacy_env`
411
- - `version`: e.g., `0.1.0`
412
- - `description`: human-readable description.
413
- - `author`: your details.
414
- - `tags`: e.g., `["healthcare", "polypharmacy", "openenv"]`
415
- - `tasks`:
416
- - One entry per task:
417
- - `id`: `"easy_screening"` / `"budgeted_screening"` / `"complex_tradeoff"`
418
- - `description`: one-line description
419
- - `difficulty`: `"easy"`, `"medium"`, `"hard"`
420
-
421
- Ensure `openenv validate` (or equivalent validator) passes once implemented.
422
-
423
- =================================================
424
- 6. Baseline heuristic (non-LLM) agent
425
- =================================================
426
-
427
- In `baselines/heuristic_agent.py`, implement a simple, deterministic baseline agent that:
428
-
429
- For each episode:
430
- - Iterates through all unordered medication pairs within query budget:
431
- - Calls `query_ddi` via the environment for each pair until the query budget is exhausted or all pairs are examined.
432
- - Records severe and moderate interactions.
433
- - After querying:
434
- - For each severe DDI pair:
435
- - Try `substitute` one of the drugs using `drug_metadata`:
436
- - Prefer substitute within same `atc_class` that:
437
- - is not marked high-risk elderly.
438
- - does not participate in known severe DDIs with the rest of the regimen.
439
- - If no substitute exists, propose `stop` for the higher-risk drug.
440
- - Respect intervention budget limits.
441
- - Finally, call `finish_review`.
442
-
443
- This baseline should be callable as a simple Python function that interacts with `PolypharmacyEnv` directly (without HTTP).
444
-
445
- =================================================
446
- 7. Baseline LLM inference script (inference.py)
447
- =================================================
448
-
449
- At repo root, create `inference.py` that:
450
-
451
- 7.1. Uses the OpenAI Python client
452
-
453
- - Import and configure the official OpenAI Python client.
454
- - Read environment variables:
455
- - `OPENAI_API_KEY` (required).
456
- - `API_BASE_URL` (base URL for LLM; default to OpenAI standard if not set).
457
- - `MODEL_NAME` (e.g., `gpt-4.1` or similar).
458
- - `HF_TOKEN` (if needed for HF auth; do not hardcode).
459
- - Read `POLYPHARMACY_ENV_URL` (or similar) for the environment’s HTTP base URL.
460
-
461
- 7.2. Implements the required logging format
462
-
463
- - For each **run** across all tasks:
464
- - Emit a `[START]` line with a JSON payload exactly matching the evaluation specification:
465
- - Fields such as `run_id`, `task_id`, `model`, etc., in the same order and naming as the sample OpenEnv inference script.
466
- - For each **step** in an episode:
467
- - Emit a `[STEP]` line with JSON fields including:
468
- - `run_id`
469
- - `task_id`
470
- - `episode_id`
471
- - `step_index`
472
- - `observation_summary` (brief, machine-readable summary)
473
- - `action_payload` (the action sent to the env)
474
- - `reward`
475
- - `done`
476
- - After finishing an episode for a task:
477
- - Emit an `[END]` line summarizing:
478
- - `run_id`
479
- - `task_id`
480
- - per-episode statistics (e.g., total reward, grader score from last step’s `info`).
481
- - The stdout format MUST follow the sample exactly:
482
- - Same tags: `[START]`, `[STEP]`, `[END]`.
483
- - Same JSON field names and ordering as the provided reference.
484
- - No extra prints except these structured logs (and necessary error messages to stderr).
485
-
486
- 7.3. LLM agent loop
487
-
488
- - For each task (`easy_screening`, `budgeted_screening`, `complex_tradeoff`):
489
- - Run a fixed small number of episodes (e.g., 5–10 per task) for baseline scoring.
490
- - For each episode:
491
- - Call `/reset` with the task id.
492
- - At each step:
493
- - Summarize the observation into a concise prompt for the LLM:
494
- - Include age, sex, conditions, high-risk flags, budgets, and a compressed view of meds and previous actions.
495
- - Ask the model to output a **strict JSON** representing `PolypharmacyAction` fields.
496
- - Parse and validate the JSON; if invalid, fall back to a safe default (e.g., `finish_review` or a no-op) and penalize in evaluation.
497
- - Send this action to `/step` and log `[STEP]`.
498
- - End when `done=True` or max_steps is reached.
499
- - At the end, print aggregate scores per task and overall.
500
-
501
- Make sure runtime < 20 minutes and that the script can run within 2 vCPUs and 8 GB RAM.
502
-
503
- =================================================
504
- 8. Dockerfile and Hugging Face Space
505
- =================================================
506
-
507
- 8.1. Dockerfile
508
-
509
- Create a `Dockerfile` that:
510
- - Starts from a slim Python image (e.g., `python:3.11-slim`).
511
- - Installs system dependencies as needed (e.g., `build-essential`, `curl`).
512
- - Copies the project into the container.
513
- - Installs Python dependencies from `requirements.txt`.
514
- - Sets appropriate environment variables for the app (e.g., `PORT=7860`).
515
- - Exposes port 7860.
516
- - Uses a `CMD` or `ENTRYPOINT` that runs the FastAPI server, for example:
517
- - `uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860`
518
-
519
- 8.2. Hugging Face Space
520
-
521
- Ensure the repository is ready to be used as a Hugging Face Space:
522
- - Space type: `docker`.
523
- - Tag: `openenv`.
524
- - On container start, the server must listen on the correct port and respond to:
525
- - `POST /reset`
526
- - `POST /step`
527
- - `GET /state`
528
- - The environment must start cleanly with `docker build` + `docker run` locally.
529
-
530
- =================================================
531
- 9. README and documentation
532
- =================================================
533
-
534
- In `README.md`, include:
535
-
536
- - **Environment description & motivation**:
537
- - What PolypharmacyEnv simulates.
538
- - Why elderly polypharmacy safety matters.
539
- - **Action and observation spaces**:
540
- - Describe `PolypharmacyAction`, `PolypharmacyObservation`, and `PolypharmacyState` fields and semantics.
541
- - **Task descriptions**:
542
- - `easy_screening`, `budgeted_screening`, `complex_tradeoff`, their difficulty and goals.
543
- - **Reward structure**:
544
- - Summarize shaping and terminal rewards.
545
- - **Setup & usage**:
546
- - How to install dependencies.
547
- - How to run the API server locally (uvicorn command).
548
- - How to run the heuristic baseline.
549
- - How to run `inference.py` with environment variables.
550
- - **Baseline scores**:
551
- - Document reproducible baseline scores for each task (heuristic agent, and LLM baseline if available).
552
-
553
- =================================================
554
- 10. Validation and quality gates
555
- =================================================
556
-
557
- - Ensure:
558
- - `openenv.yaml` and the HTTP server pass the OpenEnv validation script.
559
- - `docker build` and `docker run` work without errors.
560
- - `inference.py` completes under 20 minutes, within 2 vCPUs / 8 GB RAM.
561
- - All graders:
562
- - Are deterministic.
563
- - Return scores strictly in [0.0, 1.0].
564
- - No grader returns a constant score irrespective of behavior.
565
-
566
- Aim for clean, well-structured, well-documented code with clear separation of concerns between:
567
- - Data loading,
568
- - Environment state & dynamics,
569
- - Reward/grade logic,
570
- - HTTP serving,
571
- - Baseline agents and inference.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv-polypharmacy/README.md DELETED
@@ -1,184 +0,0 @@
1
- # PolypharmacyEnv
2
-
3
- An [OpenEnv](https://github.com/meta-pytorch/OpenEnv)-compliant reinforcement-learning environment that simulates **elderly polypharmacy medication review**. An RL agent acts as a clinical pharmacist assistant, identifying dangerous drug-drug interactions (DDIs), Beers-criteria violations, and proposing safe interventions.
4
-
5
- ---
6
-
7
- ## Motivation
8
-
9
- Polypharmacy (concurrent use of multiple medications) is extremely common in elderly patients (age >= 65) and carries significant risks:
10
-
11
- - **Drug-drug interactions** can cause adverse events, hospitalisation, and death.
12
- - **Beers-criteria violations** flag medications that are inappropriate or require dose adjustments in older adults.
13
- - Stopping critical medications (anticoagulants, insulin) without proper substitution can be equally dangerous.
14
-
15
- This environment lets RL and LLM-based agents learn to **balance risk reduction against regimen stability**.
16
-
17
- ---
18
-
19
- ## Action Space
20
-
21
- Each step, the agent sends a `PolypharmacyAction` with one of three action types:
22
-
23
- | `action_type` | Required fields | Description |
24
- |---|---|---|
25
- | `query_ddi` | `drug_id_1`, `drug_id_2` | Query the DDI database for an interaction between two drugs |
26
- | `propose_intervention` | `target_drug_id`, `intervention_type` | Propose changing a medication (`stop`, `dose_reduce`, `substitute`, `add_monitoring`) |
27
- | `finish_review` | β€” | End the review and trigger final grading |
28
-
29
- Optional fields: `proposed_new_drug_id`, `rationale`.
30
-
31
- ## Observation Space
32
-
33
- `PolypharmacyObservation` includes:
34
-
35
- - **Patient demographics**: `age`, `sex`, `conditions`, `eGFR_category`, `liver_function_category`
36
- - **Medications**: list of `MedicationEntry` (drug_id, name, class, dose, high-risk flags, Beers flags)
37
- - **History**: `interaction_queries` (past DDI query results), `interventions` (past actions)
38
- - **Budgets**: `remaining_query_budget`, `remaining_intervention_budget`
39
- - **Reward signals**: `shaped_reward`, `done`
40
-
41
- ## State
42
-
43
- `PolypharmacyState`: `episode_id`, `task_id`, `step_count`, `max_steps`, `num_query_actions`, `num_interventions`.
44
-
45
- ---
46
-
47
- ## Tasks
48
-
49
- | Task ID | Difficulty | Drugs | Query Budget | Intervention Budget | Max Steps | Description |
50
- |---|---|---|---|---|---|---|
51
- | `easy_screening` | Easy | 3-5 | 4 | 2 | 10 | One severe DDI, simple resolution |
52
- | `budgeted_screening` | Medium | 6-10 | 8 | 3 | 20 | Multiple DDIs + Beers issues, limited budgets |
53
- | `complex_tradeoff` | Hard | 10-15 | 12 | 5 | 30 | Critical drugs, trade-off between risk and regimen stability |
54
-
55
- ---
56
-
57
- ## Reward Structure
58
-
59
- **Per-step shaped rewards:**
60
-
61
- | Event | Reward |
62
- |---|---|
63
- | DDI query | -0.01 (cost) + 0.03 bonus if severe DDI discovered |
64
- | Successful intervention | +(previous_risk - new_risk) - 0.02 cost |
65
- | Invalid action | -0.10 penalty |
66
- | Timeout (max steps exceeded) | -0.20 penalty |
67
- | `finish_review` | + grader score (0.0 to 1.0) |
68
-
69
- **Terminal grader scoring:**
70
- - **Easy**: 50% risk reduction + 50% targeted intervention flag
71
- - **Medium**: 50% risk reduction + 30% intervention precision + 20% query efficiency
72
- - **Hard**: risk reduction - regimen disruption penalty - critical drug penalty
73
-
74
- ---
75
-
76
- ## Setup & Usage
77
-
78
- ### Install dependencies
79
-
80
- ```bash
81
- pip install -r requirements.txt
82
- ```
83
-
84
- ### Generate synthetic data
85
-
86
- ```bash
87
- python3 scripts/preprocess_data.py
88
- ```
89
-
90
- ### Run the API server locally
91
-
92
- ```bash
93
- PYTHONPATH=src uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860
94
- ```
95
-
96
- ### Run the heuristic baseline
97
-
98
- ```bash
99
- PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
100
- ```
101
-
102
- ### Run tests
103
-
104
- ```bash
105
- PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
106
- ```
107
-
108
- ### Run `inference.py` (LLM baseline)
109
-
110
- ```bash
111
- # Start the server first, then in another terminal:
112
- export OPENAI_API_KEY="sk-..."
113
- export MODEL_NAME="gpt-4.1"
114
- export POLYPHARMACY_ENV_URL="http://localhost:7860"
115
- python3 inference.py
116
- ```
117
-
118
- ### Docker
119
-
120
- ```bash
121
- docker build -t polypharmacy-env .
122
- docker run -p 7860:7860 polypharmacy-env
123
- ```
124
-
125
- ---
126
-
127
- ## Hugging Face Space
128
-
129
- This repo is ready for deployment as a HF Space:
130
-
131
- - **Space type**: `docker`
132
- - **Tag**: `openenv`
133
- - The container listens on port 7860 and exposes `/reset`, `/step`, `/state`, `/health`.
134
-
135
- ---
136
-
137
- ## Baseline Scores
138
-
139
- ### Heuristic Agent (deterministic, rule-based)
140
-
141
- | Task | Avg Score | Avg Reward |
142
- |---|---|---|
143
- | `easy_screening` | ~0.96 | ~1.30 |
144
- | `budgeted_screening` | ~0.48 | ~0.45 |
145
- | `complex_tradeoff` | ~0.24 | ~0.11 |
146
-
147
- *(Scores vary by seed; run `scripts/run_validation.sh` for exact numbers.)*
148
-
149
- ---
150
-
151
- ## Project Structure
152
-
153
- ```
154
- openenv-polypharmacy/
155
- openenv.yaml # OpenEnv manifest
156
- Dockerfile # Container image
157
- inference.py # LLM baseline script
158
- requirements.txt
159
- pyproject.toml
160
- src/polypharmacy_env/
161
- config.py # Constants, task configs
162
- models.py # Pydantic action/observation/state models
163
- env_core.py # PolypharmacyEnv implementation
164
- tasks.py # Task selection utilities
165
- graders.py # Deterministic graders (3 difficulty levels)
166
- rewards.py # Reward shaping logic
167
- data_loader.py # CSV data loading
168
- ddi_simulator.py # Drug interaction lookup engine
169
- api/
170
- server.py # FastAPI HTTP server
171
- schemas.py # Request/response schemas
172
- baselines/
173
- heuristic_agent.py # Rule-based baseline
174
- random_agent.py # Random baseline
175
- tests/
176
- test_env_core.py
177
- test_api.py
178
- data/
179
- lookups/ # Drug metadata, DDI rules, Beers criteria CSVs
180
- processed/ # Synthetic patient episodes
181
- scripts/
182
- preprocess_data.py # Synthetic data generator
183
- run_validation.sh # Run tests + baseline
184
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv-polypharmacy/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,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ function resolveApiBase() {
4
+ const explicitBase = import.meta.env.VITE_API_BASE;
5
+ if (explicitBase) return explicitBase.replace(/\/$/, "");
6
+
7
+ const host = window.location.hostname;
8
+ const isLocal =
9
+ host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0";
10
+
11
+ // In local Vite dev, backend runs on :7860. In Spaces/prod, serve same-origin.
12
+ if (isLocal && window.location.port === "5173") {
13
+ return "http://localhost:7860";
14
+ }
15
+ return window.location.origin.replace(/\/$/, "");
16
+ }
17
+
18
+ const API_BASE = resolveApiBase();
19
+ const WS_URL = `${API_BASE.replace(/^http/, "ws")}/ws`;
20
+ const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
21
+
22
+ async function apiPost(path, body) {
23
+ const res = await fetch(`${API_BASE}${path}`, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify(body),
27
+ });
28
+ if (!res.ok) {
29
+ const msg = await res.text();
30
+ throw new Error(msg || `HTTP ${res.status}`);
31
+ }
32
+ return res.json();
33
+ }
34
+
35
+ export default function App() {
36
+ const [taskId, setTaskId] = useState("budgeted_screening");
37
+ const [obs, setObs] = useState(null);
38
+ const [log, setLog] = useState([]);
39
+ const [loading, setLoading] = useState(false);
40
+ const [action, setAction] = useState({
41
+ action_type: "query_ddi",
42
+ drug_id_1: "",
43
+ drug_id_2: "",
44
+ target_drug_id: "",
45
+ intervention_type: "stop",
46
+ proposed_new_drug_id: "",
47
+ rationale: "",
48
+ });
49
+
50
+ const medIds = useMemo(
51
+ () => (obs?.current_medications || []).map((m) => m.drug_id),
52
+ [obs]
53
+ );
54
+ const hasValidEpisode = Boolean(obs?.episode_id) && (obs?.current_medications?.length || 0) > 0;
55
+ const isDone = Boolean(obs?.done);
56
+ const finalScore =
57
+ typeof obs?.metadata?.grader_score === "number" ? obs.metadata.grader_score : null;
58
+ const noBudgetsLeft =
59
+ hasValidEpisode &&
60
+ (obs?.remaining_query_budget ?? 0) <= 0 &&
61
+ (obs?.remaining_intervention_budget ?? 0) <= 0;
62
+ const wsRef = useRef(null);
63
+ const pendingRef = useRef([]);
64
+
65
+ const wsEnsure = async () => {
66
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
67
+ if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) {
68
+ await new Promise((r) => setTimeout(r, 80));
69
+ return wsEnsure();
70
+ }
71
+
72
+ const ws = new WebSocket(WS_URL);
73
+ wsRef.current = ws;
74
+
75
+ ws.onmessage = (evt) => {
76
+ try {
77
+ const msg = JSON.parse(evt.data);
78
+ const pending = pendingRef.current.shift();
79
+ if (pending) pending.resolve(msg);
80
+ } catch (e) {
81
+ const pending = pendingRef.current.shift();
82
+ if (pending) pending.reject(e);
83
+ }
84
+ };
85
+ ws.onerror = (err) => {
86
+ const pending = pendingRef.current.shift();
87
+ if (pending) pending.reject(err);
88
+ };
89
+ ws.onclose = () => {
90
+ wsRef.current = null;
91
+ };
92
+
93
+ await new Promise((resolve, reject) => {
94
+ const t = setTimeout(() => reject(new Error("WebSocket connect timeout")), 2500);
95
+ ws.onopen = () => {
96
+ clearTimeout(t);
97
+ resolve();
98
+ };
99
+ });
100
+ return ws;
101
+ };
102
+
103
+ const wsSend = async (type, data) => {
104
+ const ws = await wsEnsure();
105
+ return await new Promise((resolve, reject) => {
106
+ pendingRef.current.push({ resolve, reject });
107
+ ws.send(JSON.stringify({ type, data }));
108
+ });
109
+ };
110
+
111
+ useEffect(() => {
112
+ return () => {
113
+ try {
114
+ wsRef.current?.close();
115
+ } catch {
116
+ // ignore
117
+ }
118
+ };
119
+ }, []);
120
+
121
+ const appendLog = (text) => {
122
+ setLog((prev) => [`${new Date().toLocaleTimeString()} ${text}`, ...prev].slice(0, 20));
123
+ };
124
+
125
+ const normalizeObsFromWs = (packetData) => {
126
+ const observation = packetData?.observation || {};
127
+ const mergedMetadata = {
128
+ ...(observation?.metadata || {}),
129
+ ...(packetData?.info || {}),
130
+ };
131
+ return {
132
+ ...observation,
133
+ done: Boolean(packetData?.done ?? observation?.done ?? false),
134
+ reward: packetData?.reward ?? observation?.reward ?? null,
135
+ metadata: mergedMetadata,
136
+ };
137
+ };
138
+
139
+ const handleReset = async () => {
140
+ setLoading(true);
141
+ try {
142
+ const msg = await wsSend("reset", { task_id: taskId });
143
+ const data = msg?.data || {};
144
+ const normalized = normalizeObsFromWs(data);
145
+ setObs(normalized);
146
+ const ids = (normalized?.current_medications || []).map((m) => m.drug_id);
147
+ setAction((prev) => ({
148
+ ...prev,
149
+ drug_id_1: ids[0] || "",
150
+ drug_id_2: ids[1] || "",
151
+ target_drug_id: ids[0] || "",
152
+ }));
153
+ appendLog(`Reset task=${taskId}`);
154
+ } catch (err) {
155
+ appendLog(`Reset failed: ${err.message}`);
156
+ } finally {
157
+ setLoading(false);
158
+ }
159
+ };
160
+
161
+ const buildActionPayload = () => {
162
+ if (noBudgetsLeft) {
163
+ return { action_type: "finish_review" };
164
+ }
165
+ if (action.action_type === "query_ddi") {
166
+ return {
167
+ action_type: "query_ddi",
168
+ drug_id_1: action.drug_id_1,
169
+ drug_id_2: action.drug_id_2,
170
+ };
171
+ }
172
+ if (action.action_type === "propose_intervention") {
173
+ return {
174
+ action_type: "propose_intervention",
175
+ target_drug_id: action.target_drug_id,
176
+ intervention_type: action.intervention_type,
177
+ proposed_new_drug_id: action.proposed_new_drug_id || undefined,
178
+ rationale: action.rationale || undefined,
179
+ };
180
+ }
181
+ return { action_type: "finish_review" };
182
+ };
183
+
184
+ const isActionValid = () => {
185
+ if (!hasValidEpisode) return false;
186
+ if (isDone) return false;
187
+ if (noBudgetsLeft) return true;
188
+ if (action.action_type === "query_ddi") {
189
+ return Boolean(action.drug_id_1 && action.drug_id_2);
190
+ }
191
+ if (action.action_type === "propose_intervention") {
192
+ return Boolean(action.target_drug_id && action.intervention_type);
193
+ }
194
+ return true;
195
+ };
196
+
197
+ const handleStep = async (overrideAction = null) => {
198
+ if (!hasValidEpisode) {
199
+ appendLog("Run Reset Episode before stepping.");
200
+ return;
201
+ }
202
+ setLoading(true);
203
+ try {
204
+ const payload = overrideAction || buildActionPayload();
205
+ const msg = await wsSend("step", payload);
206
+ const data = msg?.data || {};
207
+ const normalized = normalizeObsFromWs(data);
208
+ setObs(normalized);
209
+ appendLog(`Step: ${payload.action_type} -> reward=${data.reward ?? 0}`);
210
+ } catch (err) {
211
+ appendLog(`Step failed: ${err.message}`);
212
+ } finally {
213
+ setLoading(false);
214
+ }
215
+ };
216
+
217
+ const askAi = async () => {
218
+ if (!hasValidEpisode) {
219
+ appendLog("Run Reset Episode before asking AI.");
220
+ return;
221
+ }
222
+ setLoading(true);
223
+ try {
224
+ const data = await apiPost("/agent/suggest", { observation: obs });
225
+ appendLog(`AI suggestion: ${data.action.action_type}`);
226
+ await handleStep(data.action);
227
+ } catch (err) {
228
+ appendLog(`AI suggestion failed: ${err.message}`);
229
+ } finally {
230
+ setLoading(false);
231
+ }
232
+ };
233
+
234
+ return (
235
+ <div className="shell">
236
+ <div className="bg-orb orb-a" />
237
+ <div className="bg-orb orb-b" />
238
+
239
+ <div className="container">
240
+ <header className="topbar glass">
241
+ <div className="title-wrap">
242
+ <h1>Polypharmacy Control Center</h1>
243
+ <p>Metaverse Clinical Ops Console</p>
244
+ </div>
245
+ <div className={`status-chip ${hasValidEpisode ? "live" : "idle"}`}>
246
+ {hasValidEpisode ? "Session Live" : "Waiting for reset"}
247
+ </div>
248
+ <div className="actions">
249
+ <select value={taskId} onChange={(e) => setTaskId(e.target.value)}>
250
+ {TASKS.map((t) => (
251
+ <option key={t} value={t}>
252
+ {t}
253
+ </option>
254
+ ))}
255
+ </select>
256
+ <button onClick={handleReset} disabled={loading}>
257
+ Reset Episode
258
+ </button>
259
+ <button className="secondary" onClick={askAi} disabled={!hasValidEpisode || isDone || loading}>
260
+ Ask AI + Auto Step
261
+ </button>
262
+ </div>
263
+ </header>
264
+
265
+ <main className="layout">
266
+ <section className="panel glass panel-wide">
267
+ <h2>Episode</h2>
268
+ {hasValidEpisode ? (
269
+ <div className="kpi-grid">
270
+ <div><span>Episode</span><strong>{obs.episode_id}</strong></div>
271
+ <div><span>Task</span><strong>{obs.task_id}</strong></div>
272
+ <div><span>Age / Sex</span><strong>{obs.age} / {obs.sex}</strong></div>
273
+ <div><span>Step</span><strong>{obs.step_index}</strong></div>
274
+ <div><span>Query budget</span><strong>{obs.remaining_query_budget}</strong></div>
275
+ <div><span>Intervention budget</span><strong>{obs.remaining_intervention_budget}</strong></div>
276
+ </div>
277
+ ) : (
278
+ <p className="muted">Start with Reset Episode. Until then, step actions are blocked.</p>
279
+ )}
280
+ {noBudgetsLeft && (
281
+ <p className="muted budget-note">Query and intervention budgets are exhausted. Finish review to get final score.</p>
282
+ )}
283
+ {isDone && (
284
+ <p className="muted budget-note">
285
+ Episode complete
286
+ {finalScore !== null ? ` β€’ final score: ${finalScore.toFixed(3)}` : ""}.
287
+ Click Reset Episode to start a new case.
288
+ </p>
289
+ )}
290
+ </section>
291
+
292
+ <section className="panel glass">
293
+ <h2>Action Console</h2>
294
+ <div className="action-row">
295
+ <label>Action type</label>
296
+ <select
297
+ value={action.action_type}
298
+ onChange={(e) => setAction((a) => ({ ...a, action_type: e.target.value }))}
299
+ >
300
+ <option value="query_ddi">query_ddi</option>
301
+ <option value="propose_intervention">propose_intervention</option>
302
+ <option value="finish_review">finish_review</option>
303
+ </select>
304
+ </div>
305
+
306
+ {action.action_type === "query_ddi" && (
307
+ <div className="stack stack-two">
308
+ <input
309
+ placeholder="drug_id_1"
310
+ value={action.drug_id_1}
311
+ onChange={(e) => setAction((a) => ({ ...a, drug_id_1: e.target.value }))}
312
+ />
313
+ <input
314
+ placeholder="drug_id_2"
315
+ value={action.drug_id_2}
316
+ onChange={(e) => setAction((a) => ({ ...a, drug_id_2: e.target.value }))}
317
+ />
318
+ </div>
319
+ )}
320
+
321
+ {action.action_type === "propose_intervention" && (
322
+ <div className="stack">
323
+ <select
324
+ value={action.target_drug_id}
325
+ onChange={(e) => setAction((a) => ({ ...a, target_drug_id: e.target.value }))}
326
+ >
327
+ <option value="">Select target drug</option>
328
+ {medIds.map((id) => (
329
+ <option key={id} value={id}>
330
+ {id}
331
+ </option>
332
+ ))}
333
+ </select>
334
+ <select
335
+ value={action.intervention_type}
336
+ onChange={(e) => setAction((a) => ({ ...a, intervention_type: e.target.value }))}
337
+ >
338
+ <option value="stop">stop</option>
339
+ <option value="dose_reduce">dose_reduce</option>
340
+ <option value="substitute">substitute</option>
341
+ <option value="add_monitoring">add_monitoring</option>
342
+ </select>
343
+ <input
344
+ placeholder="proposed_new_drug_id (optional)"
345
+ value={action.proposed_new_drug_id}
346
+ onChange={(e) =>
347
+ setAction((a) => ({ ...a, proposed_new_drug_id: e.target.value }))
348
+ }
349
+ />
350
+ <input
351
+ placeholder="rationale (optional)"
352
+ value={action.rationale}
353
+ onChange={(e) => setAction((a) => ({ ...a, rationale: e.target.value }))}
354
+ />
355
+ </div>
356
+ )}
357
+ <button onClick={() => handleStep()} disabled={!isActionValid() || loading}>
358
+ {noBudgetsLeft ? "Finish Review" : "Submit Step"}
359
+ </button>
360
+ </section>
361
+
362
+ <section className="panel glass">
363
+ <h2>Current Medications</h2>
364
+ <div className="med-grid">
365
+ {(obs?.current_medications || []).map((m) => (
366
+ <div key={m.drug_id} className="med-card">
367
+ <strong>{m.drug_id}</strong>
368
+ <p>{m.generic_name}</p>
369
+ <small>{m.dose_mg} mg β€’ {m.atc_class}</small>
370
+ </div>
371
+ ))}
372
+ </div>
373
+ </section>
374
+
375
+ <section className="panel glass">
376
+ <h2>Event Log</h2>
377
+ <div className="logs">
378
+ {log.map((line, idx) => (
379
+ <div key={idx}>{line}</div>
380
+ ))}
381
+ </div>
382
+ </section>
383
+ </main>
384
+ </div>
385
+ </div>
386
+ );
387
+ }
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,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #070814;
3
+ --bg-layer: #0a1026;
4
+ --panel: rgba(14, 22, 44, 0.72);
5
+ --panel-solid: rgba(20, 28, 52, 0.92);
6
+ --text: #e8f1ff;
7
+ --muted: #9ab2db;
8
+ --primary: #37d4ff;
9
+ --primary-2: #5a8dff;
10
+ --accent: #9d59ff;
11
+ --success: #6dfbcf;
12
+ --border: rgba(122, 162, 255, 0.28);
13
+ --line: rgba(109, 143, 225, 0.18);
14
+ --shadow: 0 16px 45px rgba(5, 8, 23, 0.6);
15
+ --shadow-strong: 0 14px 32px rgba(44, 105, 255, 0.4);
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ margin: 0;
24
+ color: var(--text);
25
+ font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
26
+ background:
27
+ radial-gradient(circle at 8% 12%, rgba(121, 87, 255, 0.22), transparent 38%),
28
+ radial-gradient(circle at 88% 20%, rgba(59, 204, 255, 0.26), transparent 34%),
29
+ radial-gradient(circle at 50% 100%, rgba(43, 128, 255, 0.26), transparent 40%),
30
+ linear-gradient(145deg, var(--bg) 0%, var(--bg-layer) 60%, #04060f 100%);
31
+ background-attachment: fixed;
32
+ }
33
+
34
+ .shell {
35
+ min-height: 100vh;
36
+ position: relative;
37
+ overflow: hidden;
38
+ padding: 24px 16px 34px;
39
+ }
40
+
41
+ .container {
42
+ width: min(1320px, 100%);
43
+ margin: 0 auto;
44
+ position: relative;
45
+ z-index: 2;
46
+ }
47
+
48
+ .bg-orb {
49
+ position: absolute;
50
+ border-radius: 50%;
51
+ pointer-events: none;
52
+ opacity: 0.9;
53
+ filter: blur(18px);
54
+ }
55
+
56
+ .orb-a {
57
+ width: min(46vw, 530px);
58
+ aspect-ratio: 1 / 1;
59
+ right: -9%;
60
+ top: -10%;
61
+ background: radial-gradient(circle, rgba(52, 203, 255, 0.35), rgba(52, 203, 255, 0.04) 70%);
62
+ }
63
+
64
+ .orb-b {
65
+ width: min(40vw, 460px);
66
+ aspect-ratio: 1 / 1;
67
+ left: -9%;
68
+ bottom: -15%;
69
+ background: radial-gradient(circle, rgba(160, 102, 255, 0.3), rgba(160, 102, 255, 0.06) 72%);
70
+ }
71
+
72
+ .glass {
73
+ background:
74
+ linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.01)),
75
+ var(--panel);
76
+ border: 1px solid var(--border);
77
+ box-shadow: var(--shadow);
78
+ backdrop-filter: blur(12px);
79
+ }
80
+
81
+ .topbar {
82
+ border-radius: 24px;
83
+ padding: clamp(14px, 2vw, 20px);
84
+ display: grid;
85
+ gap: 12px 16px;
86
+ grid-template-columns: minmax(220px, 1.2fr) auto minmax(280px, 1fr);
87
+ align-items: center;
88
+ }
89
+
90
+ .title-wrap h1 {
91
+ margin: 0;
92
+ font-size: clamp(1.15rem, 2.2vw, 1.95rem);
93
+ letter-spacing: 0.02em;
94
+ text-transform: uppercase;
95
+ text-shadow: 0 0 16px rgba(106, 192, 255, 0.3);
96
+ }
97
+
98
+ .title-wrap p {
99
+ margin: 6px 0 0;
100
+ font-size: 0.84rem;
101
+ color: var(--muted);
102
+ letter-spacing: 0.03em;
103
+ text-transform: uppercase;
104
+ }
105
+
106
+ .status-chip {
107
+ justify-self: center;
108
+ padding: 7px 14px;
109
+ border-radius: 999px;
110
+ font-size: 0.72rem;
111
+ font-weight: 700;
112
+ letter-spacing: 0.08em;
113
+ text-transform: uppercase;
114
+ border: 1px solid transparent;
115
+ }
116
+
117
+ .status-chip.live {
118
+ color: #052c24;
119
+ background: linear-gradient(90deg, rgba(126, 255, 220, 0.9), rgba(84, 244, 196, 0.95));
120
+ box-shadow: 0 0 14px rgba(96, 244, 198, 0.36);
121
+ }
122
+
123
+ .status-chip.idle {
124
+ color: #d8e8ff;
125
+ border-color: rgba(117, 186, 255, 0.48);
126
+ background: rgba(60, 106, 198, 0.25);
127
+ }
128
+
129
+ .actions {
130
+ display: flex;
131
+ justify-content: flex-end;
132
+ flex-wrap: wrap;
133
+ gap: 10px;
134
+ }
135
+
136
+ button,
137
+ select,
138
+ input {
139
+ width: 100%;
140
+ min-height: 42px;
141
+ border-radius: 12px;
142
+ border: 1px solid var(--border);
143
+ font-size: 0.92rem;
144
+ padding: 10px 12px;
145
+ color: var(--text);
146
+ background: rgba(11, 19, 38, 0.84);
147
+ }
148
+
149
+ select,
150
+ input {
151
+ transition: border-color 120ms ease, box-shadow 120ms ease;
152
+ }
153
+
154
+ select:focus,
155
+ input:focus {
156
+ outline: none;
157
+ border-color: rgba(119, 200, 255, 0.88);
158
+ box-shadow: 0 0 0 2px rgba(95, 187, 255, 0.18);
159
+ }
160
+
161
+ button {
162
+ cursor: pointer;
163
+ border: 0;
164
+ width: auto;
165
+ font-weight: 700;
166
+ letter-spacing: 0.02em;
167
+ background: linear-gradient(135deg, var(--primary), var(--primary-2) 55%, var(--accent));
168
+ box-shadow: var(--shadow-strong);
169
+ transition: transform 140ms ease, filter 140ms ease, box-shadow 140ms ease;
170
+ }
171
+
172
+ button:hover {
173
+ transform: translateY(-1px);
174
+ filter: brightness(1.04);
175
+ box-shadow: 0 18px 32px rgba(50, 141, 255, 0.48);
176
+ }
177
+
178
+ button:active {
179
+ transform: translateY(0);
180
+ }
181
+
182
+ button.secondary {
183
+ background: linear-gradient(135deg, rgba(95, 185, 255, 0.9), rgba(154, 102, 255, 0.86));
184
+ }
185
+
186
+ button:disabled {
187
+ opacity: 0.56;
188
+ cursor: not-allowed;
189
+ filter: grayscale(0.2);
190
+ box-shadow: none;
191
+ transform: none;
192
+ }
193
+
194
+ .layout {
195
+ margin-top: 16px;
196
+ display: grid;
197
+ gap: 14px;
198
+ grid-template-columns: 1.12fr 0.88fr;
199
+ align-items: start;
200
+ }
201
+
202
+ .panel {
203
+ border-radius: 20px;
204
+ padding: clamp(14px, 1.8vw, 20px);
205
+ position: relative;
206
+ }
207
+
208
+ .panel::after {
209
+ content: "";
210
+ position: absolute;
211
+ inset: 0;
212
+ border-radius: inherit;
213
+ border: 1px solid var(--line);
214
+ pointer-events: none;
215
+ }
216
+
217
+ .panel-wide {
218
+ grid-column: 1 / -1;
219
+ }
220
+
221
+ .panel h2 {
222
+ margin: 0 0 12px;
223
+ font-size: 1rem;
224
+ font-weight: 700;
225
+ letter-spacing: 0.05em;
226
+ text-transform: uppercase;
227
+ }
228
+
229
+ .kpi-grid {
230
+ display: grid;
231
+ gap: 10px;
232
+ grid-template-columns: repeat(3, minmax(0, 1fr));
233
+ }
234
+
235
+ .kpi-grid div {
236
+ border-radius: 13px;
237
+ border: 1px solid var(--border);
238
+ background: var(--panel-solid);
239
+ padding: 11px 12px;
240
+ }
241
+
242
+ .kpi-grid span {
243
+ display: block;
244
+ margin-bottom: 4px;
245
+ font-size: 0.72rem;
246
+ color: var(--muted);
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.05em;
249
+ }
250
+
251
+ .kpi-grid strong {
252
+ font-size: 1.06rem;
253
+ line-height: 1.2;
254
+ }
255
+
256
+ .action-row,
257
+ .stack {
258
+ display: grid;
259
+ gap: 10px;
260
+ margin-bottom: 12px;
261
+ }
262
+
263
+ .action-row label {
264
+ color: var(--muted);
265
+ font-size: 0.78rem;
266
+ letter-spacing: 0.05em;
267
+ text-transform: uppercase;
268
+ }
269
+
270
+ .stack-two {
271
+ grid-template-columns: repeat(2, minmax(0, 1fr));
272
+ }
273
+
274
+ .med-grid {
275
+ display: grid;
276
+ grid-template-columns: repeat(3, minmax(0, 1fr));
277
+ gap: 10px;
278
+ max-height: 430px;
279
+ overflow: auto;
280
+ padding-right: 4px;
281
+ }
282
+
283
+ .med-card {
284
+ border-radius: 14px;
285
+ border: 1px solid var(--border);
286
+ background: var(--panel-solid);
287
+ padding: 11px 12px;
288
+ transition: transform 130ms ease, border-color 130ms ease;
289
+ }
290
+
291
+ .med-card:hover {
292
+ transform: translateY(-1px);
293
+ border-color: rgba(109, 224, 255, 0.72);
294
+ }
295
+
296
+ .med-card p {
297
+ margin: 6px 0 4px;
298
+ color: var(--muted);
299
+ text-transform: capitalize;
300
+ }
301
+
302
+ .med-card small {
303
+ color: #c7d9ff;
304
+ }
305
+
306
+ .logs {
307
+ max-height: 300px;
308
+ overflow: auto;
309
+ padding-right: 4px;
310
+ display: grid;
311
+ gap: 7px;
312
+ font-size: 0.84rem;
313
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
314
+ }
315
+
316
+ .logs div {
317
+ border-radius: 10px;
318
+ border: 1px solid var(--border);
319
+ background: rgba(10, 16, 31, 0.84);
320
+ padding: 8px 10px;
321
+ color: #dbebff;
322
+ }
323
+
324
+ .muted {
325
+ margin: 0;
326
+ color: var(--muted);
327
+ }
328
+
329
+ .budget-note {
330
+ margin-top: 10px;
331
+ border: 1px solid var(--border);
332
+ border-radius: 12px;
333
+ padding: 10px 12px;
334
+ background: rgba(13, 22, 42, 0.82);
335
+ }
336
+
337
+ @media (max-width: 1180px) {
338
+ .layout {
339
+ grid-template-columns: 1fr;
340
+ }
341
+
342
+ .topbar {
343
+ grid-template-columns: 1fr;
344
+ }
345
+
346
+ .status-chip {
347
+ justify-self: start;
348
+ }
349
+
350
+ .actions {
351
+ justify-content: flex-start;
352
+ }
353
+ }
354
+
355
+ @media (max-width: 760px) {
356
+ .shell {
357
+ padding: 14px 10px 24px;
358
+ }
359
+
360
+ .topbar,
361
+ .panel {
362
+ border-radius: 16px;
363
+ }
364
+
365
+ .actions {
366
+ width: 100%;
367
+ }
368
+
369
+ .actions button,
370
+ .actions select {
371
+ width: 100%;
372
+ }
373
+
374
+ .kpi-grid,
375
+ .med-grid,
376
+ .stack-two {
377
+ grid-template-columns: 1fr;
378
+ }
379
+
380
+ .logs {
381
+ max-height: 240px;
382
+ }
383
+ }
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: