Files
my-v0-server/app/processor/nodes/reflection_node.py
T
2026-09-29 19:21:29 +08:00

155 lines
6.3 KiB
Python

"""分析失败动作,生成下一次修正建议"""
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())