diff --git a/.gitignore b/.gitignore
index 0bb8010f237a644afe235484f689fc21c449cca4..f0c54a07b09aebabc04554059c4f5238f3e4e456 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
venv/
.DS_Store
-__pycache__/
\ No newline at end of file
+__pycache__/
+node_modules/
+dist/
+.env
\ No newline at end of file
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/README.md b/openenv-polypharmacy/README.md
index 5707521073ad5ade9a331af60d2c4ad0695e1c94..dddbf4047eacd2e74adc50df3b8cf73f80f162f9 100644
--- a/openenv-polypharmacy/README.md
+++ b/openenv-polypharmacy/README.md
@@ -1,184 +1,245 @@
# 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.
+Monorepo for an OpenEnv-compatible medication safety environment with:
----
-
-## 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**.
+- a FastAPI backend (`backend/`)
+- a React frontend (`frontend/`)
+- data assets (`data/`)
+- utility scripts (`scripts/`)
---
-## Action Space
-
-Each step, the agent sends a `PolypharmacyAction` with one of three action types:
+## Repository Structure
-| `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 |
+```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
+```
-Optional fields: `proposed_new_drug_id`, `rationale`.
+---
-## Observation Space
+## What It Does
-`PolypharmacyObservation` includes:
+The environment simulates elderly polypharmacy review. Agent actions:
-- **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`
+- `query_ddi`
+- `propose_intervention`
+- `finish_review`
-## State
+Supported tasks:
-`PolypharmacyState`: `episode_id`, `task_id`, `step_count`, `max_steps`, `num_query_actions`, `num_interventions`.
+- `easy_screening`
+- `budgeted_screening`
+- `complex_tradeoff`
---
-## Tasks
+## Prerequisites
-| 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 |
+- Python 3.10+
+- Node.js 18+ (or 20+ recommended)
+- npm
+- Docker + Docker Compose (optional, for containerized run)
---
-## Reward Structure
+## Environment Setup
-**Per-step shaped rewards:**
+Create `.env`:
-| 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) |
+```bash
+cp .env.example .env
+```
-**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
+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)
---
-## Setup & Usage
+## Local Run (Recommended During Development)
+
+### 1) Install dependencies
-### Install dependencies
+Backend:
```bash
-pip install -r requirements.txt
+pip install -r backend/requirements.txt
```
-### Generate synthetic data
+Frontend:
```bash
-python3 scripts/preprocess_data.py
+cd frontend
+npm install
+cd ..
```
-### Run the API server locally
+### 2) Generate/update synthetic data (if needed)
```bash
-PYTHONPATH=src uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860
+python scripts/preprocess_data.py
```
-### Run the heuristic baseline
+### 3) Start services in two terminals
+
+Terminal A:
```bash
-PYTHONPATH=src python3 -m polypharmacy_env.baselines.heuristic_agent
+./scripts/dev_backend.sh
```
-### Run tests
+Terminal B:
```bash
-PYTHONPATH=src python3 -m pytest src/polypharmacy_env/tests/ -v
+./scripts/dev_frontend.sh
```
-### Run `inference.py` (LLM baseline)
+### 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
-# 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 compose up --build
```
-### Docker
+Stop:
```bash
-docker build -t polypharmacy-env .
-docker run -p 7860:7860 polypharmacy-env
+docker compose down
```
+Ports:
+
+- backend: `7860`
+- frontend: `5173`
+
---
-## Hugging Face Space
+## 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
-This repo is ready for deployment as a HF Space:
+- Go to [Hugging Face Spaces](https://huggingface.co/new-space)
+- Choose **Docker** SDK
+- Create the Space
-- **Space type**: `docker`
-- **Tag**: `openenv`
-- The container listens on port 7860 and exposes `/reset`, `/step`, `/state`, `/health`.
+### 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`.
---
-## Baseline Scores
+## API Endpoints
+
+OpenEnv/health:
-### Heuristic Agent (deterministic, rule-based)
+- `POST /reset`
+- `POST /step`
+- `GET /state`
+- `GET /health`
+- `GET /schema`
+- `WS /ws` (stateful session)
-| Task | Avg Score | Avg Reward |
-|---|---|---|
-| `easy_screening` | ~0.96 | ~1.30 |
-| `budgeted_screening` | ~0.48 | ~0.45 |
-| `complex_tradeoff` | ~0.24 | ~0.11 |
+AI helper:
-*(Scores vary by seed; run `scripts/run_validation.sh` for exact numbers.)*
+- `POST /agent/suggest`
---
-## Project Structure
+## Testing
+
+Run backend tests:
+```bash
+python -m pytest backend/src/polypharmacy_env/tests -v
```
-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
+
+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/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..dd59ecb3e10109445a7b2e9be47177e7cd2d3233
--- /dev/null
+++ b/openenv-polypharmacy/frontend/src/App.jsx
@@ -0,0 +1,371 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+
+const API_BASE = "http://localhost:7860";
+const WS_URL = "ws://localhost:7860/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
+
+
+ {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..4370b6a96b9e10ccdbcdfc945481998770023f90
--- /dev/null
+++ b/openenv-polypharmacy/frontend/src/styles.css
@@ -0,0 +1,304 @@
+:root {
+ --bg: #eef5ff;
+ --panel: rgba(255, 255, 255, 0.82);
+ --panel-solid: #ffffff;
+ --text: #0b2445;
+ --muted: #5b7596;
+ --primary: #1f8bff;
+ --primary-2: #69beff;
+ --accent: #0dd3ff;
+ --border: rgba(93, 156, 219, 0.22);
+ --shadow: 0 20px 50px rgba(25, 83, 143, 0.12);
+ --shadow-strong: 0 20px 42px rgba(31, 112, 182, 0.24);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: "Inter", "SF Pro Text", "Segoe UI", sans-serif;
+ background:
+ radial-gradient(circle at 8% 0%, #cce7ff 0%, rgba(204, 231, 255, 0) 42%),
+ radial-gradient(circle at 92% 100%, #d5efff 0%, rgba(213, 239, 255, 0) 42%),
+ var(--bg);
+ color: var(--text);
+}
+
+.shell {
+ min-height: 100vh;
+ position: relative;
+ padding: 28px 22px;
+ overflow: hidden;
+}
+
+.container {
+ width: min(1300px, 100%);
+ margin: 0 auto;
+ position: relative;
+ z-index: 1;
+}
+
+.bg-orb {
+ position: absolute;
+ border-radius: 50%;
+ filter: blur(18px);
+ opacity: 0.9;
+}
+.orb-a {
+ width: 420px;
+ height: 420px;
+ right: -120px;
+ top: -100px;
+ background: radial-gradient(circle, rgba(72, 168, 255, 0.5), rgba(72, 168, 255, 0.1));
+}
+.orb-b {
+ width: 360px;
+ height: 360px;
+ left: -100px;
+ bottom: -120px;
+ background: radial-gradient(circle, rgba(110, 200, 255, 0.4), rgba(141, 205, 255, 0.06));
+}
+
+.glass {
+ backdrop-filter: blur(12px);
+ border: 1px solid var(--border);
+ background: var(--panel);
+ box-shadow: var(--shadow);
+}
+
+.topbar {
+ border-radius: 20px;
+ padding: 18px;
+ display: grid;
+ grid-template-columns: 1.2fr auto 1fr;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+}
+
+.title-wrap h1 {
+ margin: 0;
+ font-size: clamp(1.1rem, 1.5vw, 1.45rem);
+ letter-spacing: 0.01em;
+}
+
+.title-wrap p {
+ margin: 4px 0 0;
+ color: var(--muted);
+ font-size: 0.92rem;
+}
+
+.status-chip {
+ justify-self: center;
+ border-radius: 999px;
+ padding: 7px 12px;
+ font-size: 0.76rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ border: 1px solid transparent;
+}
+
+.status-chip.live {
+ color: #0d6a3f;
+ background: rgba(130, 245, 195, 0.18);
+ border-color: rgba(70, 199, 142, 0.3);
+}
+
+.status-chip.idle {
+ color: #24527f;
+ background: rgba(114, 194, 255, 0.18);
+ border-color: rgba(62, 152, 223, 0.28);
+}
+
+.actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+button,
+select,
+input {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 10px 13px;
+ font-size: 0.92rem;
+ background: #fff;
+ color: var(--text);
+ min-height: 42px;
+}
+
+button {
+ cursor: pointer;
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-2) 78%, var(--accent) 100%);
+ color: #fff;
+ border: none;
+ font-weight: 700;
+ box-shadow: var(--shadow-strong);
+ transition: transform 120ms ease, filter 120ms ease;
+}
+
+button:hover {
+ transform: translateY(-1px);
+ filter: brightness(1.02);
+}
+
+button.secondary {
+ background: linear-gradient(135deg, #68c2ff, #9dd9ff);
+}
+
+button:disabled {
+ opacity: 0.58;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.layout {
+ margin-top: 18px;
+ display: grid;
+ gap: 16px;
+ grid-template-columns: 1.15fr 0.85fr;
+ align-items: start;
+}
+
+.panel {
+ border-radius: 18px;
+ padding: 18px;
+}
+
+.panel-wide {
+ grid-column: 1 / -1;
+}
+
+.panel h2 {
+ margin: 0 0 12px;
+ font-size: 1rem;
+ letter-spacing: 0.01em;
+}
+
+.kpi-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.kpi-grid div {
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 12px;
+}
+
+.kpi-grid span {
+ display: block;
+ font-size: 0.74rem;
+ color: var(--muted);
+ margin-bottom: 4px;
+}
+
+.kpi-grid strong {
+ font-size: 1.05rem;
+}
+
+.action-row,
+.stack {
+ display: grid;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.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: 420px;
+ overflow: auto;
+ padding-right: 2px;
+}
+
+.med-card {
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 12px;
+ background: var(--panel-solid);
+ transition: transform 120ms ease, box-shadow 120ms ease;
+}
+
+.med-card:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 10px 25px rgba(44, 105, 165, 0.12);
+}
+
+.med-card p {
+ margin: 6px 0 4px;
+ color: var(--muted);
+ text-transform: capitalize;
+}
+
+.logs {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 0.85rem;
+ max-height: 300px;
+ overflow: auto;
+ display: grid;
+ gap: 6px;
+ padding-right: 2px;
+}
+
+.logs div {
+ background: rgba(255, 255, 255, 0.78);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 8px 10px;
+}
+
+.muted {
+ color: var(--muted);
+ margin: 0;
+}
+
+.budget-note {
+ margin-top: 10px;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.78);
+}
+
+@media (max-width: 1120px) {
+ .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: 18px 12px;
+ }
+
+ .kpi-grid,
+ .med-grid,
+ .stack-two {
+ grid-template-columns: 1fr;
+ }
+}
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",
-)