frabbani commited on
Commit
08c1d46
·
1 Parent(s): 7c9f1d6

Fix fact extraction - pass raw data for simple tools.................,nk,

Browse files
Files changed (4) hide show
  1. Dockerfile +1 -1
  2. agent.py +390 -0
  3. server.py +2 -56
  4. 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: AgenticChatRequest):
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 run_agent_v3(request.patient_id, comprehensive_query, stream_reasoning=True):
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
- await processAgentStream(response, false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // Phase 1: Run agentic workflow to gather comprehensive data
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 reportResponse.json();
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
- // Show success
2183
- addReportReasoningStep('done', '✓ Report ready!', null);
2184
-
2185
- // Brief delay then show preview
2186
- setTimeout(() => {
2187
- placeholder.style.display = 'none';
2188
- document.getElementById('reportPreview').style.display = 'block';
2189
- }, 1000);
2190
  } else {
2191
- throw new Error(data.error || 'Unknown error');
2192
  }
2193
  } catch (error) {
2194
  console.error('Report generation error:', error);
2195
- placeholder.innerHTML = `
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');