Spaces:
Sleeping
Sleeping
Update api/server.py
Browse files- api/server.py +124 -107
api/server.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# api/server.py
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
-
from typing import Dict,
|
| 5 |
|
| 6 |
from fastapi import FastAPI, UploadFile, File, Form, Request
|
| 7 |
from fastapi.responses import FileResponse, JSONResponse
|
|
@@ -22,57 +22,11 @@ from api.clare_core import (
|
|
| 22 |
summarize_conversation,
|
| 23 |
)
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
# LangSmith (Dataset logging)
|
| 27 |
-
# ----------------------------
|
| 28 |
-
# 你在 HF Space 里需要配置:
|
| 29 |
-
# LANGSMITH_API_KEY=...
|
| 30 |
-
# 可选:
|
| 31 |
-
# LANGSMITH_DATASET_NAME=clare_user_events
|
| 32 |
-
# LANGSMITH_PROJECT=...
|
| 33 |
try:
|
| 34 |
-
from langsmith import Client
|
| 35 |
except Exception:
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
LS_DATASET_NAME = os.getenv("LANGSMITH_DATASET_NAME", "clare_user_events").strip()
|
| 39 |
-
LS_PROJECT = os.getenv("LANGSMITH_PROJECT", "").strip()
|
| 40 |
-
|
| 41 |
-
_ls_client = None
|
| 42 |
-
if LangSmithClient is not None and os.getenv("LANGSMITH_API_KEY"):
|
| 43 |
-
try:
|
| 44 |
-
_ls_client = LangSmithClient()
|
| 45 |
-
except Exception as e:
|
| 46 |
-
print(f"[langsmith] init failed: {repr(e)}")
|
| 47 |
-
_ls_client = None
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def log_event_to_langsmith(
|
| 51 |
-
*,
|
| 52 |
-
inputs: Dict[str, Any],
|
| 53 |
-
outputs: Dict[str, Any],
|
| 54 |
-
metadata: Dict[str, Any],
|
| 55 |
-
) -> None:
|
| 56 |
-
"""
|
| 57 |
-
Write a single event as an Example row into LangSmith Dataset.
|
| 58 |
-
This mirrors your old Gradio pattern (dataset作为事件日志).
|
| 59 |
-
"""
|
| 60 |
-
if _ls_client is None:
|
| 61 |
-
return
|
| 62 |
-
try:
|
| 63 |
-
# project 不是必须;dataset 足够你做过滤与分析
|
| 64 |
-
if LS_PROJECT:
|
| 65 |
-
metadata = {**metadata, "langsmith_project": LS_PROJECT}
|
| 66 |
-
|
| 67 |
-
_ls_client.create_example(
|
| 68 |
-
inputs=inputs,
|
| 69 |
-
outputs=outputs,
|
| 70 |
-
metadata=metadata,
|
| 71 |
-
dataset_name=LS_DATASET_NAME,
|
| 72 |
-
)
|
| 73 |
-
except Exception as e:
|
| 74 |
-
print(f"[langsmith] create_example failed: {repr(e)}")
|
| 75 |
-
|
| 76 |
|
| 77 |
# ----------------------------
|
| 78 |
# Paths / Constants
|
|
@@ -82,11 +36,14 @@ API_DIR = os.path.dirname(__file__)
|
|
| 82 |
MODULE10_PATH = os.path.join(API_DIR, "module10_responsible_ai.pdf")
|
| 83 |
MODULE10_DOC_TYPE = "Literature Review / Paper"
|
| 84 |
|
| 85 |
-
# Vite build output in your repo is "web/build"
|
| 86 |
WEB_DIST = os.path.abspath(os.path.join(API_DIR, "..", "web", "build"))
|
| 87 |
WEB_INDEX = os.path.join(WEB_DIST, "index.html")
|
| 88 |
WEB_ASSETS = os.path.join(WEB_DIST, "assets")
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
# ----------------------------
|
| 91 |
# App
|
| 92 |
# ----------------------------
|
|
@@ -154,6 +111,52 @@ def _get_session(user_id: str) -> Dict:
|
|
| 154 |
return SESSIONS[user_id]
|
| 155 |
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
# ----------------------------
|
| 158 |
# Schemas
|
| 159 |
# ----------------------------
|
|
@@ -182,19 +185,22 @@ class SummaryReq(BaseModel):
|
|
| 182 |
|
| 183 |
|
| 184 |
class FeedbackReq(BaseModel):
|
| 185 |
-
# FE 会发的最小字段
|
| 186 |
user_id: str
|
| 187 |
rating: str # "helpful" | "not_helpful"
|
| 188 |
-
assistant_message_id: str
|
|
|
|
|
|
|
| 189 |
assistant_text: str
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
#
|
| 192 |
-
|
| 193 |
-
comment: Optional[str] = None
|
| 194 |
-
refs: Optional[List[str]] = None
|
| 195 |
learning_mode: Optional[str] = None
|
| 196 |
doc_type: Optional[str] = None
|
| 197 |
-
timestamp_ms: Optional[
|
| 198 |
|
| 199 |
|
| 200 |
# ----------------------------
|
|
@@ -216,7 +222,6 @@ def login(req: LoginReq):
|
|
| 216 |
def chat(req: ChatReq):
|
| 217 |
user_id = (req.user_id or "").strip()
|
| 218 |
msg = (req.message or "").strip()
|
| 219 |
-
|
| 220 |
if not user_id:
|
| 221 |
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 222 |
|
|
@@ -265,27 +270,24 @@ def chat(req: ChatReq):
|
|
| 265 |
for c in (rag_used_chunks or [])
|
| 266 |
]
|
| 267 |
|
| 268 |
-
#
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
)
|
| 287 |
-
except Exception:
|
| 288 |
-
pass
|
| 289 |
|
| 290 |
return {
|
| 291 |
"reply": answer,
|
|
@@ -323,7 +325,6 @@ async def upload(
|
|
| 323 |
if doc_type == "Syllabus":
|
| 324 |
class _F:
|
| 325 |
pass
|
| 326 |
-
|
| 327 |
fo = _F()
|
| 328 |
fo.name = tmp_path
|
| 329 |
try:
|
|
@@ -339,42 +340,58 @@ async def upload(
|
|
| 339 |
new_chunks = []
|
| 340 |
|
| 341 |
status_md = f"✅ Loaded base reading + uploaded {doc_type} file."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
return {"ok": True, "added_chunks": len(new_chunks), "status_md": status_md}
|
| 343 |
|
| 344 |
|
| 345 |
@app.post("/api/feedback")
|
| 346 |
-
def
|
| 347 |
user_id = (req.user_id or "").strip()
|
| 348 |
if not user_id:
|
| 349 |
return JSONResponse({"ok": False, "error": "Missing user_id"}, status_code=400)
|
| 350 |
|
|
|
|
|
|
|
|
|
|
| 351 |
rating = (req.rating or "").strip().lower()
|
| 352 |
if rating not in ("helpful", "not_helpful"):
|
| 353 |
-
return JSONResponse({"ok": False, "error": "rating
|
| 354 |
-
|
| 355 |
-
#
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
},
|
| 375 |
-
)
|
| 376 |
-
except Exception as e:
|
| 377 |
-
print(f"[feedback] log failed: {repr(e)}")
|
| 378 |
|
| 379 |
return {"ok": True}
|
| 380 |
|
|
|
|
| 1 |
# api/server.py
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
|
| 6 |
from fastapi import FastAPI, UploadFile, File, Form, Request
|
| 7 |
from fastapi.responses import FileResponse, JSONResponse
|
|
|
|
| 22 |
summarize_conversation,
|
| 23 |
)
|
| 24 |
|
| 25 |
+
# ✅ LangSmith (same idea as your Gradio app.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
try:
|
| 27 |
+
from langsmith import Client
|
| 28 |
except Exception:
|
| 29 |
+
Client = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# ----------------------------
|
| 32 |
# Paths / Constants
|
|
|
|
| 36 |
MODULE10_PATH = os.path.join(API_DIR, "module10_responsible_ai.pdf")
|
| 37 |
MODULE10_DOC_TYPE = "Literature Review / Paper"
|
| 38 |
|
|
|
|
| 39 |
WEB_DIST = os.path.abspath(os.path.join(API_DIR, "..", "web", "build"))
|
| 40 |
WEB_INDEX = os.path.join(WEB_DIST, "index.html")
|
| 41 |
WEB_ASSETS = os.path.join(WEB_DIST, "assets")
|
| 42 |
|
| 43 |
+
# ✅ LangSmith dataset name (match what you used before)
|
| 44 |
+
LS_DATASET_NAME = os.getenv("LS_DATASET_NAME", "clare_user_events").strip()
|
| 45 |
+
LS_PROJECT = os.getenv("LANGSMITH_PROJECT", os.getenv("LANGCHAIN_PROJECT", "")).strip() # optional
|
| 46 |
+
|
| 47 |
# ----------------------------
|
| 48 |
# App
|
| 49 |
# ----------------------------
|
|
|
|
| 111 |
return SESSIONS[user_id]
|
| 112 |
|
| 113 |
|
| 114 |
+
# ----------------------------
|
| 115 |
+
# LangSmith helpers
|
| 116 |
+
# ----------------------------
|
| 117 |
+
_ls_client = None
|
| 118 |
+
if Client is not None:
|
| 119 |
+
try:
|
| 120 |
+
_ls_client = Client()
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print("[langsmith] init failed:", repr(e))
|
| 123 |
+
_ls_client = None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _log_event_to_langsmith(data: Dict):
|
| 127 |
+
"""
|
| 128 |
+
Create an Example in LangSmith Dataset (clare_user_events).
|
| 129 |
+
Mimic your previous Gradio log_event behavior.
|
| 130 |
+
|
| 131 |
+
Inputs/Outputs show up as "Inputs" / "Reference Outputs".
|
| 132 |
+
Everything else goes into metadata columns.
|
| 133 |
+
"""
|
| 134 |
+
if _ls_client is None:
|
| 135 |
+
return
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
inputs = {
|
| 139 |
+
"question": data.get("question", ""),
|
| 140 |
+
"student_id": data.get("student_id", ""),
|
| 141 |
+
"student_name": data.get("student_name", ""),
|
| 142 |
+
}
|
| 143 |
+
outputs = {"answer": data.get("answer", "")}
|
| 144 |
+
metadata = {k: v for k, v in data.items() if k not in ("question", "answer")}
|
| 145 |
+
|
| 146 |
+
# helpful for filtering in UI
|
| 147 |
+
if LS_PROJECT:
|
| 148 |
+
metadata.setdefault("langsmith_project", LS_PROJECT)
|
| 149 |
+
|
| 150 |
+
_ls_client.create_example(
|
| 151 |
+
inputs=inputs,
|
| 152 |
+
outputs=outputs,
|
| 153 |
+
metadata=metadata,
|
| 154 |
+
dataset_name=LS_DATASET_NAME,
|
| 155 |
+
)
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print("[langsmith] log failed:", repr(e))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
# ----------------------------
|
| 161 |
# Schemas
|
| 162 |
# ----------------------------
|
|
|
|
| 185 |
|
| 186 |
|
| 187 |
class FeedbackReq(BaseModel):
|
|
|
|
| 188 |
user_id: str
|
| 189 |
rating: str # "helpful" | "not_helpful"
|
| 190 |
+
assistant_message_id: Optional[str] = None
|
| 191 |
+
|
| 192 |
+
# what the user is rating
|
| 193 |
assistant_text: str
|
| 194 |
+
user_text: Optional[str] = ""
|
| 195 |
+
|
| 196 |
+
# optional free-text comment
|
| 197 |
+
comment: Optional[str] = ""
|
| 198 |
|
| 199 |
+
# context for analysis
|
| 200 |
+
refs: Optional[List[str]] = []
|
|
|
|
|
|
|
| 201 |
learning_mode: Optional[str] = None
|
| 202 |
doc_type: Optional[str] = None
|
| 203 |
+
timestamp_ms: Optional[int] = None
|
| 204 |
|
| 205 |
|
| 206 |
# ----------------------------
|
|
|
|
| 222 |
def chat(req: ChatReq):
|
| 223 |
user_id = (req.user_id or "").strip()
|
| 224 |
msg = (req.message or "").strip()
|
|
|
|
| 225 |
if not user_id:
|
| 226 |
return JSONResponse({"error": "Missing user_id"}, status_code=400)
|
| 227 |
|
|
|
|
| 270 |
for c in (rag_used_chunks or [])
|
| 271 |
]
|
| 272 |
|
| 273 |
+
# ✅ log chat_turn to LangSmith (uses login name/id; NO hardcoding)
|
| 274 |
+
_log_event_to_langsmith(
|
| 275 |
+
{
|
| 276 |
+
"experiment_id": "RESP_AI_W10",
|
| 277 |
+
"student_id": user_id,
|
| 278 |
+
"student_name": sess.get("name", ""),
|
| 279 |
+
"event_type": "chat_turn",
|
| 280 |
+
"timestamp": time.time(),
|
| 281 |
+
"latency_ms": latency_ms,
|
| 282 |
+
"question": msg,
|
| 283 |
+
"answer": answer,
|
| 284 |
+
"model_name": sess["model_name"],
|
| 285 |
+
"language": resolved_lang,
|
| 286 |
+
"learning_mode": req.learning_mode,
|
| 287 |
+
"doc_type": req.doc_type,
|
| 288 |
+
"refs": refs,
|
| 289 |
+
}
|
| 290 |
+
)
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
return {
|
| 293 |
"reply": answer,
|
|
|
|
| 325 |
if doc_type == "Syllabus":
|
| 326 |
class _F:
|
| 327 |
pass
|
|
|
|
| 328 |
fo = _F()
|
| 329 |
fo.name = tmp_path
|
| 330 |
try:
|
|
|
|
| 340 |
new_chunks = []
|
| 341 |
|
| 342 |
status_md = f"✅ Loaded base reading + uploaded {doc_type} file."
|
| 343 |
+
|
| 344 |
+
# ✅ optional: log upload event
|
| 345 |
+
_log_event_to_langsmith(
|
| 346 |
+
{
|
| 347 |
+
"experiment_id": "RESP_AI_W10",
|
| 348 |
+
"student_id": user_id,
|
| 349 |
+
"student_name": sess.get("name", ""),
|
| 350 |
+
"event_type": "upload",
|
| 351 |
+
"timestamp": time.time(),
|
| 352 |
+
"doc_type": doc_type,
|
| 353 |
+
"filename": safe_name,
|
| 354 |
+
"added_chunks": len(new_chunks),
|
| 355 |
+
"question": f"[upload] {safe_name}",
|
| 356 |
+
"answer": status_md,
|
| 357 |
+
}
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
return {"ok": True, "added_chunks": len(new_chunks), "status_md": status_md}
|
| 361 |
|
| 362 |
|
| 363 |
@app.post("/api/feedback")
|
| 364 |
+
def api_feedback(req: FeedbackReq):
|
| 365 |
user_id = (req.user_id or "").strip()
|
| 366 |
if not user_id:
|
| 367 |
return JSONResponse({"ok": False, "error": "Missing user_id"}, status_code=400)
|
| 368 |
|
| 369 |
+
sess = _get_session(user_id)
|
| 370 |
+
student_name = sess.get("name", "")
|
| 371 |
+
|
| 372 |
rating = (req.rating or "").strip().lower()
|
| 373 |
if rating not in ("helpful", "not_helpful"):
|
| 374 |
+
return JSONResponse({"ok": False, "error": "Invalid rating"}, status_code=400)
|
| 375 |
+
|
| 376 |
+
# ✅ record feedback as its own event row in the SAME dataset
|
| 377 |
+
_log_event_to_langsmith(
|
| 378 |
+
{
|
| 379 |
+
"experiment_id": "RESP_AI_W10",
|
| 380 |
+
"student_id": user_id,
|
| 381 |
+
"student_name": student_name,
|
| 382 |
+
"event_type": "feedback",
|
| 383 |
+
"timestamp": time.time(),
|
| 384 |
+
"rating": rating,
|
| 385 |
+
"assistant_message_id": req.assistant_message_id,
|
| 386 |
+
"question": (req.user_text or "").strip(),
|
| 387 |
+
"answer": (req.assistant_text or "").strip(),
|
| 388 |
+
"comment": (req.comment or "").strip(),
|
| 389 |
+
"refs": req.refs or [],
|
| 390 |
+
"learning_mode": req.learning_mode,
|
| 391 |
+
"doc_type": req.doc_type,
|
| 392 |
+
"timestamp_ms": req.timestamp_ms,
|
| 393 |
+
}
|
| 394 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
return {"ok": True}
|
| 397 |
|