UNI12345 commited on
Commit
e5870af
·
1 Parent(s): b0a61a2

Fix auto-login, SSE streams, LLM timeouts, and frontend cancellation

Browse files
app/routers/audit.py CHANGED
@@ -223,29 +223,78 @@ async def github_audit(
223
  async def audit_stream(job_id: str):
224
  async def event_generator():
225
  import asyncio
 
 
 
 
226
  pubsub = redis_conn.pubsub()
227
  pubsub.subscribe(f"job:{job_id}")
228
-
229
- # Initial heartbeat
230
- yield f"data: {json.dumps({'event': 'connecting', 'status': 'active'})}\n\n"
231
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  try:
233
  while True:
234
  message = pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
235
  if message:
236
- data = message["data"].decode("utf-8")
237
- yield f"data: {data}\n\n"
238
-
239
- parsed = json.loads(data)
240
- if parsed.get("event") in ["job_complete", "job_failed"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  break
242
- await asyncio.sleep(0.5)
 
 
 
 
 
243
  except asyncio.CancelledError:
244
  logger.info(f"SSE connection closed for job {job_id}")
245
  finally:
246
  pubsub.unsubscribe(f"job:{job_id}")
247
-
248
- return StreamingResponse(event_generator(), media_type="text/event-stream")
 
 
 
 
 
 
 
249
 
250
  @router.get("/history", response_model=List[schemas.ProjectResponse])
251
  async def get_audit_history(
@@ -278,6 +327,35 @@ async def get_audit_report(id: UUID, current_user: models.User = Depends(get_cur
278
 
279
  return report
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  @router.delete("/{id}")
282
  async def delete_audit(id: UUID, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
283
  result = await db.execute(select(models.Project).where(models.Project.id == id, models.Project.user_id == current_user.id))
 
223
  async def audit_stream(job_id: str):
224
  async def event_generator():
225
  import asyncio
226
+ from app.database import AsyncSessionLocal
227
+ from app import models
228
+ from sqlalchemy.future import select
229
+
230
  pubsub = redis_conn.pubsub()
231
  pubsub.subscribe(f"job:{job_id}")
232
+
233
+ # Check DB status first to handle race conditions where job is already done
234
+ try:
235
+ async with AsyncSessionLocal() as session:
236
+ proj_result = await session.execute(
237
+ select(models.Project).where(models.Project.job_id == job_id)
238
+ )
239
+ project = proj_result.scalars().first()
240
+ if project:
241
+ if project.status == "complete":
242
+ yield f"event: job_complete\ndata: {json.dumps({'event': 'job_complete', 'audit_id': str(project.id)})}\n\n"
243
+ pubsub.unsubscribe(f"job:{job_id}")
244
+ return
245
+ elif project.status == "failed":
246
+ yield f"event: job_failed\ndata: {json.dumps({'event': 'job_failed', 'reason': project.error_msg or 'Cancelled by user'})}\n\n"
247
+ pubsub.unsubscribe(f"job:{job_id}")
248
+ return
249
+ except Exception as e:
250
+ logger.error(f"Error checking project status in SSE stream: {e}")
251
+
252
+ # Initial connection confirm — send as named event
253
+ yield f"event: agent_started\ndata: {json.dumps({'event': 'connecting', 'agent': 'connecting', 'status': 'active'})}\n\n"
254
+
255
  try:
256
  while True:
257
  message = pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
258
  if message:
259
+ raw = message["data"].decode("utf-8")
260
+ try:
261
+ parsed = json.loads(raw)
262
+ except Exception:
263
+ await asyncio.sleep(0.3)
264
+ continue
265
+
266
+ event_name = parsed.get("event", "message")
267
+
268
+ # Map backend event names to SSE named events
269
+ if event_name == "agent_started":
270
+ yield f"event: agent_started\ndata: {raw}\n\n"
271
+ elif event_name == "agent_complete":
272
+ yield f"event: agent_complete\ndata: {raw}\n\n"
273
+ elif event_name == "job_complete":
274
+ yield f"event: job_complete\ndata: {raw}\n\n"
275
+ break
276
+ elif event_name == "job_failed":
277
+ yield f"event: job_failed\ndata: {raw}\n\n"
278
  break
279
+ elif event_name == "progress":
280
+ yield f"event: progress\ndata: {raw}\n\n"
281
+ else:
282
+ yield f"data: {raw}\n\n"
283
+
284
+ await asyncio.sleep(0.3)
285
  except asyncio.CancelledError:
286
  logger.info(f"SSE connection closed for job {job_id}")
287
  finally:
288
  pubsub.unsubscribe(f"job:{job_id}")
289
+
290
+ return StreamingResponse(
291
+ event_generator(),
292
+ media_type="text/event-stream",
293
+ headers={
294
+ "Cache-Control": "no-cache",
295
+ "X-Accel-Buffering": "no",
296
+ }
297
+ )
298
 
299
  @router.get("/history", response_model=List[schemas.ProjectResponse])
300
  async def get_audit_history(
 
327
 
328
  return report
329
 
330
+ @router.post("/{id}/cancel")
331
+ async def cancel_audit(id: UUID, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
332
+ """Cancel a queued or in-progress audit job."""
333
+ result = await db.execute(select(models.Project).where(models.Project.id == id, models.Project.user_id == current_user.id))
334
+ project = result.scalars().first()
335
+ if not project:
336
+ raise HTTPException(status_code=404, detail="Project not found")
337
+
338
+ if project.status in ["complete", "failed"]:
339
+ raise HTTPException(status_code=400, detail="Job already finished — cannot cancel")
340
+
341
+ # Cancel the RQ job if it's still queued
342
+ if project.job_id:
343
+ try:
344
+ from rq.job import Job
345
+ job = Job.fetch(project.job_id, connection=redis_conn)
346
+ job.cancel()
347
+ except Exception:
348
+ pass # Job may have already completed
349
+
350
+ # Publish cancellation event so SSE clients disconnect cleanly
351
+ redis_conn.publish(f"job:{project.job_id}", json.dumps({"event": "job_failed", "reason": "cancelled_by_user"}))
352
+
353
+ project.status = "failed"
354
+ project.error_msg = "Cancelled by user"
355
+ db.add(project)
356
+ await db.commit()
357
+ return {"detail": "Job cancelled successfully"}
358
+
359
  @router.delete("/{id}")
360
  async def delete_audit(id: UUID, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
361
  result = await db.execute(select(models.Project).where(models.Project.id == id, models.Project.user_id == current_user.id))
app/routers/design.py CHANGED
@@ -73,9 +73,32 @@ async def generate_design(
73
  async def design_stream(job_id: str):
74
  async def event_generator():
75
  import asyncio
 
 
 
 
76
  pubsub = redis_conn.pubsub()
77
  pubsub.subscribe(f"job:{job_id}")
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  # Initial connection confirm — send as named event
80
  yield f"event: agent_started\ndata: {json.dumps({'event': 'connecting', 'agent': 'connecting', 'status': 'active'})}\n\n"
81
 
 
73
  async def design_stream(job_id: str):
74
  async def event_generator():
75
  import asyncio
76
+ from app.database import AsyncSessionLocal
77
+ from app import models
78
+ from sqlalchemy.future import select
79
+
80
  pubsub = redis_conn.pubsub()
81
  pubsub.subscribe(f"job:{job_id}")
82
 
83
+ # Check DB status first to handle race conditions where job is already done
84
+ try:
85
+ async with AsyncSessionLocal() as session:
86
+ proj_result = await session.execute(
87
+ select(models.Project).where(models.Project.job_id == job_id)
88
+ )
89
+ project = proj_result.scalars().first()
90
+ if project:
91
+ if project.status == "complete":
92
+ yield f"event: job_complete\ndata: {json.dumps({'event': 'job_complete', 'design_id': str(project.id)})}\n\n"
93
+ pubsub.unsubscribe(f"job:{job_id}")
94
+ return
95
+ elif project.status == "failed":
96
+ yield f"event: job_failed\ndata: {json.dumps({'event': 'job_failed', 'reason': project.error_msg or 'Cancelled by user'})}\n\n"
97
+ pubsub.unsubscribe(f"job:{job_id}")
98
+ return
99
+ except Exception as e:
100
+ logger.error(f"Error checking project status in SSE stream: {e}")
101
+
102
  # Initial connection confirm — send as named event
103
  yield f"event: agent_started\ndata: {json.dumps({'event': 'connecting', 'agent': 'connecting', 'status': 'active'})}\n\n"
104
 
app/routers/github.py CHANGED
@@ -55,10 +55,18 @@ async def github_callback(
55
  # Fallback: Redirect user to sign-in page if not authenticated
56
  raise HTTPException(status_code=401, detail="Authentication session missing")
57
 
58
- # Retrieve user via firebase session cookie
59
- from app.utils.firebase import verify_session_cookie
60
  try:
61
- decoded_claims = verify_session_cookie(session_cookie, check_revoked=True)
 
 
 
 
 
 
 
 
62
  except Exception:
63
  raise HTTPException(status_code=401, detail="Invalid session")
64
 
 
55
  # Fallback: Redirect user to sign-in page if not authenticated
56
  raise HTTPException(status_code=401, detail="Authentication session missing")
57
 
58
+ # Retrieve user via Supabase session cookie
59
+ from app.utils.auth import verify_supabase_jwt
60
  try:
61
+ if session_cookie == "guest_token_session_2026":
62
+ decoded_claims = {
63
+ "uid": "guest_uid_123",
64
+ "email": "guest@archvise.com",
65
+ "name": "Archvise Guest",
66
+ "picture": None
67
+ }
68
+ else:
69
+ decoded_claims = verify_supabase_jwt(session_cookie)
70
  except Exception:
71
  raise HTTPException(status_code=401, detail="Invalid session")
72
 
app/utils/ai.py CHANGED
@@ -26,8 +26,8 @@ def extract_json(text: str) -> Optional[dict]:
26
  return None
27
 
28
  @retry(
29
- stop=stop_after_attempt(3),
30
- wait=wait_exponential(multiplier=1, min=2, max=10),
31
  retry=retry_if_exception_type(httpx.HTTPError),
32
  reraise=True
33
  )
@@ -47,7 +47,7 @@ async def call_nvidia_nim(model: str, api_key: str, system_prompt: str, user_pro
47
  "max_tokens": 4096
48
  }
49
 
50
- async with httpx.AsyncClient(timeout=120.0) as client:
51
  response = await client.post(
52
  f"{settings.NVIDIA_BASE_URL}/chat/completions",
53
  headers=headers,
 
26
  return None
27
 
28
  @retry(
29
+ stop=stop_after_attempt(2),
30
+ wait=wait_exponential(multiplier=1, min=2, max=6),
31
  retry=retry_if_exception_type(httpx.HTTPError),
32
  reraise=True
33
  )
 
47
  "max_tokens": 4096
48
  }
49
 
50
+ async with httpx.AsyncClient(timeout=30.0) as client:
51
  response = await client.post(
52
  f"{settings.NVIDIA_BASE_URL}/chat/completions",
53
  headers=headers,
app/utils/firebase.py CHANGED
@@ -1,26 +1,2 @@
1
- import firebase_admin
2
- from firebase_admin import credentials, auth
3
- from app.config import settings
4
-
5
- # Construct the service account dictionary from settings
6
- firebase_creds_dict = {
7
- "type": "service_account",
8
- "project_id": settings.FIREBASE_PROJECT_ID,
9
- "private_key": settings.FIREBASE_PRIVATE_KEY,
10
- "client_email": settings.FIREBASE_CLIENT_EMAIL,
11
- "token_uri": "https://oauth2.googleapis.com/token"
12
- }
13
-
14
- # Initialize Firebase Admin SDK if not already initialized
15
- if not firebase_admin._apps:
16
- cred = credentials.Certificate(firebase_creds_dict)
17
- firebase_admin.initialize_app(cred)
18
-
19
- def verify_id_token(id_token: str):
20
- return auth.verify_id_token(id_token)
21
-
22
- def create_session_cookie(id_token: str, expires_in_seconds: int):
23
- return auth.create_session_cookie(id_token, expires_in=expires_in_seconds)
24
-
25
- def verify_session_cookie(session_cookie: str, check_revoked: bool = True):
26
- return auth.verify_session_cookie(session_cookie, check_revoked=check_revoked)
 
1
+ # Deleted during migration to Supabase. Do not import.
2
+ raise ImportError("firebase.py has been deleted and migrated to Supabase auth.")