Files
my-v0-server/app/actions/batch.py
T

134 lines
6.0 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.
"""整批模拟执行:新增组件补全物料默认配置,再按顺序在大屏副本上执行,遇到第一个错误即停止
纯函数,不依赖 agent;agent 的 apply_actions 工具和测试共用
"""
from copy import deepcopy
from app.actions.layout import EXPANDERS
from app.actions.registry import apply_action
from app.actions.update import fit_existing_path, same_value
from app.resources.material_fields import check_update_field, find_material
from app.schemas.actions import Action, AddAction, AgentAction, UpdateAction
from app.schemas.capabilities import DATA_BINDING_ERROR, DATA_BINDING_SUPPORTED
from app.schemas.component_resource import ComponentCatalog
from app.schemas.screen import Screen, ScreenNode
class BatchError(ValueError):
"""整批中第 index 个动作(从 0 开始)失败,消息里带上序号和目标,模型据此修正"""
def __init__(self, index: int, action: AgentAction, reason: str):
self.index = index
self.reason = reason
if isinstance(action, AddAction):
target = action.node.id
elif ids := getattr(action, "target_ids", None):
target = "、".join(ids)
else:
target = getattr(action, "target_id", "画布")
super().__init__(f"第 {index + 1} 个动作({action.type} {target}):{reason}")
def merge_config(defaults: dict, overrides: dict) -> dict:
"""对象逐层覆盖,数组和标量整体替换,不修改传入的数据"""
result = deepcopy(defaults)
for key, value in overrides.items():
result[key] = (
merge_config(result[key], value)
if isinstance(result.get(key), dict) and isinstance(value, dict)
else deepcopy(value)
)
return result
def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
"""按 node.type 找物料补全新增组件,模型显式写出的字段优先
sample_data 模型写了就以模型为准(只有它知道是否换成了自己的数据),没写时按物料设置
"""
material = find_material(catalog, action.node.type)
if not DATA_BINDING_SUPPORTED and action.node.model_extra.get("dataId"):
raise ValueError(DATA_BINDING_ERROR)
overrides = action.node.model_dump(mode="json", exclude_unset=True)
node = ScreenNode.model_validate(merge_config(material.template(), overrides))
sample_data = (
action.sample_data
if "sample_data" in action.model_fields_set
else material.sample_data
)
return action.model_copy(update={"node": node, "sample_data": sample_data})
def misplaced_props(actions: list[AgentAction], catalog: ComponentCatalog) -> list[str]:
"""新增组件的 props 里物料没有的键:前端按物料的结构读取,多出来的键不会生效,多半是写错了层级
评测里模型把 series 写在 props 下而不是 props.option 下,物料默认的 series 留着,图表引用的列对不上;
它看到的报错说 encode 用了别的列,和自己写的不一样,反复原样提交。返回给模型的说明,每项一个组件
"""
notes = []
for action in actions:
if not isinstance(action, AddAction):
continue
try:
known = (
find_material(catalog, action.node.type).template().get("props") or {}
)
except ValueError: # 物料不存在,补全时会报错
continue
written = (
action.node.model_dump(mode="json", exclude_unset=True).get("props") or {}
)
for key in sorted(set(written) - set(known)):
parent = next(
(
name
for name, value in known.items()
if isinstance(value, dict) and key in value
),
None,
)
where = f"(物料里在 props.{parent}.{key})" if parent else ""
notes.append(f"{action.node.id} 的 props.{key}{where}")
return notes
def run_batch(
screen: Screen, catalog: ComponentCatalog, actions: list[AgentAction]
) -> tuple[list[Action], Screen]:
"""从传入的大屏开始按顺序模拟执行,返回前端要执行的动作和执行后的大屏;第一个错误抛 BatchError
布局意图(arrange、align、distribute、set_layout)在这里按当时的大屏展开成 move 和 resize
update 写入的值和字段现在的值完全一样时没有变化,跳过:不经过字段契约、也不交付;
模型常把已经正确的值再写一遍(例如已经是 category 的轴类型),字段不在契约里时整批都会失败
"""
current = screen.model_copy(deep=True)
completed: list[Action] = []
for index, action in enumerate(actions):
try:
if expander := EXPANDERS.get(action.type):
expanded = expander(current, action)
for primitive in expanded:
current = apply_action(current, primitive)
completed.extend(expanded)
continue
if isinstance(action, AddAction):
action = complete_add(action, catalog)
elif isinstance(action, UpdateAction):
# 目标不存在时交给执行器报错
node = next(
(item for item in current.nodes if item.id == action.target_id),
None,
)
if node is not None:
if same_value(node, action):
continue
check_update_field(catalog, node.type, action)
# 字段定义允许、但组件中还没有的中间对象,折叠进值里再执行
action = fit_existing_path(node, action)
current = apply_action(current, action)
except ValueError as error: # 含 pydantic ValidationError
raise BatchError(index, action, str(error)) from error
completed.append(action)
return completed, current