feat:优化增量执行、工具调用容错与评测体系
This commit is contained in:
+39
-2
@@ -7,7 +7,7 @@ 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
|
||||
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
|
||||
@@ -60,12 +60,47 @@ def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
|
||||
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
|
||||
"""从传入的大屏开始按顺序模拟执行,返回前端要执行的动作和执行后的大屏;第一个错误抛 BatchError
|
||||
|
||||
布局意图(arrange、align、distribute、set_layout)在这里按当时的大屏展开成 move 和 resize
|
||||
update 写入的值和字段现在的值完全一样时没有变化,跳过:不经过字段契约、也不交付;
|
||||
模型常把已经正确的值再写一遍(例如已经是 category 的轴类型),字段不在契约里时整批都会失败
|
||||
"""
|
||||
current = screen.model_copy(deep=True)
|
||||
completed: list[Action] = []
|
||||
@@ -86,6 +121,8 @@ def run_batch(
|
||||
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)
|
||||
|
||||
+35
-2
@@ -1,5 +1,6 @@
|
||||
"""在大屏 JSON 副本上按路径更新组件属性,用于后端校验动作能否执行"""
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
from app.actions.base import BaseActionHandler
|
||||
@@ -7,6 +8,30 @@ from app.schemas.actions import UpdateAction
|
||||
from app.schemas.screen import Screen, ScreenNode
|
||||
|
||||
|
||||
def same_value(node: ScreenNode, action: UpdateAction) -> bool:
|
||||
"""字段已经存在、并且值和要写入的完全一样(按 JSON 比较,1 和 1.0、true 和 1 不算一样)"""
|
||||
if action.section == "name":
|
||||
current = node.name
|
||||
else:
|
||||
current = getattr(node, action.section)
|
||||
for segment in action.path:
|
||||
if (
|
||||
isinstance(current, dict)
|
||||
and isinstance(segment, str)
|
||||
and segment in current
|
||||
) or (
|
||||
isinstance(current, list)
|
||||
and type(segment) is int
|
||||
and segment < len(current)
|
||||
):
|
||||
current = current[segment]
|
||||
else:
|
||||
return False
|
||||
return json.dumps(current, sort_keys=True) == json.dumps(
|
||||
action.value, sort_keys=True
|
||||
)
|
||||
|
||||
|
||||
def fit_existing_path(node: ScreenNode, action: UpdateAction) -> UpdateAction:
|
||||
"""字段定义允许的路径在组件中可能还没有中间对象(例如未开启过的数值标签)
|
||||
|
||||
@@ -17,7 +42,11 @@ def fit_existing_path(node: ScreenNode, action: UpdateAction) -> UpdateAction:
|
||||
return action
|
||||
current = getattr(node, action.section) or {}
|
||||
for index, segment in enumerate(action.path[:-1]):
|
||||
if isinstance(current, dict) and isinstance(segment, str) and segment not in current:
|
||||
if (
|
||||
isinstance(current, dict)
|
||||
and isinstance(segment, str)
|
||||
and segment not in current
|
||||
):
|
||||
rest = action.path[index + 1 :]
|
||||
if not all(isinstance(item, str) for item in rest):
|
||||
return action
|
||||
@@ -29,7 +58,11 @@ def fit_existing_path(node: ScreenNode, action: UpdateAction) -> UpdateAction:
|
||||
)
|
||||
if isinstance(current, dict) and isinstance(segment, str):
|
||||
current = current[segment]
|
||||
elif isinstance(current, list) and type(segment) is int and segment < len(current):
|
||||
elif (
|
||||
isinstance(current, list)
|
||||
and type(segment) is int
|
||||
and segment < len(current)
|
||||
):
|
||||
current = current[segment]
|
||||
else:
|
||||
return action
|
||||
|
||||
Reference in New Issue
Block a user