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. +

+ )} +
+ +
+

Action Console

+
+ + +
+ + {action.action_type === "query_ddi" && ( +
+ setAction((a) => ({ ...a, drug_id_1: e.target.value }))} + /> + setAction((a) => ({ ...a, drug_id_2: e.target.value }))} + /> +
+ )} + + {action.action_type === "propose_intervention" && ( +
+ + + + setAction((a) => ({ ...a, proposed_new_drug_id: e.target.value })) + } + /> + setAction((a) => ({ ...a, rationale: e.target.value }))} + /> +
+ )} + +
+ +
+

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", -)