ashdev commited on
Commit
bf6a463
·
1 Parent(s): 909b9c1

Fix Phase 2 validation: Integrate LiteLLM proxy and implement LLM-powered baseline inference

Browse files
PROJECT_SUMMARY.md CHANGED
@@ -26,7 +26,7 @@ Implemented three specific scenarios with automated graders:
26
  - `/baseline`: Automated trigger for the inference script.
27
 
28
  ### 4. Baseline Inference (`inference.py`)
29
- - Created a reproducible baseline script that uses medical heuristics to evaluate the environment. It ensures the environment is "solveable" and provides a benchmark score for comparison.
30
 
31
  ## 🚀 Technical Improvements & Fixes
32
  To ensure the project meets the highest submission standards, we performed the following:
 
26
  - `/baseline`: Automated trigger for the inference script.
27
 
28
  ### 4. Baseline Inference (`inference.py`)
29
+ - Created a reproducible baseline script that leverages the Hackathon's LiteLLM proxy to evaluate the environment. It ensures the environment is "solveable" using a real LLM and serves as a benchmark for API-based agent interactions.
30
 
31
  ## 🚀 Technical Improvements & Fixes
32
  To ensure the project meets the highest submission standards, we performed the following:
client.py CHANGED
@@ -6,12 +6,5 @@ from openenv.core.mcp_client import MCPToolClient
6
  class MedTriageEnv(MCPToolClient):
7
  """
8
  Client for the MedTriage Environment.
9
-
10
- Example:
11
- >>> with MedTriageEnv(base_url="http://localhost:8000") as env:
12
- ... obs = env.reset(task_id="TASK_HARD")
13
- ... print(obs.symptoms_text)
14
- ... result = env.call_tool("triage_patient", level=3, reasoning="High BP and atypical symptoms in elderly patient.")
15
- ... print(f"Reward: {result.reward}")
16
  """
17
  pass
 
6
  class MedTriageEnv(MCPToolClient):
7
  """
8
  Client for the MedTriage Environment.
 
 
 
 
 
 
 
9
  """
10
  pass
inference.py CHANGED
@@ -7,13 +7,21 @@ import json
7
  import requests
8
  import asyncio
9
  from typing import List, Dict, Any
 
10
  from client import MedTriageEnv
11
 
12
- # --- Mandatory Environment Configuration ---
13
  API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8002/v1")
 
14
  MODEL_NAME = os.getenv("MODEL_NAME", "med-triage-baseline")
15
  HF_TOKEN = os.getenv("HF_TOKEN", "dummy-token")
16
 
 
 
 
 
 
 
17
  # --- Structured Logging Functions ---
18
  def log_start(task: str, env: str, model: str):
19
  """Emit structured [START] log."""
@@ -68,6 +76,67 @@ def wait_for_server(url: str, timeout: int = 30):
68
  time.sleep(1)
69
  return False
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def run_baseline(base_url: str = "http://localhost:8002"):
72
  """Run baseline agent against all 3 tasks and return results."""
73
  # Ensure base_url uses the port from environment if available
@@ -96,32 +165,49 @@ def run_baseline(base_url: str = "http://localhost:8002"):
96
 
97
  try:
98
  with MedTriageEnv(base_url=base_url).sync() as env:
99
- obs = env.reset(task_id=task_id)
 
100
  steps_taken = 1
101
 
102
- # Heuristic Logic (mimes model output)
103
- bp_sys = int(obs.vitals.get("bp", "120/80").split("/")[0])
104
- if bp_sys > 150 or obs.age > 65:
105
- level = 3 # EMERGENCY
106
- elif "severe pain" in obs.symptoms_text.lower():
107
- level = 2 # URGENT_CARE
 
 
 
 
 
 
 
 
 
 
108
  else:
109
- level = 0 # SELF_CARE
110
 
