Files
my-v0-server/app/processor/nodes/qa_node.py
T

97 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""基于当前大屏、上传的数据和会话历史流式回答问题
由问答 agent 执行:会话里有数据文件时先查询再回答,否则只调用一次模型;只把模型输出的文字推送给前端
"""
import json
from langchain.agents.middleware.model_call_limit import ModelCallLimitExceededError
from langchain_core.messages import AIMessageChunk, HumanMessage
from langchain_core.output_parsers import StrOutputParser
from langgraph.config import get_stream_writer
from app.agent.qa_agent import QAContext, get_qa_agent
from app.processor.base import BaseNode
from app.processor.history import history_messages
from app.processor.state import QueryState, run_data
from app.processor.summary import data_file_summary
# 模型在一次回答里分几次输出文字(例如查询前后各说一段)时,段落之间空一行
PARAGRAPH_BREAK = "\n\n"
class QANode(BaseNode):
name = "qa_node"
async def process(self, state: QueryState) -> QueryState:
updated_state: QueryState = state.copy()
try:
# 图执行上下文中通过 custom 流逐段推送回答,独立调用节点时没有写入器
writer = get_stream_writer()
except RuntimeError:
writer = None
context = state["input_context"]
payload = context.model_dump(
mode="json", exclude_unset=True, exclude={"history", "data_files"}
)
if context.data_files:
# 只给表名、行数和列名,文件路径和列概况不进提示词,需要时由 describe_data 查看
payload["data_files"] = [
data_file_summary(item) for item in context.data_files
]
agent = get_qa_agent(with_data=bool(context.data_files))
parser = StrOutputParser()
parts: list[str] = []
last_message_id = None
try:
async for chunk, metadata in agent.astream(
{
"messages": [
# 会话历史作为此前的对话消息,本次请求是最后一条用户消息
*history_messages(context.history),
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
},
context=QAContext(data=run_data()),
stream_mode="messages",
):
# 工具结果、工具调用和中间件插入的消息都不推送
if metadata.get("langgraph_node") != "model" or not isinstance(
chunk, AIMessageChunk
):
continue
if not (delta := parser.invoke(chunk)):
continue
if parts and chunk.id != last_message_id:
delta = PARAGRAPH_BREAK + delta
last_message_id = chunk.id
parts.append(delta)
if writer is not None:
writer(
{
"type": "answer_delta",
"message_id": state.get("message_id"),
"delta": delta,
}
)
except ModelCallLimitExceededError:
self.logger.warning("问答 agent 超出模型调用上限(MAX_QA_STEPS)")
updated_state["status"] = "failed"
updated_state["answer"] = (
"查询步骤超出上限,这次没有完成,可以缩小范围或换个问法再试"
)
return updated_state
# 推送了什么就保存什么,会话历史和前端看到的回答一致
answer = "".join(parts).strip()
# 没有有效文本时报错,避免将空回答标记为处理完成
if not answer:
raise ValueError("问答模型未返回文本回答")
updated_state["answer"] = answer
updated_state["status"] = "completed"
return updated_state