From 2d5309a6b38f299e6f0a6fa4f7651b284f6d7789 Mon Sep 17 00:00:00 2001 From: AndrewVFranco <129307231+AndrewVFranco@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:14:45 -0700 Subject: [PATCH 1/3] Update backend API to support live status streaming --- src/api/main.py | 125 +++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index b821df9..96b3161 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -3,7 +3,10 @@ from src.agent.graph import agent from pydantic import BaseModel from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse from typing import Optional +import json +import asyncio class QueryRequest(BaseModel): query: str = "" @@ -21,7 +24,7 @@ async def lifespan(app): CORSMiddleware, allow_origins=[ "http://localhost:3000", - "127.0.0.1:3000" + "127.0.0.1:3000", "https://andrewvfranco-sentinelmd.hf.space", "https://*.hf.space", ], @@ -30,58 +33,84 @@ async def lifespan(app): allow_headers=["*"], ) + @app.post("/query") async def query(request: QueryRequest): - try: - result = agent.invoke({ - "api_key": request.api_key, - "query": request.query, - "search_query": None, - "abstracts": [], - "llm_response": None, - "claims": None, - "scored_claims": None, - "confidence_score": None, - "final_response": None, - "has_drug_query": False, - "drug_names": None, - "drug_labels": None, - "has_fhir": request.has_fhir, - "fhir_resource_type": request.fhir_resource_type, - "fhir_resource_id": request.fhir_resource_id, - "fhir_output": None - }) - - return result - except Exception as e: - print(e) - raise HTTPException(status_code=500, detail=f"Inference failed: {str(e)}") + initial_state = { + "api_key": request.api_key, + "query": request.query, + "search_query": None, + "abstracts": [], + "llm_response": None, + "claims": None, + "scored_claims": None, + "confidence_score": None, + "final_response": None, + "has_drug_query": False, + "drug_names": None, + "drug_labels": None, + "has_fhir": request.has_fhir, + "fhir_resource_type": request.fhir_resource_type, + "fhir_resource_id": request.fhir_resource_id, + "fhir_output": None + } + + async def event_generator(): + try: + async for output in agent.astream(initial_state): + node_name = list(output.keys())[0] + yield f"data: {json.dumps({'status': node_name})}\n\n" + await asyncio.sleep(0.01) + + if node_name == "assembly": + final_state = output["assembly"] + yield f"data: {json.dumps(final_state)}\n\n" + + except Exception as e: + print(f"Streaming error: {e}") + yield f"data: {json.dumps({'error': str(e)})}\n\n" + + return StreamingResponse(event_generator(), media_type="text/event-stream") + @app.post("/fhir") async def fhir(request: QueryRequest): - try: - result = agent.invoke({ - "api_key": request.api_key, - "query": request.query, - "search_query": None, - "abstracts": [], - "llm_response": None, - "claims": None, - "scored_claims": None, - "confidence_score": None, - "final_response": None, - "has_drug_query": False, - "drug_names": None, - "drug_labels": None, - "has_fhir": True, - "fhir_resource_type": request.fhir_resource_type, - "fhir_resource_id": request.fhir_resource_id, - "fhir_output": None - }) - return result - except Exception as e: - print(e) - raise HTTPException(status_code=500, detail=f"FHIR inference failed: {str(e)}") + initial_state = { + "api_key": request.api_key, + "query": request.query, + "search_query": None, + "abstracts": [], + "llm_response": None, + "claims": None, + "scored_claims": None, + "confidence_score": None, + "final_response": None, + "has_drug_query": False, + "drug_names": None, + "drug_labels": None, + "has_fhir": True, # Hardcoded to True for this endpoint + "fhir_resource_type": request.fhir_resource_type, + "fhir_resource_id": request.fhir_resource_id, + "fhir_output": None + } + + async def event_generator(): + try: + async for output in agent.astream(initial_state): + node_name = list(output.keys())[0] + yield f"data: {json.dumps({'status': node_name})}\n\n" + await asyncio.sleep(0.01) + + if node_name == "assembly": + final_state = output["assembly"] + yield f"data: {json.dumps(final_state)}\n\n" + + except Exception as e: + print(f"Streaming error: {e}") + yield f"data: {json.dumps({'error': str(e)})}\n\n" + + return StreamingResponse(event_generator(), media_type="text/event-stream") + @app.get("/health") async def health(): From 0d4dd41e53f836915a2f60178576fc6de3e74ac5 Mon Sep 17 00:00:00 2001 From: AndrewVFranco <129307231+AndrewVFranco@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:15:24 -0700 Subject: [PATCH 2/3] Add SSE stream handling and live status updates --- frontend/src/App.js | 54 ++++++++++++++++++++++----- frontend/src/components/ChatWindow.js | 51 ++++++++++++++++--------- frontend/src/components/Sidebar.js | 12 +++--- 3 files changed, 84 insertions(+), 33 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 4e3bc1d..e3e04b7 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -11,6 +11,7 @@ function App() { const [apiKey, setApiKey] = useState(''); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); + const [loadingPhase, setLoadingPhase] = useState(null); const [result, setResult] = useState(null); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [darkMode, setDarkMode] = useState(() => { @@ -35,16 +36,17 @@ function App() { const handleSubmit = async (fhirData = {}) => { if ((!query.trim() && !fhirData.has_fhir) || loading) return; - // Close the sidebar automatically on mobile when a query is sent closeSidebar(); const userMessage = { role: 'user', text: query || `FHIR ${fhirData.fhir_resource_type}/${fhirData.fhir_resource_id}` }; + setMessages(prev => [...prev, userMessage]); setQuery(''); setLoading(true); + setLoadingPhase(null); setResult(null); try { @@ -55,12 +57,42 @@ function App() { }); if (!response.ok) throw new Error('Query failed'); - const data = await response.json(); - console.log('full response:', data); - console.log('final_response:', data.final_response); - const final = data.final_response; - setResult(final); - setMessages(prev => [...prev, { role: 'assistant', data: final }]); + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + const parts = buffer.split('\n\n'); + + buffer = parts.pop(); + + for (const part of parts) { + const line = part.trim(); + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.substring(6)); + + if (data.status) { + setLoadingPhase(data.status); + } + + if (data.final_response) { + setResult(data.final_response); + setMessages(prev => [...prev, { role: 'assistant', data: data.final_response }]); + } + } catch (parseError) { + console.error("JSON Parse Error on complete chunk:", line, parseError); + } + } + } + } } catch (err) { let errorText = 'Failed to connect to the analysis server. Ensure the backend is running.'; @@ -78,6 +110,7 @@ function App() { }]); } finally { setLoading(false); + setLoadingPhase(null); } }; @@ -99,8 +132,8 @@ function App() {