Spaces:
Sleeping
Sleeping
Update api/server.py
Browse files- 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 |
-
# ✅
|
| 458 |
-
#
|
| 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 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
rag_context_text, rag_used_chunks = "", []
|
| 472 |
else:
|
| 473 |
-
|
| 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
|
| 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"],
|
| 592 |
model_name=sess["model_name"],
|
| 593 |
language_preference=resolved_lang,
|
| 594 |
-
learning_mode=req.learning_mode,
|
| 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,
|
| 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,
|
| 644 |
}
|
| 645 |
|
| 646 |
|
|
@@ -684,7 +689,19 @@ async def upload(
|
|
| 684 |
print(f"[upload] rag build error: {repr(e)}")
|
| 685 |
new_chunks = []
|
| 686 |
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 740 |
-
|
| 741 |
-
|
| 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(
|