diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..7566d677dc0c4ad7c9c7bc6a176c6b7186e9c728
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+.git
+.gitignore
+**/__pycache__
+**/*.pyc
+**/.pytest_cache
+**/.mypy_cache
+**/.ruff_cache
+**/node_modules
+**/dist
+**/.env
+**/.env.*
+!openenv-polypharmacy/.env.example
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,35 @@
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
index 0bb8010f237a644afe235484f689fc21c449cca4..a21ac13278960096fd010e674651b109be6c6cc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,35 @@
+# --- Python ---
venv/
+.venv/
+env/
+.env
+.env.*
+!openenv-polypharmacy/.env.example
+*.py[cod]
+__pycache__/
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+.coverage
+coverage.xml
+
+# --- Node / frontend ---
+node_modules/
+**/node_modules/
+frontend/dist/
+**/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# --- Build / temp ---
+*.log
+*.tmp
+*.swp
.DS_Store
-__pycache__/
\ No newline at end of file
+
+# --- Project-specific nested paths ---
+openenv-polypharmacy/frontend/node_modules/
+openenv-polypharmacy/frontend/dist/
+openenv-polypharmacy/.pytest_cache/
diff --git a/.gitignore copy b/.gitignore copy
new file mode 100644
index 0000000000000000000000000000000000000000..a21ac13278960096fd010e674651b109be6c6cc4
--- /dev/null
+++ b/.gitignore copy
@@ -0,0 +1,35 @@
+# --- Python ---
+venv/
+.venv/
+env/
+.env
+.env.*
+!openenv-polypharmacy/.env.example
+*.py[cod]
+__pycache__/
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+.coverage
+coverage.xml
+
+# --- Node / frontend ---
+node_modules/
+**/node_modules/
+frontend/dist/
+**/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# --- Build / temp ---
+*.log
+*.tmp
+*.swp
+.DS_Store
+
+# --- Project-specific nested paths ---
+openenv-polypharmacy/frontend/node_modules/
+openenv-polypharmacy/frontend/dist/
+openenv-polypharmacy/.pytest_cache/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..10f199b9446b1375bce69953826c903f9ed43efe
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,39 @@
+FROM node:20-alpine AS frontend-builder
+WORKDIR /app/frontend
+COPY openenv-polypharmacy/frontend/package*.json ./
+RUN npm ci
+COPY openenv-polypharmacy/frontend/ ./
+RUN npm run build
+
+FROM python:3.11-slim
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends build-essential curl && \
+ rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+COPY openenv-polypharmacy/backend/requirements.txt /app/backend/requirements.txt
+RUN pip install --no-cache-dir -r /app/backend/requirements.txt
+
+COPY openenv-polypharmacy/backend /app/backend
+COPY openenv-polypharmacy/data /app/data
+COPY openenv-polypharmacy/scripts /app/scripts
+COPY openenv-polypharmacy/openenv.yaml /app/openenv.yaml
+COPY openenv-polypharmacy/.env.example /app/.env.example
+COPY openenv-polypharmacy/inference.py /app/inference.py
+
+COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
+
+RUN python3 /app/scripts/preprocess_data.py
+
+ENV PORT=7860
+ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
+ENV PYTHONUNBUFFERED=1
+
+EXPOSE 7860
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
+ CMD curl -f http://localhost:7860/health || exit 1
+
+CMD ["sh", "-c", "uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000000000000000000000000000000000000..dddbf4047eacd2e74adc50df3b8cf73f80f162f9
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,245 @@
+# PolypharmacyEnv
+
+Monorepo for an OpenEnv-compatible medication safety environment with:
+
+- a FastAPI backend (`backend/`)
+- a React frontend (`frontend/`)
+- data assets (`data/`)
+- utility scripts (`scripts/`)
+
+---
+
+## Repository Structure
+
+```text
+openenv-polypharmacy/
+ backend/
+ main.py # ASGI entrypoint (uvicorn target)
+ requirements.txt # Backend dependencies
+ Dockerfile # Backend container
+ src/polypharmacy_env/ # Python package source
+ api/
+ app.py # FastAPI/OpenEnv app assembly
+ server.py # Compatibility import wrapper
+ routes/agent.py # /agent/suggest route
+ services/
+ groq_agent.py # Groq-based action suggestion logic
+ env_core.py # OpenEnv environment core
+ models.py # Action/observation/state models
+ data_loader.py # CSV loading
+ ddi_simulator.py # DDI and Beers lookups
+ rewards.py # Reward shaping
+ graders.py # Task graders
+ tasks.py # Task/episode selection
+ tests/ # Backend tests
+ frontend/
+ src/ # React UI code
+ package.json
+ Dockerfile # Frontend container
+ data/
+ lookups/ # drug_metadata.csv, ddi_rules.csv, beers_criteria.csv
+ processed/ # patients_polypharmacy.csv
+ scripts/
+ preprocess_data.py # Synthetic data generation
+ dev_backend.sh # Local backend run helper
+ dev_frontend.sh # Local frontend run helper
+ run_validation.sh # Tests + baseline validation
+ docker-compose.yml # Full stack orchestration
+ openenv.yaml # OpenEnv manifest
+ inference.py # Optional CLI inference baseline
+ .env.example # Environment template
+```
+
+---
+
+## What It Does
+
+The environment simulates elderly polypharmacy review. Agent actions:
+
+- `query_ddi`
+- `propose_intervention`
+- `finish_review`
+
+Supported tasks:
+
+- `easy_screening`
+- `budgeted_screening`
+- `complex_tradeoff`
+
+---
+
+## Prerequisites
+
+- Python 3.10+
+- Node.js 18+ (or 20+ recommended)
+- npm
+- Docker + Docker Compose (optional, for containerized run)
+
+---
+
+## Environment Setup
+
+Create `.env`:
+
+```bash
+cp .env.example .env
+```
+
+Set values:
+
+- `GROQ_API_KEY=...` (required)
+- `GROQ_BASE_URL=https://api.groq.com/openai/v1` (recommended)
+- `GROQ_MODEL_NAME=llama-3.3-70b-versatile` (recommended)
+
+---
+
+## Local Run (Recommended During Development)
+
+### 1) Install dependencies
+
+Backend:
+
+```bash
+pip install -r backend/requirements.txt
+```
+
+Frontend:
+
+```bash
+cd frontend
+npm install
+cd ..
+```
+
+### 2) Generate/update synthetic data (if needed)
+
+```bash
+python scripts/preprocess_data.py
+```
+
+### 3) Start services in two terminals
+
+Terminal A:
+
+```bash
+./scripts/dev_backend.sh
+```
+
+Terminal B:
+
+```bash
+./scripts/dev_frontend.sh
+```
+
+### 4) Open app
+
+- Frontend: [http://localhost:5173](http://localhost:5173)
+- Backend health: [http://localhost:7860/health](http://localhost:7860/health)
+
+---
+
+## Docker Run
+
+Run both services:
+
+```bash
+docker compose up --build
+```
+
+Stop:
+
+```bash
+docker compose down
+```
+
+Ports:
+
+- backend: `7860`
+- frontend: `5173`
+
+---
+
+## Hugging Face Spaces Deployment (Docker)
+
+This repo now includes a **root `Dockerfile`** that builds frontend + backend into one container, so Spaces can host both API and UI together.
+
+### 1) Create a new Space
+
+- Go to [Hugging Face Spaces](https://huggingface.co/new-space)
+- Choose **Docker** SDK
+- Create the Space
+
+### 2) Add Space secrets/variables
+
+In Space Settings -> Variables and Secrets:
+
+- Secret: `GROQ_API_KEY`
+- Variable: `GROQ_BASE_URL=https://api.groq.com/openai/v1`
+- Variable: `GROQ_MODEL_NAME=llama-3.3-70b-versatile`
+
+### 3) Push this repository to the Space
+
+Commit and push all files, including root `Dockerfile`.
+
+### 4) Verify after build
+
+- Space root URL loads the React UI
+- `/health` returns healthy status
+- OpenEnv endpoints are available (`/reset`, `/step`, `/state`, `/schema`)
+
+Notes:
+
+- Container reads `PORT` (defaults to `7860`) which is Space-friendly.
+- Frontend static assets are served by FastAPI from `frontend/dist`.
+
+---
+
+## API Endpoints
+
+OpenEnv/health:
+
+- `POST /reset`
+- `POST /step`
+- `GET /state`
+- `GET /health`
+- `GET /schema`
+- `WS /ws` (stateful session)
+
+AI helper:
+
+- `POST /agent/suggest`
+
+---
+
+## Testing
+
+Run backend tests:
+
+```bash
+python -m pytest backend/src/polypharmacy_env/tests -v
+```
+
+Or run validation script:
+
+```bash
+./scripts/run_validation.sh
+```
+
+---
+
+## Notes
+
+- OpenEnv HTTP reset/step is stateless; multi-step episode continuity should use websocket (`/ws`).
+- The frontend uses websocket for episode continuity and HTTP for AI suggestion.
+- AI behavior includes rule-based guardrails to avoid repetitive low-value loops.
+
+---
+
+## Troubleshooting
+
+- `ModuleNotFoundError: polypharmacy_env`
+ - Start backend using `./scripts/dev_backend.sh` from repo root.
+- `/agent/suggest` fails
+ - Check `.env` keys and restart backend.
+- UI state looks stale
+ - Hard refresh browser and click `Reset Episode`.
diff --git a/openenv-polypharmacy/.dockerignore b/openenv-polypharmacy/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..5007867e3a3b2c5514c4ff5bb18588b36951901f
--- /dev/null
+++ b/openenv-polypharmacy/.dockerignore
@@ -0,0 +1,8 @@
+.git
+.gitignore
+**/__pycache__/
+**/.pytest_cache/
+**/.DS_Store
+.env
+frontend/node_modules
+frontend/dist
diff --git a/openenv-polypharmacy/.env.example b/openenv-polypharmacy/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..1ef8a70cdf5d21640d8898e26a82e60734b583ff
--- /dev/null
+++ b/openenv-polypharmacy/.env.example
@@ -0,0 +1,3 @@
+GROQ_API_KEY=your_groq_api_key_here
+GROQ_BASE_URL=https://api.groq.com/openai/v1
+GROQ_MODEL_NAME=llama-3.3-70b-versatile
diff --git a/openenv-polypharmacy/Dockerfile b/openenv-polypharmacy/Dockerfile
index 8ebec352b765d69a459db95f91fd4a07427a979f..68b69d986a780501f6c9461410b2add26413473e 100644
--- a/openenv-polypharmacy/Dockerfile
+++ b/openenv-polypharmacy/Dockerfile
@@ -1,30 +1,39 @@
+FROM node:20-alpine AS frontend-builder
+WORKDIR /app/frontend
+COPY frontend/package*.json ./
+RUN npm ci
+COPY frontend/ ./
+RUN npm run build
+
FROM python:3.11-slim
-# System deps
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential curl && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
-# Install Python deps first (layer caching)
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
+COPY backend/requirements.txt /app/backend/requirements.txt
+RUN pip install --no-cache-dir -r /app/backend/requirements.txt
+
+COPY backend /app/backend
+COPY data /app/data
+COPY scripts /app/scripts
+COPY openenv.yaml /app/openenv.yaml
+COPY .env.example /app/.env.example
+COPY inference.py /app/inference.py
-# Copy project
-COPY . .
+COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
-# Generate data if not present
-RUN python3 scripts/preprocess_data.py
+RUN python3 /app/scripts/preprocess_data.py
-# Environment
ENV PORT=7860
-ENV PYTHONPATH="/app/src:${PYTHONPATH}"
+ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
ENV PYTHONUNBUFFERED=1
EXPOSE 7860
-HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
CMD curl -f http://localhost:7860/health || exit 1
-CMD ["uvicorn", "polypharmacy_env.api.server:app", "--host", "0.0.0.0", "--port", "7860"]
+CMD ["sh", "-c", "uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
diff --git a/openenv-polypharmacy/PROMPT.md b/openenv-polypharmacy/PROMPT.md
deleted file mode 100644
index d7d5481b1ba6a83c8590ebcec024b911f5800982..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/PROMPT.md
+++ /dev/null
@@ -1,571 +0,0 @@
-You are an expert Python backend, ML, and infrastructure engineer.
-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).
-
-The deliverable MUST satisfy all of the following:
-- Fully compliant with the OpenEnv spec (typed models, `step()` / `reset()` / `state()`, `openenv.yaml`, HTTP server, Dockerfile).
-- Simulates a realistic healthcare workflow around elderly polypharmacy and dangerous drug combinations.
-- Defines at least **3 tasks** (easy → medium → hard) with deterministic agent graders producing scores in (0.0, 1.0).
-- Provides shaped rewards over the trajectory (not just sparse terminal rewards).
-- Includes a baseline LLM-based inference script `inference.py` in the repo root, following the evaluation requirements:
- - Uses the OpenAI Python client.
- - Reads `OPENAI_API_KEY`, `API_BASE_URL`, `MODEL_NAME`, and `HF_TOKEN` from the environment.
- - Emits structured stdout logs in the exact `[START]`, `[STEP]`, `[END]` format from the OpenEnv sample inference script.
-- Is containerized and deployable as a **Hugging Face Space** tagged with `openenv` that responds to OpenEnv-style `reset` / `step` / `state` HTTP calls.
-
-Implement everything described below.
-
-=================================================
-1. Repository and folder structure
-=================================================
-
-Create a Python package repository with this structure (names are important unless clearly labeled as examples):
-
-- `openenv-polypharmacy/`
- - `openenv.yaml`
- - `README.md`
- - `requirements.txt`
- - `Dockerfile`
- - `inference.py` # baseline LLM agent per spec
- - `pyproject.toml` or `setup.cfg` (optional but recommended)
- - `src/`
- - `polypharmacy_env/`
- - `__init__.py`
- - `config.py`
- - `models.py` # Action, Observation, State, helper models
- - `env_core.py` # PolypharmacyEnv implementation
- - `tasks.py` # task setup utilities
- - `graders.py` # deterministic graders for each task
- - `rewards.py` # reward shaping logic
- - `data_loader.py` # load/preprocess patient and lookup data
- - `ddi_simulator.py` # local DDI / guideline simulator
- - `api/`
- - `__init__.py`
- - `schemas.py` # HTTP request/response schemas
- - `server.py` # FastAPI app exposing OpenEnv endpoints
- - `baselines/`
- - `__init__.py`
- - `heuristic_agent.py` # simple rule-based baseline agent
- - `random_agent.py` # trivial random baseline (optional)
- - `tests/`
- - `__init__.py`
- - `test_env_core.py`
- - `test_api.py`
- - `data/`
- - `raw/` # placeholder for real/synthetic source data
- - `processed/`
- - `lookups/`
- - `ddi_rules.csv`
- - `beers_criteria.csv`
- - `drug_metadata.csv`
- - `scripts/`
- - `preprocess_data.py`
- - `run_validation.sh` # optional; runs OpenEnv validator, tests, etc.
-
-Use Python 3.10+ with full type hints, and keep the code black/isort-compatible.
-
-=================================================
-2. Domain, data, and clinical abstraction
-=================================================
-
-2.1. Core scenario
-
-Model an elderly patient (age ≥ 65) with:
-- Demographics: age, sex.
-- Comorbidities: e.g., hypertension, diabetes, heart failure, CKD, dementia.
-- Basic labs: kidney function (eGFR category), liver function category.
-- A current medication list (polypharmacy, e.g., 3–15 drugs depending on task).
-
-Each **episode** is one medication-review session where the agent:
-- Observes patient info and current meds.
-- Optionally **queries** a DDI/guideline tool for specific drug pairs.
-- Proposes **interventions**:
- - `stop`: discontinue a drug.
- - `dose_reduce`: lower dose of a drug.
- - `substitute`: swap to a safer alternative.
- - `add_monitoring`: keep the drug but flag extra monitoring.
-- Calls `finish_review` when it decides the regimen is acceptable or budgets are exhausted.
-
-No external PHI, EHRs, or online APIs: all data is **synthetic** or de-identified and local to the container (CSV files).
-
-2.2. Data files and CSV schemas
-
-Implement local CSVs under `data/lookups/`:
-
-**`drug_metadata.csv`**
-- `drug_id` (string; unique key)
-- `generic_name` (string)
-- `atc_class` (string)
-- `is_high_risk_elderly` (0/1)
-- `default_dose_mg` (float)
-- `min_dose_mg` (float)
-- `max_dose_mg` (float)
-
-**`beers_criteria.csv`**
-- `drug_id` (string)
-- `criterion_type` (enum string: `avoid`, `caution`, `dose_adjust`, `avoid_in_condition`)
-- `condition` (nullable string; e.g., `CKD`, `dementia`)
-- `rationale` (brief text)
-
-**`ddi_rules.csv`**
-- `drug_id_1` (string; normalized so `drug_id_1 < drug_id_2` lexicographically)
-- `drug_id_2` (string)
-- `severity` (enum string: `mild`, `moderate`, `severe`)
-- `mechanism` (short text)
-- `recommendation` (enum string: `avoid_combination`, `monitor_closely`, `dose_adjust`, `no_action`)
-- `base_risk_score` (float in [0.0, 1.0])
-
-Implement a synthetic patient-episode dataset under `data/processed/`:
-
-**`patients_polypharmacy.csv`**
-- `episode_id` (string)
-- `age` (int)
-- `sex` (enum: `M`, `F`, `O`)
-- `conditions` (semicolon-separated; e.g., `HTN;DM;CKD`)
-- `eGFR_category` (enum: `normal`, `mild`, `moderate`, `severe`)
-- `liver_function_category` (enum: `normal`, `impaired`)
-- `medication_ids` (semicolon-separated list of `drug_id`)
-- `baseline_risk_score` (float in [0.0, 1.0])
-
-2.3. Preprocessing script
-
-In `scripts/preprocess_data.py`:
-- If real data is not provided, procedurally generate synthetic but plausible data using:
- - Random combinations of conditions and drugs constrained by simple rules (e.g., CKD + renally-cleared drugs).
- - Controlled distribution of high-risk DDIs and Beers violations.
-- Explicitly tag episodes as easy/medium/hard (e.g., via number of drugs, number/severity of DDIs, and number of Beers issues).
-- Save `patients_polypharmacy.csv` ready for the environment to consume.
-
-=================================================
-3. OpenEnv models and environment implementation
-=================================================
-
-3.1. Models
-
-In `models.py`, define dataclasses or Pydantic models that extend the appropriate OpenEnv base types (`Action`, `Observation`, `State`) and are JSON-compatible.
-
-Auxiliary models:
-
-**`MedicationEntry`**
-- `drug_id: str`
-- `generic_name: str`
-- `atc_class: str`
-- `dose_mg: float`
-- `frequency: str` # e.g., `qd`, `bid`
-- `route: str` # e.g., `po`
-- `is_high_risk_elderly: bool`
-- `beers_flags: list[str]` # e.g., `["avoid", "dose_adjust_CKD"]`
-
-**`InteractionQueryRecord`**
-- `drug_id_1: str`
-- `drug_id_2: str`
-- `severity: str | None`
-- `recommendation: str | None`
-- `risk_score: float | None`
-- `step_index: int`
-
-**`InterventionRecord`**
-- `target_drug_id: str`
-- `action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]`
-- `proposed_new_drug_id: str | None`
-- `rationale: str`
-- `step_index: int`
-
-Core wire models:
-
-**`PolypharmacyObservation`** (extends OpenEnv `Observation`)
-- `episode_id: str`
-- `task_id: Literal["easy_screening", "budgeted_screening", "complex_tradeoff"]`
-- `age: int`
-- `sex: str`
-- `conditions: list[str]`
-- `eGFR_category: str`
-- `liver_function_category: str`
-- `current_medications: list[MedicationEntry]`
-- `interaction_queries: list[InteractionQueryRecord]`
-- `interventions: list[InterventionRecord]`
-- `step_index: int`
-- `remaining_query_budget: int`
-- `remaining_intervention_budget: int`
-- `shaped_reward: float` # reward from last step
-- `done: bool`
-
-**`PolypharmacyAction`** (extends OpenEnv `Action`)
-- `action_type: Literal["query_ddi", "propose_intervention", "finish_review"]`
-- `drug_id_1: str | None` # for DDI queries or some interventions
-- `drug_id_2: str | None` # for DDI queries
-- `target_drug_id: str | None` # for interventions
-- `intervention_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"] | None`
-- `proposed_new_drug_id: str | None`
-- `rationale: str | None`
-
-**`PolypharmacyState`** (extends OpenEnv `State`)
-- `episode_id: str`
-- `task_id: str`
-- `step_count: int`
-- `max_steps: int`
-- `num_query_actions: int`
-- `num_interventions: int`
-
-3.2. Environment core
-
-In `env_core.py`, implement `PolypharmacyEnv` extending the appropriate OpenEnv environment base class. It must implement:
-
-**`reset(task_id: str | None = None) -> PolypharmacyObservation`**
-- If `task_id` is `None`, default to medium (`budgeted_screening`).
-- Sample an episode from `patients_polypharmacy.csv` filtered by difficulty.
-- Initialize:
- - `episode_id`
- - `step_count = 0`
- - task-specific budgets (query, interventions, max_steps)
- - baseline regime and risk
- - empty `interaction_queries` and `interventions`
-- Return the initial `PolypharmacyObservation` with:
- - `step_index = 0`
- - `shaped_reward = 0.0`
- - `done = False`
-
-**`step(action: PolypharmacyAction) -> dict`**
-- Validate the action; if invalid:
- - Apply a negative reward.
- - Do not modify regimen, but log error in `info`.
-- If `action_type == "query_ddi"`:
- - If query budget exhausted, apply penalty and do not query.
- - Else:
- - Use `ddi_simulator.lookup_ddi(drug_id_1, drug_id_2)` to get severity, recommendation, base_risk_score.
- - Append an `InteractionQueryRecord`.
- - Apply a small negative reward for query cost.
-- If `action_type == "propose_intervention"`:
- - If intervention budget exhausted, apply penalty and ignore change.
- - Else:
- - Update `current_medications` according to `intervention_type`:
- - `stop`: remove medication.
- - `dose_reduce`: adjust dose downward within [min_dose_mg, default_dose_mg].
- - `substitute`: replace with a safer alternative from same `atc_class`.
- - `add_monitoring`: keep drug but tag in internal state.
- - Append an `InterventionRecord`.
- - Recompute current regimen risk using the risk model (see 3.3).
- - Compute shaped reward = (previous_risk - new_risk) - small intervention cost.
-- If `action_type == "finish_review"`:
- - Mark `done = True`.
- - Call the task’s grader to get episode-level score in [0.0, 1.0].
- - Add this as a terminal bonus to the current step reward.
-
-- In all cases:
- - Increment `step_count`.
- - Check `max_steps`; if exceeded, auto-terminate:
- - `done = True`
- - apply time-out penalty
- - call grader with current trajectory for a final score if appropriate.
- - Construct next `PolypharmacyObservation` with updated fields.
- - Return a dict:
- - `observation`: `PolypharmacyObservation`
- - `reward`: float shaped reward for this step
- - `done`: bool
- - `info`: dict with fields like `current_risk`, `baseline_risk`, `grader_score_if_terminal`, and debug flags.
-
-**`state` property**
-- Returns `PolypharmacyState` reflecting the current internal state.
-
-3.3. DDI simulator and risk model
-
-In `ddi_simulator.py`:
-- Load `ddi_rules.csv` once via `data_loader`.
-- Implement `lookup_ddi(drug_id_1, drug_id_2) -> tuple[severity, recommendation, base_risk_score]`:
- - Normalize the pair ordering.
- - Look up row; if missing, return:
- - severity = `"none"`
- - recommendation = `"no_action"`
- - base_risk_score = 0.0
-
-In `rewards.py` (or a dedicated module), implement:
-- `compute_regimen_risk(current_drug_ids, patient_context, ddi_rules, beers_rules, drug_metadata) -> float`
- - Aggregate contributions from:
- - Beers violations (weighted by `criterion_type` and relevant conditions).
- - DDI base risk scores for all present drug pairs.
- - High-risk elderly drugs.
- - Normalize and clip to [0.0, 1.0].
-
-Use this function to compute:
-- `baseline_risk` at episode start.
-- Risk after each intervention step.
-
-Also implement:
-- `compute_shaped_reward(previous_risk, new_risk, action, context, partial_metrics) -> float`
- - Positive component: `previous_risk - new_risk`.
- - Negative components: per-query cost, per-intervention cost, invalid-action penalty, time-out penalty.
-
-=================================================
-4. Tasks and graders (3 difficulty levels)
-=================================================
-
-Define three task IDs and semantics in `tasks.py` and `graders.py`:
-
-Task IDs:
-- `easy_screening`
-- `budgeted_screening`
-- `complex_tradeoff`
-
-4.1. `easy_screening` (easy)
-
-- Small regimen: 3–5 drugs.
-- Exactly one **severe** DDI pair and possibly one simple Beers violation.
-- Budgets:
- - query_budget ≈ 4
- - intervention_budget ≈ 2
- - max_steps ≈ 10
-
-Grader:
-- Input: full trajectory, baseline risk, final risk, list of interventions.
-- Compute:
- - `risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, ε)` (normalized).
- - `targeted_intervention_flag = 1.0` if at least one intervention affects one of the drugs in the known severe DDI pair, else 0.0.
-- Score:
- - `score = 0.5 * risk_reduction + 0.5 * targeted_intervention_flag`
- - Clip to [0.0, 1.0].
-
-4.2. `budgeted_screening` (medium)
-
-- Medium regimen: 6–10 drugs.
-- Multiple DDIs (mild/moderate/severe) and multiple Beers issues.
-- Budgets:
- - query_budget ≈ 8
- - intervention_budget ≈ 3
- - max_steps ≈ 20
-
-Grader:
-- Compute:
- - `risk_reduction_score` as normalized risk drop.
- - `intervention_precision_score` = fraction of interventions that actually reduce risk or fix guideline violations.
- - `query_efficiency_score` = (number of severe/moderate DDIs discovered) / (number of queries used), normalized.
-- Weighted score, for example:
- - `score = 0.5 * risk_reduction_score + 0.3 * intervention_precision_score + 0.2 * query_efficiency_score`
- - Clip to [0.0, 1.0].
-
-4.3. `complex_tradeoff` (hard)
-
-- Larger regimen: 10–15 drugs.
-- Some drugs are **clinically critical** (e.g., anticoagulants, insulin analogues) and encoded as such in `drug_metadata` or a small internal map.
-- Episodes contain:
- - multiple DDIs and Beers issues, including ones involving critical drugs.
- - safer substitutes for some risky drugs.
-
-Budgets:
-- query_budget ≈ 12
-- intervention_budget ≈ 5
-- max_steps ≈ 30
-
-Grader adds a **regimen disruption penalty** component:
-- Metrics:
- - `risk_reduction_score` (as above).
- - `critical_drug_penalty` = penalty if a critical drug is stopped without substitution to another suitable agent.
- - `total_drug_changes` = number of drugs stopped or substituted.
- - `regimen_disruption_penalty` derived from `total_drug_changes` and `critical_drug_penalty`.
-
-Example scoring:
-- `base = risk_reduction_score`
-- `penalty = α * regimen_disruption_penalty`
-- `score = clamp(base - penalty, 0.0, 1.0)`
-
-4.4. Reward shaping
-
-In `rewards.py`, define a consistent shaping scheme:
-- On each query:
- - Small negative reward (e.g., −0.01) plus any small bonus if it discovers a severe DDI, if desired.
-- On each intervention:
- - Reward ≈ (previous_risk - new_risk) − small intervention cost.
-- On invalid actions:
- - Larger negative reward (e.g., −0.1) and no state change.
-- On `finish_review`:
- - Add the task-level `score` ∈ [0.0, 1.0] from the corresponding grader to that step’s shaped reward.
-
-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.
-
-=================================================
-5. HTTP API server and openenv.yaml
-=================================================
-
-5.1. HTTP server (FastAPI)
-
-In `api/server.py`:
-- Implement a FastAPI app that maintains a `PolypharmacyEnv` instance (or a multiplexing scheme if needed).
-- Endpoints:
- - `POST /reset`:
- - Request body: may include `task_id` (string).
- - Response: serialized `PolypharmacyObservation`.
- - `POST /step`:
- - Request body: serialized `PolypharmacyAction`.
- - Response: dict with:
- - `observation`: `PolypharmacyObservation`
- - `reward`: float
- - `done`: bool
- - `info`: dict
- - `GET /state`:
- - Response: `PolypharmacyState`.
-
-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).
-
-5.2. `openenv.yaml`
-
-At repo root, define `openenv.yaml` consistent with the latest OpenEnv spec. At minimum, include:
-- `name`: `polypharmacy_env`
-- `version`: e.g., `0.1.0`
-- `description`: human-readable description.
-- `author`: your details.
-- `tags`: e.g., `["healthcare", "polypharmacy", "openenv"]`
-- `tasks`:
- - One entry per task:
- - `id`: `"easy_screening"` / `"budgeted_screening"` / `"complex_tradeoff"`
- - `description`: one-line description
- - `difficulty`: `"easy"`, `"medium"`, `"hard"`
-
-Ensure `openenv validate` (or equivalent validator) passes once implemented.
-
-=================================================
-6. Baseline heuristic (non-LLM) agent
-=================================================
-
-In `baselines/heuristic_agent.py`, implement a simple, deterministic baseline agent that:
-
-For each episode:
-- Iterates through all unordered medication pairs within query budget:
- - Calls `query_ddi` via the environment for each pair until the query budget is exhausted or all pairs are examined.
- - Records severe and moderate interactions.
-- After querying:
- - For each severe DDI pair:
- - Try `substitute` one of the drugs using `drug_metadata`:
- - Prefer substitute within same `atc_class` that:
- - is not marked high-risk elderly.
- - does not participate in known severe DDIs with the rest of the regimen.
- - If no substitute exists, propose `stop` for the higher-risk drug.
- - Respect intervention budget limits.
-- Finally, call `finish_review`.
-
-This baseline should be callable as a simple Python function that interacts with `PolypharmacyEnv` directly (without HTTP).
-
-=================================================
-7. Baseline LLM inference script (inference.py)
-=================================================
-
-At repo root, create `inference.py` that:
-
-7.1. Uses the OpenAI Python client
-
-- Import and configure the official OpenAI Python client.
-- Read environment variables:
- - `OPENAI_API_KEY` (required).
- - `API_BASE_URL` (base URL for LLM; default to OpenAI standard if not set).
- - `MODEL_NAME` (e.g., `gpt-4.1` or similar).
- - `HF_TOKEN` (if needed for HF auth; do not hardcode).
-- Read `POLYPHARMACY_ENV_URL` (or similar) for the environment’s HTTP base URL.
-
-7.2. Implements the required logging format
-
-- For each **run** across all tasks:
- - Emit a `[START]` line with a JSON payload exactly matching the evaluation specification:
- - Fields such as `run_id`, `task_id`, `model`, etc., in the same order and naming as the sample OpenEnv inference script.
-- For each **step** in an episode:
- - Emit a `[STEP]` line with JSON fields including:
- - `run_id`
- - `task_id`
- - `episode_id`
- - `step_index`
- - `observation_summary` (brief, machine-readable summary)
- - `action_payload` (the action sent to the env)
- - `reward`
- - `done`
-- After finishing an episode for a task:
- - Emit an `[END]` line summarizing:
- - `run_id`
- - `task_id`
- - per-episode statistics (e.g., total reward, grader score from last step’s `info`).
-- The stdout format MUST follow the sample exactly:
- - Same tags: `[START]`, `[STEP]`, `[END]`.
- - Same JSON field names and ordering as the provided reference.
- - No extra prints except these structured logs (and necessary error messages to stderr).
-
-7.3. LLM agent loop
-
-- For each task (`easy_screening`, `budgeted_screening`, `complex_tradeoff`):
- - Run a fixed small number of episodes (e.g., 5–10 per task) for baseline scoring.
- - For each episode:
- - Call `/reset` with the task id.
- - At each step:
- - Summarize the observation into a concise prompt for the LLM:
- - Include age, sex, conditions, high-risk flags, budgets, and a compressed view of meds and previous actions.
- - Ask the model to output a **strict JSON** representing `PolypharmacyAction` fields.
- - 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.
- - Send this action to `/step` and log `[STEP]`.
- - End when `done=True` or max_steps is reached.
-- At the end, print aggregate scores per task and overall.
-
-Make sure runtime < 20 minutes and that the script can run within 2 vCPUs and 8 GB RAM.
-
-=================================================
-8. Dockerfile and Hugging Face Space
-=================================================
-
-8.1. Dockerfile
-
-Create a `Dockerfile` that:
-- Starts from a slim Python image (e.g., `python:3.11-slim`).
-- Installs system dependencies as needed (e.g., `build-essential`, `curl`).
-- Copies the project into the container.
-- Installs Python dependencies from `requirements.txt`.
-- Sets appropriate environment variables for the app (e.g., `PORT=7860`).
-- Exposes port 7860.
-- Uses a `CMD` or `ENTRYPOINT` that runs the FastAPI server, for example:
- - `uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860`
-
-8.2. Hugging Face Space
-
-Ensure the repository is ready to be used as a Hugging Face Space:
-- Space type: `docker`.
-- Tag: `openenv`.
-- On container start, the server must listen on the correct port and respond to:
- - `POST /reset`
- - `POST /step`
- - `GET /state`
-- The environment must start cleanly with `docker build` + `docker run` locally.
-
-=================================================
-9. README and documentation
-=================================================
-
-In `README.md`, include:
-
-- **Environment description & motivation**:
- - What PolypharmacyEnv simulates.
- - Why elderly polypharmacy safety matters.
-- **Action and observation spaces**:
- - Describe `PolypharmacyAction`, `PolypharmacyObservation`, and `PolypharmacyState` fields and semantics.
-- **Task descriptions**:
- - `easy_screening`, `budgeted_screening`, `complex_tradeoff`, their difficulty and goals.
-- **Reward structure**:
- - Summarize shaping and terminal rewards.
-- **Setup & usage**:
- - How to install dependencies.
- - How to run the API server locally (uvicorn command).
- - How to run the heuristic baseline.
- - How to run `inference.py` with environment variables.
-- **Baseline scores**:
- - Document reproducible baseline scores for each task (heuristic agent, and LLM baseline if available).
-
-=================================================
-10. Validation and quality gates
-=================================================
-
-- Ensure:
- - `openenv.yaml` and the HTTP server pass the OpenEnv validation script.
- - `docker build` and `docker run` work without errors.
- - `inference.py` completes under 20 minutes, within 2 vCPUs / 8 GB RAM.
- - All graders:
- - Are deterministic.
- - Return scores strictly in [0.0, 1.0].
- - No grader returns a constant score irrespective of behavior.
-
-Aim for clean, well-structured, well-documented code with clear separation of concerns between:
-- Data loading,
-- Environment state & dynamics,
-- Reward/grade logic,
-- HTTP serving,
-- Baseline agents and inference.
\ No newline at end of file
diff --git a/openenv-polypharmacy/README.md b/openenv-polypharmacy/README.md
deleted file mode 100644
index 5707521073ad5ade9a331af60d2c4ad0695e1c94..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/README.md
+++ /dev/null
@@ -1,184 +0,0 @@
-# PolypharmacyEnv
-
-An [OpenEnv](https://github.com/meta-pytorch/OpenEnv)-compliant reinforcement-learning environment that simulates **elderly polypharmacy medication review**. An RL agent acts as a clinical pharmacist assistant, identifying dangerous drug-drug interactions (DDIs), Beers-criteria violations, and proposing safe interventions.
-
----
-
-## Motivation
-
-Polypharmacy (concurrent use of multiple medications) is extremely common in elderly patients (age >= 65) and carries significant risks:
-
-- **Drug-drug interactions** can cause adverse events, hospitalisation, and death.
-- **Beers-criteria violations** flag medications that are inappropriate or require dose adjustments in older adults.
-- Stopping critical medications (anticoagulants, insulin) without proper substitution can be equally dangerous.
-
-This environment lets RL and LLM-based agents learn to **balance risk reduction against regimen stability**.
-
----
-
-## Action Space
-
-Each step, the agent sends a `PolypharmacyAction` with one of three action types:
-
-| `action_type` | Required fields | Description |
-|---|---|---|
-| `query_ddi` | `drug_id_1`, `drug_id_2` | Query the DDI database for an interaction between two drugs |
-| `propose_intervention` | `target_drug_id`, `intervention_type` | Propose changing a medication (`stop`, `dose_reduce`, `substitute`, `add_monitoring`) |
-| `finish_review` | — | End the review and trigger final grading |
-
-Optional fields: `proposed_new_drug_id`, `rationale`.
-
-## Observation Space
-
-`PolypharmacyObservation` includes:
-
-- **Patient demographics**: `age`, `sex`, `conditions`, `eGFR_category`, `liver_function_category`
-- **Medications**: list of `MedicationEntry` (drug_id, name, class, dose, high-risk flags, Beers flags)
-- **History**: `interaction_queries` (past DDI query results), `interventions` (past actions)
-- **Budgets**: `remaining_query_budget`, `remaining_intervention_budget`
-- **Reward signals**: `shaped_reward`, `done`
-
-## State
-
-`PolypharmacyState`: `episode_id`, `task_id`, `step_count`, `max_steps`, `num_query_actions`, `num_interventions`.
-
----
-
-## Tasks
-
-| Task ID | Difficulty | Drugs | Query Budget | Intervention Budget | Max Steps | Description |
-|---|---|---|---|---|---|---|
-| `easy_screening` | Easy | 3-5 | 4 | 2 | 10 | One severe DDI, simple resolution |
-| `budgeted_screening` | Medium | 6-10 | 8 | 3 | 20 | Multiple DDIs + Beers issues, limited budgets |
-| `complex_tradeoff` | Hard | 10-15 | 12 | 5 | 30 | Critical drugs, trade-off between risk and regimen stability |
-
----
-
-## Reward Structure
-
-**Per-step shaped rewards:**
-
-| Event | Reward |
-|---|---|
-| DDI query | -0.01 (cost) + 0.03 bonus if severe DDI discovered |
-| Successful intervention | +(previous_risk - new_risk) - 0.02 cost |
-| Invalid action | -0.10 penalty |
-| Timeout (max steps exceeded) | -0.20 penalty |
-| `finish_review` | + grader score (0.0 to 1.0) |
-
-**Terminal grader scoring:**
-- **Easy**: 50% risk reduction + 50% targeted intervention flag
-- **Medium**: 50% risk reduction + 30% intervention precision + 20% query efficiency
-- **Hard**: risk reduction - regimen disruption penalty - critical drug penalty
-
----
-
-## Setup & Usage
-
-### Install dependencies
-
-```bash
-pip install -r requirements.txt
-```
-
-### Generate synthetic data
-
-```bash
-python3 scripts/preprocess_data.py
-```
-
-### Run the API server locally
-
-```bash
-PYTHONPATH=src uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860
-```
-
-### Run the heuristic baseline
-
-```bash
-PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
-```
-
-### Run tests
-
-```bash
-PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
-```
-
-### Run `inference.py` (LLM baseline)
-
-```bash
-# Start the server first, then in another terminal:
-export OPENAI_API_KEY="sk-..."
-export MODEL_NAME="gpt-4.1"
-export POLYPHARMACY_ENV_URL="http://localhost:7860"
-python3 inference.py
-```
-
-### Docker
-
-```bash
-docker build -t polypharmacy-env .
-docker run -p 7860:7860 polypharmacy-env
-```
-
----
-
-## Hugging Face Space
-
-This repo is ready for deployment as a HF Space:
-
-- **Space type**: `docker`
-- **Tag**: `openenv`
-- The container listens on port 7860 and exposes `/reset`, `/step`, `/state`, `/health`.
-
----
-
-## Baseline Scores
-
-### Heuristic Agent (deterministic, rule-based)
-
-| Task | Avg Score | Avg Reward |
-|---|---|---|
-| `easy_screening` | ~0.96 | ~1.30 |
-| `budgeted_screening` | ~0.48 | ~0.45 |
-| `complex_tradeoff` | ~0.24 | ~0.11 |
-
-*(Scores vary by seed; run `scripts/run_validation.sh` for exact numbers.)*
-
----
-
-## Project Structure
-
-```
-openenv-polypharmacy/
- openenv.yaml # OpenEnv manifest
- Dockerfile # Container image
- inference.py # LLM baseline script
- requirements.txt
- pyproject.toml
- src/polypharmacy_env/
- config.py # Constants, task configs
- models.py # Pydantic action/observation/state models
- env_core.py # PolypharmacyEnv implementation
- tasks.py # Task selection utilities
- graders.py # Deterministic graders (3 difficulty levels)
- rewards.py # Reward shaping logic
- data_loader.py # CSV data loading
- ddi_simulator.py # Drug interaction lookup engine
- api/
- server.py # FastAPI HTTP server
- schemas.py # Request/response schemas
- baselines/
- heuristic_agent.py # Rule-based baseline
- random_agent.py # Random baseline
- tests/
- test_env_core.py
- test_api.py
- data/
- lookups/ # Drug metadata, DDI rules, Beers criteria CSVs
- processed/ # Synthetic patient episodes
- scripts/
- preprocess_data.py # Synthetic data generator
- run_validation.sh # Run tests + baseline
-```
diff --git a/openenv-polypharmacy/backend/Dockerfile b/openenv-polypharmacy/backend/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..0cba5065ef8349abee5686485d0ada06fe57ce05
--- /dev/null
+++ b/openenv-polypharmacy/backend/Dockerfile
@@ -0,0 +1,28 @@
+FROM python:3.11-slim
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends build-essential curl && \
+ rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+COPY backend/requirements.txt /app/backend/requirements.txt
+RUN pip install --no-cache-dir -r /app/backend/requirements.txt
+
+COPY backend /app/backend
+COPY data /app/data
+COPY scripts /app/scripts
+COPY .env.example /app/.env.example
+
+RUN python3 /app/scripts/preprocess_data.py
+
+ENV PORT=7860
+ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
+ENV PYTHONUNBUFFERED=1
+
+EXPOSE 7860
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+ CMD curl -f http://localhost:7860/health || exit 1
+
+CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
diff --git a/openenv-polypharmacy/backend/__init__.py b/openenv-polypharmacy/backend/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f40af2be5beb19dd1c96a0a63b74124b144b304
--- /dev/null
+++ b/openenv-polypharmacy/backend/__init__.py
@@ -0,0 +1 @@
+"""Backend entrypoint package for monorepo structure."""
diff --git a/openenv-polypharmacy/backend/main.py b/openenv-polypharmacy/backend/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..d7058d14ed4d68e0e2612e4168c9ec6d3ee0828c
--- /dev/null
+++ b/openenv-polypharmacy/backend/main.py
@@ -0,0 +1,15 @@
+"""ASGI entrypoint for backend service in monorepo layout."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+BACKEND_DIR = Path(__file__).resolve().parent
+SRC = BACKEND_DIR / "src"
+if str(SRC) not in sys.path:
+ sys.path.insert(0, str(SRC))
+
+from polypharmacy_env.api.app import app # noqa: E402
+
+__all__ = ["app"]
diff --git a/openenv-polypharmacy/src/polypharmacy_env.egg-info/requires.txt b/openenv-polypharmacy/backend/requirements.txt
similarity index 72%
rename from openenv-polypharmacy/src/polypharmacy_env.egg-info/requires.txt
rename to openenv-polypharmacy/backend/requirements.txt
index 21acf4c6feefb13b1c9bca99728a835b24b099a4..c975be48d5a69da77e34da8973f265732a115236 100644
--- a/openenv-polypharmacy/src/polypharmacy_env.egg-info/requires.txt
+++ b/openenv-polypharmacy/backend/requirements.txt
@@ -2,10 +2,8 @@ fastapi>=0.104.0
uvicorn>=0.24.0
pydantic>=2.0.0
requests>=2.31.0
+httpx>=0.25.0
+openenv-core>=0.2.0
openai>=1.0.0
-
-[dev]
+python-dotenv>=1.0.0
pytest>=7.0.0
-httpx>=0.25.0
-black
-isort
diff --git a/openenv-polypharmacy/src/polypharmacy_env/__init__.py b/openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/__init__.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/api/__init__.py b/openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/api/__init__.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py
diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py b/openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4377baaca9f6c0d8191c1a2e89d1e9c70b48605
--- /dev/null
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py
@@ -0,0 +1,63 @@
+"""FastAPI app factory for PolypharmacyEnv."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from dotenv import load_dotenv
+from fastapi import HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+from openenv.core.env_server.http_server import create_app
+from starlette.responses import FileResponse
+
+from ..env_core import PolypharmacyEnv
+from ..models import PolypharmacyAction, PolypharmacyObservation
+from .routes.agent import router as agent_router
+
+load_dotenv()
+
+
+class SPAStaticFiles(StaticFiles):
+ """Serve SPA index for unknown frontend routes."""
+
+ async def get_response(self, path: str, scope):
+ response = await super().get_response(path, scope)
+ if response.status_code != 404:
+ return response
+ index_path = Path(self.directory) / "index.html"
+ if index_path.exists():
+ return FileResponse(index_path)
+ raise HTTPException(status_code=404, detail="Not Found")
+
+
+def create_polypharmacy_app():
+ app = create_app(
+ PolypharmacyEnv,
+ PolypharmacyAction,
+ PolypharmacyObservation,
+ env_name="polypharmacy_env",
+ )
+
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=[
+ "http://localhost:5173",
+ "http://127.0.0.1:5173",
+ ],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+ app.include_router(agent_router)
+
+ # In Docker Space deployment, serve built frontend from same container.
+ project_root = Path(__file__).resolve().parents[4]
+ frontend_dist = project_root / "frontend" / "dist"
+ if frontend_dist.exists():
+ app.mount("/", SPAStaticFiles(directory=frontend_dist, html=True), name="frontend")
+
+ return app
+
+
+app = create_polypharmacy_app()
diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py b/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb0a2f808230d18b89b49b68b2bce37b7c869206
--- /dev/null
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py
@@ -0,0 +1 @@
+"""API route modules."""
diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py b/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..f74e627ff9f5fcea9954cc51f51991bdb6218fbf
--- /dev/null
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py
@@ -0,0 +1,35 @@
+"""Agent suggestion API routes."""
+
+from __future__ import annotations
+
+from fastapi import APIRouter, HTTPException
+from pydantic import BaseModel, Field
+
+from ...models import PolypharmacyAction, PolypharmacyObservation
+from ...services.groq_agent import suggest_action_from_observation
+
+router = APIRouter(prefix="/agent", tags=["agent"])
+
+
+class AgentSuggestRequest(BaseModel):
+ observation: PolypharmacyObservation
+ model_name: str | None = None
+
+
+class AgentSuggestResponse(BaseModel):
+ action: PolypharmacyAction
+ source: str = Field(default="groq")
+
+
+@router.post("/suggest", response_model=AgentSuggestResponse)
+def suggest_agent_action(payload: AgentSuggestRequest) -> AgentSuggestResponse:
+ """Return a model-suggested action for the current observation."""
+ try:
+ action = suggest_action_from_observation(
+ payload.observation, model_name=payload.model_name
+ )
+ except ValueError as exc:
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
+ except Exception as exc:
+ raise HTTPException(status_code=500, detail=f"Model call failed: {exc}") from exc
+ return AgentSuggestResponse(action=action)
diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py b/openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..63717ca71f81ef20f44c410d4dd7a59576942b2e
--- /dev/null
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py
@@ -0,0 +1,6 @@
+"""Backward-compatible app import path.
+
+Use `polypharmacy_env.api.app:app` for the main app module.
+"""
+
+from .app import app
diff --git a/openenv-polypharmacy/src/polypharmacy_env/baselines/__init__.py b/openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/baselines/__init__.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/baselines/heuristic_agent.py b/openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/baselines/heuristic_agent.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/baselines/random_agent.py b/openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/baselines/random_agent.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/client.py b/openenv-polypharmacy/backend/src/polypharmacy_env/client.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/client.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/client.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/config.py b/openenv-polypharmacy/backend/src/polypharmacy_env/config.py
similarity index 97%
rename from openenv-polypharmacy/src/polypharmacy_env/config.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/config.py
index bfe0d2a1878d584999ec8ae86d88362f0e221ff3..e71035ae3e075c08b7f0a9dbbef0857abb8be231 100644
--- a/openenv-polypharmacy/src/polypharmacy_env/config.py
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/config.py
@@ -7,7 +7,7 @@ from pathlib import Path
from typing import Dict
# ── Paths ────────────────────────────────────────────────────────────────────
-PROJECT_ROOT = Path(__file__).resolve().parents[2] # openenv-polypharmacy/
+PROJECT_ROOT = Path(__file__).resolve().parents[3] # openenv-polypharmacy/
DATA_DIR = PROJECT_ROOT / "data"
LOOKUPS_DIR = DATA_DIR / "lookups"
PROCESSED_DIR = DATA_DIR / "processed"
diff --git a/openenv-polypharmacy/src/polypharmacy_env/data_loader.py b/openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/data_loader.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/ddi_simulator.py b/openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/ddi_simulator.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/env_core.py b/openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/env_core.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/graders.py b/openenv-polypharmacy/backend/src/polypharmacy_env/graders.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/graders.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/graders.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/models.py b/openenv-polypharmacy/backend/src/polypharmacy_env/models.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/models.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/models.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/rewards.py b/openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/rewards.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py
diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py b/openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..daa00ba9855c5ea54e0e4d3fe0a29407c421340a
--- /dev/null
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py
@@ -0,0 +1 @@
+"""Service layer for external integrations."""
diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py b/openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f15f22ff2ec9f4710949815b29f0431b2cac63b
--- /dev/null
+++ b/openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py
@@ -0,0 +1,246 @@
+"""Groq-powered action suggester for PolypharmacyEnv."""
+
+from __future__ import annotations
+
+import json
+import os
+from typing import Any
+
+from openai import OpenAI
+
+from ..models import PolypharmacyAction, PolypharmacyObservation
+
+DEFAULT_MODEL = "llama-3.1-8b-instant"
+FALLBACK_MODELS = [
+ "llama-3.1-8b-instant",
+ "llama-3.3-70b-versatile",
+ "gemma2-9b-it",
+]
+CRITICAL_DRUG_IDS = {"DRUG_WARFARIN", "DRUG_INSULIN_GLARGINE", "DRUG_DIGOXIN"}
+
+SYSTEM_PROMPT = """You are a clinical medication safety assistant.
+Return exactly one JSON object describing the next action.
+Allowed output schema:
+{
+ "action_type": "query_ddi" | "propose_intervention" | "finish_review",
+ "drug_id_1": "optional",
+ "drug_id_2": "optional",
+ "target_drug_id": "optional",
+ "intervention_type": "stop|dose_reduce|substitute|add_monitoring|none",
+ "proposed_new_drug_id": "optional",
+ "rationale": "optional"
+}
+No markdown fences. No extra text.
+Do NOT use finish_review early. First, gather evidence with query_ddi and/or
+perform at least one meaningful intervention when needed.
+"""
+
+
+def _obs_to_prompt(obs: PolypharmacyObservation) -> str:
+ meds = ", ".join(m.drug_id for m in obs.current_medications)
+ conds = ", ".join(obs.conditions)
+ return (
+ f"Task: {obs.task_id}\n"
+ f"Age: {obs.age}, sex: {obs.sex}\n"
+ f"Conditions: {conds}\n"
+ f"Medications: {meds}\n"
+ f"Query budget: {obs.remaining_query_budget}\n"
+ f"Intervention budget: {obs.remaining_intervention_budget}\n"
+ f"Step index: {obs.step_index}\n"
+ "Choose the single safest, most useful next action."
+ )
+
+
+def _parse_action(text: str) -> PolypharmacyAction:
+ raw = text.strip()
+ if raw.startswith("```"):
+ raw = raw.split("\n", 1)[-1]
+ if raw.endswith("```"):
+ raw = raw.rsplit("```", 1)[0]
+ raw = raw.strip()
+ payload: dict[str, Any] = json.loads(raw)
+ return PolypharmacyAction.model_validate(payload)
+
+
+def _fallback_query_action(obs: PolypharmacyObservation) -> PolypharmacyAction:
+ meds = [m.drug_id for m in obs.current_medications]
+ if len(meds) >= 2 and obs.remaining_query_budget > 0:
+ return PolypharmacyAction(
+ action_type="query_ddi",
+ drug_id_1=meds[0],
+ drug_id_2=meds[1],
+ )
+ return PolypharmacyAction(action_type="finish_review")
+
+
+def _norm_pair(a: str, b: str) -> tuple[str, str]:
+ return (a, b) if a < b else (b, a)
+
+
+def _pick_unseen_query_pair(obs: PolypharmacyObservation) -> tuple[str, str] | None:
+ meds = [m.drug_id for m in obs.current_medications]
+ if len(meds) < 2 or obs.remaining_query_budget <= 0:
+ return None
+
+ seen = {
+ _norm_pair(q.drug_id_1, q.drug_id_2)
+ for q in obs.interaction_queries
+ }
+ # Prioritize pairs containing high-risk drugs.
+ high_risk = [m.drug_id for m in obs.current_medications if m.is_high_risk_elderly]
+ ordered = high_risk + [m for m in meds if m not in set(high_risk)]
+
+ for i in range(len(ordered)):
+ for j in range(i + 1, len(ordered)):
+ p = _norm_pair(ordered[i], ordered[j])
+ if p not in seen:
+ return p
+ return None
+
+
+def _pick_intervention_target(obs: PolypharmacyObservation) -> str | None:
+ if obs.remaining_intervention_budget <= 0:
+ return None
+ med_set = {m.drug_id for m in obs.current_medications}
+
+ # Use latest discovered severe/moderate query as intervention target.
+ for q in reversed(obs.interaction_queries):
+ if q.severity in ("severe", "moderate"):
+ m1 = next((m for m in obs.current_medications if m.drug_id == q.drug_id_1), None)
+ m2 = next((m for m in obs.current_medications if m.drug_id == q.drug_id_2), None)
+ candidates = [m for m in (m1, m2) if m is not None]
+ if not candidates:
+ continue
+ # Prefer non-critical risky drugs first.
+ candidates.sort(
+ key=lambda m: (
+ m.drug_id in CRITICAL_DRUG_IDS,
+ 0 if any("avoid" in f for f in m.beers_flags) else 1,
+ 0 if m.is_high_risk_elderly else 1,
+ )
+ )
+ return candidates[0].drug_id
+
+ # Fallback: if no severe/moderate discovered, still intervene on obviously
+ # risky medications (Beers/high-risk flags) when budgets permit.
+ risky = sorted(
+ obs.current_medications,
+ key=lambda m: (
+ 0 if any("avoid" in f for f in m.beers_flags) else 1,
+ 0 if m.is_high_risk_elderly else 1,
+ 1 if m.drug_id in CRITICAL_DRUG_IDS else 0,
+ ),
+ )
+ for med in risky:
+ if any("avoid" in f for f in med.beers_flags) or med.is_high_risk_elderly:
+ return med.drug_id
+ return None
+
+
+def _rule_based_action(obs: PolypharmacyObservation) -> PolypharmacyAction | None:
+ # If we already discovered significant risk, intervene before more querying.
+ target = _pick_intervention_target(obs)
+ if target and (
+ obs.step_index >= 1
+ and (
+ obs.remaining_query_budget <= 2
+ or len(obs.interaction_queries) >= 4
+ or any(q.severity in ("severe", "moderate") for q in obs.interaction_queries)
+ )
+ ):
+ intervention = "stop"
+ rationale = "Remove likely contributor to discovered interaction risk"
+ if target in CRITICAL_DRUG_IDS:
+ # Avoid blunt stop for critical meds.
+ intervention = "dose_reduce"
+ rationale = "Critical medication: prefer dose reduction over abrupt stop"
+ return PolypharmacyAction(
+ action_type="propose_intervention",
+ target_drug_id=target,
+ intervention_type=intervention,
+ rationale=rationale,
+ )
+
+ pair = _pick_unseen_query_pair(obs)
+ if pair:
+ return PolypharmacyAction(
+ action_type="query_ddi",
+ drug_id_1=pair[0],
+ drug_id_2=pair[1],
+ )
+
+ if obs.remaining_intervention_budget > 0:
+ # Final fallback before finish: at least one safety action.
+ target = _pick_intervention_target(obs)
+ if target:
+ return PolypharmacyAction(
+ action_type="propose_intervention",
+ target_drug_id=target,
+ intervention_type="dose_reduce"
+ if target in CRITICAL_DRUG_IDS
+ else "stop",
+ rationale="Fallback intervention when query options are exhausted",
+ )
+
+ if obs.step_index >= 3:
+ return PolypharmacyAction(action_type="finish_review")
+ return None
+
+
+def _postprocess_action(
+ obs: PolypharmacyObservation, action: PolypharmacyAction
+) -> PolypharmacyAction:
+ # First apply deterministic guardrails to avoid repetitive loops.
+ ruled = _rule_based_action(obs)
+ if ruled is not None:
+ return ruled
+
+ # Guardrail: prevent useless immediate finish actions.
+ if action.action_type == "finish_review":
+ if obs.step_index < 2 and obs.remaining_query_budget > 0:
+ return _fallback_query_action(obs)
+ if len(obs.interaction_queries) == 0 and obs.remaining_query_budget > 0:
+ return _fallback_query_action(obs)
+ return action
+
+
+def suggest_action_from_observation(
+ observation: PolypharmacyObservation,
+ model_name: str | None = None,
+) -> PolypharmacyAction:
+ """Use Groq chat completions to suggest a valid action."""
+ api_key = os.getenv("GROQ_API_KEY", "").strip()
+ if not api_key:
+ raise ValueError("GROQ_API_KEY is missing. Add it to your .env file.")
+
+ base_url = os.getenv("GROQ_BASE_URL", "https://api.groq.com/openai/v1").strip()
+ model = (model_name or os.getenv("GROQ_MODEL_NAME", DEFAULT_MODEL)).strip()
+ client = OpenAI(api_key=api_key, base_url=base_url)
+
+ user_prompt = _obs_to_prompt(observation)
+ tried: list[tuple[str, str]] = []
+ candidates: list[str] = [model] + [m for m in FALLBACK_MODELS if m != model]
+
+ for candidate in candidates:
+ try:
+ resp = client.chat.completions.create(
+ model=candidate,
+ messages=[
+ {"role": "system", "content": SYSTEM_PROMPT},
+ {"role": "user", "content": user_prompt},
+ ],
+ temperature=0.2,
+ max_tokens=220,
+ )
+ generated = (resp.choices[0].message.content or "").strip()
+ parsed = _parse_action(generated)
+ return _postprocess_action(observation, parsed)
+ except Exception as exc:
+ tried.append((candidate, str(exc)))
+
+ tried_txt = " | ".join(f"{m}: {err}" for m, err in tried)
+ raise ValueError(
+ "No Groq model worked. Try one of: "
+ "llama-3.3-70b-versatile, llama-3.1-8b-instant, gemma2-9b-it. "
+ f"Errors: {tried_txt}"
+ )
diff --git a/openenv-polypharmacy/src/polypharmacy_env/tasks.py b/openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/tasks.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/tests/__init__.py b/openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/tests/__init__.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/tests/test_api.py b/openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/tests/test_api.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py
diff --git a/openenv-polypharmacy/src/polypharmacy_env/tests/test_env_core.py b/openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py
similarity index 100%
rename from openenv-polypharmacy/src/polypharmacy_env/tests/test_env_core.py
rename to openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py
diff --git a/openenv-polypharmacy/data/processed/patients_polypharmacy.csv b/openenv-polypharmacy/data/processed/patients_polypharmacy.csv
index d74ead76cefab3c65e7aa9975d6f148e982aa4fa..3dfd4b83053958e03a785dd8bd057c7c5879c74b 100644
--- a/openenv-polypharmacy/data/processed/patients_polypharmacy.csv
+++ b/openenv-polypharmacy/data/processed/patients_polypharmacy.csv
@@ -1,44 +1,44 @@
episode_id,age,sex,conditions,eGFR_category,liver_function_category,medication_ids,baseline_risk_score,difficulty
-EP_0001,72,F,HTN,moderate,normal,DRUG_WARFARIN;DRUG_FUROSEMIDE;DRUG_LISINOPRIL;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.264,easy
+EP_0001,72,F,HTN,moderate,normal,DRUG_AMLODIPINE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
EP_0002,67,M,OA;COPD;neuropathy,normal,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_AMITRIPTYLINE,0.2833,easy
-EP_0003,73,F,HTN;HF,normal,normal,DRUG_IBUPROFEN;DRUG_WARFARIN;DRUG_FUROSEMIDE,0.2933,easy
-EP_0004,74,M,CKD,mild,impaired,DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_DIAZEPAM,0.3067,easy
+EP_0003,73,F,HTN;HF,normal,normal,DRUG_FUROSEMIDE;DRUG_FLUOXETINE;DRUG_TRAMADOL,0.2733,easy
+EP_0004,74,M,CKD,mild,impaired,DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.2833,easy
EP_0005,76,F,OA;neuropathy;CKD,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_AMITRIPTYLINE,0.17,easy
-EP_0006,74,M,HTN;OA,normal,impaired,DRUG_IBUPROFEN;DRUG_WARFARIN;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.44,easy
-EP_0007,90,M,BPH;OA,moderate,normal,DRUG_DIGOXIN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN;DRUG_NAPROXEN;DRUG_AMIODARONE,0.16,easy
+EP_0006,74,M,HTN;OA,normal,impaired,DRUG_IBUPROFEN;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
+EP_0007,90,M,BPH;OA,moderate,normal,DRUG_WARFARIN;DRUG_NAPROXEN;DRUG_TAMSULOSIN;DRUG_GABAPENTIN,0.225,easy
EP_0008,77,F,CKD;OA;depression,mild,normal,DRUG_AMITRIPTYLINE;DRUG_IBUPROFEN;DRUG_SERTRALINE;DRUG_TRAMADOL;DRUG_FUROSEMIDE,0.17,easy
-EP_0009,67,M,COPD;GERD;BPH,mild,normal,DRUG_TRAMADOL;DRUG_FLUOXETINE;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN,0.205,easy
-EP_0010,75,M,dementia;HTN;depression,normal,impaired,DRUG_TRAMADOL;DRUG_DIAZEPAM;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE,0.4425,easy
-EP_0011,83,F,AF,moderate,normal,DRUG_TRAMADOL;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_ALPRAZOLAM,0.2275,easy
-EP_0012,71,F,HTN;GERD;depression,normal,normal,DRUG_LISINOPRIL;DRUG_FLUOXETINE;DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.254,easy
-EP_0013,70,F,HF;HTN;AF,mild,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_ALPRAZOLAM,0.3033,easy
+EP_0009,67,M,COPD;GERD;BPH,mild,normal,DRUG_WARFARIN;DRUG_IBUPROFEN;DRUG_OMEPRAZOLE;DRUG_TAMSULOSIN,0.22,easy
+EP_0010,75,M,dementia;HTN;depression,normal,impaired,DRUG_TRAMADOL;DRUG_SERTRALINE;DRUG_AMITRIPTYLINE,0.2833,easy
+EP_0011,83,F,AF,moderate,normal,DRUG_ALPRAZOLAM;DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_TRAMADOL,0.2275,easy
+EP_0012,71,F,HTN;GERD;depression,normal,normal,DRUG_DIAZEPAM;DRUG_AMLODIPINE;DRUG_FLUOXETINE;DRUG_LISINOPRIL;DRUG_TRAMADOL,0.348,easy
+EP_0013,70,F,HF;HTN;AF,mild,normal,DRUG_ALPRAZOLAM;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.3033,easy
EP_0014,82,F,dementia,normal,normal,DRUG_DONEPEZIL;DRUG_NAPROXEN;DRUG_APIXABAN;DRUG_SPIRONOLACTONE;DRUG_FUROSEMIDE,0.17,easy
EP_0015,84,F,dementia;neuropathy,normal,normal,DRUG_DONEPEZIL;DRUG_GABAPENTIN;DRUG_AMITRIPTYLINE;DRUG_CELECOXIB;DRUG_TRAMADOL,0.17,easy
-EP_0016,83,M,HTN,normal,normal,DRUG_TRAMADOL;DRUG_METOPROLOL;DRUG_ALPRAZOLAM,0.3033,easy
-EP_0017,83,F,CKD,severe,normal,DRUG_APIXABAN;DRUG_AMLODIPINE;DRUG_NAPROXEN,0.2833,easy
-EP_0018,70,F,CKD;HF;HTN,mild,normal,DRUG_SPIRONOLACTONE;DRUG_ALPRAZOLAM;DRUG_TRAMADOL;DRUG_AMLODIPINE;DRUG_METOPROLOL,0.182,easy
-EP_0019,84,M,DM;depression,normal,normal,DRUG_GLIPIZIDE;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_INSULIN_GLARGINE;DRUG_DIAZEPAM,0.448,easy
-EP_0020,90,F,neuropathy;BPH;AF,normal,normal,DRUG_WARFARIN;DRUG_NAPROXEN;DRUG_TAMSULOSIN,0.3,easy
-EP_0021,87,M,HTN;BPH;HF,normal,normal,DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_AMLODIPINE;DRUG_SPIRONOLACTONE,0.2125,easy
-EP_0022,90,M,AF;GERD;DM,normal,impaired,DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_METOPROLOL;DRUG_OMEPRAZOLE,0.2125,easy
-EP_0023,90,F,HF,normal,normal,DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_METOPROLOL,0.2833,easy
+EP_0016,83,M,HTN,normal,normal,DRUG_ALPRAZOLAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.3033,easy
+EP_0017,83,F,CKD,severe,normal,DRUG_DIAZEPAM;DRUG_AMLODIPINE;DRUG_TRAMADOL,0.3067,easy
+EP_0018,70,F,CKD;HF;HTN,mild,normal,DRUG_SPIRONOLACTONE;DRUG_AMLODIPINE;DRUG_ALPRAZOLAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.182,easy
+EP_0019,84,M,DM;depression,normal,normal,DRUG_INSULIN_GLARGINE;DRUG_FLUOXETINE;DRUG_AMITRIPTYLINE;DRUG_GLIPIZIDE;DRUG_TRAMADOL,0.434,easy
+EP_0020,90,F,neuropathy;BPH;AF,normal,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_AMIODARONE;DRUG_TAMSULOSIN,0.2,easy
+EP_0021,87,M,HTN;BPH;HF,normal,normal,DRUG_SPIRONOLACTONE;DRUG_APIXABAN;DRUG_NAPROXEN;DRUG_AMLODIPINE,0.2125,easy
+EP_0022,90,M,AF;GERD;DM,normal,impaired,DRUG_OMEPRAZOLE;DRUG_DIAZEPAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.23,easy
+EP_0023,90,F,HF,normal,normal,DRUG_DIAZEPAM;DRUG_METOPROLOL;DRUG_TRAMADOL,0.3067,easy
EP_0024,71,F,OA,mild,normal,DRUG_IBUPROFEN;DRUG_GABAPENTIN;DRUG_TRAMADOL;DRUG_NAPROXEN;DRUG_APIXABAN,0.17,easy
-EP_0025,71,M,COPD;AF;neuropathy,mild,normal,DRUG_GABAPENTIN;DRUG_WARFARIN;DRUG_NAPROXEN,0.3,easy
-EP_0026,88,M,GERD;dementia,severe,normal,DRUG_TRAMADOL;DRUG_AMITRIPTYLINE;DRUG_DONEPEZIL;DRUG_OMEPRAZOLE,0.2125,easy
+EP_0025,71,M,COPD;AF;neuropathy,mild,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_AMIODARONE;DRUG_GABAPENTIN,0.2,easy
+EP_0026,88,M,GERD;dementia,severe,normal,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_APIXABAN;DRUG_NAPROXEN,0.2125,easy
EP_0027,76,M,AF,normal,normal,DRUG_DIGOXIN;DRUG_METOPROLOL;DRUG_WARFARIN;DRUG_APIXABAN;DRUG_NAPROXEN,0.43,easy
EP_0028,73,F,CKD,moderate,normal,DRUG_AMLODIPINE;DRUG_FUROSEMIDE;DRUG_METFORMIN;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL,0.17,easy
-EP_0029,70,F,CKD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_TRAMADOL;DRUG_GABAPENTIN;DRUG_AMLODIPINE;DRUG_DIAZEPAM,0.184,easy
-EP_0030,87,F,dementia;HF;depression,normal,normal,DRUG_WARFARIN;DRUG_DONEPEZIL;DRUG_FLUOXETINE;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.27,easy
-EP_0031,69,M,HF,severe,normal,DRUG_WARFARIN;DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_FUROSEMIDE;DRUG_NAPROXEN,0.36,easy
+EP_0029,70,F,CKD;OA,mild,normal,DRUG_IBUPROFEN;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_TRAMADOL,0.17,easy
+EP_0030,87,F,dementia;HF;depression,normal,normal,DRUG_DIGOXIN;DRUG_FLUOXETINE;DRUG_DONEPEZIL;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.25,easy
+EP_0031,69,M,HF,severe,normal,DRUG_SPIRONOLACTONE;DRUG_LISINOPRIL;DRUG_DIGOXIN;DRUG_FUROSEMIDE;DRUG_AMIODARONE,0.426,easy
EP_0032,89,F,neuropathy,mild,normal,DRUG_AMITRIPTYLINE;DRUG_GABAPENTIN;DRUG_PREDNISONE;DRUG_TRAMADOL,0.2125,easy
EP_0033,68,F,dementia,mild,impaired,DRUG_DONEPEZIL;DRUG_OMEPRAZOLE;DRUG_SPIRONOLACTONE;DRUG_TRAMADOL;DRUG_ALPRAZOLAM,0.182,easy
-EP_0034,84,F,CKD;HF;HTN,moderate,normal,DRUG_HYDROCHLOROTHIAZIDE;DRUG_DIGOXIN;DRUG_AMIODARONE,0.2667,easy
-EP_0035,74,M,HTN;DM,normal,impaired,DRUG_IBUPROFEN;DRUG_GLIPIZIDE;DRUG_WARFARIN;DRUG_HYDROCHLOROTHIAZIDE;DRUG_METOPROLOL,0.176,easy
-EP_0036,80,F,DM;neuropathy;HTN,severe,normal,DRUG_WARFARIN;DRUG_AMLODIPINE;DRUG_AMITRIPTYLINE;DRUG_NAPROXEN,0.225,easy
-EP_0037,78,M,HF,normal,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_DIAZEPAM;DRUG_LISINOPRIL,0.23,easy
-EP_0038,89,F,HTN;AF,moderate,normal,DRUG_TRAMADOL;DRUG_FUROSEMIDE;DRUG_DIAZEPAM,0.3067,easy
+EP_0034,84,F,CKD;HF;HTN,moderate,normal,DRUG_WARFARIN;DRUG_DIGOXIN;DRUG_NAPROXEN;DRUG_HYDROCHLOROTHIAZIDE,0.225,easy
+EP_0035,74,M,HTN;DM,normal,impaired,DRUG_FLUOXETINE;DRUG_HYDROCHLOROTHIAZIDE;DRUG_GLIPIZIDE;DRUG_METOPROLOL;DRUG_TRAMADOL,0.164,easy
+EP_0036,80,F,DM;neuropathy;HTN,severe,normal,DRUG_DIGOXIN;DRUG_AMLODIPINE;DRUG_AMIODARONE;DRUG_AMITRIPTYLINE,0.2,easy
+EP_0037,78,M,HF,normal,normal,DRUG_LISINOPRIL;DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.2125,easy
+EP_0038,89,F,HTN;AF,moderate,normal,DRUG_AMITRIPTYLINE;DRUG_FUROSEMIDE;DRUG_TRAMADOL,0.2833,easy
EP_0039,78,F,OA;depression,moderate,normal,DRUG_GABAPENTIN;DRUG_FLUOXETINE;DRUG_TRAMADOL;DRUG_SERTRALINE,0.205,easy
-EP_0040,72,F,neuropathy;COPD;BPH,normal,normal,DRUG_TRAMADOL;DRUG_ALPRAZOLAM;DRUG_AMITRIPTYLINE;DRUG_TAMSULOSIN,0.44,easy
+EP_0040,72,F,neuropathy;COPD;BPH,normal,normal,DRUG_ALPRAZOLAM;DRUG_AMITRIPTYLINE;DRUG_TRAMADOL;DRUG_TAMSULOSIN,0.44,easy
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
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
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
diff --git a/openenv-polypharmacy/docker-compose.yml b/openenv-polypharmacy/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2b2bde3a4b6870ab496787ea9e355ab209a085f9
--- /dev/null
+++ b/openenv-polypharmacy/docker-compose.yml
@@ -0,0 +1,35 @@
+version: "3.9"
+
+services:
+ backend:
+ build:
+ context: .
+ dockerfile: backend/Dockerfile
+ container_name: polypharmacy-backend
+ env_file:
+ - .env
+ ports:
+ - "7860:7860"
+ volumes:
+ - ./backend/src:/app/backend/src
+ - ./data:/app/data
+ - ./scripts:/app/scripts
+ - ./backend:/app/backend
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
+ interval: 20s
+ timeout: 5s
+ retries: 5
+
+ frontend:
+ build:
+ context: .
+ dockerfile: frontend/Dockerfile
+ container_name: polypharmacy-frontend
+ depends_on:
+ - backend
+ ports:
+ - "5173:5173"
+ volumes:
+ - ./frontend:/app
+ - /app/node_modules
diff --git a/openenv-polypharmacy/frontend/Dockerfile b/openenv-polypharmacy/frontend/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..aa04742e98c70c50c11a21ee578eabd0f9269e15
--- /dev/null
+++ b/openenv-polypharmacy/frontend/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY frontend/package*.json ./
+RUN npm ci
+
+COPY frontend/ ./
+
+EXPOSE 5173
+
+CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
diff --git a/openenv-polypharmacy/frontend/index.html b/openenv-polypharmacy/frontend/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..a879a0927092473c247198a104ca9a5a4ab0dac9
--- /dev/null
+++ b/openenv-polypharmacy/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Polypharmacy Control Center
+
+
+
+
+
+
diff --git a/openenv-polypharmacy/frontend/package-lock.json b/openenv-polypharmacy/frontend/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..891d0adb22e236c2a759f3266ee843269f1bec5e
--- /dev/null
+++ b/openenv-polypharmacy/frontend/package-lock.json
@@ -0,0 +1,1677 @@
+{
+ "name": "polypharmacy-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "polypharmacy-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.1",
+ "vite": "^5.4.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.16",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
+ "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001786",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
+ "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.331",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
+ "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/openenv-polypharmacy/frontend/package.json b/openenv-polypharmacy/frontend/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..858ae22b50ac1a6732557b4815e882047b198712
--- /dev/null
+++ b/openenv-polypharmacy/frontend/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "polypharmacy-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview --port 4173"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.1",
+ "vite": "^5.4.2"
+ }
+}
diff --git a/openenv-polypharmacy/frontend/src/App.jsx b/openenv-polypharmacy/frontend/src/App.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..60d1e4922436d0bde716bb9739cbf91ed0cb435a
--- /dev/null
+++ b/openenv-polypharmacy/frontend/src/App.jsx
@@ -0,0 +1,387 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+
+function resolveApiBase() {
+ const explicitBase = import.meta.env.VITE_API_BASE;
+ if (explicitBase) return explicitBase.replace(/\/$/, "");
+
+ const host = window.location.hostname;
+ const isLocal =
+ host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0";
+
+ // In local Vite dev, backend runs on :7860. In Spaces/prod, serve same-origin.
+ if (isLocal && window.location.port === "5173") {
+ return "http://localhost:7860";
+ }
+ return window.location.origin.replace(/\/$/, "");
+}
+
+const API_BASE = resolveApiBase();
+const WS_URL = `${API_BASE.replace(/^http/, "ws")}/ws`;
+const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
+
+async function apiPost(path, body) {
+ const res = await fetch(`${API_BASE}${path}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+ if (!res.ok) {
+ const msg = await res.text();
+ throw new Error(msg || `HTTP ${res.status}`);
+ }
+ return res.json();
+}
+
+export default function App() {
+ const [taskId, setTaskId] = useState("budgeted_screening");
+ const [obs, setObs] = useState(null);
+ const [log, setLog] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [action, setAction] = useState({
+ action_type: "query_ddi",
+ drug_id_1: "",
+ drug_id_2: "",
+ target_drug_id: "",
+ intervention_type: "stop",
+ proposed_new_drug_id: "",
+ rationale: "",
+ });
+
+ const medIds = useMemo(
+ () => (obs?.current_medications || []).map((m) => m.drug_id),
+ [obs]
+ );
+ const hasValidEpisode = Boolean(obs?.episode_id) && (obs?.current_medications?.length || 0) > 0;
+ const isDone = Boolean(obs?.done);
+ const finalScore =
+ typeof obs?.metadata?.grader_score === "number" ? obs.metadata.grader_score : null;
+ const noBudgetsLeft =
+ hasValidEpisode &&
+ (obs?.remaining_query_budget ?? 0) <= 0 &&
+ (obs?.remaining_intervention_budget ?? 0) <= 0;
+ const wsRef = useRef(null);
+ const pendingRef = useRef([]);
+
+ const wsEnsure = async () => {
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
+ if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) {
+ await new Promise((r) => setTimeout(r, 80));
+ return wsEnsure();
+ }
+
+ const ws = new WebSocket(WS_URL);
+ wsRef.current = ws;
+
+ ws.onmessage = (evt) => {
+ try {
+ const msg = JSON.parse(evt.data);
+ const pending = pendingRef.current.shift();
+ if (pending) pending.resolve(msg);
+ } catch (e) {
+ const pending = pendingRef.current.shift();
+ if (pending) pending.reject(e);
+ }
+ };
+ ws.onerror = (err) => {
+ const pending = pendingRef.current.shift();
+ if (pending) pending.reject(err);
+ };
+ ws.onclose = () => {
+ wsRef.current = null;
+ };
+
+ await new Promise((resolve, reject) => {
+ const t = setTimeout(() => reject(new Error("WebSocket connect timeout")), 2500);
+ ws.onopen = () => {
+ clearTimeout(t);
+ resolve();
+ };
+ });
+ return ws;
+ };
+
+ const wsSend = async (type, data) => {
+ const ws = await wsEnsure();
+ return await new Promise((resolve, reject) => {
+ pendingRef.current.push({ resolve, reject });
+ ws.send(JSON.stringify({ type, data }));
+ });
+ };
+
+ useEffect(() => {
+ return () => {
+ try {
+ wsRef.current?.close();
+ } catch {
+ // ignore
+ }
+ };
+ }, []);
+
+ const appendLog = (text) => {
+ setLog((prev) => [`${new Date().toLocaleTimeString()} ${text}`, ...prev].slice(0, 20));
+ };
+
+ const normalizeObsFromWs = (packetData) => {
+ const observation = packetData?.observation || {};
+ const mergedMetadata = {
+ ...(observation?.metadata || {}),
+ ...(packetData?.info || {}),
+ };
+ return {
+ ...observation,
+ done: Boolean(packetData?.done ?? observation?.done ?? false),
+ reward: packetData?.reward ?? observation?.reward ?? null,
+ metadata: mergedMetadata,
+ };
+ };
+
+ const handleReset = async () => {
+ setLoading(true);
+ try {
+ const msg = await wsSend("reset", { task_id: taskId });
+ const data = msg?.data || {};
+ const normalized = normalizeObsFromWs(data);
+ setObs(normalized);
+ const ids = (normalized?.current_medications || []).map((m) => m.drug_id);
+ setAction((prev) => ({
+ ...prev,
+ drug_id_1: ids[0] || "",
+ drug_id_2: ids[1] || "",
+ target_drug_id: ids[0] || "",
+ }));
+ appendLog(`Reset task=${taskId}`);
+ } catch (err) {
+ appendLog(`Reset failed: ${err.message}`);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const buildActionPayload = () => {
+ if (noBudgetsLeft) {
+ return { action_type: "finish_review" };
+ }
+ if (action.action_type === "query_ddi") {
+ return {
+ action_type: "query_ddi",
+ drug_id_1: action.drug_id_1,
+ drug_id_2: action.drug_id_2,
+ };
+ }
+ if (action.action_type === "propose_intervention") {
+ return {
+ action_type: "propose_intervention",
+ target_drug_id: action.target_drug_id,
+ intervention_type: action.intervention_type,
+ proposed_new_drug_id: action.proposed_new_drug_id || undefined,
+ rationale: action.rationale || undefined,
+ };
+ }
+ return { action_type: "finish_review" };
+ };
+
+ const isActionValid = () => {
+ if (!hasValidEpisode) return false;
+ if (isDone) return false;
+ if (noBudgetsLeft) return true;
+ if (action.action_type === "query_ddi") {
+ return Boolean(action.drug_id_1 && action.drug_id_2);
+ }
+ if (action.action_type === "propose_intervention") {
+ return Boolean(action.target_drug_id && action.intervention_type);
+ }
+ return true;
+ };
+
+ const handleStep = async (overrideAction = null) => {
+ if (!hasValidEpisode) {
+ appendLog("Run Reset Episode before stepping.");
+ return;
+ }
+ setLoading(true);
+ try {
+ const payload = overrideAction || buildActionPayload();
+ const msg = await wsSend("step", payload);
+ const data = msg?.data || {};
+ const normalized = normalizeObsFromWs(data);
+ setObs(normalized);
+ appendLog(`Step: ${payload.action_type} -> reward=${data.reward ?? 0}`);
+ } catch (err) {
+ appendLog(`Step failed: ${err.message}`);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const askAi = async () => {
+ if (!hasValidEpisode) {
+ appendLog("Run Reset Episode before asking AI.");
+ return;
+ }
+ setLoading(true);
+ try {
+ const data = await apiPost("/agent/suggest", { observation: obs });
+ appendLog(`AI suggestion: ${data.action.action_type}`);
+ await handleStep(data.action);
+ } catch (err) {
+ appendLog(`AI suggestion failed: ${err.message}`);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Polypharmacy Control Center
+
Metaverse Clinical Ops Console
+
+
+ {hasValidEpisode ? "Session Live" : "Waiting for reset"}
+
+
+
+
+
+
+
+
+
+
+ Episode
+ {hasValidEpisode ? (
+
+
Episode{obs.episode_id}
+
Task{obs.task_id}
+
Age / Sex{obs.age} / {obs.sex}
+
Step{obs.step_index}
+
Query budget{obs.remaining_query_budget}
+
Intervention budget{obs.remaining_intervention_budget}
+
+ ) : (
+ Start with Reset Episode. Until then, step actions are blocked.
+ )}
+ {noBudgetsLeft && (
+ Query and intervention budgets are exhausted. Finish review to get final score.
+ )}
+ {isDone && (
+
+ Episode complete
+ {finalScore !== null ? ` • final score: ${finalScore.toFixed(3)}` : ""}.
+ Click Reset Episode to start a new case.
+
+ )}
+
+
+
+
+
+ Current Medications
+
+ {(obs?.current_medications || []).map((m) => (
+
+
{m.drug_id}
+
{m.generic_name}
+
{m.dose_mg} mg • {m.atc_class}
+
+ ))}
+
+
+
+
+ Event Log
+
+ {log.map((line, idx) => (
+
{line}
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/openenv-polypharmacy/frontend/src/main.jsx b/openenv-polypharmacy/frontend/src/main.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5733886c4cff3e43049f0daddcb06387ffd341d1
--- /dev/null
+++ b/openenv-polypharmacy/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./styles.css";
+
+ReactDOM.createRoot(document.getElementById("root")).render(
+
+
+
+);
diff --git a/openenv-polypharmacy/frontend/src/styles.css b/openenv-polypharmacy/frontend/src/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..e1534908aabae5c05be55664e7bec336d971909e
--- /dev/null
+++ b/openenv-polypharmacy/frontend/src/styles.css
@@ -0,0 +1,383 @@
+:root {
+ --bg: #070814;
+ --bg-layer: #0a1026;
+ --panel: rgba(14, 22, 44, 0.72);
+ --panel-solid: rgba(20, 28, 52, 0.92);
+ --text: #e8f1ff;
+ --muted: #9ab2db;
+ --primary: #37d4ff;
+ --primary-2: #5a8dff;
+ --accent: #9d59ff;
+ --success: #6dfbcf;
+ --border: rgba(122, 162, 255, 0.28);
+ --line: rgba(109, 143, 225, 0.18);
+ --shadow: 0 16px 45px rgba(5, 8, 23, 0.6);
+ --shadow-strong: 0 14px 32px rgba(44, 105, 255, 0.4);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ color: var(--text);
+ font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
+ background:
+ radial-gradient(circle at 8% 12%, rgba(121, 87, 255, 0.22), transparent 38%),
+ radial-gradient(circle at 88% 20%, rgba(59, 204, 255, 0.26), transparent 34%),
+ radial-gradient(circle at 50% 100%, rgba(43, 128, 255, 0.26), transparent 40%),
+ linear-gradient(145deg, var(--bg) 0%, var(--bg-layer) 60%, #04060f 100%);
+ background-attachment: fixed;
+}
+
+.shell {
+ min-height: 100vh;
+ position: relative;
+ overflow: hidden;
+ padding: 24px 16px 34px;
+}
+
+.container {
+ width: min(1320px, 100%);
+ margin: 0 auto;
+ position: relative;
+ z-index: 2;
+}
+
+.bg-orb {
+ position: absolute;
+ border-radius: 50%;
+ pointer-events: none;
+ opacity: 0.9;
+ filter: blur(18px);
+}
+
+.orb-a {
+ width: min(46vw, 530px);
+ aspect-ratio: 1 / 1;
+ right: -9%;
+ top: -10%;
+ background: radial-gradient(circle, rgba(52, 203, 255, 0.35), rgba(52, 203, 255, 0.04) 70%);
+}
+
+.orb-b {
+ width: min(40vw, 460px);
+ aspect-ratio: 1 / 1;
+ left: -9%;
+ bottom: -15%;
+ background: radial-gradient(circle, rgba(160, 102, 255, 0.3), rgba(160, 102, 255, 0.06) 72%);
+}
+
+.glass {
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.01)),
+ var(--panel);
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow);
+ backdrop-filter: blur(12px);
+}
+
+.topbar {
+ border-radius: 24px;
+ padding: clamp(14px, 2vw, 20px);
+ display: grid;
+ gap: 12px 16px;
+ grid-template-columns: minmax(220px, 1.2fr) auto minmax(280px, 1fr);
+ align-items: center;
+}
+
+.title-wrap h1 {
+ margin: 0;
+ font-size: clamp(1.15rem, 2.2vw, 1.95rem);
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+ text-shadow: 0 0 16px rgba(106, 192, 255, 0.3);
+}
+
+.title-wrap p {
+ margin: 6px 0 0;
+ font-size: 0.84rem;
+ color: var(--muted);
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+}
+
+.status-chip {
+ justify-self: center;
+ padding: 7px 14px;
+ border-radius: 999px;
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ border: 1px solid transparent;
+}
+
+.status-chip.live {
+ color: #052c24;
+ background: linear-gradient(90deg, rgba(126, 255, 220, 0.9), rgba(84, 244, 196, 0.95));
+ box-shadow: 0 0 14px rgba(96, 244, 198, 0.36);
+}
+
+.status-chip.idle {
+ color: #d8e8ff;
+ border-color: rgba(117, 186, 255, 0.48);
+ background: rgba(60, 106, 198, 0.25);
+}
+
+.actions {
+ display: flex;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+button,
+select,
+input {
+ width: 100%;
+ min-height: 42px;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ font-size: 0.92rem;
+ padding: 10px 12px;
+ color: var(--text);
+ background: rgba(11, 19, 38, 0.84);
+}
+
+select,
+input {
+ transition: border-color 120ms ease, box-shadow 120ms ease;
+}
+
+select:focus,
+input:focus {
+ outline: none;
+ border-color: rgba(119, 200, 255, 0.88);
+ box-shadow: 0 0 0 2px rgba(95, 187, 255, 0.18);
+}
+
+button {
+ cursor: pointer;
+ border: 0;
+ width: auto;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ background: linear-gradient(135deg, var(--primary), var(--primary-2) 55%, var(--accent));
+ box-shadow: var(--shadow-strong);
+ transition: transform 140ms ease, filter 140ms ease, box-shadow 140ms ease;
+}
+
+button:hover {
+ transform: translateY(-1px);
+ filter: brightness(1.04);
+ box-shadow: 0 18px 32px rgba(50, 141, 255, 0.48);
+}
+
+button:active {
+ transform: translateY(0);
+}
+
+button.secondary {
+ background: linear-gradient(135deg, rgba(95, 185, 255, 0.9), rgba(154, 102, 255, 0.86));
+}
+
+button:disabled {
+ opacity: 0.56;
+ cursor: not-allowed;
+ filter: grayscale(0.2);
+ box-shadow: none;
+ transform: none;
+}
+
+.layout {
+ margin-top: 16px;
+ display: grid;
+ gap: 14px;
+ grid-template-columns: 1.12fr 0.88fr;
+ align-items: start;
+}
+
+.panel {
+ border-radius: 20px;
+ padding: clamp(14px, 1.8vw, 20px);
+ position: relative;
+}
+
+.panel::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ border: 1px solid var(--line);
+ pointer-events: none;
+}
+
+.panel-wide {
+ grid-column: 1 / -1;
+}
+
+.panel h2 {
+ margin: 0 0 12px;
+ font-size: 1rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.kpi-grid {
+ display: grid;
+ gap: 10px;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.kpi-grid div {
+ border-radius: 13px;
+ border: 1px solid var(--border);
+ background: var(--panel-solid);
+ padding: 11px 12px;
+}
+
+.kpi-grid span {
+ display: block;
+ margin-bottom: 4px;
+ font-size: 0.72rem;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.kpi-grid strong {
+ font-size: 1.06rem;
+ line-height: 1.2;
+}
+
+.action-row,
+.stack {
+ display: grid;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.action-row label {
+ color: var(--muted);
+ font-size: 0.78rem;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.stack-two {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.med-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+ max-height: 430px;
+ overflow: auto;
+ padding-right: 4px;
+}
+
+.med-card {
+ border-radius: 14px;
+ border: 1px solid var(--border);
+ background: var(--panel-solid);
+ padding: 11px 12px;
+ transition: transform 130ms ease, border-color 130ms ease;
+}
+
+.med-card:hover {
+ transform: translateY(-1px);
+ border-color: rgba(109, 224, 255, 0.72);
+}
+
+.med-card p {
+ margin: 6px 0 4px;
+ color: var(--muted);
+ text-transform: capitalize;
+}
+
+.med-card small {
+ color: #c7d9ff;
+}
+
+.logs {
+ max-height: 300px;
+ overflow: auto;
+ padding-right: 4px;
+ display: grid;
+ gap: 7px;
+ font-size: 0.84rem;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+}
+
+.logs div {
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: rgba(10, 16, 31, 0.84);
+ padding: 8px 10px;
+ color: #dbebff;
+}
+
+.muted {
+ margin: 0;
+ color: var(--muted);
+}
+
+.budget-note {
+ margin-top: 10px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 10px 12px;
+ background: rgba(13, 22, 42, 0.82);
+}
+
+@media (max-width: 1180px) {
+ .layout {
+ grid-template-columns: 1fr;
+ }
+
+ .topbar {
+ grid-template-columns: 1fr;
+ }
+
+ .status-chip {
+ justify-self: start;
+ }
+
+ .actions {
+ justify-content: flex-start;
+ }
+}
+
+@media (max-width: 760px) {
+ .shell {
+ padding: 14px 10px 24px;
+ }
+
+ .topbar,
+ .panel {
+ border-radius: 16px;
+ }
+
+ .actions {
+ width: 100%;
+ }
+
+ .actions button,
+ .actions select {
+ width: 100%;
+ }
+
+ .kpi-grid,
+ .med-grid,
+ .stack-two {
+ grid-template-columns: 1fr;
+ }
+
+ .logs {
+ max-height: 240px;
+ }
+}
diff --git a/openenv-polypharmacy/frontend/vite.config.js b/openenv-polypharmacy/frontend/vite.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a3f20a36de78020e9832ce22b3b8234ae1e5a13
--- /dev/null
+++ b/openenv-polypharmacy/frontend/vite.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ host: "0.0.0.0",
+ },
+});
diff --git a/openenv-polypharmacy/inference.py b/openenv-polypharmacy/inference.py
index 18406cd1aef5d8c9385df50057a357cd30bd04d7..a6809184af7dbc299ba4b8799eff00636647fb81 100644
--- a/openenv-polypharmacy/inference.py
+++ b/openenv-polypharmacy/inference.py
@@ -1,15 +1,14 @@
#!/usr/bin/env python3
"""Baseline LLM inference script for the PolypharmacyEnv.
-Uses the OpenAI Python client to drive an LLM agent through the
+Uses Groq's OpenAI-compatible Chat Completions API to drive an LLM agent through the
PolypharmacyEnv HTTP API. Emits structured stdout logs in the
[START], [STEP], [END] format required by the OpenEnv evaluation spec.
Environment variables:
- OPENAI_API_KEY – required
- API_BASE_URL – LLM endpoint (default: https://api.openai.com/v1)
- MODEL_NAME – model to use (default: gpt-4.1)
- HF_TOKEN – HuggingFace token (optional)
+ GROQ_API_KEY – required
+ GROQ_BASE_URL – optional (default: https://api.groq.com/openai/v1)
+ GROQ_MODEL_NAME – model to use (default: llama-3.1-8b-instant)
POLYPHARMACY_ENV_URL – environment HTTP base URL (default: http://localhost:7860)
"""
@@ -18,7 +17,6 @@ from __future__ import annotations
import json
import os
import sys
-import time
import uuid
from typing import Any, Dict, List
@@ -27,10 +25,9 @@ from openai import OpenAI
# ── Configuration ────────────────────────────────────────────────────────────
-API_KEY = os.environ.get("OPENAI_API_KEY", "")
-API_BASE = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
-MODEL = os.environ.get("MODEL_NAME", "gpt-4.1")
-HF_TOKEN = os.environ.get("HF_TOKEN", "")
+MODEL = os.environ.get("GROQ_MODEL_NAME", "llama-3.1-8b-instant")
+API_KEY = os.environ.get("GROQ_API_KEY", "")
+API_BASE = os.environ.get("GROQ_BASE_URL", "https://api.groq.com/openai/v1")
ENV_URL = os.environ.get("POLYPHARMACY_ENV_URL", "http://localhost:7860")
TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"]
@@ -119,16 +116,16 @@ def _summarise_obs(obs: Dict[str, Any]) -> str:
def _ask_llm(obs_summary: str) -> Dict[str, Any]:
"""Call the LLM and parse a PolypharmacyAction JSON."""
try:
- resp = client.chat.completions.create(
+ chat_resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": obs_summary},
],
- temperature=0.2,
max_tokens=256,
+ temperature=0.2,
)
- text = resp.choices[0].message.content or ""
+ text = (chat_resp.choices[0].message.content or "").strip()
# Strip markdown fences if present
text = text.strip()
if text.startswith("```"):
@@ -146,7 +143,7 @@ def _ask_llm(obs_summary: str) -> Dict[str, Any]:
def main() -> None:
if not API_KEY:
- _err("OPENAI_API_KEY is required")
+ _err("GROQ_API_KEY is required")
sys.exit(1)
run_id = str(uuid.uuid4())[:8]
@@ -159,7 +156,6 @@ def main() -> None:
"run_id": run_id,
"task_id": task_id,
"model": MODEL,
- "api_base": API_BASE,
"episodes": EPISODES_PER_TASK,
})
diff --git a/openenv-polypharmacy/openenv.yaml b/openenv-polypharmacy/openenv.yaml
index 6db62499e908e4244a846f216a136b50ea3476bc..695032aa05514c2e804d3199ef5a7ba417974f72 100644
--- a/openenv-polypharmacy/openenv.yaml
+++ b/openenv-polypharmacy/openenv.yaml
@@ -13,7 +13,7 @@ tags:
- openenv
type: space
runtime: fastapi
-app: polypharmacy_env.api.server:app
+app: backend.main:app
port: 7860
tasks:
diff --git a/openenv-polypharmacy/pyproject.toml b/openenv-polypharmacy/pyproject.toml
index 252e78b374d0f0bc25a9dac82a0c1d84e3b8dace..9bd219ea59455a8765851931c923b726ea32d1d9 100644
--- a/openenv-polypharmacy/pyproject.toml
+++ b/openenv-polypharmacy/pyproject.toml
@@ -13,6 +13,7 @@ dependencies = [
"pydantic>=2.0.0",
"requests>=2.31.0",
"openai>=1.0.0",
+ "python-dotenv>=1.0.0",
"openenv-core>=0.2.0",
]
@@ -25,11 +26,11 @@ dev = [
]
[tool.setuptools.packages.find]
-where = ["src"]
+where = ["backend/src"]
[tool.pytest.ini_options]
-testpaths = ["src/polypharmacy_env/tests"]
-pythonpath = ["src"]
+testpaths = ["backend/src/polypharmacy_env/tests"]
+pythonpath = ["backend/src"]
[tool.black]
line-length = 99
diff --git a/openenv-polypharmacy/requirements.txt b/openenv-polypharmacy/requirements.txt
index 8c3fddc07ec2557f30b561659bbead738a820776..82f21ff0993d8328c16385ebf07082f3318ecf27 100644
--- a/openenv-polypharmacy/requirements.txt
+++ b/openenv-polypharmacy/requirements.txt
@@ -1,8 +1 @@
-fastapi>=0.104.0
-uvicorn>=0.24.0
-pydantic>=2.0.0
-requests>=2.31.0
-openai>=1.0.0
-httpx>=0.25.0
-openenv-core>=0.2.0
-pytest>=7.0.0
+-r backend/requirements.txt
diff --git a/openenv-polypharmacy/scripts/dev_backend.sh b/openenv-polypharmacy/scripts/dev_backend.sh
new file mode 100755
index 0000000000000000000000000000000000000000..83c2586f4d2a8ded9eed3a1c9817ef7bcc710c30
--- /dev/null
+++ b/openenv-polypharmacy/scripts/dev_backend.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+uvicorn backend.main:app --reload --host 0.0.0.0 --port 7860
diff --git a/openenv-polypharmacy/scripts/dev_frontend.sh b/openenv-polypharmacy/scripts/dev_frontend.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d840f21d1f981bb07e20b9b055641ee198888cc2
--- /dev/null
+++ b/openenv-polypharmacy/scripts/dev_frontend.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd frontend
+npm run dev
diff --git a/openenv-polypharmacy/scripts/inference.py b/openenv-polypharmacy/scripts/inference.py
deleted file mode 100644
index ca6610cb0e37b30aa55d173564b18de7da162f72..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/scripts/inference.py
+++ /dev/null
@@ -1,217 +0,0 @@
-#!/usr/bin/env python3
-"""LLM-based inference agent for PolypharmacyEnv.
-
-Connects to a running OpenEnv server via WebSocket (using PolypharmacyClient)
-and runs an LLM agent that reviews a patient's medication regimen.
-
-Usage:
- # Start server first:
- # uvicorn polypharmacy_env.api.server:app --port 7860
-
- # Then run inference:
- python scripts/inference.py --task easy_screening --seed 42
- python scripts/inference.py --task budgeted_screening --model gpt-4o
-"""
-
-from __future__ import annotations
-
-import argparse
-import asyncio
-import json
-import os
-import sys
-from typing import Any, Dict, List
-
-# Add src to path
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-
-from polypharmacy_env.client import PolypharmacyClient
-from polypharmacy_env.models import PolypharmacyAction, PolypharmacyObservation
-
-try:
- from openai import OpenAI
-except ImportError:
- OpenAI = None # type: ignore[assignment, misc]
-
-
-def format_observation_for_llm(obs: PolypharmacyObservation) -> str:
- """Convert an observation to a human-readable prompt for the LLM."""
- lines = [
- f"Patient: {obs.age}yo {obs.sex}",
- f"Conditions: {', '.join(obs.conditions)}",
- f"eGFR: {obs.eGFR_category}, Liver: {obs.liver_function_category}",
- f"Step: {obs.step_index}",
- f"Query budget remaining: {obs.remaining_query_budget}",
- f"Intervention budget remaining: {obs.remaining_intervention_budget}",
- "",
- "Current Medications:",
- ]
- for med in obs.current_medications:
- flags = f" [BEERS: {', '.join(med.beers_flags)}]" if med.beers_flags else ""
- high_risk = " [HIGH RISK ELDERLY]" if med.is_high_risk_elderly else ""
- lines.append(
- f" - {med.drug_id} ({med.generic_name}) {med.atc_class} "
- f"{med.dose_mg}mg{high_risk}{flags}"
- )
-
- if obs.interaction_queries:
- lines.append("")
- lines.append("DDI Queries So Far:")
- for q in obs.interaction_queries:
- lines.append(
- f" - {q.drug_id_1} + {q.drug_id_2}: "
- f"severity={q.severity}, rec={q.recommendation}"
- )
-
- if obs.interventions:
- lines.append("")
- lines.append("Interventions So Far:")
- for iv in obs.interventions:
- lines.append(f" - {iv.action_type} {iv.target_drug_id}: {iv.rationale}")
-
- return "\n".join(lines)
-
-
-SYSTEM_PROMPT = """\
-You are a clinical pharmacist assistant reviewing an elderly patient's medication regimen.
-
-Your goal: identify dangerous drug-drug interactions and Beers Criteria violations,
-then propose safe interventions (stop, dose_reduce, substitute, add_monitoring) to
-reduce risk while preserving therapeutic coverage.
-
-Available actions (respond with JSON):
-1. {"action_type": "query_ddi", "drug_id_1": "...", "drug_id_2": "..."}
- - Check for a drug-drug interaction between two medications.
-2. {"action_type": "propose_intervention", "target_drug_id": "...", \
-"intervention_type": "stop|dose_reduce|substitute|add_monitoring", "rationale": "..."}
- - Propose a change to the regimen.
-3. {"action_type": "finish_review"}
- - End the review and submit your final regimen.
-
-Strategy tips:
-- Query high-risk drug pairs first (especially those flagged as high-risk elderly or Beers).
-- Prioritise resolving severe DDIs over moderate ones.
-- Prefer substitution over stopping when possible.
-- Always provide a clinical rationale for interventions.
-- Finish the review when you've addressed all major issues or exhausted your budget.
-
-Respond with ONLY a valid JSON action object, no explanation outside the JSON.\
-"""
-
-
-def parse_llm_action(text: str) -> PolypharmacyAction:
- """Parse an LLM response into a PolypharmacyAction."""
- text = text.strip()
- # Extract JSON from markdown code blocks if present
- if "```" in text:
- parts = text.split("```")
- for part in parts:
- part = part.strip()
- if part.startswith("json"):
- part = part[4:].strip()
- if part.startswith("{"):
- text = part
- break
-
- data = json.loads(text)
- return PolypharmacyAction(**data)
-
-
-async def run_llm_episode(
- base_url: str,
- task_id: str,
- seed: int,
- model: str,
- max_retries: int = 3,
-) -> Dict[str, Any]:
- """Run a single episode with LLM agent via WebSocket."""
- if OpenAI is None:
- raise ImportError("openai package is required. Install with: pip install openai")
-
- llm = OpenAI()
- total_reward = 0.0
- steps = 0
- messages: List[Dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}]
-
- async with PolypharmacyClient(base_url=base_url) as client:
- result = await client.reset(task_id=task_id, seed=seed)
- obs = result.observation
-
- while not result.done:
- obs_text = format_observation_for_llm(obs)
- messages.append({"role": "user", "content": obs_text})
-
- # Call LLM
- action = None
- for attempt in range(max_retries):
- try:
- response = llm.chat.completions.create(
- model=model,
- messages=messages,
- temperature=0.0,
- max_tokens=256,
- )
- llm_text = response.choices[0].message.content or ""
- messages.append({"role": "assistant", "content": llm_text})
- action = parse_llm_action(llm_text)
- break
- except (json.JSONDecodeError, Exception) as e:
- if attempt == max_retries - 1:
- print(f" LLM parse failed after {max_retries} attempts: {e}")
- action = PolypharmacyAction(action_type="finish_review")
- else:
- messages.append({
- "role": "user",
- "content": f"Invalid JSON. Please respond with only a valid JSON action. Error: {e}",
- })
-
- assert action is not None
- result = await client.step(action)
- obs = result.observation
- total_reward += result.reward or 0.0
- steps += 1
-
- print(
- f" step={steps} action={action.action_type} "
- f"reward={result.reward:.4f} done={result.done}"
- )
-
- return {
- "task_id": task_id,
- "seed": seed,
- "total_reward": total_reward,
- "steps": steps,
- }
-
-
-async def amain(args: argparse.Namespace) -> None:
- results = []
- for seed in range(args.seed, args.seed + args.episodes):
- print(f"\n=== Episode: task={args.task} seed={seed} ===")
- result = await run_llm_episode(
- base_url=args.url,
- task_id=args.task,
- seed=seed,
- model=args.model,
- )
- results.append(result)
- print(f" => reward={result['total_reward']:.4f} steps={result['steps']}")
-
- if results:
- avg_reward = sum(r["total_reward"] for r in results) / len(results)
- print(f"\nAverage reward over {len(results)} episodes: {avg_reward:.4f}")
-
-
-def main() -> None:
- parser = argparse.ArgumentParser(description="Run LLM agent on PolypharmacyEnv")
- parser.add_argument("--url", default="ws://localhost:7860", help="Server URL")
- parser.add_argument("--task", default="budgeted_screening", help="Task ID")
- parser.add_argument("--seed", type=int, default=0, help="Starting seed")
- parser.add_argument("--episodes", type=int, default=1, help="Number of episodes")
- parser.add_argument("--model", default="gpt-4o", help="LLM model name")
- args = parser.parse_args()
- asyncio.run(amain(args))
-
-
-if __name__ == "__main__":
- main()
diff --git a/openenv-polypharmacy/scripts/run_validation.sh b/openenv-polypharmacy/scripts/run_validation.sh
index 3e52345c08b0f5f0d8f9d9f9061bb3f1e648eea1..903c8176803d36fa91643db5aa55b521f7d725e5 100755
--- a/openenv-polypharmacy/scripts/run_validation.sh
+++ b/openenv-polypharmacy/scripts/run_validation.sh
@@ -5,11 +5,11 @@ set -euo pipefail
cd "$(dirname "$0")/.."
echo "=== Running unit tests ==="
-PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
+PYTHONPATH=backend/src python3 -m pytest backend/src/polypharmacy_env/tests/ -v
echo ""
echo "=== Running heuristic baseline ==="
-PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
+PYTHONPATH=backend/src python3 -m polypharmacy_env.baselines.heuristic_agent
echo ""
echo "=== Validation complete ==="
diff --git a/openenv-polypharmacy/src/polypharmacy_env.egg-info/PKG-INFO b/openenv-polypharmacy/src/polypharmacy_env.egg-info/PKG-INFO
deleted file mode 100644
index 05d83d9ac7e4cc3b71e1f66256af405eff75afe5..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/src/polypharmacy_env.egg-info/PKG-INFO
+++ /dev/null
@@ -1,15 +0,0 @@
-Metadata-Version: 2.4
-Name: polypharmacy-env
-Version: 0.1.0
-Summary: OpenEnv environment for elderly polypharmacy medication-review safety
-Requires-Python: >=3.10
-Requires-Dist: fastapi>=0.104.0
-Requires-Dist: uvicorn>=0.24.0
-Requires-Dist: pydantic>=2.0.0
-Requires-Dist: requests>=2.31.0
-Requires-Dist: openai>=1.0.0
-Provides-Extra: dev
-Requires-Dist: pytest>=7.0.0; extra == "dev"
-Requires-Dist: httpx>=0.25.0; extra == "dev"
-Requires-Dist: black; extra == "dev"
-Requires-Dist: isort; extra == "dev"
diff --git a/openenv-polypharmacy/src/polypharmacy_env.egg-info/SOURCES.txt b/openenv-polypharmacy/src/polypharmacy_env.egg-info/SOURCES.txt
deleted file mode 100644
index a2a2806957201f7f0f7b66e1d11408f3a340b983..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/src/polypharmacy_env.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-README.md
-pyproject.toml
-src/polypharmacy_env/__init__.py
-src/polypharmacy_env/config.py
-src/polypharmacy_env/data_loader.py
-src/polypharmacy_env/ddi_simulator.py
-src/polypharmacy_env/env_core.py
-src/polypharmacy_env/graders.py
-src/polypharmacy_env/models.py
-src/polypharmacy_env/rewards.py
-src/polypharmacy_env/tasks.py
-src/polypharmacy_env.egg-info/PKG-INFO
-src/polypharmacy_env.egg-info/SOURCES.txt
-src/polypharmacy_env.egg-info/dependency_links.txt
-src/polypharmacy_env.egg-info/requires.txt
-src/polypharmacy_env.egg-info/top_level.txt
-src/polypharmacy_env/api/__init__.py
-src/polypharmacy_env/api/schemas.py
-src/polypharmacy_env/api/server.py
-src/polypharmacy_env/baselines/__init__.py
-src/polypharmacy_env/baselines/heuristic_agent.py
-src/polypharmacy_env/baselines/random_agent.py
-src/polypharmacy_env/tests/__init__.py
-src/polypharmacy_env/tests/test_api.py
-src/polypharmacy_env/tests/test_env_core.py
\ No newline at end of file
diff --git a/openenv-polypharmacy/src/polypharmacy_env.egg-info/dependency_links.txt b/openenv-polypharmacy/src/polypharmacy_env.egg-info/dependency_links.txt
deleted file mode 100644
index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/src/polypharmacy_env.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/openenv-polypharmacy/src/polypharmacy_env.egg-info/top_level.txt b/openenv-polypharmacy/src/polypharmacy_env.egg-info/top_level.txt
deleted file mode 100644
index 672034d5f27570102fa576097cec1a9c0cec5810..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/src/polypharmacy_env.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-polypharmacy_env
diff --git a/openenv-polypharmacy/src/polypharmacy_env/api/schemas.py b/openenv-polypharmacy/src/polypharmacy_env/api/schemas.py
deleted file mode 100644
index 1dc599143ee121be0892ec2b3e39c202ac17bc53..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/src/polypharmacy_env/api/schemas.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""HTTP request/response schemas.
-
-These are re-exported from openenv.core.env_server.types for convenience.
-The OpenEnv create_app server uses these types natively.
-"""
-
-from openenv.core.env_server.types import (
- HealthResponse,
- ResetRequest,
- ResetResponse,
- StepRequest,
- StepResponse,
-)
-
-__all__ = [
- "ResetRequest",
- "StepRequest",
- "ResetResponse",
- "StepResponse",
- "HealthResponse",
-]
diff --git a/openenv-polypharmacy/src/polypharmacy_env/api/server.py b/openenv-polypharmacy/src/polypharmacy_env/api/server.py
deleted file mode 100644
index e905fa539ffe5f30266312ad3f231d9b6c8e15d4..0000000000000000000000000000000000000000
--- a/openenv-polypharmacy/src/polypharmacy_env/api/server.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""FastAPI server exposing the PolypharmacyEnv via OpenEnv HTTP endpoints.
-
-Uses openenv.core.env_server.http_server.create_app to create a
-standards-compliant OpenEnv server with WebSocket support.
-"""
-
-from __future__ import annotations
-
-from openenv.core.env_server.http_server import create_app
-
-from ..env_core import PolypharmacyEnv
-from ..models import PolypharmacyAction, PolypharmacyObservation
-
-# Create the OpenEnv-compliant app using the framework's create_app.
-# Pass the class (factory) so the server can create per-session instances.
-app = create_app(
- PolypharmacyEnv,
- PolypharmacyAction,
- PolypharmacyObservation,
- env_name="polypharmacy_env",
-)