feat:添加图表数据
This commit is contained in:
@@ -55,9 +55,9 @@ class BaseNode(ABC):
|
||||
"""子类实现节点核心逻辑,返回状态更新"""
|
||||
|
||||
def fail_on_invalid_output(self, state: T, error: ValueError, reason: str) -> T:
|
||||
"""模型输出未通过校验时收尾为失败结果,详细原因只写日志"""
|
||||
"""模型输出未通过校验时收尾为失败结果,原因交给失败收尾由模型向用户说明"""
|
||||
self.logger.warning(f"{reason}: {error}")
|
||||
return {**state, "status": "failed", "failure_reason": reason}
|
||||
return {**state, "status": "failed", "failure_reason": f"{reason}:{error}"}
|
||||
|
||||
def log_step(self, step_name: str, message: str = ""):
|
||||
"""记录步骤日志。"""
|
||||
|
||||
@@ -248,8 +248,8 @@ def build_graph(checkpointer=None):
|
||||
},
|
||||
)
|
||||
|
||||
# 失败节点统一写入最终失败状态后结束流程
|
||||
workflow.add_edge("fallback_node", END)
|
||||
# 失败节点写入最终失败状态后,由回答节点向用户说明原因
|
||||
workflow.add_edge("fallback_node", "qa_node")
|
||||
|
||||
# 用户补充后重新合并输入并判断意图,取消则直接结束
|
||||
workflow.add_conditional_edges(
|
||||
|
||||
@@ -192,6 +192,16 @@ class ActionNode(BaseNode):
|
||||
updated_state["validation_errors"] = [GENERATION_ERROR]
|
||||
return updated_state
|
||||
|
||||
# 模型确认现有能力无法满足需求时直接失败收尾,不进入重试
|
||||
if batch.unsupported_reason:
|
||||
updated_state.update(
|
||||
actions=[],
|
||||
validation_passed=False,
|
||||
failure_reason=f"动作生成判断无法完成:{batch.unsupported_reason}",
|
||||
status="failed",
|
||||
)
|
||||
return updated_state
|
||||
|
||||
# 保存生成的动作,实际目标、规划一致性及模拟执行结果仍由后续校验节点检查
|
||||
updated_state["actions"] = batch.actions
|
||||
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
"""统一收尾能力不足、资源缺失和动作修正失败"""
|
||||
"""统一收尾能力不足、资源缺失和动作修正失败
|
||||
|
||||
这里只整理内部失败原因,面向用户的说明由回答节点交给模型总结,代码中不写死给用户的文案
|
||||
"""
|
||||
|
||||
from app.processor.base import BaseNode
|
||||
from app.processor.state import QueryState
|
||||
|
||||
|
||||
def failure_notice(state: QueryState) -> str:
|
||||
"""汇总上游记录的失败原因、最近一次修正分析和校验错误,作为交给模型的事实说明"""
|
||||
facts = []
|
||||
if reason := state.get("failure_reason"):
|
||||
facts.append(reason)
|
||||
plan = state.get("repair_plan")
|
||||
if plan is not None and plan.reason not in facts:
|
||||
facts.append(f"失败分析:{plan.reason}")
|
||||
if errors := state.get("validation_errors"):
|
||||
facts.append("校验错误:" + ";".join(error.message for error in errors))
|
||||
facts.append(f"已自动修正 {state.get('retry_count', 0)} 次")
|
||||
detail = "\n".join(facts) if facts else "没有记录具体原因"
|
||||
return f"本次修改请求没有执行,内部原因如下:\n{detail}"
|
||||
|
||||
|
||||
class FallbackNode(BaseNode):
|
||||
name = "fallback_node"
|
||||
|
||||
def process(self, state: QueryState) -> QueryState:
|
||||
# 浅拷贝状态
|
||||
updated_state: QueryState = state.copy()
|
||||
# 读取上游记录的具体失败原因
|
||||
reason = state.get("failure_reason")
|
||||
notice = failure_notice(state)
|
||||
self.logger.warning(notice)
|
||||
|
||||
# 如果上游没有提供有效原因,则根据校验错误补充失败说明
|
||||
if not reason:
|
||||
# 如果仍有校验错误,使用达到自动修正上限的说明,并带上已重试次数
|
||||
if state.get("validation_errors"):
|
||||
reason = f"动作校验未通过,已达到自动修正上限,共重试 {state.get('retry_count', 0)} 次"
|
||||
else:
|
||||
# 如果既没有具体原因也没有校验错误,使用通用说明,避免返回空原因
|
||||
reason = "当前需求未能完成,缺少具体失败原因"
|
||||
|
||||
# 将任务标记为最终失败,结束本轮处理
|
||||
# 将任务标记为最终失败,由回答节点向用户说明原因
|
||||
updated_state["status"] = "failed"
|
||||
# 保存上游原因或本节点补充的说明,供后续输出给用户
|
||||
updated_state["failure_reason"] = reason
|
||||
updated_state["failure_reason"] = notice
|
||||
updated_state["notice"] = notice
|
||||
# 显式标记校验未通过,防止残留动作被当作可执行结果
|
||||
updated_state["validation_passed"] = False
|
||||
# 清空旧模拟快照,避免失败结果中仍携带之前的成功快照
|
||||
|
||||
@@ -65,7 +65,7 @@ class PlannerNode(BaseNode):
|
||||
# 解析失败、引用不存在的目标等可由模型自行纠正,超过次数才失败收尾
|
||||
if attempt == PLAN_MAX_REPAIRS:
|
||||
return self.fail_on_invalid_output(
|
||||
state, error, "未能生成可用的大屏规划,请调整需求后重试"
|
||||
state, error, "规划输出多次未通过校验"
|
||||
)
|
||||
self.logger.warning(f"规划未通过校验,重新规划: {error}")
|
||||
feedback = f"上一次输出未通过校验:{error}\n请修正后重新输出完整规划。"
|
||||
|
||||
@@ -38,7 +38,7 @@ class QANode(BaseNode):
|
||||
|
||||
content = state["input_context"].model_dump_json(exclude_unset=True)
|
||||
offer = None
|
||||
# 落地策略判定请求无法执行时,附上事实说明,由本节点如实告知用户
|
||||
# 请求无法执行或执行失败时,附上事实说明,由本节点如实告知用户
|
||||
if notice := state.get("notice"):
|
||||
content += f"\n\n系统说明:{notice}"
|
||||
# 回复中的替代修改需要结构化保存,下一轮用户同意时直接执行,因此不流式输出
|
||||
@@ -59,8 +59,9 @@ class QANode(BaseNode):
|
||||
updated_state["answer"] = answer
|
||||
# 保存等待用户确认的提议,没有提议时清空
|
||||
updated_state["offer"] = offer
|
||||
# 回答生成完成,问答分支在此结束
|
||||
updated_state["status"] = "completed"
|
||||
# 回答生成完成;失败收尾后的说明保持失败状态,不交付动作
|
||||
if state.get("status") != "failed":
|
||||
updated_state["status"] = "completed"
|
||||
|
||||
# 返回包含回答和完成状态的更新结果
|
||||
return updated_state
|
||||
|
||||
@@ -15,7 +15,6 @@ from app.processor.state import QueryState
|
||||
from app.prompt.action import FIELD_QUERY_PROMPT, uses_material_fields
|
||||
from app.prompt.reflection import REFLECTION_SYSTEM_PROMPT
|
||||
from app.schemas.action_repair import ActionRepairPlan
|
||||
from app.schemas.capabilities import USER_FAILURE_MESSAGE
|
||||
|
||||
|
||||
class ReflectionNode(BaseNode):
|
||||
@@ -88,11 +87,9 @@ class ReflectionNode(BaseNode):
|
||||
updated_state["repair_plan"] = plan
|
||||
# 仅在建议允许重试时将次数加一,缺少历史计数时从零开始,不可重试时保持原次数
|
||||
updated_state["retry_count"] = state.get("retry_count", 0) + int(plan.retryable)
|
||||
# 可以重试时清空旧失败原因;不可重试时建议中的分析含内部术语,只记录日志,对用户使用统一说明
|
||||
if not plan.retryable:
|
||||
self.logger.warning("动作无法修正: {}", plan.reason)
|
||||
# 可以重试时清空旧失败原因;不可重试时保留分析,由失败收尾交给模型向用户说明
|
||||
updated_state["failure_reason"] = (
|
||||
None if plan.retryable else USER_FAILURE_MESSAGE
|
||||
None if plan.retryable else f"修正分析判断无法完成:{plan.reason}"
|
||||
)
|
||||
|
||||
# 返回修正建议、重试次数和失败原因,由后续路由决定重新生成动作还是结束
|
||||
|
||||
@@ -18,9 +18,7 @@ class RequirementNode(BaseNode):
|
||||
return await self._generate(state)
|
||||
except ValueError as error:
|
||||
# 结构化输出解析失败或引用校验不通过(含 ValidationError、OutputParserException)
|
||||
return self.fail_on_invalid_output(
|
||||
state, error, "未能可靠理解你的需求,请换个说法再试"
|
||||
)
|
||||
return self.fail_on_invalid_output(state, error, "需求理解的输出未通过校验")
|
||||
|
||||
async def _generate(self, state: QueryState) -> QueryState:
|
||||
# 浅拷贝状态
|
||||
|
||||
@@ -22,9 +22,7 @@ class ScopeNode(BaseNode):
|
||||
return await self._generate(state)
|
||||
except ValueError as error:
|
||||
# 结构化输出解析失败或引用校验不通过(含 ValidationError、OutputParserException)
|
||||
return self.fail_on_invalid_output(
|
||||
state, error, "未能确定需要修改的范围,请描述得更具体一些"
|
||||
)
|
||||
return self.fail_on_invalid_output(state, error, "修改范围的输出未通过校验")
|
||||
|
||||
async def _generate(self, state: QueryState) -> QueryState:
|
||||
# 浅拷贝状态
|
||||
|
||||
@@ -12,6 +12,7 @@ ACTION_SYSTEM_PROMPT = f"""# 任务目标
|
||||
# 输出要求
|
||||
按提供的 ActionBatch Schema 返回 actions 列表,仅使用本次列出的动作类型
|
||||
覆盖本次全部变更,不返回大屏 JSON、解释文本或执行成功声明
|
||||
有任何变更无法用列出的动作或可用字段完成时,不执行其中一部分:actions 为空,在 unsupported_reason 中说明缺少什么
|
||||
以 input_context.screen 为起点,后续动作基于前序结果计算,不能重复执行或遗漏目标
|
||||
|
||||
# 输入边界
|
||||
@@ -58,7 +59,7 @@ field_groups 按物料类型列出本次相关组件的字段分组,节点的
|
||||
确定要改的字段:用用户原话中对属性的说法(input_context 中的提问,不用改写后的需求)调用 search_fields;
|
||||
返回多个字段时,依据字段说明判断用户指的是哪一个;用户看的是大屏画面,没有提到图层列表、属性面板等编辑器位置时,按画面上看得到的内容理解
|
||||
需要了解一组相关字段时用 get_fields,一次可查多个分组
|
||||
只能使用查询到的字段路径,值须符合字段的类型、可选值和取值范围;查不到对应字段说明不支持,不自行编造路径
|
||||
只能使用查询到的字段路径,值须符合字段的类型、可选值和取值范围;查不到对应字段说明物料不支持,按无法完成处理,不自行编造路径
|
||||
"""
|
||||
|
||||
# 这些动作会写组件属性,需要先查物料字段
|
||||
|
||||
+3
-2
@@ -19,8 +19,9 @@ QA_SYSTEM_PROMPT = """## 任务
|
||||
可以简短提示还能继续帮忙,不列举能力清单
|
||||
|
||||
## 无法执行的请求
|
||||
输入附带“系统说明”时,表示用户的修改请求因目标不存在而没有执行
|
||||
用一两句话如实说明原因,不声称已修改
|
||||
输入附带“系统说明”时,表示用户的修改请求没有执行,系统说明记录了内部原因
|
||||
用一两句话如实说明原因,不声称已修改;用用户能理解的话转述,不提字段路径、节点 ID、错误代码、重试次数等内部信息
|
||||
原因是当前能力不支持时直接说明做不到什么;原因是生成或校验出错时说明这次没能完成,可以换个说法再试
|
||||
有明显可行的替代修改时,只提一个,answer 以是否需要执行的问句结尾,例如“需要我新增一张折线图吗?”;不提供配置建议等非修改类替代
|
||||
同时把该替代修改写成 offer:一条独立完整、可直接执行的修改指令,例如“在当前大屏新增一张折线图”;没有替代修改时 offer 为 null
|
||||
新增组件使用物料示例数据,不要求用户提供数据;不要求用户离开对话去选中或操作组件
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-xy"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-xy"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
{
|
||||
"key": "bar-line-data",
|
||||
"label": "数据映射",
|
||||
|
||||
@@ -293,6 +293,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "chart-dataset",
|
||||
"label": "图表数据",
|
||||
"description": "图表画面上展示的数据,柱子高度、折线点、扇区大小等数值都来自这里",
|
||||
"fields": [
|
||||
{
|
||||
"path": "props.option.dataset.source",
|
||||
"label": "数据",
|
||||
"description": "图表展示的数据表,每个对象是一行(一个类目或数据项),键是列名,列名与数据映射字段对应;修改时提交完整的新数组:保留不需要修改的行和列,只改用户提到的数值或行;新增列或改列名时同时修改数据映射字段",
|
||||
"value_type": "array",
|
||||
"item_type": "object",
|
||||
"aliases": [
|
||||
"数据",
|
||||
"图表数据",
|
||||
"数据值",
|
||||
"示例数据",
|
||||
"数据集"
|
||||
],
|
||||
"panel": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "chart-encode-xy",
|
||||
"label": "数据映射",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-item"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-item"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-xy"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-xy",
|
||||
{
|
||||
"key": "heatmap-value",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"chart-grid",
|
||||
"chart-axis",
|
||||
"chart-line-series",
|
||||
"chart-dataset",
|
||||
"chart-encode-xy"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-xy"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"chart-legend",
|
||||
"chart-palette",
|
||||
"chart-item-series",
|
||||
"chart-dataset",
|
||||
"chart-encode-item"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-item"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
{
|
||||
"key": "radar-data",
|
||||
"label": "数据映射",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-item"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-item"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
"chart-encode-xy"
|
||||
],
|
||||
"eventOptions": [
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"chart-dataset",
|
||||
{
|
||||
"key": "stacked-data",
|
||||
"label": "数据映射",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""动作类型统一入口,新增操作时在这里显式加入联合类型"""
|
||||
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Self
|
||||
|
||||
from pydantic import Field, TypeAdapter
|
||||
from pydantic import Field, TypeAdapter, model_validator
|
||||
|
||||
from .add import AddAction
|
||||
from .base import ActionModel, BaseAction
|
||||
@@ -22,6 +22,16 @@ action_adapter = TypeAdapter(Action)
|
||||
|
||||
class ActionBatch(ActionModel):
|
||||
actions: list[Action] = Field(description="按数组顺序执行的动作列表,允许为空")
|
||||
unsupported_reason: str | None = Field(
|
||||
default=None,
|
||||
description="需求中有任何部分无法用现有动作或查到的字段完成时,说明缺少什么能力,此时 actions 为空;能完成时为 null",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def unsupported_has_no_actions(self) -> Self:
|
||||
if self.unsupported_reason and self.actions:
|
||||
raise ValueError("填写 unsupported_reason 时 actions 必须为空,不能只执行需求的一部分")
|
||||
return self
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -21,6 +21,5 @@ class AgentResult(BaseModel):
|
||||
message_id: str # 本次用户输入的 ID,不是助手回复 ID
|
||||
status: Literal["completed", "waiting_for_input", "failed", "cancelled"]
|
||||
actions: list[Action] = Field(default_factory=list)
|
||||
answer: str | None = None
|
||||
answer: str | None = None # 回答,或失败时由模型总结的原因说明
|
||||
clarification: Clarification | None = None
|
||||
failure_reason: str | None = None
|
||||
|
||||
@@ -20,6 +20,3 @@ RESOURCE_QUERY_SUPPORTED = False
|
||||
RESOURCE_FALLBACK_ASSUMPTION = (
|
||||
"尚未接入物料目录之外的资源查询,所需内容使用物料默认配置和示例数据"
|
||||
)
|
||||
|
||||
# 面向用户的失败说明,不暴露内部校验术语
|
||||
USER_FAILURE_MESSAGE = "抱歉,这次没能完成修改,请换个说法或稍后再试"
|
||||
|
||||
@@ -158,7 +158,7 @@ class AgentService:
|
||||
if result.status == "cancelled":
|
||||
return "用户取消了本次补充"
|
||||
if result.status == "failed":
|
||||
return _clip(f"处理失败:{result.failure_reason or '未知原因'}")
|
||||
return _clip(f"处理失败:{result.answer or '未知原因'}")
|
||||
if result.answer:
|
||||
return _clip(result.answer)
|
||||
requirement = state.get("requirement")
|
||||
@@ -176,8 +176,10 @@ class AgentService:
|
||||
prompt=state["request"]["prompt"],
|
||||
outcome=result.status,
|
||||
summary=self._summarize(state, result),
|
||||
# 只有正常结束的回复中的提议才等待用户确认
|
||||
offer=state.get("offer") if result.status == "completed" else None,
|
||||
# 回答或失败说明中的提议等待用户确认,等待补充和取消时没有提议
|
||||
offer=state.get("offer")
|
||||
if result.status in ("completed", "failed")
|
||||
else None,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
@@ -198,7 +200,6 @@ class AgentService:
|
||||
actions=state["actions"] if deliver else [],
|
||||
answer=state.get("answer"),
|
||||
clarification=state.get("clarification") if waiting else None,
|
||||
failure_reason=state.get("failure_reason"),
|
||||
)
|
||||
|
||||
async def stream(self, thread_id, payload, session_id, user_id=None):
|
||||
|
||||
Reference in New Issue
Block a user