111
- action = {"tool_name": "triage_patient", "arguments": {"level": level, "reasoning": "Heuristic baseline."}}
112
  result = env.step(action)
113
 
114
  reward = result.reward or 0.0
115
  done = result.done
116
  rewards.append(reward)
117
 
118
- log_step(step=1, action=action, reward=reward, done=done)
 
 
 
 
 
119
 
120
  final_score = reward
121
  success = reward >= 1.0
122
  all_scores[task_id] = reward
123
 
124
  except Exception as e:
 
 
125
  log_step(step=steps_taken, action=None, reward=0.0, done=True, error=str(e))
126
  all_scores[task_id] = 0.0
127
 
 
7
  import requests
8
  import asyncio
9
  from typing import List, Dict, Any
10
+ import openai
11
  from client import MedTriageEnv
12
 
13
+ # --- Mandatory Environment Configuration (Injected by Hackathon Validator) ---
14
  API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8002/v1")
15
+ API_KEY = os.getenv("API_KEY", "dummy-key")
16
  MODEL_NAME = os.getenv("MODEL_NAME", "med-triage-baseline")
17
  HF_TOKEN = os.getenv("HF_TOKEN", "dummy-token")
18
 
19
+ # Initialize OpenAI client with the injected LiteLLM proxy credentials
20
+ llm_client = openai.OpenAI(
21
+ base_url=API_BASE_URL,
22
+ api_key=API_KEY
23
+ )
24
+
25
  # --- Structured Logging Functions ---
26
  def log_start(task: str, env: str, model: str):
27
  """Emit structured [START] log."""
 
76
  time.sleep(1)
77
  return False
78
 
79
+ def get_llm_triage(obs: Any) -> Dict[str, Any]:
80
+ """Call the LLM via proxy to get triage level and reasoning."""
81
+ # Heuristic fallback for local development when proxy is not active
82
+ is_local = "localhost" in API_BASE_URL and API_KEY == "dummy-key"
83
+
84
+ # Extract data safely from observation
85
+ # Sometimes openenv-core puts custom fields in .metadata if they don't fit the schema
86
+ metadata = getattr(obs, "metadata", {})
87
+
88
+ age = getattr(obs, "age", metadata.get("age", 0))
89
+ gender = getattr(obs, "gender", metadata.get("gender", "unknown"))
90
+ symptoms = getattr(obs, "symptoms_text", metadata.get("symptoms_text", "No symptoms provided"))
91
+ vitals = getattr(obs, "vitals", metadata.get("vitals", {}))
92
+ history = getattr(obs, "history", metadata.get("history", []))
93
+
94
+ if is_local:
95
+ # Heuristic Logic (mimes model output for local testing)
96
+ bp_sys = int(vitals.get("bp", "120/80").split("/")[0]) if isinstance(vitals.get("bp"), str) else 120
97
+ if bp_sys > 150 or age > 65:
98
+ level = 3 # EMERGENCY
99
+ elif "severe pain" in symptoms.lower():
100
+ level = 2 # URGENT_CARE
101
+ else:
102
+ level = 0 # SELF_CARE
103
+ return {"level": level, "reasoning": "Heuristic fallback (Local testing)."}
104
+
105
+ # Actual LLM call through the LiteLLM proxy
106
+ prompt = f"""
107
+ You are an expert medical triage officer. Based on the patient data below, categorize the patient into the most appropriate care level.
108
+
109
+ PATIENT DATA:
110
+ - Age: {age}
111
+ - Gender: {gender}
112
+ - Symptoms: {symptoms}
113
+ - Vitals: {vitals}
114
+ - Medical History: {history}
115
+
116
+ TRIAGE LEVELS (0-3):
117
+ 0: SELF_CARE (Rest, OTC medication, home monitoring)
118
+ 1: CLINIC (Non-urgent appointment within 48 hours)
119
+ 2: URGENT_CARE (Immediate attention for non-life-threatening conditions)
120
+ 3: EMERGENCY (Life-threatening symptoms, immediate ER visit)
121
+
122
+ Assign a level (0, 1, 2, or 3) and provide a concise medical reasoning.
123
+ Respond ONLY in valid JSON format:
124
+ {{"level": <int>, "reasoning": "<string>"}}
125
+ """
126
+
127
+ try:
128
+ response = llm_client.chat.completions.create(
129
+ model=MODEL_NAME,
130
+ messages=[{"role": "user", "content": prompt}],
131
+ response_format={"type": "json_object"}
132
+ )
133
+ result = json.loads(response.choices[0].message.content)
134
+ return result
135
+ except Exception as e:
136
+ print(f"LLM Error encountered: {e}")
137
+ # Final safety fallback: triage to Emergency if unsure/error
138
+ return {"level": 3, "reasoning": f"Emergency fallback due to triage system error: {str(e)}"}
139
+
140
  def run_baseline(base_url: str = "http://localhost:8002"):
