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

158 lines
7.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_add_fields,
check_update_field,
find_material,
retained_examples,
)
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 找物料补全新增组件,模型显式写出的字段优先
显式写的字段先按字段定义检查,和 update 是同一套规则;sample_data 保持模型的声明,
交付时再由 mark_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)
check_add_fields(catalog, material.type, overrides)
node = ScreenNode.model_validate(merge_config(material.template(), overrides))
return action.model_copy(update={"node": node})
def mark_sample_data(
actions: list[Action], screen: Screen, catalog: ComponentCatalog | None
) -> list[Action]:
"""交付前给新增组件定 sample_data:模型声明了自己编的数据,或者组件上还留着物料的示例数据
按交付后的大屏判断,不管数据是新增时写的、之后用 update 换的,还是 $query 查来的;
后来被删掉的组件保持模型的声明。只改动作上的标记,交付记录依次执行仍得到同一个大屏
"""
nodes = {node.id: node for node in screen.nodes}
result = []
for action in actions:
if isinstance(action, AddAction) and not action.sample_data:
node = nodes.get(action.node.id)
if node is not None and any(
field.example == "data"
for field, _ in retained_examples(catalog, node.model_dump(mode="json"))
):
action = action.model_copy(update={"sample_data": True})
result.append(action)
return result
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