Spaces:
Sleeping
Sleeping
Update api/server.py
Browse files- api/server.py +127 -4
api/server.py
CHANGED
|
@@ -122,26 +122,33 @@ def _preload_module10_chunks() -> List[Dict[str, Any]]:
|
|
| 122 |
|
| 123 |
MODULE10_CHUNKS_CACHE = _preload_module10_chunks()
|
| 124 |
|
| 125 |
-
|
| 126 |
def _get_session(user_id: str) -> Dict[str, Any]:
|
| 127 |
if user_id not in SESSIONS:
|
| 128 |
SESSIONS[user_id] = {
|
| 129 |
"user_id": user_id,
|
| 130 |
"name": "",
|
| 131 |
-
"history": [],
|
| 132 |
"weaknesses": [],
|
| 133 |
"cognitive_state": {"confusion": 0, "mastery": 0},
|
| 134 |
"course_outline": DEFAULT_COURSE_TOPICS,
|
| 135 |
"rag_chunks": list(MODULE10_CHUNKS_CACHE),
|
| 136 |
"model_name": DEFAULT_MODEL,
|
| 137 |
-
"uploaded_files": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
-
# ✅ NEW: backfill for existing sessions created before this change
|
| 140 |
if "uploaded_files" not in SESSIONS[user_id]:
|
| 141 |
SESSIONS[user_id]["uploaded_files"] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
return SESSIONS[user_id]
|
| 143 |
|
| 144 |
|
|
|
|
| 145 |
# ✅ NEW: helper to build a deterministic “what files are loaded” hint for the LLM
|
| 146 |
def _build_upload_hint(sess: Dict[str, Any]) -> str:
|
| 147 |
files = sess.get("uploaded_files") or []
|
|
@@ -461,6 +468,62 @@ class FeedbackReq(BaseModel):
|
|
| 461 |
timestamp_ms: Optional[int] = None
|
| 462 |
|
| 463 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
# ----------------------------
|
| 465 |
# API Routes
|
| 466 |
# ----------------------------
|
|
@@ -871,6 +934,66 @@ def memoryline(user_id: str):
|
|
| 871 |
_ = _get_session((user_id or "").strip())
|
| 872 |
return {"next_review_label": "T+7", "progress_pct": 0.4}
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
|
| 875 |
# ----------------------------
|
| 876 |
# SPA Fallback
|
|
|
|
| 122 |
|
| 123 |
MODULE10_CHUNKS_CACHE = _preload_module10_chunks()
|
| 124 |
|
|
|
|
| 125 |
def _get_session(user_id: str) -> Dict[str, Any]:
|
| 126 |
if user_id not in SESSIONS:
|
| 127 |
SESSIONS[user_id] = {
|
| 128 |
"user_id": user_id,
|
| 129 |
"name": "",
|
| 130 |
+
"history": [],
|
| 131 |
"weaknesses": [],
|
| 132 |
"cognitive_state": {"confusion": 0, "mastery": 0},
|
| 133 |
"course_outline": DEFAULT_COURSE_TOPICS,
|
| 134 |
"rag_chunks": list(MODULE10_CHUNKS_CACHE),
|
| 135 |
"model_name": DEFAULT_MODEL,
|
| 136 |
+
"uploaded_files": [],
|
| 137 |
+
# NEW: profile init (MVP in-memory)
|
| 138 |
+
"profile_bio": "",
|
| 139 |
+
"init_answers": {}, # raw answers
|
| 140 |
+
"init_dismiss_until": 0, # unix ts seconds
|
| 141 |
}
|
|
|
|
| 142 |
if "uploaded_files" not in SESSIONS[user_id]:
|
| 143 |
SESSIONS[user_id]["uploaded_files"] = []
|
| 144 |
+
# backfill
|
| 145 |
+
SESSIONS[user_id].setdefault("profile_bio", "")
|
| 146 |
+
SESSIONS[user_id].setdefault("init_answers", {})
|
| 147 |
+
SESSIONS[user_id].setdefault("init_dismiss_until", 0)
|
| 148 |
return SESSIONS[user_id]
|
| 149 |
|
| 150 |
|
| 151 |
+
|
| 152 |
# ✅ NEW: helper to build a deterministic “what files are loaded” hint for the LLM
|
| 153 |
def _build_upload_hint(sess: Dict[str, Any]) -> str:
|
| 154 |
files = sess.get("uploaded_files") or []
|
|
|
|
| 468 |
timestamp_ms: Optional[int] = None
|
| 469 |
|
| 470 |
|
| 471 |
+
class ProfileStatusResp(BaseModel):
|
| 472 |
+
need_init: bool
|
| 473 |
+
bio_len: int
|
| 474 |
+
dismissed_until: int
|
| 475 |
+
|
| 476 |
+
class ProfileDismissReq(BaseModel):
|
| 477 |
+
user_id: str
|
| 478 |
+
days: int = 7
|
| 479 |
+
|
| 480 |
+
class ProfileInitSubmitReq(BaseModel):
|
| 481 |
+
user_id: str
|
| 482 |
+
answers: Dict[str, Any]
|
| 483 |
+
language_preference: str = "Auto"
|
| 484 |
+
|
| 485 |
+
def _generate_profile_bio_with_clare(
|
| 486 |
+
sess: Dict[str, Any],
|
| 487 |
+
answers: Dict[str, Any],
|
| 488 |
+
language_preference: str = "Auto",
|
| 489 |
+
) -> str:
|
| 490 |
+
# 生成时不要污染用户正常 history,用空 history
|
| 491 |
+
prompt = f"""
|
| 492 |
+
You are Clare, an AI teaching assistant.
|
| 493 |
+
Task: Generate a short Profile Bio for the student based ONLY on the provided initialization answers.
|
| 494 |
+
|
| 495 |
+
Rules:
|
| 496 |
+
- Tone: neutral, supportive, non-judgmental.
|
| 497 |
+
- No medical/psychological diagnosis language.
|
| 498 |
+
- Do not infer sensitive attributes (race/religion/politics/health/sexuality).
|
| 499 |
+
- Length: 80–160 Chinese characters if user seems Chinese; otherwise 60–120 English words.
|
| 500 |
+
- Structure: (1) background & goal, (2) current skill level, (3) learning preferences, (4) how Clare will support.
|
| 501 |
+
|
| 502 |
+
Student name (if any): {sess.get("name","")}
|
| 503 |
+
Initialization answers (JSON):
|
| 504 |
+
{answers}
|
| 505 |
+
""".strip()
|
| 506 |
+
|
| 507 |
+
resolved_lang = detect_language(prompt, language_preference)
|
| 508 |
+
|
| 509 |
+
try:
|
| 510 |
+
bio, _unused_history, _run_id = chat_with_clare(
|
| 511 |
+
message=prompt,
|
| 512 |
+
history=[],
|
| 513 |
+
model_name=sess["model_name"],
|
| 514 |
+
language_preference=resolved_lang,
|
| 515 |
+
learning_mode="summary",
|
| 516 |
+
doc_type="Other Course Document",
|
| 517 |
+
course_outline=sess["course_outline"],
|
| 518 |
+
weaknesses=sess["weaknesses"],
|
| 519 |
+
cognitive_state=sess["cognitive_state"],
|
| 520 |
+
rag_context="", # profile init 不需要 RAG
|
| 521 |
+
)
|
| 522 |
+
return (bio or "").strip()
|
| 523 |
+
except Exception as e:
|
| 524 |
+
print("[profile_bio] generate failed:", repr(e))
|
| 525 |
+
return ""
|
| 526 |
+
|
| 527 |
# ----------------------------
|
| 528 |
# API Routes
|
| 529 |
# ----------------------------
|
|
|
|
| 934 |
_ = _get_session((user_id or "").strip())
|
| 935 |
return {"next_review_label": "T+7", "progress_pct": 0.4}
|
| 936 |
|
| 937 |
+
@app.get("/api/profile/status")
|
| 938 |
+
def profile_status(user_id: str):
|
| 939 |
+
user_id = (user_id or "").strip()
|
| 940 |
+
if not user_id:
|
| 941 |
+
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 942 |
+
|
| 943 |
+
sess = _get_session(user_id)
|
| 944 |
+
bio = (sess.get("profile_bio") or "").strip()
|
| 945 |
+
bio_len = len(bio)
|
| 946 |
+
|
| 947 |
+
now = int(time.time())
|
| 948 |
+
dismissed_until = int(sess.get("init_dismiss_until") or 0)
|
| 949 |
+
|
| 950 |
+
# 触发条件:bio <= 50 且不在 dismiss 窗口内
|
| 951 |
+
need_init = (bio_len <= 50) and (now >= dismissed_until)
|
| 952 |
+
|
| 953 |
+
return {
|
| 954 |
+
"need_init": need_init,
|
| 955 |
+
"bio_len": bio_len,
|
| 956 |
+
"dismissed_until": dismissed_until,
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
@app.post("/api/profile/dismiss")
|
| 961 |
+
def profile_dismiss(req: ProfileDismissReq):
|
| 962 |
+
user_id = (req.user_id or "").strip()
|
| 963 |
+
if not user_id:
|
| 964 |
+
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 965 |
+
|
| 966 |
+
sess = _get_session(user_id)
|
| 967 |
+
days = max(1, min(int(req.days or 7), 30)) # 1–30天
|
| 968 |
+
sess["init_dismiss_until"] = int(time.time()) + days * 24 * 3600
|
| 969 |
+
return {"ok": True, "dismissed_until": sess["init_dismiss_until"]}
|
| 970 |
+
|
| 971 |
+
|
| 972 |
+
@app.post("/api/profile/init_submit")
|
| 973 |
+
def profile_init_submit(req: ProfileInitSubmitReq):
|
| 974 |
+
user_id = (req.user_id or "").strip()
|
| 975 |
+
if not user_id:
|
| 976 |
+
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 977 |
+
|
| 978 |
+
sess = _get_session(user_id)
|
| 979 |
+
answers = req.answers or {}
|
| 980 |
+
|
| 981 |
+
# save raw answers
|
| 982 |
+
sess["init_answers"] = answers
|
| 983 |
+
|
| 984 |
+
# generate bio
|
| 985 |
+
bio = _generate_profile_bio_with_clare(sess, answers, req.language_preference)
|
| 986 |
+
|
| 987 |
+
if not bio:
|
| 988 |
+
return JSONResponse({"error": "Failed to generate bio"}, status_code=500)
|
| 989 |
+
|
| 990 |
+
sess["profile_bio"] = bio
|
| 991 |
+
|
| 992 |
+
return {
|
| 993 |
+
"ok": True,
|
| 994 |
+
"bio": bio,
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
|
| 998 |
# ----------------------------
|
| 999 |
# SPA Fallback
|