Spaces:
Sleeping
Sleeping
Update api/server.py
Browse files- api/server.py +63 -24
api/server.py
CHANGED
|
@@ -127,29 +127,32 @@ def _get_session(user_id: str) -> Dict[str, Any]:
|
|
| 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 |
-
#
|
| 138 |
"profile_bio": "",
|
| 139 |
-
"init_answers": {},
|
| 140 |
-
"init_dismiss_until": 0,
|
| 141 |
}
|
|
|
|
| 142 |
if "uploaded_files" not in SESSIONS[user_id]:
|
| 143 |
SESSIONS[user_id]["uploaded_files"] = []
|
| 144 |
-
|
|
|
|
| 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 |
-
#
|
| 153 |
def _build_upload_hint(sess: Dict[str, Any]) -> str:
|
| 154 |
files = sess.get("uploaded_files") or []
|
| 155 |
if not files:
|
|
@@ -477,34 +480,51 @@ 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
- Tone: neutral, supportive, non-judgmental.
|
| 497 |
- No medical/psychological diagnosis language.
|
| 498 |
-
- Do not infer sensitive attributes (race
|
| 499 |
-
- Length:
|
| 500 |
-
- Structure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
|
| 502 |
-
Student name (if any): {sess.get("name","")}
|
| 503 |
Initialization answers (JSON):
|
| 504 |
{answers}
|
|
|
|
|
|
|
| 505 |
""".strip()
|
| 506 |
|
| 507 |
-
resolved_lang =
|
| 508 |
|
| 509 |
try:
|
| 510 |
bio, _unused_history, _run_id = chat_with_clare(
|
|
@@ -517,13 +537,14 @@ Initialization answers (JSON):
|
|
| 517 |
course_outline=sess["course_outline"],
|
| 518 |
weaknesses=sess["weaknesses"],
|
| 519 |
cognitive_state=sess["cognitive_state"],
|
| 520 |
-
rag_context="",
|
| 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 |
# ----------------------------
|
|
@@ -957,6 +978,31 @@ def profile_status(user_id: str):
|
|
| 957 |
}
|
| 958 |
|
| 959 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 960 |
@app.post("/api/profile/dismiss")
|
| 961 |
def profile_dismiss(req: ProfileDismissReq):
|
| 962 |
user_id = (req.user_id or "").strip()
|
|
@@ -964,7 +1010,7 @@ def profile_dismiss(req: ProfileDismissReq):
|
|
| 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 |
|
|
@@ -978,22 +1024,15 @@ def profile_init_submit(req: ProfileInitSubmitReq):
|
|
| 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
|
|
|
|
| 127 |
SESSIONS[user_id] = {
|
| 128 |
"user_id": user_id,
|
| 129 |
"name": "",
|
| 130 |
+
"history": [], # List[Tuple[str, str]]
|
| 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": {},
|
| 140 |
+
"init_dismiss_until": 0,
|
| 141 |
}
|
| 142 |
+
|
| 143 |
if "uploaded_files" not in SESSIONS[user_id]:
|
| 144 |
SESSIONS[user_id]["uploaded_files"] = []
|
| 145 |
+
|
| 146 |
+
# NEW backfill
|
| 147 |
SESSIONS[user_id].setdefault("profile_bio", "")
|
| 148 |
SESSIONS[user_id].setdefault("init_answers", {})
|
| 149 |
SESSIONS[user_id].setdefault("init_dismiss_until", 0)
|
| 150 |
+
|
| 151 |
return SESSIONS[user_id]
|
| 152 |
|
| 153 |
|
| 154 |
|
| 155 |
+
# NEW: helper to build a deterministic “what files are loaded” hint for the LLM
|
| 156 |
def _build_upload_hint(sess: Dict[str, Any]) -> str:
|
| 157 |
files = sess.get("uploaded_files") or []
|
| 158 |
if not files:
|
|
|
|
| 480 |
user_id: str
|
| 481 |
days: int = 7
|
| 482 |
|
| 483 |
+
|
| 484 |
class ProfileInitSubmitReq(BaseModel):
|
| 485 |
user_id: str
|
| 486 |
answers: Dict[str, Any]
|
| 487 |
language_preference: str = "Auto"
|
| 488 |
|
| 489 |
+
|
| 490 |
def _generate_profile_bio_with_clare(
|
| 491 |
sess: Dict[str, Any],
|
| 492 |
answers: Dict[str, Any],
|
| 493 |
language_preference: str = "Auto",
|
| 494 |
) -> str:
|
| 495 |
+
"""
|
| 496 |
+
Generates an English Profile Bio. Keep it neutral/supportive and non-judgmental.
|
| 497 |
+
IMPORTANT: Do not contaminate user's normal chat history; use empty history.
|
| 498 |
+
"""
|
| 499 |
+
student_name = (sess.get("name") or "").strip()
|
| 500 |
+
|
| 501 |
prompt = f"""
|
| 502 |
You are Clare, an AI teaching assistant.
|
|
|
|
| 503 |
|
| 504 |
+
Task:
|
| 505 |
+
Generate a concise English Profile Bio for the student using ONLY the initialization answers provided below.
|
| 506 |
+
|
| 507 |
+
Hard constraints:
|
| 508 |
+
- Output language: English.
|
| 509 |
- Tone: neutral, supportive, non-judgmental.
|
| 510 |
- No medical/psychological diagnosis language.
|
| 511 |
+
- Do not infer sensitive attributes (race, religion, political views, health status, sexuality, immigration status).
|
| 512 |
+
- Length: 60–120 words.
|
| 513 |
+
- Structure (4 short sentences max):
|
| 514 |
+
1) background & current context
|
| 515 |
+
2) learning goal for this course
|
| 516 |
+
3) learning preferences (format + pace)
|
| 517 |
+
4) how Clare will support them going forward (practical and concrete)
|
| 518 |
+
|
| 519 |
+
Student name (if available): {student_name}
|
| 520 |
|
|
|
|
| 521 |
Initialization answers (JSON):
|
| 522 |
{answers}
|
| 523 |
+
|
| 524 |
+
Return ONLY the bio text. Do not add a title.
|
| 525 |
""".strip()
|
| 526 |
|
| 527 |
+
resolved_lang = "English" # force English regardless of UI preference
|
| 528 |
|
| 529 |
try:
|
| 530 |
bio, _unused_history, _run_id = chat_with_clare(
|
|
|
|
| 537 |
course_outline=sess["course_outline"],
|
| 538 |
weaknesses=sess["weaknesses"],
|
| 539 |
cognitive_state=sess["cognitive_state"],
|
| 540 |
+
rag_context="",
|
| 541 |
)
|
| 542 |
return (bio or "").strip()
|
| 543 |
except Exception as e:
|
| 544 |
print("[profile_bio] generate failed:", repr(e))
|
| 545 |
return ""
|
| 546 |
|
| 547 |
+
|
| 548 |
# ----------------------------
|
| 549 |
# API Routes
|
| 550 |
# ----------------------------
|
|
|
|
| 978 |
}
|
| 979 |
|
| 980 |
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
@app.get("/api/profile/status")
|
| 984 |
+
def profile_status(user_id: str):
|
| 985 |
+
user_id = (user_id or "").strip()
|
| 986 |
+
if not user_id:
|
| 987 |
+
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 988 |
+
|
| 989 |
+
sess = _get_session(user_id)
|
| 990 |
+
bio = (sess.get("profile_bio") or "").strip()
|
| 991 |
+
bio_len = len(bio)
|
| 992 |
+
|
| 993 |
+
now = int(time.time())
|
| 994 |
+
dismissed_until = int(sess.get("init_dismiss_until") or 0)
|
| 995 |
+
|
| 996 |
+
# Trigger if bio is too short and not within dismiss window
|
| 997 |
+
need_init = (bio_len <= 50) and (now >= dismissed_until)
|
| 998 |
+
|
| 999 |
+
return {
|
| 1000 |
+
"need_init": need_init,
|
| 1001 |
+
"bio_len": bio_len,
|
| 1002 |
+
"dismissed_until": dismissed_until,
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
|
| 1006 |
@app.post("/api/profile/dismiss")
|
| 1007 |
def profile_dismiss(req: ProfileDismissReq):
|
| 1008 |
user_id = (req.user_id or "").strip()
|
|
|
|
| 1010 |
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 1011 |
|
| 1012 |
sess = _get_session(user_id)
|
| 1013 |
+
days = max(1, min(int(req.days or 7), 30)) # 1–30 days
|
| 1014 |
sess["init_dismiss_until"] = int(time.time()) + days * 24 * 3600
|
| 1015 |
return {"ok": True, "dismissed_until": sess["init_dismiss_until"]}
|
| 1016 |
|
|
|
|
| 1024 |
sess = _get_session(user_id)
|
| 1025 |
answers = req.answers or {}
|
| 1026 |
|
|
|
|
| 1027 |
sess["init_answers"] = answers
|
| 1028 |
|
|
|
|
| 1029 |
bio = _generate_profile_bio_with_clare(sess, answers, req.language_preference)
|
|
|
|
| 1030 |
if not bio:
|
| 1031 |
return JSONResponse({"error": "Failed to generate bio"}, status_code=500)
|
| 1032 |
|
| 1033 |
sess["profile_bio"] = bio
|
| 1034 |
|
| 1035 |
+
return {"ok": True, "bio": bio}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
|
| 1037 |
# ----------------------------
|
| 1038 |
# SPA Fallback
|