97 lines
4.7 KiB
Python
97 lines
4.7 KiB
Python
"""分析失败动作,生成下一次修正建议"""
|
|
|
|
import json
|
|
|
|
from langchain.agents.structured_output import StructuredOutputError
|
|
from langchain_core.exceptions import OutputParserException
|
|
from langchain_core.messages import HumanMessage, SystemMessage
|
|
from pydantic import ValidationError
|
|
|
|
from app.actions.plan import plan_materials
|
|
from app.clients.llm import get_llm
|
|
from app.processor.base import BaseNode
|
|
from app.processor.material_agent import field_group_directory, invoke_with_field_tools
|
|
from app.processor.state import QueryState
|
|
from app.prompt.action import FIELD_QUERY_PROMPT, uses_material_fields
|
|
from app.prompt.reflection import REFLECTION_SYSTEM_PROMPT
|
|
from app.schemas.action_repair import ActionRepairPlan
|
|
|
|
|
|
class ReflectionNode(BaseNode):
|
|
name = "reflection_node"
|
|
|
|
async def process(self, state: QueryState) -> QueryState:
|
|
# 浅拷贝状态
|
|
updated_state: QueryState = state.copy()
|
|
|
|
# 汇总原始上下文、需求和失败信息,让模型基于实际错误提出修正建议
|
|
payload = {
|
|
"input_context": state["input_context"].model_dump(mode="json"),
|
|
"requirement": state["requirement"].model_dump(mode="json"),
|
|
# 动作可能在生成或解析阶段就失败,没有动作时传入空列表
|
|
"actions": [
|
|
action.model_dump(mode="json") for action in state.get("actions") or []
|
|
],
|
|
# 将校验错误转成字典,保留错误类型、动作下标和具体原因
|
|
"validation_errors": [
|
|
error.model_dump() for error in state["validation_errors"]
|
|
],
|
|
# 如果已有修正建议,一并提供给模型,帮助分析上次修正后仍失败的原因
|
|
"previous_repair_plan": state["repair_plan"].model_dump()
|
|
if state.get("repair_plan")
|
|
else None,
|
|
}
|
|
|
|
# 如果动作来自大屏规划,补充规划和新增组件物料,避免修正时偏离规划或物料定义
|
|
if state.get("screen_plan") is not None:
|
|
payload["screen_plan"] = state["screen_plan"].model_dump(mode="json")
|
|
payload["component_materials"] = plan_materials(state)
|
|
|
|
# 涉及组件属性时,修正建议须基于查询到的字段定义,与动作生成使用同一组工具
|
|
with_fields = uses_material_fields(
|
|
state["requirement"], state.get("screen_plan")
|
|
)
|
|
if with_fields:
|
|
payload["field_groups"] = field_group_directory(state)
|
|
# 用户消息承载本轮待分析的具体资料,序列化时保留中文
|
|
content = json.dumps(payload, ensure_ascii=False)
|
|
|
|
# 只捕获输出结构和解析错误,配置、网络等异常继续向上抛出
|
|
try:
|
|
# 异步生成修正建议,本节点不直接修改动作
|
|
if with_fields:
|
|
plan = await invoke_with_field_tools(
|
|
ActionRepairPlan,
|
|
REFLECTION_SYSTEM_PROMPT + "\n" + FIELD_QUERY_PROMPT,
|
|
content,
|
|
)
|
|
else:
|
|
plan = await get_llm(ActionRepairPlan).ainvoke(
|
|
[
|
|
SystemMessage(content=REFLECTION_SYSTEM_PROMPT),
|
|
HumanMessage(content=content),
|
|
]
|
|
)
|
|
# 响应中没有工具调用时解析器会返回 None
|
|
if plan is None:
|
|
raise OutputParserException("模型未返回结构化结果")
|
|
except (ValidationError, OutputParserException, StructuredOutputError):
|
|
# 如果未能得到合法建议,清空旧修正计划,避免再次使用已失效的建议
|
|
updated_state["repair_plan"] = None
|
|
# 记录无法继续修正的原因,交给后续路由和失败处理流程
|
|
updated_state["failure_reason"] = "未能生成有效的动作修正建议"
|
|
|
|
return updated_state
|
|
|
|
# 保存通过校验的修正建议,供后续路由判断及动作重新生成使用
|
|
updated_state["repair_plan"] = plan
|
|
# 仅在建议允许重试时将次数加一,缺少历史计数时从零开始,不可重试时保持原次数
|
|
updated_state["retry_count"] = state.get("retry_count", 0) + int(plan.retryable)
|
|
# 可以重试时清空旧失败原因;不可重试时保留分析,由失败收尾交给模型向用户说明
|
|
updated_state["failure_reason"] = (
|
|
None if plan.retryable else f"修正分析判断无法完成:{plan.reason}"
|
|
)
|
|
|
|
# 返回修正建议、重试次数和失败原因,由后续路由决定重新生成动作还是结束
|
|
return updated_state
|