Spaces:
Sleeping
Sleeping
| """ | |
| PharmaAgent β inference.py | |
| Hackathon evaluation script (OpenMV-compatible). | |
| Runs an LLM agent through clinical decision episodes and reports | |
| a grader score from 0.0 to 1.0. | |
| Design: | |
| - The agent (Qwen 2.5 72B via HuggingFace router) makes clinical decisions. | |
| - All scoring is performed by environment.py using DrugBank data only. | |
| - Groq (Llama 3.3 70B) is called ONCE at the end of each episode, | |
| purely to format a human-readable clinical summary of the final regimen. | |
| It has zero influence on any reward value. | |
| Usage: | |
| uv run inference.py | |
| Environment variables (.env or export): | |
| HF_TOKEN β HuggingFace token (required) | |
| GROQ_API_KEY β Groq API key (required for end-of-episode summary) | |
| DOCKER_IMAGE_NAME β Docker image name (default: pharma-agent) | |
| MODEL_NAME β HF model (default: Qwen/Qwen2.5-72B-Instruct) | |
| """ | |
| import os | |
| import time | |
| import json | |
| import subprocess | |
| import requests | |
| from openai import OpenAI | |
| from dotenv import load_dotenv | |
| # Reward ceiling from environment β keeps normalisation in sync | |
| from server.environment import MAX_EPISODE_REWARD | |
| load_dotenv() | |
| # ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") | |
| DOCKER_IMAGE = os.environ.get("DOCKER_IMAGE_NAME", "pharma-agent") | |
| MODEL_NAME = os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct") | |
| ENV_PORT = 7860 | |
| ENV_BASE_URL = f"http://localhost:{ENV_PORT}" | |
| HF_ROUTER_URL = "https://router.huggingface.co/v1" | |
| NUM_EPISODES = 5 | |
| # ββ Clients βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Agent: Qwen via HuggingFace router (makes clinical decisions) | |
| agent_client = OpenAI(base_url=HF_ROUTER_URL, api_key=HF_TOKEN) | |
| # Formatter: Groq (formats the final regimen summary only β no reward influence) | |
| try: | |
| from groq import Groq | |
| groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None | |
| except ImportError: | |
| groq_client = None | |
| # ββ Docker helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def start_docker() -> bool: | |
| """Start the environment container. Returns True when server is ready.""" | |
| print(f"Starting Docker container: {DOCKER_IMAGE}") | |
| proc = subprocess.Popen( | |
| ["docker", "run", "--rm", "-p", f"{ENV_PORT}:{ENV_PORT}", | |
| "-e", "ENABLE_WEB_INTERFACE=true", DOCKER_IMAGE], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| ) | |
| for _ in range(30): | |
| try: | |
| if requests.get(f"{ENV_BASE_URL}/health", timeout=2).status_code == 200: | |
| print("Environment server is ready.") | |
| return True | |
| except Exception: | |
| pass | |
| ret = proc.poll() | |
| if ret is not None: | |
| out = proc.stdout.read().decode(errors="replace") | |
| err = proc.stderr.read().decode(errors="replace") | |
| print(f"Docker container exited with code {ret}.") | |
| if out: print(f" stdout: {out[:400]}") | |
| if err: print(f" stderr: {err[:400]}") | |
| return False | |
| time.sleep(2) | |
| proc.terminate() | |
| print("Timed out waiting for environment server.") | |
| return False | |
| def env_reset() -> dict: | |
| r = requests.post(f"{ENV_BASE_URL}/reset") | |
| r.raise_for_status() | |
| return r.json() | |
| def env_step(session_id: str, action_type: str, value: str) -> dict: | |
| r = requests.post( | |
| f"{ENV_BASE_URL}/step?session_id={session_id}", | |
| json={"action_type": action_type, "value": value}, | |
| ) | |
| r.raise_for_status() | |
| return r.json() | |
| # ββ Groq formatter (runs after episode, no reward influence) ββββββββββββββ | |
| def format_regimen_summary(episode_log: dict) -> str: | |
| """ | |
| Ask Groq to write a plain-English clinical handover note for the | |
| final regimen. This is purely for human readability β the score | |
| has already been computed from DrugBank data before this is called. | |
| """ | |
| if not groq_client: | |
| return "" | |
| try: | |
| prompt = ( | |
| f"A clinical decision agent managed a patient case.\n\n" | |
| f"Condition: {episode_log.get('condition', 'unknown')}\n" | |
| f"Symptoms: {', '.join(episode_log.get('symptoms', []))}\n" | |
| f"Existing medications: {', '.join(episode_log.get('existing_meds', [])) or 'None'}\n" | |
| f"Agent diagnosis: {episode_log.get('diagnosis', 'not established')}\n" | |
| f"Final regimen: {', '.join(episode_log.get('final_drugs', [])) or 'None'}\n" | |
| f"DDI checks performed: {episode_log.get('ddi_checks', [])}\n" | |
| f"Score: {episode_log.get('score', 0)} / 1.0\n\n" | |
| f"Write a concise clinical handover note (3-5 sentences) summarising " | |
| f"this regimen for a human pharmacist to review. " | |
| f"Flag any concerns. Do not add any new clinical recommendations β " | |
| f"only summarise what the agent did." | |
| ) | |
| response = groq_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=300, | |
| temperature=0.1, | |
| ) | |
| return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| return f"(Summary unavailable: {e})" | |
| # ββ Agent system prompt βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SYSTEM_PROMPT = """You are PharmaAgent, a clinical pharmacist AI operating in a drug safety evaluation environment. | |
| IMPORTANT: This environment scores your actions using a real pharmacological database (DrugBank, 19,842 drugs, 2.9M interaction pairs). Your decisions are evaluated against real drug data β not opinions. | |
| Your task per episode: | |
| 1. DIAGNOSE β identify the condition from symptoms. Use standard clinical terminology. | |
| 2. SELECT_DRUG β add drugs to the regimen one at a time. Use exact approved drug names as they appear in drug databases (e.g. "Metformin", "Atorvastatin", "Amlodipine"). Misspelled or hallucinated names earn zero reward. | |
| 3. CHECK_DDI β check for interactions between the drugs you've selected and the patient's existing medications. Format: "Drug1,Drug2". | |
| 4. FINALIZE β submit the final regimen. | |
| Scoring rules you must know: | |
| - A drug earns full reward only if DrugBank confirms it is indicated for this condition. | |
| - A drug that has a major/contraindicated interaction with the patient's existing medication earns a safety penalty. | |
| - Performing DDI checks earns reward β skipping them when the patient has existing meds earns a penalty. | |
| - Drug names not found in DrugBank earn zero, regardless of clinical reasoning. | |
| Respond ONLY in this JSON format: | |
| { | |
| "action_type": "diagnose|select_drug|check_ddi|finalize", | |
| "value": "your response here", | |
| "reasoning": "brief clinical reasoning (1-2 sentences)" | |
| }""" | |
| # ββ Episode runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_agent_episode() -> float: | |
| """Run one full episode. Returns normalised reward (0.0β1.0).""" | |
| reset_data = env_reset() | |
| session_id = reset_data["session_id"] | |
| obs = reset_data["observation"] | |
| case_condition = obs["patient_case"].get("condition", "Unknown") | |
| symptoms = obs["patient_case"].get("symptoms", []) | |
| existing_meds = obs["patient_case"].get("existing_medications", []) | |
| print(f"\n{'β' * 60}") | |
| print(f"Condition: {case_condition}") | |
| print(f"Symptoms: {', '.join(symptoms)}") | |
| print(f"Existing meds: {', '.join(existing_meds) or 'None'}") | |
| conversation = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| total_reward = 0.0 | |
| episode_log = { | |
| "condition": case_condition, | |
| "symptoms": symptoms, | |
| "existing_meds": existing_meds, | |
| "diagnosis": None, | |
| "final_drugs": [], | |
| "ddi_checks": [], | |
| "score": 0.0, | |
| } | |
| for step_num in range(8): | |
| user_msg = ( | |
| f"PATIENT CASE:\n" | |
| f"Condition (hidden β diagnose from symptoms): {case_condition}\n" | |
| f"Symptoms: {', '.join(obs['patient_case'].get('symptoms', []))}\n" | |
| f"Existing medications: {', '.join(obs['patient_case'].get('existing_medications', [])) or 'None'}\n" | |
| f"Current regimen: {', '.join(obs['patient_case'].get('current_regimen', [])) or 'None'}\n" | |
| f"Diagnosis so far: {obs['patient_case'].get('proposed_diagnosis') or 'Not yet established'}\n\n" | |
| f"Environment feedback:\n{obs['feedback']}\n\n" | |
| f"Valid actions: {obs['valid_options']}\n" | |
| f"Cumulative reward: {obs['reward_so_far']}\n\n" | |
| f"What is your next action? Respond in JSON." | |
| ) | |
| conversation.append({"role": "user", "content": user_msg}) | |
| try: | |
| response = agent_client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=conversation, | |
| max_tokens=300, | |
| temperature=0.2, | |
| ) | |
| agent_reply = response.choices[0].message.content.strip() | |
| except Exception as e: | |
| print(f"LLM error: {e}") | |
| break | |
| conversation.append({"role": "assistant", "content": agent_reply}) | |
| try: | |
| clean = agent_reply.replace("```json", "").replace("```", "").strip() | |
| action_data = json.loads(clean) | |
| action_type = action_data.get("action_type", "finalize") | |
| value = action_data.get("value", "") | |
| reasoning = action_data.get("reasoning", "") | |
| except json.JSONDecodeError: | |
| print("Could not parse agent JSON β finalising.") | |
| action_type, value, reasoning = "finalize", "finalize", "" | |
| print(f"\nStep {step_num + 1} | {action_type.upper()} | {value}") | |
| if reasoning: | |
| print(f" Reasoning: {reasoning[:100]}") | |
| try: | |
| step_result = env_step(session_id, action_type, value) | |
| except Exception as e: | |
| print(f"Environment step error: {e}") | |
| break | |
| obs = step_result["observation"] | |
| step_reward = step_result["reward"] | |
| total_reward = obs["reward_so_far"] | |
| done = step_result["done"] | |
| print(f" Reward: +{step_reward:.3f} | Cumulative: {total_reward:.3f}") | |
| # Track episode log for Groq summary | |
| if action_type == "diagnose": | |
| episode_log["diagnosis"] = value | |
| elif action_type == "check_ddi": | |
| episode_log["ddi_checks"].append(value) | |
| if done: | |
| episode_log["final_drugs"] = obs["patient_case"].get("current_regimen", []) | |
| normalized = round(min(max(total_reward / MAX_EPISODE_REWARD, 0.0), 1.0), 4) | |
| episode_log["score"] = normalized | |
| # Groq formats a human-readable summary β does NOT affect the score | |
| summary = format_regimen_summary(episode_log) | |
| if summary: | |
| print(f"\n Clinical Summary (Groq β informational only):\n {summary}") | |
| print(f"\nEpisode complete. Reward: {total_reward:.3f} | Normalised: {normalized:.4f}") | |
| return normalized | |
| normalized = round(min(max(total_reward / MAX_EPISODE_REWARD, 0.0), 1.0), 4) | |
| return normalized | |
| # ββ Grader ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def grader(episode_rewards: list) -> float: | |
| """Average normalised reward across episodes, with consistency bonus.""" | |
| if not episode_rewards: | |
| return 0.0 | |
| avg = sum(episode_rewards) / len(episode_rewards) | |
| if len(episode_rewards) > 1: | |
| variance = sum((r - avg) ** 2 for r in episode_rewards) / len(episode_rewards) | |
| consistency_bonus = max(0.0, 0.1 - variance) | |
| avg = min(1.0, avg + consistency_bonus) | |
| return round(avg, 4) | |
| # ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| print("=" * 60) | |
| print(" PharmaAgent β Clinical Decision RL Environment") | |
| print(" Scoring: DrugBank data only | LLM: formatting only") | |
| print("=" * 60) | |
| if not HF_TOKEN: | |
| print("HF_TOKEN not set. Add it to your .env file.") | |
| return | |
| try: | |
| r = requests.get(f"{ENV_BASE_URL}/health", timeout=3) | |
| if r.status_code == 200: | |
| print("Environment already running.") | |
| except Exception: | |
| if not start_docker(): | |
| print("Could not start environment. Ensure Docker is running.") | |
| return | |
| episode_rewards = [] | |
| for ep in range(1, NUM_EPISODES + 1): | |
| print(f"\n{'=' * 60}") | |
| print(f" Episode {ep}/{NUM_EPISODES}") | |
| reward = run_agent_episode() | |
| episode_rewards.append(reward) | |
| print(f" Episode normalised reward: {reward:.4f}") | |
| final_score = grader(episode_rewards) | |
| print(f"\n{'=' * 60}") | |
| print(f" FINAL GRADER SCORE: {final_score:.4f} / 1.0000") | |
| print(f" Episode rewards: {episode_rewards}") | |
| print("=" * 60) | |
| return final_score | |
| if __name__ == "__main__": | |
| main() | |