|
|
@@ -171,6 +171,124 @@ 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,7 +488,8 @@ 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 的场景
|
|
|
@@ -400,8 +519,10 @@ 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:
|
|
|
@@ -421,7 +542,8 @@ async def send_deepseek_message(
|
|
|
]
|
|
|
|
|
|
raw_response = await qwen_service.chat(messages)
|
|
|
- raw_thinking, raw_answer = split_thinking_and_answer(raw_response)
|
|
|
+ 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(
|
|
|
@@ -439,8 +561,10 @@ 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] 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:
|
|
|
@@ -460,7 +584,8 @@ async def send_deepseek_message(
|
|
|
]
|
|
|
|
|
|
raw_response = await qwen_service.chat(messages)
|
|
|
- raw_thinking, raw_answer = split_thinking_and_answer(raw_response)
|
|
|
+ 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(
|
|
|
@@ -478,8 +603,10 @@ 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 == 3:
|
|
|
@@ -504,7 +631,8 @@ async def send_deepseek_message(
|
|
|
{"role": "user", "content": message},
|
|
|
]
|
|
|
|
|
|
- response_text = await qwen_service.chat(messages)
|
|
|
+ raw_response = await qwen_service.chat(messages)
|
|
|
+ response_text = _extract_exam_json_content(raw_response)
|
|
|
|
|
|
now_ts = int(time.time())
|
|
|
user_message = AIMessage(
|
|
|
@@ -543,8 +671,10 @@ 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:
|
|
|
@@ -788,7 +918,8 @@ 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
|
|
|
@@ -811,13 +942,15 @@ 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
|
|
|
@@ -1033,7 +1166,8 @@ 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 "?????????"
|
|
|
@@ -1052,8 +1186,10 @@ 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 = ""
|
|
|
@@ -1082,22 +1218,24 @@ 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
|
|
|
|
|
|
@@ -1117,7 +1255,8 @@ 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 = ""
|
|
|
@@ -1241,7 +1380,8 @@ 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)
|
|
|
|