business_detail_text.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. from __future__ import annotations
  2. import json
  3. import re
  4. from typing import Any
  5. from .evaluation_report_parser import EvaluationReportParser
  6. from .runtime_tool_catalog import TOOL_CATALOG
  7. _STATUS_LABELS = {
  8. "merged": "已采用",
  9. "parked": "暂存",
  10. "discarded": "未采用",
  11. "open": "等待处理",
  12. "running": "进行中",
  13. "success": "成功",
  14. "failure": "失败",
  15. "failed": "失败",
  16. "ok": "完成",
  17. "completed": "完成",
  18. }
  19. _FIELD_LABELS = {
  20. "full_description": "详细执行描述",
  21. "content_range": "内容范围",
  22. "theme_elements": "主题信息",
  23. "form_elements": "形式信息",
  24. "function_elements": "作用信息",
  25. "feeling_elements": "感受信息",
  26. "element_count": "元素数量",
  27. "theme": "主题",
  28. "form": "形式",
  29. "function": "作用",
  30. "feeling": "感受",
  31. }
  32. def business_text(value: Any) -> str:
  33. """Translate stored business prose without inventing a new conclusion."""
  34. text = _string(value)
  35. if not text:
  36. return ""
  37. text = _decode_visible_escapes(text)
  38. for field, label in _FIELD_LABELS.items():
  39. text = re.sub(
  40. rf"(?<![a-z0-9_]){re.escape(field)}(?![a-z0-9_])",
  41. label,
  42. text,
  43. flags=re.I,
  44. )
  45. text = re.sub(r"(?i)(?<![a-z0-9_])branch_id\s*[=::]\s*(\d+)", r"方案 \1", text)
  46. text = re.sub(r"(?i)(?<![a-z0-9_])branch\s*#?\s*(\d+)", r"方案 \1", text)
  47. text = re.sub(r"(?i)(?<![a-z0-9_])base(?![a-z0-9_])", "主脚本", text)
  48. text = re.sub(
  49. r"选\s+方案\s*(\d+)\s*合入(?:\s*主脚本)?\s*[,,、]\s*弃\s+方案\s*(\d+)",
  50. r"采用方案 \1 并合入主脚本,方案 \2 未采用",
  51. text,
  52. )
  53. text = re.sub(
  54. r"组\s+方案\s*(\d+)\s*与\s*方案\s*(\d+)\s*合入",
  55. r"组合方案 \1 与方案 \2,并合入主脚本",
  56. text,
  57. )
  58. text = re.sub(r"(?i)(?<![a-z0-9_])target\s*[=::]", "任务目标:", text)
  59. text = re.sub(r"(?i)(?<![a-z0-9_])target(?![a-z0-9_])", "任务目标", text)
  60. text = re.sub(r"(?i)(?<![a-z0-9_])scope\s*[=::]", "账号:", text)
  61. text = re.sub(r"(?i)(?<![a-z0-9_])null(?![a-z0-9_])", "未填写", text)
  62. text = (
  63. text.replace("主脚本 脚本表", "主脚本")
  64. .replace("账号 账号:", "账号:")
  65. .replace("同一 任务目标", "同一个任务")
  66. )
  67. text = re.sub(r"(?i)patch\s*分支", "候选方案", text)
  68. text = re.sub(r"[ \t]+([,。;!?、:)】,.])", r"\1", text)
  69. return _tidy(text)
  70. def branch_task_text(value: Any) -> str:
  71. """Remove implementation headers while preserving the complete business brief."""
  72. text = business_text(value)
  73. if not text:
  74. return ""
  75. kept: list[str] = []
  76. for line in text.splitlines():
  77. stripped = line.strip()
  78. if re.match(r"^(?:路序号|action|path_type)\s*[::]", stripped, re.I):
  79. continue
  80. if re.match(r"^任务目标\s*[::]", stripped):
  81. stripped = re.sub(r"^任务目标\s*[::]\s*", "", stripped)
  82. if stripped:
  83. kept.extend(["### 实现范围", stripped])
  84. continue
  85. stripped = re.sub(r"^取证方向与约束\s*[::]?", "### 参考依据与约束", stripped)
  86. stripped = re.sub(r"^总目标方向(?:([^)]*))?\s*[::]?", "### 整体方向", stripped)
  87. stripped = re.sub(r"^本轮当前目标\s*[::]?", "**本轮重点:**", stripped)
  88. stripped = re.sub(r"^禁止事项\s*[::]?\s*", "### 需要避免\n", stripped)
  89. kept.append(stripped)
  90. return _tidy("\n".join(kept))
  91. def branch_task_sections(value: Any) -> list[dict[str, str]]:
  92. """Split a delegated task into optional display sections without data loss.
  93. Real runs mostly follow the dispatch contract, but some repair tasks are
  94. free-form. No field is therefore required: recognized labels become
  95. sections and every remaining line stays in a generic task note.
  96. """
  97. text = branch_task_text(value)
  98. if not text:
  99. return []
  100. sections: list[dict[str, str]] = []
  101. current: dict[str, Any] | None = None
  102. recognized = False
  103. def flush() -> None:
  104. nonlocal current
  105. if not current:
  106. return
  107. content = _tidy("\n".join(current.pop("lines", [])))
  108. if content:
  109. sections.append({**current, "content": content})
  110. current = None
  111. def begin(kind: str, title: str, inline: str = "") -> None:
  112. nonlocal current, recognized
  113. flush()
  114. recognized = True
  115. current = {"kind": kind, "title": title, "lines": []}
  116. if inline.strip():
  117. current["lines"].append(inline.strip())
  118. for line in text.splitlines():
  119. stripped = line.strip()
  120. heading = re.match(r"^#{1,6}\s+(.+?)\s*$", stripped)
  121. if heading:
  122. label = heading.group(1).strip()
  123. if label == "实现范围":
  124. begin("scope", label)
  125. continue
  126. if label in {"参考依据与约束", "整体方向"}:
  127. begin("constraints", label)
  128. continue
  129. if label == "需要避免":
  130. begin("avoid", label)
  131. continue
  132. labelled = _task_section_label(stripped)
  133. if labelled:
  134. kind, title, inline = labelled
  135. begin(kind, title, inline)
  136. continue
  137. if current is None:
  138. current = {"kind": "notes", "title": "任务说明", "lines": []}
  139. current["lines"].append(line)
  140. flush()
  141. if not recognized:
  142. return [{"kind": "notes", "title": "任务说明", "content": text}]
  143. return sections
  144. def _task_section_label(value: str) -> tuple[str, str, str] | None:
  145. plain = re.sub(r"^\*\*(.+?)\*\*\s*([::]?.*)$", r"\1\2", value).strip()
  146. patterns = (
  147. ("method", "实施方法", r"(?i)^method\s*[::]\s*(.*)$"),
  148. ("goal", "目标", r"^目标\s*[::]\s*(.*)$"),
  149. ("materials", None, r"^(必用素材(?:([^\n]*))?)\s*[::]\s*(.*)$"),
  150. ("context", None, r"^(前情提要(?:([^\n]*))?)\s*[::]\s*(.*)$"),
  151. (
  152. "constraints",
  153. None,
  154. r"^((?:极其重要的|统一基调|严格|重要)?约束(?:([^\n]*))?)\s*[::]\s*(.*)$",
  155. ),
  156. )
  157. for kind, fixed_title, pattern in patterns:
  158. match = re.match(pattern, plain)
  159. if not match:
  160. continue
  161. if fixed_title is not None:
  162. return kind, fixed_title, match.group(1)
  163. return kind, match.group(1), match.group(2)
  164. return None
  165. def assessment_text(value: Any) -> str:
  166. """Keep the implementer's declaration; remove thought/process and cost telemetry."""
  167. text = clean_report(value)
  168. if not text:
  169. return ""
  170. marker = re.search(r"(?i)#{1,6}\s*完成度申报", text)
  171. if marker:
  172. text = text[marker.end() :]
  173. else:
  174. text = re.sub(
  175. r"(?is)^.*?(?=^\s*[-*]\s*\*{0,2}(?:完成|未尽|存疑)\*{0,2}\s*[::])",
  176. "",
  177. text,
  178. )
  179. return _tidy(text)
  180. def clean_report(value: Any) -> str:
  181. """Turn evaluator/agent Markdown into business prose; raw input remains technical."""
  182. text = business_text(value)
  183. if not text:
  184. return ""
  185. text = re.sub(r"(?ms)^\s*```[^\n]*\n.*?^\s*```\s*$", "", text)
  186. text = re.sub(r"(?ms)^\s*```[^\n]*\n.*\Z", "", text)
  187. lines: list[str] = []
  188. skip_stats = False
  189. for line in text.splitlines():
  190. stripped = line.strip()
  191. if re.match(r"^#{1,6}\s*(?:委托任务完成|评估报告)\b", stripped):
  192. continue
  193. if re.match(r"^(?:\*\*)?执行统计(?:\*\*)?\s*[::]?", stripped):
  194. skip_stats = True
  195. continue
  196. if skip_stats and (
  197. not stripped
  198. or re.match(r"^[-*]\s*(?:消息数|Tokens?|成本)\s*[::]", stripped, re.I)
  199. ):
  200. continue
  201. if skip_stats and stripped:
  202. skip_stats = False
  203. if re.match(r"^[-*]?\s*(?:消息数|Tokens?|Token\s*usage|成本)\s*[::]", stripped, re.I):
  204. continue
  205. if "分支元信息" in stripped:
  206. continue
  207. line = re.sub(
  208. r"[((]\s*段落\s*(?:ID|id)\s*[::]\s*[\d,,、\s]+[))]",
  209. "",
  210. line,
  211. )
  212. line = re.sub(r"(?i)script_build_id\s*[=:]\s*\d+\s*[·|,,]?", "", line)
  213. line = re.sub(r"(?i)\bpost_id\s*[=::]\s*[^\s,,;;))]+", "来源记录", line)
  214. line = re.sub(r"(?i)\bround(?:_index)?\s*[=:]\s*(\d+)\b", r"第 \1 轮", line)
  215. line = re.sub(r"(?i)\b(?:ID|id)\s*[=::]\s*\d+\b", "", line)
  216. line = re.sub(r"[((]\s*[,,·|\s-]*[))]", "", line)
  217. line = re.sub(r"(?<=\d)中", " 中", line)
  218. line = re.sub(r"`?\[元素:ID\]`?\s*格式", "规范格式", line, flags=re.I)
  219. lines.append(line.rstrip())
  220. return _tidy("\n".join(lines))
  221. def event_business_sections(event: dict[str, Any]) -> list[dict[str, str]]:
  222. name = str(event.get("event_name") or "")
  223. task = _event_value(event, "input", "task")
  224. result = _event_value(event, "output", "summary")
  225. if name == "script_multipath_evaluator":
  226. report = EvaluationReportParser().parse(result, kind="multipath")
  227. scope = "、".join(report["subjects"]) or branch_task_text(task)
  228. return _compact_sections(
  229. [
  230. ("scope", "评审了哪些方案", scope, "summary"),
  231. (
  232. "conclusion",
  233. "各方案结论",
  234. _evaluation_item_text(report["itemConclusions"]),
  235. "default",
  236. ),
  237. ("recommendation", "评审建议", report["recommendation"], "report"),
  238. ]
  239. )
  240. if name == "script_evaluator":
  241. report = EvaluationReportParser().parse(result, kind="overall")
  242. return _compact_sections(
  243. [
  244. ("conclusion", "主脚本是否达标", report["conclusion"], "summary"),
  245. ("scope", "评审范围", branch_task_text(task), "default"),
  246. ("achievements", "已达成", "\n".join(report["achievements"]), "report"),
  247. (
  248. "problems",
  249. "仍需解决",
  250. _evaluation_problem_text(report["problems"]),
  251. "report",
  252. ),
  253. ("next", "下一步建议", report["nextGoal"], "report"),
  254. ]
  255. )
  256. action = friendly_event_name(event)
  257. request = branch_task_text(task)
  258. outcome = event_result_text(event)
  259. return _compact_sections(
  260. [
  261. ("action", "做了什么", action, "summary"),
  262. ("request", "处理内容", request, "default"),
  263. ("result", "得到什么", outcome, "report"),
  264. ]
  265. )
  266. def _evaluation_item_text(items: list[Any]) -> str:
  267. lines: list[str] = []
  268. for item in items:
  269. if not isinstance(item, dict):
  270. continue
  271. subject = business_text(item.get("subject") or item.get("label"))
  272. conclusion = business_text(item.get("conclusion") or item.get("summary"))
  273. if subject and conclusion:
  274. lines.append(f"{subject}:{conclusion}")
  275. return "\n".join(lines)
  276. def _evaluation_problem_text(items: list[Any]) -> str:
  277. values = []
  278. for item in items:
  279. if isinstance(item, dict):
  280. value = business_text(item.get("summary") or item.get("label"))
  281. else:
  282. value = business_text(item)
  283. if value:
  284. values.append(value)
  285. return "\n".join(values)
  286. def source_markdown(sources: list[Any]) -> str:
  287. items: list[str] = []
  288. for source in sources:
  289. if not isinstance(source, dict):
  290. continue
  291. source_type = business_text(source.get("data_type") or source.get("type")) or "参考信息"
  292. content = business_text(
  293. source.get("data_content")
  294. or source.get("content")
  295. or source.get("summary")
  296. )
  297. if content:
  298. items.append(f"- **{source_type}:** {content}")
  299. return "\n".join(items)
  300. def decisions_markdown(values: list[Any]) -> str:
  301. items = [business_text(value) for value in values]
  302. return "\n\n".join(f"{index}. {value}" for index, value in enumerate(items, 1) if value)
  303. def branches_markdown(branches: list[dict[str, Any]], *, raw: bool = False) -> str:
  304. parts: list[str] = []
  305. for index, branch in enumerate(branches, 1):
  306. branch_id = branch.get("branch_id") if raw else branch.get("branchId")
  307. title = (
  308. branch.get("target") or branch.get("impl_task")
  309. if raw
  310. else branch.get("title") or (branch.get("summary") or {}).get("task")
  311. )
  312. status = branch.get("status")
  313. output = None if raw else (branch.get("summary") or {}).get("output")
  314. disposition = (
  315. branch.get("decision_reasoning")
  316. if raw
  317. else (branch.get("outcome") or {}).get("reasoning")
  318. )
  319. heading = f"### 方案 {branch_id or index}"
  320. if status:
  321. heading += f" · {status_label(status)}"
  322. rows = [heading, business_text(title)]
  323. if output:
  324. rows.append(f"**候选产出:** {business_text(output)}")
  325. if disposition:
  326. label = "选择理由" if raw else "最终选择"
  327. rows.append(f"**{label}:** {business_text(disposition)}")
  328. parts.append("\n\n".join(item for item in rows if item))
  329. return "\n\n".join(parts)
  330. def batches_markdown(batches: list[dict[str, Any]]) -> str:
  331. if not batches:
  332. return "未记录主 Agent 多路决策。"
  333. parts = []
  334. for index, batch in enumerate(batches, 1):
  335. branch_ids = "、".join(str(value) for value in batch.get("branchIds") or [])
  336. lines = [f"### 决策 {index}"]
  337. if branch_ids:
  338. lines.append(f"**比较方案:** {branch_ids}")
  339. if batch.get("decision"):
  340. lines.append(f"**最终决定:** {business_text(batch['decision'])}")
  341. if batch.get("reasoning"):
  342. lines.append(f"**原因:** {business_text(batch['reasoning'])}")
  343. parts.append("\n\n".join(lines))
  344. return "\n\n".join(parts)
  345. def status_label(value: Any) -> str:
  346. raw = str(value or "").strip()
  347. return _STATUS_LABELS.get(raw.lower(), business_text(raw) or "状态未记录")
  348. def friendly_event_name(event: dict[str, Any]) -> str:
  349. name = str(event.get("event_name") or "")
  350. title = str(event.get("title") or "")
  351. labels = {
  352. "script_evaluator": "对本轮主脚本进行整体评审",
  353. "script_multipath_evaluator": "比较候选方案并给出独立评审",
  354. "record_data_decision": "记录实现方案的数据取舍",
  355. "get_branch_data_decisions": "读取方案的数据取舍记录",
  356. "get_script_build_context": "读取本次构建的业务背景",
  357. }
  358. if name in labels:
  359. return labels[name]
  360. definition = TOOL_CATALOG.get(name)
  361. if definition:
  362. if definition.category == "decision-input":
  363. return f"读取{definition.business_label}"
  364. return definition.business_label
  365. cleaned = re.sub(r"\s*\([^)]*\)\s*$", "", title).strip()
  366. if cleaned and not re.search(r"[a-z]+_[a-z_]", cleaned, re.I):
  367. return business_text(cleaned)
  368. lowered = name.lower()
  369. if any(token in lowered for token in ("search", "find", "query", "retrieve")):
  370. return "查找相关参考信息"
  371. if lowered.startswith("get_"):
  372. return "读取相关业务资料"
  373. if lowered.startswith(("record_", "save_")):
  374. return "保存本步骤的业务结果"
  375. return "完成一次运行活动"
  376. def event_result_text(event: dict[str, Any]) -> str:
  377. name = str(event.get("event_name") or "")
  378. status = str(event.get("status") or "").lower()
  379. output = _event_object(event, "output")
  380. if status in {"error", "failed", "failure"} or output.get("success") is False:
  381. message = business_text(output.get("message") or output.get("error"))
  382. return f"执行失败。{message}" if message else "执行失败,具体原因请查看技术详情。"
  383. if name == "get_script_direction":
  384. direction = _event_value(event, "output", "script_direction")
  385. return clean_report(direction) or "已读取当前创作方向。"
  386. if name == "get_account_script_section_patterns":
  387. summary = _event_value(event, "output", "分段规律摘要")
  388. return clean_report(summary) or "已读取账号常用的段落结构。"
  389. if name == "get_domain_info":
  390. count = _list_count(output.get("domain_info"))
  391. return f"已读取 {count} 条已核实领域信息。" if count is not None else "已读取已核实的领域信息。"
  392. if name == "get_script_snapshot":
  393. paragraphs = _list_count(output.get("paragraphs"))
  394. elements = _list_count(output.get("elements"))
  395. if paragraphs is not None and elements is not None:
  396. return f"已读取当前主脚本,共 {paragraphs} 个段落、{elements} 个元素。"
  397. return "已读取当前主脚本内容。"
  398. if name == "get_branch_data_decisions":
  399. count = _list_count(output.get("decisions"))
  400. return f"找到 {count} 条数据取舍记录。" if count is not None else "已读取方案的数据取舍记录。"
  401. summary = output.get("summary") or output.get("message")
  402. if isinstance(summary, str):
  403. readable = clean_report(summary)
  404. if readable:
  405. return readable
  406. count = _first_count(output)
  407. if count is not None:
  408. return f"已完成,共返回 {count} 条结果。"
  409. return "已完成。具体输入输出保留在技术详情中。"
  410. def _event_value(event: dict[str, Any], side: str, key: str | None = None) -> Any:
  411. body = event.get(side)
  412. content = body.get("content") if isinstance(body, dict) else body
  413. value = _nested_value(content, key)
  414. if value not in (None, ""):
  415. return value
  416. if key and isinstance(content, str) and not content.lstrip().startswith(("{", "[")):
  417. return content
  418. preview = event.get(f"{side}_preview")
  419. if key:
  420. extracted = _json_string_preview(preview, key)
  421. if extracted not in (None, ""):
  422. return extracted
  423. return _nested_value(preview, key)
  424. def _event_object(event: dict[str, Any], side: str) -> dict[str, Any]:
  425. body = event.get(side)
  426. content = body.get("content") if isinstance(body, dict) else body
  427. for value in (content, event.get(f"{side}_preview")):
  428. if isinstance(value, dict):
  429. return value
  430. if isinstance(value, str):
  431. try:
  432. parsed = json.loads(value)
  433. except json.JSONDecodeError:
  434. continue
  435. if isinstance(parsed, dict):
  436. return parsed
  437. return {}
  438. def _nested_value(value: Any, key: str | None) -> Any:
  439. if key is None:
  440. return value
  441. if isinstance(value, dict):
  442. return value.get(key)
  443. if isinstance(value, str):
  444. try:
  445. parsed = json.loads(value)
  446. except (TypeError, json.JSONDecodeError):
  447. return None
  448. return parsed.get(key) if isinstance(parsed, dict) else None
  449. return None
  450. def _json_string_preview(value: Any, key: str) -> str | None:
  451. text = _string(value)
  452. if not text:
  453. return None
  454. match = re.search(rf'"{re.escape(key)}"\s*:\s*"', text)
  455. if not match:
  456. return None
  457. start = match.end()
  458. escaped = False
  459. end = None
  460. for index in range(start, len(text)):
  461. char = text[index]
  462. if char == '"' and not escaped:
  463. end = index
  464. break
  465. if char == "\\" and not escaped:
  466. escaped = True
  467. else:
  468. escaped = False
  469. fragment = text[start:end] if end is not None else text[start:].rstrip("…")
  470. try:
  471. return json.loads(f'"{fragment}"')
  472. except json.JSONDecodeError:
  473. return _decode_visible_escapes(fragment)
  474. def _compact_sections(
  475. values: list[tuple[str, str, Any, str]],
  476. ) -> list[dict[str, str]]:
  477. return [
  478. {"id": section_id, "title": title, "content": content, "variant": variant}
  479. for section_id, title, raw, variant in values
  480. if (content := business_text(raw))
  481. ]
  482. def _list_count(value: Any) -> int | None:
  483. return len(value) if isinstance(value, list) else None
  484. def _first_count(value: dict[str, Any]) -> int | None:
  485. for key in ("count", "result_count", "path_count", "source_count", "total"):
  486. count = value.get(key)
  487. if isinstance(count, int) and not isinstance(count, bool):
  488. return count
  489. return None
  490. def _decode_visible_escapes(value: str) -> str:
  491. return value.replace("\\r\\n", "\n").replace("\\n", "\n").replace('\\"', '"')
  492. def _tidy(value: str) -> str:
  493. value = re.sub(r"[ \t]+\n", "\n", value)
  494. value = re.sub(r"\n{3,}", "\n\n", value)
  495. return value.strip()
  496. def _unique(values: list[str]) -> list[str]:
  497. return list(dict.fromkeys(values))
  498. def _string(value: Any) -> str:
  499. return value.strip() if isinstance(value, str) else ""