11import json
2+ import asyncio
23from fastapi import APIRouter , Depends , HTTPException , Query , Request , status
34from typing import List
45from uuid import UUID
@@ -112,24 +113,95 @@ async def stream_claim_analysis_exp(
112113 claim = await claim_service .get_claim (claim_id = claim_id , user_id = current_user .id )
113114
114115 session = claim_service ._claim_repo ._session
116+
117+ await session .rollback ()
115118
116119 async def event_generator ():
117120 try :
118121 logger .info (f"Starting analysis stream for claim { claim_id } " )
119122 yield f"data: { json .dumps ({'type' : 'status' , 'content' : 'Initializing analysis...' })} \n \n "
120123
121- async for event in analysis_orchestrator .analyze_claim_stream (
124+ orchestrator_stream = analysis_orchestrator .analyze_claim_stream (
122125 claim = claim , user_id = current_user .id , default = False
123- ):
124- if isinstance (event , dict ):
125- yield f"data: { json .dumps (event )} \n \n "
126+ )
127+ # ---------------------------------------------------------
128+ # THE HEALTH CHECK LOOP
129+ # ---------------------------------------------------------
130+ next_event_task = None
131+
132+ while True :
133+ # Only create a new task if we don't already have one waiting
134+ if next_event_task is None :
135+ next_event_task = asyncio .create_task (anext (orchestrator_stream ))
136+
137+ # Wait for the task to finish, but only wait 15 seconds
138+ done , pending = await asyncio .wait (
139+ [next_event_task ],
140+ timeout = 15.0 ,
141+ return_when = asyncio .FIRST_COMPLETED
142+ )
143+
144+ if next_event_task in done :
145+ # The LLM yielded a chunk! Let's process it.
146+ try :
147+ event = next_event_task .result ()
148+ if isinstance (event , dict ):
149+ yield f"data: { json .dumps (event )} \n \n "
150+
151+ # Reset the task so we grab the next chunk on the next loop
152+ next_event_task = None
153+
154+ except StopAsyncIteration :
155+ # The stream finished normally!
156+ break
157+ except Exception as e :
158+ # If the orchestrator crashed, catch it here
159+ raise e
160+
161+ else :
162+ # The task is in 'pending'. 15 seconds passed, but the LLM is still thinking.
163+ # We yield a heartbeat, but we DO NOT reset next_event_task.
164+ # It will keep running safely in the background on the next loop!
165+ logger .debug ("Stream idle for 15s. Sending health check ping..." )
166+ yield ": healthcheck\n \n "
167+
168+ yield "data: [DONE]\n \n "
169+
170+ except asyncio .CancelledError :
171+ logger .warning (f"Client disconnected during stream for claim { claim_id } " )
172+ raise
173+
174+ # async for event in analysis_orchestrator.analyze_claim_stream(
175+ # claim=claim, user_id=current_user.id, default=False
176+ # ):
177+ # if isinstance(event, dict):
178+ # yield f"data: {json.dumps(event)}\n\n"
179+
180+ # yield "data: [DONE]\n\n"
181+
182+ # except asyncio.CancelledError:
183+ # # THE FIX: The user closed their browser!
184+ # logger.info(f"Client disconnected during stream for claim {claim_id}")
185+ # await session.rollback() # Explicitly release the lock!
186+ # raise
126187
127188 except Exception as e :
128189 logger .error (f"Error in analysis stream: { str (e )} " , exc_info = True )
129190 yield f"data: { json .dumps ({'type' : 'error' , 'content' : str (e )})} \n \n "
130191 finally :
131- await session .close ()
132- yield "data: [DONE]\n \n "
192+ # async def force_cleanup():
193+ # try:
194+ # await session.rollback()
195+ # except Exception as e:
196+ # logger.error(f"Force rollback failed: {e}")
197+ # finally:
198+ # await session.close()
199+
200+ # # Fire and forget. FastAPI cannot cancel this!
201+ # asyncio.create_task(force_cleanup())
202+ if next_event_task and not next_event_task .done ():
203+ logger .debug ("Cancelling background orchestrator task..." )
204+ next_event_task .cancel ()
133205
134206 return StreamingResponse (
135207 event_generator (),
@@ -138,8 +210,8 @@ async def event_generator():
138210 "Cache-Control" : "no-cache" ,
139211 "Connection" : "keep-alive" ,
140212 "X-Accel-Buffering" : "no" ,
141- "Access-Control-Allow-Origin" : "*" ,
142- "Access-Control-Allow-Credentials" : "true" ,
213+ # "Access-Control-Allow-Origin": "*",
214+ # "Access-Control-Allow-Credentials": "true",
143215 },
144216 )
145217 except Exception as e :
0 commit comments