|
@@ -1,15 +1,34 @@
|
|
|
# -*- coding: utf-8 -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
-"""LangGraph workflow for document chat."""
|
|
|
|
|
|
|
+"""基于 LangGraph 的文档 AI 对话工作流。
|
|
|
|
|
+
|
|
|
|
|
+工作流节点及路由:
|
|
|
|
|
+ validate_input → 校验用户输入(user_id、message、selected_section)
|
|
|
|
|
+ ├─ general(无选中章节)→ general_answer(通用 LLM 回答)
|
|
|
|
|
+ └─ normal(有选中章节)→ load_context → load_skill_registry → recognize_intent
|
|
|
|
|
+ → route_intent
|
|
|
|
|
+ ├─ clarify(需补充说明)→ clarify_node → complete
|
|
|
|
|
+ ├─ unsupported(不支持的意图)→ unsupported_node → complete
|
|
|
|
|
+ ├─ answer(章节问答)→ build_retrieval_query → vector_recall
|
|
|
|
|
+ │ → rerank_context → quality_gate → run_answer_skill → complete
|
|
|
|
|
+ └─ modify(章节修改)→ build_retrieval_query → vector_recall
|
|
|
|
|
+ → rerank_context → quality_gate → run_modify_skill → complete
|
|
|
|
|
+
|
|
|
|
|
+检索阶段(RAG 链路):
|
|
|
|
|
+ build_retrieval_query:拼接用户输入 + 章节标题 + 历史对话为检索 query
|
|
|
|
|
+ vector_recall:多路召回(parent_vector / child_locator / tag / chapter_similarity)+ RRF 融合
|
|
|
|
|
+ rerank_context:调用重排模型对候选打分排序
|
|
|
|
|
+ quality_gate:按 min_rerank_score 阈值过滤低质量参考
|
|
|
|
|
+"""
|
|
|
|
|
|
|
|
import uuid
|
|
import uuid
|
|
|
from typing import Any, Dict, List, Optional
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
from langgraph.graph import END, StateGraph
|
|
from langgraph.graph import END, StateGraph
|
|
|
|
|
|
|
|
-from foundation.observability.logger.loggering import write_logger as logger
|
|
|
|
|
|
|
+from core.document_chat.component.document_chat_logger import document_chat_logger as logger
|
|
|
|
|
+from core.document_chat.component.document_chat_logger import log_document_chat_event, log_document_chat_event_truncated
|
|
|
|
|
|
|
|
from core.document_chat.component.conversation_context import ConversationContextBuilder
|
|
from core.document_chat.component.conversation_context import ConversationContextBuilder
|
|
|
-from core.document_chat.component.document_chat_logger import log_document_chat_event
|
|
|
|
|
from core.document_chat.component.intent_recognizer import IntentRecognizer
|
|
from core.document_chat.component.intent_recognizer import IntentRecognizer
|
|
|
from core.document_chat.component.rerank_service import DocumentChatRerankService
|
|
from core.document_chat.component.rerank_service import DocumentChatRerankService
|
|
|
from core.document_chat.component.retrieval_quality_gate import RetrievalQualityGate
|
|
from core.document_chat.component.retrieval_quality_gate import RetrievalQualityGate
|
|
@@ -29,7 +48,15 @@ from core.document_chat.schemas import (
|
|
|
|
|
|
|
|
|
|
|
|
|
class DocumentChatWorkflow:
|
|
class DocumentChatWorkflow:
|
|
|
- """Document chat workflow built with LangGraph."""
|
|
|
|
|
|
|
+ """施工方案文档 AI 对话的 LangGraph 工作流。
|
|
|
|
|
+
|
|
|
|
|
+ 核心职责:
|
|
|
|
|
+ - 接收前端请求,校验输入参数
|
|
|
|
|
+ - 通过 LLM 意图识别判断用户是想"问答"还是"修改"当前章节
|
|
|
|
|
+ - 对章节问答/修改走 RAG 检索链路(召回 → 重排 → 质量门)
|
|
|
|
|
+ - 调用对应技能(document-answer 或 document-modify)生成回答/草案
|
|
|
|
|
+ - 统一组装响应数据返回
|
|
|
|
|
+ """
|
|
|
|
|
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
|
self.intent_recognizer = IntentRecognizer()
|
|
self.intent_recognizer = IntentRecognizer()
|
|
@@ -41,7 +68,10 @@ class DocumentChatWorkflow:
|
|
|
self.graph = None
|
|
self.graph = None
|
|
|
|
|
|
|
|
def build_graph(self):
|
|
def build_graph(self):
|
|
|
|
|
+ """构建 LangGraph 状态图,定义节点和边。"""
|
|
|
workflow = StateGraph(DocumentChatState)
|
|
workflow = StateGraph(DocumentChatState)
|
|
|
|
|
+
|
|
|
|
|
+ # ===== 注册所有节点 =====
|
|
|
workflow.add_node("validate_input", self.validate_input_node)
|
|
workflow.add_node("validate_input", self.validate_input_node)
|
|
|
workflow.add_node("load_context", self.load_context_node)
|
|
workflow.add_node("load_context", self.load_context_node)
|
|
|
workflow.add_node("load_skill_registry", self.load_skill_registry_node)
|
|
workflow.add_node("load_skill_registry", self.load_skill_registry_node)
|
|
@@ -59,7 +89,10 @@ class DocumentChatWorkflow:
|
|
|
workflow.add_node("error_handler", self.error_handler_node)
|
|
workflow.add_node("error_handler", self.error_handler_node)
|
|
|
workflow.add_node("complete", self.complete_node)
|
|
workflow.add_node("complete", self.complete_node)
|
|
|
|
|
|
|
|
|
|
+ # ===== 定义执行流程 =====
|
|
|
workflow.set_entry_point("validate_input")
|
|
workflow.set_entry_point("validate_input")
|
|
|
|
|
+
|
|
|
|
|
+ # 入口分流:有选中章节走 normal,无选中章节走 general 通用回答
|
|
|
workflow.add_conditional_edges(
|
|
workflow.add_conditional_edges(
|
|
|
"validate_input",
|
|
"validate_input",
|
|
|
self.route_after_validate,
|
|
self.route_after_validate,
|
|
@@ -72,6 +105,8 @@ class DocumentChatWorkflow:
|
|
|
workflow.add_edge("load_context", "load_skill_registry")
|
|
workflow.add_edge("load_context", "load_skill_registry")
|
|
|
workflow.add_edge("load_skill_registry", "recognize_intent")
|
|
workflow.add_edge("load_skill_registry", "recognize_intent")
|
|
|
workflow.add_edge("recognize_intent", "route_intent")
|
|
workflow.add_edge("recognize_intent", "route_intent")
|
|
|
|
|
+
|
|
|
|
|
+ # 意图分流:clarify / unsupported / answer(问答) / modify(修改)
|
|
|
workflow.add_conditional_edges(
|
|
workflow.add_conditional_edges(
|
|
|
"route_intent",
|
|
"route_intent",
|
|
|
self.route_intent,
|
|
self.route_intent,
|
|
@@ -83,9 +118,13 @@ class DocumentChatWorkflow:
|
|
|
"error": "error_handler",
|
|
"error": "error_handler",
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ # RAG 检索链路:检索 → 重排 → 质量门
|
|
|
workflow.add_edge("build_retrieval_query", "vector_recall")
|
|
workflow.add_edge("build_retrieval_query", "vector_recall")
|
|
|
workflow.add_edge("vector_recall", "rerank_context")
|
|
workflow.add_edge("vector_recall", "rerank_context")
|
|
|
workflow.add_edge("rerank_context", "quality_gate")
|
|
workflow.add_edge("rerank_context", "quality_gate")
|
|
|
|
|
+
|
|
|
|
|
+ # 检索后分流:按意图类型调用对应技能
|
|
|
workflow.add_conditional_edges(
|
|
workflow.add_conditional_edges(
|
|
|
"quality_gate",
|
|
"quality_gate",
|
|
|
self.route_after_retrieval,
|
|
self.route_after_retrieval,
|
|
@@ -95,6 +134,8 @@ class DocumentChatWorkflow:
|
|
|
"error": "error_handler",
|
|
"error": "error_handler",
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ # 终端节点统一汇入 complete
|
|
|
workflow.add_edge("clarify", "complete")
|
|
workflow.add_edge("clarify", "complete")
|
|
|
workflow.add_edge("unsupported", "complete")
|
|
workflow.add_edge("unsupported", "complete")
|
|
|
workflow.add_edge("run_answer_skill", "complete")
|
|
workflow.add_edge("run_answer_skill", "complete")
|
|
@@ -105,11 +146,13 @@ class DocumentChatWorkflow:
|
|
|
return workflow.compile()
|
|
return workflow.compile()
|
|
|
|
|
|
|
|
def get_graph(self):
|
|
def get_graph(self):
|
|
|
|
|
+ """获取编译后的图,懒加载只构建一次。"""
|
|
|
if self.graph is None:
|
|
if self.graph is None:
|
|
|
self.graph = self.build_graph()
|
|
self.graph = self.build_graph()
|
|
|
return self.graph
|
|
return self.graph
|
|
|
|
|
|
|
|
def build_initial_state(self, request: DocumentChatRequest, callback_task_id: Optional[str] = None) -> DocumentChatState:
|
|
def build_initial_state(self, request: DocumentChatRequest, callback_task_id: Optional[str] = None) -> DocumentChatState:
|
|
|
|
|
+ """根据 HTTP 请求构建初始工作流状态。"""
|
|
|
task_id = callback_task_id or f"doc_chat_{uuid.uuid4().hex[:12]}"
|
|
task_id = callback_task_id or f"doc_chat_{uuid.uuid4().hex[:12]}"
|
|
|
return {
|
|
return {
|
|
|
"callback_task_id": task_id,
|
|
"callback_task_id": task_id,
|
|
@@ -123,6 +166,8 @@ class DocumentChatWorkflow:
|
|
|
"user_message": request.message,
|
|
"user_message": request.message,
|
|
|
"skill_registry": [],
|
|
"skill_registry": [],
|
|
|
"retrieval_query": None,
|
|
"retrieval_query": None,
|
|
|
|
|
+ "retrieval_keywords": [],
|
|
|
|
|
+ "retrieval_steps": [],
|
|
|
"retrieval_method": None,
|
|
"retrieval_method": None,
|
|
|
"retrieval_candidates": [],
|
|
"retrieval_candidates": [],
|
|
|
"reranked_references": [],
|
|
"reranked_references": [],
|
|
@@ -140,15 +185,23 @@ class DocumentChatWorkflow:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async def run(self, request: DocumentChatRequest, callback_task_id: Optional[str] = None) -> DocumentChatState:
|
|
async def run(self, request: DocumentChatRequest, callback_task_id: Optional[str] = None) -> DocumentChatState:
|
|
|
|
|
+ """执行工作流,返回最终状态。用于非 SSE 同步调用。"""
|
|
|
initial_state = self.build_initial_state(request, callback_task_id)
|
|
initial_state = self.build_initial_state(request, callback_task_id)
|
|
|
return await self.get_graph().ainvoke(initial_state)
|
|
return await self.get_graph().ainvoke(initial_state)
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:validate_input — 输入校验
|
|
|
|
|
+ # ============================================================
|
|
|
async def validate_input_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def validate_input_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """校验必填字段,确保 selected_section 包含 content 键。"""
|
|
|
try:
|
|
try:
|
|
|
selected_section = state.get("selected_section") or {}
|
|
selected_section = state.get("selected_section") or {}
|
|
|
user_message = (state.get("user_message") or "").strip()
|
|
user_message = (state.get("user_message") or "").strip()
|
|
|
|
|
+ if not state.get("user_id"):
|
|
|
|
|
+ raise ValueError("user_id is required")
|
|
|
if not user_message:
|
|
if not user_message:
|
|
|
raise ValueError("message is required")
|
|
raise ValueError("message is required")
|
|
|
|
|
+ # 保证后续检索和检索 query 构建时 content 键一定存在
|
|
|
if "content" not in selected_section:
|
|
if "content" not in selected_section:
|
|
|
selected_section["content"] = ""
|
|
selected_section["content"] = ""
|
|
|
return {
|
|
return {
|
|
@@ -160,6 +213,7 @@ class DocumentChatWorkflow:
|
|
|
return self._error_update("validate_input", exc)
|
|
return self._error_update("validate_input", exc)
|
|
|
|
|
|
|
|
def route_after_validate(self, state: DocumentChatState) -> str:
|
|
def route_after_validate(self, state: DocumentChatState) -> str:
|
|
|
|
|
+ """入口路由决策:有章节信息 → normal,无 → general(通用回答)。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return "error"
|
|
return "error"
|
|
|
selected_section = state.get("selected_section") or {}
|
|
selected_section = state.get("selected_section") or {}
|
|
@@ -168,9 +222,15 @@ class DocumentChatWorkflow:
|
|
|
or selected_section.get("chapter_level_1")
|
|
or selected_section.get("chapter_level_1")
|
|
|
or selected_section.get("chapter_level_2")
|
|
or selected_section.get("chapter_level_2")
|
|
|
)
|
|
)
|
|
|
- return "normal" if has_section else "general"
|
|
|
|
|
|
|
+ route = "normal" if has_section else "general"
|
|
|
|
|
+ logger.info(f"[DocumentChat] route_after_validate: route={route}, code={selected_section.get('code')}, level1={selected_section.get('chapter_level_1')}, level2={selected_section.get('chapter_level_2')}")
|
|
|
|
|
+ return route
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:load_context — 加载上下文(项目信息、章节、历史对话)
|
|
|
|
|
+ # ============================================================
|
|
|
async def load_context_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def load_context_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """构建完整的对话上下文,包含项目信息、选中章节、前后文片段、历史对话。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
context = self.context_builder.build(state)
|
|
context = self.context_builder.build(state)
|
|
@@ -182,7 +242,11 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "load_context",
|
|
"current_stage": "load_context",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:load_skill_registry — 加载技能注册表
|
|
|
|
|
+ # ============================================================
|
|
|
async def load_skill_registry_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def load_skill_registry_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """加载可用技能列表,供意图识别器作为参考。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
return {
|
|
return {
|
|
@@ -190,11 +254,16 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "load_skill_registry",
|
|
"current_stage": "load_skill_registry",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:recognize_intent — LLM 意图识别
|
|
|
|
|
+ # ============================================================
|
|
|
async def recognize_intent_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def recognize_intent_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """调用 LLM 分析用户输入,识别是问答(document-answer)还是修改(document-modify)意图。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
try:
|
|
try:
|
|
|
intent_result = await self.intent_recognizer.recognize(state)
|
|
intent_result = await self.intent_recognizer.recognize(state)
|
|
|
|
|
+ logger.info(f"[DocumentChat] intent recognized: intent={intent_result.intent}, skill={intent_result.skill_name}, confidence={intent_result.confidence}, operation={intent_result.operation}")
|
|
|
return {
|
|
return {
|
|
|
"intent_result": model_to_dict(intent_result),
|
|
"intent_result": model_to_dict(intent_result),
|
|
|
"current_stage": "recognize_intent",
|
|
"current_stage": "recognize_intent",
|
|
@@ -202,10 +271,22 @@ class DocumentChatWorkflow:
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
return self._error_update("recognize_intent", exc)
|
|
return self._error_update("recognize_intent", exc)
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:route_intent — 空节点,仅标记阶段
|
|
|
|
|
+ # ============================================================
|
|
|
async def route_intent_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def route_intent_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """空节点,仅用于 SSE 流中标记已进入路由阶段。"""
|
|
|
return {"current_stage": "route_intent"}
|
|
return {"current_stage": "route_intent"}
|
|
|
|
|
|
|
|
def route_intent(self, state: DocumentChatState) -> str:
|
|
def route_intent(self, state: DocumentChatState) -> str:
|
|
|
|
|
+ """根据意图识别结果路由到对应分支。
|
|
|
|
|
+
|
|
|
|
|
+ 路由规则:
|
|
|
|
|
+ - 需要补充说明 / 置信度 < 0.65 → clarify
|
|
|
|
|
+ - skill=document-answer → answer
|
|
|
|
|
+ - skill=document-modify → modify
|
|
|
|
|
+ - 不支持的意图 → unsupported
|
|
|
|
|
+ """
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return "error"
|
|
return "error"
|
|
|
intent_data = state.get("intent_result") or {}
|
|
intent_data = state.get("intent_result") or {}
|
|
@@ -224,6 +305,7 @@ class DocumentChatWorkflow:
|
|
|
return "error"
|
|
return "error"
|
|
|
|
|
|
|
|
def route_after_retrieval(self, state: DocumentChatState) -> str:
|
|
def route_after_retrieval(self, state: DocumentChatState) -> str:
|
|
|
|
|
+ """检索完成后按意图类型路由到对应技能节点。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return "error"
|
|
return "error"
|
|
|
intent_data = state.get("intent_result") or {}
|
|
intent_data = state.get("intent_result") or {}
|
|
@@ -234,35 +316,46 @@ class DocumentChatWorkflow:
|
|
|
return "modify"
|
|
return "modify"
|
|
|
return "error"
|
|
return "error"
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:build_retrieval_query — 构建检索查询
|
|
|
|
|
+ # ============================================================
|
|
|
async def build_retrieval_query_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def build_retrieval_query_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """将用户输入、章节标题、历史对话等拼接为检索 query 和关键词。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
query = self.retrieval_service.build_query(state)
|
|
query = self.retrieval_service.build_query(state)
|
|
|
- log_document_chat_event(
|
|
|
|
|
|
|
+ keywords = self.retrieval_service.build_query_keywords(state, query)
|
|
|
|
|
+ log_document_chat_event_truncated(
|
|
|
"rag_query_built",
|
|
"rag_query_built",
|
|
|
state.get("callback_task_id", ""),
|
|
state.get("callback_task_id", ""),
|
|
|
{
|
|
{
|
|
|
"retrieval_query": query,
|
|
"retrieval_query": query,
|
|
|
- "intent_result": state.get("intent_result"),
|
|
|
|
|
- "selected_section": state.get("selected_section"),
|
|
|
|
|
- "project_info": state.get("project_info"),
|
|
|
|
|
- "document_context": state.get("document_context"),
|
|
|
|
|
|
|
+ "retrieval_keywords": keywords,
|
|
|
|
|
+ "intent_result": {"skill_name": (state.get("intent_result") or {}).get("skill_name")},
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
return {
|
|
return {
|
|
|
"retrieval_query": query,
|
|
"retrieval_query": query,
|
|
|
|
|
+ "retrieval_keywords": keywords,
|
|
|
"current_stage": "build_retrieval_query",
|
|
"current_stage": "build_retrieval_query",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:vector_recall — 多路向量召回
|
|
|
|
|
+ # ============================================================
|
|
|
async def vector_recall_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def vector_recall_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """执行多路向量检索,合并候选结果。支持 parent_vector、child_locator、
|
|
|
|
|
+ 标签关键词、章节相似度四条召回路径。
|
|
|
|
|
+ """
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
result = self.retrieval_service.recall(state)
|
|
result = self.retrieval_service.recall(state)
|
|
|
- log_document_chat_event(
|
|
|
|
|
|
|
+ log_document_chat_event_truncated(
|
|
|
"rag_recall_completed",
|
|
"rag_recall_completed",
|
|
|
state.get("callback_task_id", ""),
|
|
state.get("callback_task_id", ""),
|
|
|
{
|
|
{
|
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
|
|
|
+ "retrieval_keywords": state.get("retrieval_keywords") or [],
|
|
|
"retrieval_method": result.get("retrieval_method"),
|
|
"retrieval_method": result.get("retrieval_method"),
|
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
|
"retrieval_metrics": result.get("retrieval_metrics") or {},
|
|
"retrieval_metrics": result.get("retrieval_metrics") or {},
|
|
@@ -272,6 +365,7 @@ class DocumentChatWorkflow:
|
|
|
)
|
|
)
|
|
|
return {
|
|
return {
|
|
|
"retrieval_candidates": result.get("retrieval_candidates") or [],
|
|
"retrieval_candidates": result.get("retrieval_candidates") or [],
|
|
|
|
|
+ "retrieval_steps": result.get("retrieval_steps") or [],
|
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
|
"retrieval_method": result.get("retrieval_method"),
|
|
"retrieval_method": result.get("retrieval_method"),
|
|
|
"retrieval_metrics": self._merge_metrics(state, result.get("retrieval_metrics") or {}),
|
|
"retrieval_metrics": self._merge_metrics(state, result.get("retrieval_metrics") or {}),
|
|
@@ -279,7 +373,11 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "vector_recall",
|
|
"current_stage": "vector_recall",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:rerank_context — 重排打分
|
|
|
|
|
+ # ============================================================
|
|
|
async def rerank_context_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def rerank_context_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """调用重排模型对候选文档打分排序。如果未召回候选则跳过。"""
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
if state.get("retrieval_status") != "recalled":
|
|
if state.get("retrieval_status") != "recalled":
|
|
@@ -288,9 +386,11 @@ class DocumentChatWorkflow:
|
|
|
state.get("callback_task_id", ""),
|
|
state.get("callback_task_id", ""),
|
|
|
{
|
|
{
|
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
|
|
|
+ "retrieval_keywords": state.get("retrieval_keywords") or [],
|
|
|
"retrieval_method": state.get("retrieval_method"),
|
|
"retrieval_method": state.get("retrieval_method"),
|
|
|
"retrieval_status": state.get("retrieval_status"),
|
|
"retrieval_status": state.get("retrieval_status"),
|
|
|
"retrieval_metrics": state.get("retrieval_metrics") or {},
|
|
"retrieval_metrics": state.get("retrieval_metrics") or {},
|
|
|
|
|
+ "retrieval_steps": state.get("retrieval_steps") or [],
|
|
|
"warnings": state.get("warnings") or [],
|
|
"warnings": state.get("warnings") or [],
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
@@ -304,15 +404,13 @@ class DocumentChatWorkflow:
|
|
|
query=state.get("retrieval_query") or "",
|
|
query=state.get("retrieval_query") or "",
|
|
|
candidates=state.get("retrieval_candidates") or [],
|
|
candidates=state.get("retrieval_candidates") or [],
|
|
|
)
|
|
)
|
|
|
- log_document_chat_event(
|
|
|
|
|
|
|
+ log_document_chat_event_truncated(
|
|
|
"rag_rerank_completed",
|
|
"rag_rerank_completed",
|
|
|
state.get("callback_task_id", ""),
|
|
state.get("callback_task_id", ""),
|
|
|
{
|
|
{
|
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
|
- "retrieval_method": state.get("retrieval_method"),
|
|
|
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
|
"retrieval_metrics": result.get("retrieval_metrics") or {},
|
|
"retrieval_metrics": result.get("retrieval_metrics") or {},
|
|
|
- "retrieval_candidates": state.get("retrieval_candidates") or [],
|
|
|
|
|
"reranked_references": result.get("reranked_references") or [],
|
|
"reranked_references": result.get("reranked_references") or [],
|
|
|
"warnings": result.get("warnings") or [],
|
|
"warnings": result.get("warnings") or [],
|
|
|
},
|
|
},
|
|
@@ -325,7 +423,17 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "rerank_context",
|
|
"current_stage": "rerank_context",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:quality_gate — 质量门过滤
|
|
|
|
|
+ # ============================================================
|
|
|
async def quality_gate_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def quality_gate_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """按 min_rerank_score 阈值过滤低质量参考,保留 scope 匹配的高可信引用。
|
|
|
|
|
+
|
|
|
|
|
+ 合格条件:
|
|
|
|
|
+ - rerank_score >= min_rerank_score(默认 0.65)
|
|
|
|
|
+ - metadata.source_scope_valid 为 True(项目/工程类型匹配)
|
|
|
|
|
+ - 有实际文本内容
|
|
|
|
|
+ """
|
|
|
if state.get("error_message"):
|
|
if state.get("error_message"):
|
|
|
return {}
|
|
return {}
|
|
|
if state.get("retrieval_status") != "reranked":
|
|
if state.get("retrieval_status") != "reranked":
|
|
@@ -334,9 +442,11 @@ class DocumentChatWorkflow:
|
|
|
state.get("callback_task_id", ""),
|
|
state.get("callback_task_id", ""),
|
|
|
{
|
|
{
|
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
|
|
|
+ "retrieval_keywords": state.get("retrieval_keywords") or [],
|
|
|
"retrieval_method": state.get("retrieval_method"),
|
|
"retrieval_method": state.get("retrieval_method"),
|
|
|
"retrieval_status": state.get("retrieval_status"),
|
|
"retrieval_status": state.get("retrieval_status"),
|
|
|
"retrieval_metrics": self._merge_metrics(state, {"approved_count": 0}),
|
|
"retrieval_metrics": self._merge_metrics(state, {"approved_count": 0}),
|
|
|
|
|
+ "retrieval_steps": state.get("retrieval_steps") or [],
|
|
|
"reranked_references": state.get("reranked_references") or [],
|
|
"reranked_references": state.get("reranked_references") or [],
|
|
|
"warnings": state.get("warnings") or [],
|
|
"warnings": state.get("warnings") or [],
|
|
|
},
|
|
},
|
|
@@ -348,15 +458,13 @@ class DocumentChatWorkflow:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
result = self.quality_gate.apply(state.get("reranked_references") or [])
|
|
result = self.quality_gate.apply(state.get("reranked_references") or [])
|
|
|
- log_document_chat_event(
|
|
|
|
|
|
|
+ log_document_chat_event_truncated(
|
|
|
"rag_quality_gate_completed",
|
|
"rag_quality_gate_completed",
|
|
|
state.get("callback_task_id", ""),
|
|
state.get("callback_task_id", ""),
|
|
|
{
|
|
{
|
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
"retrieval_query": state.get("retrieval_query"),
|
|
|
- "retrieval_method": state.get("retrieval_method"),
|
|
|
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
"retrieval_status": result.get("retrieval_status"),
|
|
|
"retrieval_metrics": result.get("retrieval_metrics") or {},
|
|
"retrieval_metrics": result.get("retrieval_metrics") or {},
|
|
|
- "reranked_references": state.get("reranked_references") or [],
|
|
|
|
|
"approved_references": result.get("approved_references") or [],
|
|
"approved_references": result.get("approved_references") or [],
|
|
|
"warnings": result.get("warnings") or [],
|
|
"warnings": result.get("warnings") or [],
|
|
|
},
|
|
},
|
|
@@ -369,7 +477,11 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "quality_gate",
|
|
"current_stage": "quality_gate",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:clarify — 需要用户补充说明
|
|
|
|
|
+ # ============================================================
|
|
|
async def clarify_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def clarify_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """意图置信度不足或模型要求澄清时,返回引导性问题。"""
|
|
|
intent = IntentResult(**(state.get("intent_result") or {"intent": "clarify"}))
|
|
intent = IntentResult(**(state.get("intent_result") or {"intent": "clarify"}))
|
|
|
question = intent.clarification_question or "请补充说明希望 AI 对当前章节做什么。"
|
|
question = intent.clarification_question or "请补充说明希望 AI 对当前章节做什么。"
|
|
|
skill_result = DocumentChatSkillOutput(
|
|
skill_result = DocumentChatSkillOutput(
|
|
@@ -384,7 +496,11 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "clarify",
|
|
"current_stage": "clarify",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:unsupported — 不支持的意图
|
|
|
|
|
+ # ============================================================
|
|
|
async def unsupported_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def unsupported_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """用户请求超出当前模块能力范围(非问答/非修改),返回提示说明。"""
|
|
|
intent = IntentResult(**(state.get("intent_result") or {"intent": "unsupported"}))
|
|
intent = IntentResult(**(state.get("intent_result") or {"intent": "unsupported"}))
|
|
|
message = intent.reason or "当前 AI 对话模块只支持选中章节的问答和修改。"
|
|
message = intent.reason or "当前 AI 对话模块只支持选中章节的问答和修改。"
|
|
|
skill_result = DocumentChatSkillOutput(
|
|
skill_result = DocumentChatSkillOutput(
|
|
@@ -399,8 +515,22 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "unsupported",
|
|
"current_stage": "unsupported",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:general_answer — 无选中章节时的通用回答
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _capture_stream_writer():
|
|
|
|
|
+ """获取 LangGraph 的流式写入器。在流式上下文中可用,否则返回 None。"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ from langgraph.config import get_stream_writer
|
|
|
|
|
+ writer = get_stream_writer()
|
|
|
|
|
+ return writer
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ logger.debug(f"[DocumentChat] StreamWriter not available: {exc}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
async def general_answer_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def general_answer_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
- """Respond directly via LLM when no section is selected."""
|
|
|
|
|
|
|
+ """用户未选中任何章节时,以通用助手身份通过 LLM 直接回答问题。"""
|
|
|
user_message = state.get("user_message", "")
|
|
user_message = state.get("user_message", "")
|
|
|
conversation_history = state.get("conversation_history") or []
|
|
conversation_history = state.get("conversation_history") or []
|
|
|
project_info = state.get("project_info") or {}
|
|
project_info = state.get("project_info") or {}
|
|
@@ -415,7 +545,7 @@ class DocumentChatWorkflow:
|
|
|
user_payload = {
|
|
user_payload = {
|
|
|
"user_message": user_message,
|
|
"user_message": user_message,
|
|
|
"project_info": project_info,
|
|
"project_info": project_info,
|
|
|
- "conversation_history": conversation_history[-6:],
|
|
|
|
|
|
|
+ "conversation_history": conversation_history[-6:], # 仅取最近 6 轮历史
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -423,15 +553,10 @@ class DocumentChatWorkflow:
|
|
|
from core.document_chat.component.llm_utils import compact_json
|
|
from core.document_chat.component.llm_utils import compact_json
|
|
|
|
|
|
|
|
full_text_parts: List[str] = []
|
|
full_text_parts: List[str] = []
|
|
|
|
|
+ writer = self._capture_stream_writer()
|
|
|
|
|
+ logger.info(f"[DocumentChat] general_answer_node: stream_writer={'captured' if writer else 'None'}")
|
|
|
|
|
|
|
|
- def _on_chunk(chunk: str):
|
|
|
|
|
- from langgraph.config import get_stream_writer
|
|
|
|
|
- try:
|
|
|
|
|
- writer = get_stream_writer()
|
|
|
|
|
- writer({"stream_chunk": chunk})
|
|
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
-
|
|
|
|
|
|
|
+ # 优先尝试流式生成,失败则降级为非流式
|
|
|
try:
|
|
try:
|
|
|
async for chunk in generate_model_client.get_model_generate_invoke_stream(
|
|
async for chunk in generate_model_client.get_model_generate_invoke_stream(
|
|
|
trace_id=state.get("callback_task_id", "general_answer"),
|
|
trace_id=state.get("callback_task_id", "general_answer"),
|
|
@@ -440,10 +565,11 @@ class DocumentChatWorkflow:
|
|
|
timeout=45,
|
|
timeout=45,
|
|
|
function_name="general_answer",
|
|
function_name="general_answer",
|
|
|
):
|
|
):
|
|
|
- _on_chunk(chunk)
|
|
|
|
|
|
|
+ if writer:
|
|
|
|
|
+ writer({"stream_chunk": chunk})
|
|
|
full_text_parts.append(chunk)
|
|
full_text_parts.append(chunk)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
- logger.warning(f"[DocumentChat] general_answer stream failed: {exc}, falling back to non-stream")
|
|
|
|
|
|
|
+ logger.warning(f"[DocumentChat] general_answer stream failed, falling back to non-stream")
|
|
|
if not full_text_parts:
|
|
if not full_text_parts:
|
|
|
response = await generate_model_client.get_model_generate_invoke(
|
|
response = await generate_model_client.get_model_generate_invoke(
|
|
|
trace_id=state.get("callback_task_id", "general_answer"),
|
|
trace_id=state.get("callback_task_id", "general_answer"),
|
|
@@ -458,11 +584,21 @@ class DocumentChatWorkflow:
|
|
|
if not answer:
|
|
if not answer:
|
|
|
answer = "您好,我是施工方案编辑 AI 助手。选中一个文档章节后,我可以帮您润色、扩写、改写或回答章节相关问题。"
|
|
answer = "您好,我是施工方案编辑 AI 助手。选中一个文档章节后,我可以帮您润色、扩写、改写或回答章节相关问题。"
|
|
|
|
|
|
|
|
|
|
+ logger.info(f"[DocumentChat] general_answer_node completed: chunks={len(full_text_parts)}, answer_len={len(answer)}")
|
|
|
|
|
+
|
|
|
skill_result = DocumentChatSkillOutput(
|
|
skill_result = DocumentChatSkillOutput(
|
|
|
skill_name="general-answer",
|
|
skill_name="general-answer",
|
|
|
response_type="general_answer",
|
|
response_type="general_answer",
|
|
|
answer=answer,
|
|
answer=answer,
|
|
|
)
|
|
)
|
|
|
|
|
+ log_document_chat_event(
|
|
|
|
|
+ "final_content_generated",
|
|
|
|
|
+ state.get("callback_task_id", ""),
|
|
|
|
|
+ {
|
|
|
|
|
+ "stage": "general_answer",
|
|
|
|
|
+ "skill_result": model_to_dict(skill_result),
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
return {
|
|
return {
|
|
|
"skill_result": model_to_dict(skill_result),
|
|
"skill_result": model_to_dict(skill_result),
|
|
|
"response_type": "general_answer",
|
|
"response_type": "general_answer",
|
|
@@ -472,10 +608,15 @@ class DocumentChatWorkflow:
|
|
|
logger.error(f"[DocumentChat] general_answer_node failed: {exc}", exc_info=True)
|
|
logger.error(f"[DocumentChat] general_answer_node failed: {exc}", exc_info=True)
|
|
|
return self._error_update("general_answer", exc)
|
|
return self._error_update("general_answer", exc)
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:run_answer_skill / run_modify_skill — 执行技能
|
|
|
|
|
+ # ============================================================
|
|
|
async def run_answer_skill_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def run_answer_skill_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """执行 document-answer 技能:基于检索内容回答章节相关问题。"""
|
|
|
return await self._run_skill(state, "document-answer", "run_answer_skill")
|
|
return await self._run_skill(state, "document-answer", "run_answer_skill")
|
|
|
|
|
|
|
|
async def run_modify_skill_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def run_modify_skill_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """执行 document-modify 技能:基于检索内容生成章节修改草案。"""
|
|
|
return await self._run_skill(state, "document-modify", "run_modify_skill")
|
|
return await self._run_skill(state, "document-modify", "run_modify_skill")
|
|
|
|
|
|
|
|
async def _run_skill(
|
|
async def _run_skill(
|
|
@@ -484,21 +625,39 @@ class DocumentChatWorkflow:
|
|
|
skill_name: str,
|
|
skill_name: str,
|
|
|
stage: str,
|
|
stage: str,
|
|
|
) -> Dict[str, Any]:
|
|
) -> Dict[str, Any]:
|
|
|
|
|
+ """通用技能执行方法。构建技能输入,调用流式执行,逐块输出到 SSE。"""
|
|
|
try:
|
|
try:
|
|
|
skill_input = self._build_skill_input(state)
|
|
skill_input = self._build_skill_input(state)
|
|
|
|
|
+ writer = self._capture_stream_writer()
|
|
|
|
|
+ logger.info(f"[DocumentChat] _run_skill: skill={skill_name}, stream_writer={'captured' if writer else 'None'}")
|
|
|
|
|
+
|
|
|
|
|
+ chunk_count = 0
|
|
|
|
|
|
|
|
def _on_chunk(chunk: str):
|
|
def _on_chunk(chunk: str):
|
|
|
- from langgraph.config import get_stream_writer
|
|
|
|
|
- try:
|
|
|
|
|
- writer = get_stream_writer()
|
|
|
|
|
|
|
+ """逐块回调:将技能生成的文本片段写入 SSE 流。"""
|
|
|
|
|
+ nonlocal chunk_count
|
|
|
|
|
+ if writer:
|
|
|
writer({"stream_chunk": chunk})
|
|
writer({"stream_chunk": chunk})
|
|
|
- except Exception:
|
|
|
|
|
- # 非流式路径(如 workflow.run())或不支持 StreamWriter 时跳过
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ chunk_count += 1
|
|
|
|
|
|
|
|
skill_result = await self.skill_dispatcher.run_skill_stream(
|
|
skill_result = await self.skill_dispatcher.run_skill_stream(
|
|
|
skill_name, skill_input, on_chunk=_on_chunk
|
|
skill_name, skill_input, on_chunk=_on_chunk
|
|
|
)
|
|
)
|
|
|
|
|
+ logger.info(f"[DocumentChat] _run_skill completed: skill={skill_name}, chunks_sent={chunk_count}, response_type={skill_result.response_type}")
|
|
|
|
|
+ log_document_chat_event(
|
|
|
|
|
+ "final_content_generated",
|
|
|
|
|
+ state.get("callback_task_id", ""),
|
|
|
|
|
+ {
|
|
|
|
|
+ "stage": stage,
|
|
|
|
|
+ "skill_name": skill_name,
|
|
|
|
|
+ "retrieval_query": state.get("retrieval_query"),
|
|
|
|
|
+ "retrieval_keywords": state.get("retrieval_keywords") or [],
|
|
|
|
|
+ "retrieval_status": state.get("retrieval_status"),
|
|
|
|
|
+ "retrieval_metrics": state.get("retrieval_metrics") or {},
|
|
|
|
|
+ "approved_references": state.get("approved_references") or [],
|
|
|
|
|
+ "skill_result": model_to_dict(skill_result),
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
return {
|
|
return {
|
|
|
"skill_result": model_to_dict(skill_result),
|
|
"skill_result": model_to_dict(skill_result),
|
|
|
"response_type": skill_result.response_type,
|
|
"response_type": skill_result.response_type,
|
|
@@ -507,7 +666,11 @@ class DocumentChatWorkflow:
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
return self._error_update(stage, exc)
|
|
return self._error_update(stage, exc)
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:error_handler — 错误处理
|
|
|
|
|
+ # ============================================================
|
|
|
async def error_handler_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def error_handler_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """统一错误处理节点,标记工作流失败。"""
|
|
|
error_message = state.get("error_message") or "document chat workflow failed"
|
|
error_message = state.get("error_message") or "document chat workflow failed"
|
|
|
logger.error(f"[DocumentChat] workflow error: {error_message}")
|
|
logger.error(f"[DocumentChat] workflow error: {error_message}")
|
|
|
return {
|
|
return {
|
|
@@ -516,7 +679,11 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "error_handler",
|
|
"current_stage": "error_handler",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 节点:complete — 工作流结束
|
|
|
|
|
+ # ============================================================
|
|
|
async def complete_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
async def complete_node(self, state: DocumentChatState) -> Dict[str, Any]:
|
|
|
|
|
+ """标记工作流完成。如果之前已标记为失败则保留失败状态。"""
|
|
|
if state.get("overall_task_status") == "failed":
|
|
if state.get("overall_task_status") == "failed":
|
|
|
return {"current_stage": "complete"}
|
|
return {"current_stage": "complete"}
|
|
|
return {
|
|
return {
|
|
@@ -524,7 +691,11 @@ class DocumentChatWorkflow:
|
|
|
"current_stage": "complete",
|
|
"current_stage": "complete",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # ============================================================
|
|
|
|
|
+ # 工具方法
|
|
|
|
|
+ # ============================================================
|
|
|
def to_response_data(self, state: DocumentChatState) -> DocumentChatData:
|
|
def to_response_data(self, state: DocumentChatState) -> DocumentChatData:
|
|
|
|
|
+ """将工作流最终状态转换为 HTTP 响应数据结构。"""
|
|
|
skill_result = state.get("skill_result") or {}
|
|
skill_result = state.get("skill_result") or {}
|
|
|
intent_result = state.get("intent_result")
|
|
intent_result = state.get("intent_result")
|
|
|
selected_section = state.get("selected_section") or {}
|
|
selected_section = state.get("selected_section") or {}
|
|
@@ -556,6 +727,7 @@ class DocumentChatWorkflow:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
def _build_skill_input(self, state: DocumentChatState) -> DocumentChatSkillInput:
|
|
def _build_skill_input(self, state: DocumentChatState) -> DocumentChatSkillInput:
|
|
|
|
|
+ """从工作流状态构建技能执行所需输入。"""
|
|
|
document_context = dict(state.get("document_context") or {})
|
|
document_context = dict(state.get("document_context") or {})
|
|
|
document_context["references"] = state.get("approved_references") or []
|
|
document_context["references"] = state.get("approved_references") or []
|
|
|
return DocumentChatSkillInput(
|
|
return DocumentChatSkillInput(
|
|
@@ -572,6 +744,7 @@ class DocumentChatWorkflow:
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
def _append_warnings(state: DocumentChatState, new_warnings: list) -> list:
|
|
def _append_warnings(state: DocumentChatState, new_warnings: list) -> list:
|
|
|
|
|
+ """合并告警列表,去重且不覆盖已有告警。"""
|
|
|
warnings = list(state.get("warnings") or [])
|
|
warnings = list(state.get("warnings") or [])
|
|
|
for warning in new_warnings:
|
|
for warning in new_warnings:
|
|
|
warning = str(warning).strip()
|
|
warning = str(warning).strip()
|
|
@@ -581,12 +754,14 @@ class DocumentChatWorkflow:
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
def _merge_metrics(state: DocumentChatState, new_metrics: Dict[str, Any]) -> Dict[str, Any]:
|
|
def _merge_metrics(state: DocumentChatState, new_metrics: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
+ """合并检索指标,新值覆盖旧值。各节点指标逐层累加到最终响应中。"""
|
|
|
metrics = dict(state.get("retrieval_metrics") or {})
|
|
metrics = dict(state.get("retrieval_metrics") or {})
|
|
|
metrics.update(new_metrics or {})
|
|
metrics.update(new_metrics or {})
|
|
|
return metrics
|
|
return metrics
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
def _error_update(stage: str, exc: Exception) -> Dict[str, Any]:
|
|
def _error_update(stage: str, exc: Exception) -> Dict[str, Any]:
|
|
|
|
|
+ """构建统一的错误状态更新。"""
|
|
|
return {
|
|
return {
|
|
|
"current_stage": stage,
|
|
"current_stage": stage,
|
|
|
"overall_task_status": "failed",
|
|
"overall_task_status": "failed",
|