Files
my-v0-server/app/processor/graph.py
T

280 lines
10 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.
"""输入解析、意图分流和需求理解流程"""
import inspect
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.graph import END, StateGraph
from app.processor.nodes.clarify_node import ClarifyNode
from app.processor.nodes.input_node import InputNode
from app.processor.nodes.intent_node import IntentNode
from app.processor.nodes.requirement_node import RequirementNode
from app.processor.nodes.wait_for_user_node import WaitForUserNode
from app.processor.nodes.qa_node import QANode
from app.processor.nodes.scope_node import ScopeNode
from app.processor.nodes.action_node import ActionNode, can_generate_actions
from app.processor.nodes.validator_node import ValidatorNode
from app.processor.nodes.reflection_node import ReflectionNode
from app.processor.nodes.fallback_node import FallbackNode
from app.processor.nodes.planner_node import PlannerNode
from app.processor.nodes.component_resource_node import ComponentResourceNode
from app.processor.state import QueryState
def route_after_intent(state: QueryState) -> str:
return state["intent"]
def route_after_requirement(state: QueryState) -> bool:
return state["requirement"].needs_clarification
def route_after_wait(state: QueryState) -> str:
return "input_node" if state["status"] == "running" else END
def route_after_scope(state: QueryState) -> str:
if state["change_scope"].needs_planning:
return "component_resource_node"
return "action_node" if can_generate_actions(state) else END
def build_graph(checkpointer=None):
from app.config import settings
max_retries = settings.max_action_retries
def route_failure(state: QueryState) -> str:
# 上限检查放在 Reflection 前,避免最后一次无效模型调用
return (
"reflection_node"
if state.get("retry_count", 0) < max_retries
else "fallback_node"
)
def route_after_action(state: QueryState) -> str:
if state.get("status") == "failed":
return "fallback_node"
return (
route_failure(state)
if state["validation_passed"] is False
else "validator_node"
)
def route_after_validation(state: QueryState) -> str:
return END if state["validation_passed"] else route_failure(state)
def route_after_reflection(state: QueryState) -> str:
plan = state.get("repair_plan")
return "action_node" if plan is not None and plan.retryable else "fallback_node"
# 定义工作流
workflow = StateGraph(QueryState)
# 实例化节点
nodes = {
"input_node": InputNode(),
"intent_node": IntentNode(),
"requirement_node": RequirementNode(),
"clarify_node": ClarifyNode(),
"wait_for_user_node": WaitForUserNode(),
"qa_node": QANode(),
"scope_node": ScopeNode(),
"planner_node": PlannerNode(),
"component_resource_node": ComponentResourceNode(),
"action_node": ActionNode(),
"validator_node": ValidatorNode(),
"reflection_node": ReflectionNode(),
"fallback_node": FallbackNode(),
}
# 注册节点,异步节点使用 BaseNode 的异步入口
for node_name, node in nodes.items():
action = node._acall if inspect.iscoroutinefunction(node.process) else node
workflow.add_node(node_name, action)
# 定义入口
workflow.set_entry_point("input_node")
# 添加业务边
workflow.add_edge("input_node", "intent_node")
# 新建和修改进入需求理解,意图不明进入澄清
workflow.add_conditional_edges(
source="intent_node",
path=route_after_intent,
path_map={
"create_screen": "requirement_node",
"modify_screen": "requirement_node",
"qa": "qa_node",
"unclear": "clarify_node",
},
)
workflow.add_conditional_edges(
source="requirement_node",
path=route_after_requirement,
path_map={True: "clarify_node", False: "scope_node"},
)
workflow.add_edge("clarify_node", "wait_for_user_node")
workflow.add_edge("qa_node", END)
# 先读取组件能力再规划,规划后的业务数据查询后续接入
workflow.add_conditional_edges(
"scope_node",
route_after_scope,
{
"component_resource_node": "component_resource_node",
"action_node": "action_node",
END: END,
},
)
workflow.add_conditional_edges(
"component_resource_node",
lambda state: state["component_resources"].status == "resolved",
{True: "planner_node", False: END},
)
workflow.add_edge("planner_node", "action_node")
workflow.add_conditional_edges(
"action_node",
route_after_action,
{
"validator_node": "validator_node",
"reflection_node": "reflection_node",
"fallback_node": "fallback_node",
},
)
workflow.add_conditional_edges(
"validator_node",
route_after_validation,
{
END: END,
"reflection_node": "reflection_node",
"fallback_node": "fallback_node",
},
)
workflow.add_conditional_edges(
"reflection_node",
route_after_reflection,
{
"action_node": "action_node",
"fallback_node": "fallback_node",
},
)
workflow.add_edge("fallback_node", END)
workflow.add_conditional_edges(
"wait_for_user_node",
route_after_wait,
{"input_node": "input_node", END: END},
)
# TODO 本地默认保存在内存,可传入持久化 checkpointer
if checkpointer is None:
serde = JsonPlusSerializer(
allowed_msgpack_modules=[
("app.schemas.input_context", "InputContext"),
("app.schemas.decision", "DecisionResponse"),
("app.schemas.requirement", "Requirement"),
("app.schemas.change_scope", "ChangeScope"),
("app.schemas.screen_plan", "ScreenPlan"),
("app.schemas.component_resource", "ComponentResult"),
("app.schemas.actions.add", "AddAction"),
("app.schemas.actions.move", "MoveAction"),
("app.schemas.actions.resize", "ResizeAction"),
("app.schemas.actions.update", "UpdateAction"),
("app.schemas.action_validation", "ActionValidationError"),
("app.schemas.action_repair", "ActionRepairPlan"),
("app.schemas.screen", "Screen"),
("app.schemas.clarification", "Clarification"),
("app.schemas.clarification_reply", "ClarificationReply"),
]
)
checkpointer = InMemorySaver(serde=serde)
return workflow.compile(checkpointer=checkpointer).with_config(
{
"recursion_limit": max(25, 12 + 3 * max_retries),
}
)
# 创建全局图实例
query_app = build_graph()
if __name__ == "__main__":
import asyncio
import json
from uuid import uuid4
from langgraph.types import Command
from pydantic import BaseModel
from app.clients.decision import close_decision_engine
async def main():
# 1. 修改请求,测试完整流程
state: QueryState = {
"request": {
"prompt": "帮我修改销售额统计这个图,但具体改哪里我还没说,请先问我",
"screen": {
"canvas": {"width": 1920, "height": 1080},
"nodes": [
{
"id": "sales-chart",
"type": "bar-chart",
"name": "销售额统计",
"layout": {"x": 26, "y": 34.8, "width": 400, "height": 260},
"props": {"option": {"title": {"text": "销售额统计"}}},
}
],
"dataSource": [],
},
"context": {"selected_node_ids": ["sales-chart"]},
}
}
# 2. 执行图,程序退出时关闭共享连接
config = {"configurable": {"thread_id": str(uuid4())}}
try:
final_state = await query_app.ainvoke(state, config=config)
while final_state.get("__interrupt__"):
pending = final_state["__interrupt__"][0].value
print(pending["question"])
options = pending.get("options", [])
for index, option in enumerate(options, 1):
marker = "(推荐)" if option["recommended"] else ""
print(
f"{index}. {option['label']}{marker}:{option['description']}"
)
if "error" in pending:
print(pending["error"])
answer = input("输入编号或自由回答(/cancel 取消):")
reply = {"cancel": True} if answer == "/cancel" else {"answer": answer}
if answer.isdigit() and 1 <= int(answer) <= len(options):
reply = {"option_id": options[int(answer) - 1]["id"]}
notes = input("补充说明(可留空):").strip()
if notes:
reply["answer"] = notes
final_state = await query_app.ainvoke(
Command(resume=reply), config=config
)
finally:
await close_decision_engine()
# 3. 打印完整 state
print(
json.dumps(
{
key: value.model_dump(mode="json", exclude_unset=True)
if isinstance(value, BaseModel)
else value
for key, value in final_state.items()
},
ensure_ascii=False,
indent=2,
default=lambda value: (
value.model_dump(mode="json")
if isinstance(value, BaseModel)
else str(value)
),
)
)
asyncio.run(main())