141
  """Run baseline agent against all 3 tasks and return results."""
142
  # Ensure base_url uses the port from environment if available
 
165
 
166
  try:
167
  with MedTriageEnv(base_url=base_url).sync() as env:
168
+ res = env.reset(task_id=task_id)
169
+ obs = res.observation
170
  steps_taken = 1
171
 
172
+ # Import CallToolAction for proper serialization
173
+ try:
174
+ from openenv.core.env_server.mcp_types import CallToolAction
175
+ except ImportError:
176
+ # Fallback if needed, but it should be available
177
+ CallToolAction = None
178
+
179
+ decision = get_llm_triage(obs)
180
+ level = int(decision.get("level", 0))
181
+ reasoning = decision.get("reasoning", "Baseline triage decision.")
182
+
183
+ if CallToolAction:
184
+ action = CallToolAction(
185
+ tool_name="triage_patient",
186
+ arguments={"level": level, "reasoning": reasoning}
187
+ )
188
  else:
189
+ action = {"tool_name": "triage_patient", "arguments": {"level": level, "reasoning": reasoning}}
190
 
 
191
  result = env.step(action)
192
 
193
  reward = result.reward or 0.0
194
  done = result.done
195
  rewards.append(reward)
196
 
197
+ if CallToolAction:
198
+ action_to_log = action.model_dump() if hasattr(action, "model_dump") else action
199
+ else:
200
+ action_to_log = action
201
+
202
+ log_step(step=1, action=action_to_log, reward=reward, done=done)
203
 
204
  final_score = reward
205
  success = reward >= 1.0
206
  all_scores[task_id] = reward
207
 
208
  except Exception as e:
209
+ import traceback
210
+ traceback.print_exc()
211
  log_step(step=steps_taken, action=None, reward=0.0, done=True, error=str(e))
212
  all_scores[task_id] = 0.0
213
 
pyproject.toml CHANGED
@@ -11,6 +11,7 @@ dependencies = [
11
  "pydantic",
12
  "fastmcp",
13
  "requests",
 
14
  "openenv-core>=0.2.0",
15
  ]
16
 
 
11
  "pydantic",
12
  "fastmcp",
13
  "requests",
14
+ "openai",
15
  "openenv-core>=0.2.0",
16
  ]
17
 
requirements.txt CHANGED
@@ -3,4 +3,5 @@ uvicorn
3
  pydantic
4
  fastmcp
5
  requests
 
6
  openenv-core>=0.2.0
 
3
  pydantic
4
  fastmcp
5
  requests
6
+ openai
7
  openenv-core>=0.2.0
server/app.py CHANGED
@@ -9,14 +9,14 @@ from typing import List, Dict, Any
9
 
10
  try:
11
  from server.triage_environment import MedTriageEnvironment, TASKS
12
- from models import TriageAction
13
  except ImportError:
14
  try:
15
  from .triage_environment import MedTriageEnvironment, TASKS
