feat: 接入新动作 update_canvas
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@ def plan_block_reason(state: QueryState) -> str | None:
|
||||
if state["requirement"].needs_clarification:
|
||||
return "需求尚未澄清,不能生成规划动作"
|
||||
if plan.canvas_changes:
|
||||
return "规划包含画布修改,当前尚未接入对应动作"
|
||||
return "规划中的画布修改尚未接入,当前仅支持无需规划的画布修改"
|
||||
if plan.resource_needs:
|
||||
return "规划仍有待获取资源,不能直接生成动作"
|
||||
additions = [item for item in plan.components if item.disposition == "add"]
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.actions.delete import DeleteHandler
|
||||
from app.actions.move import MoveHandler
|
||||
from app.actions.resize import ResizeHandler
|
||||
from app.actions.update import UpdateHandler
|
||||
from app.actions.update_canvas import UpdateCanvasHandler
|
||||
from app.schemas.actions import Action
|
||||
from app.schemas.screen import Screen
|
||||
|
||||
@@ -18,6 +19,7 @@ ACTION_HANDLERS: dict[str, BaseActionHandler[Any]] = {
|
||||
"move": MoveHandler(),
|
||||
"resize": ResizeHandler(),
|
||||
"update": UpdateHandler(),
|
||||
"update_canvas": UpdateCanvasHandler(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""在大屏副本上更新画布属性,检查修改后的 Canvas 是否有效"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from app.actions.base import BaseActionHandler
|
||||
from app.schemas.actions import UpdateCanvasAction
|
||||
from app.schemas.screen import Canvas, Screen
|
||||
|
||||
|
||||
class UpdateCanvasHandler(BaseActionHandler[UpdateCanvasAction]):
|
||||
def apply(self, screen: Screen, action: UpdateCanvasAction) -> Screen:
|
||||
properties = screen.canvas.model_dump()
|
||||
# 顶层字段整体替换,嵌套对象不做深度合并
|
||||
properties.update(deepcopy(action.properties))
|
||||
canvas = Canvas.model_validate(properties)
|
||||
result = screen.model_copy(deep=True)
|
||||
result.canvas = canvas
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
screen = Screen.model_validate(
|
||||
{"canvas": {"width": 1920, "height": 1080}, "nodes": []}
|
||||
)
|
||||
action = UpdateCanvasAction(
|
||||
properties={"backgroundColor": "#000000", "width": 1600}
|
||||
)
|
||||
result = UpdateCanvasHandler().apply(screen, action)
|
||||
print(result.model_dump_json(indent=2))
|
||||
print("原始画布:", screen.canvas.model_dump())
|
||||
@@ -41,8 +41,8 @@ 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
|
||||
# 已支持的简单修改直接生成动作,尚不支持的需求进入失败收尾
|
||||
return "action_node" if can_generate_actions(state) else "fallback_node"
|
||||
|
||||
|
||||
def build_graph(checkpointer=None):
|
||||
@@ -129,13 +129,13 @@ def build_graph(checkpointer=None):
|
||||
{
|
||||
"component_resource_node": "component_resource_node",
|
||||
"action_node": "action_node",
|
||||
END: END,
|
||||
"fallback_node": "fallback_node",
|
||||
},
|
||||
)
|
||||
workflow.add_conditional_edges(
|
||||
"component_resource_node",
|
||||
lambda state: state["component_resources"].status == "resolved",
|
||||
{True: "planner_node", False: END},
|
||||
{True: "planner_node", False: "fallback_node"},
|
||||
)
|
||||
workflow.add_edge("planner_node", "action_node")
|
||||
workflow.add_conditional_edges(
|
||||
@@ -185,6 +185,7 @@ def build_graph(checkpointer=None):
|
||||
("app.schemas.actions.move", "MoveAction"),
|
||||
("app.schemas.actions.resize", "ResizeAction"),
|
||||
("app.schemas.actions.update", "UpdateAction"),
|
||||
("app.schemas.actions.update_canvas", "UpdateCanvasAction"),
|
||||
("app.schemas.action_validation", "ActionValidationError"),
|
||||
("app.schemas.action_repair", "ActionRepairPlan"),
|
||||
("app.schemas.screen", "Screen"),
|
||||
|
||||
@@ -16,29 +16,38 @@ from app.schemas.actions import ActionBatch, AddAction
|
||||
|
||||
|
||||
def can_generate_actions(state: QueryState) -> bool:
|
||||
return action_block_reason(state) is None
|
||||
|
||||
|
||||
def action_block_reason(state: QueryState) -> str | None:
|
||||
# 路由和失败说明共用能力判断,避免支持范围不一致
|
||||
if state.get("screen_plan") is not None:
|
||||
return plan_block_reason(state) is None
|
||||
return plan_block_reason(state)
|
||||
|
||||
requirement = state["requirement"]
|
||||
scope = state["change_scope"]
|
||||
# 简单操作直接生成动作,复杂变更走规划分支
|
||||
return (
|
||||
requirement.intent == "modify_screen"
|
||||
and not requirement.needs_clarification
|
||||
and not scope.needs_planning
|
||||
and not scope.resource_needs
|
||||
and bool(requirement.changes)
|
||||
and all(
|
||||
change.target_scope == "nodes"
|
||||
and change.operation
|
||||
in (
|
||||
"move",
|
||||
"resize",
|
||||
"update",
|
||||
"delete",
|
||||
)
|
||||
|
||||
if requirement.needs_clarification:
|
||||
return "需求尚未澄清完整,不能生成动作"
|
||||
if scope.needs_planning or requirement.intent != "modify_screen":
|
||||
return "当前需求需要先完成规划,不能直接生成动作"
|
||||
if scope.resource_needs:
|
||||
return "尚未接入所需资源查询:" + ";".join(scope.resource_needs)
|
||||
if not requirement.changes:
|
||||
return "需求中没有可执行的变更"
|
||||
unsupported = [
|
||||
f"{change.target_scope}/{change.operation}"
|
||||
for change in requirement.changes
|
||||
if not (
|
||||
change.target_scope == "nodes"
|
||||
and change.operation in ("move", "resize", "update", "delete")
|
||||
or change.target_scope == "canvas"
|
||||
and change.operation in ("update", "resize", "update_canvas")
|
||||
)
|
||||
)
|
||||
]
|
||||
if unsupported:
|
||||
return "当前尚未支持的直接操作:" + "、".join(dict.fromkeys(unsupported))
|
||||
return None
|
||||
|
||||
|
||||
class ActionNode(BaseNode):
|
||||
@@ -60,7 +69,7 @@ class ActionNode(BaseNode):
|
||||
return updated_state
|
||||
if not can_generate_actions(state):
|
||||
raise ValueError(
|
||||
"ActionNode 当前只支持无需规划和资源的 move、resize、update、delete 需求"
|
||||
"ActionNode 当前只支持无需规划和资源的 move、resize、update、delete、update_canvas 需求"
|
||||
)
|
||||
payload = {
|
||||
"input_context": state["input_context"].model_dump(mode="json"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""停止自动修正并保留诊断信息"""
|
||||
"""统一收尾能力不足、资源缺失和动作修正失败"""
|
||||
|
||||
from app.processor.base import BaseNode
|
||||
from app.processor.state import QueryState
|
||||
@@ -11,9 +11,12 @@ class FallbackNode(BaseNode):
|
||||
updated_state: QueryState = state.copy()
|
||||
reason = state.get("failure_reason")
|
||||
if not reason:
|
||||
if state.get("validation_errors"):
|
||||
reason = f"动作校验未通过,已达到自动修正上限,共重试 {state.get('retry_count', 0)} 次"
|
||||
else:
|
||||
reason = "当前需求未能完成,缺少具体失败原因"
|
||||
|
||||
# actions 仅保留作诊断,不交付部分结果
|
||||
# 失败动作仅保留作诊断,validation_passed=False 表示不可交付执行
|
||||
updated_state["status"] = "failed"
|
||||
updated_state["failure_reason"] = reason
|
||||
updated_state["validation_passed"] = False
|
||||
|
||||
@@ -6,6 +6,7 @@ from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from app.clients.llm import get_llm
|
||||
from app.processor.base import BaseNode
|
||||
from app.processor.nodes.action_node import action_block_reason
|
||||
from app.processor.state import QueryState
|
||||
from app.prompt.scope import SCOPE_SYSTEM_PROMPT
|
||||
from app.schemas.change_scope import ChangeScope
|
||||
@@ -55,6 +56,11 @@ class ScopeNode(BaseNode):
|
||||
raise ValueError(f"范围遗漏了需求中的节点: {sorted(missing_ids)}")
|
||||
|
||||
updated_state["change_scope"] = scope
|
||||
# 规划分支继续获取组件能力,直接操作分支提前记录缺失能力
|
||||
reason = None if scope.needs_planning else action_block_reason(updated_state)
|
||||
updated_state["failure_reason"] = reason
|
||||
updated_state["status"] = "failed" if reason else "running"
|
||||
|
||||
return updated_state
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from app.actions.registry import apply_action
|
||||
from app.processor.base import BaseNode
|
||||
from app.processor.state import QueryState
|
||||
from app.schemas.action_validation import ActionValidationError, schema_errors
|
||||
from app.schemas.actions import ActionBatch, AddAction
|
||||
from app.schemas.actions import ActionBatch, AddAction, UpdateCanvasAction
|
||||
|
||||
|
||||
class ValidatorNode(BaseNode):
|
||||
@@ -60,19 +60,33 @@ class ValidatorNode(BaseNode):
|
||||
updated_state["simulated_screen"] = None
|
||||
return updated_state
|
||||
allowed = {
|
||||
(target, change.operation)
|
||||
("nodes", target, change.operation)
|
||||
for change in requirement.changes
|
||||
if change.target_scope == "nodes"
|
||||
for target in change.target_ids
|
||||
}
|
||||
actual = {(action.target_id, action.type) for action in batch.actions}
|
||||
allowed.update(
|
||||
("canvas", "", "update_canvas")
|
||||
for change in requirement.changes
|
||||
if change.target_scope == "canvas"
|
||||
and change.operation in ("update", "resize", "update_canvas")
|
||||
)
|
||||
# 画布没有组件 ID,使用独立作用域避免与同名组件混淆
|
||||
targets = [
|
||||
("canvas", "", action.type)
|
||||
if isinstance(action, UpdateCanvasAction)
|
||||
else ("nodes", action.target_id, action.type)
|
||||
for action in batch.actions
|
||||
]
|
||||
actual = set(targets)
|
||||
errors = [
|
||||
ActionValidationError(
|
||||
action_index=index,
|
||||
code="scope_error",
|
||||
message=f"动作超出需求范围: {action.target_id} / {action.type}",
|
||||
message=f"动作超出需求范围: {' / '.join(target)}",
|
||||
)
|
||||
for index, action in enumerate(batch.actions)
|
||||
if (action.target_id, action.type) not in allowed
|
||||
for index, target in enumerate(targets)
|
||||
if target not in allowed
|
||||
]
|
||||
if missing := allowed - actual:
|
||||
errors.append(
|
||||
|
||||
@@ -20,7 +20,7 @@ input_context 提供当前大屏快照、用户原文及编辑上下文
|
||||
"""
|
||||
|
||||
SIMPLE_TASK_PROMPT = """# 当前任务:直接修改
|
||||
将 requirement.changes 转换成对应动作,只修改其中指定的已有组件和属性
|
||||
将 requirement.changes 转换成对应动作,只修改其中指定的组件或画布属性
|
||||
"""
|
||||
|
||||
PLAN_TASK_PROMPT = """# 当前任务:按规划生成
|
||||
@@ -52,7 +52,10 @@ def build_action_prompt(
|
||||
sections = [ACTION_SYSTEM_PROMPT]
|
||||
if screen_plan is None:
|
||||
sections.append(SIMPLE_TASK_PROMPT)
|
||||
action_types = {change.operation for change in requirement.changes}
|
||||
action_types = {
|
||||
"update_canvas" if change.target_scope == "canvas" else change.operation
|
||||
for change in requirement.changes
|
||||
}
|
||||
else:
|
||||
sections.append(PLAN_TASK_PROMPT)
|
||||
action_types = set()
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
# 新增动作时在这里补充模型需要理解的执行语义
|
||||
ACTION_RULES: dict[str, str] = {
|
||||
"update_canvas": """## update_canvas:更新画布属性
|
||||
将 canvas 范围的 update、resize 或 update_canvas 需求转换为此动作,不填写 target_id
|
||||
properties 只包含需要修改的画布顶层字段,宽高为最终绝对值,相对变化依据当前画布换算
|
||||
顶层字段整体替换,未提供的字段保持不变;改变画布尺寸不会自动缩放或移动组件
|
||||
""",
|
||||
"delete": """## delete:删除已有组件
|
||||
使用真实 target_id,只删除需求明确指定的组件;规划中的 remove 对应 delete
|
||||
每个目标仅删除一次,删除后不能再移动、调整尺寸或更新该目标
|
||||
|
||||
@@ -10,10 +10,11 @@ from .delete import DeleteAction
|
||||
from .move import MoveAction
|
||||
from .resize import ResizeAction
|
||||
from .update import UpdateAction
|
||||
from .update_canvas import UpdateCanvasAction
|
||||
|
||||
# 按 type 选择对应模型,不维护重复的操作名称映射
|
||||
Action = Annotated[
|
||||
AddAction | DeleteAction | MoveAction | ResizeAction | UpdateAction,
|
||||
AddAction | DeleteAction | MoveAction | ResizeAction | UpdateAction | UpdateCanvasAction,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
action_adapter = TypeAdapter(Action)
|
||||
@@ -32,5 +33,6 @@ __all__ = [
|
||||
"MoveAction",
|
||||
"ResizeAction",
|
||||
"UpdateAction",
|
||||
"UpdateCanvasAction",
|
||||
"action_adapter",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""仅更新画布属性,不修改组件或数据源"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field, JsonValue
|
||||
|
||||
from app.schemas.base import NonBlankString
|
||||
|
||||
from .base import ActionModel
|
||||
|
||||
|
||||
class UpdateCanvasAction(ActionModel):
|
||||
type: Literal["update_canvas"] = "update_canvas"
|
||||
properties: dict[NonBlankString, JsonValue] = Field(
|
||||
min_length=1,
|
||||
description="要替换的 canvas 顶层字段,如 width、height、backgroundColor;未提供的字段保持不变",
|
||||
)
|
||||
Reference in New Issue
Block a user