From dd2fd8bcde266422674c5675957c35184fc19979 Mon Sep 17 00:00:00 2001 From: Marcus <1922576605@qq.com> Date: Thu, 1 Oct 2026 07:32:02 -0700 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=88=A0=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/processor/graph.py | 135 ---------------- app/processor/nodes/action_node.py | 146 ++---------------- app/processor/nodes/clarify_node.py | 42 ----- .../nodes/component_resource_node.py | 16 -- app/processor/nodes/fallback_node.py | 16 -- app/processor/nodes/input_node.py | 43 ------ app/processor/nodes/intent_node.py | 58 +------ app/processor/nodes/planner_node.py | 59 ------- app/processor/nodes/qa_node.py | 47 ------ app/processor/nodes/reflection_node.py | 86 +---------- app/processor/nodes/requirement_node.py | 64 -------- app/processor/nodes/scope_node.py | 59 ------- app/processor/nodes/validator_node.py | 81 +--------- app/prompt/intent.py | 6 +- 14 files changed, 25 insertions(+), 833 deletions(-) diff --git a/app/processor/graph.py b/app/processor/graph.py index a7f64af..9e766d1 100644 --- a/app/processor/graph.py +++ b/app/processor/graph.py @@ -274,138 +274,3 @@ def build_graph(checkpointer=None): # 模块加载时创建共享图实例,复用该实例的调用也共用其检查点存储 query_app = build_graph() - - -if __name__ == "__main__": - import argparse - 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 - from app.observability.tracing import ainvoke_screen, flush_tracing - - # 选择场景时只切换用户需求,共用同一份画布,方便比较执行结果 - examples = { - "move": "把销售额统计向右移动 50px,其他内容保持不变", - "layout": ( - "重新排列现有两个图表,销售额统计放在左侧,销售占比放在右侧," - "统一尺寸为 600×400,画布改为 1600×900,不新增或删除组件" - ), - "add": "保留现有组件,在下方新增一个柱状图,使用物料示例数据,不需要真实数据绑定", - "clarify": "这个图不太对", - } - parser = argparse.ArgumentParser(description="运行大屏 Agent 完整流程示例") - parser.add_argument("--case", choices=examples, default="move", help="选择联调场景") - args = parser.parse_args() - - async def main(): - # 1. 创建请求,每次运行使用独立会话,避免混入之前的澄清历史 - state: QueryState = { - "request": { - "prompt": examples[args.case], - "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": "销售额统计"}}}, - }, - { - "id": "sales-share", - "type": "pie-chart", - "name": "销售占比", - "layout": { - "x": 500, - "y": 34.8, - "width": 400, - "height": 260, - }, - "props": {"option": {"title": {"text": "销售占比"}}}, - }, - ], - "dataSource": [], - }, - "context": {"selected_node_ids": ["sales-chart"]}, - } - } - - # 2. 执行图,程序退出时关闭共享连接 - thread_id = str(uuid4()) - session_id = ( - state["request"].get("context", {}).get("conversation_id") or thread_id - ) - - print(f"场景:{args.case}\n需求:{examples[args.case]}") - if args.case == "clarify": - print("澄清回答示例:把销售额统计向右移动 50px,其他内容保持不变") - - try: - final_state = await ainvoke_screen( - query_app, state, thread_id=thread_id, session_id=session_id - ) - # 3. 展示真实澄清问题,用同一个 thread_id 恢复流程 - 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']}" - ) - 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 ainvoke_screen( - query_app, - Command(resume=reply), - thread_id=thread_id, - session_id=session_id, - ) - finally: - await close_decision_engine() - flush_tracing() - - # 4. 聚焦最终结果,校验失败时 simulated_screen 应为 None - result = { - "status": final_state.get("status"), - "intent": final_state.get("intent"), - # 保留需求与规划,方便定位模型输出和执行能力之间的不一致 - "requirement": final_state.get("requirement"), - "screen_plan": final_state.get("screen_plan"), - "validation_passed": final_state.get("validation_passed"), - "retry_count": final_state.get("retry_count", 0), - "failure_reason": final_state.get("failure_reason"), - "validation_errors": final_state.get("validation_errors", []), - "actions": final_state.get("actions", []), - "simulated_screen": final_state.get("simulated_screen"), - "answer": final_state.get("answer"), - } - print( - json.dumps( - result, - ensure_ascii=False, - indent=2, - default=lambda value: ( - value.model_dump(mode="json") - if isinstance(value, BaseModel) - else str(value) - ), - ) - ) - # 原始坐标单独展示,确认后端试执行没有修改请求中的画布 - original = state["request"]["screen"]["nodes"][0]["layout"] - print(f"原始销售额坐标:{original['x']},{original['y']}") - - asyncio.run(main()) diff --git a/app/processor/nodes/action_node.py b/app/processor/nodes/action_node.py index 501ecc3..5a598ab 100644 --- a/app/processor/nodes/action_node.py +++ b/app/processor/nodes/action_node.py @@ -122,7 +122,9 @@ class ActionNode(BaseNode): repairing=state.get("repair_plan") is not None, ) # 需要写组件属性时提供相关组件的字段分组,模型再按需查询字段定义 - with_fields = uses_material_fields(state["requirement"], state.get("screen_plan")) + with_fields = uses_material_fields( + state["requirement"], state.get("screen_plan") + ) if with_fields: payload["field_groups"] = field_group_directory(state) # 将本次具体资料放入用户消息,保留中文以便阅读和排查 @@ -140,10 +142,15 @@ class ActionNode(BaseNode): try: # 异步调用模型,根据需求、规划及是否正在修正选择对应的系统规则 if with_fields: - result = await invoke_with_field_tools(ActionBatch, system_prompt, content) + result = await invoke_with_field_tools( + ActionBatch, system_prompt, content + ) else: result = await get_llm(ActionBatch).ainvoke( - [SystemMessage(content=system_prompt), HumanMessage(content=content)] + [ + SystemMessage(content=system_prompt), + HumanMessage(content=content), + ] ) # 响应中没有工具调用时解析器会返回 None @@ -190,136 +197,3 @@ class ActionNode(BaseNode): # 返回待校验状态,此时 validation_passed 仍为 None,不代表动作已执行或通过校验 return updated_state - - -if __name__ == "__main__": - import asyncio - import sys - - from pydantic import BaseModel - - from app.processor.nodes.component_resource_node import ComponentResourceNode - from app.processor.nodes.input_node import InputNode - from app.processor.nodes.validator_node import ValidatorNode - from app.schemas.change_scope import ChangeScope - from app.schemas.requirement import Requirement - from app.schemas.screen_plan import ScreenPlan - - async def main(): - state = InputNode().process( - { - "request": { - "prompt": "将销售额统计向右移动 50px", - "screen": { - "canvas": {"width": 1920, "height": 1080}, - "nodes": [ - { - "id": "sales-chart", - "type": "chart", - "layout": { - "x": 100, - "y": 200, - "width": 400, - "height": 300, - }, - } - ], - }, - } - } - ) - state["requirement"] = Requirement( - intent="modify_screen", - goal="向右移动 50px", - needs_clarification=False, - changes=[ - { - "target_scope": "nodes", - "target_ids": ["sales-chart"], - "operation": "move", - "parameters": {"delta_x": 50}, - } - ], - ) - state["change_scope"] = ChangeScope( - scope="local", - target_ids=["sales-chart"], - needs_planning=False, - reason="明确的组件位移", - ) - # 加 --add 测试规划到新增动作,默认仍测试简单移动 - if "--add" in sys.argv: - state = InputNode().process( - { - "request": { - "prompt": "保留已有组件,在顶部新增销售总览标题", - "screen": state["input_context"].screen.model_dump(mode="json"), - } - } - ) - state["requirement"] = Requirement( - intent="modify_screen", - goal="新增标题", - needs_clarification=False, - changes=[{"target_scope": "screen", "operation": "add"}], - ) - state["change_scope"] = ChangeScope( - scope="local", - needs_planning=True, - reason="顶部新增标题", - ) - state = ComponentResourceNode().process(state) - state["screen_plan"] = ScreenPlan( - goal="新增标题", - design_summary="顶部标题", - layout_description="保留原图表", - components=[ - { - "plan_id": "heading", - "disposition": "add", - "component_type": "text", - "purpose": "显示销售总览", - "design": {"text": "销售总览"}, - "layout": {"x": 100, "y": 40, "width": 500, "height": 60}, - } - ], - ) - # 加 --modify 测试规划到已有组件调整,无需新增物料 - if "--modify" in sys.argv: - state = ComponentResourceNode().process(state) - state["change_scope"].needs_planning = True - state["screen_plan"] = ScreenPlan( - goal="向右移动 50px", - design_summary="调整图表位置", - layout_description="保留尺寸,向右移动", - components=[ - { - "plan_id": "sales-position", - "disposition": "modify", - "target_id": "sales-chart", - "purpose": "调整位置", - "layout": {"x": 150, "y": 200, "width": 400, "height": 300}, - } - ], - ) - node = ActionNode() - final_state = await node.process(state) - if ( - state.get("screen_plan") is not None - and final_state.get("actions") is not None - ): - final_state = ValidatorNode().process(final_state) - print( - json.dumps( - final_state, - ensure_ascii=False, - indent=2, - default=lambda value: ( - value.model_dump(mode="json") - if isinstance(value, BaseModel) - else str(value) - ), - ) - ) - - asyncio.run(main()) diff --git a/app/processor/nodes/clarify_node.py b/app/processor/nodes/clarify_node.py index 10fbe77..12deab9 100644 --- a/app/processor/nodes/clarify_node.py +++ b/app/processor/nodes/clarify_node.py @@ -78,45 +78,3 @@ class ClarifyNode(BaseNode): # 返回澄清内容和等待状态,实际暂停由后续调用 interrupt 的节点完成 return updated_state - - -if __name__ == "__main__": - import asyncio - import json - - from pydantic import BaseModel - - from app.clients.decision import close_decision_engine - from app.observability.tracing import ainvoke_screen, flush_tracing - from app.processor.graph import query_app - - async def main(): - # 修改请求,通过完整流程测试澄清分支 - state: QueryState = { - "request": { - "prompt": "这个图不太对", - "screen": {"canvas": {"width": 1920, "height": 1080}, "nodes": []}, - } - } - try: - final_state = await ainvoke_screen( - query_app, state, thread_id="clarify-test" - ) - finally: - await close_decision_engine() - flush_tracing() - print( - json.dumps( - { - key: value.model_dump(mode="json") - if isinstance(value, BaseModel) - else value - for key, value in final_state.items() - }, - ensure_ascii=False, - indent=2, - default=str, - ) - ) - - asyncio.run(main()) diff --git a/app/processor/nodes/component_resource_node.py b/app/processor/nodes/component_resource_node.py index 6844f78..df42042 100644 --- a/app/processor/nodes/component_resource_node.py +++ b/app/processor/nodes/component_resource_node.py @@ -32,19 +32,3 @@ class ComponentResourceNode(BaseNode): # 返回资源查询结果和处理状态,由图决定继续规划还是进入失败处理 return updated_state - - -if __name__ == "__main__": - import json - - state: QueryState = {"request": {}} - node = ComponentResourceNode() - final_state = node.process(state) - print( - json.dumps( - final_state, - ensure_ascii=False, - indent=2, - default=lambda value: value.model_dump(mode="json"), - ) - ) diff --git a/app/processor/nodes/fallback_node.py b/app/processor/nodes/fallback_node.py index 61ae0ca..fac2a43 100644 --- a/app/processor/nodes/fallback_node.py +++ b/app/processor/nodes/fallback_node.py @@ -33,19 +33,3 @@ class FallbackNode(BaseNode): # 返回统一的失败状态,保留原动作和错误信息供排查 return updated_state - - -if __name__ == "__main__": - from app.schemas.action_validation import ActionValidationError - - state = { - "request": {}, - "retry_count": 2, - "actions": None, - "validation_errors": [ - ActionValidationError(code="generation_error", message="动作生成失败") - ], - } - node = FallbackNode() - final_state = node.process(state) - print(final_state) diff --git a/app/processor/nodes/input_node.py b/app/processor/nodes/input_node.py index 70a78b4..b0331fb 100644 --- a/app/processor/nodes/input_node.py +++ b/app/processor/nodes/input_node.py @@ -97,46 +97,3 @@ class InputNode(BaseNode): updated_state["status"] = "running" # 返回初始化后的状态,由图接续执行后续节点 return updated_state - - -if __name__ == "__main__": - import json - - # 1. 创建 state:可将 screen 替换成前端的完整大屏 JSON - state: QueryState = { - "request": { - "prompt": "把销售额统计向右移动 50px", - "screen": { - "canvas": { - "width": 1920, - "height": 1080, - "backgroundColor": "#ffffff10", - }, - "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"]}, - "attachments": [], - } - } - - # 2. 执行节点 - node = InputNode() - final_state = node.process(state) - - # 3. 将上下文模型转成可序列化的字典,再打印完整状态 - printable_state = { - **final_state, - "input_context": final_state["input_context"].model_dump( - mode="json", exclude_unset=True - ), - } - print(json.dumps(printable_state, ensure_ascii=False, indent=2)) diff --git a/app/processor/nodes/intent_node.py b/app/processor/nodes/intent_node.py index 02814be..31f9466 100644 --- a/app/processor/nodes/intent_node.py +++ b/app/processor/nodes/intent_node.py @@ -1,6 +1,6 @@ """意图识别节点""" -from app.clients.decision import close_decision_engine, get_decision_engine +from app.clients.decision import get_decision_engine from app.processor.base import BaseNode from app.processor.intent_policy import resolve_intent, resolve_offer_response from app.processor.state import QueryState @@ -141,59 +141,3 @@ class IntentNode(BaseNode): # 返回更新后的状态,由图继续执行对应的处理节点 return updated_state - - -if __name__ == "__main__": - import asyncio - import json - - from pydantic import BaseModel - - from app.processor.nodes.input_node import InputNode - - 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. 先校验输入,再真实调用 Jev,退出时关闭 Jev 连接 - state = InputNode().process(state) - node = IntentNode() - try: - final_state = await node.process(state) - finally: - # 无论调用成功还是抛出异常,都关闭示例使用的 Jev 连接 - await close_decision_engine() - # 3. 打印完整 state,包含选项、概率、置信度和调用信息 - # 将状态中的 Pydantic 模型转成字典,其他值保持原样,以便输出 JSON - 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, - ) - ) - - # 启动事件循环,执行上面的异步调试流程 - asyncio.run(main()) diff --git a/app/processor/nodes/planner_node.py b/app/processor/nodes/planner_node.py index 21f2824..1ce2e24 100644 --- a/app/processor/nodes/planner_node.py +++ b/app/processor/nodes/planner_node.py @@ -150,62 +150,3 @@ class PlannerNode(BaseNode): # 如果实际修改或删除的目标不在允许范围内,则拒绝扩大修改范围 if outside := changed_ids - set(scope.target_ids): raise ValueError(f"局部规划修改了范围外的组件: {sorted(outside)}") - - -if __name__ == "__main__": - import asyncio - - from pydantic import BaseModel - - from app.processor.nodes.component_resource_node import ComponentResourceNode - from app.processor.nodes.input_node import InputNode - from app.schemas.change_scope import ChangeScope - from app.schemas.requirement import Requirement - - async def main(): - # 1. 创建 state,直接提供需求和范围,单独测试规划 - state = InputNode().process( - { - "request": { - "prompt": "创建销售大屏,上方展示销售总额,下方展示月度趋势", - "screen": {"canvas": {"width": 1920, "height": 1080}, "nodes": []}, - } - } - ) - state["requirement"] = Requirement( - intent="create_screen", - goal="创建销售总览大屏", - needs_clarification=False, - changes=[ - { - "target_scope": "screen", - "operation": "add", - "parameters": {"metrics": ["销售总额", "月度销售额"]}, - } - ], - constraints=["总额在上,趋势在下"], - ) - state["change_scope"] = ChangeScope( - scope="screen", - needs_planning=True, - resource_needs=["销售总额和月度销售数据,用于指标与趋势展示"], - reason="需要安排区域和组件组合", - ) - # 2. 执行节点并打印完整 state,仅调用 LLM - node = PlannerNode() - state = ComponentResourceNode().process(state) - final_state = await node.process(state) - print( - json.dumps( - final_state, - ensure_ascii=False, - indent=2, - default=lambda value: ( - value.model_dump(mode="json") - if isinstance(value, BaseModel) - else str(value) - ), - ) - ) - - asyncio.run(main()) diff --git a/app/processor/nodes/qa_node.py b/app/processor/nodes/qa_node.py index bd84a73..9aae076 100644 --- a/app/processor/nodes/qa_node.py +++ b/app/processor/nodes/qa_node.py @@ -105,50 +105,3 @@ class QANode(BaseNode): emit(delta) # 拼接完整回答,并去除首尾的空白 return "".join(parts).strip() - - -if __name__ == "__main__": - import asyncio - import json - - from pydantic import BaseModel - - from app.processor.nodes.input_node import InputNode - - async def main(): - # 直接测试问答,仅需 LLM 配置 - 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": "销售额统计"}}}, - } - ], - }, - "context": {"selected_node_ids": ["sales-chart"]}, - } - } - state = InputNode().process(state) - node = QANode() - final_state = await node.process(state) - print( - json.dumps( - { - key: value.model_dump(mode="json") - if isinstance(value, BaseModel) - else value - for key, value in final_state.items() - }, - ensure_ascii=False, - indent=2, - ) - ) - - asyncio.run(main()) diff --git a/app/processor/nodes/reflection_node.py b/app/processor/nodes/reflection_node.py index 1788a0a..fa778c9 100644 --- a/app/processor/nodes/reflection_node.py +++ b/app/processor/nodes/reflection_node.py @@ -13,9 +13,9 @@ 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.schemas.capabilities import USER_FAILURE_MESSAGE from app.prompt.reflection import REFLECTION_SYSTEM_PROMPT from app.schemas.action_repair import ActionRepairPlan +from app.schemas.capabilities import USER_FAILURE_MESSAGE class ReflectionNode(BaseNode): @@ -49,7 +49,9 @@ class ReflectionNode(BaseNode): payload["component_materials"] = plan_materials(state) # 涉及组件属性时,修正建议须基于查询到的字段定义,与动作生成使用同一组工具 - with_fields = uses_material_fields(state["requirement"], state.get("screen_plan")) + with_fields = uses_material_fields( + state["requirement"], state.get("screen_plan") + ) if with_fields: payload["field_groups"] = field_group_directory(state) # 用户消息承载本轮待分析的具体资料,序列化时保留中文 @@ -66,7 +68,10 @@ class ReflectionNode(BaseNode): ) else: plan = await get_llm(ActionRepairPlan).ainvoke( - [SystemMessage(content=REFLECTION_SYSTEM_PROMPT), HumanMessage(content=content)] + [ + SystemMessage(content=REFLECTION_SYSTEM_PROMPT), + HumanMessage(content=content), + ] ) # 响应中没有工具调用时解析器会返回 None if plan is None: @@ -92,78 +97,3 @@ class ReflectionNode(BaseNode): # 返回修正建议、重试次数和失败原因,由后续路由决定重新生成动作还是结束 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()) diff --git a/app/processor/nodes/requirement_node.py b/app/processor/nodes/requirement_node.py index 50fbcd3..b57278a 100644 --- a/app/processor/nodes/requirement_node.py +++ b/app/processor/nodes/requirement_node.py @@ -1,7 +1,5 @@ """沿用意图判断,提取具体需求""" -import json - from langchain_core.messages import HumanMessage, SystemMessage from app.clients.llm import get_llm @@ -74,65 +72,3 @@ class RequirementNode(BaseNode): # 返回更新后的状态,由图继续执行后续节点 return updated_state - - -if __name__ == "__main__": - import asyncio - - from pydantic import BaseModel - - from app.clients.decision import close_decision_engine - from app.processor.nodes.input_node import InputNode - from app.processor.nodes.intent_node import IntentNode - - async def main(): - # 1. 修改这里的请求,测试具体需求提取 - state: QueryState = { - "request": { - "prompt": "把销售额统计向右移动 50px,其他不变", - "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. 依次执行输入、意图判断和需求理解 - try: - # 先将原始请求校验为统一的输入上下文 - state = InputNode().process(state) - # 调用 Jev 判断意图,为需求提取提供已确认的分类结果 - state = await IntentNode().process(state) - # 基于上下文和意图调用模型,提取结构化需求 - node = RequirementNode() - final_state = await node.process(state) - finally: - # 无论处理成功还是抛出异常,都关闭示例使用的 Jev 连接 - await close_decision_engine() - - # 3. 打印完整 state - # 将状态中的 Pydantic 模型转成字典,其他值保持原样,以便输出 JSON - 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, - ) - ) - - # 启动事件循环,执行上面的异步调试流程 - asyncio.run(main()) diff --git a/app/processor/nodes/scope_node.py b/app/processor/nodes/scope_node.py index d5c3166..03023bd 100644 --- a/app/processor/nodes/scope_node.py +++ b/app/processor/nodes/scope_node.py @@ -94,62 +94,3 @@ class ScopeNode(BaseNode): # 返回包含范围、失败原因和处理状态的更新结果 return updated_state - - -if __name__ == "__main__": - import asyncio - - from pydantic import BaseModel - - from app.processor.nodes.input_node import InputNode - from app.schemas.requirement import Requirement - - async def main(): - # 直接提供已理解的需求,单独测试范围判断,仅需 LLM 配置 - state: QueryState = { - "request": { - "prompt": "把销售额统计向右移动 50px,其他不变", - "screen": { - "canvas": {"width": 1920, "height": 1080}, - "nodes": [ - { - "id": "sales-chart", - "type": "bar-chart", - "name": "销售额统计", - "layout": {"x": 26, "y": 34.8, "width": 400, "height": 260}, - } - ], - }, - } - } - state = InputNode().process(state) - state["requirement"] = Requirement( - intent="modify_screen", - goal="销售额统计向右移动 50px", - changes=[ - { - "target_scope": "nodes", - "target_ids": ["sales-chart"], - "operation": "move", - "parameters": {"delta_x": 50}, - } - ], - needs_clarification=False, - constraints=["其他不变"], - ) - node = ScopeNode() - final_state = await node.process(state) - print( - json.dumps( - { - key: value.model_dump(mode="json") - if isinstance(value, BaseModel) - else value - for key, value in final_state.items() - }, - ensure_ascii=False, - indent=2, - ) - ) - - asyncio.run(main()) diff --git a/app/processor/nodes/validator_node.py b/app/processor/nodes/validator_node.py index c5fb57b..818490a 100644 --- a/app/processor/nodes/validator_node.py +++ b/app/processor/nodes/validator_node.py @@ -137,7 +137,9 @@ class ValidatorNode(BaseNode): for index, action in enumerate(actions): # 属性更新先按物料字段定义检查,目标不存在时交给处理器报错 node = ( - next((item for item in screen.nodes if item.id == action.target_id), None) + next( + (item for item in screen.nodes if item.id == action.target_id), None + ) if isinstance(action, UpdateAction) else None ) @@ -191,80 +193,3 @@ class ValidatorNode(BaseNode): # 返回动作、校验结果和完整模拟快照,供后续输出使用 return updated_state - - -if __name__ == "__main__": - import json - - from app.processor.nodes.input_node import InputNode - from app.schemas.actions import MoveAction, ResizeAction, UpdateAction - from app.schemas.requirement import Requirement - - # 1. 创建 state,不需要调用模型 - state = InputNode().process( - { - "request": { - "prompt": "移动、调整尺寸并修改标题", - "screen": { - "canvas": {"width": 1920, "height": 1080}, - "nodes": [ - { - "id": "sales-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": ["sales-chart"], "operation": op} - for op in ("move", "resize", "update") - ], - ) - state["actions"] = [ - MoveAction(target_id="sales-chart", delta_x=50), - ResizeAction(target_id="sales-chart", width=500), - UpdateAction( - target_id="sales-chart", section="props", path=["title"], value="月度销售额" - ), - ] - # 2. 执行节点并打印完整 state - node = ValidatorNode() - final_state = node.process(state) - print( - json.dumps( - final_state, - ensure_ascii=False, - indent=2, - default=lambda value: value.model_dump(mode="json"), - ) - ) - - # 3. 在第二个动作制造错误,验证整批失败且不返回部分结果 - failed_state = { - **final_state, - "requirement": Requirement( - intent="modify_screen", - goal="移动两个组件", - needs_clarification=False, - changes=[ - {"target_scope": "nodes", "target_ids": [target], "operation": "move"} - for target in ("sales-chart", "missing") - ], - ), - "actions": [ - state["actions"][0], - MoveAction(target_id="missing", delta_x=50), - ], - } - failed_result = node.process(failed_state) - print("失败校验:", failed_result["validation_passed"]) - print("错误:", [error.model_dump() for error in failed_result["validation_errors"]]) - print("失败快照:", failed_result["simulated_screen"]) diff --git a/app/prompt/intent.py b/app/prompt/intent.py index 49c3956..b52e7bd 100644 --- a/app/prompt/intent.py +++ b/app/prompt/intent.py @@ -13,9 +13,9 @@ def build_offer_question() -> ChoiceDecision: ), criteria={ # 同意执行提议,包括简短的肯定回答 - "accept": "Agrees to perform exactly the offered edit, e.g. 好的, 需要, 要, 可以, 行, 嗯, yes", + "accept": "Agrees to perform exactly the offered edit, e.g. Okay, need, want, can, alright, mm-hmm, yes", # 拒绝提议 - "decline": "Declines the offer or says it is not needed, e.g. 不用了, 算了, no", + "decline": "Declines the offer or says it is not needed, e.g. No, never mind, no", # 提出新的或修改过的请求、提问,或与提议无关 "other": "Asks something else, requests a different or changed edit, or does not respond to the offer", }, @@ -46,6 +46,6 @@ def build_intent_question() -> ChoiceDecision: # 明确的文字回答或交流请求,包括助手身份和能力,但没有要求实际操作大屏 "qa": "Clearly request a textual answer or general conversation without requesting screen creation or an actual edit; includes assistant identity and capabilities, information, reasons, diagnosis, and suggestions, e.g. Why is there no data in this chart or How to optimize this graph; also includes brief acknowledgments, thanks, greetings and farewells that need only a short reply, e.g. ok, Okay, thank you", # 语义上没有明确表达要问什么或做什么,不等同于模型分数低 - "unclear": "The user's desired response or action is still undetermined after incorporating the latest answer and selected option; vague feedback such as 这个图不太对 is unclear only while subsequent answers have not resolved what to do", + "unclear": "The user's desired response or action is still undetermined after incorporating the latest answer and selected option; vague feedback such as 'This figure doesn't look quite right' is unclear only while subsequent answers have not resolved what to do", }, )