|
|
@@ -9,6 +9,7 @@ from models.total import RecommendQuestion
|
|
|
from utils.config import settings
|
|
|
from utils.logger import logger
|
|
|
from services.qwen_service import qwen_service
|
|
|
+from services.deepseek_service import deepseek_service
|
|
|
from utils.prompt_loader import load_prompt
|
|
|
from utils.thinking_summary import split_thinking_and_answer, summarize_thinking_content
|
|
|
import time
|
|
|
@@ -171,124 +172,6 @@ def _finalize_related_questions(questions: list, content: str, limit: int = 3) -
|
|
|
return cleaned_questions[:limit]
|
|
|
|
|
|
|
|
|
-def _extract_json_object_candidates(text: str) -> list[str]:
|
|
|
- text = text or ""
|
|
|
- objects = []
|
|
|
- start = -1
|
|
|
- depth = 0
|
|
|
- in_string = False
|
|
|
- string_quote = '"'
|
|
|
- escaped = False
|
|
|
-
|
|
|
- for index, ch in enumerate(text):
|
|
|
- if escaped:
|
|
|
- escaped = False
|
|
|
- continue
|
|
|
-
|
|
|
- if in_string:
|
|
|
- if ch == "\\":
|
|
|
- escaped = True
|
|
|
- continue
|
|
|
- if ch == string_quote:
|
|
|
- in_string = False
|
|
|
- continue
|
|
|
-
|
|
|
- if ch in ('"', "'"):
|
|
|
- in_string = True
|
|
|
- string_quote = ch
|
|
|
- continue
|
|
|
-
|
|
|
- if ch == "{":
|
|
|
- if depth == 0:
|
|
|
- start = index
|
|
|
- depth += 1
|
|
|
- continue
|
|
|
-
|
|
|
- if ch == "}" and depth > 0:
|
|
|
- depth -= 1
|
|
|
- if depth == 0 and start >= 0:
|
|
|
- objects.append(text[start:index + 1])
|
|
|
- start = -1
|
|
|
-
|
|
|
- return objects
|
|
|
-
|
|
|
-
|
|
|
-def _extract_first_json_object(text: str) -> str:
|
|
|
- candidates = _extract_json_object_candidates(text)
|
|
|
- if not candidates:
|
|
|
- raise ValueError("未找到完整的JSON对象")
|
|
|
- return candidates[0]
|
|
|
-
|
|
|
-
|
|
|
-def _looks_like_exam_payload(payload) -> bool:
|
|
|
- if not isinstance(payload, dict):
|
|
|
- return False
|
|
|
- questions_root = payload.get("questions") if isinstance(
|
|
|
- payload.get("questions"), dict) else {}
|
|
|
- return any([
|
|
|
- payload.get("singleChoice"),
|
|
|
- payload.get("single_choice"),
|
|
|
- payload.get("judge"),
|
|
|
- payload.get("multiple"),
|
|
|
- payload.get("multiple_choice"),
|
|
|
- payload.get("multipleChoice"),
|
|
|
- payload.get("short"),
|
|
|
- payload.get("short_answer"),
|
|
|
- payload.get("shortAnswer"),
|
|
|
- payload.get("单选题"),
|
|
|
- payload.get("判断题"),
|
|
|
- payload.get("多选题"),
|
|
|
- payload.get("简答题"),
|
|
|
- questions_root.get("single_choice"),
|
|
|
- questions_root.get("singleChoice"),
|
|
|
- questions_root.get("judge"),
|
|
|
- questions_root.get("multiple"),
|
|
|
- questions_root.get("multiple_choice"),
|
|
|
- questions_root.get("multipleChoice"),
|
|
|
- questions_root.get("short"),
|
|
|
- questions_root.get("short_answer"),
|
|
|
- questions_root.get("shortAnswer"),
|
|
|
- questions_root.get("单选题"),
|
|
|
- questions_root.get("判断题"),
|
|
|
- questions_root.get("多选题"),
|
|
|
- questions_root.get("简答题"),
|
|
|
- ])
|
|
|
-
|
|
|
-
|
|
|
-def _extract_exam_json_content(response_text: str) -> str:
|
|
|
- raw_thinking, raw_answer = split_thinking_and_answer(response_text or "")
|
|
|
- answer_text = (raw_answer or response_text or "").strip()
|
|
|
- candidates = _extract_json_object_candidates(answer_text)
|
|
|
- if not candidates:
|
|
|
- raise ValueError("未找到完整的JSON对象")
|
|
|
-
|
|
|
- normalized_candidates = [
|
|
|
- candidate
|
|
|
- .replace("“", '"')
|
|
|
- .replace("”", '"')
|
|
|
- .replace("‘", "'")
|
|
|
- .replace("’", "'")
|
|
|
- .replace(",", ",")
|
|
|
- for candidate in candidates
|
|
|
- ]
|
|
|
-
|
|
|
- parsed_candidates = []
|
|
|
- for candidate in normalized_candidates:
|
|
|
- try:
|
|
|
- parsed_candidates.append((candidate, json.loads(candidate)))
|
|
|
- except Exception:
|
|
|
- continue
|
|
|
-
|
|
|
- if not parsed_candidates:
|
|
|
- return max(normalized_candidates, key=len)
|
|
|
-
|
|
|
- for candidate, parsed in parsed_candidates:
|
|
|
- if _looks_like_exam_payload(parsed):
|
|
|
- return candidate
|
|
|
-
|
|
|
- return max((candidate for candidate, _ in parsed_candidates), key=len)
|
|
|
-
|
|
|
-
|
|
|
def _refresh_conversation_snapshot(db: Session, conversation_id: int, user_id: int) -> None:
|
|
|
latest_message = (
|
|
|
db.query(AIMessage)
|
|
|
@@ -370,6 +253,310 @@ async def _rag_search(message: str, top_k: int = 5) -> str:
|
|
|
return ""
|
|
|
|
|
|
|
|
|
+SAFETY_TRAINING_PLAN_SYSTEM_PROMPT = """
|
|
|
+你是安全培训需求整理助手。请把用户的自然语言输入整理成安全培训PPT大纲生成任务。
|
|
|
+
|
|
|
+规则:
|
|
|
+1. 只输出一个 JSON 对象,不要输出 Markdown、解释或额外文字。
|
|
|
+2. 即使用户说“通知”“材料”“文档”,也必须理解为安全培训模块中的 PPT 大纲需求,不要切换到其他文档生成任务。
|
|
|
+3. 如果字段缺失,请根据安全培训场景合理补全,但不要编造具体制度编号、人员姓名或不存在的事实。
|
|
|
+4. template 字段用于选择大纲模板,默认填“标准安全培训PPT大纲”。
|
|
|
+5. content_focus 至少给出 3 个要点。
|
|
|
+
|
|
|
+JSON 字段:
|
|
|
+{
|
|
|
+ "topic": "培训主题",
|
|
|
+ "template": "模板名称",
|
|
|
+ "content_focus": ["内容要点1", "内容要点2", "内容要点3"],
|
|
|
+ "audience": "参训对象",
|
|
|
+ "time": "培训时间",
|
|
|
+ "location": "培训地点",
|
|
|
+ "goal": "培训目标",
|
|
|
+ "notes": "其他要求",
|
|
|
+ "normalized_request": "归一化后的安全培训PPT大纲生成需求"
|
|
|
+}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+def _extract_tag_value(message: str, tag: str) -> str:
|
|
|
+ match = re.search(fr"<{tag}>(.*?)</{tag}>", message or "", re.DOTALL)
|
|
|
+ return match.group(1).strip() if match else ""
|
|
|
+
|
|
|
+
|
|
|
+def _strip_document_tags(message: str) -> str:
|
|
|
+ text = message or ""
|
|
|
+ for tag in ("word", "filename", "filesize"):
|
|
|
+ text = re.sub(fr"<{tag}>.*?</{tag}>", " ", text, flags=re.DOTALL)
|
|
|
+ return re.sub(r"\s+", " ", text).strip()
|
|
|
+
|
|
|
+
|
|
|
+def _extract_safety_training_request_payload(message: str) -> dict:
|
|
|
+ return {
|
|
|
+ "document_content": _extract_tag_value(message, "word"),
|
|
|
+ "filename": _extract_tag_value(message, "filename"),
|
|
|
+ "filesize": _extract_tag_value(message, "filesize"),
|
|
|
+ "request": _strip_document_tags(message),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _clean_safety_training_topic(message: str) -> str:
|
|
|
+ request_text = _extract_safety_training_request_payload(message)["request"]
|
|
|
+ first_clause = re.split(r"[,。;;,\n]", request_text, maxsplit=1)[0].strip()
|
|
|
+ topic = first_clause or request_text or "安全培训"
|
|
|
+ for token in ("请", "帮我", "帮忙", "生成", "制作", "输出", "一份", "一个", "一下", "PPT大纲", "ppt大纲", "大纲", "通知", "文档", "材料"):
|
|
|
+ topic = topic.replace(token, "")
|
|
|
+ topic = re.sub(r"\s+", "", topic).strip(" ::,,。;;")
|
|
|
+ if not topic:
|
|
|
+ topic = "安全培训"
|
|
|
+ if "培训" not in topic:
|
|
|
+ topic = f"{topic}安全培训"
|
|
|
+ return topic
|
|
|
+
|
|
|
+
|
|
|
+def _parse_json_object(text: str) -> dict:
|
|
|
+ if not text:
|
|
|
+ return {}
|
|
|
+ cleaned = re.sub(r"```(?:json)?\s*", "", str(text)).replace("```", "").strip()
|
|
|
+ match = re.search(r"\{.*\}", cleaned, re.DOTALL)
|
|
|
+ if not match:
|
|
|
+ return {}
|
|
|
+ try:
|
|
|
+ parsed = json.loads(match.group(0))
|
|
|
+ return parsed if isinstance(parsed, dict) else {}
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ return {}
|
|
|
+
|
|
|
+
|
|
|
+def _build_fallback_safety_training_plan(message: str) -> dict:
|
|
|
+ topic = _clean_safety_training_topic(message)
|
|
|
+ payload = _extract_safety_training_request_payload(message)
|
|
|
+ return {
|
|
|
+ "topic": topic,
|
|
|
+ "template": "标准安全培训PPT大纲",
|
|
|
+ "content_focus": ["安全生产责任", "现场风险识别", "安全意识提升", "培训纪律与行为规范"],
|
|
|
+ "audience": "参训员工",
|
|
|
+ "time": "",
|
|
|
+ "location": "",
|
|
|
+ "goal": "提升参训人员安全意识和施工现场风险防控能力",
|
|
|
+ "notes": payload["request"],
|
|
|
+ "normalized_request": f"围绕{topic}生成安全培训PPT大纲",
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _normalize_safety_training_plan(message: str, raw_plan: dict) -> dict:
|
|
|
+ plan = _build_fallback_safety_training_plan(message)
|
|
|
+ if not isinstance(raw_plan, dict):
|
|
|
+ return plan
|
|
|
+
|
|
|
+ for key in ("topic", "template", "audience", "time", "location", "goal", "notes", "normalized_request"):
|
|
|
+ value = raw_plan.get(key)
|
|
|
+ if isinstance(value, str) and value.strip():
|
|
|
+ plan[key] = value.strip()
|
|
|
+
|
|
|
+ focus = raw_plan.get("content_focus")
|
|
|
+ if isinstance(focus, list):
|
|
|
+ normalized_focus = [str(item).strip() for item in focus if str(item).strip()]
|
|
|
+ if normalized_focus:
|
|
|
+ plan["content_focus"] = normalized_focus
|
|
|
+ elif isinstance(focus, str) and focus.strip():
|
|
|
+ plan["content_focus"] = [item.strip() for item in re.split(r"[、,,;\n]", focus) if item.strip()]
|
|
|
+
|
|
|
+ if "培训" not in plan["topic"]:
|
|
|
+ plan["topic"] = f"{plan['topic']}安全培训"
|
|
|
+ if "PPT大纲" not in plan["template"]:
|
|
|
+ plan["template"] = f"{plan['template']}PPT大纲"
|
|
|
+ return plan
|
|
|
+
|
|
|
+
|
|
|
+def _build_safety_training_generation_message(message: str, plan: dict) -> str:
|
|
|
+ payload = _extract_safety_training_request_payload(message)
|
|
|
+ focus_text = "、".join(plan.get("content_focus") or [])
|
|
|
+ lines = [
|
|
|
+ "输出类型:安全培训PPT大纲",
|
|
|
+ "请基于以下结构化需求生成安全培训PPT大纲,不要生成通知正文,不要切换到其他文档生成任务。",
|
|
|
+ f"主题:{plan.get('topic') or '安全培训'}",
|
|
|
+ f"模板:{plan.get('template') or '标准安全培训PPT大纲'}",
|
|
|
+ f"内容要点:{focus_text or '安全生产责任、风险识别、应急处置、安全意识提升'}",
|
|
|
+ f"参训对象:{plan.get('audience') or '参训员工'}",
|
|
|
+ f"培训时间:{plan.get('time') or '未指定'}",
|
|
|
+ f"培训地点:{plan.get('location') or '未指定'}",
|
|
|
+ f"培训目标:{plan.get('goal') or '提升参训人员安全意识和风险防控能力'}",
|
|
|
+ f"其他要求:{plan.get('notes') or '无'}",
|
|
|
+ f"归一化需求:{plan.get('normalized_request') or ''}",
|
|
|
+ f"原始需求:{payload['request'] or message}",
|
|
|
+ ]
|
|
|
+ if payload["filename"] or payload["document_content"]:
|
|
|
+ lines.extend([
|
|
|
+ f"上传文档名称:{payload['filename'] or '未命名文档'}",
|
|
|
+ f"上传文档大小:{payload['filesize'] or '未知'}",
|
|
|
+ "上传文档内容:",
|
|
|
+ payload["document_content"] or "无",
|
|
|
+ ])
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
+
|
|
|
+async def _infer_safety_training_plan(message: str) -> dict:
|
|
|
+ payload = _extract_safety_training_request_payload(message)
|
|
|
+ planning_input = payload["request"] or message
|
|
|
+ if payload["document_content"]:
|
|
|
+ planning_input = (
|
|
|
+ f"{planning_input}\n\n"
|
|
|
+ f"上传文档名称:{payload['filename'] or '未命名文档'}\n"
|
|
|
+ f"上传文档内容摘要:{payload['document_content'][:3000]}"
|
|
|
+ )
|
|
|
+
|
|
|
+ try:
|
|
|
+ response = await qwen_service.chat([
|
|
|
+ {"role": "system", "content": SAFETY_TRAINING_PLAN_SYSTEM_PROMPT},
|
|
|
+ {"role": "user", "content": planning_input},
|
|
|
+ ])
|
|
|
+ return _normalize_safety_training_plan(message, _parse_json_object(response))
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"[safety_training] 需求整理失败,使用兜底结构: {type(e).__name__}: {e}")
|
|
|
+ return _build_fallback_safety_training_plan(message)
|
|
|
+
|
|
|
+
|
|
|
+def _clean_ai_writing_response(content: str) -> str:
|
|
|
+ text = str(content or "").strip()
|
|
|
+ if not text:
|
|
|
+ return ""
|
|
|
+
|
|
|
+ text = re.sub(r"```(?:html)?\s*", "", text, flags=re.IGNORECASE).replace("```", "").strip()
|
|
|
+
|
|
|
+ body_match = re.search(r"<body[^>]*>(.*?)</body>", text, re.IGNORECASE | re.DOTALL)
|
|
|
+ if body_match:
|
|
|
+ text = body_match.group(1).strip()
|
|
|
+
|
|
|
+ first_content_tag = re.search(
|
|
|
+ r"<(?:article|section|main|div|h[1-6]|p|table|ul|ol)\b",
|
|
|
+ text,
|
|
|
+ re.IGNORECASE,
|
|
|
+ )
|
|
|
+ if first_content_tag and text[:first_content_tag.start()].strip():
|
|
|
+ text = text[first_content_tag.start():]
|
|
|
+
|
|
|
+ cleanup_patterns = (
|
|
|
+ r"<!DOCTYPE[^>]*>",
|
|
|
+ r"<html[^>]*>",
|
|
|
+ r"</html>",
|
|
|
+ r"<head[^>]*>.*?</head>",
|
|
|
+ r"<body[^>]*>",
|
|
|
+ r"</body>",
|
|
|
+ r"<style[^>]*>.*?</style>",
|
|
|
+ r"<script[^>]*>.*?</script>",
|
|
|
+ r"<meta[^>]*>",
|
|
|
+ r"<title[^>]*>.*?</title>",
|
|
|
+ )
|
|
|
+ for pattern in cleanup_patterns:
|
|
|
+ text = re.sub(pattern, "", text, flags=re.IGNORECASE | re.DOTALL)
|
|
|
+
|
|
|
+ return text.strip()
|
|
|
+
|
|
|
+
|
|
|
+async def _generate_ai_writing_response(message: str) -> str:
|
|
|
+ rag_context = await _rag_search(message, top_k=10)
|
|
|
+ system_content = load_prompt(
|
|
|
+ "document_writing",
|
|
|
+ userMessage=message,
|
|
|
+ contextJSON=rag_context if rag_context else "暂无相关知识库内容",
|
|
|
+ )
|
|
|
+
|
|
|
+ messages = [
|
|
|
+ {"role": "system", "content": system_content},
|
|
|
+ {
|
|
|
+ "role": "user",
|
|
|
+ "content": (
|
|
|
+ "请根据上面的写作规范和我的原始需求,直接生成可放入富文本编辑器的公文正文 HTML 片段。"
|
|
|
+ "不要输出道歉、解释、DOCTYPE、html、head、body、style 或 script 标签。\n\n"
|
|
|
+ f"原始需求:\n{message}"
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ raw_response = await deepseek_service.chat(messages)
|
|
|
+ raw_thinking, raw_answer = split_thinking_and_answer(raw_response)
|
|
|
+ answer_text = _clean_ai_writing_response(raw_answer or raw_response)
|
|
|
+ if raw_thinking:
|
|
|
+ thinking_summary = await summarize_thinking_content(
|
|
|
+ user_question=message,
|
|
|
+ raw_thinking=raw_thinking,
|
|
|
+ final_answer=answer_text,
|
|
|
+ chat_service=deepseek_service,
|
|
|
+ context="document_writing",
|
|
|
+ )
|
|
|
+ return (
|
|
|
+ f"思考过程:\n{thinking_summary}\n\n回答:\n{answer_text}"
|
|
|
+ if thinking_summary
|
|
|
+ else answer_text
|
|
|
+ )
|
|
|
+ return answer_text
|
|
|
+
|
|
|
+
|
|
|
+async def _generate_ppt_outline_response(message: str) -> str:
|
|
|
+ training_plan = await _infer_safety_training_plan(message)
|
|
|
+ generation_message = _build_safety_training_generation_message(message, training_plan)
|
|
|
+ rag_context = await _rag_search(generation_message, top_k=10)
|
|
|
+ system_content = load_prompt(
|
|
|
+ "ppt_outline",
|
|
|
+ userMessage=generation_message,
|
|
|
+ contextJSON=rag_context if rag_context else "暂无相关知识库内容",
|
|
|
+ )
|
|
|
+
|
|
|
+ messages = [
|
|
|
+ {"role": "system", "content": system_content},
|
|
|
+ {"role": "user", "content": "请直接输出安全培训PPT大纲正文,从标题开始,不要解释提示词或安全规则。"},
|
|
|
+ ]
|
|
|
+
|
|
|
+ raw_response = await qwen_service.chat(messages)
|
|
|
+ raw_thinking, raw_answer = split_thinking_and_answer(raw_response)
|
|
|
+ answer_text = raw_answer or raw_response
|
|
|
+ if raw_thinking:
|
|
|
+ thinking_summary = await summarize_thinking_content(
|
|
|
+ user_question=message,
|
|
|
+ raw_thinking=raw_thinking,
|
|
|
+ final_answer=answer_text,
|
|
|
+ chat_service=qwen_service,
|
|
|
+ context="ppt_outline",
|
|
|
+ )
|
|
|
+ return (
|
|
|
+ f"思考过程:\n{thinking_summary}\n\n回答:\n{answer_text}"
|
|
|
+ if thinking_summary
|
|
|
+ else answer_text
|
|
|
+ )
|
|
|
+ return answer_text
|
|
|
+
|
|
|
+
|
|
|
+def _persist_message_pair(db: Session, conv_id: int, user, user_content: str, ai_content: str):
|
|
|
+ now_ts = int(time.time())
|
|
|
+ user_message = AIMessage(
|
|
|
+ ai_conversation_id=conv_id,
|
|
|
+ user_id=user.user_id,
|
|
|
+ type="user",
|
|
|
+ content=user_content,
|
|
|
+ created_at=now_ts,
|
|
|
+ updated_at=now_ts,
|
|
|
+ is_deleted=0,
|
|
|
+ )
|
|
|
+ db.add(user_message)
|
|
|
+ db.commit()
|
|
|
+ db.refresh(user_message)
|
|
|
+
|
|
|
+ ai_message = AIMessage(
|
|
|
+ ai_conversation_id=conv_id,
|
|
|
+ user_id=user.user_id,
|
|
|
+ type="ai",
|
|
|
+ content=ai_content,
|
|
|
+ prev_user_id=user_message.id,
|
|
|
+ created_at=now_ts,
|
|
|
+ updated_at=now_ts,
|
|
|
+ is_deleted=0,
|
|
|
+ )
|
|
|
+ db.add(ai_message)
|
|
|
+ db.commit()
|
|
|
+ db.refresh(ai_message)
|
|
|
+ return user_message, ai_message
|
|
|
+
|
|
|
+
|
|
|
def _build_history_messages(conv_id: int, limit: int = 10) -> list:
|
|
|
"""从数据库读取最近对话历史,构建 messages 列表"""
|
|
|
db = SessionLocal()
|
|
|
@@ -460,6 +647,7 @@ async def send_deepseek_message(
|
|
|
db.commit()
|
|
|
|
|
|
response_text = ""
|
|
|
+ ai_message_id = 0
|
|
|
|
|
|
if data.business_type == 0:
|
|
|
# AI问答:意图识别 + RAG
|
|
|
@@ -488,8 +676,7 @@ async def send_deepseek_message(
|
|
|
]
|
|
|
|
|
|
qwen_response = await qwen_service.chat(messages)
|
|
|
- raw_thinking, raw_answer = split_thinking_and_answer(
|
|
|
- qwen_response)
|
|
|
+ raw_thinking, raw_answer = split_thinking_and_answer(qwen_response)
|
|
|
answer_source = raw_answer or qwen_response
|
|
|
|
|
|
# 兼容模型直接返回 JSON 的场景
|
|
|
@@ -519,94 +706,76 @@ async def send_deepseek_message(
|
|
|
else:
|
|
|
response_text = answer_text
|
|
|
except Exception as e:
|
|
|
- error_detail = str(e).strip() if str(
|
|
|
- e).strip() else f"未知错误({type(e).__name__})"
|
|
|
- logger.error(
|
|
|
- f"[send_deepseek_message] AI问答异常: {type(e).__name__}: {error_detail}")
|
|
|
+ error_detail = str(e).strip() if str(e).strip() else f"未知错误({type(e).__name__})"
|
|
|
+ logger.error(f"[send_deepseek_message] AI问答异常: {type(e).__name__}: {error_detail}")
|
|
|
response_text = f"处理失败: {error_detail}"
|
|
|
|
|
|
elif data.business_type == 1:
|
|
|
# PPT大纲生成
|
|
|
try:
|
|
|
- rag_context = await _rag_search(message, top_k=10)
|
|
|
-
|
|
|
- # 使用prompt加载器加载PPT大纲生成prompt
|
|
|
- system_content = load_prompt(
|
|
|
- "ppt_outline",
|
|
|
- userMessage=message,
|
|
|
- contextJSON=rag_context if rag_context else "暂无相关知识库内容"
|
|
|
+ response_text = await _generate_ppt_outline_response(message)
|
|
|
+ _, ai_message = _persist_message_pair(
|
|
|
+ db=db,
|
|
|
+ conv_id=conv_id,
|
|
|
+ user=user,
|
|
|
+ user_content=message,
|
|
|
+ ai_content=response_text,
|
|
|
)
|
|
|
-
|
|
|
- messages = [
|
|
|
- {"role": "user", "content": system_content},
|
|
|
- ]
|
|
|
-
|
|
|
- raw_response = await qwen_service.chat(messages)
|
|
|
- raw_thinking, raw_answer = split_thinking_and_answer(
|
|
|
- raw_response)
|
|
|
- answer_text = raw_answer or raw_response
|
|
|
- if raw_thinking:
|
|
|
- thinking_summary = await summarize_thinking_content(
|
|
|
- user_question=message,
|
|
|
- raw_thinking=raw_thinking,
|
|
|
- final_answer=answer_text,
|
|
|
- chat_service=qwen_service,
|
|
|
- context="ppt_outline",
|
|
|
- )
|
|
|
- response_text = (
|
|
|
- f"思考过程:\n{thinking_summary}\n\n回答:\n{answer_text}"
|
|
|
- if thinking_summary
|
|
|
- else answer_text
|
|
|
- )
|
|
|
- else:
|
|
|
- response_text = answer_text
|
|
|
+ ai_message_id = ai_message.id
|
|
|
+ _refresh_conversation_snapshot(db, conv_id, user.user_id)
|
|
|
+ db.commit()
|
|
|
+ return {
|
|
|
+ "statusCode": 200,
|
|
|
+ "msg": "success",
|
|
|
+ "data": {
|
|
|
+ "conversation_id": conv_id,
|
|
|
+ "ai_conversation_id": conv_id,
|
|
|
+ "response": response_text,
|
|
|
+ "reply": response_text,
|
|
|
+ "content": response_text,
|
|
|
+ "message": response_text,
|
|
|
+ "ai_message_id": ai_message_id,
|
|
|
+ "user_id": user.user_id,
|
|
|
+ "business_type": data.business_type,
|
|
|
+ },
|
|
|
+ }
|
|
|
except Exception as e:
|
|
|
- error_detail = str(e).strip() if str(
|
|
|
- e).strip() else f"未知错误({type(e).__name__})"
|
|
|
- logger.error(
|
|
|
- f"[send_deepseek_message] PPT大纲生成异常: {type(e).__name__}: {error_detail}")
|
|
|
+ error_detail = str(e).strip() if str(e).strip() else f"未知错误({type(e).__name__})"
|
|
|
+ logger.error(f"[send_deepseek_message] PPT大纲生成异常: {type(e).__name__}: {error_detail}")
|
|
|
response_text = f"处理失败: {error_detail}"
|
|
|
|
|
|
elif data.business_type == 2:
|
|
|
# AI写作
|
|
|
try:
|
|
|
- rag_context = await _rag_search(message, top_k=10)
|
|
|
-
|
|
|
- # 使用prompt加载器加载公文写作prompt
|
|
|
- system_content = load_prompt(
|
|
|
- "document_writing",
|
|
|
- userMessage=message,
|
|
|
- contextJSON=rag_context if rag_context else "暂无相关知识库内容"
|
|
|
+ response_text = await _generate_ai_writing_response(message)
|
|
|
+ _, ai_message = _persist_message_pair(
|
|
|
+ db=db,
|
|
|
+ conv_id=conv_id,
|
|
|
+ user=user,
|
|
|
+ user_content=message,
|
|
|
+ ai_content=response_text,
|
|
|
)
|
|
|
-
|
|
|
- messages = [
|
|
|
- {"role": "user", "content": system_content},
|
|
|
- ]
|
|
|
-
|
|
|
- raw_response = await qwen_service.chat(messages)
|
|
|
- raw_thinking, raw_answer = split_thinking_and_answer(
|
|
|
- raw_response)
|
|
|
- answer_text = raw_answer or raw_response
|
|
|
- if raw_thinking:
|
|
|
- thinking_summary = await summarize_thinking_content(
|
|
|
- user_question=message,
|
|
|
- raw_thinking=raw_thinking,
|
|
|
- final_answer=answer_text,
|
|
|
- chat_service=qwen_service,
|
|
|
- context="document_writing",
|
|
|
- )
|
|
|
- response_text = (
|
|
|
- f"思考过程:\n{thinking_summary}\n\n回答:\n{answer_text}"
|
|
|
- if thinking_summary
|
|
|
- else answer_text
|
|
|
- )
|
|
|
- else:
|
|
|
- response_text = answer_text
|
|
|
+ ai_message_id = ai_message.id
|
|
|
+ _refresh_conversation_snapshot(db, conv_id, user.user_id)
|
|
|
+ db.commit()
|
|
|
+ return {
|
|
|
+ "statusCode": 200,
|
|
|
+ "msg": "success",
|
|
|
+ "data": {
|
|
|
+ "conversation_id": conv_id,
|
|
|
+ "ai_conversation_id": conv_id,
|
|
|
+ "response": response_text,
|
|
|
+ "reply": response_text,
|
|
|
+ "content": response_text,
|
|
|
+ "message": response_text,
|
|
|
+ "ai_message_id": ai_message_id,
|
|
|
+ "user_id": user.user_id,
|
|
|
+ "business_type": data.business_type,
|
|
|
+ },
|
|
|
+ }
|
|
|
except Exception as e:
|
|
|
- error_detail = str(e).strip() if str(
|
|
|
- e).strip() else f"未知错误({type(e).__name__})"
|
|
|
- logger.error(
|
|
|
- f"[send_deepseek_message] AI写作异常: {type(e).__name__}: {error_detail}")
|
|
|
+ error_detail = str(e).strip() if str(e).strip() else f"未知错误({type(e).__name__})"
|
|
|
+ logger.error(f"[send_deepseek_message] AI写作异常: {type(e).__name__}: {error_detail}")
|
|
|
response_text = f"处理失败: {error_detail}"
|
|
|
|
|
|
elif data.business_type == 3:
|
|
|
@@ -631,8 +800,7 @@ async def send_deepseek_message(
|
|
|
{"role": "user", "content": message},
|
|
|
]
|
|
|
|
|
|
- raw_response = await qwen_service.chat(messages)
|
|
|
- response_text = _extract_exam_json_content(raw_response)
|
|
|
+ response_text = await qwen_service.chat(messages)
|
|
|
|
|
|
now_ts = int(time.time())
|
|
|
user_message = AIMessage(
|
|
|
@@ -671,10 +839,8 @@ async def send_deepseek_message(
|
|
|
)
|
|
|
db.commit()
|
|
|
except Exception as e:
|
|
|
- error_detail = str(e).strip() if str(
|
|
|
- e).strip() else f"未知错误({type(e).__name__})"
|
|
|
- logger.error(
|
|
|
- f"[send_deepseek_message] 考试工坊异常: {type(e).__name__}: {error_detail}")
|
|
|
+ error_detail = str(e).strip() if str(e).strip() else f"未知错误({type(e).__name__})"
|
|
|
+ logger.error(f"[send_deepseek_message] 考试工坊异常: {type(e).__name__}: {error_detail}")
|
|
|
response_text = f"处理失败: {error_detail}"
|
|
|
|
|
|
else:
|
|
|
@@ -918,8 +1084,7 @@ async def stream_chat(request: Request, data: StreamChatRequest):
|
|
|
thinking_buf = ""
|
|
|
in_think = False
|
|
|
thinking_done = False
|
|
|
- max_input_chars = getattr(
|
|
|
- settings.thinking_summary, "max_input_chars", 1500)
|
|
|
+ max_input_chars = getattr(settings.thinking_summary, "max_input_chars", 1500)
|
|
|
|
|
|
async for chunk in qwen_service.stream_chat(messages):
|
|
|
buffer += chunk
|
|
|
@@ -942,15 +1107,13 @@ async def stream_chat(request: Request, data: StreamChatRequest):
|
|
|
end_idx = lower.find("</think>")
|
|
|
if end_idx == -1:
|
|
|
if max_input_chars and len(thinking_buf) < max_input_chars:
|
|
|
- thinking_buf += buffer[: max_input_chars -
|
|
|
- len(thinking_buf)]
|
|
|
+ thinking_buf += buffer[: max_input_chars - len(thinking_buf)]
|
|
|
buffer = ""
|
|
|
break
|
|
|
|
|
|
if max_input_chars and len(thinking_buf) < max_input_chars:
|
|
|
thinking_part = buffer[:end_idx]
|
|
|
- thinking_buf += thinking_part[: max_input_chars - len(
|
|
|
- thinking_buf)]
|
|
|
+ thinking_buf += thinking_part[: max_input_chars - len(thinking_buf)]
|
|
|
|
|
|
buffer = buffer[end_idx + len("</think>"):]
|
|
|
in_think = False
|
|
|
@@ -1166,8 +1329,7 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
|
|
|
if rag_context:
|
|
|
context_parts.append(f"??????\n{rag_context}")
|
|
|
if data.online_search_content:
|
|
|
- context_parts.append(
|
|
|
- f"???????\n{data.online_search_content}")
|
|
|
+ context_parts.append(f"???????\n{data.online_search_content}")
|
|
|
|
|
|
context_json = "\n\n".join(
|
|
|
context_parts) if context_parts else "?????????"
|
|
|
@@ -1186,10 +1348,8 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
|
|
|
# 8. 流式输出并收集完整回复
|
|
|
full_response = ""
|
|
|
try:
|
|
|
- summary_enabled = getattr(
|
|
|
- settings.thinking_summary, "enabled", True)
|
|
|
- max_input_chars = getattr(
|
|
|
- settings.thinking_summary, "max_input_chars", 1500)
|
|
|
+ summary_enabled = getattr(settings.thinking_summary, "enabled", True)
|
|
|
+ max_input_chars = getattr(settings.thinking_summary, "max_input_chars", 1500)
|
|
|
|
|
|
buffer = ""
|
|
|
pre_answer = ""
|
|
|
@@ -1218,24 +1378,22 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
|
|
|
break
|
|
|
|
|
|
pre_answer += buffer[:start_idx]
|
|
|
- buffer = buffer[start_idx + len("<think>"):]
|
|
|
+ buffer = buffer[start_idx + len("<think>") :]
|
|
|
in_think = True
|
|
|
continue
|
|
|
|
|
|
end_idx = lower.find("</think>")
|
|
|
if end_idx == -1:
|
|
|
if max_input_chars and len(thinking_buf) < max_input_chars:
|
|
|
- thinking_buf += buffer[: max_input_chars -
|
|
|
- len(thinking_buf)]
|
|
|
+ thinking_buf += buffer[: max_input_chars - len(thinking_buf)]
|
|
|
buffer = ""
|
|
|
break
|
|
|
|
|
|
if max_input_chars and len(thinking_buf) < max_input_chars:
|
|
|
thinking_part = buffer[:end_idx]
|
|
|
- thinking_buf += thinking_part[: max_input_chars - len(
|
|
|
- thinking_buf)]
|
|
|
+ thinking_buf += thinking_part[: max_input_chars - len(thinking_buf)]
|
|
|
|
|
|
- buffer = buffer[end_idx + len("</think>"):]
|
|
|
+ buffer = buffer[end_idx + len("</think>") :]
|
|
|
in_think = False
|
|
|
thinking_done = True
|
|
|
|
|
|
@@ -1255,8 +1413,7 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
|
|
|
answer_chunk = (pre_answer + buffer).lstrip()
|
|
|
if answer_chunk:
|
|
|
full_response += answer_chunk
|
|
|
- escaped_answer = answer_chunk.replace(
|
|
|
- '\n', '\\n')
|
|
|
+ escaped_answer = answer_chunk.replace('\n', '\\n')
|
|
|
yield f"data: {escaped_answer}\n\n"
|
|
|
|
|
|
pre_answer = ""
|
|
|
@@ -1380,8 +1537,7 @@ async def guess_you_want(
|
|
|
if not questions:
|
|
|
questions = ["该话题的具体应用场景?", "有哪些注意事项?", "相关案例分析?"]
|
|
|
|
|
|
- questions = _finalize_related_questions(
|
|
|
- questions, ai_msg.content, limit=3)
|
|
|
+ questions = _finalize_related_questions(questions, ai_msg.content, limit=3)
|
|
|
|
|
|
guess_json = json.dumps({"questions": questions}, ensure_ascii=False)
|
|
|
|