SarahXia0405 commited on
Commit
09fcf0d
·
verified ·
1 Parent(s): e7bf3fe

Update api/server.py

Browse files
Files changed (1) hide show
  1. api/server.py +92 -83
api/server.py CHANGED
@@ -128,6 +128,11 @@ def _get_session(user_id: str) -> Dict[str, Any]:
128
  "course_outline": DEFAULT_COURSE_TOPICS,
129
  "rag_chunks": list(MODULE10_CHUNKS_CACHE),
130
  "model_name": DEFAULT_MODEL,
 
 
 
 
 
131
  }
132
  return SESSIONS[user_id]
133
 
@@ -351,32 +356,72 @@ class SummaryReq(BaseModel):
351
 
352
 
353
  class FeedbackReq(BaseModel):
354
- # IMPORTANT: allow extra fields so FE can evolve without breaking backend
355
  class Config:
356
  extra = "ignore"
357
 
358
  user_id: str
359
  rating: str # "helpful" | "not_helpful"
360
-
361
- # NEW: attach feedback to a specific LangSmith run
362
  run_id: Optional[str] = None
363
-
364
  assistant_message_id: Optional[str] = None
365
-
366
  assistant_text: str
367
  user_text: Optional[str] = ""
368
-
369
  comment: Optional[str] = ""
370
-
371
- # optional structured fields
372
  tags: Optional[List[str]] = []
373
  refs: Optional[List[str]] = []
374
-
375
  learning_mode: Optional[str] = None
376
  doc_type: Optional[str] = None
377
  timestamp_ms: Optional[int] = None
378
 
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  # ----------------------------
381
  # API Routes
382
  # ----------------------------
@@ -391,6 +436,7 @@ def login(req: LoginReq):
391
  sess["name"] = name
392
  return {"ok": True, "user": {"name": name, "user_id": user_id}}
393
 
 
394
  @app.post("/api/chat")
395
  def chat(req: ChatReq):
396
  user_id = (req.user_id or "").strip()
@@ -411,37 +457,6 @@ def chat(req: ChatReq):
411
  "run_id": None,
412
  }
413
 
414
- # ----------------------------
415
- # RAG query normalization (short file-intent prompts)
416
- # ----------------------------
417
- def _looks_like_file_request(text: str) -> bool:
418
- t = (text or "").strip().lower()
419
- if not t:
420
- return False
421
- triggers = [
422
- "read this",
423
- "summarize",
424
- "summary",
425
- "can you see",
426
- "see that file",
427
- "see the file",
428
- "that file",
429
- "this file",
430
- "the file",
431
- "attached",
432
- "attachment",
433
- "upload",
434
- "uploaded",
435
- "document",
436
- "pdf",
437
- "ppt",
438
- "slides",
439
- "docx",
440
- "analyze",
441
- "explain this doc",
442
- ]
443
- return any(k in t for k in triggers)
444
-
445
  t0 = time.time()
446
  marks_ms: Dict[str, float] = {"start": 0.0}
447
 
@@ -454,31 +469,27 @@ def chat(req: ChatReq):
454
  sess["cognitive_state"] = update_cognitive_state_from_message(msg, sess["cognitive_state"])
455
  marks_ms["cognitive_update_done"] = (time.time() - t0) * 1000.0
456
 
457
- # ✅ Key fix:
458
- # - DO NOT gate RAG purely by message length.
459
- # - For very short generic messages (e.g. "hi"), skip to save latency.
460
- # - For short file-intent messages (e.g. "Read this"), force a better retrieval query.
461
  rag_context_text, rag_used_chunks = "", []
462
  try:
463
- chunks = sess.get("rag_chunks") or []
464
- has_chunks = len(chunks) > 0
465
-
466
- if has_chunks:
467
- is_file_intent = _looks_like_file_request(msg)
468
- is_too_short_generic = (len(msg) < 8) and (not is_file_intent)
469
-
470
- if is_too_short_generic:
 
 
 
 
471
  rag_context_text, rag_used_chunks = "", []
472
  else:
473
- retrieval_query = msg
474
- if is_file_intent:
475
- # Make retrieval robust even when user message is vague.
476
- # Include doc_type to bias retrieval toward recently uploaded material.
477
- retrieval_query = f"uploaded document ({req.doc_type}) key content and relevant excerpts for: {msg}"
478
-
479
- rag_context_text, rag_used_chunks = retrieve_relevant_chunks(retrieval_query, chunks)
480
  except Exception as e:
481
- print(f"[chat] rag retrieve error: {repr(e)}")
482
  rag_context_text, rag_used_chunks = "", []
