fix:处理模型错误添加大量文本的bug

This commit is contained in:
2026-10-14 01:08:50 -07:00
parent a80727b7a0
commit 41b769adde
35 changed files with 388 additions and 149 deletions
+91 -66
View File
@@ -4,6 +4,7 @@
读取、修改和校验都基于同一份草稿:原始大屏加上之前写入草稿的修改;回复 done 时草稿交付给用户
"""
import json
from collections.abc import Sequence
from dataclasses import dataclass, field
from typing import Annotated, NotRequired
@@ -17,12 +18,12 @@ from pydantic import Field
from app.actions.batch import BatchError, misplaced_props, run_batch
from app.agent.data_tools import DataContext
from app.agent.query_values import (
SOURCE_PATH,
EmbeddedQuery,
encode_problems,
fill_queries,
rebound_nodes,
)
from app.resources.material_fields import paragraph_texts, retained_examples
from app.schemas.actions import (
Action,
AddAction,
@@ -73,11 +74,10 @@ def _result_text(
before: Screen,
after: Screen,
actions: list[Action],
undeclared_sample: frozenset[str] = frozenset(),
catalog: ComponentCatalog | None = None,
queries: Sequence[EmbeddedQuery] = (),
unchanged: bool = False,
repeated: bool = False,
relabeled: Sequence[str] = (),
) -> str:
"""执行结果:除了动作数量,还给出执行后的画布和每个组件的布局
@@ -107,25 +107,34 @@ def _result_text(
lines = [
f"执行成功,共 {len(actions)} 个动作已写入草稿;回复 done 时草稿交付给用户,不需要再提交确认"
]
sample = [
if declared := [
action.node.id
for action in actions
if isinstance(action, AddAction) and action.sample_data
]
# 模型没写 sample_data 时是后端按物料猜的;说明这一点和怎么改,否则模型只看到“用了示例数据”,
# 以为是自己的数据没生效,会反复提交同样的内容
if guessed := [item for item in sample if item in undeclared_sample]:
]:
lines.append(
f"未写 sample_data,按物料默认标成了示例数据:{'、'.join(guessed)};"
"如果这些组件展示的数据都来自大屏上已有的组件或上传的数据文件,"
"用 $query 整体替换它们的 option.dataset.source,或再提交一批:先 delete,再用 sample_data=false 重新 add,否则不用处理"
f"声明为自己编写数据的新增组件,交付时标为示例数据:{'、'.join(declared)}"
)
if declared := [item for item in sample if item not in undeclared_sample]:
lines.append(f"标为示例数据的新增组件:{'、'.join(declared)}")
if relabeled:
# 新增组件没写到的字段沿用默认配置,其中的标题、系列名、数据是演示内容;不说出来时模型以为都按自己的意思写好了,
# 结果真实数据的卡片带着示例的前缀和环比、图例还叫"访问量"。只陈述事实和后果,由模型决定改不改
if examples := _retained_example_notes(actions, after, catalog):
lines.append(
f"数据整体换成了 $query 的查询结果,已改标为非示例数据:{'、'.join(relabeled)}"
"新增组件里这些值还是物料的示例内容:示例文字和组件要展示的内容不符就改写;"
"示例数据会让组件交付时标为示例数据,要展示真实数据就换掉(数据来自上传的文件时用 $query),"
"没有真实数据时可以保留并在回复里说明"
)
lines.extend(examples)
# 问答之后"把结果放到大屏上",模型会把分析结论抄成文本组件;skill 里写了规则仍会发生,写入时再陈述一次,
# 用户明确要在大屏上放这段文字时由模型保留。要说清空出的位置怎么办:只说"改成简短的标题"时,
# 模型把结论块缩成一行小标题,留下一大块空位,反复加删组件去填,直到调用次数用尽
if paragraphs := _paragraph_notes(actions, after, catalog):
lines.append(
"这一批写入的标题或文字是一段话:大屏上的文字只放标题和简短标签,"
"分析结论、洞察、数据说明和注意事项写在回复里。用户明确要求在大屏上放这段文字时保留;"
"否则只放这段文字的文本组件直接删除,原来的位置给图表或让相邻组件占满,"
"图表标题、指标名这类改成简短的名称"
)
lines.extend(paragraphs)
# 模型提交时只写了 SQL,看不到结果;给出列名和行数,用来核对 encode 和数据量
if queries:
lines.append("$query 已替换为查询结果:")
@@ -158,6 +167,68 @@ def _result_text(
return "\n".join(lines)
def _retained_example_notes(
actions: Sequence[Action], after: Screen, catalog: ComponentCatalog | None
) -> list[str]:
"""这一批新增的组件上还和默认配置一样的示例内容,每个组件一行"""
nodes = {node.id: node for node in after.nodes}
notes = []
for action in actions:
if not isinstance(action, AddAction) or action.node.id not in nodes:
continue
node = nodes[action.node.id].model_dump(mode="json")
if retained := retained_examples(catalog, node):
notes.append(
f"- {action.node.id}:"
+ "、".join(
f"{field.path}({field.label}){_brief(value)}"
for field, value in retained
)
)
return notes
def _paragraph_notes(
actions: Sequence[Action], after: Screen, catalog: ComponentCatalog | None
) -> list[str]:
"""这一批写入的识别字段里成段的文字,每个组件一行
新增的组件看全部识别字段,修改只看这一批写到的字段:只改颜色时不提原有的说明文字
"""
written: dict[str, set[str] | None] = {}
for action in actions:
if isinstance(action, AddAction):
written[action.node.id] = None
elif (
isinstance(action, UpdateAction)
and written.get(action.target_id, set()) is not None
):
written.setdefault(action.target_id, set()).add(action.field_path)
nodes = {node.id: node for node in after.nodes}
notes = []
for node_id, paths in written.items():
if node_id not in nodes:
continue
node = nodes[node_id].model_dump(mode="json")
if found := paragraph_texts(catalog, node, paths):
notes.append(
f"- {node_id}:"
+ "、".join(
f"{field.path}({field.label}){len(text.strip())} 字 {_brief(' '.join(text.split()))}"
for field, text in found
)
)
return notes
def _brief(value) -> str:
if isinstance(value, list):
return f"示例数据 {len(value)} 行"
if isinstance(value, str):
return f"“{value if len(value) <= 20 else value[:20] + '…'}”"
return json.dumps(value, ensure_ascii=False)
def _reply(
runtime: ToolRuntime, text: str, *, error: bool = False, **update
) -> Command:
@@ -264,74 +335,28 @@ def apply_actions(
f"- {item}" for item in problems
)
return _reply(runtime, message + hint, error=True)
undeclared = frozenset(
action.node.id
for action in actions
if isinstance(action, AddAction)
and "sample_data" not in action.model_fields_set
)
log = runtime.state.get("actions", [])
delivered, relabeled = _relabel_real_data(
[*log, *completed], _replaced_sources(actions, queries)
)
delivered = [*log, *completed]
# 按交付结果判断有没有变化,不比较参数:同样的 move 再执行一次会继续移动,同样的 update 再执行一次没有变化
# 交付结果除了大屏还有只在动作上的示例数据标记,只比大屏会把"重新 add 改 sample_data"当成没变化而丢掉
# 交付结果除了大屏还有只在动作上的"自己编写的数据"声明,只比大屏会把"重新 add 改 sample_data"当成没变化而丢掉
unchanged = after == before and _sample_marks(delivered) == _sample_marks(log)
# 这次运行之前已有成功提交时,没有变化多半是在重复自己的修改;还没有时说明原始大屏本来就是这样
text = _result_text(
before,
after,
delivered[len(log) :],
undeclared,
completed,
runtime.context.catalog,
queries,
unchanged,
repeated=bool(log),
relabeled=relabeled,
)
if unchanged:
return _reply(runtime, text + hint)
return _reply(runtime, text + hint, working_screen=after, actions=delivered)
def _replaced_sources(
actions: Sequence[AgentAction], queries: Sequence[EmbeddedQuery]
) -> set[str]:
"""这一批用 update 把整个 dataset.source 换成 $query 结果的组件;只替换其中一部分时不算"""
return {
item.node_id
for item in queries
if item.path == SOURCE_PATH and isinstance(actions[item.index], UpdateAction)
}
def _relabel_real_data(
actions: list[Action], node_ids: set[str]
) -> tuple[list[Action], list[str]]:
"""数据整体换成查询结果的组件,把它最近一次 add 的示例数据标记改为 false
标记只在 add 动作上、不影响大屏,改它不影响"交付记录依次执行得到草稿";
原始大屏上的组件没有 add,跨请求的标记要等大屏和前端契约支持
"""
result = list(actions)
relabeled = []
for node_id in sorted(node_ids):
index = next(
(
position
for position in range(len(result) - 1, -1, -1)
if isinstance(result[position], AddAction)
and result[position].node.id == node_id
),
None,
)
if index is not None and result[index].sample_data:
result[index] = result[index].model_copy(update={"sample_data": False})
relabeled.append(node_id)
return result, relabeled
def _sample_marks(actions: Sequence[Action]) -> dict[str, bool]:
"""按交付记录,当前每个新增组件是否标为示例数据;这个标记只在 add 动作上,不在大屏里"""
"""按交付记录,每个新增组件是否声明为自己编写的数据;这个声明只在 add 动作上,不在大屏里"""
marks: dict[str, bool] = {}
for action in actions:
if isinstance(action, AddAction):