feat:优化意图识别

This commit is contained in:
2026-09-30 21:31:20 -07:00
parent 1b727cde42
commit 8e37d30ef2
7 changed files with 138 additions and 52 deletions
+104 -42
View File
@@ -3,6 +3,9 @@
from copy import deepcopy from copy import deepcopy
from math import isclose from math import isclose
from loguru import logger
from pydantic import ValidationError
from app.processor.state import QueryState from app.processor.state import QueryState
from app.schemas.action_validation import ActionValidationError from app.schemas.action_validation import ActionValidationError
from app.schemas.actions import ( from app.schemas.actions import (
@@ -44,15 +47,18 @@ def plan_block_reason(state: QueryState) -> str | None:
modifications = { modifications = {
item.target_id: item for item in plan.components if item.disposition == "modify" item.target_id: item for item in plan.components if item.disposition == "modify"
} }
replacements = [item for item in plan.components if item.disposition == "replace"]
# 如果没有任何组件或画布变更,则没有可转换为动作的规划内容 # 如果没有任何组件或画布变更,则没有可转换为动作的规划内容
if not additions and not modifications and not removals and not plan.canvas_changes: if not (
additions or modifications or removals or replacements or plan.canvas_changes
):
return "规划未包含可执行的组件或画布修改" return "规划未包含可执行的组件或画布修改"
# 收集目录支持的类型,拒绝规划虚构前端未注册的新增组件类型 # 收集目录支持的类型,拒绝规划虚构前端未注册的新增或更换类型
supported = {item.type for item in resources.components} supported = {item.type for item in resources.components}
if any(item.component_type not in supported for item in additions): if any(item.component_type not in supported for item in additions + replacements):
return "规划中的新增类型未在组件目录中找到" return "规划中的新增或更换类型未在组件目录中找到"
# 按 ID 索引原大屏节点,检查已有目标是否存在及类型是否保持一致 # 按 ID 索引原大屏节点,检查已有目标是否存在及类型是否保持一致
nodes = {node.id: node for node in state["input_context"].screen.nodes} nodes = {node.id: node for node in state["input_context"].screen.nodes}
@@ -65,9 +71,10 @@ def plan_block_reason(state: QueryState) -> str | None:
if item.target_id not in nodes: if item.target_id not in nodes:
return f"规划目标不存在: {item.target_id}" return f"规划目标不存在: {item.target_id}"
# 如果规划显式指定类型,必须与已有节点相同,不能借修改操作更换类型 # 更换类型只能通过 replace,其余处置显式指定类型时必须与已有节点相同
if ( if (
item.component_type is not None item.disposition != "replace"
and item.component_type is not None
and item.component_type != nodes[item.target_id].type and item.component_type != nodes[item.target_id].type
): ):
return "当前不支持更换已有组件类型" return "当前不支持更换已有组件类型"
@@ -88,6 +95,16 @@ def plan_block_reason(state: QueryState) -> str | None:
if removals != allowed_removals: if removals != allowed_removals:
return "规划删除目标与需求不一致,不能遗漏或扩大删除范围" return "规划删除目标与需求不一致,不能遗漏或扩大删除范围"
# 更换类型会先删除原组件,同样必须与需求逐个目标一致
allowed_replacements = {
target
for change in changes
if change.target_scope == "nodes" and change.operation == "replace"
for target in change.target_ids
}
if {item.target_id for item in replacements} != allowed_replacements:
return "规划更换目标与需求不一致,不能遗漏或扩大更换范围"
# 逐项核对需求是否被规划覆盖,任一不支持的需求都会阻止整批生成 # 逐项核对需求是否被规划覆盖,任一不支持的需求都会阻止整批生成
for change in changes: for change in changes:
# 画布更新需求必须对应非空的画布变更规划 # 画布更新需求必须对应非空的画布变更规划
@@ -98,8 +115,11 @@ def plan_block_reason(state: QueryState) -> str | None:
# 新增需求至少需要一个新增组件规划 # 新增需求至少需要一个新增组件规划
if not additions: if not additions:
return "规划遗漏新增需求" return "规划遗漏新增需求"
elif change.target_scope == "nodes" and change.operation == "delete": elif change.target_scope == "nodes" and change.operation in (
# 删除目标已在上面通过集合相等检查,此处不重复处理 "delete",
"replace",
):
# 删除和更换目标已在上面通过集合相等检查,此处不重复处理
continue continue
elif ( elif (
change.target_scope in {"nodes", "screen"} change.target_scope in {"nodes", "screen"}
@@ -134,14 +154,14 @@ def plan_block_reason(state: QueryState) -> str | None:
def plan_materials(state: QueryState) -> list[dict]: def plan_materials(state: QueryState) -> list[dict]:
"""提取规划需要的完整物料配置,供动作生成和修正使用""" """提取规划需要的完整物料配置,供动作生成和修正使用"""
# 只收集新增组件所需的类型,使用集合去除重复类型 # 只收集新增和更换组件所需的类型,使用集合去除重复类型
types = { types = {
item.component_type item.component_type
for item in state["screen_plan"].components for item in state["screen_plan"].components
if item.disposition == "add" if item.disposition in ("add", "replace")
} }
# 如果没有新增组件,就不需要向模型提供物料配置 # 如果没有新增或更换组件,就不需要向模型提供物料配置
if not types: if not types:
return [] return []
@@ -170,31 +190,61 @@ def merge_config(defaults: dict, overrides: dict) -> dict:
def assemble_plan_actions(state: QueryState, batch: ActionBatch) -> ActionBatch: def assemble_plan_actions(state: QueryState, batch: ActionBatch) -> ActionBatch:
"""用物料默认配置补全新增节点,模型显式提供的字段优先""" """用物料默认配置补全新增节点,模型显式提供的字段优先;
更换类型的新增节点沿用原 ID 和位置,并在它之前删除原组件"""
# 按物料编号建立索引,动作通过 component_key 选择具体预设 # 按物料编号建立索引,动作通过 component_key 选择具体预设
materials = {item.key: item for item in state["component_resources"].components} materials = {item.key: item for item in state["component_resources"].components}
# 更换规划按 plan_id 索引,原组件按 ID 索引,用于沿用原位置
replacements = {
item.plan_id: item
for item in state["screen_plan"].components
if item.disposition == "replace"
}
replaced_ids = {item.target_id for item in replacements.values()}
nodes = {node.id: node for node in state["input_context"].screen.nodes}
# 复制整批动作,补全配置时不改动调用方提供的动作对象 actions = []
result = batch.model_copy(deep=True) for source in batch.actions:
for action in result.actions: # 原组件的删除统一由系统在对应新增之前生成,丢弃模型自行生成的删除,避免重复或顺序错误
if isinstance(source, DeleteAction) and source.target_id in replaced_ids:
continue
# 非新增动作或找不到物料时不补全,物料合法性由规划动作校验负责 # 非新增动作或找不到物料时不补全,物料合法性由规划动作校验负责
if not isinstance(action, AddAction) or action.component_key not in materials: if not isinstance(source, AddAction) or source.component_key not in materials:
actions.append(source.model_copy(deep=True))
continue continue
# 获取所选物料,仅用模型显式提供的节点字段覆盖默认配置 # 复制动作,补全配置时不改动调用方提供的动作对象
action = source.model_copy(deep=True)
material = materials[action.component_key] material = materials[action.component_key]
node = merge_config( defaults = material.schema_.model_dump(mode="json")
material.schema_.model_dump(mode="json"), overrides = action.node.model_dump(mode="json", exclude_unset=True)
action.node.model_dump(mode="json", exclude_unset=True), item = replacements.get(action.plan_id)
) if item is not None and item.target_id in nodes:
# 更换后仍是同一个组件:沿用原 ID,位置以规划为准,未规划时保持原位置和尺寸
layout = item.layout or nodes[item.target_id].layout
pinned = {"id": item.target_id, "layout": layout.model_dump(mode="json")}
try:
node = ScreenNode.model_validate(
merge_config(defaults, {**overrides, **pinned})
)
except ValidationError:
# 迁移后的配置不合法时退回物料示例数据,保证更换后仍能正常展示
logger.warning(
f"更换组件配置不合法,改用物料默认配置: {item.target_id}"
)
node = ScreenNode.model_validate(merge_config(defaults, pinned))
actions.append(DeleteAction(target_id=item.target_id))
else:
# 对合并后的完整节点重新校验,防止默认配置与覆盖值组合后不合法
node = ScreenNode.model_validate(merge_config(defaults, overrides))
# 对合并后的完整节点重新校验,防止默认配置与覆盖值组合后不合法 action.node = node
action.node = ScreenNode.model_validate(node)
# 保守标记物料含示例数据,不能由模型声称已取得真实数据 # 保守标记物料含示例数据,不能由模型声称已取得真实数据
action.sample_data = material.sample_data action.sample_data = material.sample_data
actions.append(action)
return result return ActionBatch(actions=actions)
def _validate_canvas_action( def _validate_canvas_action(
@@ -218,7 +268,7 @@ def _validate_delete_action(
"""只允许删除 remove 目标,通过后记录目标以防重复删除""" """只允许删除 remove 目标,通过后记录目标以防重复删除"""
# 如果目标未被规划标记为删除,拒绝该动作 # 如果目标未被规划标记为删除,拒绝该动作
if action.target_id not in planned_ids: if action.target_id not in planned_ids:
return f"不能删除非 remove 规划目标: {action.target_id}" return f"不能删除非 remove 或 replace 规划目标: {action.target_id}"
# 如果前面的动作已经删除该目标,拒绝重复删除 # 如果前面的动作已经删除该目标,拒绝重复删除
if action.target_id in removed_ids: if action.target_id in removed_ids:
@@ -254,11 +304,11 @@ def _validate_add_action(
action: AddAction, action: AddAction,
planned: dict[str, PlanComponent], planned: dict[str, PlanComponent],
materials: dict[str, ComponentDefinition], materials: dict[str, ComponentDefinition],
existing_ids: set[str], existing: dict[str, ScreenNode],
seen_plan_ids: set[str], seen_plan_ids: set[str],
) -> str | None: ) -> str | None:
"""检查新增规划关联、ID、物料类型、布局及数据标记""" """检查新增或更换规划关联、ID、物料类型、布局及数据标记"""
# 新增动作必须关联有效的新增规划编号 # 新增动作必须关联有效的新增或更换规划编号
if action.plan_id not in planned: if action.plan_id not in planned:
return f"动作未关联有效的新增规划: {action.plan_id}" return f"动作未关联有效的新增规划: {action.plan_id}"
@@ -266,15 +316,19 @@ def _validate_add_action(
if action.plan_id in seen_plan_ids: if action.plan_id in seen_plan_ids:
return f"同一规划组件不能重复新增: {action.plan_id}" return f"同一规划组件不能重复新增: {action.plan_id}"
# 新组件不能复用原大屏已有的节点 ID item = planned[action.plan_id]
if action.node.id in existing_ids: if item.disposition == "replace":
# 更换后仍是同一个组件,必须沿用原 ID
if action.node.id != item.target_id:
return f"更换组件必须沿用原 ID: {item.target_id}"
elif action.node.id in existing:
# 新组件不能复用原大屏已有的节点 ID
return f"新增组件不能复用原有 ID: {action.node.id}" return f"新增组件不能复用原有 ID: {action.node.id}"
# 关联有效即记录出现,后续配置错误单独报告,不再重复报遗漏 # 关联有效即记录出现,后续配置错误单独报告,不再重复报遗漏
seen_plan_ids.add(action.plan_id) seen_plan_ids.add(action.plan_id)
# 获取关联规划与具体物料,核对三者声明的组件类型 # 获取具体物料,核对规划、物料和节点三者声明的组件类型
item = planned[action.plan_id]
material = materials.get(action.component_key) material = materials.get(action.component_key)
if ( if (
@@ -283,8 +337,11 @@ def _validate_add_action(
or action.node.type != item.component_type or action.node.type != item.component_type
): ):
return "新增节点类型、物料和规划类型不一致" return "新增节点类型、物料和规划类型不一致"
# 如果规划已明确新增布局,动作必须完整沿用该布局 # 如果规划已明确新增布局,动作必须完整沿用该布局;更换未规划布局时保持原位置
if item.layout is not None and action.node.layout != item.layout: layout = item.layout
if layout is None and item.disposition == "replace":
layout = existing[item.target_id].layout
if layout is not None and action.node.layout != layout:
return f"新增布局与规划不一致: {item.plan_id}" return f"新增布局与规划不一致: {item.plan_id}"
# 示例数据标记必须与物料一致,不能将示例内容标记为真实业务数据 # 示例数据标记必须与物料一致,不能将示例内容标记为真实业务数据
@@ -306,11 +363,11 @@ def validate_plan_actions(
if reason := plan_block_reason(state): if reason := plan_block_reason(state):
return [ActionValidationError(code="scope_error", message=reason)] return [ActionValidationError(code="scope_error", message=reason)]
# 新增动作通过 plan_id 查找对应规划 # 新增和更换动作通过 plan_id 查找对应规划,更换生成的也是新增动作
expected = { expected = {
item.plan_id: item item.plan_id: item
for item in state["screen_plan"].components for item in state["screen_plan"].components
if item.disposition == "add" if item.disposition in ("add", "replace")
} }
# 修改动作通过已有组件 ID 查找对应规划 # 修改动作通过已有组件 ID 查找对应规划
@@ -331,6 +388,12 @@ def validate_plan_actions(
for item in state["screen_plan"].components for item in state["screen_plan"].components
if item.disposition == "remove" if item.disposition == "remove"
} }
# 更换的原组件要先删除,再以同一 ID 新增
replaced = {
item.target_id
for item in state["screen_plan"].components
if item.disposition == "replace"
}
removed: set[str] = set() # 已覆盖的删除目标 removed: set[str] = set() # 已覆盖的删除目标
canvas_fields: set[str] = set() # 已覆盖的画布字段 canvas_fields: set[str] = set() # 已覆盖的画布字段
@@ -341,8 +404,8 @@ def validate_plan_actions(
# 累计所有动作及覆盖范围错误,便于一次性反馈给修正流程 # 累计所有动作及覆盖范围错误,便于一次性反馈给修正流程
errors = [] errors = []
# 保存原有组件 ID,防止新增组件复用已有 ID # 按 ID 索引原有组件,防止新增组件复用已有 ID,并读取更换组件的原位置
existing_ids = {node.id for node in state["input_context"].screen.nodes} existing = {node.id: node for node in state["input_context"].screen.nodes}
# 按顺序校验每个动作,保留索引以便定位错误 # 按顺序校验每个动作,保留索引以便定位错误
for index, action in enumerate(batch.actions): for index, action in enumerate(batch.actions):
if isinstance(action, UpdateCanvasAction): if isinstance(action, UpdateCanvasAction):
@@ -352,12 +415,10 @@ def validate_plan_actions(
) )
elif isinstance(action, DeleteAction): elif isinstance(action, DeleteAction):
# 检查删除目标是否允许、是否重复,并记录已删除目标 # 检查删除目标是否允许、是否重复,并记录已删除目标
message = _validate_delete_action(action, removals, removed) message = _validate_delete_action(action, removals | replaced, removed)
elif isinstance(action, AddAction): elif isinstance(action, AddAction):
# 核对新增规划、ID、物料和布局,并记录已关联的规划编号 # 核对新增规划、ID、物料和布局,并记录已关联的规划编号
message = _validate_add_action( message = _validate_add_action(action, expected, materials, existing, seen)
action, expected, materials, existing_ids, seen
)
elif isinstance(action, (MoveAction, ResizeAction, UpdateAction)): elif isinstance(action, (MoveAction, ResizeAction, UpdateAction)):
# 检查目标是否允许修改,更新属性时还需有设计要求 # 检查目标是否允许修改,更新属性时还需有设计要求
message = _validate_modify_action(action, modifications, modified) message = _validate_modify_action(action, modifications, modified)
@@ -377,6 +438,7 @@ def validate_plan_actions(
("新增规划组件", set(expected) - seen), ("新增规划组件", set(expected) - seen),
("调整规划组件", set(modifications) - modified), ("调整规划组件", set(modifications) - modified),
("删除规划组件", removals - removed), ("删除规划组件", removals - removed),
("更换前删除的组件", replaced - removed),
("画布规划字段", set(planned_canvas) - canvas_fields), ("画布规划字段", set(planned_canvas) - canvas_fields),
): ):
if missing: if missing:
+3 -3
View File
@@ -109,9 +109,9 @@ class PlannerNode(BaseNode):
# 如果新增组件没有指定类型,无法选择对应物料,立即报错 # 如果新增组件没有指定类型,无法选择对应物料,立即报错
if component.disposition == "add" and component.component_type is None: if component.disposition == "add" and component.component_type is None:
raise ValueError(f"新增规划组件未指定物料类型: {component.plan_id}") raise ValueError(f"新增规划组件未指定物料类型: {component.plan_id}")
# 新增或修改组件时,只要明确指定了类型,就必须在目录中存在 # 新增、修改或更换组件时,只要明确指定了类型,就必须在目录中存在
if ( if (
component.disposition in ("add", "modify") component.disposition in ("add", "modify", "replace")
and component.component_type is not None and component.component_type is not None
and component.component_type not in supported_types and component.component_type not in supported_types
): ):
@@ -145,7 +145,7 @@ class PlannerNode(BaseNode):
changed_ids = { changed_ids = {
component.target_id component.target_id
for component in plan.components for component in plan.components
if component.disposition in ("modify", "remove") if component.disposition in ("modify", "remove", "replace")
} }
# 如果实际修改或删除的目标不在允许范围内,则拒绝扩大修改范围 # 如果实际修改或删除的目标不在允许范围内,则拒绝扩大修改范围
if outside := changed_ids - set(scope.target_ids): if outside := changed_ids - set(scope.target_ids):
+12
View File
@@ -37,6 +37,15 @@ layout 表示最终布局,依据当前快照计算位移和尺寸
design 表示属性设计要求,转换为 props/style 更新;只有布局要求时不修改属性 design 表示属性设计要求,转换为 props/style 更新;只有布局要求时不修改属性
""" """
REPLACE_PLAN_PROMPT = """## 更换组件类型
每个 replace 规划生成一个 add:plan_id 用该规划编号,node.id 沿用原组件的 target_id,component_key 选择 component_type 对应的物料
系统会在它之前删除原组件并保持原位置和尺寸,不要为它生成 delete、move 或 resize
把原组件快照中的展示数据迁移到新物料的配置结构;原标题只是旧类型名称(如“折线图”)时改为新类型名称(如“饼图”),不用物料示例标题,用户自定义的标题保留:保留原数据的维度和数值,按新物料默认配置的写法改写字段映射,
例如原 encode 的 x/y 改为新图表需要的名称和数值映射;原类型特有而新物料不用的配置(如坐标轴)不要带过去
series 等数组会整体替换,提供时以物料默认配置中的对应项为基础补全
原数据无法对应到新类型时,不提供数据相关字段,沿用物料示例数据
"""
REPAIR_TASK_PROMPT = """# 当前阶段:修正重试 REPAIR_TASK_PROMPT = """# 当前阶段:修正重试
结合 previous_actions、validation_errors 和 repair_plan,修正并重新生成完整动作列表 结合 previous_actions、validation_errors 和 repair_plan,修正并重新生成完整动作列表
仍从 input_context.screen 原始快照开始,不在失败动作或部分成功结果上追加执行 仍从 input_context.screen 原始快照开始,不在失败动作或部分成功结果上追加执行
@@ -68,6 +77,9 @@ def build_action_prompt(
action_types.add("update_canvas") action_types.add("update_canvas")
if any(item.disposition == "add" for item in screen_plan.components): if any(item.disposition == "add" for item in screen_plan.components):
action_types.add("add") action_types.add("add")
if any(item.disposition == "replace" for item in screen_plan.components):
sections.append(REPLACE_PLAN_PROMPT)
action_types.add("add")
if any(item.disposition == "remove" for item in screen_plan.components): if any(item.disposition == "remove" for item in screen_plan.components):
action_types.add("delete") action_types.add("delete")
modifications = [ modifications = [
+2 -1
View File
@@ -24,11 +24,12 @@ properties 只包含需要修改的画布顶层字段,宽高为最终绝对值
依据快照选择 props / style 下的字段路径,不猜测不存在的嵌套结构 依据快照选择 props / style 下的字段路径,不猜测不存在的嵌套结构
path 中字符串是对象键,整数是数组下标;中间路径必须存在,数组下标不能越界 path 中字符串是对象键,整数是数组下标;中间路径必须存在,数组下标不能越界
只替换指定字段,不做深度合并;null 表示赋值,不表示删除 只替换指定字段,不做深度合并;null 表示赋值,不表示删除
不通过修改属性改变组件类型(如把 series 的 type 改成其他图表类型),组件类型由节点 type 和物料决定
style 为 null 时可用单层路径添加属性,保留未要求修改的配置 style 为 null 时可用单层路径添加属性,保留未要求修改的配置
""", """,
"add": """## add:按物料新增组件 "add": """## add:按物料新增组件
每个 add 规划对应一个新增动作,用 plan_id 关联规划,component_key 选择 component_materials 中的物料 每个 add 规划对应一个新增动作,用 plan_id 关联规划,component_key 选择 component_materials 中的物料
node.id 使用不与已有节点冲突的新 ID,node.type 必须与物料和规划一致 node.id 使用不与已有节点冲突的新 ID(更换类型时沿用原 ID),node.type 必须与物料和规划一致
component_materials 提供物料默认 schema,依据它和设计要求生成 node component_materials 提供物料默认 schema,依据它和设计要求生成 node
后端会用物料默认配置补全 node:对象逐层覆盖,数组整体替换,null 明确覆盖 后端会用物料默认配置补全 node:对象逐层覆盖,数组整体替换,null 明确覆盖
物料示例数据仅作占位展示,sample_data 由后端依据物料设置,不代表已获取真实数据 物料示例数据仅作占位展示,sample_data 由后端依据物料设置,不代表已获取真实数据
+2
View File
@@ -13,6 +13,8 @@ PLANNER_SYSTEM_PROMPT = f"""## 任务
canvas_changes 只填写需求范围内要修改的画布顶层字段及最终值,不填写相对变化或设计描述 canvas_changes 只填写需求范围内要修改的画布顶层字段及最终值,不填写相对变化或设计描述
已有组件使用真实 target_id;无需改动的组件可保留或省略,省略不代表删除 已有组件使用真实 target_id;无需改动的组件可保留或省略,省略不代表删除
remove 仅用于需求明确指定的删除目标,不能因为重新布局而擅自移除组件 remove 仅用于需求明确指定的删除目标,不能因为重新布局而擅自移除组件
需求的 replace 对应 disposition=replace:target_id 为原组件,component_type 从目录中选择用户要的新类型,原位置和尺寸由系统保留
modify 不能更换组件类型
布局未确定时写清排列关系;提供 layout 时表示最终的画布绝对坐标和尺寸,单位 px 布局未确定时写清排列关系;提供 layout 时表示最终的画布绝对坐标和尺寸,单位 px
提供 component_resources 时,新增组件从目录选择真实 type;默认宽高仅作布局参考 提供 component_resources 时,新增组件从目录选择真实 type;默认宽高仅作布局参考
组件摘要用于选型;组件的属性配置和数据绑定由后续动作阶段处理,规划阶段不需要确认 组件摘要用于选型;组件的属性配置和数据绑定由后续动作阶段处理,规划阶段不需要确认
+9 -3
View File
@@ -1,7 +1,11 @@
"""需求操作名词表的单一来源,需求协议、提示词、能力检查和范围校验都引用这里""" """需求操作名词表的单一来源,需求协议、提示词、能力检查和范围校验都引用这里"""
# 模型可能使用的同义操作名,统一成词表中的标准名称;新增别名只改这里 # 模型可能使用的同义操作名,统一成词表中的标准名称;新增别名只改这里
OPERATION_ALIASES: dict[str, str] = {"restyle": "update"} OPERATION_ALIASES: dict[str, str] = {
"restyle": "update",
"change_type": "replace",
"convert": "replace",
}
# 直接修改已有组件支持的操作,每一项都要有对应的动作规则 # 直接修改已有组件支持的操作,每一项都要有对应的动作规则
NODE_OPERATIONS: tuple[str, ...] = ("move", "resize", "update", "delete") NODE_OPERATIONS: tuple[str, ...] = ("move", "resize", "update", "delete")
@@ -18,13 +22,15 @@ PLAN_ADJUST_OPERATIONS: tuple[str, ...] = (
"layout", "layout",
) )
# 只能通过规划完成的操作:新增要从物料目录选型并安排位置,重排要整体布局 # 只能通过规划完成的操作:新增要从物料目录选型并安排位置,重排要整体布局,
PLAN_ONLY_OPERATIONS: tuple[str, ...] = ("add", "reflow", "layout") # 更换组件类型实际是换成另一种物料,要从目录选型,不能靠修改属性完成
PLAN_ONLY_OPERATIONS: tuple[str, ...] = ("add", "reflow", "layout", "replace")
# 写进协议描述和提示词的操作说明,词表变化时自动同步 # 写进协议描述和提示词的操作说明,词表变化时自动同步
OPERATION_GUIDE = ( OPERATION_GUIDE = (
f"修改已有组件用 {'、'.join(NODE_OPERATIONS)}," f"修改已有组件用 {'、'.join(NODE_OPERATIONS)},"
"修改画布用 update 或 resize,新增用 add;" "修改画布用 update 或 resize,新增用 add;"
"更换已有组件的类型(如折线图改饼图)用 replace,目标类型写入 parameters,不用 update;"
"颜色、背景、文字等样式和属性修改统一用 update,具体内容写入 parameters" "颜色、背景、文字等样式和属性修改统一用 update,具体内容写入 parameters"
) )
+6 -3
View File
@@ -24,8 +24,9 @@ class PlanComponent(ExtensibleModel):
plan_id: NonBlankString = Field( plan_id: NonBlankString = Field(
description="规划内的组件编号,用于关联资源,不作为新增组件的实际 ID" description="规划内的组件编号,用于关联资源,不作为新增组件的实际 ID"
) )
disposition: Literal["keep", "modify", "add", "remove"] = Field( disposition: Literal["keep", "modify", "add", "remove", "replace"] = Field(
description="保留、调整、新增或移除,只表达规划意图,不代表执行能力已经支持" description="保留、调整、新增、移除或更换类型,只表达规划意图,不代表执行能力已经支持;"
"replace 表示把 target_id 换成 component_type 对应的物料,保留原位置"
) )
target_id: NonBlankString | None = Field( target_id: NonBlankString | None = Field(
default=None, description="已有组件 ID,新增时必须为空" default=None, description="已有组件 ID,新增时必须为空"
@@ -53,7 +54,9 @@ class PlanComponent(ExtensibleModel):
if self.target_id is not None: if self.target_id is not None:
raise ValueError("新增组件不能指定已有 target_id") raise ValueError("新增组件不能指定已有 target_id")
elif self.target_id is None: elif self.target_id is None:
raise ValueError("保留、调整或移除组件必须提供 target_id") raise ValueError("保留、调整、移除或更换组件必须提供 target_id")
if self.disposition == "replace" and self.component_type is None:
raise ValueError("更换组件必须指定新的 component_type")
return self return self