16
- from .models import TriageAction
17
  except ImportError:
18
  from triage_environment import MedTriageEnvironment, TASKS
19
- from models import TriageAction
20
 
21
  # Initialize the environment instance to be used by the app
22
  env_instance = MedTriageEnvironment()
@@ -25,7 +25,7 @@ env_instance = MedTriageEnvironment()
25
  app = create_app(
26
  MedTriageEnvironment,
27
  CallToolAction,
28
- CallToolObservation,
29
  env_name="med_triage_env"
30
  )
31
 
 
9
 
10
  try:
11
  from server.triage_environment import MedTriageEnvironment, TASKS
12
+ from models import TriageAction, TriageObservation
13
  except ImportError:
14
  try:
15
  from .triage_environment import MedTriageEnvironment, TASKS
16
+ from .models import TriageAction, TriageObservation
17
  except ImportError:
18
  from triage_environment import MedTriageEnvironment, TASKS
19
+ from models import TriageAction, TriageObservation
20
 
21
  # Initialize the environment instance to be used by the app
22
  env_instance = MedTriageEnvironment()
 
25
  app = create_app(
26
  MedTriageEnvironment,
27
  CallToolAction,
28
+ TriageObservation,
29
  env_name="med_triage_env"
30
  )
31
 
server/requirements.txt CHANGED
@@ -3,4 +3,5 @@ uvicorn
3
  pydantic
4
  fastmcp
5
  requests
 
6
  openenv-core>=0.2.0
 
3
  pydantic
4
  fastmcp
5
  requests
6
+ openai
7
  openenv-core>=0.2.0
server/triage_environment.py CHANGED
@@ -126,6 +126,9 @@ class MedTriageEnvironment(MCPEnvironment):
126
  """
127
  Process the agent's triage decision and return a score.
128
  """
 
 
 
129
  self._state.step_count += 1
130
 
131
  # If the action is an MCP CallToolAction
@@ -133,12 +136,18 @@ class MedTriageEnvironment(MCPEnvironment):
133
 
134
  if isinstance(action, CallToolAction) and action.tool_name == "triage_patient":
135
  agent_level = action.arguments.get("level")
136
- reward = self._calculate_reward(TriageLevel(agent_level), self._state.ground_truth_level)
137
  self._last_reward = reward
138
 
139
  patient = self._current_task["patient"]
 
140
  return TriageObservation(
141
- **patient,
 
 
 
 
 
142
  done=True,
143
  reward=reward,
144
  message=f"Episode complete. Agent Triage: {agent_level}. Ground Truth: {self._state.ground_truth_level.value}. Score: {reward}"
 
126
  """
127
  Process the agent's triage decision and return a score.
128
  """
129
+ print(f"DEBUG: Received action type: {type(action)}")
130
+ if hasattr(action, "tool_name"):
131
+ print(f"DEBUG: tool_name: {action.tool_name}")
132
  self._state.step_count += 1
133
 
134
  # If the action is an MCP CallToolAction
 
136
 
137
  if isinstance(action, CallToolAction) and action.tool_name == "triage_patient":
138
  agent_level = action.arguments.get("level")
139
+ reward = self._calculate_reward(TriageLevel(int(agent_level)), self._state.ground_truth_level)
140
  self._last_reward = reward
141
 
142
  patient = self._current_task["patient"]
143
+ # Ensure we return the model type expected by the app
144
  return TriageObservation(
145
+ patient_id=patient["patient_id"],
146
+ age=patient["age"],
147
+ gender=patient["gender"],
148
+ symptoms_text=patient["symptoms_text"],
149
+ vitals=patient["vitals"],
150
+ history=patient["history"],
151
  done=True,
152
  reward=reward,
153
  message=f"Episode complete. Agent Triage: {agent_level}. Ground Truth: {self._state.ground_truth_level.value}. Score: {reward}"