diff --git a/.dockerignore b/.dockerignore index 7566d677dc0c4ad7c9c7bc6a176c6b7186e9c728..ea2205ebecdbc61a7ad350e8d4261a94ae2e7466 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,4 +9,4 @@ **/dist **/.env **/.env.* -!openenv-polypharmacy/.env.example +!.env.example diff --git a/openenv-polypharmacy/.env.example b/.env.example similarity index 100% rename from openenv-polypharmacy/.env.example rename to .env.example diff --git a/.gitignore b/.gitignore index a21ac13278960096fd010e674651b109be6c6cc4..f1beb4e73c68e8f3e03711363ed8909980cd05df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ venv/ env/ .env .env.* -!openenv-polypharmacy/.env.example +!.env.example *.py[cod] __pycache__/ .pytest_cache/ @@ -29,7 +29,3 @@ pnpm-debug.log* *.swp .DS_Store -# --- Project-specific nested paths --- -openenv-polypharmacy/frontend/node_modules/ -openenv-polypharmacy/frontend/dist/ -openenv-polypharmacy/.pytest_cache/ diff --git a/Dockerfile b/Dockerfile index 10f199b9446b1375bce69953826c903f9ed43efe..68b69d986a780501f6c9461410b2add26413473e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM node:20-alpine AS frontend-builder WORKDIR /app/frontend -COPY openenv-polypharmacy/frontend/package*.json ./ +COPY frontend/package*.json ./ RUN npm ci -COPY openenv-polypharmacy/frontend/ ./ +COPY frontend/ ./ RUN npm run build FROM python:3.11-slim @@ -13,15 +13,15 @@ RUN apt-get update && \ WORKDIR /app -COPY openenv-polypharmacy/backend/requirements.txt /app/backend/requirements.txt +COPY backend/requirements.txt /app/backend/requirements.txt RUN pip install --no-cache-dir -r /app/backend/requirements.txt -COPY openenv-polypharmacy/backend /app/backend -COPY openenv-polypharmacy/data /app/data -COPY openenv-polypharmacy/scripts /app/scripts -COPY openenv-polypharmacy/openenv.yaml /app/openenv.yaml -COPY openenv-polypharmacy/.env.example /app/.env.example -COPY openenv-polypharmacy/inference.py /app/inference.py +COPY 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 --from=frontend-builder /app/frontend/dist /app/frontend/dist diff --git a/openenv-polypharmacy/PROMPT.md b/PROMPT.md similarity index 100% rename from openenv-polypharmacy/PROMPT.md rename to PROMPT.md diff --git a/README.md b/README.md index 47cf15298ea2d8ab20e70f456ab4d13e57247980..3bc9290d48e3b9b05d31a2d253526c654735804e 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,250 @@ sdk: docker pinned: false --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# PolypharmacyEnv + +Monorepo for an OpenEnv-compatible medication safety environment with: + +- a FastAPI backend (`backend/`) +- a React frontend (`frontend/`) +- data assets (`data/`) +- utility scripts (`scripts/`) + +--- + +## Repository Structure + +```text +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 # Baseline inference script (required at root) +.env.example # Environment template +``` + +--- + +## What It Does + +The environment simulates elderly polypharmacy review. Agent actions: + +- `query_ddi` +- `propose_intervention` +- `finish_review` + +Supported tasks: + +- `easy_screening` +- `budgeted_screening` +- `complex_tradeoff` + +--- + +## Prerequisites + +- Python 3.10+ +- Node.js 18+ (or 20+ recommended) +- npm +- Docker + Docker Compose (optional, for containerized run) + +--- + +## Environment Setup + +Create `.env`: + +```bash +cp .env.example .env +``` + +Set values for local backend integrations as needed. + +--- + +## Local Run (Recommended During Development) + +### 1) Install dependencies + +Backend: + +```bash +pip install -r backend/requirements.txt +``` + +Frontend: + +```bash +cd frontend +npm install +cd .. +``` + +### 2) Generate/update synthetic data (if needed) + +```bash +python scripts/preprocess_data.py +``` + +### 3) Start services in two terminals + +Terminal A: + +```bash +./scripts/dev_backend.sh +``` + +Terminal B: + +```bash +./scripts/dev_frontend.sh +``` + +### 4) Open app + +- Frontend: [http://localhost:5173](http://localhost:5173) +- Backend health: [http://localhost:7860/health](http://localhost:7860/health) + +--- + +## Docker Run + +Run both services: + +```bash +docker compose up --build +``` + +Stop: + +```bash +docker compose down +``` + +Ports: + +- backend: `7860` +- frontend: `5173` + +--- + +## Hugging Face Spaces Deployment (Docker) + +This repo now includes a **root `Dockerfile`** that builds frontend + backend into one container, so Spaces can host both API and UI together. + +### 1) Create a new Space + +- Go to [Hugging Face Spaces](https://huggingface.co/new-space) +- Choose **Docker** SDK +- Create the Space + +### 2) Add Space secrets/variables + +In Space Settings -> Variables and Secrets: + +- Secret: `HF_TOKEN` +- Variable: `API_BASE_URL=https://router.huggingface.co/v1` +- Variable: `MODEL_NAME=Qwen/Qwen2.5-72B-Instruct` + +### 3) Push this repository to the Space + +Commit and push all files, including root `Dockerfile`. + +### 4) Verify after build + +- Space root URL loads the React UI +- `/health` returns healthy status +- OpenEnv endpoints are available (`/reset`, `/step`, `/state`, `/schema`) + +Notes: + +- Container reads `PORT` (defaults to `7860`) which is Space-friendly. +- Frontend static assets are served by FastAPI from `frontend/dist`. + +--- + +## API Endpoints + +OpenEnv/health: + +- `POST /reset` +- `POST /step` +- `GET /state` +- `GET /health` +- `GET /schema` +- `WS /ws` (stateful session) + +AI helper: + +- `POST /agent/suggest` + +--- + +## Testing + +Run backend tests: + +```bash +python -m pytest backend/src/polypharmacy_env/tests -v +``` + +Or run validation script: + +```bash +./scripts/run_validation.sh +``` + +### Submission validation + +```bash +openenv validate +python inference.py +``` + +--- + +## 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/backend/Dockerfile similarity index 100% rename from openenv-polypharmacy/backend/Dockerfile rename to backend/Dockerfile diff --git a/openenv-polypharmacy/backend/__init__.py b/backend/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/__init__.py rename to backend/__init__.py diff --git a/openenv-polypharmacy/backend/main.py b/backend/main.py similarity index 100% rename from openenv-polypharmacy/backend/main.py rename to backend/main.py diff --git a/openenv-polypharmacy/backend/requirements.txt b/backend/requirements.txt similarity index 100% rename from openenv-polypharmacy/backend/requirements.txt rename to backend/requirements.txt diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py b/backend/src/polypharmacy_env/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/__init__.py rename to backend/src/polypharmacy_env/__init__.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py b/backend/src/polypharmacy_env/api/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/api/__init__.py rename to backend/src/polypharmacy_env/api/__init__.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py b/backend/src/polypharmacy_env/api/app.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/api/app.py rename to backend/src/polypharmacy_env/api/app.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py b/backend/src/polypharmacy_env/api/routes/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/__init__.py rename to backend/src/polypharmacy_env/api/routes/__init__.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py b/backend/src/polypharmacy_env/api/routes/agent.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/api/routes/agent.py rename to backend/src/polypharmacy_env/api/routes/agent.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py b/backend/src/polypharmacy_env/api/server.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/api/server.py rename to backend/src/polypharmacy_env/api/server.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py b/backend/src/polypharmacy_env/baselines/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/baselines/__init__.py rename to backend/src/polypharmacy_env/baselines/__init__.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py b/backend/src/polypharmacy_env/baselines/heuristic_agent.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/baselines/heuristic_agent.py rename to backend/src/polypharmacy_env/baselines/heuristic_agent.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py b/backend/src/polypharmacy_env/baselines/random_agent.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/baselines/random_agent.py rename to backend/src/polypharmacy_env/baselines/random_agent.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/client.py b/backend/src/polypharmacy_env/client.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/client.py rename to backend/src/polypharmacy_env/client.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/config.py b/backend/src/polypharmacy_env/config.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/config.py rename to backend/src/polypharmacy_env/config.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py b/backend/src/polypharmacy_env/data_loader.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/data_loader.py rename to backend/src/polypharmacy_env/data_loader.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py b/backend/src/polypharmacy_env/ddi_simulator.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/ddi_simulator.py rename to backend/src/polypharmacy_env/ddi_simulator.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py b/backend/src/polypharmacy_env/env_core.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/env_core.py rename to backend/src/polypharmacy_env/env_core.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/graders.py b/backend/src/polypharmacy_env/graders.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/graders.py rename to backend/src/polypharmacy_env/graders.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/models.py b/backend/src/polypharmacy_env/models.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/models.py rename to backend/src/polypharmacy_env/models.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py b/backend/src/polypharmacy_env/rewards.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/rewards.py rename to backend/src/polypharmacy_env/rewards.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py b/backend/src/polypharmacy_env/services/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/services/__init__.py rename to backend/src/polypharmacy_env/services/__init__.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py b/backend/src/polypharmacy_env/services/groq_agent.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/services/groq_agent.py rename to backend/src/polypharmacy_env/services/groq_agent.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py b/backend/src/polypharmacy_env/tasks.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/tasks.py rename to backend/src/polypharmacy_env/tasks.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py b/backend/src/polypharmacy_env/tests/__init__.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/tests/__init__.py rename to backend/src/polypharmacy_env/tests/__init__.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py b/backend/src/polypharmacy_env/tests/test_api.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_api.py rename to backend/src/polypharmacy_env/tests/test_api.py diff --git a/openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py b/backend/src/polypharmacy_env/tests/test_env_core.py similarity index 100% rename from openenv-polypharmacy/backend/src/polypharmacy_env/tests/test_env_core.py rename to backend/src/polypharmacy_env/tests/test_env_core.py diff --git a/openenv-polypharmacy/data/lookups/beers_criteria.csv b/data/lookups/beers_criteria.csv similarity index 100% rename from openenv-polypharmacy/data/lookups/beers_criteria.csv rename to data/lookups/beers_criteria.csv diff --git a/openenv-polypharmacy/data/lookups/ddi_rules.csv b/data/lookups/ddi_rules.csv similarity index 100% rename from openenv-polypharmacy/data/lookups/ddi_rules.csv rename to data/lookups/ddi_rules.csv diff --git a/openenv-polypharmacy/data/lookups/drug_metadata.csv b/data/lookups/drug_metadata.csv similarity index 100% rename from openenv-polypharmacy/data/lookups/drug_metadata.csv rename to data/lookups/drug_metadata.csv diff --git a/openenv-polypharmacy/data/processed/patients_polypharmacy.csv b/data/processed/patients_polypharmacy.csv similarity index 100% rename from openenv-polypharmacy/data/processed/patients_polypharmacy.csv rename to data/processed/patients_polypharmacy.csv diff --git a/openenv-polypharmacy/docker-compose.yml b/docker-compose.yml similarity index 100% rename from openenv-polypharmacy/docker-compose.yml rename to docker-compose.yml diff --git a/openenv-polypharmacy/frontend/Dockerfile b/frontend/Dockerfile similarity index 100% rename from openenv-polypharmacy/frontend/Dockerfile rename to frontend/Dockerfile diff --git a/openenv-polypharmacy/frontend/index.html b/frontend/index.html similarity index 100% rename from openenv-polypharmacy/frontend/index.html rename to frontend/index.html diff --git a/openenv-polypharmacy/frontend/package-lock.json b/frontend/package-lock.json similarity index 100% rename from openenv-polypharmacy/frontend/package-lock.json rename to frontend/package-lock.json diff --git a/openenv-polypharmacy/frontend/package.json b/frontend/package.json similarity index 100% rename from openenv-polypharmacy/frontend/package.json rename to frontend/package.json diff --git a/openenv-polypharmacy/frontend/src/App.jsx b/frontend/src/App.jsx similarity index 100% rename from openenv-polypharmacy/frontend/src/App.jsx rename to frontend/src/App.jsx diff --git a/openenv-polypharmacy/frontend/src/main.jsx b/frontend/src/main.jsx similarity index 100% rename from openenv-polypharmacy/frontend/src/main.jsx rename to frontend/src/main.jsx diff --git a/openenv-polypharmacy/frontend/src/styles.css b/frontend/src/styles.css similarity index 100% rename from openenv-polypharmacy/frontend/src/styles.css rename to frontend/src/styles.css diff --git a/openenv-polypharmacy/frontend/vite.config.js b/frontend/vite.config.js similarity index 100% rename from openenv-polypharmacy/frontend/vite.config.js rename to frontend/vite.config.js diff --git a/inference.py b/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..a8a00389ea7c79f9c7c3e1ec5c0f21eda89ceaf2 --- /dev/null +++ b/inference.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Submission inference script for Polypharmacy OpenEnv environment. + +Required environment variables: + API_BASE_URL OpenAI-compatible base URL + MODEL_NAME Model identifier + HF_TOKEN API key/token + +Optional: + POLYPHARMACY_ENV_URL Environment API base (default: http://localhost:7860) +""" + +from __future__ import annotations + +import json +import os +import re +from typing import Any, Dict, List + +import requests +from openai import OpenAI + +API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") +MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct") +HF_TOKEN = os.getenv("HF_TOKEN", "") +ENV_URL = os.getenv("POLYPHARMACY_ENV_URL", "http://localhost:7860").rstrip("/") + +BENCHMARK = "polypharmacy_env" +TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"] +MAX_STEPS = 16 +TEMPERATURE = 0.0 +MAX_TOKENS = 220 + +SYSTEM_PROMPT = ( + "You are a clinical-pharmacist agent. " + "Return one JSON action only with keys matching this schema: " + '{"action_type":"query_ddi|propose_intervention|finish_review",' + '"drug_id_1":"", "drug_id_2":"", "target_drug_id":"",' + '"intervention_type":"stop|dose_reduce|substitute|add_monitoring",' + '"proposed_new_drug_id":"", "rationale":""}. ' + "Prefer safe, high-impact actions and finish when useful actions are exhausted." +) + + +def _b(v: bool) -> str: + return str(bool(v)).lower() + + +def _fmt_reward(v: float) -> str: + return f"{float(v):.2f}" + + +def _clamp01(v: float) -> float: + return max(0.0, min(1.0, float(v))) + + +def log_start(task: str) -> None: + print(f"[START] task={task} env={BENCHMARK} model={MODEL_NAME}", flush=True) + + +def log_step(step: int, action_str: str, reward: float, done: bool, error: str | None) -> None: + err = error if error else "null" + print( + f"[STEP] step={step} action={action_str} reward={_fmt_reward(reward)} " + f"done={_b(done)} error={err}", + flush=True, + ) + + +def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None: + rewards_str = ",".join(_fmt_reward(r) for r in rewards) + print( + f"[END] success={_b(success)} steps={steps} score={_clamp01(score):.3f} rewards={rewards_str}", + flush=True, + ) + + +def _safe_json(text: str) -> Dict[str, Any]: + text = text.strip() + if text.startswith("```"): + text = re.sub(r"^```[a-zA-Z]*\n?", "", text) + text = text.replace("```", "").strip() + try: + data = json.loads(text) + if isinstance(data, dict): + return data + except Exception: + pass + return {"action_type": "finish_review"} + + +def _llm_action(client: OpenAI, obs: Dict[str, Any]) -> Dict[str, Any]: + meds = obs.get("current_medications", []) + summary = { + "step_index": obs.get("step_index", 0), + "remaining_query_budget": obs.get("remaining_query_budget", 0), + "remaining_intervention_budget": obs.get("remaining_intervention_budget", 0), + "conditions": obs.get("conditions", []), + "current_medications": [ + { + "drug_id": m.get("drug_id"), + "generic_name": m.get("generic_name"), + "dose_mg": m.get("dose_mg"), + "beers_flags": m.get("beers_flags", []), + } + for m in meds + ], + "interaction_queries": obs.get("interaction_queries", []), + "interventions": obs.get("interventions", []), + } + resp = client.chat.completions.create( + model=MODEL_NAME, + temperature=TEMPERATURE, + max_tokens=MAX_TOKENS, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": json.dumps(summary, separators=(",", ":"))}, + ], + ) + content = (resp.choices[0].message.content or "").strip() + return _safe_json(content) + + +def _reset(task_id: str) -> Dict[str, Any]: + r = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id}, timeout=45) + r.raise_for_status() + return r.json() + + +def _step(action: Dict[str, Any]) -> Dict[str, Any]: + r = requests.post(f"{ENV_URL}/step", json={"action": action}, timeout=45) + r.raise_for_status() + return r.json() + + +def run_task(client: OpenAI, task_id: str) -> None: + rewards: List[float] = [] + steps = 0 + success = False + score = 0.0 + log_start(task_id) + try: + reset_payload = _reset(task_id) + obs = reset_payload.get("observation", {}) + done = bool(reset_payload.get("done", False)) + + for i in range(1, MAX_STEPS + 1): + if done: + break + action = _llm_action(client, obs) + action_str = json.dumps(action, separators=(",", ":")) + step_payload = _step(action) + obs = step_payload.get("observation", {}) + reward = float(step_payload.get("reward") or 0.0) + done = bool(step_payload.get("done", False)) + metadata = (obs or {}).get("metadata", {}) or {} + last_error = metadata.get("error") + rewards.append(reward) + steps = i + log_step(i, action_str, reward, done, str(last_error) if last_error else None) + + if done: + raw_score = metadata.get("grader_score", None) + if raw_score is not None: + score = _clamp01(float(raw_score)) + else: + score = _clamp01(sum(max(0.0, r) for r in rewards) / max(len(rewards), 1)) + success = score > 0.0 + break + except Exception: + # Still emit END to keep evaluator parser stable. + success = False + finally: + log_end(success=success, steps=steps, score=score, rewards=rewards) + + +def main() -> int: + if not HF_TOKEN: + print("HF_TOKEN is required", flush=True) + return 1 + client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN) + for task in TASKS: + run_task(client, task) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/openenv-polypharmacy/.dockerignore b/openenv-polypharmacy/.dockerignore deleted file mode 100644 index 5007867e3a3b2c5514c4ff5bb18588b36951901f..0000000000000000000000000000000000000000 --- a/openenv-polypharmacy/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -.git -.gitignore -**/__pycache__/ -**/.pytest_cache/ -**/.DS_Store -.env -frontend/node_modules -frontend/dist diff --git a/openenv-polypharmacy/Dockerfile b/openenv-polypharmacy/Dockerfile deleted file mode 100644 index 68b69d986a780501f6c9461410b2add26413473e..0000000000000000000000000000000000000000 --- a/openenv-polypharmacy/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -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 - -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 openenv.yaml /app/openenv.yaml -COPY .env.example /app/.env.example -COPY inference.py /app/inference.py - -COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist - -RUN python3 /app/scripts/preprocess_data.py - -ENV PORT=7860 -ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}" -ENV PYTHONUNBUFFERED=1 - -EXPOSE 7860 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \ - CMD curl -f http://localhost:7860/health || exit 1 - -CMD ["sh", "-c", "uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860}"] diff --git a/openenv-polypharmacy/README.md b/openenv-polypharmacy/README.md deleted file mode 100644 index dddbf4047eacd2e74adc50df3b8cf73f80f162f9..0000000000000000000000000000000000000000 --- a/openenv-polypharmacy/README.md +++ /dev/null @@ -1,245 +0,0 @@ -# PolypharmacyEnv - -Monorepo for an OpenEnv-compatible medication safety environment with: - -- a FastAPI backend (`backend/`) -- a React frontend (`frontend/`) -- data assets (`data/`) -- utility scripts (`scripts/`) - ---- - -## Repository Structure - -```text -openenv-polypharmacy/ - backend/ - main.py # ASGI entrypoint (uvicorn target) - requirements.txt # Backend dependencies - Dockerfile # Backend container - src/polypharmacy_env/ # Python package source - api/ - app.py # FastAPI/OpenEnv app assembly - server.py # Compatibility import wrapper - routes/agent.py # /agent/suggest route - services/ - groq_agent.py # Groq-based action suggestion logic - env_core.py # OpenEnv environment core - models.py # Action/observation/state models - data_loader.py # CSV loading - ddi_simulator.py # DDI and Beers lookups - rewards.py # Reward shaping - graders.py # Task graders - tasks.py # Task/episode selection - tests/ # Backend tests - frontend/ - src/ # React UI code - package.json - Dockerfile # Frontend container - data/ - lookups/ # drug_metadata.csv, ddi_rules.csv, beers_criteria.csv - processed/ # patients_polypharmacy.csv - scripts/ - preprocess_data.py # Synthetic data generation - dev_backend.sh # Local backend run helper - dev_frontend.sh # Local frontend run helper - run_validation.sh # Tests + baseline validation - docker-compose.yml # Full stack orchestration - openenv.yaml # OpenEnv manifest - inference.py # Optional CLI inference baseline - .env.example # Environment template -``` - ---- - -## What It Does - -The environment simulates elderly polypharmacy review. Agent actions: - -- `query_ddi` -- `propose_intervention` -- `finish_review` - -Supported tasks: - -- `easy_screening` -- `budgeted_screening` -- `complex_tradeoff` - ---- - -## Prerequisites - -- Python 3.10+ -- Node.js 18+ (or 20+ recommended) -- npm -- Docker + Docker Compose (optional, for containerized run) - ---- - -## Environment Setup - -Create `.env`: - -```bash -cp .env.example .env -``` - -Set values: - -- `GROQ_API_KEY=...` (required) -- `GROQ_BASE_URL=https://api.groq.com/openai/v1` (recommended) -- `GROQ_MODEL_NAME=llama-3.3-70b-versatile` (recommended) - ---- - -## Local Run (Recommended During Development) - -### 1) Install dependencies - -Backend: - -```bash -pip install -r backend/requirements.txt -``` - -Frontend: - -```bash -cd frontend -npm install -cd .. -``` - -### 2) Generate/update synthetic data (if needed) - -```bash -python scripts/preprocess_data.py -``` - -### 3) Start services in two terminals - -Terminal A: - -```bash -./scripts/dev_backend.sh -``` - -Terminal B: - -```bash -./scripts/dev_frontend.sh -``` - -### 4) Open app - -- Frontend: [http://localhost:5173](http://localhost:5173) -- Backend health: [http://localhost:7860/health](http://localhost:7860/health) - ---- - -## Docker Run - -Run both services: - -```bash -docker compose up --build -``` - -Stop: - -```bash -docker compose down -``` - -Ports: - -- backend: `7860` -- frontend: `5173` - ---- - -## Hugging Face Spaces Deployment (Docker) - -This repo now includes a **root `Dockerfile`** that builds frontend + backend into one container, so Spaces can host both API and UI together. - -### 1) Create a new Space - -- Go to [Hugging Face Spaces](https://huggingface.co/new-space) -- Choose **Docker** SDK -- Create the Space - -### 2) Add Space secrets/variables - -In Space Settings -> Variables and Secrets: - -- Secret: `GROQ_API_KEY` -- Variable: `GROQ_BASE_URL=https://api.groq.com/openai/v1` -- Variable: `GROQ_MODEL_NAME=llama-3.3-70b-versatile` - -### 3) Push this repository to the Space - -Commit and push all files, including root `Dockerfile`. - -### 4) Verify after build - -- Space root URL loads the React UI -- `/health` returns healthy status -- OpenEnv endpoints are available (`/reset`, `/step`, `/state`, `/schema`) - -Notes: - -- Container reads `PORT` (defaults to `7860`) which is Space-friendly. -- Frontend static assets are served by FastAPI from `frontend/dist`. - ---- - -## API Endpoints - -OpenEnv/health: - -- `POST /reset` -- `POST /step` -- `GET /state` -- `GET /health` -- `GET /schema` -- `WS /ws` (stateful session) - -AI helper: - -- `POST /agent/suggest` - ---- - -## Testing - -Run backend tests: - -```bash -python -m pytest backend/src/polypharmacy_env/tests -v -``` - -Or run validation script: - -```bash -./scripts/run_validation.sh -``` - ---- - -## Notes - -- OpenEnv HTTP reset/step is stateless; multi-step episode continuity should use websocket (`/ws`). -- The frontend uses websocket for episode continuity and HTTP for AI suggestion. -- AI behavior includes rule-based guardrails to avoid repetitive low-value loops. - ---- - -## Troubleshooting - -- `ModuleNotFoundError: polypharmacy_env` - - Start backend using `./scripts/dev_backend.sh` from repo root. -- `/agent/suggest` fails - - Check `.env` keys and restart backend. -- UI state looks stale - - Hard refresh browser and click `Reset Episode`. diff --git a/openenv-polypharmacy/inference.py b/openenv-polypharmacy/inference.py deleted file mode 100644 index a6809184af7dbc299ba4b8799eff00636647fb81..0000000000000000000000000000000000000000 --- a/openenv-polypharmacy/inference.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -"""Baseline LLM inference script for the PolypharmacyEnv. - -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: - 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) -""" - -from __future__ import annotations - -import json -import os -import sys -import uuid -from typing import Any, Dict, List - -import requests -from openai import OpenAI - -# ── Configuration ──────────────────────────────────────────────────────────── - -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"] -EPISODES_PER_TASK = 5 - -client = OpenAI(api_key=API_KEY, base_url=API_BASE) - -# ── Logging helpers ────────────────────────────────────────────────────────── - -def _log(tag: str, payload: Dict[str, Any]) -> None: - print(f"[{tag}] {json.dumps(payload, default=str)}", flush=True) - - -def _err(msg: str) -> None: - print(msg, file=sys.stderr, flush=True) - - -# ── Environment HTTP helpers ───────────────────────────────────────────────── - -def env_reset(task_id: str) -> Dict[str, Any]: - resp = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id}, timeout=30) - resp.raise_for_status() - return resp.json() - - -def env_step(action: Dict[str, Any]) -> Dict[str, Any]: - resp = requests.post(f"{ENV_URL}/step", json={"action": action}, timeout=30) - resp.raise_for_status() - return resp.json() - - -# ── Observation → prompt ───────────────────────────────────────────────────── - -SYSTEM_PROMPT = """\ -You are a clinical pharmacist AI assistant reviewing an elderly patient's medication regimen. -You must reduce drug-interaction risk and address Beers-criteria violations while minimising -unnecessary medication changes. - -Available actions (respond with STRICT JSON, no extra text): -1. Query a drug pair for interactions: - {"action_type": "query_ddi", "drug_id_1": "...", "drug_id_2": "..."} - -2. Propose an intervention: - {"action_type": "propose_intervention", "target_drug_id": "...", - "intervention_type": "stop|dose_reduce|substitute|add_monitoring", - "proposed_new_drug_id": "...(optional)", "rationale": "..."} - -3. Finish the review: - {"action_type": "finish_review"} - -Respond with EXACTLY ONE JSON object per turn. No markdown, no explanation outside JSON. -""" - - -def _summarise_obs(obs: Dict[str, Any]) -> str: - meds = obs.get("current_medications", []) - med_summary = "; ".join( - f"{m['drug_id']}({m['generic_name']},{m['dose_mg']}mg)" - for m in meds - ) - queries = obs.get("interaction_queries", []) - q_summary = "; ".join( - f"{q['drug_id_1']}+{q['drug_id_2']}={q.get('severity','?')}" - for q in queries - ) - interventions = obs.get("interventions", []) - iv_summary = "; ".join( - f"{iv['action_type']}({iv['target_drug_id']})" - for iv in interventions - ) - return ( - f"Patient: age={obs.get('age')}, sex={obs.get('sex')}, " - f"conditions={obs.get('conditions')}, " - f"eGFR={obs.get('eGFR_category')}, liver={obs.get('liver_function_category')}\n" - f"Medications: {med_summary}\n" - f"Queries so far: {q_summary or 'none'}\n" - f"Interventions so far: {iv_summary or 'none'}\n" - f"Remaining query budget: {obs.get('remaining_query_budget')}\n" - f"Remaining intervention budget: {obs.get('remaining_intervention_budget')}\n" - f"Step: {obs.get('step_index')}" - ) - - -# ── LLM call ───────────────────────────────────────────────────────────────── - -def _ask_llm(obs_summary: str) -> Dict[str, Any]: - """Call the LLM and parse a PolypharmacyAction JSON.""" - try: - chat_resp = client.chat.completions.create( - model=MODEL, - messages=[ - {"role": "system", "content": SYSTEM_PROMPT}, - {"role": "user", "content": obs_summary}, - ], - max_tokens=256, - temperature=0.2, - ) - text = (chat_resp.choices[0].message.content or "").strip() - # Strip markdown fences if present - text = text.strip() - if text.startswith("```"): - text = text.split("\n", 1)[-1] - if text.endswith("```"): - text = text.rsplit("```", 1)[0] - text = text.strip() - return json.loads(text) - except Exception as e: - _err(f"LLM parse error: {e}") - return {"action_type": "finish_review"} - - -# ── Main loop ──────────────────────────────────────────────────────────────── - -def main() -> None: - if not API_KEY: - _err("GROQ_API_KEY is required") - sys.exit(1) - - run_id = str(uuid.uuid4())[:8] - - for task_id in TASKS: - task_scores: List[float] = [] - task_rewards: List[float] = [] - - _log("START", { - "run_id": run_id, - "task_id": task_id, - "model": MODEL, - "episodes": EPISODES_PER_TASK, - }) - - for ep_idx in range(EPISODES_PER_TASK): - reset_resp = env_reset(task_id) - obs = reset_resp["observation"] - done = reset_resp.get("done", False) - episode_id = obs.get("episode_id", f"ep_{ep_idx}") - total_reward = 0.0 - step_idx = 0 - - while not done: - obs_summary = _summarise_obs(obs) - action_payload = _ask_llm(obs_summary) - - step_resp = env_step(action_payload) - obs = step_resp["observation"] - reward = step_resp.get("reward", 0.0) - done = step_resp.get("done", False) - total_reward += reward - - _log("STEP", { - "run_id": run_id, - "task_id": task_id, - "episode_id": episode_id, - "step_index": step_idx, - "observation_summary": obs_summary[:200], - "action_payload": action_payload, - "reward": reward, - "done": done, - }) - - step_idx += 1 - - grader_score = step_resp.get("info", {}).get("grader_score", 0.0) - task_scores.append(grader_score) - task_rewards.append(total_reward) - - _log("END", { - "run_id": run_id, - "task_id": task_id, - "episodes": EPISODES_PER_TASK, - "avg_grader_score": sum(task_scores) / max(len(task_scores), 1), - "avg_total_reward": sum(task_rewards) / max(len(task_rewards), 1), - "per_episode_scores": task_scores, - }) - - _err("Inference complete.") - - -if __name__ == "__main__": - main() diff --git a/openenv-polypharmacy/openenv.yaml b/openenv.yaml similarity index 100% rename from openenv-polypharmacy/openenv.yaml rename to openenv.yaml diff --git a/openenv-polypharmacy/pyproject.toml b/pyproject.toml similarity index 94% rename from openenv-polypharmacy/pyproject.toml rename to pyproject.toml index 9bd219ea59455a8765851931c923b726ea32d1d9..43d98f7f65f20870aa09ee8ec9deb42e2888bd92 100644 --- a/openenv-polypharmacy/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,9 @@ dev = [ "isort", ] +[project.scripts] +server = "server.app:main" + [tool.setuptools.packages.find] where = ["backend/src"] diff --git a/openenv-polypharmacy/requirements.txt b/requirements.txt similarity index 100% rename from openenv-polypharmacy/requirements.txt rename to requirements.txt diff --git a/openenv-polypharmacy/scripts/dev_backend.sh b/scripts/dev_backend.sh similarity index 100% rename from openenv-polypharmacy/scripts/dev_backend.sh rename to scripts/dev_backend.sh diff --git a/openenv-polypharmacy/scripts/dev_frontend.sh b/scripts/dev_frontend.sh similarity index 100% rename from openenv-polypharmacy/scripts/dev_frontend.sh rename to scripts/dev_frontend.sh diff --git a/openenv-polypharmacy/scripts/preprocess_data.py b/scripts/preprocess_data.py similarity index 100% rename from openenv-polypharmacy/scripts/preprocess_data.py rename to scripts/preprocess_data.py diff --git a/openenv-polypharmacy/scripts/run_validation.sh b/scripts/run_validation.sh similarity index 100% rename from openenv-polypharmacy/scripts/run_validation.sh rename to scripts/run_validation.sh diff --git a/server/app.py b/server/app.py new file mode 100644 index 0000000000000000000000000000000000000000..ff76c2f132d0b3e4614b4ba0d037edc9f19b7b1d --- /dev/null +++ b/server/app.py @@ -0,0 +1,13 @@ +"""Validator compatibility entrypoint.""" + +from backend.main import app + + +def main(): + """Return ASGI app for validator multi-mode checks.""" + return app + + +if __name__ == "__main__": + main() + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..82447185ad37365d8022f14b2c624b90fa70b688 --- /dev/null +++ b/uv.lock @@ -0,0 +1 @@ +# Generated for OpenEnv validator compatibility.