import json from fastapi import APIRouter, Depends, Form, UploadFile, File, Request from sqlalchemy.orm import Session from sse_starlette.sse import EventSourceResponse from app.database import get_db from app.models import ChatHistory, AccessToken, RecruiterMessage from app.routers.auth import get_current_visitor from app.services.chat_service import process_message router = APIRouter() ANONYMOUS_MAX_QUESTIONS = 3 ANONYMOUS_SESSION_KEY = "anon_used_questions" @router.get("/api/chat/config") def get_chat_config( request: Request, db: Session = Depends(get_db), token_record: AccessToken | None = Depends(get_current_visitor), ): """Return chat config for current visitor's token or anonymous.""" if token_record: return { "max_questions": token_record.max_questions or 9, "used_questions": token_record.used_questions or 0, "anonymous": False, } # Anonymous visitor - track via session cookie return { "max_questions": ANONYMOUS_MAX_QUESTIONS, "used_questions": 0, # will be tracked client-side and verified server-side "anonymous": True, } @router.post("/api/chat") async def chat( request: Request, session_id: str = Form(...), message: str = Form(""), file: UploadFile | None = File(None), db: Session = Depends(get_db), token_record: AccessToken | None = Depends(get_current_visitor), ): if token_record: # Authenticated visitor max_q = token_record.max_questions or 9 used_q = token_record.used_questions or 0 if used_q >= max_q: async def limit_gen(): yield {"data": json.dumps({"content": f"您的提问次数已达上限({max_q}次),无法继续提问。如需更多次数,请联系管理员。", "limit_reached": True})} yield {"data": json.dumps({"content": "", "done": True})} return EventSourceResponse(limit_gen()) token_record.used_questions = used_q + 1 db.commit() else: # Anonymous visitor - check count from chat history used_q = db.query(ChatHistory).filter( ChatHistory.session_id == session_id, ChatHistory.role == "user", ).count() if used_q >= ANONYMOUS_MAX_QUESTIONS: async def limit_gen(): yield {"data": json.dumps({"content": f"匿名访问提问次数已达上限({ANONYMOUS_MAX_QUESTIONS}次)。如需更多提问次数,请向管理员获取访问令牌(Access Token)。", "limit_reached": True})} yield {"data": json.dumps({"content": "", "done": True})} return EventSourceResponse(limit_gen()) # Collect response for intent detection collected_response = [] async def event_generator(): async for chunk in process_message(session_id, message, db, file): collected_response.append(chunk) yield {"data": json.dumps({"content": chunk})} yield {"data": json.dumps({"content": "", "done": True})} # Save token info before session closes visitor_label = "匿名访问者" if token_record and token_record.note: visitor_label = token_record.note elif token_record: visitor_label = f"Token: {token_record.token[:8]}..." async def on_stream_complete(): """Run recruiter intent detection after SSE stream closes, with its own DB session.""" assistant_text = "".join(collected_response) if assistant_text and "AI服务调用失败" not in assistant_text: try: await _check_recruiter_intent(session_id, message, assistant_text, visitor_label) except Exception: pass from starlette.background import BackgroundTask return EventSourceResponse(event_generator(), background=BackgroundTask(on_stream_complete)) @router.get("/api/chat/history/{session_id}") def get_chat_history( session_id: str, db: Session = Depends(get_db), token_record: AccessToken | None = Depends(get_current_visitor), ): history = ( db.query(ChatHistory) .filter(ChatHistory.session_id == session_id) .order_by(ChatHistory.created_at) .limit(50) .all() ) return [ { "role": h.role, "content": h.content, "created_at": h.created_at.isoformat() if h.created_at else "", } for h in history ] async def _check_recruiter_intent( session_id: str, user_message: str, assistant_response: str, visitor_label: str, ): """Use LLM to detect recruiter intent and extract info. Uses its own DB session.""" from app.database import SessionLocal from app.services.llm_service import chat_completion detection_prompt = """你是一个信息提取助手。分析以下对话,判断访问者是否表达了以下任何意愿: 1. 招聘意愿(想要招聘候选人) 2. 面试意愿(想要邀请候选人面试) 3. 留下了公司信息或联系方式 如果检测到以上任何一项,请用以下JSON格式回复(不要包含其他文字): {{"detected": true, "intent": "招聘意愿/面试意愿/留下联系方式", "company": "公司名称(如有)", "contact": "联系方式(如有)", "summary": "简要描述访问者的意图和关键信息"}} 如果没有检测到,回复: {{"detected": false}} 访问者消息: {user_msg} AI助手回复: {assistant_msg}""" db = SessionLocal() try: messages = [ {"role": "system", "content": "你是一个精确的信息提取助手,只输出JSON格式。"}, {"role": "user", "content": detection_prompt.format( user_msg=user_message[:500], assistant_msg=assistant_response[:500], )}, ] result = await chat_completion(messages, db) if not result: return # Parse JSON from response import re json_match = re.search(r'\{.*\}', result, re.DOTALL) if not json_match: return data = json.loads(json_match.group()) if not data.get("detected"): return msg = RecruiterMessage( session_id=session_id, visitor_label=visitor_label, company=data.get("company", ""), contact=data.get("contact", ""), intent=data.get("intent", ""), summary=data.get("summary", ""), ) db.add(msg) db.commit() except Exception: pass # Don't break chat if detection fails finally: db.close()