Spaces:
Sleeping
Sleeping
Commit ·
2043afa
1
Parent(s): eba6899
Set up repository for Hugging Face Spaces deployment.
Browse filesAdd the project files and strengthen .gitignore rules so Python, Node, build artifacts, and env files are excluded from version control.
Made-with: Cursor
This view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +30 -2
- openenv-polypharmacy/.dockerignore +8 -0
- openenv-polypharmacy/Dockerfile +39 -0
- openenv-polypharmacy/PROMPT.md +571 -0
- openenv-polypharmacy/README.md +245 -0
- openenv-polypharmacy/backend/Dockerfile +28 -0
- openenv-polypharmacy/backend/__init__.py +1 -0
- openenv-polypharmacy/backend/main.py +15 -0
- openenv-polypharmacy/backend/requirements.txt +9 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py +11 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py +1 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py +63 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py +1 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py +35 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py +6 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py +1 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py +197 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py +53 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/client.py +49 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/config.py +79 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py +142 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py +115 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py +416 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/graders.py +98 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/models.py +111 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py +92 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py +1 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py +246 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py +47 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py +1 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py +146 -0
- openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py +192 -0
- openenv-polypharmacy/data/lookups/beers_criteria.csv +16 -0
- openenv-polypharmacy/data/lookups/ddi_rules.csv +25 -0
- openenv-polypharmacy/data/lookups/drug_metadata.csv +34 -0
- openenv-polypharmacy/data/processed/patients_polypharmacy.csv +121 -0
- openenv-polypharmacy/docker-compose.yml +35 -0
- openenv-polypharmacy/frontend/Dockerfile +12 -0
- openenv-polypharmacy/frontend/index.html +12 -0
- openenv-polypharmacy/frontend/package-lock.json +1677 -0
- openenv-polypharmacy/frontend/package.json +19 -0
- openenv-polypharmacy/frontend/src/App.jsx +371 -0
- openenv-polypharmacy/frontend/src/main.jsx +10 -0
- openenv-polypharmacy/frontend/src/styles.css +304 -0
- openenv-polypharmacy/frontend/vite.config.js +10 -0
- openenv-polypharmacy/inference.py +210 -0
- openenv-polypharmacy/openenv.yaml +30 -0
- openenv-polypharmacy/pyproject.toml +41 -0
- openenv-polypharmacy/requirements.txt +1 -0
- openenv-polypharmacy/scripts/dev_backend.sh +4 -0
.gitignore
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
|
|
| 1 |
venv/
|
|
|
|
|
|
|
| 2 |
.env
|
|
|
|
|
|
|
| 3 |
__pycache__/
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
frontend/dist/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Python ---
|
| 2 |
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
env/
|
| 5 |
.env
|
| 6 |
+
.env.*
|
| 7 |
+
*.py[cod]
|
| 8 |
__pycache__/
|
| 9 |
+
.pytest_cache/
|
| 10 |
+
.mypy_cache/
|
| 11 |
+
.ruff_cache/
|
| 12 |
+
.coverage
|
| 13 |
+
coverage.xml
|
| 14 |
+
|
| 15 |
+
# --- Node / frontend ---
|
| 16 |
+
node_modules/
|
| 17 |
+
**/node_modules/
|
| 18 |
frontend/dist/
|
| 19 |
+
**/dist/
|
| 20 |
+
npm-debug.log*
|
| 21 |
+
yarn-debug.log*
|
| 22 |
+
yarn-error.log*
|
| 23 |
+
pnpm-debug.log*
|
| 24 |
+
|
| 25 |
+
# --- Build / temp ---
|
| 26 |
+
*.log
|
| 27 |
+
*.tmp
|
| 28 |
+
*.swp
|
| 29 |
+
.DS_Store
|
| 30 |
+
|
| 31 |
+
# --- Project-specific nested paths ---
|
| 32 |
+
openenv-polypharmacy/frontend/node_modules/
|
| 33 |
+
openenv-polypharmacy/frontend/dist/
|
| 34 |
+
openenv-polypharmacy/.pytest_cache/
|
openenv-polypharmacy/.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.gitignore
|
| 3 |
+
**/__pycache__/
|
| 4 |
+
**/.pytest_cache/
|
| 5 |
+
**/.DS_Store
|
| 6 |
+
.env
|
| 7 |
+
frontend/node_modules
|
| 8 |
+
frontend/dist
|
openenv-polypharmacy/Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS frontend-builder
|
| 2 |
+
WORKDIR /app/frontend
|
| 3 |
+
COPY frontend/package*.json ./
|
| 4 |
+
RUN npm ci
|
| 5 |
+
COPY frontend/ ./
|
| 6 |
+
RUN npm run build
|
| 7 |
+
|
| 8 |
+
FROM python:3.11-slim
|
| 9 |
+
|
| 10 |
+
RUN apt-get update && \
|
| 11 |
+
apt-get install -y --no-install-recommends build-essential curl && \
|
| 12 |
+
rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
COPY backend/requirements.txt /app/backend/requirements.txt
|
| 17 |
+
RUN pip install --no-cache-dir -r /app/backend/requirements.txt
|
| 18 |
+
|
| 19 |
+
COPY backend /app/backend
|
| 20 |
+
COPY data /app/data
|
| 21 |
+
COPY scripts /app/scripts
|
| 22 |
+
COPY openenv.yaml /app/openenv.yaml
|
| 23 |
+
COPY .env.example /app/.env.example
|
| 24 |
+
COPY inference.py /app/inference.py
|
| 25 |
+
|
| 26 |
+
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
| 27 |
+
|
| 28 |
+
RUN python3 /app/scripts/preprocess_data.py
|
| 29 |
+
|
| 30 |
+
ENV PORT=7860
|
| 31 |
+
ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
|
| 32 |
+
ENV PYTHONUNBUFFERED=1
|
| 33 |
+
|
| 34 |
+
EXPOSE 7860
|
| 35 |
+
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
|
| 37 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 38 |
+
|
| 39 |
+
CMD ["sh", "-c", "uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
|
openenv-polypharmacy/PROMPT.md
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are an expert Python backend, ML, and infrastructure engineer.
|
| 2 |
+
Your task is to implement a complete, production-ready OpenEnv environment called **PolypharmacyEnv** for training and evaluating agentic RL policies that act as an "elderly polypharmacy safety agent" (clinical pharmacist assistant).
|
| 3 |
+
|
| 4 |
+
The deliverable MUST satisfy all of the following:
|
| 5 |
+
- Fully compliant with the OpenEnv spec (typed models, `step()` / `reset()` / `state()`, `openenv.yaml`, HTTP server, Dockerfile).
|
| 6 |
+
- Simulates a realistic healthcare workflow around elderly polypharmacy and dangerous drug combinations.
|
| 7 |
+
- Defines at least **3 tasks** (easy → medium → hard) with deterministic agent graders producing scores in (0.0, 1.0).
|
| 8 |
+
- Provides shaped rewards over the trajectory (not just sparse terminal rewards).
|
| 9 |
+
- Includes a baseline LLM-based inference script `inference.py` in the repo root, following the evaluation requirements:
|
| 10 |
+
- Uses the OpenAI Python client.
|
| 11 |
+
- Reads `OPENAI_API_KEY`, `API_BASE_URL`, `MODEL_NAME`, and `HF_TOKEN` from the environment.
|
| 12 |
+
- Emits structured stdout logs in the exact `[START]`, `[STEP]`, `[END]` format from the OpenEnv sample inference script.
|
| 13 |
+
- Is containerized and deployable as a **Hugging Face Space** tagged with `openenv` that responds to OpenEnv-style `reset` / `step` / `state` HTTP calls.
|
| 14 |
+
|
| 15 |
+
Implement everything described below.
|
| 16 |
+
|
| 17 |
+
=================================================
|
| 18 |
+
1. Repository and folder structure
|
| 19 |
+
=================================================
|
| 20 |
+
|
| 21 |
+
Create a Python package repository with this structure (names are important unless clearly labeled as examples):
|
| 22 |
+
|
| 23 |
+
- `openenv-polypharmacy/`
|
| 24 |
+
- `openenv.yaml`
|
| 25 |
+
- `README.md`
|
| 26 |
+
- `requirements.txt`
|
| 27 |
+
- `Dockerfile`
|
| 28 |
+
- `inference.py` # baseline LLM agent per spec
|
| 29 |
+
- `pyproject.toml` or `setup.cfg` (optional but recommended)
|
| 30 |
+
- `src/`
|
| 31 |
+
- `polypharmacy_env/`
|
| 32 |
+
- `__init__.py`
|
| 33 |
+
- `config.py`
|
| 34 |
+
- `models.py` # Action, Observation, State, helper models
|
| 35 |
+
- `env_core.py` # PolypharmacyEnv implementation
|
| 36 |
+
- `tasks.py` # task setup utilities
|
| 37 |
+
- `graders.py` # deterministic graders for each task
|
| 38 |
+
- `rewards.py` # reward shaping logic
|
| 39 |
+
- `data_loader.py` # load/preprocess patient and lookup data
|
| 40 |
+
- `ddi_simulator.py` # local DDI / guideline simulator
|
| 41 |
+
- `api/`
|
| 42 |
+
- `__init__.py`
|
| 43 |
+
- `schemas.py` # HTTP request/response schemas
|
| 44 |
+
- `server.py` # FastAPI app exposing OpenEnv endpoints
|
| 45 |
+
- `baselines/`
|
| 46 |
+
- `__init__.py`
|
| 47 |
+
- `heuristic_agent.py` # simple rule-based baseline agent
|
| 48 |
+
- `random_agent.py` # trivial random baseline (optional)
|
| 49 |
+
- `tests/`
|
| 50 |
+
- `__init__.py`
|
| 51 |
+
- `test_env_core.py`
|
| 52 |
+
- `test_api.py`
|
| 53 |
+
- `data/`
|
| 54 |
+
- `raw/` # placeholder for real/synthetic source data
|
| 55 |
+
- `processed/`
|
| 56 |
+
- `lookups/`
|
| 57 |
+
- `ddi_rules.csv`
|
| 58 |
+
- `beers_criteria.csv`
|
| 59 |
+
- `drug_metadata.csv`
|
| 60 |
+
- `scripts/`
|
| 61 |
+
- `preprocess_data.py`
|
| 62 |
+
- `run_validation.sh` # optional; runs OpenEnv validator, tests, etc.
|
| 63 |
+
|
| 64 |
+
Use Python 3.10+ with full type hints, and keep the code black/isort-compatible.
|
| 65 |
+
|
| 66 |
+
=================================================
|
| 67 |
+
2. Domain, data, and clinical abstraction
|
| 68 |
+
=================================================
|
| 69 |
+
|
| 70 |
+
2.1. Core scenario
|
| 71 |
+
|
| 72 |
+
Model an elderly patient (age ≥ 65) with:
|
| 73 |
+
- Demographics: age, sex.
|
| 74 |
+
- Comorbidities: e.g., hypertension, diabetes, heart failure, CKD, dementia.
|
| 75 |
+
- Basic labs: kidney function (eGFR category), liver function category.
|
| 76 |
+
- A current medication list (polypharmacy, e.g., 3–15 drugs depending on task).
|
| 77 |
+
|
| 78 |
+
Each **episode** is one medication-review session where the agent:
|
| 79 |
+
- Observes patient info and current meds.
|
| 80 |
+
- Optionally **queries** a DDI/guideline tool for specific drug pairs.
|
| 81 |
+
- Proposes **interventions**:
|
| 82 |
+
- `stop`: discontinue a drug.
|
| 83 |
+
- `dose_reduce`: lower dose of a drug.
|
| 84 |
+
- `substitute`: swap to a safer alternative.
|
| 85 |
+
- `add_monitoring`: keep the drug but flag extra monitoring.
|
| 86 |
+
- Calls `finish_review` when it decides the regimen is acceptable or budgets are exhausted.
|
| 87 |
+
|
| 88 |
+
No external PHI, EHRs, or online APIs: all data is **synthetic** or de-identified and local to the container (CSV files).
|
| 89 |
+
|
| 90 |
+
2.2. Data files and CSV schemas
|
| 91 |
+
|
| 92 |
+
Implement local CSVs under `data/lookups/`:
|
| 93 |
+
|
| 94 |
+
**`drug_metadata.csv`**
|
| 95 |
+
- `drug_id` (string; unique key)
|
| 96 |
+
- `generic_name` (string)
|
| 97 |
+
- `atc_class` (string)
|
| 98 |
+
- `is_high_risk_elderly` (0/1)
|
| 99 |
+
- `default_dose_mg` (float)
|
| 100 |
+
- `min_dose_mg` (float)
|
| 101 |
+
- `max_dose_mg` (float)
|
| 102 |
+
|
| 103 |
+
**`beers_criteria.csv`**
|
| 104 |
+
- `drug_id` (string)
|
| 105 |
+
- `criterion_type` (enum string: `avoid`, `caution`, `dose_adjust`, `avoid_in_condition`)
|
| 106 |
+
- `condition` (nullable string; e.g., `CKD`, `dementia`)
|
| 107 |
+
- `rationale` (brief text)
|
| 108 |
+
|
| 109 |
+
**`ddi_rules.csv`**
|
| 110 |
+
- `drug_id_1` (string; normalized so `drug_id_1 < drug_id_2` lexicographically)
|
| 111 |
+
- `drug_id_2` (string)
|
| 112 |
+
- `severity` (enum string: `mild`, `moderate`, `severe`)
|
| 113 |
+
- `mechanism` (short text)
|
| 114 |
+
- `recommendation` (enum string: `avoid_combination`, `monitor_closely`, `dose_adjust`, `no_action`)
|
| 115 |
+
- `base_risk_score` (float in [0.0, 1.0])
|
| 116 |
+
|
| 117 |
+
Implement a synthetic patient-episode dataset under `data/processed/`:
|
| 118 |
+
|
| 119 |
+
**`patients_polypharmacy.csv`**
|
| 120 |
+
- `episode_id` (string)
|
| 121 |
+
- `age` (int)
|
| 122 |
+
- `sex` (enum: `M`, `F`, `O`)
|
| 123 |
+
- `conditions` (semicolon-separated; e.g., `HTN;DM;CKD`)
|
| 124 |
+
- `eGFR_category` (enum: `normal`, `mild`, `moderate`, `severe`)
|
| 125 |
+
- `liver_function_category` (enum: `normal`, `impaired`)
|
| 126 |
+
- `medication_ids` (semicolon-separated list of `drug_id`)
|
| 127 |
+
- `baseline_risk_score` (float in [0.0, 1.0])
|
| 128 |
+
|
| 129 |
+
2.3. Preprocessing script
|
| 130 |
+
|
| 131 |
+
In `scripts/preprocess_data.py`:
|
| 132 |
+
- If real data is not provided, procedurally generate synthetic but plausible data using:
|
| 133 |
+
- Random combinations of conditions and drugs constrained by simple rules (e.g., CKD + renally-cleared drugs).
|
| 134 |
+
- Controlled distribution of high-risk DDIs and Beers violations.
|
| 135 |
+
- Explicitly tag episodes as easy/medium/hard (e.g., via number of drugs, number/severity of DDIs, and number of Beers issues).
|
| 136 |
+
- Save `patients_polypharmacy.csv` ready for the environment to consume.
|
| 137 |
+
|
| 138 |
+
=================================================
|
| 139 |
+
3. OpenEnv models and environment implementation
|
| 140 |
+
=================================================
|
| 141 |
+
|
| 142 |
+
3.1. Models
|
| 143 |
+
|
| 144 |
+
In `models.py`, define dataclasses or Pydantic models that extend the appropriate OpenEnv base types (`Action`, `Observation`, `State`) and are JSON-compatible.
|
| 145 |
+
|
| 146 |
+
Auxiliary models:
|
| 147 |
+
|
| 148 |
+
**`MedicationEntry`**
|
| 149 |
+
- `drug_id: str`
|
| 150 |
+
- `generic_name: str`
|
| 151 |
+
- `atc_class: str`
|
| 152 |
+
- `dose_mg: float`
|
| 153 |
+
- `frequency: str` # e.g., `qd`, `bid`
|
| 154 |
+
- `route: str` # e.g., `po`
|
| 155 |
+
- `is_high_risk_elderly: bool`
|
| 156 |
+
- `beers_flags: list[str]` # e.g., `["avoid", "dose_adjust_CKD"]`
|
| 157 |
+
|
| 158 |
+
**`InteractionQueryRecord`**
|
| 159 |
+
- `drug_id_1: str`
|
| 160 |
+
- `drug_id_2: str`
|
| 161 |
+
- `severity: str | None`
|
| 162 |
+
- `recommendation: str | None`
|
| 163 |
+
- `risk_score: float | None`
|
| 164 |
+
- `step_index: int`
|
| 165 |
+
|
| 166 |
+
**`InterventionRecord`**
|
| 167 |
+
- `target_drug_id: str`
|
| 168 |
+
- `action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]`
|
| 169 |
+
- `proposed_new_drug_id: str | None`
|
| 170 |
+
- `rationale: str`
|
| 171 |
+
- `step_index: int`
|
| 172 |
+
|
| 173 |
+
Core wire models:
|
| 174 |
+
|
| 175 |
+
**`PolypharmacyObservation`** (extends OpenEnv `Observation`)
|
| 176 |
+
- `episode_id: str`
|
| 177 |
+
- `task_id: Literal["easy_screening", "budgeted_screening", "complex_tradeoff"]`
|
| 178 |
+
- `age: int`
|
| 179 |
+
- `sex: str`
|
| 180 |
+
- `conditions: list[str]`
|
| 181 |
+
- `eGFR_category: str`
|
| 182 |
+
- `liver_function_category: str`
|
| 183 |
+
- `current_medications: list[MedicationEntry]`
|
| 184 |
+
- `interaction_queries: list[InteractionQueryRecord]`
|
| 185 |
+
- `interventions: list[InterventionRecord]`
|
| 186 |
+
- `step_index: int`
|
| 187 |
+
- `remaining_query_budget: int`
|
| 188 |
+
- `remaining_intervention_budget: int`
|
| 189 |
+
- `shaped_reward: float` # reward from last step
|
| 190 |
+
- `done: bool`
|
| 191 |
+
|
| 192 |
+
**`PolypharmacyAction`** (extends OpenEnv `Action`)
|
| 193 |
+
- `action_type: Literal["query_ddi", "propose_intervention", "finish_review"]`
|
| 194 |
+
- `drug_id_1: str | None` # for DDI queries or some interventions
|
| 195 |
+
- `drug_id_2: str | None` # for DDI queries
|
| 196 |
+
- `target_drug_id: str | None` # for interventions
|
| 197 |
+
- `intervention_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"] | None`
|
| 198 |
+
- `proposed_new_drug_id: str | None`
|
| 199 |
+
- `rationale: str | None`
|
| 200 |
+
|
| 201 |
+
**`PolypharmacyState`** (extends OpenEnv `State`)
|
| 202 |
+
- `episode_id: str`
|
| 203 |
+
- `task_id: str`
|
| 204 |
+
- `step_count: int`
|
| 205 |
+
- `max_steps: int`
|
| 206 |
+
- `num_query_actions: int`
|
| 207 |
+
- `num_interventions: int`
|
| 208 |
+
|
| 209 |
+
3.2. Environment core
|
| 210 |
+
|
| 211 |
+
In `env_core.py`, implement `PolypharmacyEnv` extending the appropriate OpenEnv environment base class. It must implement:
|
| 212 |
+
|
| 213 |
+
**`reset(task_id: str | None = None) -> PolypharmacyObservation`**
|
| 214 |
+
- If `task_id` is `None`, default to medium (`budgeted_screening`).
|
| 215 |
+
- Sample an episode from `patients_polypharmacy.csv` filtered by difficulty.
|
| 216 |
+
- Initialize:
|
| 217 |
+
- `episode_id`
|
| 218 |
+
- `step_count = 0`
|
| 219 |
+
- task-specific budgets (query, interventions, max_steps)
|
| 220 |
+
- baseline regime and risk
|
| 221 |
+
- empty `interaction_queries` and `interventions`
|
| 222 |
+
- Return the initial `PolypharmacyObservation` with:
|
| 223 |
+
- `step_index = 0`
|
| 224 |
+
- `shaped_reward = 0.0`
|
| 225 |
+
- `done = False`
|
| 226 |
+
|
| 227 |
+
**`step(action: PolypharmacyAction) -> dict`**
|
| 228 |
+
- Validate the action; if invalid:
|
| 229 |
+
- Apply a negative reward.
|
| 230 |
+
- Do not modify regimen, but log error in `info`.
|
| 231 |
+
- If `action_type == "query_ddi"`:
|
| 232 |
+
- If query budget exhausted, apply penalty and do not query.
|
| 233 |
+
- Else:
|
| 234 |
+
- Use `ddi_simulator.lookup_ddi(drug_id_1, drug_id_2)` to get severity, recommendation, base_risk_score.
|
| 235 |
+
- Append an `InteractionQueryRecord`.
|
| 236 |
+
- Apply a small negative reward for query cost.
|
| 237 |
+
- If `action_type == "propose_intervention"`:
|
| 238 |
+
- If intervention budget exhausted, apply penalty and ignore change.
|
| 239 |
+
- Else:
|
| 240 |
+
- Update `current_medications` according to `intervention_type`:
|
| 241 |
+
- `stop`: remove medication.
|
| 242 |
+
- `dose_reduce`: adjust dose downward within [min_dose_mg, default_dose_mg].
|
| 243 |
+
- `substitute`: replace with a safer alternative from same `atc_class`.
|
| 244 |
+
- `add_monitoring`: keep drug but tag in internal state.
|
| 245 |
+
- Append an `InterventionRecord`.
|
| 246 |
+
- Recompute current regimen risk using the risk model (see 3.3).
|
| 247 |
+
- Compute shaped reward = (previous_risk - new_risk) - small intervention cost.
|
| 248 |
+
- If `action_type == "finish_review"`:
|
| 249 |
+
- Mark `done = True`.
|
| 250 |
+
- Call the task’s grader to get episode-level score in [0.0, 1.0].
|
| 251 |
+
- Add this as a terminal bonus to the current step reward.
|
| 252 |
+
|
| 253 |
+
- In all cases:
|
| 254 |
+
- Increment `step_count`.
|
| 255 |
+
- Check `max_steps`; if exceeded, auto-terminate:
|
| 256 |
+
- `done = True`
|
| 257 |
+
- apply time-out penalty
|
| 258 |
+
- call grader with current trajectory for a final score if appropriate.
|
| 259 |
+
- Construct next `PolypharmacyObservation` with updated fields.
|
| 260 |
+
- Return a dict:
|
| 261 |
+
- `observation`: `PolypharmacyObservation`
|
| 262 |
+
- `reward`: float shaped reward for this step
|
| 263 |
+
- `done`: bool
|
| 264 |
+
- `info`: dict with fields like `current_risk`, `baseline_risk`, `grader_score_if_terminal`, and debug flags.
|
| 265 |
+
|
| 266 |
+
**`state` property**
|
| 267 |
+
- Returns `PolypharmacyState` reflecting the current internal state.
|
| 268 |
+
|
| 269 |
+
3.3. DDI simulator and risk model
|
| 270 |
+
|
| 271 |
+
In `ddi_simulator.py`:
|
| 272 |
+
- Load `ddi_rules.csv` once via `data_loader`.
|
| 273 |
+
- Implement `lookup_ddi(drug_id_1, drug_id_2) -> tuple[severity, recommendation, base_risk_score]`:
|
| 274 |
+
- Normalize the pair ordering.
|
| 275 |
+
- Look up row; if missing, return:
|
| 276 |
+
- severity = `"none"`
|
| 277 |
+
- recommendation = `"no_action"`
|
| 278 |
+
- base_risk_score = 0.0
|
| 279 |
+
|
| 280 |
+
In `rewards.py` (or a dedicated module), implement:
|
| 281 |
+
- `compute_regimen_risk(current_drug_ids, patient_context, ddi_rules, beers_rules, drug_metadata) -> float`
|
| 282 |
+
- Aggregate contributions from:
|
| 283 |
+
- Beers violations (weighted by `criterion_type` and relevant conditions).
|
| 284 |
+
- DDI base risk scores for all present drug pairs.
|
| 285 |
+
- High-risk elderly drugs.
|
| 286 |
+
- Normalize and clip to [0.0, 1.0].
|
| 287 |
+
|
| 288 |
+
Use this function to compute:
|
| 289 |
+
- `baseline_risk` at episode start.
|
| 290 |
+
- Risk after each intervention step.
|
| 291 |
+
|
| 292 |
+
Also implement:
|
| 293 |
+
- `compute_shaped_reward(previous_risk, new_risk, action, context, partial_metrics) -> float`
|
| 294 |
+
- Positive component: `previous_risk - new_risk`.
|
| 295 |
+
- Negative components: per-query cost, per-intervention cost, invalid-action penalty, time-out penalty.
|
| 296 |
+
|
| 297 |
+
=================================================
|
| 298 |
+
4. Tasks and graders (3 difficulty levels)
|
| 299 |
+
=================================================
|
| 300 |
+
|
| 301 |
+
Define three task IDs and semantics in `tasks.py` and `graders.py`:
|
| 302 |
+
|
| 303 |
+
Task IDs:
|
| 304 |
+
- `easy_screening`
|
| 305 |
+
- `budgeted_screening`
|
| 306 |
+
- `complex_tradeoff`
|
| 307 |
+
|
| 308 |
+
4.1. `easy_screening` (easy)
|
| 309 |
+
|
| 310 |
+
- Small regimen: 3–5 drugs.
|
| 311 |
+
- Exactly one **severe** DDI pair and possibly one simple Beers violation.
|
| 312 |
+
- Budgets:
|
| 313 |
+
- query_budget ≈ 4
|
| 314 |
+
- intervention_budget ≈ 2
|
| 315 |
+
- max_steps ≈ 10
|
| 316 |
+
|
| 317 |
+
Grader:
|
| 318 |
+
- Input: full trajectory, baseline risk, final risk, list of interventions.
|
| 319 |
+
- Compute:
|
| 320 |
+
- `risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, ε)` (normalized).
|
| 321 |
+
- `targeted_intervention_flag = 1.0` if at least one intervention affects one of the drugs in the known severe DDI pair, else 0.0.
|
| 322 |
+
- Score:
|
| 323 |
+
- `score = 0.5 * risk_reduction + 0.5 * targeted_intervention_flag`
|
| 324 |
+
- Clip to [0.0, 1.0].
|
| 325 |
+
|
| 326 |
+
4.2. `budgeted_screening` (medium)
|
| 327 |
+
|
| 328 |
+
- Medium regimen: 6–10 drugs.
|
| 329 |
+
- Multiple DDIs (mild/moderate/severe) and multiple Beers issues.
|
| 330 |
+
- Budgets:
|
| 331 |
+
- query_budget ≈ 8
|
| 332 |
+
- intervention_budget ≈ 3
|
| 333 |
+
- max_steps ≈ 20
|
| 334 |
+
|
| 335 |
+
Grader:
|
| 336 |
+
- Compute:
|
| 337 |
+
- `risk_reduction_score` as normalized risk drop.
|
| 338 |
+
- `intervention_precision_score` = fraction of interventions that actually reduce risk or fix guideline violations.
|
| 339 |
+
- `query_efficiency_score` = (number of severe/moderate DDIs discovered) / (number of queries used), normalized.
|
| 340 |
+
- Weighted score, for example:
|
| 341 |
+
- `score = 0.5 * risk_reduction_score + 0.3 * intervention_precision_score + 0.2 * query_efficiency_score`
|
| 342 |
+
- Clip to [0.0, 1.0].
|
| 343 |
+
|
| 344 |
+
4.3. `complex_tradeoff` (hard)
|
| 345 |
+
|
| 346 |
+
- Larger regimen: 10–15 drugs.
|
| 347 |
+
- Some drugs are **clinically critical** (e.g., anticoagulants, insulin analogues) and encoded as such in `drug_metadata` or a small internal map.
|
| 348 |
+
- Episodes contain:
|
| 349 |
+
- multiple DDIs and Beers issues, including ones involving critical drugs.
|
| 350 |
+
- safer substitutes for some risky drugs.
|
| 351 |
+
|
| 352 |
+
Budgets:
|
| 353 |
+
- query_budget ≈ 12
|
| 354 |
+
- intervention_budget ≈ 5
|
| 355 |
+
- max_steps ≈ 30
|
| 356 |
+
|
| 357 |
+
Grader adds a **regimen disruption penalty** component:
|
| 358 |
+
- Metrics:
|
| 359 |
+
- `risk_reduction_score` (as above).
|
| 360 |
+
- `critical_drug_penalty` = penalty if a critical drug is stopped without substitution to another suitable agent.
|
| 361 |
+
- `total_drug_changes` = number of drugs stopped or substituted.
|
| 362 |
+
- `regimen_disruption_penalty` derived from `total_drug_changes` and `critical_drug_penalty`.
|
| 363 |
+
|
| 364 |
+
Example scoring:
|
| 365 |
+
- `base = risk_reduction_score`
|
| 366 |
+
- `penalty = α * regimen_disruption_penalty`
|
| 367 |
+
- `score = clamp(base - penalty, 0.0, 1.0)`
|
| 368 |
+
|
| 369 |
+
4.4. Reward shaping
|
| 370 |
+
|
| 371 |
+
In `rewards.py`, define a consistent shaping scheme:
|
| 372 |
+
- On each query:
|
| 373 |
+
- Small negative reward (e.g., −0.01) plus any small bonus if it discovers a severe DDI, if desired.
|
| 374 |
+
- On each intervention:
|
| 375 |
+
- Reward ≈ (previous_risk - new_risk) − small intervention cost.
|
| 376 |
+
- On invalid actions:
|
| 377 |
+
- Larger negative reward (e.g., −0.1) and no state change.
|
| 378 |
+
- On `finish_review`:
|
| 379 |
+
- Add the task-level `score` ∈ [0.0, 1.0] from the corresponding grader to that step’s shaped reward.
|
| 380 |
+
|
| 381 |
+
Ensure the sum of step rewards per episode remains in a reasonable numeric range (e.g., roughly -5 to +5) while still allowing meaningful differentiation by graders.
|
| 382 |
+
|
| 383 |
+
=================================================
|
| 384 |
+
5. HTTP API server and openenv.yaml
|
| 385 |
+
=================================================
|
| 386 |
+
|
| 387 |
+
5.1. HTTP server (FastAPI)
|
| 388 |
+
|
| 389 |
+
In `api/server.py`:
|
| 390 |
+
- Implement a FastAPI app that maintains a `PolypharmacyEnv` instance (or a multiplexing scheme if needed).
|
| 391 |
+
- Endpoints:
|
| 392 |
+
- `POST /reset`:
|
| 393 |
+
- Request body: may include `task_id` (string).
|
| 394 |
+
- Response: serialized `PolypharmacyObservation`.
|
| 395 |
+
- `POST /step`:
|
| 396 |
+
- Request body: serialized `PolypharmacyAction`.
|
| 397 |
+
- Response: dict with:
|
| 398 |
+
- `observation`: `PolypharmacyObservation`
|
| 399 |
+
- `reward`: float
|
| 400 |
+
- `done`: bool
|
| 401 |
+
- `info`: dict
|
| 402 |
+
- `GET /state`:
|
| 403 |
+
- Response: `PolypharmacyState`.
|
| 404 |
+
|
| 405 |
+
Provide a module-level `app = FastAPI(...)` object for use with uvicorn and Hugging Face Spaces. Ensure the JSON schema is consistent with OpenEnv clients (simple, flat JSON for observation/action/state).
|
| 406 |
+
|
| 407 |
+
5.2. `openenv.yaml`
|
| 408 |
+
|
| 409 |
+
At repo root, define `openenv.yaml` consistent with the latest OpenEnv spec. At minimum, include:
|
| 410 |
+
- `name`: `polypharmacy_env`
|
| 411 |
+
- `version`: e.g., `0.1.0`
|
| 412 |
+
- `description`: human-readable description.
|
| 413 |
+
- `author`: your details.
|
| 414 |
+
- `tags`: e.g., `["healthcare", "polypharmacy", "openenv"]`
|
| 415 |
+
- `tasks`:
|
| 416 |
+
- One entry per task:
|
| 417 |
+
- `id`: `"easy_screening"` / `"budgeted_screening"` / `"complex_tradeoff"`
|
| 418 |
+
- `description`: one-line description
|
| 419 |
+
- `difficulty`: `"easy"`, `"medium"`, `"hard"`
|
| 420 |
+
|
| 421 |
+
Ensure `openenv validate` (or equivalent validator) passes once implemented.
|
| 422 |
+
|
| 423 |
+
=================================================
|
| 424 |
+
6. Baseline heuristic (non-LLM) agent
|
| 425 |
+
=================================================
|
| 426 |
+
|
| 427 |
+
In `baselines/heuristic_agent.py`, implement a simple, deterministic baseline agent that:
|
| 428 |
+
|
| 429 |
+
For each episode:
|
| 430 |
+
- Iterates through all unordered medication pairs within query budget:
|
| 431 |
+
- Calls `query_ddi` via the environment for each pair until the query budget is exhausted or all pairs are examined.
|
| 432 |
+
- Records severe and moderate interactions.
|
| 433 |
+
- After querying:
|
| 434 |
+
- For each severe DDI pair:
|
| 435 |
+
- Try `substitute` one of the drugs using `drug_metadata`:
|
| 436 |
+
- Prefer substitute within same `atc_class` that:
|
| 437 |
+
- is not marked high-risk elderly.
|
| 438 |
+
- does not participate in known severe DDIs with the rest of the regimen.
|
| 439 |
+
- If no substitute exists, propose `stop` for the higher-risk drug.
|
| 440 |
+
- Respect intervention budget limits.
|
| 441 |
+
- Finally, call `finish_review`.
|
| 442 |
+
|
| 443 |
+
This baseline should be callable as a simple Python function that interacts with `PolypharmacyEnv` directly (without HTTP).
|
| 444 |
+
|
| 445 |
+
=================================================
|
| 446 |
+
7. Baseline LLM inference script (inference.py)
|
| 447 |
+
=================================================
|
| 448 |
+
|
| 449 |
+
At repo root, create `inference.py` that:
|
| 450 |
+
|
| 451 |
+
7.1. Uses the OpenAI Python client
|
| 452 |
+
|
| 453 |
+
- Import and configure the official OpenAI Python client.
|
| 454 |
+
- Read environment variables:
|
| 455 |
+
- `OPENAI_API_KEY` (required).
|
| 456 |
+
- `API_BASE_URL` (base URL for LLM; default to OpenAI standard if not set).
|
| 457 |
+
- `MODEL_NAME` (e.g., `gpt-4.1` or similar).
|
| 458 |
+
- `HF_TOKEN` (if needed for HF auth; do not hardcode).
|
| 459 |
+
- Read `POLYPHARMACY_ENV_URL` (or similar) for the environment’s HTTP base URL.
|
| 460 |
+
|
| 461 |
+
7.2. Implements the required logging format
|
| 462 |
+
|
| 463 |
+
- For each **run** across all tasks:
|
| 464 |
+
- Emit a `[START]` line with a JSON payload exactly matching the evaluation specification:
|
| 465 |
+
- Fields such as `run_id`, `task_id`, `model`, etc., in the same order and naming as the sample OpenEnv inference script.
|
| 466 |
+
- For each **step** in an episode:
|
| 467 |
+
- Emit a `[STEP]` line with JSON fields including:
|
| 468 |
+
- `run_id`
|
| 469 |
+
- `task_id`
|
| 470 |
+
- `episode_id`
|
| 471 |
+
- `step_index`
|
| 472 |
+
- `observation_summary` (brief, machine-readable summary)
|
| 473 |
+
- `action_payload` (the action sent to the env)
|
| 474 |
+
- `reward`
|
| 475 |
+
- `done`
|
| 476 |
+
- After finishing an episode for a task:
|
| 477 |
+
- Emit an `[END]` line summarizing:
|
| 478 |
+
- `run_id`
|
| 479 |
+
- `task_id`
|
| 480 |
+
- per-episode statistics (e.g., total reward, grader score from last step’s `info`).
|
| 481 |
+
- The stdout format MUST follow the sample exactly:
|
| 482 |
+
- Same tags: `[START]`, `[STEP]`, `[END]`.
|
| 483 |
+
- Same JSON field names and ordering as the provided reference.
|
| 484 |
+
- No extra prints except these structured logs (and necessary error messages to stderr).
|
| 485 |
+
|
| 486 |
+
7.3. LLM agent loop
|
| 487 |
+
|
| 488 |
+
- For each task (`easy_screening`, `budgeted_screening`, `complex_tradeoff`):
|
| 489 |
+
- Run a fixed small number of episodes (e.g., 5–10 per task) for baseline scoring.
|
| 490 |
+
- For each episode:
|
| 491 |
+
- Call `/reset` with the task id.
|
| 492 |
+
- At each step:
|
| 493 |
+
- Summarize the observation into a concise prompt for the LLM:
|
| 494 |
+
- Include age, sex, conditions, high-risk flags, budgets, and a compressed view of meds and previous actions.
|
| 495 |
+
- Ask the model to output a **strict JSON** representing `PolypharmacyAction` fields.
|
| 496 |
+
- Parse and validate the JSON; if invalid, fall back to a safe default (e.g., `finish_review` or a no-op) and penalize in evaluation.
|
| 497 |
+
- Send this action to `/step` and log `[STEP]`.
|
| 498 |
+
- End when `done=True` or max_steps is reached.
|
| 499 |
+
- At the end, print aggregate scores per task and overall.
|
| 500 |
+
|
| 501 |
+
Make sure runtime < 20 minutes and that the script can run within 2 vCPUs and 8 GB RAM.
|
| 502 |
+
|
| 503 |
+
=================================================
|
| 504 |
+
8. Dockerfile and Hugging Face Space
|
| 505 |
+
=================================================
|
| 506 |
+
|
| 507 |
+
8.1. Dockerfile
|
| 508 |
+
|
| 509 |
+
Create a `Dockerfile` that:
|
| 510 |
+
- Starts from a slim Python image (e.g., `python:3.11-slim`).
|
| 511 |
+
- Installs system dependencies as needed (e.g., `build-essential`, `curl`).
|
| 512 |
+
- Copies the project into the container.
|
| 513 |
+
- Installs Python dependencies from `requirements.txt`.
|
| 514 |
+
- Sets appropriate environment variables for the app (e.g., `PORT=7860`).
|
| 515 |
+
- Exposes port 7860.
|
| 516 |
+
- Uses a `CMD` or `ENTRYPOINT` that runs the FastAPI server, for example:
|
| 517 |
+
- `uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860`
|
| 518 |
+
|
| 519 |
+
8.2. Hugging Face Space
|
| 520 |
+
|
| 521 |
+
Ensure the repository is ready to be used as a Hugging Face Space:
|
| 522 |
+
- Space type: `docker`.
|
| 523 |
+
- Tag: `openenv`.
|
| 524 |
+
- On container start, the server must listen on the correct port and respond to:
|
| 525 |
+
- `POST /reset`
|
| 526 |
+
- `POST /step`
|
| 527 |
+
- `GET /state`
|
| 528 |
+
- The environment must start cleanly with `docker build` + `docker run` locally.
|
| 529 |
+
|
| 530 |
+
=================================================
|
| 531 |
+
9. README and documentation
|
| 532 |
+
=================================================
|
| 533 |
+
|
| 534 |
+
In `README.md`, include:
|
| 535 |
+
|
| 536 |
+
- **Environment description & motivation**:
|
| 537 |
+
- What PolypharmacyEnv simulates.
|
| 538 |
+
- Why elderly polypharmacy safety matters.
|
| 539 |
+
- **Action and observation spaces**:
|
| 540 |
+
- Describe `PolypharmacyAction`, `PolypharmacyObservation`, and `PolypharmacyState` fields and semantics.
|
| 541 |
+
- **Task descriptions**:
|
| 542 |
+
- `easy_screening`, `budgeted_screening`, `complex_tradeoff`, their difficulty and goals.
|
| 543 |
+
- **Reward structure**:
|
| 544 |
+
- Summarize shaping and terminal rewards.
|
| 545 |
+
- **Setup & usage**:
|
| 546 |
+
- How to install dependencies.
|
| 547 |
+
- How to run the API server locally (uvicorn command).
|
| 548 |
+
- How to run the heuristic baseline.
|
| 549 |
+
- How to run `inference.py` with environment variables.
|
| 550 |
+
- **Baseline scores**:
|
| 551 |
+
- Document reproducible baseline scores for each task (heuristic agent, and LLM baseline if available).
|
| 552 |
+
|
| 553 |
+
=================================================
|
| 554 |
+
10. Validation and quality gates
|
| 555 |
+
=================================================
|
| 556 |
+
|
| 557 |
+
- Ensure:
|
| 558 |
+
- `openenv.yaml` and the HTTP server pass the OpenEnv validation script.
|
| 559 |
+
- `docker build` and `docker run` work without errors.
|
| 560 |
+
- `inference.py` completes under 20 minutes, within 2 vCPUs / 8 GB RAM.
|
| 561 |
+
- All graders:
|
| 562 |
+
- Are deterministic.
|
| 563 |
+
- Return scores strictly in [0.0, 1.0].
|
| 564 |
+
- No grader returns a constant score irrespective of behavior.
|
| 565 |
+
|
| 566 |
+
Aim for clean, well-structured, well-documented code with clear separation of concerns between:
|
| 567 |
+
- Data loading,
|
| 568 |
+
- Environment state & dynamics,
|
| 569 |
+
- Reward/grade logic,
|
| 570 |
+
- HTTP serving,
|
| 571 |
+
- Baseline agents and inference.
|
openenv-polypharmacy/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PolypharmacyEnv
|
| 2 |
+
|
| 3 |
+
Monorepo for an OpenEnv-compatible medication safety environment with:
|
| 4 |
+
|
| 5 |
+
- a FastAPI backend (`backend/`)
|
| 6 |
+
- a React frontend (`frontend/`)
|
| 7 |
+
- data assets (`data/`)
|
| 8 |
+
- utility scripts (`scripts/`)
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## Repository Structure
|
| 13 |
+
|
| 14 |
+
```text
|
| 15 |
+
openenv-polypharmacy/
|
| 16 |
+
backend/
|
| 17 |
+
main.py # ASGI entrypoint (uvicorn target)
|
| 18 |
+
requirements.txt # Backend dependencies
|
| 19 |
+
Dockerfile # Backend container
|
| 20 |
+
src/polypharmacy_env/ # Python package source
|
| 21 |
+
api/
|
| 22 |
+
app.py # FastAPI/OpenEnv app assembly
|
| 23 |
+
server.py # Compatibility import wrapper
|
| 24 |
+
routes/agent.py # /agent/suggest route
|
| 25 |
+
services/
|
| 26 |
+
groq_agent.py # Groq-based action suggestion logic
|
| 27 |
+
env_core.py # OpenEnv environment core
|
| 28 |
+
models.py # Action/observation/state models
|
| 29 |
+
data_loader.py # CSV loading
|
| 30 |
+
ddi_simulator.py # DDI and Beers lookups
|
| 31 |
+
rewards.py # Reward shaping
|
| 32 |
+
graders.py # Task graders
|
| 33 |
+
tasks.py # Task/episode selection
|
| 34 |
+
tests/ # Backend tests
|
| 35 |
+
frontend/
|
| 36 |
+
src/ # React UI code
|
| 37 |
+
package.json
|
| 38 |
+
Dockerfile # Frontend container
|
| 39 |
+
data/
|
| 40 |
+
lookups/ # drug_metadata.csv, ddi_rules.csv, beers_criteria.csv
|
| 41 |
+
processed/ # patients_polypharmacy.csv
|
| 42 |
+
scripts/
|
| 43 |
+
preprocess_data.py # Synthetic data generation
|
| 44 |
+
dev_backend.sh # Local backend run helper
|
| 45 |
+
dev_frontend.sh # Local frontend run helper
|
| 46 |
+
run_validation.sh # Tests + baseline validation
|
| 47 |
+
docker-compose.yml # Full stack orchestration
|
| 48 |
+
openenv.yaml # OpenEnv manifest
|
| 49 |
+
inference.py # Optional CLI inference baseline
|
| 50 |
+
.env.example # Environment template
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## What It Does
|
| 56 |
+
|
| 57 |
+
The environment simulates elderly polypharmacy review. Agent actions:
|
| 58 |
+
|
| 59 |
+
- `query_ddi`
|
| 60 |
+
- `propose_intervention`
|
| 61 |
+
- `finish_review`
|
| 62 |
+
|
| 63 |
+
Supported tasks:
|
| 64 |
+
|
| 65 |
+
- `easy_screening`
|
| 66 |
+
- `budgeted_screening`
|
| 67 |
+
- `complex_tradeoff`
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## Prerequisites
|
| 72 |
+
|
| 73 |
+
- Python 3.10+
|
| 74 |
+
- Node.js 18+ (or 20+ recommended)
|
| 75 |
+
- npm
|
| 76 |
+
- Docker + Docker Compose (optional, for containerized run)
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## Environment Setup
|
| 81 |
+
|
| 82 |
+
Create `.env`:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
cp .env.example .env
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
Set values:
|
| 89 |
+
|
| 90 |
+
- `GROQ_API_KEY=...` (required)
|
| 91 |
+
- `GROQ_BASE_URL=https://api.groq.com/openai/v1` (recommended)
|
| 92 |
+
- `GROQ_MODEL_NAME=llama-3.3-70b-versatile` (recommended)
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## Local Run (Recommended During Development)
|
| 97 |
+
|
| 98 |
+
### 1) Install dependencies
|
| 99 |
+
|
| 100 |
+
Backend:
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
pip install -r backend/requirements.txt
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
Frontend:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
cd frontend
|
| 110 |
+
npm install
|
| 111 |
+
cd ..
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### 2) Generate/update synthetic data (if needed)
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
python scripts/preprocess_data.py
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
### 3) Start services in two terminals
|
| 121 |
+
|
| 122 |
+
Terminal A:
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
./scripts/dev_backend.sh
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
Terminal B:
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
./scripts/dev_frontend.sh
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 4) Open app
|
| 135 |
+
|
| 136 |
+
- Frontend: [http://localhost:5173](http://localhost:5173)
|
| 137 |
+
- Backend health: [http://localhost:7860/health](http://localhost:7860/health)
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## Docker Run
|
| 142 |
+
|
| 143 |
+
Run both services:
|
| 144 |
+
|
| 145 |
+
```bash
|
| 146 |
+
docker compose up --build
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
Stop:
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
docker compose down
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
Ports:
|
| 156 |
+
|
| 157 |
+
- backend: `7860`
|
| 158 |
+
- frontend: `5173`
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Hugging Face Spaces Deployment (Docker)
|
| 163 |
+
|
| 164 |
+
This repo now includes a **root `Dockerfile`** that builds frontend + backend into one container, so Spaces can host both API and UI together.
|
| 165 |
+
|
| 166 |
+
### 1) Create a new Space
|
| 167 |
+
|
| 168 |
+
- Go to [Hugging Face Spaces](https://huggingface.co/new-space)
|
| 169 |
+
- Choose **Docker** SDK
|
| 170 |
+
- Create the Space
|
| 171 |
+
|
| 172 |
+
### 2) Add Space secrets/variables
|
| 173 |
+
|
| 174 |
+
In Space Settings -> Variables and Secrets:
|
| 175 |
+
|
| 176 |
+
- Secret: `GROQ_API_KEY`
|
| 177 |
+
- Variable: `GROQ_BASE_URL=https://api.groq.com/openai/v1`
|
| 178 |
+
- Variable: `GROQ_MODEL_NAME=llama-3.3-70b-versatile`
|
| 179 |
+
|
| 180 |
+
### 3) Push this repository to the Space
|
| 181 |
+
|
| 182 |
+
Commit and push all files, including root `Dockerfile`.
|
| 183 |
+
|
| 184 |
+
### 4) Verify after build
|
| 185 |
+
|
| 186 |
+
- Space root URL loads the React UI
|
| 187 |
+
- `/health` returns healthy status
|
| 188 |
+
- OpenEnv endpoints are available (`/reset`, `/step`, `/state`, `/schema`)
|
| 189 |
+
|
| 190 |
+
Notes:
|
| 191 |
+
|
| 192 |
+
- Container reads `PORT` (defaults to `7860`) which is Space-friendly.
|
| 193 |
+
- Frontend static assets are served by FastAPI from `frontend/dist`.
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## API Endpoints
|
| 198 |
+
|
| 199 |
+
OpenEnv/health:
|
| 200 |
+
|
| 201 |
+
- `POST /reset`
|
| 202 |
+
- `POST /step`
|
| 203 |
+
- `GET /state`
|
| 204 |
+
- `GET /health`
|
| 205 |
+
- `GET /schema`
|
| 206 |
+
- `WS /ws` (stateful session)
|
| 207 |
+
|
| 208 |
+
AI helper:
|
| 209 |
+
|
| 210 |
+
- `POST /agent/suggest`
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## Testing
|
| 215 |
+
|
| 216 |
+
Run backend tests:
|
| 217 |
+
|
| 218 |
+
```bash
|
| 219 |
+
python -m pytest backend/src/polypharmacy_env/tests -v
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
Or run validation script:
|
| 223 |
+
|
| 224 |
+
```bash
|
| 225 |
+
./scripts/run_validation.sh
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## Notes
|
| 231 |
+
|
| 232 |
+
- OpenEnv HTTP reset/step is stateless; multi-step episode continuity should use websocket (`/ws`).
|
| 233 |
+
- The frontend uses websocket for episode continuity and HTTP for AI suggestion.
|
| 234 |
+
- AI behavior includes rule-based guardrails to avoid repetitive low-value loops.
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## Troubleshooting
|
| 239 |
+
|
| 240 |
+
- `ModuleNotFoundError: polypharmacy_env`
|
| 241 |
+
- Start backend using `./scripts/dev_backend.sh` from repo root.
|
| 242 |
+
- `/agent/suggest` fails
|
| 243 |
+
- Check `.env` keys and restart backend.
|
| 244 |
+
- UI state looks stale
|
| 245 |
+
- Hard refresh browser and click `Reset Episode`.
|
openenv-polypharmacy/backend/Dockerfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && \
|
| 4 |
+
apt-get install -y --no-install-recommends build-essential curl && \
|
| 5 |
+
rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY backend/requirements.txt /app/backend/requirements.txt
|
| 10 |
+
RUN pip install --no-cache-dir -r /app/backend/requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY backend /app/backend
|
| 13 |
+
COPY data /app/data
|
| 14 |
+
COPY scripts /app/scripts
|
| 15 |
+
COPY .env.example /app/.env.example
|
| 16 |
+
|
| 17 |
+
RUN python3 /app/scripts/preprocess_data.py
|
| 18 |
+
|
| 19 |
+
ENV PORT=7860
|
| 20 |
+
ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
|
| 21 |
+
ENV PYTHONUNBUFFERED=1
|
| 22 |
+
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
| 26 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 27 |
+
|
| 28 |
+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
openenv-polypharmacy/backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Backend entrypoint package for monorepo structure."""
|
openenv-polypharmacy/backend/main.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ASGI entrypoint for backend service in monorepo layout."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
BACKEND_DIR = Path(__file__).resolve().parent
|
| 9 |
+
SRC = BACKEND_DIR / "src"
|
| 10 |
+
if str(SRC) not in sys.path:
|
| 11 |
+
sys.path.insert(0, str(SRC))
|
| 12 |
+
|
| 13 |
+
from polypharmacy_env.api.app import app # noqa: E402
|
| 14 |
+
|
| 15 |
+
__all__ = ["app"]
|
openenv-polypharmacy/backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.104.0
|
| 2 |
+
uvicorn>=0.24.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
requests>=2.31.0
|
| 5 |
+
httpx>=0.25.0
|
| 6 |
+
openenv-core>=0.2.0
|
| 7 |
+
openai>=1.0.0
|
| 8 |
+
python-dotenv>=1.0.0
|
| 9 |
+
pytest>=7.0.0
|
openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PolypharmacyEnv – an OpenEnv environment for elderly polypharmacy safety."""
|
| 2 |
+
|
| 3 |
+
from .client import PolypharmacyClient
|
| 4 |
+
from .models import PolypharmacyAction, PolypharmacyObservation, PolypharmacyState
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"PolypharmacyClient",
|
| 8 |
+
"PolypharmacyAction",
|
| 9 |
+
"PolypharmacyObservation",
|
| 10 |
+
"PolypharmacyState",
|
| 11 |
+
]
|
openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API package."""
|
openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI app factory for PolypharmacyEnv."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from fastapi.staticfiles import StaticFiles
|
| 11 |
+
from openenv.core.env_server.http_server import create_app
|
| 12 |
+
from starlette.responses import FileResponse
|
| 13 |
+
|
| 14 |
+
from ..env_core import PolypharmacyEnv
|
| 15 |
+
from ..models import PolypharmacyAction, PolypharmacyObservation
|
| 16 |
+
from .routes.agent import router as agent_router
|
| 17 |
+
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class SPAStaticFiles(StaticFiles):
|
| 22 |
+
"""Serve SPA index for unknown frontend routes."""
|
| 23 |
+
|
| 24 |
+
async def get_response(self, path: str, scope):
|
| 25 |
+
response = await super().get_response(path, scope)
|
| 26 |
+
if response.status_code != 404:
|
| 27 |
+
return response
|
| 28 |
+
index_path = Path(self.directory) / "index.html"
|
| 29 |
+
if index_path.exists():
|
| 30 |
+
return FileResponse(index_path)
|
| 31 |
+
raise HTTPException(status_code=404, detail="Not Found")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def create_polypharmacy_app():
|
| 35 |
+
app = create_app(
|
| 36 |
+
PolypharmacyEnv,
|
| 37 |
+
PolypharmacyAction,
|
| 38 |
+
PolypharmacyObservation,
|
| 39 |
+
env_name="polypharmacy_env",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
app.add_middleware(
|
| 43 |
+
CORSMiddleware,
|
| 44 |
+
allow_origins=[
|
| 45 |
+
"http://localhost:5173",
|
| 46 |
+
"http://127.0.0.1:5173",
|
| 47 |
+
],
|
| 48 |
+
allow_credentials=True,
|
| 49 |
+
allow_methods=["*"],
|
| 50 |
+
allow_headers=["*"],
|
| 51 |
+
)
|
| 52 |
+
app.include_router(agent_router)
|
| 53 |
+
|
| 54 |
+
# In Docker Space deployment, serve built frontend from same container.
|
| 55 |
+
project_root = Path(__file__).resolve().parents[4]
|
| 56 |
+
frontend_dist = project_root / "frontend" / "dist"
|
| 57 |
+
if frontend_dist.exists():
|
| 58 |
+
app.mount("/", SPAStaticFiles(directory=frontend_dist, html=True), name="frontend")
|
| 59 |
+
|
| 60 |
+
return app
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
app = create_polypharmacy_app()
|
openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API route modules."""
|
openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agent suggestion API routes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, HTTPException
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
|
| 8 |
+
from ...models import PolypharmacyAction, PolypharmacyObservation
|
| 9 |
+
from ...services.groq_agent import suggest_action_from_observation
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/agent", tags=["agent"])
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AgentSuggestRequest(BaseModel):
|
| 15 |
+
observation: PolypharmacyObservation
|
| 16 |
+
model_name: str | None = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AgentSuggestResponse(BaseModel):
|
| 20 |
+
action: PolypharmacyAction
|
| 21 |
+
source: str = Field(default="groq")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@router.post("/suggest", response_model=AgentSuggestResponse)
|
| 25 |
+
def suggest_agent_action(payload: AgentSuggestRequest) -> AgentSuggestResponse:
|
| 26 |
+
"""Return a model-suggested action for the current observation."""
|
| 27 |
+
try:
|
| 28 |
+
action = suggest_action_from_observation(
|
| 29 |
+
payload.observation, model_name=payload.model_name
|
| 30 |
+
)
|
| 31 |
+
except ValueError as exc:
|
| 32 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 33 |
+
except Exception as exc:
|
| 34 |
+
raise HTTPException(status_code=500, detail=f"Model call failed: {exc}") from exc
|
| 35 |
+
return AgentSuggestResponse(action=action)
|
openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backward-compatible app import path.
|
| 2 |
+
|
| 3 |
+
Use `polypharmacy_env.api.app:app` for the main app module.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from .app import app
|
openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Baseline agents."""
|
openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic heuristic baseline agent for PolypharmacyEnv.
|
| 2 |
+
|
| 3 |
+
Strategy:
|
| 4 |
+
1. Query all unordered medication pairs for DDIs (within budget),
|
| 5 |
+
prioritising high-risk elderly drugs first.
|
| 6 |
+
2. For each severe DDI found, attempt substitution or stop.
|
| 7 |
+
3. For each moderate DDI found, attempt substitution or stop.
|
| 8 |
+
4. For remaining budget, address Beers-flagged "avoid" drugs.
|
| 9 |
+
5. Call finish_review.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from itertools import combinations
|
| 15 |
+
from typing import List, Tuple
|
| 16 |
+
|
| 17 |
+
from ..env_core import PolypharmacyEnv
|
| 18 |
+
from ..models import PolypharmacyAction, PolypharmacyObservation
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def run_heuristic_episode(
|
| 22 |
+
env: PolypharmacyEnv,
|
| 23 |
+
task_id: str = "budgeted_screening",
|
| 24 |
+
seed: int | None = None,
|
| 25 |
+
) -> Tuple[float, float, int]:
|
| 26 |
+
"""Run one episode with the heuristic agent.
|
| 27 |
+
|
| 28 |
+
Returns (total_reward, grader_score, steps).
|
| 29 |
+
"""
|
| 30 |
+
obs = env.reset(seed=seed, task_id=task_id)
|
| 31 |
+
total_reward = 0.0
|
| 32 |
+
grader_score = 0.0
|
| 33 |
+
steps = 0
|
| 34 |
+
|
| 35 |
+
# Phase 1: Query DDIs between medication pairs, prioritising high-risk drugs
|
| 36 |
+
meds = obs.current_medications
|
| 37 |
+
# Sort: high-risk elderly drugs first, then by Beers flag count
|
| 38 |
+
meds_sorted = sorted(
|
| 39 |
+
meds,
|
| 40 |
+
key=lambda m: (not m.is_high_risk_elderly, -len(m.beers_flags), m.drug_id),
|
| 41 |
+
)
|
| 42 |
+
med_ids = [m.drug_id for m in meds_sorted]
|
| 43 |
+
pairs: List[Tuple[str, str]] = list(combinations(med_ids, 2))
|
| 44 |
+
severe_pairs: List[Tuple[str, str]] = []
|
| 45 |
+
moderate_pairs: List[Tuple[str, str]] = []
|
| 46 |
+
|
| 47 |
+
for a, b in pairs:
|
| 48 |
+
if obs.remaining_query_budget <= 0:
|
| 49 |
+
break
|
| 50 |
+
action = PolypharmacyAction(
|
| 51 |
+
action_type="query_ddi",
|
| 52 |
+
drug_id_1=a,
|
| 53 |
+
drug_id_2=b,
|
| 54 |
+
)
|
| 55 |
+
obs = env.step(action)
|
| 56 |
+
reward = obs.reward or 0.0
|
| 57 |
+
total_reward += reward
|
| 58 |
+
steps += 1
|
| 59 |
+
|
| 60 |
+
if obs.done:
|
| 61 |
+
grader_score = obs.metadata.get("grader_score", 0.0)
|
| 62 |
+
return total_reward, grader_score, steps
|
| 63 |
+
|
| 64 |
+
# Track severity from metadata
|
| 65 |
+
ddi_info = obs.metadata.get("ddi_result", {})
|
| 66 |
+
sev = ddi_info.get("severity", "none")
|
| 67 |
+
if sev == "severe":
|
| 68 |
+
severe_pairs.append((a, b))
|
| 69 |
+
elif sev == "moderate":
|
| 70 |
+
moderate_pairs.append((a, b))
|
| 71 |
+
|
| 72 |
+
# Phase 2: Intervene on severe DDI drugs first
|
| 73 |
+
intervened: set[str] = set()
|
| 74 |
+
|
| 75 |
+
def _try_intervene(
|
| 76 |
+
target: str,
|
| 77 |
+
rationale: str,
|
| 78 |
+
) -> Tuple[bool, PolypharmacyObservation]:
|
| 79 |
+
"""Try substitute then stop. Returns (done, obs)."""
|
| 80 |
+
nonlocal total_reward, steps
|
| 81 |
+
# Try substitute first
|
| 82 |
+
act = PolypharmacyAction(
|
| 83 |
+
action_type="propose_intervention",
|
| 84 |
+
target_drug_id=target,
|
| 85 |
+
intervention_type="substitute",
|
| 86 |
+
rationale=rationale,
|
| 87 |
+
)
|
| 88 |
+
obs_new = env.step(act)
|
| 89 |
+
total_reward += obs_new.reward or 0.0
|
| 90 |
+
steps += 1
|
| 91 |
+
|
| 92 |
+
if obs_new.done:
|
| 93 |
+
return True, obs_new
|
| 94 |
+
|
| 95 |
+
# If substitute failed, try stop
|
| 96 |
+
if obs_new.metadata.get("warning"):
|
| 97 |
+
if obs_new.remaining_intervention_budget <= 0:
|
| 98 |
+
return False, obs_new
|
| 99 |
+
act2 = PolypharmacyAction(
|
| 100 |
+
action_type="propose_intervention",
|
| 101 |
+
target_drug_id=target,
|
| 102 |
+
intervention_type="stop",
|
| 103 |
+
rationale=f"No substitute; {rationale}",
|
| 104 |
+
)
|
| 105 |
+
obs_new = env.step(act2)
|
| 106 |
+
total_reward += obs_new.reward or 0.0
|
| 107 |
+
steps += 1
|
| 108 |
+
if obs_new.done:
|
| 109 |
+
return True, obs_new
|
| 110 |
+
|
| 111 |
+
return False, obs_new
|
| 112 |
+
|
| 113 |
+
# Intervene on severe pairs
|
| 114 |
+
for a, b in severe_pairs:
|
| 115 |
+
if obs.remaining_intervention_budget <= 0:
|
| 116 |
+
break
|
| 117 |
+
target = b if a in intervened else a
|
| 118 |
+
if target in intervened:
|
| 119 |
+
target = b
|
| 120 |
+
if target in intervened:
|
| 121 |
+
continue
|
| 122 |
+
intervened.add(target)
|
| 123 |
+
|
| 124 |
+
done, obs = _try_intervene(target, f"Severe DDI between {a} and {b}")
|
| 125 |
+
if done:
|
| 126 |
+
grader_score = obs.metadata.get("grader_score", 0.0)
|
| 127 |
+
return total_reward, grader_score, steps
|
| 128 |
+
|
| 129 |
+
# Phase 2b: Intervene on moderate DDI drugs
|
| 130 |
+
for a, b in moderate_pairs:
|
| 131 |
+
if obs.remaining_intervention_budget <= 0:
|
| 132 |
+
break
|
| 133 |
+
target = b if a in intervened else a
|
| 134 |
+
if target in intervened:
|
| 135 |
+
target = b
|
| 136 |
+
if target in intervened:
|
| 137 |
+
continue
|
| 138 |
+
intervened.add(target)
|
| 139 |
+
|
| 140 |
+
done, obs = _try_intervene(target, f"Moderate DDI between {a} and {b}")
|
| 141 |
+
if done:
|
| 142 |
+
grader_score = obs.metadata.get("grader_score", 0.0)
|
| 143 |
+
return total_reward, grader_score, steps
|
| 144 |
+
|
| 145 |
+
# Phase 3: Address Beers-flagged "avoid" drugs
|
| 146 |
+
for med in meds_sorted:
|
| 147 |
+
if obs.remaining_intervention_budget <= 0:
|
| 148 |
+
break
|
| 149 |
+
if med.drug_id in intervened:
|
| 150 |
+
continue
|
| 151 |
+
if not med.beers_flags:
|
| 152 |
+
continue
|
| 153 |
+
if any("avoid" in f for f in med.beers_flags):
|
| 154 |
+
intervened.add(med.drug_id)
|
| 155 |
+
done, obs = _try_intervene(
|
| 156 |
+
med.drug_id, f"Beers criteria: {', '.join(med.beers_flags)}"
|
| 157 |
+
)
|
| 158 |
+
if done:
|
| 159 |
+
grader_score = obs.metadata.get("grader_score", 0.0)
|
| 160 |
+
return total_reward, grader_score, steps
|
| 161 |
+
|
| 162 |
+
# Phase 4: Finish
|
| 163 |
+
action = PolypharmacyAction(action_type="finish_review")
|
| 164 |
+
obs = env.step(action)
|
| 165 |
+
total_reward += obs.reward or 0.0
|
| 166 |
+
steps += 1
|
| 167 |
+
grader_score = obs.metadata.get("grader_score", 0.0)
|
| 168 |
+
|
| 169 |
+
return total_reward, grader_score, steps
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def run_heuristic_baseline(
|
| 173 |
+
n_episodes: int = 5,
|
| 174 |
+
task_ids: List[str] | None = None,
|
| 175 |
+
) -> None:
|
| 176 |
+
"""Run the heuristic agent across tasks and print results."""
|
| 177 |
+
if task_ids is None:
|
| 178 |
+
task_ids = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
|
| 179 |
+
|
| 180 |
+
env = PolypharmacyEnv()
|
| 181 |
+
|
| 182 |
+
for tid in task_ids:
|
| 183 |
+
scores: list[float] = []
|
| 184 |
+
rewards: list[float] = []
|
| 185 |
+
for i in range(n_episodes):
|
| 186 |
+
total_r, score, steps = run_heuristic_episode(env, task_id=tid, seed=i)
|
| 187 |
+
scores.append(score)
|
| 188 |
+
rewards.append(total_r)
|
| 189 |
+
print(f" [{tid}] ep={i} steps={steps} reward={total_r:.4f} score={score:.4f}")
|
| 190 |
+
|
| 191 |
+
avg_s = sum(scores) / len(scores) if scores else 0.0
|
| 192 |
+
avg_r = sum(rewards) / len(rewards) if rewards else 0.0
|
| 193 |
+
print(f" [{tid}] avg_score={avg_s:.4f} avg_reward={avg_r:.4f}\n")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
run_heuristic_baseline()
|
openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trivial random baseline agent for PolypharmacyEnv."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import random
|
| 6 |
+
from typing import List, Tuple
|
| 7 |
+
|
| 8 |
+
from ..env_core import PolypharmacyEnv
|
| 9 |
+
from ..models import PolypharmacyAction, PolypharmacyObservation
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def run_random_episode(
|
| 13 |
+
env: PolypharmacyEnv,
|
| 14 |
+
task_id: str = "budgeted_screening",
|
| 15 |
+
seed: int | None = None,
|
| 16 |
+
) -> Tuple[float, float, int]:
|
| 17 |
+
rng = random.Random(seed)
|
| 18 |
+
obs = env.reset(task_id=task_id, seed=seed)
|
| 19 |
+
total_reward = 0.0
|
| 20 |
+
grader_score = 0.0
|
| 21 |
+
steps = 0
|
| 22 |
+
|
| 23 |
+
while not obs.done:
|
| 24 |
+
med_ids = [m.drug_id for m in obs.current_medications]
|
| 25 |
+
choice = rng.choice(["query_ddi", "propose_intervention", "finish_review"])
|
| 26 |
+
|
| 27 |
+
if choice == "query_ddi" and len(med_ids) >= 2 and obs.remaining_query_budget > 0:
|
| 28 |
+
pair = rng.sample(med_ids, 2)
|
| 29 |
+
action = PolypharmacyAction(
|
| 30 |
+
action_type="query_ddi",
|
| 31 |
+
drug_id_1=pair[0],
|
| 32 |
+
drug_id_2=pair[1],
|
| 33 |
+
)
|
| 34 |
+
elif choice == "propose_intervention" and med_ids and obs.remaining_intervention_budget > 0:
|
| 35 |
+
target = rng.choice(med_ids)
|
| 36 |
+
itype = rng.choice(["stop", "dose_reduce", "substitute", "add_monitoring"])
|
| 37 |
+
action = PolypharmacyAction(
|
| 38 |
+
action_type="propose_intervention",
|
| 39 |
+
target_drug_id=target,
|
| 40 |
+
intervention_type=itype,
|
| 41 |
+
rationale="random",
|
| 42 |
+
)
|
| 43 |
+
else:
|
| 44 |
+
action = PolypharmacyAction(action_type="finish_review")
|
| 45 |
+
|
| 46 |
+
obs = env.step(action)
|
| 47 |
+
total_reward += obs.reward or 0.0
|
| 48 |
+
steps += 1
|
| 49 |
+
if obs.done:
|
| 50 |
+
grader_score = obs.metadata.get("grader_score", 0.0)
|
| 51 |
+
break
|
| 52 |
+
|
| 53 |
+
return total_reward, grader_score, steps
|
openenv-polypharmacy/backend/src/polypharmacy_env/client.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenEnv client for PolypharmacyEnv.
|
| 2 |
+
|
| 3 |
+
Provides a typed async/sync client for interacting with a PolypharmacyEnv
|
| 4 |
+
server via WebSocket, following the OpenEnv EnvClient pattern.
|
| 5 |
+
|
| 6 |
+
Example (async):
|
| 7 |
+
>>> async with PolypharmacyClient(base_url="ws://localhost:8000") as env:
|
| 8 |
+
... result = await env.reset(task_id="easy_screening")
|
| 9 |
+
... result = await env.step(PolypharmacyAction(action_type="finish_review"))
|
| 10 |
+
|
| 11 |
+
Example (sync):
|
| 12 |
+
>>> with PolypharmacyClient(base_url="ws://localhost:8000").sync() as env:
|
| 13 |
+
... result = env.reset(task_id="easy_screening")
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from typing import Any, Dict
|
| 19 |
+
|
| 20 |
+
from openenv.core.client_types import StepResult
|
| 21 |
+
from openenv.core.env_client import EnvClient
|
| 22 |
+
|
| 23 |
+
from .models import PolypharmacyAction, PolypharmacyObservation, PolypharmacyState
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class PolypharmacyClient(
|
| 27 |
+
EnvClient[PolypharmacyAction, PolypharmacyObservation, PolypharmacyState]
|
| 28 |
+
):
|
| 29 |
+
"""Typed OpenEnv client for the PolypharmacyEnv environment."""
|
| 30 |
+
|
| 31 |
+
def _step_payload(self, action: PolypharmacyAction) -> Dict[str, Any]:
|
| 32 |
+
"""Convert a PolypharmacyAction to the JSON payload for the server."""
|
| 33 |
+
return action.model_dump(exclude_none=True)
|
| 34 |
+
|
| 35 |
+
def _parse_result(
|
| 36 |
+
self, payload: Dict[str, Any]
|
| 37 |
+
) -> StepResult[PolypharmacyObservation]:
|
| 38 |
+
"""Parse a server response into a StepResult with typed observation."""
|
| 39 |
+
obs_data = payload.get("observation", payload)
|
| 40 |
+
obs = PolypharmacyObservation.model_validate(obs_data)
|
| 41 |
+
return StepResult(
|
| 42 |
+
observation=obs,
|
| 43 |
+
reward=payload.get("reward"),
|
| 44 |
+
done=payload.get("done", False),
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
def _parse_state(self, payload: Dict[str, Any]) -> PolypharmacyState:
|
| 48 |
+
"""Parse a server state response into a typed PolypharmacyState."""
|
| 49 |
+
return PolypharmacyState.model_validate(payload)
|
openenv-polypharmacy/backend/src/polypharmacy_env/config.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environment configuration constants and task parameter definitions."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Dict
|
| 8 |
+
|
| 9 |
+
# ── Paths ────────────────────────────────────────────────────────────────────
|
| 10 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[3] # openenv-polypharmacy/
|
| 11 |
+
DATA_DIR = PROJECT_ROOT / "data"
|
| 12 |
+
LOOKUPS_DIR = DATA_DIR / "lookups"
|
| 13 |
+
PROCESSED_DIR = DATA_DIR / "processed"
|
| 14 |
+
|
| 15 |
+
DDI_RULES_CSV = LOOKUPS_DIR / "ddi_rules.csv"
|
| 16 |
+
BEERS_CRITERIA_CSV = LOOKUPS_DIR / "beers_criteria.csv"
|
| 17 |
+
DRUG_METADATA_CSV = LOOKUPS_DIR / "drug_metadata.csv"
|
| 18 |
+
PATIENTS_CSV = PROCESSED_DIR / "patients_polypharmacy.csv"
|
| 19 |
+
|
| 20 |
+
# ── Reward hyper-parameters ──────────────────────────────────────────────────
|
| 21 |
+
QUERY_COST: float = 0.01
|
| 22 |
+
INTERVENTION_COST: float = 0.02
|
| 23 |
+
INVALID_ACTION_PENALTY: float = 0.10
|
| 24 |
+
TIMEOUT_PENALTY: float = 0.20
|
| 25 |
+
SEVERE_DDI_DISCOVERY_BONUS: float = 0.03
|
| 26 |
+
|
| 27 |
+
# ── Task parameters ─────────────────────────────────────────────────────────
|
| 28 |
+
|
| 29 |
+
@dataclass(frozen=True)
|
| 30 |
+
class TaskConfig:
|
| 31 |
+
task_id: str
|
| 32 |
+
difficulty: str
|
| 33 |
+
min_drugs: int
|
| 34 |
+
max_drugs: int
|
| 35 |
+
query_budget: int
|
| 36 |
+
intervention_budget: int
|
| 37 |
+
max_steps: int
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
TASK_CONFIGS: Dict[str, TaskConfig] = {
|
| 41 |
+
"easy_screening": TaskConfig(
|
| 42 |
+
task_id="easy_screening",
|
| 43 |
+
difficulty="easy",
|
| 44 |
+
min_drugs=3,
|
| 45 |
+
max_drugs=5,
|
| 46 |
+
query_budget=4,
|
| 47 |
+
intervention_budget=2,
|
| 48 |
+
max_steps=10,
|
| 49 |
+
),
|
| 50 |
+
"budgeted_screening": TaskConfig(
|
| 51 |
+
task_id="budgeted_screening",
|
| 52 |
+
difficulty="medium",
|
| 53 |
+
min_drugs=6,
|
| 54 |
+
max_drugs=10,
|
| 55 |
+
query_budget=8,
|
| 56 |
+
intervention_budget=3,
|
| 57 |
+
max_steps=20,
|
| 58 |
+
),
|
| 59 |
+
"complex_tradeoff": TaskConfig(
|
| 60 |
+
task_id="complex_tradeoff",
|
| 61 |
+
difficulty="hard",
|
| 62 |
+
min_drugs=10,
|
| 63 |
+
max_drugs=15,
|
| 64 |
+
query_budget=12,
|
| 65 |
+
intervention_budget=5,
|
| 66 |
+
max_steps=30,
|
| 67 |
+
),
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
DEFAULT_TASK = "budgeted_screening"
|
| 71 |
+
|
| 72 |
+
# ── Critical drugs (must not be stopped without substitution) ────────────────
|
| 73 |
+
CRITICAL_DRUG_IDS: set[str] = {
|
| 74 |
+
"DRUG_WARFARIN",
|
| 75 |
+
"DRUG_APIXABAN",
|
| 76 |
+
"DRUG_INSULIN_GLARGINE",
|
| 77 |
+
"DRUG_METOPROLOL",
|
| 78 |
+
"DRUG_DIGOXIN",
|
| 79 |
+
}
|
openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load and cache CSV lookup data for the PolypharmacyEnv."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import csv
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Dict, List, Optional, Tuple
|
| 10 |
+
|
| 11 |
+
from .config import (
|
| 12 |
+
BEERS_CRITERIA_CSV,
|
| 13 |
+
DDI_RULES_CSV,
|
| 14 |
+
DRUG_METADATA_CSV,
|
| 15 |
+
PATIENTS_CSV,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# ── Row-level data classes ───────────────────────────────────────────────────
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class DrugMeta:
|
| 23 |
+
drug_id: str
|
| 24 |
+
generic_name: str
|
| 25 |
+
atc_class: str
|
| 26 |
+
is_high_risk_elderly: bool
|
| 27 |
+
default_dose_mg: float
|
| 28 |
+
min_dose_mg: float
|
| 29 |
+
max_dose_mg: float
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True)
|
| 33 |
+
class DDIRule:
|
| 34 |
+
drug_id_1: str
|
| 35 |
+
drug_id_2: str
|
| 36 |
+
severity: str
|
| 37 |
+
mechanism: str
|
| 38 |
+
recommendation: str
|
| 39 |
+
base_risk_score: float
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass(frozen=True)
|
| 43 |
+
class BeersCriterion:
|
| 44 |
+
drug_id: str
|
| 45 |
+
criterion_type: str # avoid | caution | dose_adjust | avoid_in_condition
|
| 46 |
+
condition: Optional[str]
|
| 47 |
+
rationale: str
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class PatientEpisode:
|
| 52 |
+
episode_id: str
|
| 53 |
+
age: int
|
| 54 |
+
sex: str
|
| 55 |
+
conditions: List[str]
|
| 56 |
+
eGFR_category: str
|
| 57 |
+
liver_function_category: str
|
| 58 |
+
medication_ids: List[str]
|
| 59 |
+
baseline_risk_score: float
|
| 60 |
+
difficulty: str
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ── Loaders (cached) ────────────────────────────────────────────────────────
|
| 64 |
+
|
| 65 |
+
def _read_csv(path: Path) -> List[Dict[str, str]]:
|
| 66 |
+
with open(path, newline="") as f:
|
| 67 |
+
return list(csv.DictReader(f))
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@lru_cache(maxsize=1)
|
| 71 |
+
def load_drug_metadata(path: Path = DRUG_METADATA_CSV) -> Dict[str, DrugMeta]:
|
| 72 |
+
out: Dict[str, DrugMeta] = {}
|
| 73 |
+
for row in _read_csv(path):
|
| 74 |
+
dm = DrugMeta(
|
| 75 |
+
drug_id=row["drug_id"],
|
| 76 |
+
generic_name=row["generic_name"],
|
| 77 |
+
atc_class=row["atc_class"],
|
| 78 |
+
is_high_risk_elderly=row["is_high_risk_elderly"] == "1",
|
| 79 |
+
default_dose_mg=float(row["default_dose_mg"]),
|
| 80 |
+
min_dose_mg=float(row["min_dose_mg"]),
|
| 81 |
+
max_dose_mg=float(row["max_dose_mg"]),
|
| 82 |
+
)
|
| 83 |
+
out[dm.drug_id] = dm
|
| 84 |
+
return out
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _normalise_pair(a: str, b: str) -> Tuple[str, str]:
|
| 88 |
+
return (a, b) if a < b else (b, a)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@lru_cache(maxsize=1)
|
| 92 |
+
def load_ddi_rules(path: Path = DDI_RULES_CSV) -> Dict[Tuple[str, str], DDIRule]:
|
| 93 |
+
out: Dict[Tuple[str, str], DDIRule] = {}
|
| 94 |
+
for row in _read_csv(path):
|
| 95 |
+
key = _normalise_pair(row["drug_id_1"], row["drug_id_2"])
|
| 96 |
+
out[key] = DDIRule(
|
| 97 |
+
drug_id_1=key[0],
|
| 98 |
+
drug_id_2=key[1],
|
| 99 |
+
severity=row["severity"],
|
| 100 |
+
mechanism=row["mechanism"],
|
| 101 |
+
recommendation=row["recommendation"],
|
| 102 |
+
base_risk_score=float(row["base_risk_score"]),
|
| 103 |
+
)
|
| 104 |
+
return out
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@lru_cache(maxsize=1)
|
| 108 |
+
def load_beers_criteria(path: Path = BEERS_CRITERIA_CSV) -> List[BeersCriterion]:
|
| 109 |
+
out: List[BeersCriterion] = []
|
| 110 |
+
for row in _read_csv(path):
|
| 111 |
+
cond = row["condition"].strip() or None
|
| 112 |
+
out.append(BeersCriterion(
|
| 113 |
+
drug_id=row["drug_id"],
|
| 114 |
+
criterion_type=row["criterion_type"],
|
| 115 |
+
condition=cond,
|
| 116 |
+
rationale=row["rationale"],
|
| 117 |
+
))
|
| 118 |
+
return out
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def load_patients(
|
| 122 |
+
path: Path = PATIENTS_CSV,
|
| 123 |
+
difficulty: Optional[str] = None,
|
| 124 |
+
) -> List[PatientEpisode]:
|
| 125 |
+
rows = _read_csv(path)
|
| 126 |
+
eps: List[PatientEpisode] = []
|
| 127 |
+
for row in rows:
|
| 128 |
+
d = row.get("difficulty", "medium")
|
| 129 |
+
if difficulty and d != difficulty:
|
| 130 |
+
continue
|
| 131 |
+
eps.append(PatientEpisode(
|
| 132 |
+
episode_id=row["episode_id"],
|
| 133 |
+
age=int(row["age"]),
|
| 134 |
+
sex=row["sex"],
|
| 135 |
+
conditions=[c.strip() for c in row["conditions"].split(";") if c.strip()],
|
| 136 |
+
eGFR_category=row["eGFR_category"],
|
| 137 |
+
liver_function_category=row["liver_function_category"],
|
| 138 |
+
medication_ids=[m.strip() for m in row["medication_ids"].split(";") if m.strip()],
|
| 139 |
+
baseline_risk_score=float(row["baseline_risk_score"]),
|
| 140 |
+
difficulty=d,
|
| 141 |
+
))
|
| 142 |
+
return eps
|
openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Local DDI and guideline simulation using CSV lookup data."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Dict, List, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
from .data_loader import (
|
| 9 |
+
BeersCriterion,
|
| 10 |
+
DDIRule,
|
| 11 |
+
DrugMeta,
|
| 12 |
+
load_beers_criteria,
|
| 13 |
+
load_ddi_rules,
|
| 14 |
+
load_drug_metadata,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass(frozen=True)
|
| 19 |
+
class DDIResult:
|
| 20 |
+
severity: str
|
| 21 |
+
recommendation: str
|
| 22 |
+
base_risk_score: float
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
_NO_INTERACTION = DDIResult(severity="none", recommendation="no_action", base_risk_score=0.0)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class DDISimulator:
|
| 29 |
+
"""Provides drug–drug interaction and Beers-criteria lookups."""
|
| 30 |
+
|
| 31 |
+
def __init__(self) -> None:
|
| 32 |
+
self._ddi_rules: Dict[Tuple[str, str], DDIRule] = load_ddi_rules()
|
| 33 |
+
self._drug_meta: Dict[str, DrugMeta] = load_drug_metadata()
|
| 34 |
+
self._beers: List[BeersCriterion] = load_beers_criteria()
|
| 35 |
+
|
| 36 |
+
@staticmethod
|
| 37 |
+
def _normalise_pair(a: str, b: str) -> Tuple[str, str]:
|
| 38 |
+
return (a, b) if a < b else (b, a)
|
| 39 |
+
|
| 40 |
+
def lookup_ddi(self, drug_id_1: str, drug_id_2: str) -> DDIResult:
|
| 41 |
+
key = self._normalise_pair(drug_id_1, drug_id_2)
|
| 42 |
+
rule = self._ddi_rules.get(key)
|
| 43 |
+
if rule is None:
|
| 44 |
+
return _NO_INTERACTION
|
| 45 |
+
return DDIResult(
|
| 46 |
+
severity=rule.severity,
|
| 47 |
+
recommendation=rule.recommendation,
|
| 48 |
+
base_risk_score=rule.base_risk_score,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
def get_beers_flags(
|
| 52 |
+
self,
|
| 53 |
+
drug_id: str,
|
| 54 |
+
patient_conditions: List[str],
|
| 55 |
+
) -> List[str]:
|
| 56 |
+
"""Return list of Beers flags applicable to *drug_id* given patient conditions."""
|
| 57 |
+
flags: List[str] = []
|
| 58 |
+
for bc in self._beers:
|
| 59 |
+
if bc.drug_id != drug_id:
|
| 60 |
+
continue
|
| 61 |
+
if bc.condition is None:
|
| 62 |
+
flags.append(bc.criterion_type)
|
| 63 |
+
elif bc.condition in patient_conditions:
|
| 64 |
+
flags.append(f"{bc.criterion_type}_{bc.condition}")
|
| 65 |
+
return flags
|
| 66 |
+
|
| 67 |
+
def get_drug_meta(self, drug_id: str) -> Optional[DrugMeta]:
|
| 68 |
+
return self._drug_meta.get(drug_id)
|
| 69 |
+
|
| 70 |
+
def find_substitute(
|
| 71 |
+
self,
|
| 72 |
+
drug_id: str,
|
| 73 |
+
current_drug_ids: List[str],
|
| 74 |
+
) -> Optional[str]:
|
| 75 |
+
"""Find a safer same-class substitute not already in the regimen."""
|
| 76 |
+
meta = self._drug_meta.get(drug_id)
|
| 77 |
+
if meta is None:
|
| 78 |
+
return None
|
| 79 |
+
candidates = [
|
| 80 |
+
dm
|
| 81 |
+
for dm in self._drug_meta.values()
|
| 82 |
+
if (
|
| 83 |
+
dm.atc_class == meta.atc_class
|
| 84 |
+
and dm.drug_id != drug_id
|
| 85 |
+
and dm.drug_id not in current_drug_ids
|
| 86 |
+
and not dm.is_high_risk_elderly
|
| 87 |
+
)
|
| 88 |
+
]
|
| 89 |
+
if not candidates:
|
| 90 |
+
return None
|
| 91 |
+
# Pick the candidate with fewest severe DDIs with current regimen
|
| 92 |
+
def _severe_count(cand: DrugMeta) -> int:
|
| 93 |
+
count = 0
|
| 94 |
+
for did in current_drug_ids:
|
| 95 |
+
if did == drug_id:
|
| 96 |
+
continue
|
| 97 |
+
r = self.lookup_ddi(cand.drug_id, did)
|
| 98 |
+
if r.severity == "severe":
|
| 99 |
+
count += 1
|
| 100 |
+
return count
|
| 101 |
+
|
| 102 |
+
candidates.sort(key=lambda c: (_severe_count(c), c.drug_id))
|
| 103 |
+
return candidates[0].drug_id
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def drug_metadata(self) -> Dict[str, DrugMeta]:
|
| 107 |
+
return self._drug_meta
|
| 108 |
+
|
| 109 |
+
@property
|
| 110 |
+
def ddi_rules(self) -> Dict[Tuple[str, str], DDIRule]:
|
| 111 |
+
return self._ddi_rules
|
| 112 |
+
|
| 113 |
+
@property
|
| 114 |
+
def beers_criteria(self) -> List[BeersCriterion]:
|
| 115 |
+
return self._beers
|
openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PolypharmacyEnv – core environment implementing OpenEnv step / reset / state."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from itertools import combinations
|
| 7 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 8 |
+
|
| 9 |
+
from openenv.core.env_server.interfaces import Environment
|
| 10 |
+
|
| 11 |
+
from .config import CRITICAL_DRUG_IDS, TaskConfig
|
| 12 |
+
from .data_loader import PatientEpisode
|
| 13 |
+
from .ddi_simulator import DDISimulator
|
| 14 |
+
from .graders import (
|
| 15 |
+
grade_budgeted_screening,
|
| 16 |
+
grade_complex_tradeoff,
|
| 17 |
+
grade_easy_screening,
|
| 18 |
+
)
|
| 19 |
+
from .models import (
|
| 20 |
+
InteractionQueryRecord,
|
| 21 |
+
InterventionRecord,
|
| 22 |
+
MedicationEntry,
|
| 23 |
+
PolypharmacyAction,
|
| 24 |
+
PolypharmacyObservation,
|
| 25 |
+
PolypharmacyState,
|
| 26 |
+
)
|
| 27 |
+
from .rewards import compute_regimen_risk, compute_shaped_reward
|
| 28 |
+
from .tasks import get_task_config, sample_episode
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class PolypharmacyEnv(
|
| 32 |
+
Environment[PolypharmacyAction, PolypharmacyObservation, PolypharmacyState]
|
| 33 |
+
):
|
| 34 |
+
"""OpenEnv-compliant environment for elderly polypharmacy medication review.
|
| 35 |
+
|
| 36 |
+
Extends openenv.core.env_server.interfaces.Environment with typed
|
| 37 |
+
Action/Observation/State generics.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(self) -> None:
|
| 41 |
+
super().__init__()
|
| 42 |
+
self._sim = DDISimulator()
|
| 43 |
+
self._task_cfg: Optional[TaskConfig] = None
|
| 44 |
+
self._episode: Optional[PatientEpisode] = None
|
| 45 |
+
self._medications: List[MedicationEntry] = []
|
| 46 |
+
self._interaction_queries: List[InteractionQueryRecord] = []
|
| 47 |
+
self._interventions: List[InterventionRecord] = []
|
| 48 |
+
self._risk_deltas: List[float] = [] # per-intervention risk improvement
|
| 49 |
+
self._step_count: int = 0
|
| 50 |
+
self._done: bool = True
|
| 51 |
+
self._baseline_risk: float = 0.0
|
| 52 |
+
self._current_risk: float = 0.0
|
| 53 |
+
self._remaining_query_budget: int = 0
|
| 54 |
+
self._remaining_intervention_budget: int = 0
|
| 55 |
+
self._severe_moderate_discovered: int = 0
|
| 56 |
+
self._total_drug_changes: int = 0
|
| 57 |
+
self._critical_stopped_without_sub: int = 0
|
| 58 |
+
self._last_reward: float = 0.0
|
| 59 |
+
|
| 60 |
+
# ── reset ────────────────────────────────────────────────────────────────
|
| 61 |
+
|
| 62 |
+
def reset(
|
| 63 |
+
self,
|
| 64 |
+
seed: Optional[int] = None,
|
| 65 |
+
episode_id: Optional[str] = None,
|
| 66 |
+
**kwargs: Any,
|
| 67 |
+
) -> PolypharmacyObservation:
|
| 68 |
+
task_id = kwargs.get("task_id", None)
|
| 69 |
+
self._task_cfg = get_task_config(task_id)
|
| 70 |
+
self._episode = sample_episode(task_id, seed=seed, episode_id=episode_id)
|
| 71 |
+
|
| 72 |
+
# Build medication list
|
| 73 |
+
self._medications = []
|
| 74 |
+
for did in self._episode.medication_ids:
|
| 75 |
+
meta = self._sim.get_drug_meta(did)
|
| 76 |
+
if meta is None:
|
| 77 |
+
continue
|
| 78 |
+
flags = self._sim.get_beers_flags(did, self._episode.conditions)
|
| 79 |
+
self._medications.append(MedicationEntry(
|
| 80 |
+
drug_id=did,
|
| 81 |
+
generic_name=meta.generic_name,
|
| 82 |
+
atc_class=meta.atc_class,
|
| 83 |
+
dose_mg=meta.default_dose_mg,
|
| 84 |
+
is_high_risk_elderly=meta.is_high_risk_elderly,
|
| 85 |
+
beers_flags=flags,
|
| 86 |
+
))
|
| 87 |
+
|
| 88 |
+
self._interaction_queries = []
|
| 89 |
+
self._interventions = []
|
| 90 |
+
self._risk_deltas = []
|
| 91 |
+
self._step_count = 0
|
| 92 |
+
self._done = False
|
| 93 |
+
self._remaining_query_budget = self._task_cfg.query_budget
|
| 94 |
+
self._remaining_intervention_budget = self._task_cfg.intervention_budget
|
| 95 |
+
self._severe_moderate_discovered = 0
|
| 96 |
+
self._total_drug_changes = 0
|
| 97 |
+
self._critical_stopped_without_sub = 0
|
| 98 |
+
self._last_reward = 0.0
|
| 99 |
+
|
| 100 |
+
# Compute baseline risk
|
| 101 |
+
self._baseline_risk = self._compute_risk()
|
| 102 |
+
self._current_risk = self._baseline_risk
|
| 103 |
+
|
| 104 |
+
return self._make_observation()
|
| 105 |
+
|
| 106 |
+
# ── step ─────────────────────────────────────────────────────────────────
|
| 107 |
+
|
| 108 |
+
def step(
|
| 109 |
+
self,
|
| 110 |
+
action: PolypharmacyAction,
|
| 111 |
+
timeout_s: Optional[float] = None,
|
| 112 |
+
**kwargs: Any,
|
| 113 |
+
) -> PolypharmacyObservation:
|
| 114 |
+
if self._done:
|
| 115 |
+
return self._make_observation()
|
| 116 |
+
|
| 117 |
+
assert self._task_cfg is not None
|
| 118 |
+
assert self._episode is not None
|
| 119 |
+
|
| 120 |
+
reward = 0.0
|
| 121 |
+
info: Dict[str, Any] = {}
|
| 122 |
+
|
| 123 |
+
# Validate basic action structure
|
| 124 |
+
valid, err = self._validate_action(action)
|
| 125 |
+
if not valid:
|
| 126 |
+
reward = compute_shaped_reward(
|
| 127 |
+
self._current_risk, self._current_risk,
|
| 128 |
+
action.action_type, is_invalid=True,
|
| 129 |
+
)
|
| 130 |
+
info["error"] = err
|
| 131 |
+
self._step_count += 1
|
| 132 |
+
return self._check_timeout_and_build_obs(reward, info)
|
| 133 |
+
|
| 134 |
+
if action.action_type == "query_ddi":
|
| 135 |
+
reward, info = self._handle_query(action)
|
| 136 |
+
|
| 137 |
+
elif action.action_type == "propose_intervention":
|
| 138 |
+
reward, info = self._handle_intervention(action)
|
| 139 |
+
|
| 140 |
+
elif action.action_type == "finish_review":
|
| 141 |
+
self._done = True
|
| 142 |
+
score = self._run_grader()
|
| 143 |
+
reward = score # terminal bonus
|
| 144 |
+
info["grader_score"] = score
|
| 145 |
+
|
| 146 |
+
self._step_count += 1
|
| 147 |
+
return self._check_timeout_and_build_obs(reward, info)
|
| 148 |
+
|
| 149 |
+
# ── state property ───────────────────────────────────────────────────────
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def state(self) -> PolypharmacyState:
|
| 153 |
+
return PolypharmacyState(
|
| 154 |
+
episode_id=self._episode.episode_id if self._episode else None,
|
| 155 |
+
step_count=self._step_count,
|
| 156 |
+
task_id=self._task_cfg.task_id if self._task_cfg else "",
|
| 157 |
+
max_steps=self._task_cfg.max_steps if self._task_cfg else 0,
|
| 158 |
+
num_query_actions=len(self._interaction_queries),
|
| 159 |
+
num_interventions=len(self._interventions),
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
| 163 |
+
|
| 164 |
+
def _compute_risk(self) -> float:
|
| 165 |
+
drug_ids = [m.drug_id for m in self._medications]
|
| 166 |
+
return compute_regimen_risk(
|
| 167 |
+
drug_ids,
|
| 168 |
+
self._episode.conditions if self._episode else [],
|
| 169 |
+
self._sim.ddi_rules,
|
| 170 |
+
self._sim.beers_criteria,
|
| 171 |
+
self._sim.drug_metadata,
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
def _validate_action(self, action: PolypharmacyAction) -> Tuple[bool, str]:
|
| 175 |
+
if action.action_type == "query_ddi":
|
| 176 |
+
if not action.drug_id_1 or not action.drug_id_2:
|
| 177 |
+
return False, "query_ddi requires drug_id_1 and drug_id_2"
|
| 178 |
+
elif action.action_type == "propose_intervention":
|
| 179 |
+
if not action.target_drug_id:
|
| 180 |
+
return False, "propose_intervention requires target_drug_id"
|
| 181 |
+
if action.intervention_type in (None, "none"):
|
| 182 |
+
return False, "propose_intervention requires a valid intervention_type"
|
| 183 |
+
return True, ""
|
| 184 |
+
|
| 185 |
+
def _handle_query(self, action: PolypharmacyAction) -> Tuple[float, Dict[str, Any]]:
|
| 186 |
+
info: Dict[str, Any] = {}
|
| 187 |
+
assert action.drug_id_1 and action.drug_id_2
|
| 188 |
+
|
| 189 |
+
if self._remaining_query_budget <= 0:
|
| 190 |
+
reward = compute_shaped_reward(
|
| 191 |
+
self._current_risk, self._current_risk,
|
| 192 |
+
"query_ddi", is_invalid=True,
|
| 193 |
+
)
|
| 194 |
+
info["error"] = "Query budget exhausted"
|
| 195 |
+
return reward, info
|
| 196 |
+
|
| 197 |
+
result = self._sim.lookup_ddi(action.drug_id_1, action.drug_id_2)
|
| 198 |
+
self._remaining_query_budget -= 1
|
| 199 |
+
|
| 200 |
+
self._interaction_queries.append(InteractionQueryRecord(
|
| 201 |
+
drug_id_1=action.drug_id_1,
|
| 202 |
+
drug_id_2=action.drug_id_2,
|
| 203 |
+
severity=result.severity,
|
| 204 |
+
recommendation=result.recommendation,
|
| 205 |
+
risk_score=result.base_risk_score,
|
| 206 |
+
step_index=self._step_count,
|
| 207 |
+
))
|
| 208 |
+
|
| 209 |
+
discovered_severe = result.severity in ("severe", "moderate")
|
| 210 |
+
if discovered_severe:
|
| 211 |
+
self._severe_moderate_discovered += 1
|
| 212 |
+
|
| 213 |
+
reward = compute_shaped_reward(
|
| 214 |
+
self._current_risk, self._current_risk,
|
| 215 |
+
"query_ddi",
|
| 216 |
+
discovered_severe=(result.severity == "severe"),
|
| 217 |
+
)
|
| 218 |
+
info["ddi_result"] = {
|
| 219 |
+
"severity": result.severity,
|
| 220 |
+
"recommendation": result.recommendation,
|
| 221 |
+
"risk_score": result.base_risk_score,
|
| 222 |
+
}
|
| 223 |
+
return reward, info
|
| 224 |
+
|
| 225 |
+
def _handle_intervention(self, action: PolypharmacyAction) -> Tuple[float, Dict[str, Any]]:
|
| 226 |
+
info: Dict[str, Any] = {}
|
| 227 |
+
assert action.target_drug_id
|
| 228 |
+
assert action.intervention_type and action.intervention_type != "none"
|
| 229 |
+
|
| 230 |
+
if self._remaining_intervention_budget <= 0:
|
| 231 |
+
reward = compute_shaped_reward(
|
| 232 |
+
self._current_risk, self._current_risk,
|
| 233 |
+
"propose_intervention", is_invalid=True,
|
| 234 |
+
)
|
| 235 |
+
info["error"] = "Intervention budget exhausted"
|
| 236 |
+
return reward, info
|
| 237 |
+
|
| 238 |
+
# Find target medication
|
| 239 |
+
target_idx: Optional[int] = None
|
| 240 |
+
for i, m in enumerate(self._medications):
|
| 241 |
+
if m.drug_id == action.target_drug_id:
|
| 242 |
+
target_idx = i
|
| 243 |
+
break
|
| 244 |
+
|
| 245 |
+
if target_idx is None:
|
| 246 |
+
reward = compute_shaped_reward(
|
| 247 |
+
self._current_risk, self._current_risk,
|
| 248 |
+
"propose_intervention", is_invalid=True,
|
| 249 |
+
)
|
| 250 |
+
info["error"] = f"Drug {action.target_drug_id} not in current medications"
|
| 251 |
+
return reward, info
|
| 252 |
+
|
| 253 |
+
previous_risk = self._current_risk
|
| 254 |
+
target_med = self._medications[target_idx]
|
| 255 |
+
|
| 256 |
+
if action.intervention_type == "stop":
|
| 257 |
+
self._medications.pop(target_idx)
|
| 258 |
+
self._total_drug_changes += 1
|
| 259 |
+
if action.target_drug_id in CRITICAL_DRUG_IDS:
|
| 260 |
+
self._critical_stopped_without_sub += 1
|
| 261 |
+
|
| 262 |
+
elif action.intervention_type == "dose_reduce":
|
| 263 |
+
meta = self._sim.get_drug_meta(action.target_drug_id)
|
| 264 |
+
if meta:
|
| 265 |
+
new_dose = max(meta.min_dose_mg, target_med.dose_mg * 0.5)
|
| 266 |
+
self._medications[target_idx] = target_med.model_copy(
|
| 267 |
+
update={"dose_mg": new_dose}
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
elif action.intervention_type == "substitute":
|
| 271 |
+
new_drug_id = action.proposed_new_drug_id
|
| 272 |
+
if not new_drug_id:
|
| 273 |
+
# Auto-find substitute
|
| 274 |
+
current_ids = [m.drug_id for m in self._medications]
|
| 275 |
+
new_drug_id = self._sim.find_substitute(action.target_drug_id, current_ids)
|
| 276 |
+
if new_drug_id:
|
| 277 |
+
new_meta = self._sim.get_drug_meta(new_drug_id)
|
| 278 |
+
if new_meta:
|
| 279 |
+
flags = self._sim.get_beers_flags(
|
| 280 |
+
new_drug_id,
|
| 281 |
+
self._episode.conditions if self._episode else [],
|
| 282 |
+
)
|
| 283 |
+
self._medications[target_idx] = MedicationEntry(
|
| 284 |
+
drug_id=new_drug_id,
|
| 285 |
+
generic_name=new_meta.generic_name,
|
| 286 |
+
atc_class=new_meta.atc_class,
|
| 287 |
+
dose_mg=new_meta.default_dose_mg,
|
| 288 |
+
is_high_risk_elderly=new_meta.is_high_risk_elderly,
|
| 289 |
+
beers_flags=flags,
|
| 290 |
+
)
|
| 291 |
+
self._total_drug_changes += 1
|
| 292 |
+
# If critical drug was substituted, don't penalise
|
| 293 |
+
if action.target_drug_id in CRITICAL_DRUG_IDS:
|
| 294 |
+
pass # substitution is acceptable
|
| 295 |
+
else:
|
| 296 |
+
info["warning"] = f"Substitute {new_drug_id} not found in metadata"
|
| 297 |
+
# Don't consume budget for a failed substitute
|
| 298 |
+
self._remaining_intervention_budget += 1
|
| 299 |
+
else:
|
| 300 |
+
info["warning"] = "No suitable substitute found"
|
| 301 |
+
# Don't consume budget for a failed substitute
|
| 302 |
+
self._remaining_intervention_budget += 1
|
| 303 |
+
|
| 304 |
+
elif action.intervention_type == "add_monitoring":
|
| 305 |
+
# Tag in metadata but don't change regimen
|
| 306 |
+
self._medications[target_idx] = target_med.model_copy(
|
| 307 |
+
update={"beers_flags": target_med.beers_flags + ["monitored"]}
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
self._remaining_intervention_budget -= 1
|
| 311 |
+
self._current_risk = self._compute_risk()
|
| 312 |
+
risk_delta = previous_risk - self._current_risk
|
| 313 |
+
self._risk_deltas.append(risk_delta)
|
| 314 |
+
|
| 315 |
+
self._interventions.append(InterventionRecord(
|
| 316 |
+
target_drug_id=action.target_drug_id,
|
| 317 |
+
action_type=action.intervention_type,
|
| 318 |
+
proposed_new_drug_id=action.proposed_new_drug_id,
|
| 319 |
+
rationale=action.rationale or "",
|
| 320 |
+
step_index=self._step_count,
|
| 321 |
+
))
|
| 322 |
+
|
| 323 |
+
reward = compute_shaped_reward(previous_risk, self._current_risk, "propose_intervention")
|
| 324 |
+
info["risk_delta"] = risk_delta
|
| 325 |
+
return reward, info
|
| 326 |
+
|
| 327 |
+
def _run_grader(self) -> float:
|
| 328 |
+
assert self._task_cfg is not None
|
| 329 |
+
tid = self._task_cfg.task_id
|
| 330 |
+
|
| 331 |
+
if tid == "easy_screening":
|
| 332 |
+
severe_pairs = self._get_severe_pairs()
|
| 333 |
+
return grade_easy_screening(
|
| 334 |
+
self._baseline_risk,
|
| 335 |
+
self._current_risk,
|
| 336 |
+
self._interventions,
|
| 337 |
+
severe_pairs,
|
| 338 |
+
)
|
| 339 |
+
elif tid == "budgeted_screening":
|
| 340 |
+
return grade_budgeted_screening(
|
| 341 |
+
self._baseline_risk,
|
| 342 |
+
self._current_risk,
|
| 343 |
+
self._interventions,
|
| 344 |
+
self._risk_deltas,
|
| 345 |
+
len(self._interaction_queries),
|
| 346 |
+
self._severe_moderate_discovered,
|
| 347 |
+
)
|
| 348 |
+
elif tid == "complex_tradeoff":
|
| 349 |
+
return grade_complex_tradeoff(
|
| 350 |
+
self._baseline_risk,
|
| 351 |
+
self._current_risk,
|
| 352 |
+
self._interventions,
|
| 353 |
+
self._total_drug_changes,
|
| 354 |
+
self._critical_stopped_without_sub,
|
| 355 |
+
)
|
| 356 |
+
return 0.0
|
| 357 |
+
|
| 358 |
+
def _get_severe_pairs(self) -> List[Tuple[str, str]]:
|
| 359 |
+
"""Return all severe DDI pairs present in the *initial* medication list."""
|
| 360 |
+
if not self._episode:
|
| 361 |
+
return []
|
| 362 |
+
pairs: List[Tuple[str, str]] = []
|
| 363 |
+
med_ids = self._episode.medication_ids
|
| 364 |
+
for a, b in combinations(sorted(set(med_ids)), 2):
|
| 365 |
+
key = (a, b) if a < b else (b, a)
|
| 366 |
+
rule = self._sim.ddi_rules.get(key)
|
| 367 |
+
if rule and rule.severity == "severe":
|
| 368 |
+
pairs.append(key)
|
| 369 |
+
return pairs
|
| 370 |
+
|
| 371 |
+
def _check_timeout_and_build_obs(
|
| 372 |
+
self, reward: float, info: Dict[str, Any]
|
| 373 |
+
) -> PolypharmacyObservation:
|
| 374 |
+
assert self._task_cfg is not None
|
| 375 |
+
|
| 376 |
+
if not self._done and self._step_count >= self._task_cfg.max_steps:
|
| 377 |
+
self._done = True
|
| 378 |
+
timeout_penalty = compute_shaped_reward(
|
| 379 |
+
self._current_risk, self._current_risk,
|
| 380 |
+
"finish_review", is_timeout=True,
|
| 381 |
+
)
|
| 382 |
+
score = self._run_grader()
|
| 383 |
+
reward += timeout_penalty + score
|
| 384 |
+
info["timeout"] = True
|
| 385 |
+
info["grader_score"] = score
|
| 386 |
+
|
| 387 |
+
self._last_reward = reward
|
| 388 |
+
info["current_risk"] = self._current_risk
|
| 389 |
+
info["baseline_risk"] = self._baseline_risk
|
| 390 |
+
|
| 391 |
+
return self._make_observation(reward=reward, info=info)
|
| 392 |
+
|
| 393 |
+
def _make_observation(
|
| 394 |
+
self, reward: float = 0.0, info: Optional[Dict[str, Any]] = None,
|
| 395 |
+
) -> PolypharmacyObservation:
|
| 396 |
+
ep = self._episode
|
| 397 |
+
cfg = self._task_cfg
|
| 398 |
+
return PolypharmacyObservation(
|
| 399 |
+
episode_id=ep.episode_id if ep else "",
|
| 400 |
+
task_id=cfg.task_id if cfg else "budgeted_screening",
|
| 401 |
+
age=ep.age if ep else 65,
|
| 402 |
+
sex=ep.sex if ep else "M",
|
| 403 |
+
conditions=ep.conditions if ep else [],
|
| 404 |
+
eGFR_category=ep.eGFR_category if ep else "normal",
|
| 405 |
+
liver_function_category=ep.liver_function_category if ep else "normal",
|
| 406 |
+
current_medications=deepcopy(self._medications),
|
| 407 |
+
interaction_queries=deepcopy(self._interaction_queries),
|
| 408 |
+
interventions=deepcopy(self._interventions),
|
| 409 |
+
step_index=self._step_count,
|
| 410 |
+
remaining_query_budget=self._remaining_query_budget,
|
| 411 |
+
remaining_intervention_budget=self._remaining_intervention_budget,
|
| 412 |
+
shaped_reward=reward,
|
| 413 |
+
done=self._done,
|
| 414 |
+
reward=reward,
|
| 415 |
+
metadata=info or {},
|
| 416 |
+
)
|
openenv-polypharmacy/backend/src/polypharmacy_env/graders.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic graders for the three PolypharmacyEnv task difficulties."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from itertools import combinations
|
| 6 |
+
from typing import Dict, List, Tuple
|
| 7 |
+
|
| 8 |
+
from .data_loader import DDIRule
|
| 9 |
+
from .config import CRITICAL_DRUG_IDS
|
| 10 |
+
from .models import InterventionRecord
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_EPS = 1e-8
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _clip(x: float) -> float:
|
| 17 |
+
return max(0.0, min(x, 1.0))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ── Easy: easy_screening ─────────────────────────────────────────────────────
|
| 21 |
+
|
| 22 |
+
def grade_easy_screening(
|
| 23 |
+
baseline_risk: float,
|
| 24 |
+
final_risk: float,
|
| 25 |
+
interventions: List[InterventionRecord],
|
| 26 |
+
severe_ddi_drug_ids: List[Tuple[str, str]],
|
| 27 |
+
) -> float:
|
| 28 |
+
"""Score ∈ [0, 1] for the easy task.
|
| 29 |
+
|
| 30 |
+
50 % risk reduction + 50 % targeted-intervention flag.
|
| 31 |
+
"""
|
| 32 |
+
risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, _EPS)
|
| 33 |
+
|
| 34 |
+
targeted = 0.0
|
| 35 |
+
severe_drugs = set()
|
| 36 |
+
for a, b in severe_ddi_drug_ids:
|
| 37 |
+
severe_drugs.add(a)
|
| 38 |
+
severe_drugs.add(b)
|
| 39 |
+
for iv in interventions:
|
| 40 |
+
if iv.target_drug_id in severe_drugs:
|
| 41 |
+
targeted = 1.0
|
| 42 |
+
break
|
| 43 |
+
|
| 44 |
+
return _clip(0.5 * risk_reduction + 0.5 * targeted)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ── Medium: budgeted_screening ───────────────────────────────────────────────
|
| 48 |
+
|
| 49 |
+
def grade_budgeted_screening(
|
| 50 |
+
baseline_risk: float,
|
| 51 |
+
final_risk: float,
|
| 52 |
+
interventions: List[InterventionRecord],
|
| 53 |
+
risk_deltas: List[float],
|
| 54 |
+
num_queries: int,
|
| 55 |
+
severe_moderate_discovered: int,
|
| 56 |
+
) -> float:
|
| 57 |
+
"""Score ∈ [0, 1] for the medium task.
|
| 58 |
+
|
| 59 |
+
50 % risk reduction + 30 % intervention precision + 20 % query efficiency.
|
| 60 |
+
"""
|
| 61 |
+
risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, _EPS)
|
| 62 |
+
|
| 63 |
+
# Intervention precision: fraction of interventions that reduced risk
|
| 64 |
+
if interventions:
|
| 65 |
+
good = sum(1 for d in risk_deltas if d > 0)
|
| 66 |
+
precision = good / len(interventions)
|
| 67 |
+
else:
|
| 68 |
+
precision = 0.0
|
| 69 |
+
|
| 70 |
+
# Query efficiency
|
| 71 |
+
if num_queries > 0:
|
| 72 |
+
query_eff = min(severe_moderate_discovered / num_queries, 1.0)
|
| 73 |
+
else:
|
| 74 |
+
query_eff = 0.0
|
| 75 |
+
|
| 76 |
+
return _clip(0.5 * risk_reduction + 0.3 * precision + 0.2 * query_eff)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ── Hard: complex_tradeoff ───────────────────────────────────────────────────
|
| 80 |
+
|
| 81 |
+
def grade_complex_tradeoff(
|
| 82 |
+
baseline_risk: float,
|
| 83 |
+
final_risk: float,
|
| 84 |
+
interventions: List[InterventionRecord],
|
| 85 |
+
total_drug_changes: int,
|
| 86 |
+
critical_drugs_stopped_without_sub: int,
|
| 87 |
+
) -> float:
|
| 88 |
+
"""Score ∈ [0, 1] for the hard task.
|
| 89 |
+
|
| 90 |
+
Base = risk reduction; penalty for regimen disruption and critical-drug stops.
|
| 91 |
+
"""
|
| 92 |
+
risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, _EPS)
|
| 93 |
+
|
| 94 |
+
# Regimen disruption: penalise excessive changes
|
| 95 |
+
disruption = 0.05 * total_drug_changes
|
| 96 |
+
critical_penalty = 0.20 * critical_drugs_stopped_without_sub
|
| 97 |
+
|
| 98 |
+
return _clip(risk_reduction - disruption - critical_penalty)
|
openenv-polypharmacy/backend/src/polypharmacy_env/models.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic models for the PolypharmacyEnv environment.
|
| 2 |
+
|
| 3 |
+
Extends OpenEnv base types (Action, Observation, State) and defines
|
| 4 |
+
auxiliary records for medications, interactions, and interventions.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 10 |
+
|
| 11 |
+
from pydantic import BaseModel, ConfigDict, Field
|
| 12 |
+
|
| 13 |
+
from openenv.core.env_server.types import (
|
| 14 |
+
Action as OpenEnvAction,
|
| 15 |
+
Observation as OpenEnvObservation,
|
| 16 |
+
State as OpenEnvState,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ── Auxiliary models ─────────────────────────────────────────────────────────
|
| 21 |
+
|
| 22 |
+
class MedicationEntry(BaseModel):
|
| 23 |
+
model_config = ConfigDict(extra="forbid")
|
| 24 |
+
|
| 25 |
+
drug_id: str
|
| 26 |
+
generic_name: str
|
| 27 |
+
atc_class: str
|
| 28 |
+
dose_mg: float
|
| 29 |
+
frequency: str = "qd"
|
| 30 |
+
route: str = "po"
|
| 31 |
+
is_high_risk_elderly: bool = False
|
| 32 |
+
beers_flags: List[str] = Field(default_factory=list)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class InteractionQueryRecord(BaseModel):
|
| 36 |
+
model_config = ConfigDict(extra="forbid")
|
| 37 |
+
|
| 38 |
+
drug_id_1: str
|
| 39 |
+
drug_id_2: str
|
| 40 |
+
severity: Optional[str] = None
|
| 41 |
+
recommendation: Optional[str] = None
|
| 42 |
+
risk_score: Optional[float] = None
|
| 43 |
+
step_index: int = 0
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class InterventionRecord(BaseModel):
|
| 47 |
+
model_config = ConfigDict(extra="forbid")
|
| 48 |
+
|
| 49 |
+
target_drug_id: str
|
| 50 |
+
action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]
|
| 51 |
+
proposed_new_drug_id: Optional[str] = None
|
| 52 |
+
rationale: str = ""
|
| 53 |
+
step_index: int = 0
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ── OpenEnv wire models ─────────────────────────────────────────────────────
|
| 57 |
+
|
| 58 |
+
class PolypharmacyAction(OpenEnvAction):
|
| 59 |
+
"""Action sent by the agent each step.
|
| 60 |
+
|
| 61 |
+
Extends openenv.core.env_server.types.Action.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
action_type: Literal["query_ddi", "propose_intervention", "finish_review"]
|
| 65 |
+
drug_id_1: Optional[str] = None
|
| 66 |
+
drug_id_2: Optional[str] = None
|
| 67 |
+
target_drug_id: Optional[str] = None
|
| 68 |
+
intervention_type: Optional[
|
| 69 |
+
Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"]
|
| 70 |
+
] = None
|
| 71 |
+
proposed_new_drug_id: Optional[str] = None
|
| 72 |
+
rationale: Optional[str] = None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class PolypharmacyObservation(OpenEnvObservation):
|
| 76 |
+
"""Observation returned to the agent.
|
| 77 |
+
|
| 78 |
+
Extends openenv.core.env_server.types.Observation which provides:
|
| 79 |
+
- done: bool
|
| 80 |
+
- reward: float | None
|
| 81 |
+
- metadata: Dict[str, Any]
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
episode_id: str = ""
|
| 85 |
+
task_id: str = "budgeted_screening"
|
| 86 |
+
age: int = 65
|
| 87 |
+
sex: str = "M"
|
| 88 |
+
conditions: List[str] = Field(default_factory=list)
|
| 89 |
+
eGFR_category: str = "normal"
|
| 90 |
+
liver_function_category: str = "normal"
|
| 91 |
+
current_medications: List[MedicationEntry] = Field(default_factory=list)
|
| 92 |
+
interaction_queries: List[InteractionQueryRecord] = Field(default_factory=list)
|
| 93 |
+
interventions: List[InterventionRecord] = Field(default_factory=list)
|
| 94 |
+
step_index: int = 0
|
| 95 |
+
remaining_query_budget: int = 0
|
| 96 |
+
remaining_intervention_budget: int = 0
|
| 97 |
+
shaped_reward: float = 0.0
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class PolypharmacyState(OpenEnvState):
|
| 101 |
+
"""Compact state snapshot for the /state endpoint.
|
| 102 |
+
|
| 103 |
+
Extends openenv.core.env_server.types.State which provides:
|
| 104 |
+
- episode_id: str | None
|
| 105 |
+
- step_count: int
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
task_id: str = ""
|
| 109 |
+
max_steps: int = 0
|
| 110 |
+
num_query_actions: int = 0
|
| 111 |
+
num_interventions: int = 0
|
openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reward shaping and regimen-risk computation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from itertools import combinations
|
| 6 |
+
from typing import Dict, List, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
from .config import (
|
| 9 |
+
INTERVENTION_COST,
|
| 10 |
+
INVALID_ACTION_PENALTY,
|
| 11 |
+
QUERY_COST,
|
| 12 |
+
SEVERE_DDI_DISCOVERY_BONUS,
|
| 13 |
+
TIMEOUT_PENALTY,
|
| 14 |
+
)
|
| 15 |
+
from .data_loader import BeersCriterion, DDIRule, DrugMeta
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def compute_regimen_risk(
|
| 19 |
+
current_drug_ids: List[str],
|
| 20 |
+
patient_conditions: List[str],
|
| 21 |
+
ddi_rules: Dict[Tuple[str, str], DDIRule],
|
| 22 |
+
beers_criteria: List[BeersCriterion],
|
| 23 |
+
drug_metadata: Dict[str, DrugMeta],
|
| 24 |
+
) -> float:
|
| 25 |
+
"""Compute an aggregate risk score for the current medication regimen.
|
| 26 |
+
|
| 27 |
+
Returns a float clipped to [0.0, 1.0].
|
| 28 |
+
"""
|
| 29 |
+
if not current_drug_ids:
|
| 30 |
+
return 0.0
|
| 31 |
+
|
| 32 |
+
risk = 0.0
|
| 33 |
+
drug_set = set(current_drug_ids)
|
| 34 |
+
|
| 35 |
+
# 1. DDI pairwise risk
|
| 36 |
+
for a, b in combinations(sorted(drug_set), 2):
|
| 37 |
+
key = (a, b) if a < b else (b, a)
|
| 38 |
+
rule = ddi_rules.get(key)
|
| 39 |
+
if rule is not None:
|
| 40 |
+
risk += rule.base_risk_score
|
| 41 |
+
|
| 42 |
+
# 2. Beers violations
|
| 43 |
+
beers_weight = {"avoid": 0.25, "caution": 0.10, "dose_adjust": 0.08, "avoid_in_condition": 0.20}
|
| 44 |
+
for bc in beers_criteria:
|
| 45 |
+
if bc.drug_id not in drug_set:
|
| 46 |
+
continue
|
| 47 |
+
if bc.condition is None:
|
| 48 |
+
risk += beers_weight.get(bc.criterion_type, 0.05)
|
| 49 |
+
elif bc.condition in patient_conditions:
|
| 50 |
+
risk += beers_weight.get(bc.criterion_type, 0.05)
|
| 51 |
+
|
| 52 |
+
# 3. High-risk elderly drugs
|
| 53 |
+
for did in drug_set:
|
| 54 |
+
dm = drug_metadata.get(did)
|
| 55 |
+
if dm and dm.is_high_risk_elderly:
|
| 56 |
+
risk += 0.05
|
| 57 |
+
|
| 58 |
+
# Normalise by regimen size to keep score comparable across difficulties
|
| 59 |
+
risk /= max(len(drug_set), 1)
|
| 60 |
+
return min(max(risk, 0.0), 1.0)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def compute_shaped_reward(
|
| 64 |
+
previous_risk: float,
|
| 65 |
+
new_risk: float,
|
| 66 |
+
action_type: str,
|
| 67 |
+
*,
|
| 68 |
+
is_invalid: bool = False,
|
| 69 |
+
is_timeout: bool = False,
|
| 70 |
+
discovered_severe: bool = False,
|
| 71 |
+
) -> float:
|
| 72 |
+
"""Compute the step-level shaped reward."""
|
| 73 |
+
reward = 0.0
|
| 74 |
+
|
| 75 |
+
if is_invalid:
|
| 76 |
+
return -INVALID_ACTION_PENALTY
|
| 77 |
+
|
| 78 |
+
if is_timeout:
|
| 79 |
+
return -TIMEOUT_PENALTY
|
| 80 |
+
|
| 81 |
+
if action_type == "query_ddi":
|
| 82 |
+
reward -= QUERY_COST
|
| 83 |
+
if discovered_severe:
|
| 84 |
+
reward += SEVERE_DDI_DISCOVERY_BONUS
|
| 85 |
+
|
| 86 |
+
elif action_type == "propose_intervention":
|
| 87 |
+
reward += (previous_risk - new_risk)
|
| 88 |
+
reward -= INTERVENTION_COST
|
| 89 |
+
|
| 90 |
+
# finish_review terminal bonus is added by the caller after grading
|
| 91 |
+
|
| 92 |
+
return reward
|
openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Service layer for external integrations."""
|
openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Groq-powered action suggester for PolypharmacyEnv."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from openai import OpenAI
|
| 10 |
+
|
| 11 |
+
from ..models import PolypharmacyAction, PolypharmacyObservation
|
| 12 |
+
|
| 13 |
+
DEFAULT_MODEL = "llama-3.1-8b-instant"
|
| 14 |
+
FALLBACK_MODELS = [
|
| 15 |
+
"llama-3.1-8b-instant",
|
| 16 |
+
"llama-3.3-70b-versatile",
|
| 17 |
+
"gemma2-9b-it",
|
| 18 |
+
]
|
| 19 |
+
CRITICAL_DRUG_IDS = {"DRUG_WARFARIN", "DRUG_INSULIN_GLARGINE", "DRUG_DIGOXIN"}
|
| 20 |
+
|
| 21 |
+
SYSTEM_PROMPT = """You are a clinical medication safety assistant.
|
| 22 |
+
Return exactly one JSON object describing the next action.
|
| 23 |
+
Allowed output schema:
|
| 24 |
+
{
|
| 25 |
+
"action_type": "query_ddi" | "propose_intervention" | "finish_review",
|
| 26 |
+
"drug_id_1": "optional",
|
| 27 |
+
"drug_id_2": "optional",
|
| 28 |
+
"target_drug_id": "optional",
|
| 29 |
+
"intervention_type": "stop|dose_reduce|substitute|add_monitoring|none",
|
| 30 |
+
"proposed_new_drug_id": "optional",
|
| 31 |
+
"rationale": "optional"
|
| 32 |
+
}
|
| 33 |
+
No markdown fences. No extra text.
|
| 34 |
+
Do NOT use finish_review early. First, gather evidence with query_ddi and/or
|
| 35 |
+
perform at least one meaningful intervention when needed.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _obs_to_prompt(obs: PolypharmacyObservation) -> str:
|
| 40 |
+
meds = ", ".join(m.drug_id for m in obs.current_medications)
|
| 41 |
+
conds = ", ".join(obs.conditions)
|
| 42 |
+
return (
|
| 43 |
+
f"Task: {obs.task_id}\n"
|
| 44 |
+
f"Age: {obs.age}, sex: {obs.sex}\n"
|
| 45 |
+
f"Conditions: {conds}\n"
|
| 46 |
+
f"Medications: {meds}\n"
|
| 47 |
+
f"Query budget: {obs.remaining_query_budget}\n"
|
| 48 |
+
f"Intervention budget: {obs.remaining_intervention_budget}\n"
|
| 49 |
+
f"Step index: {obs.step_index}\n"
|
| 50 |
+
"Choose the single safest, most useful next action."
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _parse_action(text: str) -> PolypharmacyAction:
|
| 55 |
+
raw = text.strip()
|
| 56 |
+
if raw.startswith("```"):
|
| 57 |
+
raw = raw.split("\n", 1)[-1]
|
| 58 |
+
if raw.endswith("```"):
|
| 59 |
+
raw = raw.rsplit("```", 1)[0]
|
| 60 |
+
raw = raw.strip()
|
| 61 |
+
payload: dict[str, Any] = json.loads(raw)
|
| 62 |
+
return PolypharmacyAction.model_validate(payload)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _fallback_query_action(obs: PolypharmacyObservation) -> PolypharmacyAction:
|
| 66 |
+
meds = [m.drug_id for m in obs.current_medications]
|
| 67 |
+
if len(meds) >= 2 and obs.remaining_query_budget > 0:
|
| 68 |
+
return PolypharmacyAction(
|
| 69 |
+
action_type="query_ddi",
|
| 70 |
+
drug_id_1=meds[0],
|
| 71 |
+
drug_id_2=meds[1],
|
| 72 |
+
)
|
| 73 |
+
return PolypharmacyAction(action_type="finish_review")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _norm_pair(a: str, b: str) -> tuple[str, str]:
|
| 77 |
+
return (a, b) if a < b else (b, a)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _pick_unseen_query_pair(obs: PolypharmacyObservation) -> tuple[str, str] | None:
|
| 81 |
+
meds = [m.drug_id for m in obs.current_medications]
|
| 82 |
+
if len(meds) < 2 or obs.remaining_query_budget <= 0:
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
seen = {
|
| 86 |
+
_norm_pair(q.drug_id_1, q.drug_id_2)
|
| 87 |
+
for q in obs.interaction_queries
|
| 88 |
+
}
|
| 89 |
+
# Prioritize pairs containing high-risk drugs.
|
| 90 |
+
high_risk = [m.drug_id for m in obs.current_medications if m.is_high_risk_elderly]
|
| 91 |
+
ordered = high_risk + [m for m in meds if m not in set(high_risk)]
|
| 92 |
+
|
| 93 |
+
for i in range(len(ordered)):
|
| 94 |
+
for j in range(i + 1, len(ordered)):
|
| 95 |
+
p = _norm_pair(ordered[i], ordered[j])
|
| 96 |
+
if p not in seen:
|
| 97 |
+
return p
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _pick_intervention_target(obs: PolypharmacyObservation) -> str | None:
|
| 102 |
+
if obs.remaining_intervention_budget <= 0:
|
| 103 |
+
return None
|
| 104 |
+
med_set = {m.drug_id for m in obs.current_medications}
|
| 105 |
+
|
| 106 |
+
# Use latest discovered severe/moderate query as intervention target.
|
| 107 |
+
for q in reversed(obs.interaction_queries):
|
| 108 |
+
if q.severity in ("severe", "moderate"):
|
| 109 |
+
m1 = next((m for m in obs.current_medications if m.drug_id == q.drug_id_1), None)
|
| 110 |
+
m2 = next((m for m in obs.current_medications if m.drug_id == q.drug_id_2), None)
|
| 111 |
+
candidates = [m for m in (m1, m2) if m is not None]
|
| 112 |
+
if not candidates:
|
| 113 |
+
continue
|
| 114 |
+
# Prefer non-critical risky drugs first.
|
| 115 |
+
candidates.sort(
|
| 116 |
+
key=lambda m: (
|
| 117 |
+
m.drug_id in CRITICAL_DRUG_IDS,
|
| 118 |
+
0 if any("avoid" in f for f in m.beers_flags) else 1,
|
| 119 |
+
0 if m.is_high_risk_elderly else 1,
|
| 120 |
+
)
|
| 121 |
+
)
|
| 122 |
+
return candidates[0].drug_id
|
| 123 |
+
|
| 124 |
+
# Fallback: if no severe/moderate discovered, still intervene on obviously
|
| 125 |
+
# risky medications (Beers/high-risk flags) when budgets permit.
|
| 126 |
+
risky = sorted(
|
| 127 |
+
obs.current_medications,
|
| 128 |
+
key=lambda m: (
|
| 129 |
+
0 if any("avoid" in f for f in m.beers_flags) else 1,
|
| 130 |
+
0 if m.is_high_risk_elderly else 1,
|
| 131 |
+
1 if m.drug_id in CRITICAL_DRUG_IDS else 0,
|
| 132 |
+
),
|
| 133 |
+
)
|
| 134 |
+
for med in risky:
|
| 135 |
+
if any("avoid" in f for f in med.beers_flags) or med.is_high_risk_elderly:
|
| 136 |
+
return med.drug_id
|
| 137 |
+
return None
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _rule_based_action(obs: PolypharmacyObservation) -> PolypharmacyAction | None:
|
| 141 |
+
# If we already discovered significant risk, intervene before more querying.
|
| 142 |
+
target = _pick_intervention_target(obs)
|
| 143 |
+
if target and (
|
| 144 |
+
obs.step_index >= 1
|
| 145 |
+
and (
|
| 146 |
+
obs.remaining_query_budget <= 2
|
| 147 |
+
or len(obs.interaction_queries) >= 4
|
| 148 |
+
or any(q.severity in ("severe", "moderate") for q in obs.interaction_queries)
|
| 149 |
+
)
|
| 150 |
+
):
|
| 151 |
+
intervention = "stop"
|
| 152 |
+
rationale = "Remove likely contributor to discovered interaction risk"
|
| 153 |
+
if target in CRITICAL_DRUG_IDS:
|
| 154 |
+
# Avoid blunt stop for critical meds.
|
| 155 |
+
intervention = "dose_reduce"
|
| 156 |
+
rationale = "Critical medication: prefer dose reduction over abrupt stop"
|
| 157 |
+
return PolypharmacyAction(
|
| 158 |
+
action_type="propose_intervention",
|
| 159 |
+
target_drug_id=target,
|
| 160 |
+
intervention_type=intervention,
|
| 161 |
+
rationale=rationale,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
pair = _pick_unseen_query_pair(obs)
|
| 165 |
+
if pair:
|
| 166 |
+
return PolypharmacyAction(
|
| 167 |
+
action_type="query_ddi",
|
| 168 |
+
drug_id_1=pair[0],
|
| 169 |
+
drug_id_2=pair[1],
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
if obs.remaining_intervention_budget > 0:
|
| 173 |
+
# Final fallback before finish: at least one safety action.
|
| 174 |
+
target = _pick_intervention_target(obs)
|
| 175 |
+
if target:
|
| 176 |
+
return PolypharmacyAction(
|
| 177 |
+
action_type="propose_intervention",
|
| 178 |
+
target_drug_id=target,
|
| 179 |
+
intervention_type="dose_reduce"
|
| 180 |
+
if target in CRITICAL_DRUG_IDS
|
| 181 |
+
else "stop",
|
| 182 |
+
rationale="Fallback intervention when query options are exhausted",
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
if obs.step_index >= 3:
|
| 186 |
+
return PolypharmacyAction(action_type="finish_review")
|
| 187 |
+
return None
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _postprocess_action(
|
| 191 |
+
obs: PolypharmacyObservation, action: PolypharmacyAction
|
| 192 |
+
) -> PolypharmacyAction:
|
| 193 |
+
# First apply deterministic guardrails to avoid repetitive loops.
|
| 194 |
+
ruled = _rule_based_action(obs)
|
| 195 |
+
if ruled is not None:
|
| 196 |
+
return ruled
|
| 197 |
+
|
| 198 |
+
# Guardrail: prevent useless immediate finish actions.
|
| 199 |
+
if action.action_type == "finish_review":
|
| 200 |
+
if obs.step_index < 2 and obs.remaining_query_budget > 0:
|
| 201 |
+
return _fallback_query_action(obs)
|
| 202 |
+
if len(obs.interaction_queries) == 0 and obs.remaining_query_budget > 0:
|
| 203 |
+
return _fallback_query_action(obs)
|
| 204 |
+
return action
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def suggest_action_from_observation(
|
| 208 |
+
observation: PolypharmacyObservation,
|
| 209 |
+
model_name: str | None = None,
|
| 210 |
+
) -> PolypharmacyAction:
|
| 211 |
+
"""Use Groq chat completions to suggest a valid action."""
|
| 212 |
+
api_key = os.getenv("GROQ_API_KEY", "").strip()
|
| 213 |
+
if not api_key:
|
| 214 |
+
raise ValueError("GROQ_API_KEY is missing. Add it to your .env file.")
|
| 215 |
+
|
| 216 |
+
base_url = os.getenv("GROQ_BASE_URL", "https://api.groq.com/openai/v1").strip()
|
| 217 |
+
model = (model_name or os.getenv("GROQ_MODEL_NAME", DEFAULT_MODEL)).strip()
|
| 218 |
+
client = OpenAI(api_key=api_key, base_url=base_url)
|
| 219 |
+
|
| 220 |
+
user_prompt = _obs_to_prompt(observation)
|
| 221 |
+
tried: list[tuple[str, str]] = []
|
| 222 |
+
candidates: list[str] = [model] + [m for m in FALLBACK_MODELS if m != model]
|
| 223 |
+
|
| 224 |
+
for candidate in candidates:
|
| 225 |
+
try:
|
| 226 |
+
resp = client.chat.completions.create(
|
| 227 |
+
model=candidate,
|
| 228 |
+
messages=[
|
| 229 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 230 |
+
{"role": "user", "content": user_prompt},
|
| 231 |
+
],
|
| 232 |
+
temperature=0.2,
|
| 233 |
+
max_tokens=220,
|
| 234 |
+
)
|
| 235 |
+
generated = (resp.choices[0].message.content or "").strip()
|
| 236 |
+
parsed = _parse_action(generated)
|
| 237 |
+
return _postprocess_action(observation, parsed)
|
| 238 |
+
except Exception as exc:
|
| 239 |
+
tried.append((candidate, str(exc)))
|
| 240 |
+
|
| 241 |
+
tried_txt = " | ".join(f"{m}: {err}" for m, err in tried)
|
| 242 |
+
raise ValueError(
|
| 243 |
+
"No Groq model worked. Try one of: "
|
| 244 |
+
"llama-3.3-70b-versatile, llama-3.1-8b-instant, gemma2-9b-it. "
|
| 245 |
+
f"Errors: {tried_txt}"
|
| 246 |
+
)
|
openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task setup utilities: select episodes and configure budgets per difficulty."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import random
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
from .config import DEFAULT_TASK, TASK_CONFIGS, TaskConfig
|
| 9 |
+
from .data_loader import PatientEpisode, load_patients
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Map OpenEnv difficulty labels to the CSV difficulty tags
|
| 13 |
+
_DIFFICULTY_MAP = {
|
| 14 |
+
"easy_screening": "easy",
|
| 15 |
+
"budgeted_screening": "medium",
|
| 16 |
+
"complex_tradeoff": "hard",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_task_config(task_id: Optional[str] = None) -> TaskConfig:
|
| 21 |
+
tid = task_id or DEFAULT_TASK
|
| 22 |
+
cfg = TASK_CONFIGS.get(tid)
|
| 23 |
+
if cfg is None:
|
| 24 |
+
raise ValueError(f"Unknown task_id {tid!r}. Choose from {list(TASK_CONFIGS)}")
|
| 25 |
+
return cfg
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def sample_episode(
|
| 29 |
+
task_id: Optional[str] = None,
|
| 30 |
+
seed: Optional[int] = None,
|
| 31 |
+
episode_id: Optional[str] = None,
|
| 32 |
+
) -> PatientEpisode:
|
| 33 |
+
"""Return a single patient episode appropriate for *task_id*."""
|
| 34 |
+
tid = task_id or DEFAULT_TASK
|
| 35 |
+
difficulty = _DIFFICULTY_MAP.get(tid, "medium")
|
| 36 |
+
episodes = load_patients(difficulty=difficulty)
|
| 37 |
+
if not episodes:
|
| 38 |
+
raise RuntimeError(f"No episodes found for difficulty={difficulty!r}")
|
| 39 |
+
|
| 40 |
+
if episode_id:
|
| 41 |
+
for ep in episodes:
|
| 42 |
+
if ep.episode_id == episode_id:
|
| 43 |
+
return ep
|
| 44 |
+
raise ValueError(f"Episode {episode_id!r} not found for difficulty={difficulty!r}")
|
| 45 |
+
|
| 46 |
+
rng = random.Random(seed)
|
| 47 |
+
return rng.choice(episodes)
|
openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Tests."""
|
openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the FastAPI HTTP server (OpenEnv create_app endpoints).
|
| 2 |
+
|
| 3 |
+
OpenEnv HTTP endpoints are *stateless*: each /reset and /step creates a
|
| 4 |
+
fresh environment instance. Multi-step sessions only work via WebSocket.
|
| 5 |
+
These tests validate single-call behaviour and schema contracts.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
import pytest
|
| 13 |
+
from fastapi.testclient import TestClient
|
| 14 |
+
|
| 15 |
+
from polypharmacy_env.api.server import app
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@pytest.fixture
|
| 19 |
+
def client() -> TestClient:
|
| 20 |
+
return TestClient(app)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TestHealth:
|
| 24 |
+
def test_health(self, client: TestClient) -> None:
|
| 25 |
+
resp = client.get("/health")
|
| 26 |
+
assert resp.status_code == 200
|
| 27 |
+
data = resp.json()
|
| 28 |
+
assert data["status"] == "healthy"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TestReset:
|
| 32 |
+
def test_reset_default(self, client: TestClient) -> None:
|
| 33 |
+
resp = client.post("/reset", json={})
|
| 34 |
+
assert resp.status_code == 200
|
| 35 |
+
data = resp.json()
|
| 36 |
+
assert "observation" in data
|
| 37 |
+
assert data["done"] is False
|
| 38 |
+
|
| 39 |
+
def test_reset_with_task(self, client: TestClient) -> None:
|
| 40 |
+
resp = client.post("/reset", json={"task_id": "easy_screening"})
|
| 41 |
+
assert resp.status_code == 200
|
| 42 |
+
obs = resp.json()["observation"]
|
| 43 |
+
assert obs["task_id"] == "easy_screening"
|
| 44 |
+
|
| 45 |
+
def test_reset_observation_has_medications(self, client: TestClient) -> None:
|
| 46 |
+
resp = client.post("/reset", json={"task_id": "easy_screening", "seed": 42})
|
| 47 |
+
assert resp.status_code == 200
|
| 48 |
+
obs = resp.json()["observation"]
|
| 49 |
+
assert len(obs["current_medications"]) >= 3
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TestStep:
|
| 53 |
+
"""Test /step endpoint – each call is independent (stateless)."""
|
| 54 |
+
|
| 55 |
+
def test_step_finish(self, client: TestClient) -> None:
|
| 56 |
+
resp = client.post(
|
| 57 |
+
"/step",
|
| 58 |
+
json={"action": {"action_type": "finish_review"}},
|
| 59 |
+
)
|
| 60 |
+
assert resp.status_code == 200
|
| 61 |
+
data = resp.json()
|
| 62 |
+
assert "observation" in data
|
| 63 |
+
|
| 64 |
+
def test_invalid_action_422(self, client: TestClient) -> None:
|
| 65 |
+
resp = client.post(
|
| 66 |
+
"/step",
|
| 67 |
+
json={"action": {"action_type": "invalid_type"}},
|
| 68 |
+
)
|
| 69 |
+
assert resp.status_code == 422
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class TestSchema:
|
| 73 |
+
def test_schema(self, client: TestClient) -> None:
|
| 74 |
+
resp = client.get("/schema")
|
| 75 |
+
assert resp.status_code == 200
|
| 76 |
+
data = resp.json()
|
| 77 |
+
# OpenEnv schema endpoint returns keys: action, observation, state
|
| 78 |
+
assert "action" in data
|
| 79 |
+
assert "observation" in data
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class TestWebSocketSession:
|
| 83 |
+
"""Test multi-step sessions through the /ws WebSocket endpoint.
|
| 84 |
+
|
| 85 |
+
OpenEnv WS protocol:
|
| 86 |
+
Send: {"type": "reset", "data": {"task_id": "...", "seed": ...}}
|
| 87 |
+
Recv: {"type": "observation", "data": {"observation": {...}, "reward": ..., "done": ...}}
|
| 88 |
+
Send: {"type": "step", "data": {"action_type": "...", ...}}
|
| 89 |
+
Recv: {"type": "observation", "data": {"observation": {...}, ...}}
|
| 90 |
+
Send: {"type": "state"}
|
| 91 |
+
Recv: {"type": "state", "data": {...state fields...}}
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def test_ws_reset_step_finish(self, client: TestClient) -> None:
|
| 95 |
+
with client.websocket_connect("/ws") as ws:
|
| 96 |
+
# Reset
|
| 97 |
+
ws.send_json({
|
| 98 |
+
"type": "reset",
|
| 99 |
+
"data": {"task_id": "easy_screening", "seed": 42},
|
| 100 |
+
})
|
| 101 |
+
reset_resp = ws.receive_json()
|
| 102 |
+
assert reset_resp["type"] == "observation"
|
| 103 |
+
reset_data = reset_resp["data"]
|
| 104 |
+
assert reset_data["done"] is False
|
| 105 |
+
obs = reset_data["observation"]
|
| 106 |
+
assert obs["task_id"] == "easy_screening"
|
| 107 |
+
meds = obs["current_medications"]
|
| 108 |
+
assert len(meds) >= 3
|
| 109 |
+
|
| 110 |
+
# Step – query DDI
|
| 111 |
+
if len(meds) >= 2:
|
| 112 |
+
ws.send_json({
|
| 113 |
+
"type": "step",
|
| 114 |
+
"data": {
|
| 115 |
+
"action_type": "query_ddi",
|
| 116 |
+
"drug_id_1": meds[0]["drug_id"],
|
| 117 |
+
"drug_id_2": meds[1]["drug_id"],
|
| 118 |
+
},
|
| 119 |
+
})
|
| 120 |
+
step_resp = ws.receive_json()
|
| 121 |
+
assert step_resp["type"] == "observation"
|
| 122 |
+
assert step_resp["data"]["done"] is False
|
| 123 |
+
|
| 124 |
+
# Finish
|
| 125 |
+
ws.send_json({
|
| 126 |
+
"type": "step",
|
| 127 |
+
"data": {"action_type": "finish_review"},
|
| 128 |
+
})
|
| 129 |
+
finish_resp = ws.receive_json()
|
| 130 |
+
assert finish_resp["type"] == "observation"
|
| 131 |
+
assert finish_resp["data"]["done"] is True
|
| 132 |
+
|
| 133 |
+
def test_ws_state(self, client: TestClient) -> None:
|
| 134 |
+
with client.websocket_connect("/ws") as ws:
|
| 135 |
+
ws.send_json({
|
| 136 |
+
"type": "reset",
|
| 137 |
+
"data": {"task_id": "easy_screening", "seed": 0},
|
| 138 |
+
})
|
| 139 |
+
ws.receive_json() # consume reset response
|
| 140 |
+
|
| 141 |
+
ws.send_json({"type": "state"})
|
| 142 |
+
state_resp = ws.receive_json()
|
| 143 |
+
assert state_resp["type"] == "state"
|
| 144 |
+
state_data = state_resp["data"]
|
| 145 |
+
assert state_data["step_count"] == 0
|
| 146 |
+
assert state_data["task_id"] == "easy_screening"
|
openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the PolypharmacyEnv core."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from polypharmacy_env.env_core import PolypharmacyEnv
|
| 8 |
+
from polypharmacy_env.models import (
|
| 9 |
+
PolypharmacyAction,
|
| 10 |
+
PolypharmacyObservation,
|
| 11 |
+
PolypharmacyState,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TestReset:
|
| 16 |
+
def test_reset_returns_observation(self) -> None:
|
| 17 |
+
env = PolypharmacyEnv()
|
| 18 |
+
obs = env.reset(task_id="easy_screening", seed=42)
|
| 19 |
+
assert isinstance(obs, PolypharmacyObservation)
|
| 20 |
+
assert obs.done is False
|
| 21 |
+
assert obs.step_index == 0
|
| 22 |
+
assert len(obs.current_medications) >= 3
|
| 23 |
+
|
| 24 |
+
def test_reset_medium(self) -> None:
|
| 25 |
+
env = PolypharmacyEnv()
|
| 26 |
+
obs = env.reset(task_id="budgeted_screening", seed=0)
|
| 27 |
+
assert obs.remaining_query_budget == 8
|
| 28 |
+
assert obs.remaining_intervention_budget == 3
|
| 29 |
+
assert len(obs.current_medications) >= 6
|
| 30 |
+
|
| 31 |
+
def test_reset_hard(self) -> None:
|
| 32 |
+
env = PolypharmacyEnv()
|
| 33 |
+
obs = env.reset(task_id="complex_tradeoff", seed=0)
|
| 34 |
+
assert obs.remaining_query_budget == 12
|
| 35 |
+
assert obs.remaining_intervention_budget == 5
|
| 36 |
+
assert len(obs.current_medications) >= 10
|
| 37 |
+
|
| 38 |
+
def test_default_task(self) -> None:
|
| 39 |
+
env = PolypharmacyEnv()
|
| 40 |
+
obs = env.reset(seed=0)
|
| 41 |
+
assert obs.task_id == "budgeted_screening"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class TestStep:
|
| 45 |
+
def test_query_ddi(self) -> None:
|
| 46 |
+
env = PolypharmacyEnv()
|
| 47 |
+
obs = env.reset(task_id="easy_screening", seed=42)
|
| 48 |
+
meds = obs.current_medications
|
| 49 |
+
assert len(meds) >= 2
|
| 50 |
+
|
| 51 |
+
action = PolypharmacyAction(
|
| 52 |
+
action_type="query_ddi",
|
| 53 |
+
drug_id_1=meds[0].drug_id,
|
| 54 |
+
drug_id_2=meds[1].drug_id,
|
| 55 |
+
)
|
| 56 |
+
obs = env.step(action)
|
| 57 |
+
assert isinstance(obs, PolypharmacyObservation)
|
| 58 |
+
assert obs.done is False
|
| 59 |
+
assert obs.step_index == 1
|
| 60 |
+
assert len(obs.interaction_queries) == 1
|
| 61 |
+
|
| 62 |
+
def test_invalid_action_penalised(self) -> None:
|
| 63 |
+
env = PolypharmacyEnv()
|
| 64 |
+
env.reset(task_id="easy_screening", seed=42)
|
| 65 |
+
|
| 66 |
+
action = PolypharmacyAction(
|
| 67 |
+
action_type="propose_intervention",
|
| 68 |
+
target_drug_id=None,
|
| 69 |
+
intervention_type=None,
|
| 70 |
+
)
|
| 71 |
+
obs = env.step(action)
|
| 72 |
+
assert obs.reward is not None
|
| 73 |
+
assert obs.reward < 0 # penalty
|
| 74 |
+
|
| 75 |
+
def test_finish_review(self) -> None:
|
| 76 |
+
env = PolypharmacyEnv()
|
| 77 |
+
env.reset(task_id="easy_screening", seed=42)
|
| 78 |
+
|
| 79 |
+
action = PolypharmacyAction(action_type="finish_review")
|
| 80 |
+
obs = env.step(action)
|
| 81 |
+
assert obs.done is True
|
| 82 |
+
assert "grader_score" in obs.metadata
|
| 83 |
+
score = obs.metadata["grader_score"]
|
| 84 |
+
assert 0.0 <= score <= 1.0
|
| 85 |
+
|
| 86 |
+
def test_intervention_stop(self) -> None:
|
| 87 |
+
env = PolypharmacyEnv()
|
| 88 |
+
obs = env.reset(task_id="easy_screening", seed=42)
|
| 89 |
+
target = obs.current_medications[0].drug_id
|
| 90 |
+
n_meds = len(obs.current_medications)
|
| 91 |
+
|
| 92 |
+
action = PolypharmacyAction(
|
| 93 |
+
action_type="propose_intervention",
|
| 94 |
+
target_drug_id=target,
|
| 95 |
+
intervention_type="stop",
|
| 96 |
+
rationale="test stop",
|
| 97 |
+
)
|
| 98 |
+
obs = env.step(action)
|
| 99 |
+
assert len(obs.current_medications) == n_meds - 1
|
| 100 |
+
|
| 101 |
+
def test_budget_exhaustion(self) -> None:
|
| 102 |
+
env = PolypharmacyEnv()
|
| 103 |
+
obs = env.reset(task_id="easy_screening", seed=42)
|
| 104 |
+
meds = obs.current_medications
|
| 105 |
+
|
| 106 |
+
# Exhaust query budget (4 for easy)
|
| 107 |
+
for i in range(4):
|
| 108 |
+
a_idx = i % len(meds)
|
| 109 |
+
b_idx = (i + 1) % len(meds)
|
| 110 |
+
action = PolypharmacyAction(
|
| 111 |
+
action_type="query_ddi",
|
| 112 |
+
drug_id_1=meds[a_idx].drug_id,
|
| 113 |
+
drug_id_2=meds[b_idx].drug_id,
|
| 114 |
+
)
|
| 115 |
+
obs = env.step(action)
|
| 116 |
+
if obs.done:
|
| 117 |
+
break
|
| 118 |
+
|
| 119 |
+
if not obs.done:
|
| 120 |
+
assert obs.remaining_query_budget == 0
|
| 121 |
+
# Trying another query should be penalised
|
| 122 |
+
action = PolypharmacyAction(
|
| 123 |
+
action_type="query_ddi",
|
| 124 |
+
drug_id_1=meds[0].drug_id,
|
| 125 |
+
drug_id_2=meds[1].drug_id,
|
| 126 |
+
)
|
| 127 |
+
obs = env.step(action)
|
| 128 |
+
assert obs.reward is not None
|
| 129 |
+
assert obs.reward < 0
|
| 130 |
+
|
| 131 |
+
def test_max_steps_timeout(self) -> None:
|
| 132 |
+
env = PolypharmacyEnv()
|
| 133 |
+
obs = env.reset(task_id="easy_screening", seed=42) # max_steps=10
|
| 134 |
+
meds = obs.current_medications
|
| 135 |
+
|
| 136 |
+
# Keep querying until timeout
|
| 137 |
+
for i in range(15):
|
| 138 |
+
if obs.done:
|
| 139 |
+
break
|
| 140 |
+
a = meds[i % len(meds)].drug_id
|
| 141 |
+
b = meds[(i + 1) % len(meds)].drug_id
|
| 142 |
+
action = PolypharmacyAction(
|
| 143 |
+
action_type="query_ddi",
|
| 144 |
+
drug_id_1=a,
|
| 145 |
+
drug_id_2=b,
|
| 146 |
+
)
|
| 147 |
+
obs = env.step(action)
|
| 148 |
+
|
| 149 |
+
assert obs.done is True
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class TestState:
|
| 153 |
+
def test_state_after_reset(self) -> None:
|
| 154 |
+
env = PolypharmacyEnv()
|
| 155 |
+
env.reset(task_id="easy_screening", seed=42)
|
| 156 |
+
st = env.state
|
| 157 |
+
assert isinstance(st, PolypharmacyState)
|
| 158 |
+
assert st.step_count == 0
|
| 159 |
+
assert st.episode_id is not None
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class TestGraderDeterminism:
|
| 163 |
+
def test_same_trajectory_same_score(self) -> None:
|
| 164 |
+
scores = []
|
| 165 |
+
for _ in range(3):
|
| 166 |
+
env = PolypharmacyEnv()
|
| 167 |
+
env.reset(task_id="easy_screening", seed=99)
|
| 168 |
+
obs = env.step(PolypharmacyAction(action_type="finish_review"))
|
| 169 |
+
scores.append(obs.metadata.get("grader_score", 0.0))
|
| 170 |
+
assert all(s == scores[0] for s in scores)
|
| 171 |
+
|
| 172 |
+
def test_intervention_changes_score(self) -> None:
|
| 173 |
+
# No intervention
|
| 174 |
+
env = PolypharmacyEnv()
|
| 175 |
+
env.reset(task_id="budgeted_screening", seed=42)
|
| 176 |
+
obs = env.step(PolypharmacyAction(action_type="finish_review"))
|
| 177 |
+
score_noop = obs.metadata.get("grader_score", 0.0)
|
| 178 |
+
|
| 179 |
+
# With intervention
|
| 180 |
+
env2 = PolypharmacyEnv()
|
| 181 |
+
obs_init2 = env2.reset(task_id="budgeted_screening", seed=42)
|
| 182 |
+
if obs_init2.current_medications:
|
| 183 |
+
env2.step(PolypharmacyAction(
|
| 184 |
+
action_type="propose_intervention",
|
| 185 |
+
target_drug_id=obs_init2.current_medications[0].drug_id,
|
| 186 |
+
intervention_type="stop",
|
| 187 |
+
rationale="test",
|
| 188 |
+
))
|
| 189 |
+
obs2 = env2.step(PolypharmacyAction(action_type="finish_review"))
|
| 190 |
+
score_act = obs2.metadata.get("grader_score", 0.0)
|
| 191 |
+
|
| 192 |
+
assert score_noop != score_act
|
openenv-polypharmacy/data/lookups/beers_criteria.csv
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
drug_id,criterion_type,condition,rationale
|
| 2 |
+
DRUG_DIAZEPAM,avoid,,"Long-acting benzodiazepine: falls, fractures, cognitive impairment in elderly"
|
| 3 |
+
DRUG_ALPRAZOLAM,avoid,,"Benzodiazepine: falls, fractures, cognitive impairment in elderly"
|
| 4 |
+
DRUG_AMITRIPTYLINE,avoid,,"Strongly anticholinergic TCA: sedation, confusion, urinary retention in elderly"
|
| 5 |
+
DRUG_GLIPIZIDE,caution,,Sulfonylurea: hypoglycemia risk higher in elderly
|
| 6 |
+
DRUG_NAPROXEN,avoid,CKD,"NSAID contraindicated in CKD – renal deterioration, fluid retention"
|
| 7 |
+
DRUG_IBUPROFEN,avoid,CKD,"NSAID contraindicated in CKD – renal deterioration, fluid retention"
|
| 8 |
+
DRUG_NAPROXEN,caution,,NSAID: GI bleeding and renal risk in elderly
|
| 9 |
+
DRUG_IBUPROFEN,caution,,NSAID: GI bleeding and renal risk in elderly
|
| 10 |
+
DRUG_DIGOXIN,dose_adjust,,Avoid doses > 0.125 mg/day in elderly – toxicity risk
|
| 11 |
+
DRUG_TRAMADOL,avoid,,"Opioid: CNS depression, falls, constipation in elderly"
|
| 12 |
+
DRUG_METFORMIN,dose_adjust,CKD,Reduce dose or avoid if eGFR < 30 – lactic acidosis risk
|
| 13 |
+
DRUG_INSULIN_GLARGINE,caution,,Tight glycemic control increases hypoglycemia risk in elderly
|
| 14 |
+
DRUG_PREDNISONE,avoid_in_condition,DM,Corticosteroid worsens glycemic control in diabetes
|
| 15 |
+
DRUG_DONEPEZIL,avoid_in_condition,dementia,"Limited benefit, GI side effects; reassess regularly"
|
| 16 |
+
DRUG_CIPROFLOXACIN,caution,,"Fluoroquinolone: tendon rupture, QT prolongation risk in elderly"
|
openenv-polypharmacy/data/lookups/ddi_rules.csv
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
drug_id_1,drug_id_2,severity,mechanism,recommendation,base_risk_score
|
| 2 |
+
DRUG_NAPROXEN,DRUG_WARFARIN,severe,Increased bleeding risk – NSAID inhibits platelet + anticoagulant,avoid_combination,0.9
|
| 3 |
+
DRUG_IBUPROFEN,DRUG_WARFARIN,severe,Increased bleeding risk – NSAID + anticoagulant synergy,avoid_combination,0.88
|
| 4 |
+
DRUG_ASPIRIN,DRUG_WARFARIN,moderate,Additive antiplatelet + anticoagulant bleeding risk,monitor_closely,0.55
|
| 5 |
+
DRUG_FLUOXETINE,DRUG_WARFARIN,moderate,SSRI increases serotonin and may potentiate bleeding,monitor_closely,0.45
|
| 6 |
+
DRUG_CIPROFLOXACIN,DRUG_WARFARIN,moderate,CYP1A2 inhibition raises warfarin levels,dose_adjust,0.5
|
| 7 |
+
DRUG_APIXABAN,DRUG_NAPROXEN,severe,DOAC + NSAID – high bleeding risk,avoid_combination,0.85
|
| 8 |
+
DRUG_APIXABAN,DRUG_ASPIRIN,moderate,Additive bleeding risk with antiplatelet,monitor_closely,0.5
|
| 9 |
+
DRUG_AMIODARONE,DRUG_DIGOXIN,severe,Amiodarone increases digoxin levels – toxicity risk,dose_adjust,0.8
|
| 10 |
+
DRUG_DIGOXIN,DRUG_SPIRONOLACTONE,moderate,Spironolactone may raise digoxin levels,monitor_closely,0.4
|
| 11 |
+
DRUG_CIPROFLOXACIN,DRUG_METFORMIN,moderate,Fluoroquinolone may cause dysglycemia with metformin,monitor_closely,0.35
|
| 12 |
+
DRUG_DIAZEPAM,DRUG_TRAMADOL,severe,CNS depression – benzodiazepine + opioid,avoid_combination,0.92
|
| 13 |
+
DRUG_ALPRAZOLAM,DRUG_TRAMADOL,severe,CNS depression – benzodiazepine + opioid,avoid_combination,0.91
|
| 14 |
+
DRUG_LISINOPRIL,DRUG_SPIRONOLACTONE,moderate,Hyperkalemia risk – ACE-I + K-sparing diuretic,monitor_closely,0.48
|
| 15 |
+
DRUG_LISINOPRIL,DRUG_NAPROXEN,moderate,"NSAID reduces ACE-I efficacy, renal risk",monitor_closely,0.42
|
| 16 |
+
DRUG_AMLODIPINE,DRUG_SIMVASTATIN,moderate,CYP3A4 interaction increases statin exposure,dose_adjust,0.38
|
| 17 |
+
DRUG_ATORVASTATIN,DRUG_CIPROFLOXACIN,mild,Minor CYP interaction raising statin levels,no_action,0.15
|
| 18 |
+
DRUG_CLOPIDOGREL,DRUG_OMEPRAZOLE,moderate,PPI reduces clopidogrel activation via CYP2C19,dose_adjust,0.45
|
| 19 |
+
DRUG_GLIPIZIDE,DRUG_INSULIN_GLARGINE,moderate,Additive hypoglycemia risk,monitor_closely,0.5
|
| 20 |
+
DRUG_FLUOXETINE,DRUG_TRAMADOL,severe,Serotonin syndrome risk – SSRI + serotonergic opioid,avoid_combination,0.82
|
| 21 |
+
DRUG_AMITRIPTYLINE,DRUG_TRAMADOL,severe,Serotonin syndrome + CNS depression,avoid_combination,0.85
|
| 22 |
+
DRUG_DIGOXIN,DRUG_METOPROLOL,moderate,Additive bradycardia,monitor_closely,0.4
|
| 23 |
+
DRUG_DIGOXIN,DRUG_FUROSEMIDE,moderate,Loop diuretic causes hypokalemia increasing digoxin toxicity risk,monitor_closely,0.45
|
| 24 |
+
DRUG_NAPROXEN,DRUG_PREDNISONE,moderate,GI bleeding risk – corticosteroid + NSAID,monitor_closely,0.5
|
| 25 |
+
DRUG_PREDNISONE,DRUG_WARFARIN,mild,Corticosteroid may alter INR,monitor_closely,0.25
|
openenv-polypharmacy/data/lookups/drug_metadata.csv
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
drug_id,generic_name,atc_class,is_high_risk_elderly,default_dose_mg,min_dose_mg,max_dose_mg
|
| 2 |
+
DRUG_WARFARIN,warfarin,B01AA,1,5.0,1.0,10.0
|
| 3 |
+
DRUG_APIXABAN,apixaban,B01AF,1,5.0,2.5,10.0
|
| 4 |
+
DRUG_METFORMIN,metformin,A10BA,0,1000,500,2000
|
| 5 |
+
DRUG_GLIPIZIDE,glipizide,A10BB,1,5.0,2.5,20.0
|
| 6 |
+
DRUG_LISINOPRIL,lisinopril,C09AA,0,10.0,2.5,40.0
|
| 7 |
+
DRUG_AMLODIPINE,amlodipine,C08CA,0,5.0,2.5,10.0
|
| 8 |
+
DRUG_METOPROLOL,metoprolol,C07AB,0,50.0,25.0,200.0
|
| 9 |
+
DRUG_DIGOXIN,digoxin,C01AA,1,0.25,0.0625,0.5
|
| 10 |
+
DRUG_FUROSEMIDE,furosemide,C03CA,0,40.0,20.0,160.0
|
| 11 |
+
DRUG_SPIRONOLACTONE,spironolactone,C03DA,0,25.0,12.5,50.0
|
| 12 |
+
DRUG_ATORVASTATIN,atorvastatin,C10AA,0,20.0,10.0,80.0
|
| 13 |
+
DRUG_SIMVASTATIN,simvastatin,C10AA,0,20.0,10.0,40.0
|
| 14 |
+
DRUG_OMEPRAZOLE,omeprazole,A02BC,0,20.0,10.0,40.0
|
| 15 |
+
DRUG_DIAZEPAM,diazepam,N05BA,1,5.0,2.0,10.0
|
| 16 |
+
DRUG_ALPRAZOLAM,alprazolam,N05BA,1,0.5,0.25,2.0
|
| 17 |
+
DRUG_AMITRIPTYLINE,amitriptyline,N06AA,1,25.0,10.0,75.0
|
| 18 |
+
DRUG_INSULIN_GLARGINE,insulin glargine,A10AE,1,20.0,10.0,60.0
|
| 19 |
+
DRUG_PREDNISONE,prednisone,H02AB,0,10.0,5.0,60.0
|
| 20 |
+
DRUG_NAPROXEN,naproxen,M01AE,1,500,250,1000
|
| 21 |
+
DRUG_IBUPROFEN,ibuprofen,M01AE,1,400,200,800
|
| 22 |
+
DRUG_CLOPIDOGREL,clopidogrel,B01AC,0,75.0,75.0,75.0
|
| 23 |
+
DRUG_ASPIRIN,aspirin,B01AC,0,81.0,81.0,325.0
|
| 24 |
+
DRUG_HYDROCHLOROTHIAZIDE,HCTZ,C03AA,0,25.0,12.5,50.0
|
| 25 |
+
DRUG_DONEPEZIL,donepezil,N06DA,0,5.0,5.0,10.0
|
| 26 |
+
DRUG_GABAPENTIN,gabapentin,N03AX,0,300,100,1200
|
| 27 |
+
DRUG_TRAMADOL,tramadol,N02AX,1,50.0,25.0,200.0
|
| 28 |
+
DRUG_FLUOXETINE,fluoxetine,N06AB,0,20.0,10.0,60.0
|
| 29 |
+
DRUG_SERTRALINE,sertraline,N06AB,0,50.0,25.0,200.0
|
| 30 |
+
DRUG_CIPROFLOXACIN,ciprofloxacin,J01MA,0,500,250,750
|
| 31 |
+
DRUG_TAMSULOSIN,tamsulosin,G04CA,0,0.4,0.4,0.8
|
| 32 |
+
DRUG_CELECOXIB,celecoxib,M01AE,0,200,100,400
|
| 33 |
+
DRUG_NORTRIPTYLINE,nortriptyline,N06AA,0,25.0,10.0,75.0
|
| 34 |
+
DRUG_LOSARTAN,losartan,C09AA,0,50.0,25.0,100.0
|
openenv-polypharmacy/data/processed/patients_polypharmacy.csv
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
episode_id,age,sex,conditions,eGFR_category,liver_function_category,medication_ids,baseline_risk_score,difficulty
|
| 2 |
+
EP_0001,72,F,HTN,moderate,normal,DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
|
| 3 |
+
EP_0002,67,M,OA;COPD;neuropathy,normal,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE,0.2833,easy
|
| 4 |
+
EP_0003,73,F,HTN;HF,normal,normal,DRUG_FUROSEMIDE;DRUG_FLUOXETINE;DRUG_TRAMADOL,0.2733,easy
|
| 5 |
+
EP_0004,74,M,CKD,mild,impaired,DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.2833,easy
|
| 6 |
+
EP_0005,76,F,OA;neuropathy;CKD,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE,0.17,easy
|
| 7 |
+
EP_0006,74,M,HTN;OA,normal,impaired,DRUG_IBUPROFEN;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
|
| 8 |
+
EP_0007,90,M,BPH;OA,moderate,normal,DRUG_WARFARIN;DRUG_NAPROXEN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN,0.225,easy
|
| 9 |
+
EP_0008,77,F,CKD;OA;depression,mild,normal,DRUG_AMITRIPTYLINE;DRUG_IBUPROFEN;DRUG_SERTRALINE;DRUG_TRAMADOL;DRUG_FUROSEMIDE,0.17,easy
|
| 10 |
+
EP_0009,67,M,COPD;GERD;BPH,mild,normal,DRUG_WARFARIN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN,0.22,easy
|
| 11 |
+
EP_0010,75,M,dementia;HTN;depression,normal,impaired,DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE,0.2833,easy
|
| 12 |
+
EP_0011,83,F,AF,moderate,normal,DRUG_ALPRAZOLAM;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_TRAMADOL,0.2275,easy
|
| 13 |
+
EP_0012,71,F,HTN;GERD;depression,normal,normal,DRUG_DIAZEPAM;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_TRAMADOL,0.348,easy
|
| 14 |
+
EP_0013,70,F,HF;HTN;AF,mild,normal,DRUG_ALPRAZOLAM;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.3033,easy
|
| 15 |
+
EP_0014,82,F,dementia,normal,normal,DRUG_DONEPEZIL;DRUG_NAPROXEN;DRUG_APIXABAN;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE,0.17,easy
|
| 16 |
+
EP_0015,84,F,dementia;neuropathy,normal,normal,DRUG_DONEPEZIL;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_CELECOXIB;DRUG_TRAMADOL,0.17,easy
|
| 17 |
+
EP_0016,83,M,HTN,normal,normal,DRUG_ALPRAZOLAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.3033,easy
|
| 18 |
+
EP_0017,83,F,CKD,severe,normal,DRUG_DIAZEPAM;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.3067,easy
|
| 19 |
+
EP_0018,70,F,CKD;HF;HTN,mild,normal,DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_ALPRAZOLAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.182,easy
|
| 20 |
+
EP_0019,84,M,DM;depression,normal,normal,DRUG_INSULIN_GLARGINE;DRUG_FLUOXETINE;DRUG_AMITRIPTYLINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL,0.434,easy
|
| 21 |
+
EP_0020,90,F,neuropathy;BPH;AF,normal,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_AMIODARONE;DRUG_TAMSULOSIN,0.2,easy
|
| 22 |
+
EP_0021,87,M,HTN;BPH;HF,normal,normal,DRUG_SPIRONOLACTONE;DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_AMLODIPINE,0.2125,easy
|
| 23 |
+
EP_0022,90,M,AF;GERD;DM,normal,impaired,DRUG_OMEPRAZOLE;DRUG_DIAZEPAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.23,easy
|
| 24 |
+
EP_0023,90,F,HF,normal,normal,DRUG_DIAZEPAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.3067,easy
|
| 25 |
+
EP_0024,71,F,OA,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_APIXABAN,0.17,easy
|
| 26 |
+
EP_0025,71,M,COPD;AF;neuropathy,mild,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_AMIODARONE;DRUG_GABAPENTIN,0.2,easy
|
| 27 |
+
EP_0026,88,M,GERD;dementia,severe,normal,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_APIXABAN;DRUG_NAPROXEN,0.2125,easy
|
| 28 |
+
EP_0027,76,M,AF,normal,normal,DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_NAPROXEN,0.43,easy
|
| 29 |
+
EP_0028,73,F,CKD,moderate,normal,DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL,0.17,easy
|
| 30 |
+
EP_0029,70,F,CKD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_TRAMADOL,0.17,easy
|
| 31 |
+
EP_0030,87,F,dementia;HF;depression,normal,normal,DRUG_DIGOXIN;DRUG_FLUOXETINE;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
|
| 32 |
+
EP_0031,69,M,HF,severe,normal,DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.426,easy
|
| 33 |
+
EP_0032,89,F,neuropathy,mild,normal,DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_PREDNISONE;DRUG_TRAMADOL,0.2125,easy
|
| 34 |
+
EP_0033,68,F,dementia,mild,impaired,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_SPIRONOLACTONE;DRUG_TRAMADOL;DRUG_ALPRAZOLAM,0.182,easy
|
| 35 |
+
EP_0034,84,F,CKD;HF;HTN,moderate,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_HYDROCHLOROTHIAZIDE,0.225,easy
|
| 36 |
+
EP_0035,74,M,HTN;DM,normal,impaired,DRUG_FLUOXETINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_GLIPIZIDE;DRUG_METOPROLOL;DRUG_TRAMADOL,0.164,easy
|
| 37 |
+
EP_0036,80,F,DM;neuropathy;HTN,severe,normal,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_AMIODARONE;DRUG_AMITRIPTYLINE,0.2,easy
|
| 38 |
+
EP_0037,78,M,HF,normal,normal,DRUG_LISINOPRIL;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.2125,easy
|
| 39 |
+
EP_0038,89,F,HTN;AF,moderate,normal,DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.2833,easy
|
| 40 |
+
EP_0039,78,F,OA;depression,moderate,normal,DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_SERTRALINE,0.205,easy
|
| 41 |
+
EP_0040,72,F,neuropathy;COPD;BPH,normal,normal,DRUG_ALPRAZOLAM;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_TAMSULOSIN,0.44,easy
|
| 42 |
+
EP_0041,89,F,AF;BPH;DM;HF;HTN,mild,normal,DRUG_GLIPIZIDE;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_INSULIN_GLARGINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN,0.1,medium
|
| 43 |
+
EP_0042,66,F,HTN;AF;CKD,moderate,normal,DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_SERTRALINE,0.173,medium
|
| 44 |
+
EP_0043,70,F,OA;HTN;dementia,moderate,normal,DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_LISINOPRIL;DRUG_METOPROLOL,0.0467,medium
|
| 45 |
+
EP_0044,77,M,HF;HTN;GERD;COPD;neuropathy,normal,normal,DRUG_OMEPRAZOLE;DRUG_AMLODIPINE;DRUG_PREDNISONE;DRUG_LISINOPRIL;DRUG_SPIRONOLACTONE;DRUG_METOPROLOL;DRUG_GABAPENTIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN,0.1422,medium
|
| 46 |
+
EP_0045,78,M,CKD;depression;dementia;GERD;OA,severe,normal,DRUG_FLUOXETINE;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE,0.167,medium
|
| 47 |
+
EP_0046,82,M,BPH;DM;CKD;dementia;HF,moderate,normal,DRUG_GLIPIZIDE;DRUG_INSULIN_GLARGINE;DRUG_METOPROLOL;DRUG_METFORMIN;DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_TAMSULOSIN;DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_LISINOPRIL,0.178,medium
|
| 48 |
+
EP_0047,83,F,depression;HTN;BPH;neuropathy;AF,normal,impaired,DRUG_SERTRALINE;DRUG_GABAPENTIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL;DRUG_LISINOPRIL;DRUG_APIXABAN;DRUG_TAMSULOSIN;DRUG_AMLODIPINE;DRUG_FUROSEMIDE,0.0,medium
|
| 49 |
+
EP_0048,85,F,AF;DM;OA,severe,impaired,DRUG_WARFARIN;DRUG_GLIPIZIDE;DRUG_INSULIN_GLARGINE;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_DIGOXIN;DRUG_METFORMIN;DRUG_IBUPROFEN;DRUG_METOPROLOL,0.268,medium
|
| 50 |
+
EP_0049,65,F,BPH;COPD;neuropathy,normal,normal,DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_PREDNISONE;DRUG_WARFARIN;DRUG_FLUOXETINE;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.2963,medium
|
| 51 |
+
EP_0050,86,M,dementia;depression;OA;neuropathy,mild,impaired,DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_SERTRALINE;DRUG_NAPROXEN;DRUG_DONEPEZIL;DRUG_IBUPROFEN;DRUG_TRAMADOL,0.1214,medium
|
| 52 |
+
EP_0051,90,M,OA;HF;HTN;DM,normal,normal,DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_METFORMIN;DRUG_NAPROXEN,0.085,medium
|
| 53 |
+
EP_0052,70,M,AF;depression;GERD,moderate,normal,DRUG_FLUOXETINE;DRUG_METOPROLOL;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE;DRUG_OMEPRAZOLE;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_DIGOXIN,0.1063,medium
|
| 54 |
+
EP_0053,65,F,HF;DM;GERD;neuropathy;BPH,moderate,impaired,DRUG_INSULIN_GLARGINE;DRUG_GLIPIZIDE;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_SPIRONOLACTONE;DRUG_AMITRIPTYLINE;DRUG_LISINOPRIL;DRUG_GABAPENTIN,0.098,medium
|
| 55 |
+
EP_0054,82,F,OA;neuropathy;AF;DM,mild,normal,DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_APIXABAN;DRUG_INSULIN_GLARGINE;DRUG_IBUPROFEN;DRUG_NAPROXEN,0.1417,medium
|
| 56 |
+
EP_0055,74,M,GERD;HTN;CKD,moderate,normal,DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_SPIRONOLACTONE;DRUG_INSULIN_GLARGINE,0.06,medium
|
| 57 |
+
EP_0056,67,F,HTN;GERD;COPD;AF,moderate,normal,DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_OMEPRAZOLE;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_AMLODIPINE,0.1222,medium
|
| 58 |
+
EP_0057,74,F,DM;HTN;BPH,normal,normal,DRUG_INSULIN_GLARGINE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_GLIPIZIDE;DRUG_METFORMIN;DRUG_FUROSEMIDE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN,0.05,medium
|
| 59 |
+
EP_0058,90,F,AF;OA;BPH,moderate,normal,DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_IBUPROFEN;DRUG_NAPROXEN;DRUG_TAMSULOSIN;DRUG_TRAMADOL;DRUG_APIXABAN;DRUG_CLOPIDOGREL,0.303,medium
|
| 60 |
+
EP_0059,85,F,BPH;HTN;depression;dementia;COPD,mild,normal,DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_SERTRALINE;DRUG_FLUOXETINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_AMITRIPTYLINE,0.0,medium
|
| 61 |
+
EP_0060,80,F,HF;CKD;neuropathy;HTN,moderate,normal,DRUG_AMLODIPINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_SPIRONOLACTONE;DRUG_GABAPENTIN;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_AMITRIPTYLINE;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_ATORVASTATIN,0.173,medium
|
| 62 |
+
EP_0061,90,M,GERD;BPH;HF;HTN;CKD,mild,normal,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_TAMSULOSIN;DRUG_FUROSEMIDE,0.1417,medium
|
| 63 |
+
EP_0062,65,M,neuropathy;COPD;GERD;BPH;AF,moderate,normal,DRUG_PREDNISONE;DRUG_TAMSULOSIN;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_OMEPRAZOLE;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_WARFARIN;DRUG_APIXABAN,0.0722,medium
|
| 64 |
+
EP_0063,76,M,depression;COPD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE;DRUG_FLUOXETINE;DRUG_SERTRALINE;DRUG_GABAPENTIN,0.2386,medium
|
| 65 |
+
EP_0064,88,M,BPH;GERD;COPD,mild,normal,DRUG_OMEPRAZOLE;DRUG_PREDNISONE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_SIMVASTATIN;DRUG_AMLODIPINE;DRUG_ATORVASTATIN;DRUG_ALPRAZOLAM,0.0867,medium
|
| 66 |
+
EP_0065,75,M,HTN;HF;AF,mild,normal,DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_APIXABAN;DRUG_SPIRONOLACTONE;DRUG_DIGOXIN,0.1786,medium
|
| 67 |
+
EP_0066,66,M,HF;dementia;GERD;OA;DM,moderate,normal,DRUG_INSULIN_GLARGINE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_METFORMIN;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE,0.0563,medium
|
| 68 |
+
EP_0067,70,F,CKD;HTN;AF;HF,moderate,normal,DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_AMLODIPINE,0.0,medium
|
| 69 |
+
EP_0068,85,M,depression;dementia;neuropathy;HF,normal,impaired,DRUG_FLUOXETINE;DRUG_SERTRALINE;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_LISINOPRIL;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_FUROSEMIDE,0.173,medium
|
| 70 |
+
EP_0069,74,F,OA;CKD;AF,mild,normal,DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_AMLODIPINE;DRUG_APIXABAN;DRUG_METOPROLOL;DRUG_TRAMADOL;DRUG_DIGOXIN,0.1889,medium
|
| 71 |
+
EP_0070,75,F,dementia;GERD;COPD;OA,mild,normal,DRUG_DONEPEZIL;DRUG_PREDNISONE;DRUG_OMEPRAZOLE;DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_GABAPENTIN,0.0,medium
|
| 72 |
+
EP_0071,68,M,BPH;DM;COPD;neuropathy,normal,normal,DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_METFORMIN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_PREDNISONE;DRUG_GLIPIZIDE,0.0714,medium
|
| 73 |
+
EP_0072,92,F,CKD;BPH;COPD;AF,normal,normal,DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_APIXABAN,0.1214,medium
|
| 74 |
+
EP_0073,88,F,OA;GERD;HTN;depression,mild,normal,DRUG_AMITRIPTYLINE;DRUG_OMEPRAZOLE;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_SERTRALINE,0.0525,medium
|
| 75 |
+
EP_0074,80,F,neuropathy;OA;CKD;depression,mild,normal,DRUG_AMLODIPINE;DRUG_SERTRALINE;DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN,0.1214,medium
|
| 76 |
+
EP_0075,68,F,dementia;AF;COPD;HTN;neuropathy,mild,normal,DRUG_GABAPENTIN;DRUG_AMLODIPINE;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_DONEPEZIL;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_LISINOPRIL,0.0944,medium
|
| 77 |
+
EP_0076,71,M,HF;DM;dementia,severe,normal,DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_INSULIN_GLARGINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_METFORMIN;DRUG_GLIPIZIDE;DRUG_FUROSEMIDE,0.2287,medium
|
| 78 |
+
EP_0077,75,F,AF;BPH;dementia,mild,impaired,DRUG_METOPROLOL;DRUG_TAMSULOSIN;DRUG_APIXABAN;DRUG_DONEPEZIL;DRUG_DIGOXIN;DRUG_WARFARIN;DRUG_SPIRONOLACTONE,0.1143,medium
|
| 79 |
+
EP_0078,81,F,OA;depression;DM;neuropathy;CKD,normal,normal,DRUG_GLIPIZIDE;DRUG_NAPROXEN;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_SERTRALINE;DRUG_INSULIN_GLARGINE;DRUG_IBUPROFEN,0.0714,medium
|
| 80 |
+
EP_0079,74,F,DM;OA;GERD;CKD,mild,impaired,DRUG_GLIPIZIDE;DRUG_AMLODIPINE;DRUG_INSULIN_GLARGINE;DRUG_METFORMIN;DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_NAPROXEN,0.0714,medium
|
| 81 |
+
EP_0080,72,M,GERD;HF;OA;CKD,normal,normal,DRUG_AMLODIPINE;DRUG_TRAMADOL;DRUG_LISINOPRIL;DRUG_OMEPRAZOLE;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_IBUPROFEN,0.1063,medium
|
| 82 |
+
EP_0081,84,M,neuropathy;CKD;depression;OA,normal,impaired,DRUG_FLUOXETINE;DRUG_AMLODIPINE;DRUG_IBUPROFEN;DRUG_NAPROXEN;DRUG_SERTRALINE;DRUG_GABAPENTIN;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_SIMVASTATIN;DRUG_SPIRONOLACTONE;DRUG_DIGOXIN;DRUG_WARFARIN,0.395,hard
|
| 83 |
+
EP_0082,75,M,OA;COPD;neuropathy;CKD;GERD;HTN;depression,severe,impaired,DRUG_SERTRALINE;DRUG_FUROSEMIDE;DRUG_METOPROLOL;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_OMEPRAZOLE;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_PREDNISONE;DRUG_DIGOXIN,0.2293,hard
|
| 84 |
+
EP_0083,82,F,DM;dementia;OA;HF;neuropathy;COPD,moderate,normal,DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_PREDNISONE;DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_WARFARIN,0.2931,hard
|
| 85 |
+
EP_0084,80,F,CKD;neuropathy;COPD;BPH;dementia;HTN;OA,moderate,normal,DRUG_GABAPENTIN;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_PREDNISONE;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_TAMSULOSIN;DRUG_DIGOXIN;DRUG_WARFARIN,0.2767,hard
|
| 86 |
+
EP_0085,79,F,OA;DM;CKD;GERD;BPH;HF;neuropathy,severe,normal,DRUG_OMEPRAZOLE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE;DRUG_METFORMIN;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_FUROSEMIDE;DRUG_GABAPENTIN;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.2008,hard
|
| 87 |
+
EP_0086,82,M,AF;DM;GERD;COPD;OA,moderate,impaired,DRUG_PREDNISONE;DRUG_METFORMIN;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_OMEPRAZOLE;DRUG_INSULIN_GLARGINE;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_GLIPIZIDE;DRUG_METOPROLOL;DRUG_TRAMADOL;DRUG_IBUPROFEN;DRUG_LOSARTAN,0.3057,hard
|
| 88 |
+
EP_0087,90,M,HTN;GERD;DM;AF;CKD,mild,impaired,DRUG_APIXABAN;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_OMEPRAZOLE;DRUG_WARFARIN;DRUG_METOPROLOL;DRUG_INSULIN_GLARGINE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_HYDROCHLOROTHIAZIDE,0.05,hard
|
| 89 |
+
EP_0088,86,F,HF;AF;COPD;HTN;OA;GERD,normal,impaired,DRUG_PREDNISONE;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_APIXABAN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_SPIRONOLACTONE;DRUG_NAPROXEN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_TRAMADOL;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE,0.3687,hard
|
| 90 |
+
EP_0089,75,M,DM;CKD;HTN;HF;BPH;neuropathy;GERD,mild,normal,DRUG_HYDROCHLOROTHIAZIDE;DRUG_AMLODIPINE;DRUG_METFORMIN;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_LISINOPRIL;DRUG_AMITRIPTYLINE;DRUG_GLIPIZIDE;DRUG_GABAPENTIN;DRUG_TAMSULOSIN;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.0417,hard
|
| 91 |
+
EP_0090,87,M,AF;depression;DM;COPD;OA,mild,normal,DRUG_NAPROXEN;DRUG_FLUOXETINE;DRUG_APIXABAN;DRUG_SERTRALINE;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE;DRUG_TRAMADOL;DRUG_PREDNISONE;DRUG_GLIPIZIDE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_METFORMIN;DRUG_METOPROLOL;DRUG_AMITRIPTYLINE,0.4267,hard
|
| 92 |
+
EP_0091,83,M,OA;dementia;GERD;depression;DM;HF,severe,normal,DRUG_GLIPIZIDE;DRUG_DIGOXIN;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_METFORMIN;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_WARFARIN,0.2843,hard
|
| 93 |
+
EP_0092,78,F,CKD;OA;AF;COPD;depression,severe,normal,DRUG_METOPROLOL;DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE;DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_IBUPROFEN;DRUG_DIGOXIN;DRUG_WARFARIN;DRUG_FUROSEMIDE;DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_PREDNISONE,0.4536,hard
|
| 94 |
+
EP_0093,93,F,HTN;DM;BPH;OA;dementia,severe,impaired,DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_METFORMIN;DRUG_FUROSEMIDE;DRUG_INSULIN_GLARGINE;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_METOPROLOL;DRUG_GLIPIZIDE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_DIGOXIN,0.1125,hard
|
| 95 |
+
EP_0094,94,F,BPH;GERD;COPD;HF,moderate,impaired,DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_TAMSULOSIN;DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_WARFARIN,0.198,hard
|
| 96 |
+
EP_0095,90,M,HF;neuropathy;COPD;BPH;dementia;DM;CKD,normal,normal,DRUG_GABAPENTIN;DRUG_METFORMIN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_PREDNISONE;DRUG_SPIRONOLACTONE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_SERTRALINE,0.1487,hard
|
| 97 |
+
EP_0096,85,F,DM;COPD;HTN;CKD;depression;dementia,severe,impaired,DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_PREDNISONE;DRUG_METOPROLOL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_DONEPEZIL;DRUG_DIGOXIN,0.0773,hard
|
| 98 |
+
EP_0097,70,M,BPH;COPD;neuropathy;CKD;GERD;depression,severe,normal,DRUG_PREDNISONE;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_GABAPENTIN;DRUG_TAMSULOSIN;DRUG_FUROSEMIDE;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_IBUPROFEN;DRUG_LOSARTAN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_WARFARIN,0.145,hard
|
| 99 |
+
EP_0098,81,F,COPD;depression;GERD;BPH;OA,severe,impaired,DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_NAPROXEN;DRUG_SERTRALINE;DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_TAMSULOSIN;DRUG_PREDNISONE;DRUG_AMITRIPTYLINE;DRUG_NORTRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_LOSARTAN;DRUG_HYDROCHLOROTHIAZIDE,0.1447,hard
|
| 100 |
+
EP_0099,91,M,neuropathy;OA;AF;CKD,mild,normal,DRUG_TRAMADOL;DRUG_APIXABAN;DRUG_WARFARIN;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_SIMVASTATIN;DRUG_FLUOXETINE;DRUG_INSULIN_GLARGINE,0.4271,hard
|
| 101 |
+
EP_0100,90,F,DM;OA;HTN;HF,mild,impaired,DRUG_INSULIN_GLARGINE;DRUG_SPIRONOLACTONE;DRUG_METFORMIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL;DRUG_AMLODIPINE;DRUG_DIGOXIN;DRUG_GLIPIZIDE;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE,0.2333,hard
|
| 102 |
+
EP_0101,88,M,BPH;neuropathy;dementia;OA;CKD;DM;HF,severe,impaired,DRUG_AMLODIPINE;DRUG_TAMSULOSIN;DRUG_TRAMADOL;DRUG_GLIPIZIDE;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE;DRUG_LISINOPRIL;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_METFORMIN;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_METOPROLOL;DRUG_GABAPENTIN;DRUG_NAPROXEN,0.2333,hard
|
| 103 |
+
EP_0102,74,M,BPH;HF;dementia;CKD;DM;GERD,moderate,normal,DRUG_OMEPRAZOLE;DRUG_LISINOPRIL;DRUG_INSULIN_GLARGINE;DRUG_METOPROLOL;DRUG_FUROSEMIDE;DRUG_GLIPIZIDE;DRUG_DONEPEZIL;DRUG_SPIRONOLACTONE;DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_METFORMIN;DRUG_TAMSULOSIN;DRUG_WARFARIN,0.1715,hard
|
| 104 |
+
EP_0103,70,F,AF;GERD;depression;CKD,mild,impaired,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_APIXABAN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_AMITRIPTYLINE;DRUG_LOSARTAN;DRUG_GLIPIZIDE;DRUG_DIAZEPAM;DRUG_TRAMADOL;DRUG_HYDROCHLOROTHIAZIDE,0.2593,hard
|
| 105 |
+
EP_0104,84,F,OA;DM;dementia;AF;GERD;COPD;BPH,mild,normal,DRUG_METOPROLOL;DRUG_DONEPEZIL;DRUG_METFORMIN;DRUG_GABAPENTIN;DRUG_APIXABAN;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN;DRUG_NAPROXEN;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_IBUPROFEN;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN;DRUG_WARFARIN,0.2521,hard
|
| 106 |
+
EP_0105,88,F,neuropathy;BPH;HTN;COPD;DM,severe,impaired,DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_PREDNISONE;DRUG_GABAPENTIN;DRUG_INSULIN_GLARGINE;DRUG_METFORMIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_WARFARIN,0.0208,hard
|
| 107 |
+
EP_0106,73,M,HTN;DM;CKD;OA;depression,normal,normal,DRUG_LISINOPRIL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE;DRUG_SERTRALINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METFORMIN;DRUG_FLUOXETINE;DRUG_INSULIN_GLARGINE;DRUG_AMLODIPINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_WARFARIN,0.3443,hard
|
| 108 |
+
EP_0107,82,M,HTN;GERD;BPH;depression;AF,mild,impaired,DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_SERTRALINE;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_TAMSULOSIN;DRUG_METOPROLOL;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_DIGOXIN;DRUG_APIXABAN;DRUG_ATORVASTATIN;DRUG_CLOPIDOGREL,0.1167,hard
|
| 109 |
+
EP_0108,72,F,DM;dementia;GERD;BPH;neuropathy;OA;depression,severe,normal,DRUG_FLUOXETINE;DRUG_GABAPENTIN;DRUG_TAMSULOSIN;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_METFORMIN;DRUG_SERTRALINE;DRUG_IBUPROFEN;DRUG_INSULIN_GLARGINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL;DRUG_NORTRIPTYLINE;DRUG_WARFARIN,0.2933,hard
|
| 110 |
+
EP_0109,91,M,COPD;dementia;HF;OA;HTN;DM,mild,normal,DRUG_DONEPEZIL;DRUG_GLIPIZIDE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE;DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_NAPROXEN;DRUG_METFORMIN;DRUG_WARFARIN,0.2864,hard
|
| 111 |
+
EP_0110,84,M,HF;GERD;dementia;CKD;COPD,severe,impaired,DRUG_DONEPEZIL;DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_PREDNISONE;DRUG_OMEPRAZOLE;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_AMITRIPTYLINE;DRUG_ASPIRIN;DRUG_NORTRIPTYLINE;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.1807,hard
|
| 112 |
+
EP_0111,91,F,dementia;CKD;DM;AF;BPH,normal,impaired,DRUG_GLIPIZIDE;DRUG_DONEPEZIL;DRUG_TAMSULOSIN;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE;DRUG_CELECOXIB;DRUG_AMITRIPTYLINE;DRUG_ATORVASTATIN;DRUG_IBUPROFEN,0.1487,hard
|
| 113 |
+
EP_0112,87,F,AF;HF;CKD;neuropathy;HTN;depression,moderate,impaired,DRUG_HYDROCHLOROTHIAZIDE;DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_FLUOXETINE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE;DRUG_METOPROLOL;DRUG_AMITRIPTYLINE;DRUG_APIXABAN;DRUG_SERTRALINE;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE,0.1308,hard
|
| 114 |
+
EP_0113,92,M,OA;HF;COPD;dementia;neuropathy;CKD,moderate,normal,DRUG_AMLODIPINE;DRUG_DONEPEZIL;DRUG_LISINOPRIL;DRUG_GABAPENTIN;DRUG_METOPROLOL;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_AMITRIPTYLINE;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_FUROSEMIDE;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.3023,hard
|
| 115 |
+
EP_0114,72,M,depression;COPD;neuropathy;dementia;AF,moderate,normal,DRUG_DIGOXIN;DRUG_APIXABAN;DRUG_SERTRALINE;DRUG_METOPROLOL;DRUG_FLUOXETINE;DRUG_DONEPEZIL;DRUG_AMITRIPTYLINE;DRUG_PREDNISONE;DRUG_WARFARIN;DRUG_GABAPENTIN;DRUG_LISINOPRIL;DRUG_NORTRIPTYLINE;DRUG_AMLODIPINE,0.0846,hard
|
| 116 |
+
EP_0115,75,M,HTN;OA;dementia;HF;depression;CKD;AF,mild,normal,DRUG_WARFARIN;DRUG_SERTRALINE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE;DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_NAPROXEN;DRUG_GABAPENTIN;DRUG_IBUPROFEN;DRUG_AMITRIPTYLINE;DRUG_DIGOXIN,0.3233,hard
|
| 117 |
+
EP_0116,76,F,OA;depression;neuropathy;HF,severe,impaired,DRUG_FLUOXETINE;DRUG_NAPROXEN;DRUG_FUROSEMIDE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_LISINOPRIL;DRUG_GABAPENTIN;DRUG_DIGOXIN;DRUG_IBUPROFEN;DRUG_SERTRALINE;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.4321,hard
|
| 118 |
+
EP_0117,85,M,depression;GERD;neuropathy;HTN,normal,normal,DRUG_GABAPENTIN;DRUG_SERTRALINE;DRUG_OMEPRAZOLE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_FUROSEMIDE;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_METOPROLOL;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_SPIRONOLACTONE;DRUG_IBUPROFEN;DRUG_NAPROXEN;DRUG_WARFARIN;DRUG_INSULIN_GLARGINE,0.2087,hard
|
| 119 |
+
EP_0118,70,F,DM;depression;HTN;HF;AF;neuropathy;CKD,severe,impaired,DRUG_FUROSEMIDE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METFORMIN;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE;DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_GLIPIZIDE;DRUG_WARFARIN;DRUG_METOPROLOL;DRUG_INSULIN_GLARGINE;DRUG_DIGOXIN,0.1833,hard
|
| 120 |
+
EP_0119,86,F,AF;HTN;HF;OA;dementia,normal,impaired,DRUG_APIXABAN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_IBUPROFEN;DRUG_SPIRONOLACTONE;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_DONEPEZIL;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_DIGOXIN;DRUG_INSULIN_GLARGINE,0.2364,hard
|
| 121 |
+
EP_0120,94,F,HF;COPD;dementia;HTN;CKD,mild,normal,DRUG_AMLODIPINE;DRUG_METOPROLOL;DRUG_SPIRONOLACTONE;DRUG_DONEPEZIL;DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN;DRUG_LISINOPRIL;DRUG_PREDNISONE;DRUG_FUROSEMIDE;DRUG_AMITRIPTYLINE;DRUG_INSULIN_GLARGINE;DRUG_WARFARIN,0.165,hard
|
openenv-polypharmacy/docker-compose.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
backend:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: backend/Dockerfile
|
| 8 |
+
container_name: polypharmacy-backend
|
| 9 |
+
env_file:
|
| 10 |
+
- .env
|
| 11 |
+
ports:
|
| 12 |
+
- "7860:7860"
|
| 13 |
+
volumes:
|
| 14 |
+
- ./backend/src:/app/backend/src
|
| 15 |
+
- ./data:/app/data
|
| 16 |
+
- ./scripts:/app/scripts
|
| 17 |
+
- ./backend:/app/backend
|
| 18 |
+
healthcheck:
|
| 19 |
+
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
|
| 20 |
+
interval: 20s
|
| 21 |
+
timeout: 5s
|
| 22 |
+
retries: 5
|
| 23 |
+
|
| 24 |
+
frontend:
|
| 25 |
+
build:
|
| 26 |
+
context: .
|
| 27 |
+
dockerfile: frontend/Dockerfile
|
| 28 |
+
container_name: polypharmacy-frontend
|
| 29 |
+
depends_on:
|
| 30 |
+
- backend
|
| 31 |
+
ports:
|
| 32 |
+
- "5173:5173"
|
| 33 |
+
volumes:
|
| 34 |
+
- ./frontend:/app
|
| 35 |
+
- /app/node_modules
|
openenv-polypharmacy/frontend/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY frontend/package*.json ./
|
| 6 |
+
RUN npm ci
|
| 7 |
+
|
| 8 |
+
COPY frontend/ ./
|
| 9 |
+
|
| 10 |
+
EXPOSE 5173
|
| 11 |
+
|
| 12 |
+
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
openenv-polypharmacy/frontend/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Polypharmacy Control Center</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
openenv-polypharmacy/frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1677 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "polypharmacy-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "polypharmacy-frontend",
|
| 9 |
+
"version": "0.1.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"react": "^18.3.1",
|
| 12 |
+
"react-dom": "^18.3.1"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 16 |
+
"vite": "^5.4.2"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"node_modules/@babel/code-frame": {
|
| 20 |
+
"version": "7.29.0",
|
| 21 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 22 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 23 |
+
"dev": true,
|
| 24 |
+
"license": "MIT",
|
| 25 |
+
"dependencies": {
|
| 26 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 27 |
+
"js-tokens": "^4.0.0",
|
| 28 |
+
"picocolors": "^1.1.1"
|
| 29 |
+
},
|
| 30 |
+
"engines": {
|
| 31 |
+
"node": ">=6.9.0"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"node_modules/@babel/compat-data": {
|
| 35 |
+
"version": "7.29.0",
|
| 36 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
| 37 |
+
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
| 38 |
+
"dev": true,
|
| 39 |
+
"license": "MIT",
|
| 40 |
+
"engines": {
|
| 41 |
+
"node": ">=6.9.0"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"node_modules/@babel/core": {
|
| 45 |
+
"version": "7.29.0",
|
| 46 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
| 47 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 48 |
+
"dev": true,
|
| 49 |
+
"license": "MIT",
|
| 50 |
+
"dependencies": {
|
| 51 |
+
"@babel/code-frame": "^7.29.0",
|
| 52 |
+
"@babel/generator": "^7.29.0",
|
| 53 |
+
"@babel/helper-compilation-targets": "^7.28.6",
|
| 54 |
+
"@babel/helper-module-transforms": "^7.28.6",
|
| 55 |
+
"@babel/helpers": "^7.28.6",
|
| 56 |
+
"@babel/parser": "^7.29.0",
|
| 57 |
+
"@babel/template": "^7.28.6",
|
| 58 |
+
"@babel/traverse": "^7.29.0",
|
| 59 |
+
"@babel/types": "^7.29.0",
|
| 60 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 61 |
+
"convert-source-map": "^2.0.0",
|
| 62 |
+
"debug": "^4.1.0",
|
| 63 |
+
"gensync": "^1.0.0-beta.2",
|
| 64 |
+
"json5": "^2.2.3",
|
| 65 |
+
"semver": "^6.3.1"
|
| 66 |
+
},
|
| 67 |
+
"engines": {
|
| 68 |
+
"node": ">=6.9.0"
|
| 69 |
+
},
|
| 70 |
+
"funding": {
|
| 71 |
+
"type": "opencollective",
|
| 72 |
+
"url": "https://opencollective.com/babel"
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"node_modules/@babel/generator": {
|
| 76 |
+
"version": "7.29.1",
|
| 77 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 78 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 79 |
+
"dev": true,
|
| 80 |
+
"license": "MIT",
|
| 81 |
+
"dependencies": {
|
| 82 |
+
"@babel/parser": "^7.29.0",
|
| 83 |
+
"@babel/types": "^7.29.0",
|
| 84 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 85 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 86 |
+
"jsesc": "^3.0.2"
|
| 87 |
+
},
|
| 88 |
+
"engines": {
|
| 89 |
+
"node": ">=6.9.0"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 93 |
+
"version": "7.28.6",
|
| 94 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
| 95 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 96 |
+
"dev": true,
|
| 97 |
+
"license": "MIT",
|
| 98 |
+
"dependencies": {
|
| 99 |
+
"@babel/compat-data": "^7.28.6",
|
| 100 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 101 |
+
"browserslist": "^4.24.0",
|
| 102 |
+
"lru-cache": "^5.1.1",
|
| 103 |
+
"semver": "^6.3.1"
|
| 104 |
+
},
|
| 105 |
+
"engines": {
|
| 106 |
+
"node": ">=6.9.0"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"node_modules/@babel/helper-globals": {
|
| 110 |
+
"version": "7.28.0",
|
| 111 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 112 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 113 |
+
"dev": true,
|
| 114 |
+
"license": "MIT",
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">=6.9.0"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/@babel/helper-module-imports": {
|
| 120 |
+
"version": "7.28.6",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 122 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 123 |
+
"dev": true,
|
| 124 |
+
"license": "MIT",
|
| 125 |
+
"dependencies": {
|
| 126 |
+
"@babel/traverse": "^7.28.6",
|
| 127 |
+
"@babel/types": "^7.28.6"
|
| 128 |
+
},
|
| 129 |
+
"engines": {
|
| 130 |
+
"node": ">=6.9.0"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 134 |
+
"version": "7.28.6",
|
| 135 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
| 136 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 137 |
+
"dev": true,
|
| 138 |
+
"license": "MIT",
|
| 139 |
+
"dependencies": {
|
| 140 |
+
"@babel/helper-module-imports": "^7.28.6",
|
| 141 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 142 |
+
"@babel/traverse": "^7.28.6"
|
| 143 |
+
},
|
| 144 |
+
"engines": {
|
| 145 |
+
"node": ">=6.9.0"
|
| 146 |
+
},
|
| 147 |
+
"peerDependencies": {
|
| 148 |
+
"@babel/core": "^7.0.0"
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 152 |
+
"version": "7.28.6",
|
| 153 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
| 154 |
+
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
| 155 |
+
"dev": true,
|
| 156 |
+
"license": "MIT",
|
| 157 |
+
"engines": {
|
| 158 |
+
"node": ">=6.9.0"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"node_modules/@babel/helper-string-parser": {
|
| 162 |
+
"version": "7.27.1",
|
| 163 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 164 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 165 |
+
"dev": true,
|
| 166 |
+
"license": "MIT",
|
| 167 |
+
"engines": {
|
| 168 |
+
"node": ">=6.9.0"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 172 |
+
"version": "7.28.5",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 174 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 175 |
+
"dev": true,
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"engines": {
|
| 178 |
+
"node": ">=6.9.0"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"node_modules/@babel/helper-validator-option": {
|
| 182 |
+
"version": "7.27.1",
|
| 183 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 184 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 185 |
+
"dev": true,
|
| 186 |
+
"license": "MIT",
|
| 187 |
+
"engines": {
|
| 188 |
+
"node": ">=6.9.0"
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
"node_modules/@babel/helpers": {
|
| 192 |
+
"version": "7.29.2",
|
| 193 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
|
| 194 |
+
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
| 195 |
+
"dev": true,
|
| 196 |
+
"license": "MIT",
|
| 197 |
+
"dependencies": {
|
| 198 |
+
"@babel/template": "^7.28.6",
|
| 199 |
+
"@babel/types": "^7.29.0"
|
| 200 |
+
},
|
| 201 |
+
"engines": {
|
| 202 |
+
"node": ">=6.9.0"
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"node_modules/@babel/parser": {
|
| 206 |
+
"version": "7.29.2",
|
| 207 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
| 208 |
+
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
| 209 |
+
"dev": true,
|
| 210 |
+
"license": "MIT",
|
| 211 |
+
"dependencies": {
|
| 212 |
+
"@babel/types": "^7.29.0"
|
| 213 |
+
},
|
| 214 |
+
"bin": {
|
| 215 |
+
"parser": "bin/babel-parser.js"
|
| 216 |
+
},
|
| 217 |
+
"engines": {
|
| 218 |
+
"node": ">=6.0.0"
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 222 |
+
"version": "7.27.1",
|
| 223 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 224 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 225 |
+
"dev": true,
|
| 226 |
+
"license": "MIT",
|
| 227 |
+
"dependencies": {
|
| 228 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 229 |
+
},
|
| 230 |
+
"engines": {
|
| 231 |
+
"node": ">=6.9.0"
|
| 232 |
+
},
|
| 233 |
+
"peerDependencies": {
|
| 234 |
+
"@babel/core": "^7.0.0-0"
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 238 |
+
"version": "7.27.1",
|
| 239 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 240 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 241 |
+
"dev": true,
|
| 242 |
+
"license": "MIT",
|
| 243 |
+
"dependencies": {
|
| 244 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 245 |
+
},
|
| 246 |
+
"engines": {
|
| 247 |
+
"node": ">=6.9.0"
|
| 248 |
+
},
|
| 249 |
+
"peerDependencies": {
|
| 250 |
+
"@babel/core": "^7.0.0-0"
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"node_modules/@babel/template": {
|
| 254 |
+
"version": "7.28.6",
|
| 255 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 256 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 257 |
+
"dev": true,
|
| 258 |
+
"license": "MIT",
|
| 259 |
+
"dependencies": {
|
| 260 |
+
"@babel/code-frame": "^7.28.6",
|
| 261 |
+
"@babel/parser": "^7.28.6",
|
| 262 |
+
"@babel/types": "^7.28.6"
|
| 263 |
+
},
|
| 264 |
+
"engines": {
|
| 265 |
+
"node": ">=6.9.0"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/@babel/traverse": {
|
| 269 |
+
"version": "7.29.0",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 271 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 272 |
+
"dev": true,
|
| 273 |
+
"license": "MIT",
|
| 274 |
+
"dependencies": {
|
| 275 |
+
"@babel/code-frame": "^7.29.0",
|
| 276 |
+
"@babel/generator": "^7.29.0",
|
| 277 |
+
"@babel/helper-globals": "^7.28.0",
|
| 278 |
+
"@babel/parser": "^7.29.0",
|
| 279 |
+
"@babel/template": "^7.28.6",
|
| 280 |
+
"@babel/types": "^7.29.0",
|
| 281 |
+
"debug": "^4.3.1"
|
| 282 |
+
},
|
| 283 |
+
"engines": {
|
| 284 |
+
"node": ">=6.9.0"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
"node_modules/@babel/types": {
|
| 288 |
+
"version": "7.29.0",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 290 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 291 |
+
"dev": true,
|
| 292 |
+
"license": "MIT",
|
| 293 |
+
"dependencies": {
|
| 294 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 295 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 296 |
+
},
|
| 297 |
+
"engines": {
|
| 298 |
+
"node": ">=6.9.0"
|
| 299 |
+
}
|
| 300 |
+
},
|
| 301 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 302 |
+
"version": "0.21.5",
|
| 303 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 304 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 305 |
+
"cpu": [
|
| 306 |
+
"ppc64"
|
| 307 |
+
],
|
| 308 |
+
"dev": true,
|
| 309 |
+
"license": "MIT",
|
| 310 |
+
"optional": true,
|
| 311 |
+
"os": [
|
| 312 |
+
"aix"
|
| 313 |
+
],
|
| 314 |
+
"engines": {
|
| 315 |
+
"node": ">=12"
|
| 316 |
+
}
|
| 317 |
+
},
|
| 318 |
+
"node_modules/@esbuild/android-arm": {
|
| 319 |
+
"version": "0.21.5",
|
| 320 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 321 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 322 |
+
"cpu": [
|
| 323 |
+
"arm"
|
| 324 |
+
],
|
| 325 |
+
"dev": true,
|
| 326 |
+
"license": "MIT",
|
| 327 |
+
"optional": true,
|
| 328 |
+
"os": [
|
| 329 |
+
"android"
|
| 330 |
+
],
|
| 331 |
+
"engines": {
|
| 332 |
+
"node": ">=12"
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"node_modules/@esbuild/android-arm64": {
|
| 336 |
+
"version": "0.21.5",
|
| 337 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 338 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 339 |
+
"cpu": [
|
| 340 |
+
"arm64"
|
| 341 |
+
],
|
| 342 |
+
"dev": true,
|
| 343 |
+
"license": "MIT",
|
| 344 |
+
"optional": true,
|
| 345 |
+
"os": [
|
| 346 |
+
"android"
|
| 347 |
+
],
|
| 348 |
+
"engines": {
|
| 349 |
+
"node": ">=12"
|
| 350 |
+
}
|
| 351 |
+
},
|
| 352 |
+
"node_modules/@esbuild/android-x64": {
|
| 353 |
+
"version": "0.21.5",
|
| 354 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 355 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 356 |
+
"cpu": [
|
| 357 |
+
"x64"
|
| 358 |
+
],
|
| 359 |
+
"dev": true,
|
| 360 |
+
"license": "MIT",
|
| 361 |
+
"optional": true,
|
| 362 |
+
"os": [
|
| 363 |
+
"android"
|
| 364 |
+
],
|
| 365 |
+
"engines": {
|
| 366 |
+
"node": ">=12"
|
| 367 |
+
}
|
| 368 |
+
},
|
| 369 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 370 |
+
"version": "0.21.5",
|
| 371 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 372 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 373 |
+
"cpu": [
|
| 374 |
+
"arm64"
|
| 375 |
+
],
|
| 376 |
+
"dev": true,
|
| 377 |
+
"license": "MIT",
|
| 378 |
+
"optional": true,
|
| 379 |
+
"os": [
|
| 380 |
+
"darwin"
|
| 381 |
+
],
|
| 382 |
+
"engines": {
|
| 383 |
+
"node": ">=12"
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 387 |
+
"version": "0.21.5",
|
| 388 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 389 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 390 |
+
"cpu": [
|
| 391 |
+
"x64"
|
| 392 |
+
],
|
| 393 |
+
"dev": true,
|
| 394 |
+
"license": "MIT",
|
| 395 |
+
"optional": true,
|
| 396 |
+
"os": [
|
| 397 |
+
"darwin"
|
| 398 |
+
],
|
| 399 |
+
"engines": {
|
| 400 |
+
"node": ">=12"
|
| 401 |
+
}
|
| 402 |
+
},
|
| 403 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 404 |
+
"version": "0.21.5",
|
| 405 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 406 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 407 |
+
"cpu": [
|
| 408 |
+
"arm64"
|
| 409 |
+
],
|
| 410 |
+
"dev": true,
|
| 411 |
+
"license": "MIT",
|
| 412 |
+
"optional": true,
|
| 413 |
+
"os": [
|
| 414 |
+
"freebsd"
|
| 415 |
+
],
|
| 416 |
+
"engines": {
|
| 417 |
+
"node": ">=12"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 421 |
+
"version": "0.21.5",
|
| 422 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 423 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 424 |
+
"cpu": [
|
| 425 |
+
"x64"
|
| 426 |
+
],
|
| 427 |
+
"dev": true,
|
| 428 |
+
"license": "MIT",
|
| 429 |
+
"optional": true,
|
| 430 |
+
"os": [
|
| 431 |
+
"freebsd"
|
| 432 |
+
],
|
| 433 |
+
"engines": {
|
| 434 |
+
"node": ">=12"
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"node_modules/@esbuild/linux-arm": {
|
| 438 |
+
"version": "0.21.5",
|
| 439 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 440 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 441 |
+
"cpu": [
|
| 442 |
+
"arm"
|
| 443 |
+
],
|
| 444 |
+
"dev": true,
|
| 445 |
+
"license": "MIT",
|
| 446 |
+
"optional": true,
|
| 447 |
+
"os": [
|
| 448 |
+
"linux"
|
| 449 |
+
],
|
| 450 |
+
"engines": {
|
| 451 |
+
"node": ">=12"
|
| 452 |
+
}
|
| 453 |
+
},
|
| 454 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 455 |
+
"version": "0.21.5",
|
| 456 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 457 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 458 |
+
"cpu": [
|
| 459 |
+
"arm64"
|
| 460 |
+
],
|
| 461 |
+
"dev": true,
|
| 462 |
+
"license": "MIT",
|
| 463 |
+
"optional": true,
|
| 464 |
+
"os": [
|
| 465 |
+
"linux"
|
| 466 |
+
],
|
| 467 |
+
"engines": {
|
| 468 |
+
"node": ">=12"
|
| 469 |
+
}
|
| 470 |
+
},
|
| 471 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 472 |
+
"version": "0.21.5",
|
| 473 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 474 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 475 |
+
"cpu": [
|
| 476 |
+
"ia32"
|
| 477 |
+
],
|
| 478 |
+
"dev": true,
|
| 479 |
+
"license": "MIT",
|
| 480 |
+
"optional": true,
|
| 481 |
+
"os": [
|
| 482 |
+
"linux"
|
| 483 |
+
],
|
| 484 |
+
"engines": {
|
| 485 |
+
"node": ">=12"
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 489 |
+
"version": "0.21.5",
|
| 490 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 491 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 492 |
+
"cpu": [
|
| 493 |
+
"loong64"
|
| 494 |
+
],
|
| 495 |
+
"dev": true,
|
| 496 |
+
"license": "MIT",
|
| 497 |
+
"optional": true,
|
| 498 |
+
"os": [
|
| 499 |
+
"linux"
|
| 500 |
+
],
|
| 501 |
+
"engines": {
|
| 502 |
+
"node": ">=12"
|
| 503 |
+
}
|
| 504 |
+
},
|
| 505 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 506 |
+
"version": "0.21.5",
|
| 507 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 508 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 509 |
+
"cpu": [
|
| 510 |
+
"mips64el"
|
| 511 |
+
],
|
| 512 |
+
"dev": true,
|
| 513 |
+
"license": "MIT",
|
| 514 |
+
"optional": true,
|
| 515 |
+
"os": [
|
| 516 |
+
"linux"
|
| 517 |
+
],
|
| 518 |
+
"engines": {
|
| 519 |
+
"node": ">=12"
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 523 |
+
"version": "0.21.5",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 525 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 526 |
+
"cpu": [
|
| 527 |
+
"ppc64"
|
| 528 |
+
],
|
| 529 |
+
"dev": true,
|
| 530 |
+
"license": "MIT",
|
| 531 |
+
"optional": true,
|
| 532 |
+
"os": [
|
| 533 |
+
"linux"
|
| 534 |
+
],
|
| 535 |
+
"engines": {
|
| 536 |
+
"node": ">=12"
|
| 537 |
+
}
|
| 538 |
+
},
|
| 539 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 540 |
+
"version": "0.21.5",
|
| 541 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 542 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 543 |
+
"cpu": [
|
| 544 |
+
"riscv64"
|
| 545 |
+
],
|
| 546 |
+
"dev": true,
|
| 547 |
+
"license": "MIT",
|
| 548 |
+
"optional": true,
|
| 549 |
+
"os": [
|
| 550 |
+
"linux"
|
| 551 |
+
],
|
| 552 |
+
"engines": {
|
| 553 |
+
"node": ">=12"
|
| 554 |
+
}
|
| 555 |
+
},
|
| 556 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 557 |
+
"version": "0.21.5",
|
| 558 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 559 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 560 |
+
"cpu": [
|
| 561 |
+
"s390x"
|
| 562 |
+
],
|
| 563 |
+
"dev": true,
|
| 564 |
+
"license": "MIT",
|
| 565 |
+
"optional": true,
|
| 566 |
+
"os": [
|
| 567 |
+
"linux"
|
| 568 |
+
],
|
| 569 |
+
"engines": {
|
| 570 |
+
"node": ">=12"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"node_modules/@esbuild/linux-x64": {
|
| 574 |
+
"version": "0.21.5",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 576 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 577 |
+
"cpu": [
|
| 578 |
+
"x64"
|
| 579 |
+
],
|
| 580 |
+
"dev": true,
|
| 581 |
+
"license": "MIT",
|
| 582 |
+
"optional": true,
|
| 583 |
+
"os": [
|
| 584 |
+
"linux"
|
| 585 |
+
],
|
| 586 |
+
"engines": {
|
| 587 |
+
"node": ">=12"
|
| 588 |
+
}
|
| 589 |
+
},
|
| 590 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 591 |
+
"version": "0.21.5",
|
| 592 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 593 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 594 |
+
"cpu": [
|
| 595 |
+
"x64"
|
| 596 |
+
],
|
| 597 |
+
"dev": true,
|
| 598 |
+
"license": "MIT",
|
| 599 |
+
"optional": true,
|
| 600 |
+
"os": [
|
| 601 |
+
"netbsd"
|
| 602 |
+
],
|
| 603 |
+
"engines": {
|
| 604 |
+
"node": ">=12"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 608 |
+
"version": "0.21.5",
|
| 609 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 610 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 611 |
+
"cpu": [
|
| 612 |
+
"x64"
|
| 613 |
+
],
|
| 614 |
+
"dev": true,
|
| 615 |
+
"license": "MIT",
|
| 616 |
+
"optional": true,
|
| 617 |
+
"os": [
|
| 618 |
+
"openbsd"
|
| 619 |
+
],
|
| 620 |
+
"engines": {
|
| 621 |
+
"node": ">=12"
|
| 622 |
+
}
|
| 623 |
+
},
|
| 624 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 625 |
+
"version": "0.21.5",
|
| 626 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 627 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 628 |
+
"cpu": [
|
| 629 |
+
"x64"
|
| 630 |
+
],
|
| 631 |
+
"dev": true,
|
| 632 |
+
"license": "MIT",
|
| 633 |
+
"optional": true,
|
| 634 |
+
"os": [
|
| 635 |
+
"sunos"
|
| 636 |
+
],
|
| 637 |
+
"engines": {
|
| 638 |
+
"node": ">=12"
|
| 639 |
+
}
|
| 640 |
+
},
|
| 641 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 642 |
+
"version": "0.21.5",
|
| 643 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 644 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 645 |
+
"cpu": [
|
| 646 |
+
"arm64"
|
| 647 |
+
],
|
| 648 |
+
"dev": true,
|
| 649 |
+
"license": "MIT",
|
| 650 |
+
"optional": true,
|
| 651 |
+
"os": [
|
| 652 |
+
"win32"
|
| 653 |
+
],
|
| 654 |
+
"engines": {
|
| 655 |
+
"node": ">=12"
|
| 656 |
+
}
|
| 657 |
+
},
|
| 658 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 659 |
+
"version": "0.21.5",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 661 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 662 |
+
"cpu": [
|
| 663 |
+
"ia32"
|
| 664 |
+
],
|
| 665 |
+
"dev": true,
|
| 666 |
+
"license": "MIT",
|
| 667 |
+
"optional": true,
|
| 668 |
+
"os": [
|
| 669 |
+
"win32"
|
| 670 |
+
],
|
| 671 |
+
"engines": {
|
| 672 |
+
"node": ">=12"
|
| 673 |
+
}
|
| 674 |
+
},
|
| 675 |
+
"node_modules/@esbuild/win32-x64": {
|
| 676 |
+
"version": "0.21.5",
|
| 677 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 678 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 679 |
+
"cpu": [
|
| 680 |
+
"x64"
|
| 681 |
+
],
|
| 682 |
+
"dev": true,
|
| 683 |
+
"license": "MIT",
|
| 684 |
+
"optional": true,
|
| 685 |
+
"os": [
|
| 686 |
+
"win32"
|
| 687 |
+
],
|
| 688 |
+
"engines": {
|
| 689 |
+
"node": ">=12"
|
| 690 |
+
}
|
| 691 |
+
},
|
| 692 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 693 |
+
"version": "0.3.13",
|
| 694 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 695 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 696 |
+
"dev": true,
|
| 697 |
+
"license": "MIT",
|
| 698 |
+
"dependencies": {
|
| 699 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 700 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 701 |
+
}
|
| 702 |
+
},
|
| 703 |
+
"node_modules/@jridgewell/remapping": {
|
| 704 |
+
"version": "2.3.5",
|
| 705 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 706 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 707 |
+
"dev": true,
|
| 708 |
+
"license": "MIT",
|
| 709 |
+
"dependencies": {
|
| 710 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 711 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 712 |
+
}
|
| 713 |
+
},
|
| 714 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 715 |
+
"version": "3.1.2",
|
| 716 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 717 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 718 |
+
"dev": true,
|
| 719 |
+
"license": "MIT",
|
| 720 |
+
"engines": {
|
| 721 |
+
"node": ">=6.0.0"
|
| 722 |
+
}
|
| 723 |
+
},
|
| 724 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 725 |
+
"version": "1.5.5",
|
| 726 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 727 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 728 |
+
"dev": true,
|
| 729 |
+
"license": "MIT"
|
| 730 |
+
},
|
| 731 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 732 |
+
"version": "0.3.31",
|
| 733 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 734 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 735 |
+
"dev": true,
|
| 736 |
+
"license": "MIT",
|
| 737 |
+
"dependencies": {
|
| 738 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 739 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 740 |
+
}
|
| 741 |
+
},
|
| 742 |
+
"node_modules/@rolldown/pluginutils": {
|
| 743 |
+
"version": "1.0.0-beta.27",
|
| 744 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
| 745 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
| 746 |
+
"dev": true,
|
| 747 |
+
"license": "MIT"
|
| 748 |
+
},
|
| 749 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 750 |
+
"version": "4.60.1",
|
| 751 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
| 752 |
+
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
| 753 |
+
"cpu": [
|
| 754 |
+
"arm"
|
| 755 |
+
],
|
| 756 |
+
"dev": true,
|
| 757 |
+
"license": "MIT",
|
| 758 |
+
"optional": true,
|
| 759 |
+
"os": [
|
| 760 |
+
"android"
|
| 761 |
+
]
|
| 762 |
+
},
|
| 763 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 764 |
+
"version": "4.60.1",
|
| 765 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
| 766 |
+
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
| 767 |
+
"cpu": [
|
| 768 |
+
"arm64"
|
| 769 |
+
],
|
| 770 |
+
"dev": true,
|
| 771 |
+
"license": "MIT",
|
| 772 |
+
"optional": true,
|
| 773 |
+
"os": [
|
| 774 |
+
"android"
|
| 775 |
+
]
|
| 776 |
+
},
|
| 777 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 778 |
+
"version": "4.60.1",
|
| 779 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
| 780 |
+
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
| 781 |
+
"cpu": [
|
| 782 |
+
"arm64"
|
| 783 |
+
],
|
| 784 |
+
"dev": true,
|
| 785 |
+
"license": "MIT",
|
| 786 |
+
"optional": true,
|
| 787 |
+
"os": [
|
| 788 |
+
"darwin"
|
| 789 |
+
]
|
| 790 |
+
},
|
| 791 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 792 |
+
"version": "4.60.1",
|
| 793 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
| 794 |
+
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
| 795 |
+
"cpu": [
|
| 796 |
+
"x64"
|
| 797 |
+
],
|
| 798 |
+
"dev": true,
|
| 799 |
+
"license": "MIT",
|
| 800 |
+
"optional": true,
|
| 801 |
+
"os": [
|
| 802 |
+
"darwin"
|
| 803 |
+
]
|
| 804 |
+
},
|
| 805 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 806 |
+
"version": "4.60.1",
|
| 807 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
| 808 |
+
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
| 809 |
+
"cpu": [
|
| 810 |
+
"arm64"
|
| 811 |
+
],
|
| 812 |
+
"dev": true,
|
| 813 |
+
"license": "MIT",
|
| 814 |
+
"optional": true,
|
| 815 |
+
"os": [
|
| 816 |
+
"freebsd"
|
| 817 |
+
]
|
| 818 |
+
},
|
| 819 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 820 |
+
"version": "4.60.1",
|
| 821 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
| 822 |
+
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
| 823 |
+
"cpu": [
|
| 824 |
+
"x64"
|
| 825 |
+
],
|
| 826 |
+
"dev": true,
|
| 827 |
+
"license": "MIT",
|
| 828 |
+
"optional": true,
|
| 829 |
+
"os": [
|
| 830 |
+
"freebsd"
|
| 831 |
+
]
|
| 832 |
+
},
|
| 833 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 834 |
+
"version": "4.60.1",
|
| 835 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
| 836 |
+
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
| 837 |
+
"cpu": [
|
| 838 |
+
"arm"
|
| 839 |
+
],
|
| 840 |
+
"dev": true,
|
| 841 |
+
"license": "MIT",
|
| 842 |
+
"optional": true,
|
| 843 |
+
"os": [
|
| 844 |
+
"linux"
|
| 845 |
+
]
|
| 846 |
+
},
|
| 847 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 848 |
+
"version": "4.60.1",
|
| 849 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
| 850 |
+
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
| 851 |
+
"cpu": [
|
| 852 |
+
"arm"
|
| 853 |
+
],
|
| 854 |
+
"dev": true,
|
| 855 |
+
"license": "MIT",
|
| 856 |
+
"optional": true,
|
| 857 |
+
"os": [
|
| 858 |
+
"linux"
|
| 859 |
+
]
|
| 860 |
+
},
|
| 861 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 862 |
+
"version": "4.60.1",
|
| 863 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
| 864 |
+
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
| 865 |
+
"cpu": [
|
| 866 |
+
"arm64"
|
| 867 |
+
],
|
| 868 |
+
"dev": true,
|
| 869 |
+
"license": "MIT",
|
| 870 |
+
"optional": true,
|
| 871 |
+
"os": [
|
| 872 |
+
"linux"
|
| 873 |
+
]
|
| 874 |
+
},
|
| 875 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 876 |
+
"version": "4.60.1",
|
| 877 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
| 878 |
+
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
| 879 |
+
"cpu": [
|
| 880 |
+
"arm64"
|
| 881 |
+
],
|
| 882 |
+
"dev": true,
|
| 883 |
+
"license": "MIT",
|
| 884 |
+
"optional": true,
|
| 885 |
+
"os": [
|
| 886 |
+
"linux"
|
| 887 |
+
]
|
| 888 |
+
},
|
| 889 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 890 |
+
"version": "4.60.1",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
| 892 |
+
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
| 893 |
+
"cpu": [
|
| 894 |
+
"loong64"
|
| 895 |
+
],
|
| 896 |
+
"dev": true,
|
| 897 |
+
"license": "MIT",
|
| 898 |
+
"optional": true,
|
| 899 |
+
"os": [
|
| 900 |
+
"linux"
|
| 901 |
+
]
|
| 902 |
+
},
|
| 903 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 904 |
+
"version": "4.60.1",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
| 906 |
+
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
| 907 |
+
"cpu": [
|
| 908 |
+
"loong64"
|
| 909 |
+
],
|
| 910 |
+
"dev": true,
|
| 911 |
+
"license": "MIT",
|
| 912 |
+
"optional": true,
|
| 913 |
+
"os": [
|
| 914 |
+
"linux"
|
| 915 |
+
]
|
| 916 |
+
},
|
| 917 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 918 |
+
"version": "4.60.1",
|
| 919 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
| 920 |
+
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
| 921 |
+
"cpu": [
|
| 922 |
+
"ppc64"
|
| 923 |
+
],
|
| 924 |
+
"dev": true,
|
| 925 |
+
"license": "MIT",
|
| 926 |
+
"optional": true,
|
| 927 |
+
"os": [
|
| 928 |
+
"linux"
|
| 929 |
+
]
|
| 930 |
+
},
|
| 931 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 932 |
+
"version": "4.60.1",
|
| 933 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
| 934 |
+
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
| 935 |
+
"cpu": [
|
| 936 |
+
"ppc64"
|
| 937 |
+
],
|
| 938 |
+
"dev": true,
|
| 939 |
+
"license": "MIT",
|
| 940 |
+
"optional": true,
|
| 941 |
+
"os": [
|
| 942 |
+
"linux"
|
| 943 |
+
]
|
| 944 |
+
},
|
| 945 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 946 |
+
"version": "4.60.1",
|
| 947 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
| 948 |
+
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
| 949 |
+
"cpu": [
|
| 950 |
+
"riscv64"
|
| 951 |
+
],
|
| 952 |
+
"dev": true,
|
| 953 |
+
"license": "MIT",
|
| 954 |
+
"optional": true,
|
| 955 |
+
"os": [
|
| 956 |
+
"linux"
|
| 957 |
+
]
|
| 958 |
+
},
|
| 959 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 960 |
+
"version": "4.60.1",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
| 962 |
+
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
| 963 |
+
"cpu": [
|
| 964 |
+
"riscv64"
|
| 965 |
+
],
|
| 966 |
+
"dev": true,
|
| 967 |
+
"license": "MIT",
|
| 968 |
+
"optional": true,
|
| 969 |
+
"os": [
|
| 970 |
+
"linux"
|
| 971 |
+
]
|
| 972 |
+
},
|
| 973 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 974 |
+
"version": "4.60.1",
|
| 975 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
| 976 |
+
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
| 977 |
+
"cpu": [
|
| 978 |
+
"s390x"
|
| 979 |
+
],
|
| 980 |
+
"dev": true,
|
| 981 |
+
"license": "MIT",
|
| 982 |
+
"optional": true,
|
| 983 |
+
"os": [
|
| 984 |
+
"linux"
|
| 985 |
+
]
|
| 986 |
+
},
|
| 987 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 988 |
+
"version": "4.60.1",
|
| 989 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
| 990 |
+
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
| 991 |
+
"cpu": [
|
| 992 |
+
"x64"
|
| 993 |
+
],
|
| 994 |
+
"dev": true,
|
| 995 |
+
"license": "MIT",
|
| 996 |
+
"optional": true,
|
| 997 |
+
"os": [
|
| 998 |
+
"linux"
|
| 999 |
+
]
|
| 1000 |
+
},
|
| 1001 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1002 |
+
"version": "4.60.1",
|
| 1003 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
| 1004 |
+
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
| 1005 |
+
"cpu": [
|
| 1006 |
+
"x64"
|
| 1007 |
+
],
|
| 1008 |
+
"dev": true,
|
| 1009 |
+
"license": "MIT",
|
| 1010 |
+
"optional": true,
|
| 1011 |
+
"os": [
|
| 1012 |
+
"linux"
|
| 1013 |
+
]
|
| 1014 |
+
},
|
| 1015 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1016 |
+
"version": "4.60.1",
|
| 1017 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
| 1018 |
+
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
| 1019 |
+
"cpu": [
|
| 1020 |
+
"x64"
|
| 1021 |
+
],
|
| 1022 |
+
"dev": true,
|
| 1023 |
+
"license": "MIT",
|
| 1024 |
+
"optional": true,
|
| 1025 |
+
"os": [
|
| 1026 |
+
"openbsd"
|
| 1027 |
+
]
|
| 1028 |
+
},
|
| 1029 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1030 |
+
"version": "4.60.1",
|
| 1031 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
| 1032 |
+
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
| 1033 |
+
"cpu": [
|
| 1034 |
+
"arm64"
|
| 1035 |
+
],
|
| 1036 |
+
"dev": true,
|
| 1037 |
+
"license": "MIT",
|
| 1038 |
+
"optional": true,
|
| 1039 |
+
"os": [
|
| 1040 |
+
"openharmony"
|
| 1041 |
+
]
|
| 1042 |
+
},
|
| 1043 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1044 |
+
"version": "4.60.1",
|
| 1045 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
| 1046 |
+
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
| 1047 |
+
"cpu": [
|
| 1048 |
+
"arm64"
|
| 1049 |
+
],
|
| 1050 |
+
"dev": true,
|
| 1051 |
+
"license": "MIT",
|
| 1052 |
+
"optional": true,
|
| 1053 |
+
"os": [
|
| 1054 |
+
"win32"
|
| 1055 |
+
]
|
| 1056 |
+
},
|
| 1057 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1058 |
+
"version": "4.60.1",
|
| 1059 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
| 1060 |
+
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
| 1061 |
+
"cpu": [
|
| 1062 |
+
"ia32"
|
| 1063 |
+
],
|
| 1064 |
+
"dev": true,
|
| 1065 |
+
"license": "MIT",
|
| 1066 |
+
"optional": true,
|
| 1067 |
+
"os": [
|
| 1068 |
+
"win32"
|
| 1069 |
+
]
|
| 1070 |
+
},
|
| 1071 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1072 |
+
"version": "4.60.1",
|
| 1073 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
| 1074 |
+
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
| 1075 |
+
"cpu": [
|
| 1076 |
+
"x64"
|
| 1077 |
+
],
|
| 1078 |
+
"dev": true,
|
| 1079 |
+
"license": "MIT",
|
| 1080 |
+
"optional": true,
|
| 1081 |
+
"os": [
|
| 1082 |
+
"win32"
|
| 1083 |
+
]
|
| 1084 |
+
},
|
| 1085 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1086 |
+
"version": "4.60.1",
|
| 1087 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
| 1088 |
+
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
| 1089 |
+
"cpu": [
|
| 1090 |
+
"x64"
|
| 1091 |
+
],
|
| 1092 |
+
"dev": true,
|
| 1093 |
+
"license": "MIT",
|
| 1094 |
+
"optional": true,
|
| 1095 |
+
"os": [
|
| 1096 |
+
"win32"
|
| 1097 |
+
]
|
| 1098 |
+
},
|
| 1099 |
+
"node_modules/@types/babel__core": {
|
| 1100 |
+
"version": "7.20.5",
|
| 1101 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1102 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1103 |
+
"dev": true,
|
| 1104 |
+
"license": "MIT",
|
| 1105 |
+
"dependencies": {
|
| 1106 |
+
"@babel/parser": "^7.20.7",
|
| 1107 |
+
"@babel/types": "^7.20.7",
|
| 1108 |
+
"@types/babel__generator": "*",
|
| 1109 |
+
"@types/babel__template": "*",
|
| 1110 |
+
"@types/babel__traverse": "*"
|
| 1111 |
+
}
|
| 1112 |
+
},
|
| 1113 |
+
"node_modules/@types/babel__generator": {
|
| 1114 |
+
"version": "7.27.0",
|
| 1115 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1116 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1117 |
+
"dev": true,
|
| 1118 |
+
"license": "MIT",
|
| 1119 |
+
"dependencies": {
|
| 1120 |
+
"@babel/types": "^7.0.0"
|
| 1121 |
+
}
|
| 1122 |
+
},
|
| 1123 |
+
"node_modules/@types/babel__template": {
|
| 1124 |
+
"version": "7.4.4",
|
| 1125 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1126 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1127 |
+
"dev": true,
|
| 1128 |
+
"license": "MIT",
|
| 1129 |
+
"dependencies": {
|
| 1130 |
+
"@babel/parser": "^7.1.0",
|
| 1131 |
+
"@babel/types": "^7.0.0"
|
| 1132 |
+
}
|
| 1133 |
+
},
|
| 1134 |
+
"node_modules/@types/babel__traverse": {
|
| 1135 |
+
"version": "7.28.0",
|
| 1136 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1137 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1138 |
+
"dev": true,
|
| 1139 |
+
"license": "MIT",
|
| 1140 |
+
"dependencies": {
|
| 1141 |
+
"@babel/types": "^7.28.2"
|
| 1142 |
+
}
|
| 1143 |
+
},
|
| 1144 |
+
"node_modules/@types/estree": {
|
| 1145 |
+
"version": "1.0.8",
|
| 1146 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1147 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1148 |
+
"dev": true,
|
| 1149 |
+
"license": "MIT"
|
| 1150 |
+
},
|
| 1151 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1152 |
+
"version": "4.7.0",
|
| 1153 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
| 1154 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 1155 |
+
"dev": true,
|
| 1156 |
+
"license": "MIT",
|
| 1157 |
+
"dependencies": {
|
| 1158 |
+
"@babel/core": "^7.28.0",
|
| 1159 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1160 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1161 |
+
"@rolldown/pluginutils": "1.0.0-beta.27",
|
| 1162 |
+
"@types/babel__core": "^7.20.5",
|
| 1163 |
+
"react-refresh": "^0.17.0"
|
| 1164 |
+
},
|
| 1165 |
+
"engines": {
|
| 1166 |
+
"node": "^14.18.0 || >=16.0.0"
|
| 1167 |
+
},
|
| 1168 |
+
"peerDependencies": {
|
| 1169 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1170 |
+
}
|
| 1171 |
+
},
|
| 1172 |
+
"node_modules/baseline-browser-mapping": {
|
| 1173 |
+
"version": "2.10.16",
|
| 1174 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
| 1175 |
+
"integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
|
| 1176 |
+
"dev": true,
|
| 1177 |
+
"license": "Apache-2.0",
|
| 1178 |
+
"bin": {
|
| 1179 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1180 |
+
},
|
| 1181 |
+
"engines": {
|
| 1182 |
+
"node": ">=6.0.0"
|
| 1183 |
+
}
|
| 1184 |
+
},
|
| 1185 |
+
"node_modules/browserslist": {
|
| 1186 |
+
"version": "4.28.2",
|
| 1187 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
| 1188 |
+
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
| 1189 |
+
"dev": true,
|
| 1190 |
+
"funding": [
|
| 1191 |
+
{
|
| 1192 |
+
"type": "opencollective",
|
| 1193 |
+
"url": "https://opencollective.com/browserslist"
|
| 1194 |
+
},
|
| 1195 |
+
{
|
| 1196 |
+
"type": "tidelift",
|
| 1197 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"type": "github",
|
| 1201 |
+
"url": "https://github.com/sponsors/ai"
|
| 1202 |
+
}
|
| 1203 |
+
],
|
| 1204 |
+
"license": "MIT",
|
| 1205 |
+
"dependencies": {
|
| 1206 |
+
"baseline-browser-mapping": "^2.10.12",
|
| 1207 |
+
"caniuse-lite": "^1.0.30001782",
|
| 1208 |
+
"electron-to-chromium": "^1.5.328",
|
| 1209 |
+
"node-releases": "^2.0.36",
|
| 1210 |
+
"update-browserslist-db": "^1.2.3"
|
| 1211 |
+
},
|
| 1212 |
+
"bin": {
|
| 1213 |
+
"browserslist": "cli.js"
|
| 1214 |
+
},
|
| 1215 |
+
"engines": {
|
| 1216 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1217 |
+
}
|
| 1218 |
+
},
|
| 1219 |
+
"node_modules/caniuse-lite": {
|
| 1220 |
+
"version": "1.0.30001786",
|
| 1221 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
|
| 1222 |
+
"integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==",
|
| 1223 |
+
"dev": true,
|
| 1224 |
+
"funding": [
|
| 1225 |
+
{
|
| 1226 |
+
"type": "opencollective",
|
| 1227 |
+
"url": "https://opencollective.com/browserslist"
|
| 1228 |
+
},
|
| 1229 |
+
{
|
| 1230 |
+
"type": "tidelift",
|
| 1231 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1232 |
+
},
|
| 1233 |
+
{
|
| 1234 |
+
"type": "github",
|
| 1235 |
+
"url": "https://github.com/sponsors/ai"
|
| 1236 |
+
}
|
| 1237 |
+
],
|
| 1238 |
+
"license": "CC-BY-4.0"
|
| 1239 |
+
},
|
| 1240 |
+
"node_modules/convert-source-map": {
|
| 1241 |
+
"version": "2.0.0",
|
| 1242 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1243 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1244 |
+
"dev": true,
|
| 1245 |
+
"license": "MIT"
|
| 1246 |
+
},
|
| 1247 |
+
"node_modules/debug": {
|
| 1248 |
+
"version": "4.4.3",
|
| 1249 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1250 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1251 |
+
"dev": true,
|
| 1252 |
+
"license": "MIT",
|
| 1253 |
+
"dependencies": {
|
| 1254 |
+
"ms": "^2.1.3"
|
| 1255 |
+
},
|
| 1256 |
+
"engines": {
|
| 1257 |
+
"node": ">=6.0"
|
| 1258 |
+
},
|
| 1259 |
+
"peerDependenciesMeta": {
|
| 1260 |
+
"supports-color": {
|
| 1261 |
+
"optional": true
|
| 1262 |
+
}
|
| 1263 |
+
}
|
| 1264 |
+
},
|
| 1265 |
+
"node_modules/electron-to-chromium": {
|
| 1266 |
+
"version": "1.5.331",
|
| 1267 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
| 1268 |
+
"integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
|
| 1269 |
+
"dev": true,
|
| 1270 |
+
"license": "ISC"
|
| 1271 |
+
},
|
| 1272 |
+
"node_modules/esbuild": {
|
| 1273 |
+
"version": "0.21.5",
|
| 1274 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 1275 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 1276 |
+
"dev": true,
|
| 1277 |
+
"hasInstallScript": true,
|
| 1278 |
+
"license": "MIT",
|
| 1279 |
+
"bin": {
|
| 1280 |
+
"esbuild": "bin/esbuild"
|
| 1281 |
+
},
|
| 1282 |
+
"engines": {
|
| 1283 |
+
"node": ">=12"
|
| 1284 |
+
},
|
| 1285 |
+
"optionalDependencies": {
|
| 1286 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 1287 |
+
"@esbuild/android-arm": "0.21.5",
|
| 1288 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 1289 |
+
"@esbuild/android-x64": "0.21.5",
|
| 1290 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 1291 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 1292 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 1293 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 1294 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 1295 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 1296 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 1297 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 1298 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 1299 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 1300 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 1301 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 1302 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 1303 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 1304 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 1305 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 1306 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 1307 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 1308 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 1309 |
+
}
|
| 1310 |
+
},
|
| 1311 |
+
"node_modules/escalade": {
|
| 1312 |
+
"version": "3.2.0",
|
| 1313 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1314 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1315 |
+
"dev": true,
|
| 1316 |
+
"license": "MIT",
|
| 1317 |
+
"engines": {
|
| 1318 |
+
"node": ">=6"
|
| 1319 |
+
}
|
| 1320 |
+
},
|
| 1321 |
+
"node_modules/fsevents": {
|
| 1322 |
+
"version": "2.3.3",
|
| 1323 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1324 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1325 |
+
"dev": true,
|
| 1326 |
+
"hasInstallScript": true,
|
| 1327 |
+
"license": "MIT",
|
| 1328 |
+
"optional": true,
|
| 1329 |
+
"os": [
|
| 1330 |
+
"darwin"
|
| 1331 |
+
],
|
| 1332 |
+
"engines": {
|
| 1333 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1334 |
+
}
|
| 1335 |
+
},
|
| 1336 |
+
"node_modules/gensync": {
|
| 1337 |
+
"version": "1.0.0-beta.2",
|
| 1338 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1339 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1340 |
+
"dev": true,
|
| 1341 |
+
"license": "MIT",
|
| 1342 |
+
"engines": {
|
| 1343 |
+
"node": ">=6.9.0"
|
| 1344 |
+
}
|
| 1345 |
+
},
|
| 1346 |
+
"node_modules/js-tokens": {
|
| 1347 |
+
"version": "4.0.0",
|
| 1348 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1349 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1350 |
+
"license": "MIT"
|
| 1351 |
+
},
|
| 1352 |
+
"node_modules/jsesc": {
|
| 1353 |
+
"version": "3.1.0",
|
| 1354 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1355 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1356 |
+
"dev": true,
|
| 1357 |
+
"license": "MIT",
|
| 1358 |
+
"bin": {
|
| 1359 |
+
"jsesc": "bin/jsesc"
|
| 1360 |
+
},
|
| 1361 |
+
"engines": {
|
| 1362 |
+
"node": ">=6"
|
| 1363 |
+
}
|
| 1364 |
+
},
|
| 1365 |
+
"node_modules/json5": {
|
| 1366 |
+
"version": "2.2.3",
|
| 1367 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1368 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1369 |
+
"dev": true,
|
| 1370 |
+
"license": "MIT",
|
| 1371 |
+
"bin": {
|
| 1372 |
+
"json5": "lib/cli.js"
|
| 1373 |
+
},
|
| 1374 |
+
"engines": {
|
| 1375 |
+
"node": ">=6"
|
| 1376 |
+
}
|
| 1377 |
+
},
|
| 1378 |
+
"node_modules/loose-envify": {
|
| 1379 |
+
"version": "1.4.0",
|
| 1380 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 1381 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 1382 |
+
"license": "MIT",
|
| 1383 |
+
"dependencies": {
|
| 1384 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 1385 |
+
},
|
| 1386 |
+
"bin": {
|
| 1387 |
+
"loose-envify": "cli.js"
|
| 1388 |
+
}
|
| 1389 |
+
},
|
| 1390 |
+
"node_modules/lru-cache": {
|
| 1391 |
+
"version": "5.1.1",
|
| 1392 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1393 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1394 |
+
"dev": true,
|
| 1395 |
+
"license": "ISC",
|
| 1396 |
+
"dependencies": {
|
| 1397 |
+
"yallist": "^3.0.2"
|
| 1398 |
+
}
|
| 1399 |
+
},
|
| 1400 |
+
"node_modules/ms": {
|
| 1401 |
+
"version": "2.1.3",
|
| 1402 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1403 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1404 |
+
"dev": true,
|
| 1405 |
+
"license": "MIT"
|
| 1406 |
+
},
|
| 1407 |
+
"node_modules/nanoid": {
|
| 1408 |
+
"version": "3.3.11",
|
| 1409 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 1410 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1411 |
+
"dev": true,
|
| 1412 |
+
"funding": [
|
| 1413 |
+
{
|
| 1414 |
+
"type": "github",
|
| 1415 |
+
"url": "https://github.com/sponsors/ai"
|
| 1416 |
+
}
|
| 1417 |
+
],
|
| 1418 |
+
"license": "MIT",
|
| 1419 |
+
"bin": {
|
| 1420 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1421 |
+
},
|
| 1422 |
+
"engines": {
|
| 1423 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1424 |
+
}
|
| 1425 |
+
},
|
| 1426 |
+
"node_modules/node-releases": {
|
| 1427 |
+
"version": "2.0.37",
|
| 1428 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
| 1429 |
+
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
| 1430 |
+
"dev": true,
|
| 1431 |
+
"license": "MIT"
|
| 1432 |
+
},
|
| 1433 |
+
"node_modules/picocolors": {
|
| 1434 |
+
"version": "1.1.1",
|
| 1435 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1436 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1437 |
+
"dev": true,
|
| 1438 |
+
"license": "ISC"
|
| 1439 |
+
},
|
| 1440 |
+
"node_modules/postcss": {
|
| 1441 |
+
"version": "8.5.8",
|
| 1442 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
| 1443 |
+
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
| 1444 |
+
"dev": true,
|
| 1445 |
+
"funding": [
|
| 1446 |
+
{
|
| 1447 |
+
"type": "opencollective",
|
| 1448 |
+
"url": "https://opencollective.com/postcss/"
|
| 1449 |
+
},
|
| 1450 |
+
{
|
| 1451 |
+
"type": "tidelift",
|
| 1452 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1453 |
+
},
|
| 1454 |
+
{
|
| 1455 |
+
"type": "github",
|
| 1456 |
+
"url": "https://github.com/sponsors/ai"
|
| 1457 |
+
}
|
| 1458 |
+
],
|
| 1459 |
+
"license": "MIT",
|
| 1460 |
+
"dependencies": {
|
| 1461 |
+
"nanoid": "^3.3.11",
|
| 1462 |
+
"picocolors": "^1.1.1",
|
| 1463 |
+
"source-map-js": "^1.2.1"
|
| 1464 |
+
},
|
| 1465 |
+
"engines": {
|
| 1466 |
+
"node": "^10 || ^12 || >=14"
|
| 1467 |
+
}
|
| 1468 |
+
},
|
| 1469 |
+
"node_modules/react": {
|
| 1470 |
+
"version": "18.3.1",
|
| 1471 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 1472 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 1473 |
+
"license": "MIT",
|
| 1474 |
+
"dependencies": {
|
| 1475 |
+
"loose-envify": "^1.1.0"
|
| 1476 |
+
},
|
| 1477 |
+
"engines": {
|
| 1478 |
+
"node": ">=0.10.0"
|
| 1479 |
+
}
|
| 1480 |
+
},
|
| 1481 |
+
"node_modules/react-dom": {
|
| 1482 |
+
"version": "18.3.1",
|
| 1483 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 1484 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 1485 |
+
"license": "MIT",
|
| 1486 |
+
"dependencies": {
|
| 1487 |
+
"loose-envify": "^1.1.0",
|
| 1488 |
+
"scheduler": "^0.23.2"
|
| 1489 |
+
},
|
| 1490 |
+
"peerDependencies": {
|
| 1491 |
+
"react": "^18.3.1"
|
| 1492 |
+
}
|
| 1493 |
+
},
|
| 1494 |
+
"node_modules/react-refresh": {
|
| 1495 |
+
"version": "0.17.0",
|
| 1496 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 1497 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 1498 |
+
"dev": true,
|
| 1499 |
+
"license": "MIT",
|
| 1500 |
+
"engines": {
|
| 1501 |
+
"node": ">=0.10.0"
|
| 1502 |
+
}
|
| 1503 |
+
},
|
| 1504 |
+
"node_modules/rollup": {
|
| 1505 |
+
"version": "4.60.1",
|
| 1506 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
| 1507 |
+
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
| 1508 |
+
"dev": true,
|
| 1509 |
+
"license": "MIT",
|
| 1510 |
+
"dependencies": {
|
| 1511 |
+
"@types/estree": "1.0.8"
|
| 1512 |
+
},
|
| 1513 |
+
"bin": {
|
| 1514 |
+
"rollup": "dist/bin/rollup"
|
| 1515 |
+
},
|
| 1516 |
+
"engines": {
|
| 1517 |
+
"node": ">=18.0.0",
|
| 1518 |
+
"npm": ">=8.0.0"
|
| 1519 |
+
},
|
| 1520 |
+
"optionalDependencies": {
|
| 1521 |
+
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
| 1522 |
+
"@rollup/rollup-android-arm64": "4.60.1",
|
| 1523 |
+
"@rollup/rollup-darwin-arm64": "4.60.1",
|
| 1524 |
+
"@rollup/rollup-darwin-x64": "4.60.1",
|
| 1525 |
+
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
| 1526 |
+
"@rollup/rollup-freebsd-x64": "4.60.1",
|
| 1527 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
| 1528 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
| 1529 |
+
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
| 1530 |
+
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
| 1531 |
+
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
| 1532 |
+
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
| 1533 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
| 1534 |
+
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
| 1535 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
| 1536 |
+
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
| 1537 |
+
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
| 1538 |
+
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
| 1539 |
+
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
| 1540 |
+
"@rollup/rollup-openbsd-x64": "4.60.1",
|
| 1541 |
+
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
| 1542 |
+
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
| 1543 |
+
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
| 1544 |
+
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
| 1545 |
+
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
| 1546 |
+
"fsevents": "~2.3.2"
|
| 1547 |
+
}
|
| 1548 |
+
},
|
| 1549 |
+
"node_modules/scheduler": {
|
| 1550 |
+
"version": "0.23.2",
|
| 1551 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 1552 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 1553 |
+
"license": "MIT",
|
| 1554 |
+
"dependencies": {
|
| 1555 |
+
"loose-envify": "^1.1.0"
|
| 1556 |
+
}
|
| 1557 |
+
},
|
| 1558 |
+
"node_modules/semver": {
|
| 1559 |
+
"version": "6.3.1",
|
| 1560 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 1561 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1562 |
+
"dev": true,
|
| 1563 |
+
"license": "ISC",
|
| 1564 |
+
"bin": {
|
| 1565 |
+
"semver": "bin/semver.js"
|
| 1566 |
+
}
|
| 1567 |
+
},
|
| 1568 |
+
"node_modules/source-map-js": {
|
| 1569 |
+
"version": "1.2.1",
|
| 1570 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1571 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1572 |
+
"dev": true,
|
| 1573 |
+
"license": "BSD-3-Clause",
|
| 1574 |
+
"engines": {
|
| 1575 |
+
"node": ">=0.10.0"
|
| 1576 |
+
}
|
| 1577 |
+
},
|
| 1578 |
+
"node_modules/update-browserslist-db": {
|
| 1579 |
+
"version": "1.2.3",
|
| 1580 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 1581 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 1582 |
+
"dev": true,
|
| 1583 |
+
"funding": [
|
| 1584 |
+
{
|
| 1585 |
+
"type": "opencollective",
|
| 1586 |
+
"url": "https://opencollective.com/browserslist"
|
| 1587 |
+
},
|
| 1588 |
+
{
|
| 1589 |
+
"type": "tidelift",
|
| 1590 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1591 |
+
},
|
| 1592 |
+
{
|
| 1593 |
+
"type": "github",
|
| 1594 |
+
"url": "https://github.com/sponsors/ai"
|
| 1595 |
+
}
|
| 1596 |
+
],
|
| 1597 |
+
"license": "MIT",
|
| 1598 |
+
"dependencies": {
|
| 1599 |
+
"escalade": "^3.2.0",
|
| 1600 |
+
"picocolors": "^1.1.1"
|
| 1601 |
+
},
|
| 1602 |
+
"bin": {
|
| 1603 |
+
"update-browserslist-db": "cli.js"
|
| 1604 |
+
},
|
| 1605 |
+
"peerDependencies": {
|
| 1606 |
+
"browserslist": ">= 4.21.0"
|
| 1607 |
+
}
|
| 1608 |
+
},
|
| 1609 |
+
"node_modules/vite": {
|
| 1610 |
+
"version": "5.4.21",
|
| 1611 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 1612 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 1613 |
+
"dev": true,
|
| 1614 |
+
"license": "MIT",
|
| 1615 |
+
"dependencies": {
|
| 1616 |
+
"esbuild": "^0.21.3",
|
| 1617 |
+
"postcss": "^8.4.43",
|
| 1618 |
+
"rollup": "^4.20.0"
|
| 1619 |
+
},
|
| 1620 |
+
"bin": {
|
| 1621 |
+
"vite": "bin/vite.js"
|
| 1622 |
+
},
|
| 1623 |
+
"engines": {
|
| 1624 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1625 |
+
},
|
| 1626 |
+
"funding": {
|
| 1627 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1628 |
+
},
|
| 1629 |
+
"optionalDependencies": {
|
| 1630 |
+
"fsevents": "~2.3.3"
|
| 1631 |
+
},
|
| 1632 |
+
"peerDependencies": {
|
| 1633 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 1634 |
+
"less": "*",
|
| 1635 |
+
"lightningcss": "^1.21.0",
|
| 1636 |
+
"sass": "*",
|
| 1637 |
+
"sass-embedded": "*",
|
| 1638 |
+
"stylus": "*",
|
| 1639 |
+
"sugarss": "*",
|
| 1640 |
+
"terser": "^5.4.0"
|
| 1641 |
+
},
|
| 1642 |
+
"peerDependenciesMeta": {
|
| 1643 |
+
"@types/node": {
|
| 1644 |
+
"optional": true
|
| 1645 |
+
},
|
| 1646 |
+
"less": {
|
| 1647 |
+
"optional": true
|
| 1648 |
+
},
|
| 1649 |
+
"lightningcss": {
|
| 1650 |
+
"optional": true
|
| 1651 |
+
},
|
| 1652 |
+
"sass": {
|
| 1653 |
+
"optional": true
|
| 1654 |
+
},
|
| 1655 |
+
"sass-embedded": {
|
| 1656 |
+
"optional": true
|
| 1657 |
+
},
|
| 1658 |
+
"stylus": {
|
| 1659 |
+
"optional": true
|
| 1660 |
+
},
|
| 1661 |
+
"sugarss": {
|
| 1662 |
+
"optional": true
|
| 1663 |
+
},
|
| 1664 |
+
"terser": {
|
| 1665 |
+
"optional": true
|
| 1666 |
+
}
|
| 1667 |
+
}
|
| 1668 |
+
},
|
| 1669 |
+
"node_modules/yallist": {
|
| 1670 |
+
"version": "3.1.1",
|
| 1671 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 1672 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 1673 |
+
"dev": true,
|
| 1674 |
+
"license": "ISC"
|
| 1675 |
+
}
|
| 1676 |
+
}
|
| 1677 |
+
}
|
openenv-polypharmacy/frontend/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "polypharmacy-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview --port 4173"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 17 |
+
"vite": "^5.4.2"
|
| 18 |
+
}
|
| 19 |
+
}
|
openenv-polypharmacy/frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useMemo, useRef, useState } from "react";
|
| 2 |
+
|
| 3 |
+
const API_BASE = "http://localhost:7860";
|
| 4 |
+
const WS_URL = "ws://localhost:7860/ws";
|
| 5 |
+
const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
|
| 6 |
+
|
| 7 |
+
async function apiPost(path, body) {
|
| 8 |
+
const res = await fetch(`${API_BASE}${path}`, {
|
| 9 |
+
method: "POST",
|
| 10 |
+
headers: { "Content-Type": "application/json" },
|
| 11 |
+
body: JSON.stringify(body),
|
| 12 |
+
});
|
| 13 |
+
if (!res.ok) {
|
| 14 |
+
const msg = await res.text();
|
| 15 |
+
throw new Error(msg || `HTTP ${res.status}`);
|
| 16 |
+
}
|
| 17 |
+
return res.json();
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default function App() {
|
| 21 |
+
const [taskId, setTaskId] = useState("budgeted_screening");
|
| 22 |
+
const [obs, setObs] = useState(null);
|
| 23 |
+
const [log, setLog] = useState([]);
|
| 24 |
+
const [loading, setLoading] = useState(false);
|
| 25 |
+
const [action, setAction] = useState({
|
| 26 |
+
action_type: "query_ddi",
|
| 27 |
+
drug_id_1: "",
|
| 28 |
+
drug_id_2: "",
|
| 29 |
+
target_drug_id: "",
|
| 30 |
+
intervention_type: "stop",
|
| 31 |
+
proposed_new_drug_id: "",
|
| 32 |
+
rationale: "",
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
const medIds = useMemo(
|
| 36 |
+
() => (obs?.current_medications || []).map((m) => m.drug_id),
|
| 37 |
+
[obs]
|
| 38 |
+
);
|
| 39 |
+
const hasValidEpisode = Boolean(obs?.episode_id) && (obs?.current_medications?.length || 0) > 0;
|
| 40 |
+
const isDone = Boolean(obs?.done);
|
| 41 |
+
const finalScore =
|
| 42 |
+
typeof obs?.metadata?.grader_score === "number" ? obs.metadata.grader_score : null;
|
| 43 |
+
const noBudgetsLeft =
|
| 44 |
+
hasValidEpisode &&
|
| 45 |
+
(obs?.remaining_query_budget ?? 0) <= 0 &&
|
| 46 |
+
(obs?.remaining_intervention_budget ?? 0) <= 0;
|
| 47 |
+
const wsRef = useRef(null);
|
| 48 |
+
const pendingRef = useRef([]);
|
| 49 |
+
|
| 50 |
+
const wsEnsure = async () => {
|
| 51 |
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
|
| 52 |
+
if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) {
|
| 53 |
+
await new Promise((r) => setTimeout(r, 80));
|
| 54 |
+
return wsEnsure();
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
const ws = new WebSocket(WS_URL);
|
| 58 |
+
wsRef.current = ws;
|
| 59 |
+
|
| 60 |
+
ws.onmessage = (evt) => {
|
| 61 |
+
try {
|
| 62 |
+
const msg = JSON.parse(evt.data);
|
| 63 |
+
const pending = pendingRef.current.shift();
|
| 64 |
+
if (pending) pending.resolve(msg);
|
| 65 |
+
} catch (e) {
|
| 66 |
+
const pending = pendingRef.current.shift();
|
| 67 |
+
if (pending) pending.reject(e);
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
ws.onerror = (err) => {
|
| 71 |
+
const pending = pendingRef.current.shift();
|
| 72 |
+
if (pending) pending.reject(err);
|
| 73 |
+
};
|
| 74 |
+
ws.onclose = () => {
|
| 75 |
+
wsRef.current = null;
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
await new Promise((resolve, reject) => {
|
| 79 |
+
const t = setTimeout(() => reject(new Error("WebSocket connect timeout")), 2500);
|
| 80 |
+
ws.onopen = () => {
|
| 81 |
+
clearTimeout(t);
|
| 82 |
+
resolve();
|
| 83 |
+
};
|
| 84 |
+
});
|
| 85 |
+
return ws;
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
const wsSend = async (type, data) => {
|
| 89 |
+
const ws = await wsEnsure();
|
| 90 |
+
return await new Promise((resolve, reject) => {
|
| 91 |
+
pendingRef.current.push({ resolve, reject });
|
| 92 |
+
ws.send(JSON.stringify({ type, data }));
|
| 93 |
+
});
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
useEffect(() => {
|
| 97 |
+
return () => {
|
| 98 |
+
try {
|
| 99 |
+
wsRef.current?.close();
|
| 100 |
+
} catch {
|
| 101 |
+
// ignore
|
| 102 |
+
}
|
| 103 |
+
};
|
| 104 |
+
}, []);
|
| 105 |
+
|
| 106 |
+
const appendLog = (text) => {
|
| 107 |
+
setLog((prev) => [`${new Date().toLocaleTimeString()} ${text}`, ...prev].slice(0, 20));
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const normalizeObsFromWs = (packetData) => {
|
| 111 |
+
const observation = packetData?.observation || {};
|
| 112 |
+
const mergedMetadata = {
|
| 113 |
+
...(observation?.metadata || {}),
|
| 114 |
+
...(packetData?.info || {}),
|
| 115 |
+
};
|
| 116 |
+
return {
|
| 117 |
+
...observation,
|
| 118 |
+
done: Boolean(packetData?.done ?? observation?.done ?? false),
|
| 119 |
+
reward: packetData?.reward ?? observation?.reward ?? null,
|
| 120 |
+
metadata: mergedMetadata,
|
| 121 |
+
};
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
const handleReset = async () => {
|
| 125 |
+
setLoading(true);
|
| 126 |
+
try {
|
| 127 |
+
const msg = await wsSend("reset", { task_id: taskId });
|
| 128 |
+
const data = msg?.data || {};
|
| 129 |
+
const normalized = normalizeObsFromWs(data);
|
| 130 |
+
setObs(normalized);
|
| 131 |
+
const ids = (normalized?.current_medications || []).map((m) => m.drug_id);
|
| 132 |
+
setAction((prev) => ({
|
| 133 |
+
...prev,
|
| 134 |
+
drug_id_1: ids[0] || "",
|
| 135 |
+
drug_id_2: ids[1] || "",
|
| 136 |
+
target_drug_id: ids[0] || "",
|
| 137 |
+
}));
|
| 138 |
+
appendLog(`Reset task=${taskId}`);
|
| 139 |
+
} catch (err) {
|
| 140 |
+
appendLog(`Reset failed: ${err.message}`);
|
| 141 |
+
} finally {
|
| 142 |
+
setLoading(false);
|
| 143 |
+
}
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const buildActionPayload = () => {
|
| 147 |
+
if (noBudgetsLeft) {
|
| 148 |
+
return { action_type: "finish_review" };
|
| 149 |
+
}
|
| 150 |
+
if (action.action_type === "query_ddi") {
|
| 151 |
+
return {
|
| 152 |
+
action_type: "query_ddi",
|
| 153 |
+
drug_id_1: action.drug_id_1,
|
| 154 |
+
drug_id_2: action.drug_id_2,
|
| 155 |
+
};
|
| 156 |
+
}
|
| 157 |
+
if (action.action_type === "propose_intervention") {
|
| 158 |
+
return {
|
| 159 |
+
action_type: "propose_intervention",
|
| 160 |
+
target_drug_id: action.target_drug_id,
|
| 161 |
+
intervention_type: action.intervention_type,
|
| 162 |
+
proposed_new_drug_id: action.proposed_new_drug_id || undefined,
|
| 163 |
+
rationale: action.rationale || undefined,
|
| 164 |
+
};
|
| 165 |
+
}
|
| 166 |
+
return { action_type: "finish_review" };
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
const isActionValid = () => {
|
| 170 |
+
if (!hasValidEpisode) return false;
|
| 171 |
+
if (isDone) return false;
|
| 172 |
+
if (noBudgetsLeft) return true;
|
| 173 |
+
if (action.action_type === "query_ddi") {
|
| 174 |
+
return Boolean(action.drug_id_1 && action.drug_id_2);
|
| 175 |
+
}
|
| 176 |
+
if (action.action_type === "propose_intervention") {
|
| 177 |
+
return Boolean(action.target_drug_id && action.intervention_type);
|
| 178 |
+
}
|
| 179 |
+
return true;
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
const handleStep = async (overrideAction = null) => {
|
| 183 |
+
if (!hasValidEpisode) {
|
| 184 |
+
appendLog("Run Reset Episode before stepping.");
|
| 185 |
+
return;
|
| 186 |
+
}
|
| 187 |
+
setLoading(true);
|
| 188 |
+
try {
|
| 189 |
+
const payload = overrideAction || buildActionPayload();
|
| 190 |
+
const msg = await wsSend("step", payload);
|
| 191 |
+
const data = msg?.data || {};
|
| 192 |
+
const normalized = normalizeObsFromWs(data);
|
| 193 |
+
setObs(normalized);
|
| 194 |
+
appendLog(`Step: ${payload.action_type} -> reward=${data.reward ?? 0}`);
|
| 195 |
+
} catch (err) {
|
| 196 |
+
appendLog(`Step failed: ${err.message}`);
|
| 197 |
+
} finally {
|
| 198 |
+
setLoading(false);
|
| 199 |
+
}
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
const askAi = async () => {
|
| 203 |
+
if (!hasValidEpisode) {
|
| 204 |
+
appendLog("Run Reset Episode before asking AI.");
|
| 205 |
+
return;
|
| 206 |
+
}
|
| 207 |
+
setLoading(true);
|
| 208 |
+
try {
|
| 209 |
+
const data = await apiPost("/agent/suggest", { observation: obs });
|
| 210 |
+
appendLog(`AI suggestion: ${data.action.action_type}`);
|
| 211 |
+
await handleStep(data.action);
|
| 212 |
+
} catch (err) {
|
| 213 |
+
appendLog(`AI suggestion failed: ${err.message}`);
|
| 214 |
+
} finally {
|
| 215 |
+
setLoading(false);
|
| 216 |
+
}
|
| 217 |
+
};
|
| 218 |
+
|
| 219 |
+
return (
|
| 220 |
+
<div className="shell">
|
| 221 |
+
<div className="bg-orb orb-a" />
|
| 222 |
+
<div className="bg-orb orb-b" />
|
| 223 |
+
|
| 224 |
+
<div className="container">
|
| 225 |
+
<header className="topbar glass">
|
| 226 |
+
<div className="title-wrap">
|
| 227 |
+
<h1>Polypharmacy Control Center</h1>
|
| 228 |
+
</div>
|
| 229 |
+
<div className={`status-chip ${hasValidEpisode ? "live" : "idle"}`}>
|
| 230 |
+
{hasValidEpisode ? "Session Live" : "Waiting for reset"}
|
| 231 |
+
</div>
|
| 232 |
+
<div className="actions">
|
| 233 |
+
<select value={taskId} onChange={(e) => setTaskId(e.target.value)}>
|
| 234 |
+
{TASKS.map((t) => (
|
| 235 |
+
<option key={t} value={t}>
|
| 236 |
+
{t}
|
| 237 |
+
</option>
|
| 238 |
+
))}
|
| 239 |
+
</select>
|
| 240 |
+
<button onClick={handleReset} disabled={loading}>
|
| 241 |
+
Reset Episode
|
| 242 |
+
</button>
|
| 243 |
+
<button className="secondary" onClick={askAi} disabled={!hasValidEpisode || isDone || loading}>
|
| 244 |
+
Ask AI + Auto Step
|
| 245 |
+
</button>
|
| 246 |
+
</div>
|
| 247 |
+
</header>
|
| 248 |
+
|
| 249 |
+
<main className="layout">
|
| 250 |
+
<section className="panel glass panel-wide">
|
| 251 |
+
<h2>Episode</h2>
|
| 252 |
+
{hasValidEpisode ? (
|
| 253 |
+
<div className="kpi-grid">
|
| 254 |
+
<div><span>Episode</span><strong>{obs.episode_id}</strong></div>
|
| 255 |
+
<div><span>Task</span><strong>{obs.task_id}</strong></div>
|
| 256 |
+
<div><span>Age / Sex</span><strong>{obs.age} / {obs.sex}</strong></div>
|
| 257 |
+
<div><span>Step</span><strong>{obs.step_index}</strong></div>
|
| 258 |
+
<div><span>Query budget</span><strong>{obs.remaining_query_budget}</strong></div>
|
| 259 |
+
<div><span>Intervention budget</span><strong>{obs.remaining_intervention_budget}</strong></div>
|
| 260 |
+
</div>
|
| 261 |
+
) : (
|
| 262 |
+
<p className="muted">Start with Reset Episode. Until then, step actions are blocked.</p>
|
| 263 |
+
)}
|
| 264 |
+
{noBudgetsLeft && (
|
| 265 |
+
<p className="muted budget-note">Query and intervention budgets are exhausted. Finish review to get final score.</p>
|
| 266 |
+
)}
|
| 267 |
+
{isDone && (
|
| 268 |
+
<p className="muted budget-note">
|
| 269 |
+
Episode complete
|
| 270 |
+
{finalScore !== null ? ` • final score: ${finalScore.toFixed(3)}` : ""}.
|
| 271 |
+
Click Reset Episode to start a new case.
|
| 272 |
+
</p>
|
| 273 |
+
)}
|
| 274 |
+
</section>
|
| 275 |
+
|
| 276 |
+
<section className="panel glass">
|
| 277 |
+
<h2>Action Console</h2>
|
| 278 |
+
<div className="action-row">
|
| 279 |
+
<label>Action type</label>
|
| 280 |
+
<select
|
| 281 |
+
value={action.action_type}
|
| 282 |
+
onChange={(e) => setAction((a) => ({ ...a, action_type: e.target.value }))}
|
| 283 |
+
>
|
| 284 |
+
<option value="query_ddi">query_ddi</option>
|
| 285 |
+
<option value="propose_intervention">propose_intervention</option>
|
| 286 |
+
<option value="finish_review">finish_review</option>
|
| 287 |
+
</select>
|
| 288 |
+
</div>
|
| 289 |
+
|
| 290 |
+
{action.action_type === "query_ddi" && (
|
| 291 |
+
<div className="stack stack-two">
|
| 292 |
+
<input
|
| 293 |
+
placeholder="drug_id_1"
|
| 294 |
+
value={action.drug_id_1}
|
| 295 |
+
onChange={(e) => setAction((a) => ({ ...a, drug_id_1: e.target.value }))}
|
| 296 |
+
/>
|
| 297 |
+
<input
|
| 298 |
+
placeholder="drug_id_2"
|
| 299 |
+
value={action.drug_id_2}
|
| 300 |
+
onChange={(e) => setAction((a) => ({ ...a, drug_id_2: e.target.value }))}
|
| 301 |
+
/>
|
| 302 |
+
</div>
|
| 303 |
+
)}
|
| 304 |
+
|
| 305 |
+
{action.action_type === "propose_intervention" && (
|
| 306 |
+
<div className="stack">
|
| 307 |
+
<select
|
| 308 |
+
value={action.target_drug_id}
|
| 309 |
+
onChange={(e) => setAction((a) => ({ ...a, target_drug_id: e.target.value }))}
|
| 310 |
+
>
|
| 311 |
+
<option value="">Select target drug</option>
|
| 312 |
+
{medIds.map((id) => (
|
| 313 |
+
<option key={id} value={id}>
|
| 314 |
+
{id}
|
| 315 |
+
</option>
|
| 316 |
+
))}
|
| 317 |
+
</select>
|
| 318 |
+
<select
|
| 319 |
+
value={action.intervention_type}
|
| 320 |
+
onChange={(e) => setAction((a) => ({ ...a, intervention_type: e.target.value }))}
|
| 321 |
+
>
|
| 322 |
+
<option value="stop">stop</option>
|
| 323 |
+
<option value="dose_reduce">dose_reduce</option>
|
| 324 |
+
<option value="substitute">substitute</option>
|
| 325 |
+
<option value="add_monitoring">add_monitoring</option>
|
| 326 |
+
</select>
|
| 327 |
+
<input
|
| 328 |
+
placeholder="proposed_new_drug_id (optional)"
|
| 329 |
+
value={action.proposed_new_drug_id}
|
| 330 |
+
onChange={(e) =>
|
| 331 |
+
setAction((a) => ({ ...a, proposed_new_drug_id: e.target.value }))
|
| 332 |
+
}
|
| 333 |
+
/>
|
| 334 |
+
<input
|
| 335 |
+
placeholder="rationale (optional)"
|
| 336 |
+
value={action.rationale}
|
| 337 |
+
onChange={(e) => setAction((a) => ({ ...a, rationale: e.target.value }))}
|
| 338 |
+
/>
|
| 339 |
+
</div>
|
| 340 |
+
)}
|
| 341 |
+
<button onClick={() => handleStep()} disabled={!isActionValid() || loading}>
|
| 342 |
+
{noBudgetsLeft ? "Finish Review" : "Submit Step"}
|
| 343 |
+
</button>
|
| 344 |
+
</section>
|
| 345 |
+
|
| 346 |
+
<section className="panel glass">
|
| 347 |
+
<h2>Current Medications</h2>
|
| 348 |
+
<div className="med-grid">
|
| 349 |
+
{(obs?.current_medications || []).map((m) => (
|
| 350 |
+
<div key={m.drug_id} className="med-card">
|
| 351 |
+
<strong>{m.drug_id}</strong>
|
| 352 |
+
<p>{m.generic_name}</p>
|
| 353 |
+
<small>{m.dose_mg} mg • {m.atc_class}</small>
|
| 354 |
+
</div>
|
| 355 |
+
))}
|
| 356 |
+
</div>
|
| 357 |
+
</section>
|
| 358 |
+
|
| 359 |
+
<section className="panel glass">
|
| 360 |
+
<h2>Event Log</h2>
|
| 361 |
+
<div className="logs">
|
| 362 |
+
{log.map((line, idx) => (
|
| 363 |
+
<div key={idx}>{line}</div>
|
| 364 |
+
))}
|
| 365 |
+
</div>
|
| 366 |
+
</section>
|
| 367 |
+
</main>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
);
|
| 371 |
+
}
|
openenv-polypharmacy/frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import ReactDOM from "react-dom/client";
|
| 3 |
+
import App from "./App";
|
| 4 |
+
import "./styles.css";
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>
|
| 10 |
+
);
|
openenv-polypharmacy/frontend/src/styles.css
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #eef5ff;
|
| 3 |
+
--panel: rgba(255, 255, 255, 0.82);
|
| 4 |
+
--panel-solid: #ffffff;
|
| 5 |
+
--text: #0b2445;
|
| 6 |
+
--muted: #5b7596;
|
| 7 |
+
--primary: #1f8bff;
|
| 8 |
+
--primary-2: #69beff;
|
| 9 |
+
--accent: #0dd3ff;
|
| 10 |
+
--border: rgba(93, 156, 219, 0.22);
|
| 11 |
+
--shadow: 0 20px 50px rgba(25, 83, 143, 0.12);
|
| 12 |
+
--shadow-strong: 0 20px 42px rgba(31, 112, 182, 0.24);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
* {
|
| 16 |
+
box-sizing: border-box;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
body {
|
| 20 |
+
margin: 0;
|
| 21 |
+
font-family: "Inter", "SF Pro Text", "Segoe UI", sans-serif;
|
| 22 |
+
background:
|
| 23 |
+
radial-gradient(circle at 8% 0%, #cce7ff 0%, rgba(204, 231, 255, 0) 42%),
|
| 24 |
+
radial-gradient(circle at 92% 100%, #d5efff 0%, rgba(213, 239, 255, 0) 42%),
|
| 25 |
+
var(--bg);
|
| 26 |
+
color: var(--text);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.shell {
|
| 30 |
+
min-height: 100vh;
|
| 31 |
+
position: relative;
|
| 32 |
+
padding: 28px 22px;
|
| 33 |
+
overflow: hidden;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.container {
|
| 37 |
+
width: min(1300px, 100%);
|
| 38 |
+
margin: 0 auto;
|
| 39 |
+
position: relative;
|
| 40 |
+
z-index: 1;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.bg-orb {
|
| 44 |
+
position: absolute;
|
| 45 |
+
border-radius: 50%;
|
| 46 |
+
filter: blur(18px);
|
| 47 |
+
opacity: 0.9;
|
| 48 |
+
}
|
| 49 |
+
.orb-a {
|
| 50 |
+
width: 420px;
|
| 51 |
+
height: 420px;
|
| 52 |
+
right: -120px;
|
| 53 |
+
top: -100px;
|
| 54 |
+
background: radial-gradient(circle, rgba(72, 168, 255, 0.5), rgba(72, 168, 255, 0.1));
|
| 55 |
+
}
|
| 56 |
+
.orb-b {
|
| 57 |
+
width: 360px;
|
| 58 |
+
height: 360px;
|
| 59 |
+
left: -100px;
|
| 60 |
+
bottom: -120px;
|
| 61 |
+
background: radial-gradient(circle, rgba(110, 200, 255, 0.4), rgba(141, 205, 255, 0.06));
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.glass {
|
| 65 |
+
backdrop-filter: blur(12px);
|
| 66 |
+
border: 1px solid var(--border);
|
| 67 |
+
background: var(--panel);
|
| 68 |
+
box-shadow: var(--shadow);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.topbar {
|
| 72 |
+
border-radius: 20px;
|
| 73 |
+
padding: 18px;
|
| 74 |
+
display: grid;
|
| 75 |
+
grid-template-columns: 1.2fr auto 1fr;
|
| 76 |
+
justify-content: space-between;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 12px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.title-wrap h1 {
|
| 82 |
+
margin: 0;
|
| 83 |
+
font-size: clamp(1.1rem, 1.5vw, 1.45rem);
|
| 84 |
+
letter-spacing: 0.01em;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.title-wrap p {
|
| 88 |
+
margin: 4px 0 0;
|
| 89 |
+
color: var(--muted);
|
| 90 |
+
font-size: 0.92rem;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.status-chip {
|
| 94 |
+
justify-self: center;
|
| 95 |
+
border-radius: 999px;
|
| 96 |
+
padding: 7px 12px;
|
| 97 |
+
font-size: 0.76rem;
|
| 98 |
+
font-weight: 700;
|
| 99 |
+
letter-spacing: 0.04em;
|
| 100 |
+
text-transform: uppercase;
|
| 101 |
+
border: 1px solid transparent;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.status-chip.live {
|
| 105 |
+
color: #0d6a3f;
|
| 106 |
+
background: rgba(130, 245, 195, 0.18);
|
| 107 |
+
border-color: rgba(70, 199, 142, 0.3);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.status-chip.idle {
|
| 111 |
+
color: #24527f;
|
| 112 |
+
background: rgba(114, 194, 255, 0.18);
|
| 113 |
+
border-color: rgba(62, 152, 223, 0.28);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.actions {
|
| 117 |
+
display: flex;
|
| 118 |
+
justify-content: flex-end;
|
| 119 |
+
gap: 10px;
|
| 120 |
+
flex-wrap: wrap;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
button,
|
| 124 |
+
select,
|
| 125 |
+
input {
|
| 126 |
+
border: 1px solid var(--border);
|
| 127 |
+
border-radius: 12px;
|
| 128 |
+
padding: 10px 13px;
|
| 129 |
+
font-size: 0.92rem;
|
| 130 |
+
background: #fff;
|
| 131 |
+
color: var(--text);
|
| 132 |
+
min-height: 42px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
button {
|
| 136 |
+
cursor: pointer;
|
| 137 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-2) 78%, var(--accent) 100%);
|
| 138 |
+
color: #fff;
|
| 139 |
+
border: none;
|
| 140 |
+
font-weight: 700;
|
| 141 |
+
box-shadow: var(--shadow-strong);
|
| 142 |
+
transition: transform 120ms ease, filter 120ms ease;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
button:hover {
|
| 146 |
+
transform: translateY(-1px);
|
| 147 |
+
filter: brightness(1.02);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
button.secondary {
|
| 151 |
+
background: linear-gradient(135deg, #68c2ff, #9dd9ff);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
button:disabled {
|
| 155 |
+
opacity: 0.58;
|
| 156 |
+
cursor: not-allowed;
|
| 157 |
+
transform: none;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.layout {
|
| 161 |
+
margin-top: 18px;
|
| 162 |
+
display: grid;
|
| 163 |
+
gap: 16px;
|
| 164 |
+
grid-template-columns: 1.15fr 0.85fr;
|
| 165 |
+
align-items: start;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.panel {
|
| 169 |
+
border-radius: 18px;
|
| 170 |
+
padding: 18px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.panel-wide {
|
| 174 |
+
grid-column: 1 / -1;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.panel h2 {
|
| 178 |
+
margin: 0 0 12px;
|
| 179 |
+
font-size: 1rem;
|
| 180 |
+
letter-spacing: 0.01em;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.kpi-grid {
|
| 184 |
+
display: grid;
|
| 185 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 186 |
+
gap: 12px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.kpi-grid div {
|
| 190 |
+
background: rgba(255, 255, 255, 0.9);
|
| 191 |
+
border: 1px solid var(--border);
|
| 192 |
+
border-radius: 14px;
|
| 193 |
+
padding: 12px;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.kpi-grid span {
|
| 197 |
+
display: block;
|
| 198 |
+
font-size: 0.74rem;
|
| 199 |
+
color: var(--muted);
|
| 200 |
+
margin-bottom: 4px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.kpi-grid strong {
|
| 204 |
+
font-size: 1.05rem;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.action-row,
|
| 208 |
+
.stack {
|
| 209 |
+
display: grid;
|
| 210 |
+
gap: 10px;
|
| 211 |
+
margin-bottom: 12px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.stack-two {
|
| 215 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.med-grid {
|
| 219 |
+
display: grid;
|
| 220 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 221 |
+
gap: 10px;
|
| 222 |
+
max-height: 420px;
|
| 223 |
+
overflow: auto;
|
| 224 |
+
padding-right: 2px;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.med-card {
|
| 228 |
+
border: 1px solid var(--border);
|
| 229 |
+
border-radius: 14px;
|
| 230 |
+
padding: 12px;
|
| 231 |
+
background: var(--panel-solid);
|
| 232 |
+
transition: transform 120ms ease, box-shadow 120ms ease;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.med-card:hover {
|
| 236 |
+
transform: translateY(-1px);
|
| 237 |
+
box-shadow: 0 10px 25px rgba(44, 105, 165, 0.12);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.med-card p {
|
| 241 |
+
margin: 6px 0 4px;
|
| 242 |
+
color: var(--muted);
|
| 243 |
+
text-transform: capitalize;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.logs {
|
| 247 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
| 248 |
+
font-size: 0.85rem;
|
| 249 |
+
max-height: 300px;
|
| 250 |
+
overflow: auto;
|
| 251 |
+
display: grid;
|
| 252 |
+
gap: 6px;
|
| 253 |
+
padding-right: 2px;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.logs div {
|
| 257 |
+
background: rgba(255, 255, 255, 0.78);
|
| 258 |
+
border: 1px solid var(--border);
|
| 259 |
+
border-radius: 10px;
|
| 260 |
+
padding: 8px 10px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.muted {
|
| 264 |
+
color: var(--muted);
|
| 265 |
+
margin: 0;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.budget-note {
|
| 269 |
+
margin-top: 10px;
|
| 270 |
+
padding: 10px 12px;
|
| 271 |
+
border: 1px solid var(--border);
|
| 272 |
+
border-radius: 12px;
|
| 273 |
+
background: rgba(255, 255, 255, 0.78);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
@media (max-width: 1120px) {
|
| 277 |
+
.layout {
|
| 278 |
+
grid-template-columns: 1fr;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.topbar {
|
| 282 |
+
grid-template-columns: 1fr;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.status-chip {
|
| 286 |
+
justify-self: start;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.actions {
|
| 290 |
+
justify-content: flex-start;
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
@media (max-width: 760px) {
|
| 295 |
+
.shell {
|
| 296 |
+
padding: 18px 12px;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.kpi-grid,
|
| 300 |
+
.med-grid,
|
| 301 |
+
.stack-two {
|
| 302 |
+
grid-template-columns: 1fr;
|
| 303 |
+
}
|
| 304 |
+
}
|
openenv-polypharmacy/frontend/vite.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import react from "@vitejs/plugin-react";
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
port: 5173,
|
| 8 |
+
host: "0.0.0.0",
|
| 9 |
+
},
|
| 10 |
+
});
|
openenv-polypharmacy/inference.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Baseline LLM inference script for the PolypharmacyEnv.
|
| 3 |
+
|
| 4 |
+
Uses Groq's OpenAI-compatible Chat Completions API to drive an LLM agent through the
|
| 5 |
+
PolypharmacyEnv HTTP API. Emits structured stdout logs in the
|
| 6 |
+
[START], [STEP], [END] format required by the OpenEnv evaluation spec.
|
| 7 |
+
|
| 8 |
+
Environment variables:
|
| 9 |
+
GROQ_API_KEY – required
|
| 10 |
+
GROQ_BASE_URL – optional (default: https://api.groq.com/openai/v1)
|
| 11 |
+
GROQ_MODEL_NAME – model to use (default: llama-3.1-8b-instant)
|
| 12 |
+
POLYPHARMACY_ENV_URL – environment HTTP base URL (default: http://localhost:7860)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
import uuid
|
| 21 |
+
from typing import Any, Dict, List
|
| 22 |
+
|
| 23 |
+
import requests
|
| 24 |
+
from openai import OpenAI
|
| 25 |
+
|
| 26 |
+
# ── Configuration ────────────────────────────────────────────────────────────
|
| 27 |
+
|
| 28 |
+
MODEL = os.environ.get("GROQ_MODEL_NAME", "llama-3.1-8b-instant")
|
| 29 |
+
API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 30 |
+
API_BASE = os.environ.get("GROQ_BASE_URL", "https://api.groq.com/openai/v1")
|
| 31 |
+
ENV_URL = os.environ.get("POLYPHARMACY_ENV_URL", "http://localhost:7860")
|
| 32 |
+
|
| 33 |
+
TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
|
| 34 |
+
EPISODES_PER_TASK = 5
|
| 35 |
+
|
| 36 |
+
client = OpenAI(api_key=API_KEY, base_url=API_BASE)
|
| 37 |
+
|
| 38 |
+
# ── Logging helpers ──────────────────────────────────────────────────────────
|
| 39 |
+
|
| 40 |
+
def _log(tag: str, payload: Dict[str, Any]) -> None:
|
| 41 |
+
print(f"[{tag}] {json.dumps(payload, default=str)}", flush=True)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _err(msg: str) -> None:
|
| 45 |
+
print(msg, file=sys.stderr, flush=True)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ── Environment HTTP helpers ─────────────────────────────────────────────────
|
| 49 |
+
|
| 50 |
+
def env_reset(task_id: str) -> Dict[str, Any]:
|
| 51 |
+
resp = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id}, timeout=30)
|
| 52 |
+
resp.raise_for_status()
|
| 53 |
+
return resp.json()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def env_step(action: Dict[str, Any]) -> Dict[str, Any]:
|
| 57 |
+
resp = requests.post(f"{ENV_URL}/step", json={"action": action}, timeout=30)
|
| 58 |
+
resp.raise_for_status()
|
| 59 |
+
return resp.json()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ── Observation → prompt ─────────────────────────────────────────────────────
|
| 63 |
+
|
| 64 |
+
SYSTEM_PROMPT = """\
|
| 65 |
+
You are a clinical pharmacist AI assistant reviewing an elderly patient's medication regimen.
|
| 66 |
+
You must reduce drug-interaction risk and address Beers-criteria violations while minimising
|
| 67 |
+
unnecessary medication changes.
|
| 68 |
+
|
| 69 |
+
Available actions (respond with STRICT JSON, no extra text):
|
| 70 |
+
1. Query a drug pair for interactions:
|
| 71 |
+
{"action_type": "query_ddi", "drug_id_1": "...", "drug_id_2": "..."}
|
| 72 |
+
|
| 73 |
+
2. Propose an intervention:
|
| 74 |
+
{"action_type": "propose_intervention", "target_drug_id": "...",
|
| 75 |
+
"intervention_type": "stop|dose_reduce|substitute|add_monitoring",
|
| 76 |
+
"proposed_new_drug_id": "...(optional)", "rationale": "..."}
|
| 77 |
+
|
| 78 |
+
3. Finish the review:
|
| 79 |
+
{"action_type": "finish_review"}
|
| 80 |
+
|
| 81 |
+
Respond with EXACTLY ONE JSON object per turn. No markdown, no explanation outside JSON.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _summarise_obs(obs: Dict[str, Any]) -> str:
|
| 86 |
+
meds = obs.get("current_medications", [])
|
| 87 |
+
med_summary = "; ".join(
|
| 88 |
+
f"{m['drug_id']}({m['generic_name']},{m['dose_mg']}mg)"
|
| 89 |
+
for m in meds
|
| 90 |
+
)
|
| 91 |
+
queries = obs.get("interaction_queries", [])
|
| 92 |
+
q_summary = "; ".join(
|
| 93 |
+
f"{q['drug_id_1']}+{q['drug_id_2']}={q.get('severity','?')}"
|
| 94 |
+
for q in queries
|
| 95 |
+
)
|
| 96 |
+
interventions = obs.get("interventions", [])
|
| 97 |
+
iv_summary = "; ".join(
|
| 98 |
+
f"{iv['action_type']}({iv['target_drug_id']})"
|
| 99 |
+
for iv in interventions
|
| 100 |
+
)
|
| 101 |
+
return (
|
| 102 |
+
f"Patient: age={obs.get('age')}, sex={obs.get('sex')}, "
|
| 103 |
+
f"conditions={obs.get('conditions')}, "
|
| 104 |
+
f"eGFR={obs.get('eGFR_category')}, liver={obs.get('liver_function_category')}\n"
|
| 105 |
+
f"Medications: {med_summary}\n"
|
| 106 |
+
f"Queries so far: {q_summary or 'none'}\n"
|
| 107 |
+
f"Interventions so far: {iv_summary or 'none'}\n"
|
| 108 |
+
f"Remaining query budget: {obs.get('remaining_query_budget')}\n"
|
| 109 |
+
f"Remaining intervention budget: {obs.get('remaining_intervention_budget')}\n"
|
| 110 |
+
f"Step: {obs.get('step_index')}"
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# ── LLM call ─────────────────────────────────────────────────────────────────
|
| 115 |
+
|
| 116 |
+
def _ask_llm(obs_summary: str) -> Dict[str, Any]:
|
| 117 |
+
"""Call the LLM and parse a PolypharmacyAction JSON."""
|
| 118 |
+
try:
|
| 119 |
+
chat_resp = client.chat.completions.create(
|
| 120 |
+
model=MODEL,
|
| 121 |
+
messages=[
|
| 122 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 123 |
+
{"role": "user", "content": obs_summary},
|
| 124 |
+
],
|
| 125 |
+
max_tokens=256,
|
| 126 |
+
temperature=0.2,
|
| 127 |
+
)
|
| 128 |
+
text = (chat_resp.choices[0].message.content or "").strip()
|
| 129 |
+
# Strip markdown fences if present
|
| 130 |
+
text = text.strip()
|
| 131 |
+
if text.startswith("```"):
|
| 132 |
+
text = text.split("\n", 1)[-1]
|
| 133 |
+
if text.endswith("```"):
|
| 134 |
+
text = text.rsplit("```", 1)[0]
|
| 135 |
+
text = text.strip()
|
| 136 |
+
return json.loads(text)
|
| 137 |
+
except Exception as e:
|
| 138 |
+
_err(f"LLM parse error: {e}")
|
| 139 |
+
return {"action_type": "finish_review"}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# ── Main loop ────────────────────────────────────────────────────────────────
|
| 143 |
+
|
| 144 |
+
def main() -> None:
|
| 145 |
+
if not API_KEY:
|
| 146 |
+
_err("GROQ_API_KEY is required")
|
| 147 |
+
sys.exit(1)
|
| 148 |
+
|
| 149 |
+
run_id = str(uuid.uuid4())[:8]
|
| 150 |
+
|
| 151 |
+
for task_id in TASKS:
|
| 152 |
+
task_scores: List[float] = []
|
| 153 |
+
task_rewards: List[float] = []
|
| 154 |
+
|
| 155 |
+
_log("START", {
|
| 156 |
+
"run_id": run_id,
|
| 157 |
+
"task_id": task_id,
|
| 158 |
+
"model": MODEL,
|
| 159 |
+
"episodes": EPISODES_PER_TASK,
|
| 160 |
+
})
|
| 161 |
+
|
| 162 |
+
for ep_idx in range(EPISODES_PER_TASK):
|
| 163 |
+
reset_resp = env_reset(task_id)
|
| 164 |
+
obs = reset_resp["observation"]
|
| 165 |
+
done = reset_resp.get("done", False)
|
| 166 |
+
episode_id = obs.get("episode_id", f"ep_{ep_idx}")
|
| 167 |
+
total_reward = 0.0
|
| 168 |
+
step_idx = 0
|
| 169 |
+
|
| 170 |
+
while not done:
|
| 171 |
+
obs_summary = _summarise_obs(obs)
|
| 172 |
+
action_payload = _ask_llm(obs_summary)
|
| 173 |
+
|
| 174 |
+
step_resp = env_step(action_payload)
|
| 175 |
+
obs = step_resp["observation"]
|
| 176 |
+
reward = step_resp.get("reward", 0.0)
|
| 177 |
+
done = step_resp.get("done", False)
|
| 178 |
+
total_reward += reward
|
| 179 |
+
|
| 180 |
+
_log("STEP", {
|
| 181 |
+
"run_id": run_id,
|
| 182 |
+
"task_id": task_id,
|
| 183 |
+
"episode_id": episode_id,
|
| 184 |
+
"step_index": step_idx,
|
| 185 |
+
"observation_summary": obs_summary[:200],
|
| 186 |
+
"action_payload": action_payload,
|
| 187 |
+
"reward": reward,
|
| 188 |
+
"done": done,
|
| 189 |
+
})
|
| 190 |
+
|
| 191 |
+
step_idx += 1
|
| 192 |
+
|
| 193 |
+
grader_score = step_resp.get("info", {}).get("grader_score", 0.0)
|
| 194 |
+
task_scores.append(grader_score)
|
| 195 |
+
task_rewards.append(total_reward)
|
| 196 |
+
|
| 197 |
+
_log("END", {
|
| 198 |
+
"run_id": run_id,
|
| 199 |
+
"task_id": task_id,
|
| 200 |
+
"episodes": EPISODES_PER_TASK,
|
| 201 |
+
"avg_grader_score": sum(task_scores) / max(len(task_scores), 1),
|
| 202 |
+
"avg_total_reward": sum(task_rewards) / max(len(task_rewards), 1),
|
| 203 |
+
"per_episode_scores": task_scores,
|
| 204 |
+
})
|
| 205 |
+
|
| 206 |
+
_err("Inference complete.")
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
if __name__ == "__main__":
|
| 210 |
+
main()
|
openenv-polypharmacy/openenv.yaml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: polypharmacy_env
|
| 3 |
+
version: "0.1.0"
|
| 4 |
+
description: >
|
| 5 |
+
An OpenEnv environment that simulates elderly polypharmacy medication review.
|
| 6 |
+
An RL agent acts as a clinical pharmacist assistant, identifying dangerous
|
| 7 |
+
drug-drug interactions, Beers-criteria violations, and proposing safe
|
| 8 |
+
interventions (stop, dose-reduce, substitute, monitor).
|
| 9 |
+
author: "PolypharmacyEnv Team"
|
| 10 |
+
tags:
|
| 11 |
+
- healthcare
|
| 12 |
+
- polypharmacy
|
| 13 |
+
- openenv
|
| 14 |
+
type: space
|
| 15 |
+
runtime: fastapi
|
| 16 |
+
app: backend.main:app
|
| 17 |
+
port: 7860
|
| 18 |
+
|
| 19 |
+
tasks:
|
| 20 |
+
- id: easy_screening
|
| 21 |
+
description: "Small regimen (3-5 drugs) with one severe DDI. Identify and resolve it."
|
| 22 |
+
difficulty: easy
|
| 23 |
+
|
| 24 |
+
- id: budgeted_screening
|
| 25 |
+
description: "Medium regimen (6-10 drugs) with multiple DDIs and Beers issues under query/intervention budgets."
|
| 26 |
+
difficulty: medium
|
| 27 |
+
|
| 28 |
+
- id: complex_tradeoff
|
| 29 |
+
description: "Large regimen (10-15 drugs) including critical drugs. Balance risk reduction against regimen disruption."
|
| 30 |
+
difficulty: hard
|
openenv-polypharmacy/pyproject.toml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "polypharmacy-env"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "OpenEnv environment for elderly polypharmacy medication-review safety"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"fastapi>=0.104.0",
|
| 12 |
+
"uvicorn>=0.24.0",
|
| 13 |
+
"pydantic>=2.0.0",
|
| 14 |
+
"requests>=2.31.0",
|
| 15 |
+
"openai>=1.0.0",
|
| 16 |
+
"python-dotenv>=1.0.0",
|
| 17 |
+
"openenv-core>=0.2.0",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[project.optional-dependencies]
|
| 21 |
+
dev = [
|
| 22 |
+
"pytest>=7.0.0",
|
| 23 |
+
"httpx>=0.25.0",
|
| 24 |
+
"black",
|
| 25 |
+
"isort",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
[tool.setuptools.packages.find]
|
| 29 |
+
where = ["backend/src"]
|
| 30 |
+
|
| 31 |
+
[tool.pytest.ini_options]
|
| 32 |
+
testpaths = ["backend/src/polypharmacy_env/tests"]
|
| 33 |
+
pythonpath = ["backend/src"]
|
| 34 |
+
|
| 35 |
+
[tool.black]
|
| 36 |
+
line-length = 99
|
| 37 |
+
target-version = ["py310"]
|
| 38 |
+
|
| 39 |
+
[tool.isort]
|
| 40 |
+
profile = "black"
|
| 41 |
+
line_length = 99
|
openenv-polypharmacy/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
-r backend/requirements.txt
|
openenv-polypharmacy/scripts/dev_backend.sh
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
uvicorn backend.main:app --reload --host 0.0.0.0 --port 7860
|