Spaces:
Sleeping
Sleeping
frabbani commited on
Commit ·
08c1d46
1
Parent(s): 7c9f1d6
Fix fact extraction - pass raw data for simple tools.................,nk,
Browse files- Dockerfile +1 -1
- agent.py +390 -0
- server.py +2 -56
- static/index.html +77 -355
Dockerfile
CHANGED
|
@@ -14,8 +14,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 14 |
|
| 15 |
# Copy application code
|
| 16 |
COPY server.py .
|
|
|
|
| 17 |
COPY agent_v2.py .
|
| 18 |
-
COPY agent_v3.py .
|
| 19 |
COPY report_generator.py .
|
| 20 |
COPY tools.py .
|
| 21 |
COPY init_db_hybrid.py .
|
|
|
|
| 14 |
|
| 15 |
# Copy application code
|
| 16 |
COPY server.py .
|
| 17 |
+
COPY agent.py .
|
| 18 |
COPY agent_v2.py .
|
|
|
|
| 19 |
COPY report_generator.py .
|
| 20 |
COPY tools.py .
|
| 21 |
COPY init_db_hybrid.py .
|
agent.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
MedGemma Agent with Tool Calling
|
| 4 |
+
|
| 5 |
+
Simple agent loop that:
|
| 6 |
+
1. Receives a question
|
| 7 |
+
2. Decides which tools to call
|
| 8 |
+
3. Executes tools and gathers results
|
| 9 |
+
4. Synthesizes a final answer
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import json
|
| 14 |
+
import re
|
| 15 |
+
from typing import AsyncGenerator, Optional, Dict
|
| 16 |
+
import httpx
|
| 17 |
+
|
| 18 |
+
from tools import get_tools_description, execute_tool
|
| 19 |
+
|
| 20 |
+
LLAMA_SERVER_URL = os.getenv("LLAMA_SERVER_URL", "http://localhost:8081")
|
| 21 |
+
MAX_STEPS = 5 # Max tool calls per question
|
| 22 |
+
|
| 23 |
+
# Headers for LLM requests (ngrok requires this to skip browser warning)
|
| 24 |
+
LLM_HEADERS = {
|
| 25 |
+
"Content-Type": "application/json",
|
| 26 |
+
"ngrok-skip-browser-warning": "true"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def build_system_prompt(patient_id: str) -> str:
|
| 31 |
+
"""Build the system prompt with tool descriptions."""
|
| 32 |
+
tools_desc = get_tools_description()
|
| 33 |
+
|
| 34 |
+
return f"""You are MedGemma, a helpful medical AI assistant with access to a patient's health records.
|
| 35 |
+
|
| 36 |
+
Patient ID: {patient_id}
|
| 37 |
+
|
| 38 |
+
{tools_desc}
|
| 39 |
+
|
| 40 |
+
HOW TO USE TOOLS:
|
| 41 |
+
When you need information, respond with a tool call in this format:
|
| 42 |
+
TOOL_CALL: {{"tool": "tool_name", "args": {{"param1": "value1"}}}}
|
| 43 |
+
|
| 44 |
+
WHEN TO USE TOOLS vs ANSWER DIRECTLY:
|
| 45 |
+
- USE TOOLS when user asks about THEIR specific data: "show MY blood pressure", "what are MY medications"
|
| 46 |
+
- ANSWER DIRECTLY for general health questions: "is walking good?", "what is diabetes?", "how does aspirin work?"
|
| 47 |
+
- You can combine both: get patient data THEN provide personalized advice
|
| 48 |
+
|
| 49 |
+
CHART TOOL GUIDELINES:
|
| 50 |
+
- Use get_vital_chart_data for VITALS: blood pressure, heart rate, weight, temperature, oxygen
|
| 51 |
+
- Use get_lab_chart_data for LABS: cholesterol, A1c, glucose, kidney function
|
| 52 |
+
- Use these chart tools when user asks to "show", "display", "graph", "trend", or "visualize"
|
| 53 |
+
|
| 54 |
+
EXAMPLES:
|
| 55 |
+
- "Show my blood pressure" → get_vital_chart_data with vital_type="blood_pressure"
|
| 56 |
+
- "Show my cholesterol" → get_lab_chart_data with lab_type="cholesterol"
|
| 57 |
+
- "How is my A1c trending?" → get_lab_chart_data with lab_type="a1c"
|
| 58 |
+
- "Is walking good for health?" → ANSWER directly (general knowledge)
|
| 59 |
+
- "Is walking good for MY heart given my conditions?" → get_conditions, then synthesize answer
|
| 60 |
+
|
| 61 |
+
GENERAL GUIDELINES:
|
| 62 |
+
1. Use get_recent_vitals or get_lab_results for TEXT summaries only
|
| 63 |
+
2. Use chart tools for any visual/trend/graph request
|
| 64 |
+
3. Be specific - include numbers, dates, and medication names
|
| 65 |
+
4. For general health questions, you can answer from medical knowledge
|
| 66 |
+
5. Remind users to consult their healthcare provider for medical decisions
|
| 67 |
+
|
| 68 |
+
When ready to give your final answer, start with "ANSWER:" followed by your response."""
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def build_prompt(system: str, question: str, history: list) -> str:
|
| 72 |
+
"""Build the full prompt."""
|
| 73 |
+
prompt = f"""<start_of_turn>user
|
| 74 |
+
{system}
|
| 75 |
+
|
| 76 |
+
Question: {question}
|
| 77 |
+
<end_of_turn>
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
for entry in history:
|
| 81 |
+
if entry["role"] == "assistant":
|
| 82 |
+
prompt += f"<start_of_turn>model\n{entry['content']}\n<end_of_turn>\n"
|
| 83 |
+
elif entry["role"] == "tool_result":
|
| 84 |
+
prompt += f"<start_of_turn>user\nTool result ({entry['tool']}):\n{entry['content']}\n\nContinue or provide your ANSWER:\n<end_of_turn>\n"
|
| 85 |
+
|
| 86 |
+
prompt += "<start_of_turn>model\n"
|
| 87 |
+
return prompt
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def parse_tool_call(text: str) -> Optional[Dict]:
|
| 91 |
+
"""Extract tool call from response."""
|
| 92 |
+
# Format 1: TOOL_CALL: {...}
|
| 93 |
+
match = re.search(r'TOOL_CALL:\s*(\{.*)', text, re.IGNORECASE | re.DOTALL)
|
| 94 |
+
if match:
|
| 95 |
+
try:
|
| 96 |
+
json_str = match.group(1)
|
| 97 |
+
brace_count = 0
|
| 98 |
+
end_idx = 0
|
| 99 |
+
for i, char in enumerate(json_str):
|
| 100 |
+
if char == '{':
|
| 101 |
+
brace_count += 1
|
| 102 |
+
elif char == '}':
|
| 103 |
+
brace_count -= 1
|
| 104 |
+
if brace_count == 0:
|
| 105 |
+
end_idx = i + 1
|
| 106 |
+
break
|
| 107 |
+
if end_idx > 0:
|
| 108 |
+
return json.loads(json_str[:end_idx])
|
| 109 |
+
except json.JSONDecodeError:
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
# Format 2: ```tool_call\n{...}\n``` or ```tool\n{...}\n```
|
| 113 |
+
match = re.search(r'```(?:tool_call|tool)\s*\n?\s*(\{.*?\})\s*\n?```', text, re.IGNORECASE | re.DOTALL)
|
| 114 |
+
if match:
|
| 115 |
+
try:
|
| 116 |
+
return json.loads(match.group(1))
|
| 117 |
+
except json.JSONDecodeError:
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
# Format 3: ```json\n{"tool":...}\n``` - find last occurrence (in case of thinking)
|
| 121 |
+
matches = re.findall(r'```json\s*\n?\s*(\{[^`]*\})\s*\n?```', text, re.IGNORECASE | re.DOTALL)
|
| 122 |
+
for m in reversed(matches): # Check from last to first
|
| 123 |
+
try:
|
| 124 |
+
parsed = json.loads(m)
|
| 125 |
+
if "tool" in parsed and "args" in parsed:
|
| 126 |
+
return parsed
|
| 127 |
+
except json.JSONDecodeError:
|
| 128 |
+
pass
|
| 129 |
+
|
| 130 |
+
# Format 4: Just find any JSON with "tool" and "args" keys
|
| 131 |
+
# Use a more flexible pattern
|
| 132 |
+
for match in re.finditer(r'\{\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}', text):
|
| 133 |
+
try:
|
| 134 |
+
return json.loads(match.group(0))
|
| 135 |
+
except json.JSONDecodeError:
|
| 136 |
+
pass
|
| 137 |
+
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def extract_answer(text: str) -> str:
|
| 142 |
+
"""Extract final answer from response."""
|
| 143 |
+
# Look for ANSWER: prefix
|
| 144 |
+
for marker in ["ANSWER:", "Answer:", "FINAL ANSWER:", "Final Answer:"]:
|
| 145 |
+
if marker in text:
|
| 146 |
+
idx = text.find(marker)
|
| 147 |
+
return text[idx + len(marker):].strip()
|
| 148 |
+
return text.strip()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def has_answer(text: str) -> bool:
|
| 152 |
+
"""Check if response contains a final answer."""
|
| 153 |
+
markers = ["ANSWER:", "Answer:", "FINAL ANSWER:", "Final Answer:"]
|
| 154 |
+
return any(m in text for m in markers)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def filter_thinking(text: str) -> str:
|
| 158 |
+
"""Remove thinking blocks from text."""
|
| 159 |
+
# Remove <think>...</think> blocks
|
| 160 |
+
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
|
| 161 |
+
|
| 162 |
+
# Remove "thought ..." at the start (MedGemma sometimes outputs this)
|
| 163 |
+
# Keep everything from TOOL_CALL: or ANSWER: onwards
|
| 164 |
+
if text.lower().strip().startswith('thought'):
|
| 165 |
+
# Find where the actual content starts
|
| 166 |
+
tool_match = re.search(r'(TOOL_CALL:.*)', text, re.IGNORECASE | re.DOTALL)
|
| 167 |
+
answer_match = re.search(r'(ANSWER:.*)', text, re.IGNORECASE | re.DOTALL)
|
| 168 |
+
|
| 169 |
+
if tool_match:
|
| 170 |
+
text = tool_match.group(1)
|
| 171 |
+
elif answer_match:
|
| 172 |
+
text = answer_match.group(1)
|
| 173 |
+
|
| 174 |
+
return text.strip()
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
async def call_llm(prompt: str) -> str:
|
| 178 |
+
"""Call LLM and get response (non-streaming)."""
|
| 179 |
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
| 180 |
+
response = await client.post(
|
| 181 |
+
f"{LLAMA_SERVER_URL}/completion",
|
| 182 |
+
headers=LLM_HEADERS,
|
| 183 |
+
json={
|
| 184 |
+
"prompt": prompt,
|
| 185 |
+
"n_predict": 1024,
|
| 186 |
+
"temperature": 0.7,
|
| 187 |
+
"stop": ["<end_of_turn>", "</s>", "<|im_end|>"],
|
| 188 |
+
"stream": False
|
| 189 |
+
}
|
| 190 |
+
)
|
| 191 |
+
response.raise_for_status()
|
| 192 |
+
result = response.json()
|
| 193 |
+
return result.get("content", "").strip()
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
async def stream_llm(prompt: str) -> AsyncGenerator[str, None]:
|
| 197 |
+
"""Stream LLM response token by token."""
|
| 198 |
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
| 199 |
+
async with client.stream(
|
| 200 |
+
"POST",
|
| 201 |
+
f"{LLAMA_SERVER_URL}/completion",
|
| 202 |
+
headers=LLM_HEADERS,
|
| 203 |
+
json={
|
| 204 |
+
"prompt": prompt,
|
| 205 |
+
"n_predict": 1024,
|
| 206 |
+
"temperature": 0.7,
|
| 207 |
+
"stop": ["<end_of_turn>", "</s>", "<|im_end|>"],
|
| 208 |
+
"stream": True
|
| 209 |
+
}
|
| 210 |
+
) as response:
|
| 211 |
+
async for line in response.aiter_lines():
|
| 212 |
+
if line.startswith("data: "):
|
| 213 |
+
data = line[6:]
|
| 214 |
+
if data.strip() == "[DONE]":
|
| 215 |
+
break
|
| 216 |
+
try:
|
| 217 |
+
chunk = json.loads(data)
|
| 218 |
+
content = chunk.get("content", "")
|
| 219 |
+
if content:
|
| 220 |
+
yield content
|
| 221 |
+
except json.JSONDecodeError:
|
| 222 |
+
pass
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
async def run_agent(patient_id: str, question: str) -> AsyncGenerator[dict, None]:
|
| 226 |
+
"""
|
| 227 |
+
Run the agent loop with streaming support.
|
| 228 |
+
|
| 229 |
+
Yields events:
|
| 230 |
+
- {"type": "status", "message": "..."}
|
| 231 |
+
- {"type": "tool_call", "tool": "...", "args": {...}}
|
| 232 |
+
- {"type": "tool_result", "tool": "...", "result": "..."}
|
| 233 |
+
- {"type": "chart_data", "data": {...}}
|
| 234 |
+
- {"type": "answer_start"}
|
| 235 |
+
- {"type": "token", "content": "..."}
|
| 236 |
+
- {"type": "answer_end"}
|
| 237 |
+
- {"type": "error", "message": "..."}
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
system = build_system_prompt(patient_id)
|
| 241 |
+
history = []
|
| 242 |
+
|
| 243 |
+
yield {"type": "status", "message": "Analyzing your question..."}
|
| 244 |
+
|
| 245 |
+
for step in range(MAX_STEPS):
|
| 246 |
+
prompt = build_prompt(system, question, history)
|
| 247 |
+
|
| 248 |
+
# Stream the response and detect tool calls vs answers
|
| 249 |
+
full_response = ""
|
| 250 |
+
is_tool_call = False
|
| 251 |
+
is_streaming_answer = False
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
async for token in stream_llm(prompt):
|
| 255 |
+
full_response += token
|
| 256 |
+
|
| 257 |
+
# Check for tool call patterns anywhere in response
|
| 258 |
+
has_tool_json = ('"tool"' in full_response and '"args"' in full_response)
|
| 259 |
+
has_tool_marker = ("TOOL_CALL:" in full_response or
|
| 260 |
+
"```tool" in full_response.lower() or
|
| 261 |
+
"```json" in full_response.lower() or
|
| 262 |
+
has_tool_json)
|
| 263 |
+
|
| 264 |
+
# If we see tool patterns, keep buffering until JSON is complete
|
| 265 |
+
if has_tool_marker:
|
| 266 |
+
if full_response.count('{') > 0 and full_response.count('{') == full_response.count('}'):
|
| 267 |
+
is_tool_call = True
|
| 268 |
+
break
|
| 269 |
+
continue # Keep buffering
|
| 270 |
+
|
| 271 |
+
# Check for PARTIAL tool markers - keep buffering
|
| 272 |
+
stripped = full_response.strip().upper()
|
| 273 |
+
if stripped.startswith("TOOL") or stripped.startswith("`"):
|
| 274 |
+
continue # Wait for more tokens
|
| 275 |
+
|
| 276 |
+
# Check for thinking patterns - keep buffering
|
| 277 |
+
thinking_patterns = ["thought", "thinking", "let me", "i need to", "i will", "step 1", "1."]
|
| 278 |
+
has_thinking = any(p in full_response.lower()[:200] for p in thinking_patterns)
|
| 279 |
+
|
| 280 |
+
if has_thinking:
|
| 281 |
+
# Model is thinking - keep buffering until we see what it decides
|
| 282 |
+
# But set a limit to avoid infinite buffering
|
| 283 |
+
if len(full_response) < 2000:
|
| 284 |
+
continue
|
| 285 |
+
|
| 286 |
+
# No tool call or thinking patterns - stream as direct answer
|
| 287 |
+
if "ANSWER:" in full_response:
|
| 288 |
+
if not is_streaming_answer:
|
| 289 |
+
is_streaming_answer = True
|
| 290 |
+
yield {"type": "answer_start", "content": ""}
|
| 291 |
+
answer_part = full_response.split("ANSWER:", 1)[1]
|
| 292 |
+
if answer_part.strip():
|
| 293 |
+
yield {"type": "token", "content": answer_part}
|
| 294 |
+
else:
|
| 295 |
+
yield {"type": "token", "content": token}
|
| 296 |
+
else:
|
| 297 |
+
# Direct answer without ANSWER: prefix
|
| 298 |
+
if not is_streaming_answer:
|
| 299 |
+
is_streaming_answer = True
|
| 300 |
+
yield {"type": "answer_start", "content": ""}
|
| 301 |
+
yield {"type": "token", "content": full_response}
|
| 302 |
+
else:
|
| 303 |
+
yield {"type": "token", "content": token}
|
| 304 |
+
|
| 305 |
+
except Exception as e:
|
| 306 |
+
yield {"type": "error", "message": f"LLM error: {str(e)}"}
|
| 307 |
+
return
|
| 308 |
+
|
| 309 |
+
# If we were streaming an answer, we're done
|
| 310 |
+
if is_streaming_answer:
|
| 311 |
+
yield {"type": "answer_end", "content": ""}
|
| 312 |
+
return
|
| 313 |
+
|
| 314 |
+
# Handle tool call
|
| 315 |
+
full_response = filter_thinking(full_response)
|
| 316 |
+
tool_call = parse_tool_call(full_response)
|
| 317 |
+
|
| 318 |
+
if tool_call:
|
| 319 |
+
tool_name = tool_call.get("tool", "")
|
| 320 |
+
tool_args = tool_call.get("args", {})
|
| 321 |
+
|
| 322 |
+
if "patient_id" not in tool_args:
|
| 323 |
+
tool_args["patient_id"] = patient_id
|
| 324 |
+
|
| 325 |
+
yield {"type": "tool_call", "tool": tool_name, "args": tool_args}
|
| 326 |
+
|
| 327 |
+
# Execute tool
|
| 328 |
+
result = execute_tool(tool_name, tool_args)
|
| 329 |
+
|
| 330 |
+
# For chart tools, return immediately
|
| 331 |
+
if tool_name in ["get_vital_chart_data", "get_lab_chart_data", "compare_before_after_treatment"]:
|
| 332 |
+
try:
|
| 333 |
+
parsed = json.loads(result)
|
| 334 |
+
if "chart_type" in parsed and "error" not in parsed:
|
| 335 |
+
yield {"type": "chart_data", "data": parsed}
|
| 336 |
+
chart_title = parsed.get("title", "chart")
|
| 337 |
+
if "summary" in parsed:
|
| 338 |
+
summary_text = "\n".join(parsed["summary"])
|
| 339 |
+
yield {"type": "answer_start", "content": ""}
|
| 340 |
+
yield {"type": "token", "content": f"Here's your {chart_title.lower()}.\n\n**Changes:** {summary_text}\n\nDiscuss these results with your healthcare provider."}
|
| 341 |
+
yield {"type": "answer_end", "content": ""}
|
| 342 |
+
else:
|
| 343 |
+
yield {"type": "answer_start", "content": ""}
|
| 344 |
+
yield {"type": "token", "content": f"Here's your {chart_title.lower()}. If you notice any concerning patterns, please discuss with your healthcare provider."}
|
| 345 |
+
yield {"type": "answer_end", "content": ""}
|
| 346 |
+
return
|
| 347 |
+
except:
|
| 348 |
+
pass
|
| 349 |
+
|
| 350 |
+
# Show tool result
|
| 351 |
+
display_result = result[:500] + "..." if len(result) > 500 else result
|
| 352 |
+
yield {"type": "tool_result", "tool": tool_name, "result": display_result}
|
| 353 |
+
|
| 354 |
+
# Add to history
|
| 355 |
+
history_result = result[:300] + "\n... [truncated]" if len(result) > 300 else result
|
| 356 |
+
history.append({"role": "assistant", "content": full_response})
|
| 357 |
+
history.append({"role": "tool_result", "tool": tool_name, "content": history_result})
|
| 358 |
+
|
| 359 |
+
else:
|
| 360 |
+
# No tool call detected - treat response as answer
|
| 361 |
+
yield {"type": "answer_start", "content": ""}
|
| 362 |
+
yield {"type": "token", "content": filter_thinking(full_response)}
|
| 363 |
+
yield {"type": "answer_end", "content": ""}
|
| 364 |
+
return
|
| 365 |
+
|
| 366 |
+
# Max steps reached - stream final answer
|
| 367 |
+
yield {"type": "status", "message": "Generating final answer..."}
|
| 368 |
+
|
| 369 |
+
prompt = build_prompt(system, question, history)
|
| 370 |
+
prompt += "\nProvide your ANSWER now based on the information gathered:"
|
| 371 |
+
|
| 372 |
+
try:
|
| 373 |
+
yield {"type": "answer_start", "content": ""}
|
| 374 |
+
async for token in stream_llm(prompt):
|
| 375 |
+
# Skip thinking blocks and ANSWER: prefix
|
| 376 |
+
yield {"type": "token", "content": token}
|
| 377 |
+
yield {"type": "answer_end", "content": ""}
|
| 378 |
+
except Exception as e:
|
| 379 |
+
yield {"type": "error", "message": f"Failed to generate answer: {str(e)}"}
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
async def run_agent_simple(patient_id: str, question: str) -> str:
|
| 383 |
+
"""Simple interface - returns just the final answer."""
|
| 384 |
+
answer = ""
|
| 385 |
+
async for event in run_agent(patient_id, question):
|
| 386 |
+
if event["type"] == "answer":
|
| 387 |
+
answer = event["content"]
|
| 388 |
+
elif event["type"] == "error":
|
| 389 |
+
answer = f"Error: {event['message']}"
|
| 390 |
+
return answer
|
server.py
CHANGED
|
@@ -398,66 +398,12 @@ async def health_check():
|
|
| 398 |
# Agent endpoint (v2 with discovery, planning, fact extraction)
|
| 399 |
# ============================================================================
|
| 400 |
from agent_v2 import run_agent_v2
|
| 401 |
-
from agent_v3 import run_agent_v3, chat_with_agent_v3
|
| 402 |
-
|
| 403 |
-
class AgenticChatRequest(BaseModel):
|
| 404 |
-
patient_id: str
|
| 405 |
-
message: str
|
| 406 |
-
include_context: bool = True
|
| 407 |
-
agentic_mode: bool = False # Enable enhanced reasoning trace
|
| 408 |
|
| 409 |
@app.post("/api/agent/chat")
|
| 410 |
-
async def agent_chat_endpoint(request:
|
| 411 |
-
async def generate():
|
| 412 |
-
try:
|
| 413 |
-
if request.agentic_mode:
|
| 414 |
-
# Use enhanced agent v3 with visible reasoning
|
| 415 |
-
async for event in run_agent_v3(request.patient_id, request.message, stream_reasoning=True):
|
| 416 |
-
yield f"data: {json.dumps(event)}\n\n"
|
| 417 |
-
else:
|
| 418 |
-
# Use standard agent v2
|
| 419 |
-
async for event in run_agent_v2(request.patient_id, request.message):
|
| 420 |
-
yield f"data: {json.dumps(event)}\n\n"
|
| 421 |
-
except Exception as e:
|
| 422 |
-
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
| 423 |
-
yield "data: [DONE]\n\n"
|
| 424 |
-
|
| 425 |
-
return StreamingResponse(
|
| 426 |
-
generate(),
|
| 427 |
-
media_type="text/event-stream",
|
| 428 |
-
headers={
|
| 429 |
-
"Cache-Control": "no-cache",
|
| 430 |
-
"Connection": "keep-alive",
|
| 431 |
-
"X-Accel-Buffering": "no"
|
| 432 |
-
}
|
| 433 |
-
)
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
# =============================================================================
|
| 437 |
-
# AGENTIC WORKFLOW SHOWCASE ENDPOINT
|
| 438 |
-
# =============================================================================
|
| 439 |
-
@app.post("/api/agent/comprehensive")
|
| 440 |
-
async def comprehensive_previsit_summary(request: ChatRequest):
|
| 441 |
-
"""
|
| 442 |
-
Generate a comprehensive pre-visit summary using the enhanced agentic workflow.
|
| 443 |
-
|
| 444 |
-
This endpoint showcases:
|
| 445 |
-
1. DISCOVER: Analyze available patient data
|
| 446 |
-
2. PLAN: Create multi-step execution plan
|
| 447 |
-
3. EXECUTE: Call multiple tools with self-correction
|
| 448 |
-
4. REFLECT: Verify completeness
|
| 449 |
-
5. SYNTHESIZE: Generate comprehensive summary
|
| 450 |
-
|
| 451 |
-
The reasoning trace is streamed to show the agent's decision-making process.
|
| 452 |
-
"""
|
| 453 |
-
# Force comprehensive query
|
| 454 |
-
comprehensive_query = f"""Prepare a comprehensive pre-visit summary for my upcoming appointment.
|
| 455 |
-
Include: all my medical conditions, current medications, recent vital signs with trends,
|
| 456 |
-
any allergies, and recent lab results. {request.message}"""
|
| 457 |
-
|
| 458 |
async def generate():
|
| 459 |
try:
|
| 460 |
-
async for event in
|
| 461 |
yield f"data: {json.dumps(event)}\n\n"
|
| 462 |
except Exception as e:
|
| 463 |
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
|
|
|
| 398 |
# Agent endpoint (v2 with discovery, planning, fact extraction)
|
| 399 |
# ============================================================================
|
| 400 |
from agent_v2 import run_agent_v2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
@app.post("/api/agent/chat")
|
| 403 |
+
async def agent_chat_endpoint(request: ChatRequest):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
async def generate():
|
| 405 |
try:
|
| 406 |
+
async for event in run_agent_v2(request.patient_id, request.message):
|
| 407 |
yield f"data: {json.dumps(event)}\n\n"
|
| 408 |
except Exception as e:
|
| 409 |
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
static/index.html
CHANGED
|
@@ -286,151 +286,6 @@
|
|
| 286 |
transition: all 0.2s;
|
| 287 |
}
|
| 288 |
.chip:hover { border-color: var(--primary); color: var(--primary); background: rgba(94, 114, 228, 0.1); }
|
| 289 |
-
|
| 290 |
-
/* Agentic chip - special styling for comprehensive summary */
|
| 291 |
-
.chip.agentic-chip {
|
| 292 |
-
background: linear-gradient(135deg, rgba(94, 114, 228, 0.2), rgba(45, 206, 137, 0.2));
|
| 293 |
-
border-color: var(--primary);
|
| 294 |
-
color: var(--text-main);
|
| 295 |
-
font-weight: 600;
|
| 296 |
-
}
|
| 297 |
-
.chip.agentic-chip:hover {
|
| 298 |
-
background: linear-gradient(135deg, rgba(94, 114, 228, 0.3), rgba(45, 206, 137, 0.3));
|
| 299 |
-
border-color: var(--success);
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
/* Reasoning Trace Panel */
|
| 303 |
-
.reasoning-trace {
|
| 304 |
-
background: linear-gradient(145deg, #1a1f2e, #171c29);
|
| 305 |
-
border: 1px solid var(--border-color);
|
| 306 |
-
border-left: 3px solid var(--primary);
|
| 307 |
-
border-radius: 12px;
|
| 308 |
-
margin: 10px 0;
|
| 309 |
-
padding: 15px;
|
| 310 |
-
font-size: 13px;
|
| 311 |
-
}
|
| 312 |
-
.reasoning-trace-header {
|
| 313 |
-
display: flex;
|
| 314 |
-
align-items: center;
|
| 315 |
-
gap: 8px;
|
| 316 |
-
color: var(--primary);
|
| 317 |
-
font-weight: 600;
|
| 318 |
-
margin-bottom: 12px;
|
| 319 |
-
}
|
| 320 |
-
.reasoning-step {
|
| 321 |
-
display: flex;
|
| 322 |
-
align-items: flex-start;
|
| 323 |
-
gap: 10px;
|
| 324 |
-
padding: 8px 0;
|
| 325 |
-
border-bottom: 1px solid rgba(255,255,255,0.05);
|
| 326 |
-
}
|
| 327 |
-
.reasoning-step:last-child {
|
| 328 |
-
border-bottom: none;
|
| 329 |
-
}
|
| 330 |
-
.reasoning-phase {
|
| 331 |
-
font-size: 11px;
|
| 332 |
-
font-weight: 700;
|
| 333 |
-
text-transform: uppercase;
|
| 334 |
-
padding: 3px 8px;
|
| 335 |
-
border-radius: 4px;
|
| 336 |
-
min-width: 70px;
|
| 337 |
-
text-align: center;
|
| 338 |
-
}
|
| 339 |
-
.reasoning-phase.discover { background: rgba(17, 205, 239, 0.2); color: #11cdef; }
|
| 340 |
-
.reasoning-phase.plan { background: rgba(94, 114, 228, 0.2); color: #5e72e4; }
|
| 341 |
-
.reasoning-phase.execute { background: rgba(251, 99, 64, 0.2); color: #fb6340; }
|
| 342 |
-
.reasoning-phase.reflect { background: rgba(251, 191, 36, 0.2); color: #fbbf24; }
|
| 343 |
-
.reasoning-phase.synthesize { background: rgba(45, 206, 137, 0.2); color: #2dce89; }
|
| 344 |
-
.reasoning-content {
|
| 345 |
-
flex: 1;
|
| 346 |
-
}
|
| 347 |
-
.reasoning-action {
|
| 348 |
-
color: var(--text-main);
|
| 349 |
-
}
|
| 350 |
-
.reasoning-result {
|
| 351 |
-
color: var(--text-muted);
|
| 352 |
-
font-size: 12px;
|
| 353 |
-
margin-top: 4px;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
/* Execution Plan Display */
|
| 357 |
-
.execution-plan {
|
| 358 |
-
background: var(--secondary);
|
| 359 |
-
border-radius: 8px;
|
| 360 |
-
padding: 12px;
|
| 361 |
-
margin: 10px 0;
|
| 362 |
-
}
|
| 363 |
-
.plan-header {
|
| 364 |
-
color: var(--primary);
|
| 365 |
-
font-weight: 600;
|
| 366 |
-
margin-bottom: 10px;
|
| 367 |
-
display: flex;
|
| 368 |
-
align-items: center;
|
| 369 |
-
gap: 6px;
|
| 370 |
-
}
|
| 371 |
-
.plan-step {
|
| 372 |
-
display: flex;
|
| 373 |
-
align-items: center;
|
| 374 |
-
gap: 8px;
|
| 375 |
-
padding: 6px 0;
|
| 376 |
-
font-size: 13px;
|
| 377 |
-
}
|
| 378 |
-
.plan-step-num {
|
| 379 |
-
background: var(--primary);
|
| 380 |
-
color: white;
|
| 381 |
-
width: 20px;
|
| 382 |
-
height: 20px;
|
| 383 |
-
border-radius: 50%;
|
| 384 |
-
display: flex;
|
| 385 |
-
align-items: center;
|
| 386 |
-
justify-content: center;
|
| 387 |
-
font-size: 11px;
|
| 388 |
-
font-weight: 700;
|
| 389 |
-
}
|
| 390 |
-
.plan-step-tool {
|
| 391 |
-
color: var(--info);
|
| 392 |
-
font-family: monospace;
|
| 393 |
-
}
|
| 394 |
-
.plan-step-reason {
|
| 395 |
-
color: var(--text-muted);
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
/* Agentic Progress in Report Panel */
|
| 399 |
-
.agentic-progress {
|
| 400 |
-
text-align: left;
|
| 401 |
-
padding: 10px;
|
| 402 |
-
}
|
| 403 |
-
.agentic-progress .progress-header {
|
| 404 |
-
font-weight: 600;
|
| 405 |
-
color: var(--primary);
|
| 406 |
-
margin-bottom: 12px;
|
| 407 |
-
display: flex;
|
| 408 |
-
align-items: center;
|
| 409 |
-
gap: 8px;
|
| 410 |
-
}
|
| 411 |
-
.reasoning-steps {
|
| 412 |
-
max-height: 200px;
|
| 413 |
-
overflow-y: auto;
|
| 414 |
-
}
|
| 415 |
-
.reasoning-step-mini {
|
| 416 |
-
display: flex;
|
| 417 |
-
align-items: center;
|
| 418 |
-
gap: 8px;
|
| 419 |
-
padding: 6px 0;
|
| 420 |
-
font-size: 12px;
|
| 421 |
-
color: var(--text-main);
|
| 422 |
-
animation: fadeIn 0.3s ease;
|
| 423 |
-
}
|
| 424 |
-
.reasoning-step-mini .step-emoji {
|
| 425 |
-
font-size: 14px;
|
| 426 |
-
}
|
| 427 |
-
.reasoning-step-mini .step-text {
|
| 428 |
-
color: var(--text-muted);
|
| 429 |
-
}
|
| 430 |
-
@keyframes fadeIn {
|
| 431 |
-
from { opacity: 0; transform: translateY(-5px); }
|
| 432 |
-
to { opacity: 1; transform: translateY(0); }
|
| 433 |
-
}
|
| 434 |
|
| 435 |
/* Report Toggle Button - Inline & Compact */
|
| 436 |
.btn-toggle-report {
|
|
@@ -1789,8 +1644,6 @@
|
|
| 1789 |
// ==========================================
|
| 1790 |
// CHAT LOGIC
|
| 1791 |
// ==========================================
|
| 1792 |
-
let currentReasoningCard = null; // Track reasoning card for updates
|
| 1793 |
-
|
| 1794 |
async function sendMessage() {
|
| 1795 |
const message = chatInput.value.trim();
|
| 1796 |
if (!message || !patientId) return;
|
|
@@ -1804,85 +1657,6 @@
|
|
| 1804 |
chatSend.disabled = false;
|
| 1805 |
chatInput.focus();
|
| 1806 |
}
|
| 1807 |
-
|
| 1808 |
-
async function processAgentStream(response, isAgentic = false) {
|
| 1809 |
-
const reader = response.body.getReader();
|
| 1810 |
-
const decoder = new TextDecoder();
|
| 1811 |
-
|
| 1812 |
-
while (true) {
|
| 1813 |
-
const { done, value } = await reader.read();
|
| 1814 |
-
if (done) break;
|
| 1815 |
-
|
| 1816 |
-
const chunk = decoder.decode(value);
|
| 1817 |
-
const lines = chunk.split('\n');
|
| 1818 |
-
|
| 1819 |
-
for (const line of lines) {
|
| 1820 |
-
const trimmedLine = line.trim();
|
| 1821 |
-
if (!trimmedLine || !trimmedLine.startsWith('data: ')) continue;
|
| 1822 |
-
|
| 1823 |
-
const dataStr = trimmedLine.slice(6).trim();
|
| 1824 |
-
if (dataStr === '[DONE]') return;
|
| 1825 |
-
|
| 1826 |
-
try {
|
| 1827 |
-
const event = JSON.parse(dataStr);
|
| 1828 |
-
handleAgentEvent(event, isAgentic);
|
| 1829 |
-
} catch (e) {
|
| 1830 |
-
console.log('Skipping non-JSON chunk:', dataStr);
|
| 1831 |
-
}
|
| 1832 |
-
}
|
| 1833 |
-
}
|
| 1834 |
-
}
|
| 1835 |
-
|
| 1836 |
-
function handleAgentEvent(event, isAgentic) {
|
| 1837 |
-
switch (event.type) {
|
| 1838 |
-
// Standard agent events
|
| 1839 |
-
case 'status':
|
| 1840 |
-
break;
|
| 1841 |
-
case 'discovery':
|
| 1842 |
-
if (event.summary) {
|
| 1843 |
-
addDiscoveryCard(event.summary, event.manifest);
|
| 1844 |
-
}
|
| 1845 |
-
break;
|
| 1846 |
-
case 'plan':
|
| 1847 |
-
if (event.tools && event.tools.length > 0) {
|
| 1848 |
-
addPlanCard(event.tools);
|
| 1849 |
-
}
|
| 1850 |
-
break;
|
| 1851 |
-
case 'tool_call':
|
| 1852 |
-
break;
|
| 1853 |
-
case 'tool_result':
|
| 1854 |
-
if (event.facts && event.facts.trim()) {
|
| 1855 |
-
addFactsCard(event.tool, event.facts, event.raw_preview);
|
| 1856 |
-
} else if (event.result && event.result.trim()) {
|
| 1857 |
-
addToolResult(event.tool, event.result);
|
| 1858 |
-
}
|
| 1859 |
-
break;
|
| 1860 |
-
case 'chart_data':
|
| 1861 |
-
renderChartWidget(event.data);
|
| 1862 |
-
break;
|
| 1863 |
-
case 'chart':
|
| 1864 |
-
// Agent v3 chart event
|
| 1865 |
-
if (event.data) {
|
| 1866 |
-
renderChartWidget(event.data);
|
| 1867 |
-
}
|
| 1868 |
-
break;
|
| 1869 |
-
case 'answer':
|
| 1870 |
-
addAssistantMessage(event.content);
|
| 1871 |
-
break;
|
| 1872 |
-
case 'answer_start':
|
| 1873 |
-
startStreamingAnswer();
|
| 1874 |
-
break;
|
| 1875 |
-
case 'token':
|
| 1876 |
-
appendStreamingToken(event.content);
|
| 1877 |
-
break;
|
| 1878 |
-
case 'answer_end':
|
| 1879 |
-
endStreamingAnswer();
|
| 1880 |
-
break;
|
| 1881 |
-
case 'error':
|
| 1882 |
-
addSystemLog(event.message, 'error');
|
| 1883 |
-
break;
|
| 1884 |
-
}
|
| 1885 |
-
}
|
| 1886 |
|
| 1887 |
async function sendAgentMessage(message) {
|
| 1888 |
try {
|
|
@@ -1892,7 +1666,73 @@
|
|
| 1892 |
body: JSON.stringify({ patient_id: patientId, message })
|
| 1893 |
});
|
| 1894 |
|
| 1895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1896 |
} catch (error) {
|
| 1897 |
addSystemLog('Network Error', 'error');
|
| 1898 |
console.error(error);
|
|
@@ -2069,108 +1909,22 @@
|
|
| 2069 |
if (!patientId) return;
|
| 2070 |
|
| 2071 |
const btn = document.getElementById('btnGenerateReport');
|
| 2072 |
-
const placeholder = document.getElementById('reportPlaceholder');
|
| 2073 |
-
|
| 2074 |
btn.classList.add('loading');
|
| 2075 |
btn.disabled = true;
|
| 2076 |
-
btn.innerHTML = '🤖 Gathering Data...';
|
| 2077 |
-
|
| 2078 |
-
// Show reasoning steps in placeholder area
|
| 2079 |
-
placeholder.innerHTML = `
|
| 2080 |
-
<div class="agentic-progress">
|
| 2081 |
-
<div class="progress-header">🤖 AI Agent Working...</div>
|
| 2082 |
-
<div class="reasoning-steps" id="reportReasoningSteps"></div>
|
| 2083 |
-
</div>
|
| 2084 |
-
`;
|
| 2085 |
-
placeholder.style.display = 'block';
|
| 2086 |
-
document.getElementById('reportPreview').style.display = 'none';
|
| 2087 |
-
|
| 2088 |
-
// Open panel if not already open
|
| 2089 |
-
const panel = document.getElementById('reportPanel');
|
| 2090 |
-
if (!panel.classList.contains('open')) {
|
| 2091 |
-
toggleReportPanel();
|
| 2092 |
-
}
|
| 2093 |
|
| 2094 |
try {
|
| 2095 |
-
|
| 2096 |
-
const agentResponse = await fetch('/api/agent/comprehensive', {
|
| 2097 |
-
method: 'POST',
|
| 2098 |
-
headers: { 'Content-Type': 'application/json' },
|
| 2099 |
-
body: JSON.stringify({ patient_id: patientId, message: '' })
|
| 2100 |
-
});
|
| 2101 |
-
|
| 2102 |
-
const reader = agentResponse.body.getReader();
|
| 2103 |
-
const decoder = new TextDecoder();
|
| 2104 |
-
|
| 2105 |
-
let agentFacts = [];
|
| 2106 |
-
let agentCharts = [];
|
| 2107 |
-
|
| 2108 |
-
// Process the agentic stream
|
| 2109 |
-
while (true) {
|
| 2110 |
-
const { done, value } = await reader.read();
|
| 2111 |
-
if (done) break;
|
| 2112 |
-
|
| 2113 |
-
const chunk = decoder.decode(value);
|
| 2114 |
-
const lines = chunk.split('\n');
|
| 2115 |
-
|
| 2116 |
-
for (const line of lines) {
|
| 2117 |
-
const trimmedLine = line.trim();
|
| 2118 |
-
if (!trimmedLine || !trimmedLine.startsWith('data: ')) continue;
|
| 2119 |
-
|
| 2120 |
-
const dataStr = trimmedLine.slice(6).trim();
|
| 2121 |
-
if (dataStr === '[DONE]') break;
|
| 2122 |
-
|
| 2123 |
-
try {
|
| 2124 |
-
const event = JSON.parse(dataStr);
|
| 2125 |
-
|
| 2126 |
-
// Show reasoning steps
|
| 2127 |
-
if (event.type === 'reasoning') {
|
| 2128 |
-
addReportReasoningStep(event.phase, event.action, event.result);
|
| 2129 |
-
}
|
| 2130 |
-
|
| 2131 |
-
// Show execution plan
|
| 2132 |
-
if (event.type === 'plan' && event.summary) {
|
| 2133 |
-
addReportReasoningStep('plan', `Planning ${event.summary.length} tool calls`, null);
|
| 2134 |
-
}
|
| 2135 |
-
|
| 2136 |
-
// Collect chart data
|
| 2137 |
-
if (event.type === 'chart' && event.data) {
|
| 2138 |
-
agentCharts.push(event.data);
|
| 2139 |
-
}
|
| 2140 |
-
|
| 2141 |
-
// Collect token responses as facts
|
| 2142 |
-
if (event.type === 'token') {
|
| 2143 |
-
// Accumulate for final summary
|
| 2144 |
-
}
|
| 2145 |
-
|
| 2146 |
-
// Track completion
|
| 2147 |
-
if (event.type === 'done') {
|
| 2148 |
-
addReportReasoningStep('done', `✓ Gathered ${event.facts_collected} data points`, null);
|
| 2149 |
-
}
|
| 2150 |
-
} catch (e) {
|
| 2151 |
-
// Skip non-JSON
|
| 2152 |
-
}
|
| 2153 |
-
}
|
| 2154 |
-
}
|
| 2155 |
-
|
| 2156 |
-
// Update button
|
| 2157 |
-
btn.innerHTML = '📋 Creating Report...';
|
| 2158 |
-
addReportReasoningStep('synthesize', 'Generating PDF report...', null);
|
| 2159 |
-
|
| 2160 |
-
// Phase 2: Generate the actual report
|
| 2161 |
-
const reportResponse = await fetch('/api/report/generate', {
|
| 2162 |
method: 'POST',
|
| 2163 |
headers: { 'Content-Type': 'application/json' },
|
| 2164 |
body: JSON.stringify({
|
| 2165 |
patient_id: patientId,
|
| 2166 |
conversation: conversationHistory,
|
| 2167 |
tool_results: collectedToolResults,
|
| 2168 |
-
attachments: collectedAttachments
|
| 2169 |
-
comprehensive: true // Flag that we ran agentic workflow
|
| 2170 |
})
|
| 2171 |
});
|
| 2172 |
|
| 2173 |
-
const data = await
|
| 2174 |
|
| 2175 |
if (data.success) {
|
| 2176 |
currentReport = data;
|
|
@@ -2179,55 +1933,23 @@
|
|
| 2179 |
// Update toggle button
|
| 2180 |
document.getElementById('btnToggleReport').classList.add('has-report');
|
| 2181 |
|
| 2182 |
-
//
|
| 2183 |
-
|
| 2184 |
-
|
| 2185 |
-
|
| 2186 |
-
|
| 2187 |
-
placeholder.style.display = 'none';
|
| 2188 |
-
document.getElementById('reportPreview').style.display = 'block';
|
| 2189 |
-
}, 1000);
|
| 2190 |
} else {
|
| 2191 |
-
|
| 2192 |
}
|
| 2193 |
} catch (error) {
|
| 2194 |
console.error('Report generation error:', error);
|
| 2195 |
-
|
| 2196 |
-
<div class="icon">❌</div>
|
| 2197 |
-
<p>Failed to generate report. Please try again.</p>
|
| 2198 |
-
`;
|
| 2199 |
} finally {
|
| 2200 |
btn.classList.remove('loading');
|
| 2201 |
btn.disabled = false;
|
| 2202 |
-
btn.innerHTML = '📋 Generate Report';
|
| 2203 |
}
|
| 2204 |
}
|
| 2205 |
|
| 2206 |
-
function addReportReasoningStep(phase, action, result) {
|
| 2207 |
-
const container = document.getElementById('reportReasoningSteps');
|
| 2208 |
-
if (!container) return;
|
| 2209 |
-
|
| 2210 |
-
const phaseEmoji = {
|
| 2211 |
-
'discover': '🔍',
|
| 2212 |
-
'plan': '📋',
|
| 2213 |
-
'execute': '⚙️',
|
| 2214 |
-
'reflect': '🤔',
|
| 2215 |
-
'synthesize': '✨',
|
| 2216 |
-
'done': '✅'
|
| 2217 |
-
};
|
| 2218 |
-
|
| 2219 |
-
const step = document.createElement('div');
|
| 2220 |
-
step.className = 'reasoning-step-mini';
|
| 2221 |
-
step.innerHTML = `
|
| 2222 |
-
<span class="step-emoji">${phaseEmoji[phase] || '•'}</span>
|
| 2223 |
-
<span class="step-text">${escapeHtml(action)}</span>
|
| 2224 |
-
`;
|
| 2225 |
-
container.appendChild(step);
|
| 2226 |
-
|
| 2227 |
-
// Auto-scroll
|
| 2228 |
-
container.scrollTop = container.scrollHeight;
|
| 2229 |
-
}
|
| 2230 |
-
|
| 2231 |
function updateReportPreview(report) {
|
| 2232 |
document.getElementById('reportPlaceholder').style.display = 'none';
|
| 2233 |
const preview = document.getElementById('reportPreview');
|
|
|
|
| 286 |
transition: all 0.2s;
|
| 287 |
}
|
| 288 |
.chip:hover { border-color: var(--primary); color: var(--primary); background: rgba(94, 114, 228, 0.1); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
/* Report Toggle Button - Inline & Compact */
|
| 291 |
.btn-toggle-report {
|
|
|
|
| 1644 |
// ==========================================
|
| 1645 |
// CHAT LOGIC
|
| 1646 |
// ==========================================
|
|
|
|
|
|
|
| 1647 |
async function sendMessage() {
|
| 1648 |
const message = chatInput.value.trim();
|
| 1649 |
if (!message || !patientId) return;
|
|
|
|
| 1657 |
chatSend.disabled = false;
|
| 1658 |
chatInput.focus();
|
| 1659 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1660 |
|
| 1661 |
async function sendAgentMessage(message) {
|
| 1662 |
try {
|
|
|
|
| 1666 |
body: JSON.stringify({ patient_id: patientId, message })
|
| 1667 |
});
|
| 1668 |
|
| 1669 |
+
const reader = response.body.getReader();
|
| 1670 |
+
const decoder = new TextDecoder();
|
| 1671 |
+
|
| 1672 |
+
while (true) {
|
| 1673 |
+
const { done, value } = await reader.read();
|
| 1674 |
+
if (done) break;
|
| 1675 |
+
|
| 1676 |
+
const chunk = decoder.decode(value);
|
| 1677 |
+
const lines = chunk.split('\n');
|
| 1678 |
+
|
| 1679 |
+
for (const line of lines) {
|
| 1680 |
+
const trimmedLine = line.trim();
|
| 1681 |
+
if (!trimmedLine || !trimmedLine.startsWith('data: ')) continue;
|
| 1682 |
+
|
| 1683 |
+
const dataStr = trimmedLine.slice(6).trim();
|
| 1684 |
+
if (dataStr === '[DONE]') return;
|
| 1685 |
+
|
| 1686 |
+
try {
|
| 1687 |
+
const event = JSON.parse(dataStr);
|
| 1688 |
+
switch (event.type) {
|
| 1689 |
+
case 'status':
|
| 1690 |
+
// Skip status messages - we have the feedback card now
|
| 1691 |
+
break;
|
| 1692 |
+
case 'discovery':
|
| 1693 |
+
// Agent v2: Show what data is available
|
| 1694 |
+
if (event.summary) {
|
| 1695 |
+
addDiscoveryCard(event.summary, event.manifest);
|
| 1696 |
+
}
|
| 1697 |
+
break;
|
| 1698 |
+
case 'plan':
|
| 1699 |
+
// Agent v2: Show planned tools with reasons
|
| 1700 |
+
if (event.tools && event.tools.length > 0) {
|
| 1701 |
+
addPlanCard(event.tools);
|
| 1702 |
+
}
|
| 1703 |
+
break;
|
| 1704 |
+
case 'tool_call':
|
| 1705 |
+
// Skip tool call logs - handled by feedback card
|
| 1706 |
+
break;
|
| 1707 |
+
case 'tool_result':
|
| 1708 |
+
// Agent v2: Display extracted facts, fallback to raw result
|
| 1709 |
+
if (event.facts && event.facts.trim()) {
|
| 1710 |
+
addFactsCard(event.tool, event.facts, event.raw_preview);
|
| 1711 |
+
} else if (event.result && event.result.trim()) {
|
| 1712 |
+
addToolResult(event.tool, event.result);
|
| 1713 |
+
}
|
| 1714 |
+
break;
|
| 1715 |
+
case 'chart_data':
|
| 1716 |
+
renderChartWidget(event.data); break;
|
| 1717 |
+
case 'answer':
|
| 1718 |
+
addAssistantMessage(event.content); break;
|
| 1719 |
+
case 'answer_start':
|
| 1720 |
+
startStreamingAnswer();
|
| 1721 |
+
break;
|
| 1722 |
+
case 'token':
|
| 1723 |
+
appendStreamingToken(event.content);
|
| 1724 |
+
break;
|
| 1725 |
+
case 'answer_end':
|
| 1726 |
+
endStreamingAnswer();
|
| 1727 |
+
break;
|
| 1728 |
+
case 'error':
|
| 1729 |
+
addSystemLog(event.message, 'error'); break;
|
| 1730 |
+
}
|
| 1731 |
+
} catch (e) {
|
| 1732 |
+
console.log('Skipping non-JSON chunk:', dataStr);
|
| 1733 |
+
}
|
| 1734 |
+
}
|
| 1735 |
+
}
|
| 1736 |
} catch (error) {
|
| 1737 |
addSystemLog('Network Error', 'error');
|
| 1738 |
console.error(error);
|
|
|
|
| 1909 |
if (!patientId) return;
|
| 1910 |
|
| 1911 |
const btn = document.getElementById('btnGenerateReport');
|
|
|
|
|
|
|
| 1912 |
btn.classList.add('loading');
|
| 1913 |
btn.disabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1914 |
|
| 1915 |
try {
|
| 1916 |
+
const response = await fetch('/api/report/generate', {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1917 |
method: 'POST',
|
| 1918 |
headers: { 'Content-Type': 'application/json' },
|
| 1919 |
body: JSON.stringify({
|
| 1920 |
patient_id: patientId,
|
| 1921 |
conversation: conversationHistory,
|
| 1922 |
tool_results: collectedToolResults,
|
| 1923 |
+
attachments: collectedAttachments
|
|
|
|
| 1924 |
})
|
| 1925 |
});
|
| 1926 |
|
| 1927 |
+
const data = await response.json();
|
| 1928 |
|
| 1929 |
if (data.success) {
|
| 1930 |
currentReport = data;
|
|
|
|
| 1933 |
// Update toggle button
|
| 1934 |
document.getElementById('btnToggleReport').classList.add('has-report');
|
| 1935 |
|
| 1936 |
+
// Open panel if not already open
|
| 1937 |
+
const panel = document.getElementById('reportPanel');
|
| 1938 |
+
if (!panel.classList.contains('open')) {
|
| 1939 |
+
toggleReportPanel();
|
| 1940 |
+
}
|
|
|
|
|
|
|
|
|
|
| 1941 |
} else {
|
| 1942 |
+
alert('Failed to generate report: ' + (data.error || 'Unknown error'));
|
| 1943 |
}
|
| 1944 |
} catch (error) {
|
| 1945 |
console.error('Report generation error:', error);
|
| 1946 |
+
alert('Failed to generate report. Please try again.');
|
|
|
|
|
|
|
|
|
|
| 1947 |
} finally {
|
| 1948 |
btn.classList.remove('loading');
|
| 1949 |
btn.disabled = false;
|
|
|
|
| 1950 |
}
|
| 1951 |
}
|
| 1952 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1953 |
function updateReportPreview(report) {
|
| 1954 |
document.getElementById('reportPlaceholder').style.display = 'none';
|
| 1955 |
const preview = document.getElementById('reportPreview');
|