"""分析失败动作,生成下一次修正建议""" import json from langchain_core.exceptions import OutputParserException from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel, ValidationError from app.actions.plan import plan_materials from app.clients.llm import get_llm from app.processor.base import BaseNode from app.processor.state import QueryState 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() llm_client = get_llm(ActionRepairPlan) # 汇总原始上下文、需求和失败信息,让模型基于实际错误提出修正建议 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) # 只捕获输出结构和解析错误,配置、网络等异常继续向上抛出 try: # 异步生成修正建议,本节点不直接修改动作 result = await llm_client.ainvoke( [ # 系统消息提供稳定的失败分析和修正规则 SystemMessage(content=REFLECTION_SYSTEM_PROMPT), # 用户消息承载本轮待分析的具体资料,序列化时保留中文 HumanMessage(content=json.dumps(payload, ensure_ascii=False)), ] ) # 将模型对象转成字典后统一校验,非模型返回值直接交给协议校验 plan = ActionRepairPlan.model_validate( result.model_dump() if isinstance(result, BaseModel) else result ) except (ValidationError, OutputParserException): # 如果未能得到合法建议,清空旧修正计划,避免再次使用已失效的建议 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 plan.reason # 返回修正建议、重试次数和失败原因,由后续路由决定重新生成动作还是结束 return updated_state if __name__ == "__main__": import asyncio from app.processor.nodes.input_node import InputNode from app.schemas.action_validation import ActionValidationError from app.schemas.actions import UpdateAction from app.schemas.requirement import Requirement async def main(): state = InputNode().process( { "request": { "prompt": "把标题改成月度销售额", "screen": { "canvas": {"width": 1920, "height": 1080}, "nodes": [ { "id": "chart", "type": "chart", "layout": { "x": 100, "y": 200, "width": 400, "height": 300, }, "props": {"title": "销售额"}, } ], }, } } ) state["requirement"] = Requirement( intent="modify_screen", goal="修改标题", needs_clarification=False, changes=[ { "target_scope": "nodes", "target_ids": ["chart"], "operation": "update", "parameters": {"title": "月度销售额"}, } ], ) state["actions"] = [ UpdateAction( target_id="chart", section="props", path=["option", "title"], value="月度销售额", ) ] state["validation_passed"] = False state["validation_errors"] = [ ActionValidationError( action_index=0, code="execution_error", message="中间路径不存在: props ['option']", ) ] node = ReflectionNode() final_state = await node.process(state) print( json.dumps( final_state, ensure_ascii=False, indent=2, default=lambda value: value.model_dump(mode="json"), ) ) asyncio.run(main())