483
 
484
  marks_ms["rag_retrieve_done"] = (time.time() - t0) * 1000.0
@@ -572,15 +583,11 @@ def quiz_start(req: QuizStartReq):
572
 
573
  sess = _get_session(user_id)
574
 
575
- # 用 quiz instruction 启动(不更新 weaknesses/cognitive_state,避免“系统指令”污染状态)
576
  quiz_instruction = MICRO_QUIZ_INSTRUCTION
577
-
578
  t0 = time.time()
579
 
580
- # 语言:如果 Auto,让 detect_language 决定;否则按传入语言
581
  resolved_lang = detect_language(quiz_instruction, req.language_preference)
582
 
583
- # RAG:强制用 module10/当前 session 的 chunks,检索一个稳定 query
584
  rag_context_text, rag_used_chunks = retrieve_relevant_chunks(
585
  "Module 10 quiz", sess["rag_chunks"]
586
  )
@@ -588,10 +595,10 @@ def quiz_start(req: QuizStartReq):
588
  try:
589
  answer, new_history, run_id = chat_with_clare(
590
  message=quiz_instruction,
591
- history=sess["history"], # 直接接在当前会话 history 后面
592
  model_name=sess["model_name"],
593
  language_preference=resolved_lang,
594
- learning_mode=req.learning_mode, # 默认 "quiz"
595
  doc_type=req.doc_type,
596
  course_outline=sess["course_outline"],
597
  weaknesses=sess["weaknesses"],
@@ -603,8 +610,6 @@ def quiz_start(req: QuizStartReq):
603
  return JSONResponse({"error": f"quiz_start failed: {repr(e)}"}, status_code=500)
604
 
605
  total_ms = (time.time() - t0) * 1000.0
606
-
607
- # 写回 session history(后续用户回答继续走 /api/chat,会延续 quiz 上下文)
608
  sess["history"] = new_history
609
 
610
  refs = [
@@ -629,7 +634,7 @@ def quiz_start(req: QuizStartReq):
629
  "refs": refs,
630
  "rag_used_chunks_count": len(rag_used_chunks or []),
631
  "history_len": len(sess["history"]),
632
- "run_id": run_id, # NEW
633
  }
634
  )
635
 
@@ -640,7 +645,7 @@ def quiz_start(req: QuizStartReq):
640
  ),
641
  "refs": refs,
642
  "latency_ms": total_ms,
643
- "run_id": run_id, # NEW
644
  }
645
 
646
 
