|
|
@@ -101,6 +101,27 @@ _FIELD_TITLES = {
|
|
|
|
|
|
_IMPORTANT_FIELDS = tuple(_FIELD_TITLES)
|
|
|
_MAX_SUMMARY_CHARS = 900
|
|
|
+_CORRECTION_PREFIX = "上一版未通过"
|
|
|
+_MEDIA_REVIEW_PREFIX = "最终媒体证据自检:"
|
|
|
+
|
|
|
+_RESPONSE_SUBJECTS = {
|
|
|
+ "global_data_plan": "Global Data 规划",
|
|
|
+ "production_plan": "Production 规划",
|
|
|
+ "executor_delivery": "Executor 交付",
|
|
|
+ "task_validation_report": "Task 验收结论",
|
|
|
+ "stage_validation_report": "阶段验收结论",
|
|
|
+ "global_data_stage_delivery": "Global Data 阶段交付",
|
|
|
+ "progress_decision": "Production 调度决定",
|
|
|
+}
|
|
|
+
|
|
|
+_CORRECTION_REASON_TITLES = {
|
|
|
+ "missing_binding_evidence": "Artifact Binding 缺少工具调用证据",
|
|
|
+ "undispositioned_source_asset": "存在来源素材尚未纳入交付安排",
|
|
|
+ "source_asset_count_mismatch": "来源素材数量与交付要求不一致",
|
|
|
+ "missing_source_asset_binding": "存在来源素材尚未绑定正式产物",
|
|
|
+ "insufficient_artifact_bindings": "正式产物的验收目标绑定不足",
|
|
|
+ "planner_missing_required_read": "Planner 尚未读取完整必需范围",
|
|
|
+}
|
|
|
|
|
|
|
|
|
def tool_title(name: object) -> str:
|
|
|
@@ -266,6 +287,139 @@ def readable_tool_calls(tool_calls: list[dict[str, Any]]) -> list[dict[str, Any]
|
|
|
]
|
|
|
|
|
|
|
|
|
+def _message_text(message: Any) -> str:
|
|
|
+ return _text(getattr(message, "content", None)).strip()
|
|
|
+
|
|
|
+
|
|
|
+def _human_messages(messages: list[Any]) -> list[Any]:
|
|
|
+ return [
|
|
|
+ message
|
|
|
+ for message in messages
|
|
|
+ if getattr(message, "type", None) == "human"
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def _correction_reason_codes(text: str) -> list[str]:
|
|
|
+ codes: list[str] = []
|
|
|
+ for code in re.findall(r"\b([a-z][a-z0-9_]{2,})\s*:", text):
|
|
|
+ if code not in codes:
|
|
|
+ codes.append(code)
|
|
|
+ return codes
|
|
|
+
|
|
|
+
|
|
|
+def _correction_reason(text: str, codes: list[str]) -> str:
|
|
|
+ known = [
|
|
|
+ _CORRECTION_REASON_TITLES[code]
|
|
|
+ for code in codes
|
|
|
+ if code in _CORRECTION_REASON_TITLES
|
|
|
+ ]
|
|
|
+ if known:
|
|
|
+ return ";".join(known)
|
|
|
+ if "未出现在工具或依赖产物中的 URI" in text:
|
|
|
+ return "交付引用了尚未由工具或依赖产物验证的地址"
|
|
|
+ if "格式校验" in text:
|
|
|
+ return "上一版输出格式未通过运行时校验"
|
|
|
+ if "合同校验" in text:
|
|
|
+ return "上一版输出未通过运行时合同校验"
|
|
|
+ return "上一版输出未通过运行时校验"
|
|
|
+
|
|
|
+
|
|
|
+def _reviewed_media_count(text: str, message: Any) -> int:
|
|
|
+ parsed = _parse(text)
|
|
|
+ artifact_uris = (
|
|
|
+ parsed.get("artifact_uris")
|
|
|
+ if isinstance(parsed, Mapping)
|
|
|
+ else None
|
|
|
+ )
|
|
|
+ declared = len(artifact_uris) if isinstance(artifact_uris, list) else 0
|
|
|
+ content = getattr(message, "content", None)
|
|
|
+ attached = (
|
|
|
+ sum(
|
|
|
+ 1
|
|
|
+ for block in content
|
|
|
+ if isinstance(block, Mapping)
|
|
|
+ and block.get("type") in {"image", "image_url"}
|
|
|
+ )
|
|
|
+ if isinstance(content, list)
|
|
|
+ else 0
|
|
|
+ )
|
|
|
+ # artifact_uris 是业务产物数量;随消息附带的图片可能包含视频抽帧,
|
|
|
+ # 只能在没有显式产物清单时作为回退,不能把 1 个视频的 N 帧算成 N 个产物。
|
|
|
+ return declared or attached
|
|
|
+
|
|
|
+
|
|
|
+def _turn_projection(
|
|
|
+ messages: list[Any],
|
|
|
+ *,
|
|
|
+ record_kind: str | None,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ humans = _human_messages(messages)
|
|
|
+ last_human = humans[-1] if humans else None
|
|
|
+ last_text = _message_text(last_human) if last_human is not None else ""
|
|
|
+ correction_index = sum(
|
|
|
+ 1
|
|
|
+ for message in humans
|
|
|
+ if _message_text(message).startswith(_CORRECTION_PREFIX)
|
|
|
+ )
|
|
|
+ subject = _RESPONSE_SUBJECTS.get(record_kind, "业务答复")
|
|
|
+
|
|
|
+ if last_text.startswith(_CORRECTION_PREFIX):
|
|
|
+ codes = _correction_reason_codes(last_text)
|
|
|
+ reason = _correction_reason(last_text, codes)
|
|
|
+ return {
|
|
|
+ "phase": "contract_correction",
|
|
|
+ "label": f"合同校正 · 第 {correction_index} 次",
|
|
|
+ "tool_prefix": "校正中 · ",
|
|
|
+ "content_prefix": f"校正原因:{reason}\n校正结果:",
|
|
|
+ "payload": {
|
|
|
+ "business_phase": "contract_correction",
|
|
|
+ "attempt": correction_index + 1,
|
|
|
+ "correction_index": correction_index,
|
|
|
+ "trigger": "runtime_contract_validation",
|
|
|
+ "previous_candidate_ok": False,
|
|
|
+ "correction_reason": reason,
|
|
|
+ "correction_reason_codes": codes,
|
|
|
+ "response_kind": record_kind,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ if last_text.startswith(_MEDIA_REVIEW_PREFIX):
|
|
|
+ media_count = _reviewed_media_count(last_text, last_human)
|
|
|
+ scope = (
|
|
|
+ f"{media_count} 个媒体产物"
|
|
|
+ if media_count
|
|
|
+ else "本轮媒体产物"
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ "phase": "media_evidence_review",
|
|
|
+ "label": "媒体证据复核",
|
|
|
+ "tool_prefix": "媒体复核中 · ",
|
|
|
+ "content_prefix": f"复核范围:{scope}\n复核结果:",
|
|
|
+ "payload": {
|
|
|
+ "business_phase": "media_evidence_review",
|
|
|
+ "attempt": correction_index + 1,
|
|
|
+ "trigger": "final_media_evidence_review",
|
|
|
+ "reviewed_media_count": media_count,
|
|
|
+ "response_kind": record_kind,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ "phase": "initial_candidate",
|
|
|
+ "label": (
|
|
|
+ f"初版候选 · {subject}"
|
|
|
+ if record_kind is not None
|
|
|
+ else business_response_title(None)
|
|
|
+ ),
|
|
|
+ "tool_prefix": "",
|
|
|
+ "content_prefix": "",
|
|
|
+ "payload": {
|
|
|
+ "business_phase": "initial_candidate",
|
|
|
+ "attempt": 1,
|
|
|
+ "trigger": "initial_request",
|
|
|
+ "response_kind": record_kind,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
class BusinessReadableFlowCollector(FlowCollector):
|
|
|
"""保留 SDK 采集事实,只替换 LLM/Tool 卡片的展示投影。"""
|
|
|
|
|
|
@@ -298,6 +452,10 @@ class BusinessReadableFlowCollector(FlowCollector):
|
|
|
)
|
|
|
reasoning = _reasoning(message) if message is not None else ""
|
|
|
input_tokens, output_tokens = _usage(message, response)
|
|
|
+ source = list(self._pending_msgs.get(run_id, None) or [])
|
|
|
+ parsed_text = _parse(text)
|
|
|
+ record_kind = infer_business_record_kind(parsed_text)
|
|
|
+ turn = _turn_projection(source, record_kind=record_kind)
|
|
|
|
|
|
group = self._new_group() if len(tool_calls) >= 2 else ""
|
|
|
if group:
|
|
|
@@ -309,21 +467,23 @@ class BusinessReadableFlowCollector(FlowCollector):
|
|
|
readable_calls = readable_tool_calls(tool_calls)
|
|
|
names = [item["title"] for item in readable_calls]
|
|
|
if names:
|
|
|
- label = (
|
|
|
+ action_label = (
|
|
|
f"模型决定调用:{names[0]}"
|
|
|
if len(names) == 1
|
|
|
else f"模型并行调用 {len(names)} 个工具"
|
|
|
)
|
|
|
+ label = f"{turn['tool_prefix']}{action_label}"
|
|
|
content = readable_value(
|
|
|
text,
|
|
|
empty_text=f"下一步:{'、'.join(names)}",
|
|
|
)
|
|
|
else:
|
|
|
- parsed_text = _parse(text)
|
|
|
- label = business_response_title(parsed_text)
|
|
|
+ label = turn["label"]
|
|
|
content = readable_value(text, empty_text="模型已完成本轮判断")
|
|
|
+ if turn["content_prefix"]:
|
|
|
+ content = f"{turn['content_prefix']}{content}"
|
|
|
|
|
|
- payload: dict[str, Any] = {}
|
|
|
+ payload: dict[str, Any] = dict(turn["payload"])
|
|
|
if readable_calls:
|
|
|
payload["tool_calls"] = readable_calls
|
|
|
if reasoning:
|
|
|
@@ -345,10 +505,11 @@ class BusinessReadableFlowCollector(FlowCollector):
|
|
|
if self.run is not None:
|
|
|
self.run.account(input_tokens or 0, output_tokens or 0)
|
|
|
|
|
|
- source = self._pending_msgs.pop(run_id, None)
|
|
|
- if source is not None:
|
|
|
+ original_source = self._pending_msgs.pop(run_id, None)
|
|
|
+ if original_source is not None:
|
|
|
mc.record_messages(
|
|
|
- list(source) + ([message] if message is not None else []),
|
|
|
+ list(original_source)
|
|
|
+ + ([message] if message is not None else []),
|
|
|
idx=idx,
|
|
|
)
|
|
|
except Exception:
|