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