@@ -684,7 +689,19 @@ async def upload(
684
  print(f"[upload] rag build error: {repr(e)}")
685
  new_chunks = []
686
 
687
- status_md = f"Loaded base reading + uploaded {doc_type} file."
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
  _log_event_to_langsmith(
690
  {
@@ -701,7 +718,7 @@ async def upload(
701
  }
702
  )
703
 
704
- return {"ok": True, "added_chunks": len(new_chunks), "status_md": status_md}
705
 
706
 
707
  @app.post("/api/feedback")
@@ -717,7 +734,6 @@ def api_feedback(req: FeedbackReq):
717
  if rating not in ("helpful", "not_helpful"):
718
  return JSONResponse({"ok": False, "error": "Invalid rating"}, status_code=400)
719
 
720
- # normalize fields
721
  assistant_text = (req.assistant_text or "").strip()
722
  user_text = (req.user_text or "").strip()
723
  comment = (req.comment or "").strip()
@@ -725,7 +741,6 @@ def api_feedback(req: FeedbackReq):
725
  tags = req.tags or []
726
  timestamp_ms = int(req.timestamp_ms or int(time.time() * 1000))
727
 
728
- # 1) Dataset event log (what you already have)
729
  _log_event_to_langsmith(
730
  {
731
  "experiment_id": EXPERIMENT_ID,
@@ -736,13 +751,9 @@ def api_feedback(req: FeedbackReq):
736
  "timestamp_ms": timestamp_ms,
737
  "rating": rating,
738
  "assistant_message_id": req.assistant_message_id,
739
- "run_id": req.run_id, # NEW
740
-
741
- # Keep the Example readable:
742
- "question": user_text, # what user asked (optional)
743
- "answer": assistant_text, # the assistant response being rated
744
-
745
- # metadata
746
  "comment": comment,
747
  "tags": tags,
748
  "refs": refs,
@@ -751,8 +762,6 @@ def api_feedback(req: FeedbackReq):
751
  }
752
  )
753
 
754
- # 2) Run-level feedback (attach to actual LangSmith run)
755
- # Only works when FE provides run_id and LangSmith credentials are configured.
756
  wrote_run_feedback = False
757
  if req.run_id:
758
  wrote_run_feedback = _write_feedback_to_langsmith_run(
 
128
  "course_outline": DEFAULT_COURSE_TOPICS,
129
  "rag_chunks": list(MODULE10_CHUNKS_CACHE),
130
  "model_name": DEFAULT_MODEL,
131
+
132
+ # ✅ NEW: keep track of uploaded files and their chunks
133
+ "uploaded_chunks_by_file": {}, # Dict[str, List[Dict[str, Any]]]
134
+ "last_uploaded_filename": None, # Optional[str]
135
+ "uploaded_filenames": [], # List[str]
136
  }
137
  return SESSIONS[user_id]
138
 
 
356
 
357
 
358
  class FeedbackReq(BaseModel):
 
359
  class Config:
360
  extra = "ignore"
361
 
362
  user_id: str
363
  rating: str # "helpful" | "not_helpful"
 
 
364
  run_id: Optional[str] = None
 
365
  assistant_message_id: Optional[str] = None
 
366
  assistant_text: str
367
  user_text: Optional[str] = ""
 
368
  comment: Optional[str] = ""
 
 
369
  tags: Optional[List[str]] = []
370
  refs: Optional[List[str]] = []
 
371
  learning_mode: Optional[str] = None
372
  doc_type: Optional[str] = None
373
  timestamp_ms: Optional[int] = None
374
 
375
 
376
+ # ----------------------------
377
+ # Helpers: prefer last uploaded file when user asks "read/summarize uploaded file"
378
+ # ----------------------------
379
+ def _wants_last_uploaded_file(msg: str) -> bool:
380
+ t = (msg or "").lower()
381
+ triggers = [
382
+ "summarize the uploaded file",
383
+ "summarise the uploaded file",
384
+ "summarize uploaded file",
385
+ "uploaded file",
386
+ "read this",
387
+ "can you see that file",
388
+ "can you see the file",
389
+ "read the file",
390
+ "summarize the file i uploaded",
391
+ "summarize the document i uploaded",
392
+ "summarize the document",
393
+ "总结我上传的文件",
394
+ "总结上传的文件",
395
+ "读一下我上传的",
396
+ "能看到我上传的文件吗",
397
+ "看一下我上传的文件",
398
+ ]
399
+ return any(k in t for k in triggers)
400
+
401
+
402
+ def _concat_chunks_text(chunks: List[Dict[str, Any]], max_chars: int = 2000) -> str:
403
+ if not chunks:
404
+ return ""
405
+ out: List[str] = []
406
+ total = 0
407
+ for c in chunks:
408
+ # common keys: "text" / "content" / "chunk"
409
+ txt = c.get("text") or c.get("content") or c.get("chunk") or ""
410
+ txt = (txt or "").strip()
411
+ if not txt:
412
+ continue
413
+ remain = max_chars - total
414
+ if remain <= 0:
415
+ break
416
+ if len(txt) > remain:
417
+ txt = txt[:remain]
418
+ out.append(txt)
419
+ total += len(txt) + 1
420
+ if total >= max_chars:
421
+ break
422
+ return "\n\n".join(out)
423
+
424
+
425
  # ----------------------------
426
  # API Routes
427
  # ----------------------------
 
436
  sess["name"] = name
437
  return {"ok": True, "user": {"name": name, "user_id": user_id}}
438
 
439
+
440
  @app.post("/api/chat")
441
  def chat(req: ChatReq):
442
  user_id = (req.user_id or "").strip()
 
457
  "run_id": None,
458
  }
459
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  t0 = time.time()
461
  marks_ms: Dict[str, float] = {"start": 0.0}
462
 
 
469
  sess["cognitive_state"] = update_cognitive_state_from_message(msg, sess["cognitive_state"])
470
  marks_ms["cognitive_update_done"] = (time.time() - t0) * 1000.0
471
 
472
+ # ✅ RAG selection:
473
+ # If user explicitly asks to read/summarize the uploaded file, prefer last uploaded file chunks
 
 
474
  rag_context_text, rag_used_chunks = "", []
475
  try:
476
+ if _wants_last_uploaded_file(msg):
477
+ last_fn = sess.get("last_uploaded_filename")
478
+ by_file = sess.get("uploaded_chunks_by_file") or {}
479
+ last_chunks = by_file.get(last_fn) if last_fn else None
480
+ if last_chunks:
481
+ rag_context_text = _concat_chunks_text(last_chunks, max_chars=2000)
482
+ rag_used_chunks = list(last_chunks)[:6] # keep refs small/stable
483
+ else:
484
+ # fallback: if no last upload available, do normal retrieval
485
+ rag_context_text, rag_used_chunks = retrieve_relevant_chunks(msg, sess["rag_chunks"])
486
+ else:
487
+ if len(msg) < 20 and ("?" not in msg):
488
  rag_context_text, rag_used_chunks = "", []
489
  else:
490
+ rag_context_text, rag_used_chunks = retrieve_relevant_chunks(msg, sess["rag_chunks"])
 
 
 
 
 
 
491
  except Exception as e:
492
+ print(f"[chat] rag error: {repr(e)}")
493
  rag_context_text, rag_used_chunks = "", []
494
 
495
  marks_ms["rag_retrieve_done"] = (time.time() - t0) * 1000.0
 
583
 
584
  sess = _get_session(user_id)
585
 
 
586
  quiz_instruction = MICRO_QUIZ_INSTRUCTION
 
587
  t0 = time.time()
588
 
 
589
  resolved_lang = detect_language(quiz_instruction, req.language_preference)
590
 
 
591
  rag_context_text, rag_used_chunks = retrieve_relevant_chunks(
592
  "Module 10 quiz", sess["rag_chunks"]
593
  )
 
595
  try:
596
  answer, new_history, run_id = chat_with_clare(
597
  message=quiz_instruction,
598
+ history=sess["history"],
599
  model_name=sess["model_name"],
600
  language_preference=resolved_lang,
601
+ learning_mode=req.learning_mode,
602
  doc_type=req.doc_type,
603
  course_outline=sess["course_outline"],
604
  weaknesses=sess["weaknesses"],
 
610
  return JSONResponse({"error": f"quiz_start failed: {repr(e)}"}, status_code=500)
611
 
612
  total_ms = (time.time() - t0) * 1000.0
 
 
613
  sess["history"] = new_history
614
 
615
  refs = [
 
634
  "refs": refs,
635
  "rag_used_chunks_count": len(rag_used_chunks or []),
636
  "history_len": len(sess["history"]),
637
+ "run_id": run_id,
638
  }
639
  )
640
 
 
645
  ),
