fix:处理模型错误添加大量文本的bug
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
"""按物料展开字段定义:解析共享分组引用、取字段默认值、派生前端属性面板 setters"""
|
||||
|
||||
import re
|
||||
|
||||
from pydantic import JsonValue
|
||||
|
||||
from app.schemas.base import JsonObject
|
||||
from app.schemas.component_resource import (
|
||||
ComponentCatalog,
|
||||
ComponentDefinition,
|
||||
@@ -61,13 +64,35 @@ def panel_setters(groups: list[FieldGroup]) -> list[dict]:
|
||||
|
||||
|
||||
def frontend_catalog(catalog: ComponentCatalog) -> dict:
|
||||
"""前端物料接口的数据:在保存的定义上附加由字段定义派生的 setters"""
|
||||
"""前端物料接口的数据:在保存的定义上附加由字段定义派生的 setters 和 defaults"""
|
||||
data = catalog.model_dump(mode="json", by_alias=True)
|
||||
for item, component in zip(data["components"], catalog.components, strict=True):
|
||||
item["setters"] = panel_setters(resolve_field_groups(catalog, component))
|
||||
item["defaults"] = structural_defaults(catalog, component)
|
||||
return data
|
||||
|
||||
|
||||
def structural_defaults(
|
||||
catalog: ComponentCatalog, component: ComponentDefinition
|
||||
) -> JsonObject:
|
||||
"""默认配置去掉示例内容(字段定义里标了 example 的标题、数据……)后剩下的结构默认值
|
||||
|
||||
前端打开大屏时用它给旧节点补上物料后来新增的字段;不用完整的默认配置,否则用户删掉的标题
|
||||
会被补回成示例标题,真实大屏上又出现演示内容
|
||||
"""
|
||||
defaults = component.template()
|
||||
for field in component_fields(catalog, component):
|
||||
if field.example:
|
||||
*parents, last = field.segments
|
||||
try:
|
||||
container = lookup(defaults, parents)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(container, dict) and isinstance(last, str):
|
||||
container.pop(last, None)
|
||||
return defaults
|
||||
|
||||
|
||||
# ---- 字段查询:MCP 工具、动作提示词和动作校验共用同一套逻辑 ----
|
||||
|
||||
|
||||
@@ -128,7 +153,15 @@ def describe_field(
|
||||
"""给模型看的字段说明:省略空值和属性面板专用的信息"""
|
||||
data = field.model_dump(
|
||||
mode="json",
|
||||
exclude={"default", "panel", "span", "editable", "themed"},
|
||||
exclude={
|
||||
"default",
|
||||
"panel",
|
||||
"span",
|
||||
"editable",
|
||||
"themed",
|
||||
"example",
|
||||
"identifies",
|
||||
},
|
||||
exclude_none=True,
|
||||
exclude_defaults=True,
|
||||
)
|
||||
@@ -136,6 +169,9 @@ def describe_field(
|
||||
if field.themed:
|
||||
# 默认为 null:没单独设置时画面上按画布主题取色
|
||||
data["follows_theme"] = True
|
||||
if field.example:
|
||||
# 默认值是演示内容,要按实际内容写,不能当成合理的默认
|
||||
data["default_is_example"] = True
|
||||
if not field.editable:
|
||||
data["editable"] = False
|
||||
data["group"] = group.key
|
||||
@@ -257,3 +293,139 @@ def check_update_field(
|
||||
raise ValueError(f"字段 {path}({field.label})不允许修改")
|
||||
if error := field.value_error(action.value):
|
||||
raise ValueError(f"字段 {path}({field.label}){error}")
|
||||
|
||||
|
||||
def component_fields(
|
||||
catalog: ComponentCatalog, component: ComponentDefinition
|
||||
) -> list[MaterialField]:
|
||||
return [
|
||||
field
|
||||
for group in resolve_field_groups(catalog, component)
|
||||
for field in group.fields
|
||||
]
|
||||
|
||||
|
||||
def check_add_fields(
|
||||
catalog: ComponentCatalog, node_type: str, written: JsonValue
|
||||
) -> None:
|
||||
"""新增组件时模型显式写的字段,和 update 用同一套字段定义检查;不合法时一次列出全部问题
|
||||
|
||||
没写的字段来自默认配置,载入目录时已经检查过;物料没有定义的路径不检查,新增时可以整段写 option
|
||||
"""
|
||||
problems = []
|
||||
for field in component_fields(catalog, find_material(catalog, node_type)):
|
||||
try:
|
||||
value = lookup(written, field.segments)
|
||||
except ValueError:
|
||||
problems.append(
|
||||
f"字段 {field.path}({field.label})所在的上一层结构和物料不同,"
|
||||
"按 get_component_materials 返回的默认配置写"
|
||||
)
|
||||
continue
|
||||
if value is not MISSING and (error := field.value_error(value)):
|
||||
problems.append(f"字段 {field.path}({field.label}){error}")
|
||||
if problems:
|
||||
raise ValueError(";".join(problems))
|
||||
|
||||
|
||||
def retained_examples(
|
||||
catalog: ComponentCatalog | None, node: dict
|
||||
) -> list[tuple[MaterialField, JsonValue]]:
|
||||
"""组件上还和默认配置一样的示例内容(标题、系列名、数据……),按字段定义的 example 判断
|
||||
|
||||
只比较现在的值,不管是新增时没写、写了一样的值,还是之后又改回来;物料不存在时为空
|
||||
"""
|
||||
if catalog is None:
|
||||
return []
|
||||
try:
|
||||
component = find_material(catalog, node["type"])
|
||||
except ValueError:
|
||||
return []
|
||||
template = component.template()
|
||||
retained = []
|
||||
for field in component_fields(catalog, component):
|
||||
if not field.example:
|
||||
continue
|
||||
try:
|
||||
current = lookup(node, field.segments)
|
||||
except ValueError:
|
||||
continue
|
||||
default = lookup(template, field.segments)
|
||||
if current is not MISSING and current == default:
|
||||
retained.append((field, default))
|
||||
return retained
|
||||
|
||||
|
||||
# 摘要里每段文字的长度上限:文本组件的内容可能很长,认出组件只需要开头
|
||||
IDENTITY_MAX_CHARS = 40
|
||||
|
||||
|
||||
def identifying_texts(catalog: ComponentCatalog | None, node: dict) -> list[str]:
|
||||
"""组件在画面上的名字或内容:字段定义里标了 identifies 的字段的文字,过长时截断
|
||||
|
||||
路径途经数组而字段按对象写时(例如 ECharts 的多个 title),每一项都取
|
||||
"""
|
||||
if catalog is None:
|
||||
return []
|
||||
try:
|
||||
component = find_material(catalog, node["type"])
|
||||
except ValueError:
|
||||
return []
|
||||
texts = []
|
||||
for field in component_fields(catalog, component):
|
||||
if field.identifies:
|
||||
texts.extend(
|
||||
text
|
||||
if len(text) <= IDENTITY_MAX_CHARS
|
||||
else text[:IDENTITY_MAX_CHARS] + "…"
|
||||
for value in _values_at(node, field.segments)
|
||||
if isinstance(value, str) and (text := value.strip())
|
||||
)
|
||||
return texts
|
||||
|
||||
|
||||
# 识别字段(标题、名称、文本内容)在画面上是一行短文字。换行、超过这个长度或带句号分号时是一段话:
|
||||
# 线上问答之后"把结果放到大屏上",模型把"关键发现 1. …"、"注:……;……。"写进了文本组件
|
||||
LABEL_MAX_CHARS = 60
|
||||
SENTENCE_MARKS = re.compile(r"[。;;]")
|
||||
|
||||
|
||||
def is_label(text: str) -> bool:
|
||||
text = text.strip()
|
||||
return (
|
||||
"\n" not in text
|
||||
and len(text) <= LABEL_MAX_CHARS
|
||||
and not SENTENCE_MARKS.search(text)
|
||||
)
|
||||
|
||||
|
||||
def paragraph_texts(
|
||||
catalog: ComponentCatalog | None, node: dict, paths: set[str] | None = None
|
||||
) -> list[tuple[MaterialField, str]]:
|
||||
"""组件的识别字段里写成一段话的文字;paths 给出时只看这些字段"""
|
||||
if catalog is None:
|
||||
return []
|
||||
try:
|
||||
component = find_material(catalog, node["type"])
|
||||
except ValueError:
|
||||
return []
|
||||
return [
|
||||
(field, value)
|
||||
for field in component_fields(catalog, component)
|
||||
if field.identifies and (paths is None or field.path in paths)
|
||||
for value in _values_at(node, field.segments)
|
||||
if isinstance(value, str) and not is_label(value)
|
||||
]
|
||||
|
||||
|
||||
def _values_at(value: JsonValue, segments: list[str | int]) -> list[JsonValue]:
|
||||
if not segments:
|
||||
return [value]
|
||||
head, *rest = segments
|
||||
if isinstance(value, dict) and isinstance(head, str) and head in value:
|
||||
return _values_at(value[head], rest)
|
||||
if isinstance(value, list) and isinstance(head, int) and 0 <= head < len(value):
|
||||
return _values_at(value[head], rest)
|
||||
if isinstance(value, list) and isinstance(head, str):
|
||||
return [found for item in value for found in _values_at(item, segments)]
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user