646
  "refs": refs,
647
  "latency_ms": total_ms,
648
+ "run_id": run_id,
649
  }
650
 
651
 
 
689
  print(f"[upload] rag build error: {repr(e)}")
690
  new_chunks = []
691
 
692
+ #NEW: remember this upload as "last uploaded file"
693
+ try:
694
+ sess["uploaded_chunks_by_file"] = sess.get("uploaded_chunks_by_file") or {}
695
+ sess["uploaded_chunks_by_file"][safe_name] = new_chunks
696
+ sess["last_uploaded_filename"] = safe_name
697
+ lst = sess.get("uploaded_filenames") or []
698
+ if safe_name not in lst:
699
+ lst.append(safe_name)
700
+ sess["uploaded_filenames"] = lst
701
+ except Exception as e:
702
+ print(f"[upload] session remember failed: {repr(e)}")
703
+
704
+ status_md = f"✅ Loaded base reading + uploaded {doc_type} file: {safe_name} (chunks={len(new_chunks)})."
705
 
706
  _log_event_to_langsmith(
707
  {
 
718
  }
719
  )
720
 
721
+ return {"ok": True, "added_chunks": len(new_chunks), "status_md": status_md, "filename": safe_name}
722
 
723
 
724
  @app.post("/api/feedback")
 
734
  if rating not in ("helpful", "not_helpful"):
735
  return JSONResponse({"ok": False, "error": "Invalid rating"}, status_code=400)
736
 
 
737
  assistant_text = (req.assistant_text or "").strip()
738
  user_text = (req.user_text or "").strip()
739
  comment = (req.comment or "").strip()
 
741
  tags = req.tags or []
742
  timestamp_ms = int(req.timestamp_ms or int(time.time() * 1000))
743
 
 
744
  _log_event_to_langsmith(
745
  {
746
  "experiment_id": EXPERIMENT_ID,
 
751
  "timestamp_ms": timestamp_ms,
752
  "rating": rating,
753
  "assistant_message_id": req.assistant_message_id,
754
+ "run_id": req.run_id,
755
+ "question": user_text,
756
+ "answer": assistant_text,
 
 
 
 
757
  "comment": comment,
758
  "tags": tags,
759
  "refs": refs,
 
762
  }
763
  )
764
 
 
 
765
  wrote_run_feedback = False
766
  if req.run_id:
767
  wrote_run_feedback = _write_feedback_to_langsmith_run(