creative_event_projection.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. from __future__ import annotations
  2. import re
  3. from collections import defaultdict
  4. from typing import Any
  5. from .business_detail_text import branch_task_text, business_text
  6. from .decision_projection import AgentDecisionProjector
  7. from .runtime_event_index import RuntimeEventIndex, event_succeeded, integer
  8. from .runtime_tool_catalog import (
  9. RETRIEVAL_AGENT_LABELS,
  10. TOOL_CATALOG,
  11. business_tool_label,
  12. tools_in_category,
  13. )
  14. _CREATIVE_ACTION_TOOLS = tools_in_category("creative-action")
  15. _DECISION_INPUT_TOOLS = tools_in_category("decision-input")
  16. class CreativeEventProjector:
  17. """Project only safely owned implementer ``think_and_plan`` records."""
  18. def __init__(self, decisions: AgentDecisionProjector | None = None):
  19. self._decisions = decisions or AgentDecisionProjector()
  20. def project(
  21. self,
  22. index: RuntimeEventIndex,
  23. *,
  24. valid_rounds: set[int],
  25. valid_branches: set[int],
  26. ) -> dict[str, Any]:
  27. implementers: dict[int, dict[str, Any]] = {}
  28. for event in index.ordered:
  29. if (
  30. str(event.get("event_type") or "") == "agent_invoke"
  31. and str(event.get("event_name") or "") == "script_implementer"
  32. and (branch_id := integer(event.get("branch_id"))) in valid_branches
  33. ):
  34. implementers[branch_id] = event
  35. events_by_branch: dict[int, list[dict[str, Any]]] = defaultdict(list)
  36. for event in index.ordered:
  37. if (
  38. str(event.get("event_type") or "") != "tool_call"
  39. or str(event.get("event_name") or "") != "think_and_plan"
  40. or not event_succeeded(event)
  41. ):
  42. continue
  43. branch_id = integer(event.get("branch_id"))
  44. round_index = integer(event.get("round_index"))
  45. implementer = implementers.get(branch_id or -1)
  46. if (
  47. branch_id not in valid_branches
  48. or round_index not in valid_rounds
  49. or implementer is None
  50. or integer(event.get("scope_event_id"))
  51. not in {integer(implementer.get("id")), integer(implementer.get("scope_event_id"))}
  52. or str(event.get("agent_role") or "") not in {"script_implementer", "implementer"}
  53. ):
  54. continue
  55. events_by_branch[branch_id].append(event)
  56. projected: dict[int, dict[str, Any]] = {}
  57. for branch_id, records in events_by_branch.items():
  58. implementer = implementers[branch_id]
  59. latest = records[-1]
  60. task = _task(implementer)
  61. uncertainty = _uncertainties(records)
  62. steps = _steps(records)
  63. reasoning = _thought(latest)
  64. runtime_actions = _runtime_actions(index, implementer)
  65. available_upstream = _available_upstream(index, implementer)
  66. decision = self._decisions.creative(
  67. {
  68. "id": f"creative:{integer(latest.get('id')) or 0}",
  69. "detailRef": f"creative:{integer(latest.get('id')) or 0}",
  70. "promptRef": f"prompt:event:{integer(implementer.get('id')) or 0}",
  71. "safeLink": True,
  72. "event": latest,
  73. "task": task,
  74. "steps": steps,
  75. "reasoning": reasoning,
  76. "uncertainties": uncertainty,
  77. "inputs": available_upstream,
  78. "completeness": "partial",
  79. "notices": [
  80. {
  81. "code": "creative-record-partial",
  82. "message": "当前运行只保存了通用思考与计划,没有稳定的完整创作四元组。",
  83. }
  84. ],
  85. }
  86. )
  87. if decision is None:
  88. continue
  89. decision["body"] = {
  90. **(decision.get("body") or {}),
  91. "availableUpstream": available_upstream,
  92. "runtimeActions": runtime_actions,
  93. }
  94. blocks = list((decision.get("detail") or {}).get("blocks") or [])
  95. insert_at = next(
  96. (
  97. index_ + 1
  98. for index_, block in enumerate(blocks)
  99. if isinstance(block, dict)
  100. and block.get("type") == "creative-process"
  101. ),
  102. 0,
  103. )
  104. additions = []
  105. if available_upstream:
  106. additions.append(
  107. {
  108. "type": "items",
  109. "title": "可确认的上游信息",
  110. "items": [
  111. {
  112. "label": item["label"],
  113. "value": item.get("summary"),
  114. "note": "可确认在上游存在或已返回,但无法确认是否被本次创作采用。",
  115. "detailRef": item.get("detailRef"),
  116. }
  117. for item in available_upstream
  118. ],
  119. "visualRole": "evidence",
  120. "presentation": "list",
  121. "collapsible": True,
  122. }
  123. )
  124. if runtime_actions:
  125. additions.append(
  126. {
  127. "type": "items",
  128. "title": "实际脚本改动",
  129. "items": runtime_actions,
  130. "visualRole": "section",
  131. "presentation": "list",
  132. "collapsible": True,
  133. }
  134. )
  135. blocks[insert_at:insert_at] = additions
  136. decision["detail"] = {
  137. **(decision.get("detail") or {}),
  138. "blocks": blocks,
  139. }
  140. ref_set = {
  141. *(f"event:{integer(item.get('id')) or 0}" for item in records),
  142. *(str(item.get("detailRef")) for item in available_upstream if item.get("detailRef")),
  143. *(str(item.get("detailRef")) for item in runtime_actions if item.get("detailRef")),
  144. }
  145. decision["eventRefs"] = [
  146. ref
  147. for item in index.ordered
  148. if (ref := f"event:{integer(item.get('id')) or 0}") in ref_set
  149. ]
  150. decision["technicalRefs"] = decision["eventRefs"]
  151. projected[branch_id] = decision
  152. return {"creativeByBranch": projected, "unassigned": []}
  153. def _task(event: dict[str, Any]) -> str | None:
  154. value = event.get("inputData")
  155. if isinstance(value, dict) and isinstance(value.get("task"), str):
  156. text = value["task"].strip()
  157. if match := re.search(r"(?:^|\s)目标\s*[::]\s*(.+)$", text, re.S):
  158. text = match.group(1).strip()
  159. return branch_task_text(text) or None
  160. return None
  161. def _steps(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
  162. values: list[dict[str, Any]] = []
  163. for fallback_index, event in enumerate(events, start=1):
  164. payload = event.get("inputData")
  165. if not isinstance(payload, dict):
  166. continue
  167. summary = _creative_action_text(payload.get("thought_summary"))
  168. action = _creative_action_text(payload.get("action")) or summary
  169. plan = _creative_action_text(payload.get("plan"))
  170. reasoning = _creative_action_text(payload.get("thought"))
  171. if not any((action, plan, summary, reasoning)):
  172. continue
  173. values.append(
  174. {
  175. "stepIndex": integer(payload.get("thought_number")) or fallback_index,
  176. "action": action or f"步骤 {fallback_index}",
  177. "summary": summary,
  178. "plan": plan,
  179. "reasoning": reasoning,
  180. "eventRef": f"event:{integer(event.get('id')) or 0}",
  181. }
  182. )
  183. return values
  184. def _creative_action_text(value: Any) -> str | None:
  185. if not isinstance(value, str) or not value.strip():
  186. return None
  187. text = value.strip()
  188. exact = text.lower()
  189. if exact in RETRIEVAL_AGENT_LABELS:
  190. return f"委派{RETRIEVAL_AGENT_LABELS[exact]}取数"
  191. if exact in TOOL_CATALOG:
  192. definition = TOOL_CATALOG[exact]
  193. if definition.category == "decision-input":
  194. return f"读取{definition.business_label}"
  195. return definition.business_label
  196. if re.fullmatch(r"[a-z][a-z0-9_]{2,}", exact):
  197. return None
  198. replacements = {
  199. **{
  200. name: f"委派{label}取数"
  201. for name, label in RETRIEVAL_AGENT_LABELS.items()
  202. },
  203. **{
  204. name: (
  205. f"读取{definition.business_label}"
  206. if definition.category == "decision-input"
  207. else definition.business_label
  208. )
  209. for name, definition in TOOL_CATALOG.items()
  210. },
  211. }
  212. for name in sorted(replacements, key=len, reverse=True):
  213. text = re.sub(
  214. rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])",
  215. replacements[name],
  216. text,
  217. flags=re.I,
  218. )
  219. text = business_text(text)
  220. text = re.sub(r"调用\s*(读取|记录|新增|更新|补充|批量)", r"\1", text)
  221. text = re.sub(r"委派\s*委派", "委派", text)
  222. return text.strip() or None
  223. def _thought(event: dict[str, Any]) -> str | None:
  224. payload = event.get("inputData")
  225. if not isinstance(payload, dict):
  226. return None
  227. value = payload.get("thought_summary") or payload.get("thought")
  228. return value.strip() if isinstance(value, str) and value.strip() else None
  229. def _uncertainties(events: list[dict[str, Any]]) -> list[str]:
  230. values: list[str] = []
  231. for event in events:
  232. payload = event.get("inputData")
  233. if not isinstance(payload, dict):
  234. continue
  235. text = "\n".join(
  236. str(payload.get(key) or "") for key in ("thought", "thought_summary", "plan")
  237. )
  238. for sentence in re.split(r"[。!?\n]+", text):
  239. if any(token in sentence for token in ("缺少直接证据", "未验证", "常识", "推导", "无法确认")):
  240. cleaned = sentence.strip()
  241. if cleaned and cleaned not in values:
  242. values.append(cleaned)
  243. return values[:3]
  244. def _runtime_actions(
  245. index: RuntimeEventIndex, implementer: dict[str, Any]
  246. ) -> list[dict[str, Any]]:
  247. scope_id = integer(implementer.get("id"))
  248. if scope_id is None:
  249. return []
  250. values: list[dict[str, Any]] = []
  251. for event in index.events_in_scope(scope_id):
  252. name = str(event.get("event_name") or "")
  253. if str(event.get("event_type") or "") != "tool_call" or name not in _CREATIVE_ACTION_TOOLS:
  254. continue
  255. event_id = integer(event.get("id")) or 0
  256. status = str(event.get("status") or "unknown")
  257. values.append(
  258. {
  259. "label": business_tool_label(name),
  260. "value": "已完成" if event_succeeded(event) else "未完成",
  261. "note": f"运行状态:{status}",
  262. "detailRef": f"event:{event_id}",
  263. }
  264. )
  265. return values
  266. def _available_upstream(
  267. index: RuntimeEventIndex, implementer: dict[str, Any]
  268. ) -> list[dict[str, Any]]:
  269. implementer_id = integer(implementer.get("id"))
  270. branch_id = integer(implementer.get("branch_id"))
  271. round_index = integer(implementer.get("round_index"))
  272. if implementer_id is None:
  273. return []
  274. values: list[dict[str, Any]] = []
  275. for event in index.events_in_scope(implementer_id):
  276. name = str(event.get("event_name") or "")
  277. if str(event.get("event_type") or "") != "tool_call" or name not in _DECISION_INPUT_TOOLS:
  278. continue
  279. if not event_succeeded(event):
  280. continue
  281. values.append(
  282. {
  283. "label": business_tool_label(name),
  284. "summary": "实现 Agent 在创作过程中直接读取",
  285. "observedRelation": "read-by-actor",
  286. "decisionUse": "not-recorded",
  287. "detailRef": f"event:{integer(event.get('id')) or 0}",
  288. }
  289. )
  290. for event in index.ordered:
  291. name = str(event.get("event_name") or "")
  292. if (
  293. str(event.get("event_type") or "") != "agent_invoke"
  294. or name not in RETRIEVAL_AGENT_LABELS
  295. or integer(event.get("parent_event_id")) != implementer_id
  296. or integer(event.get("branch_id")) != branch_id
  297. or integer(event.get("round_index")) != round_index
  298. or not event_succeeded(event)
  299. ):
  300. continue
  301. values.append(
  302. {
  303. "label": RETRIEVAL_AGENT_LABELS[name],
  304. "summary": "取数结果已返回实现 Agent",
  305. "observedRelation": "returned-to-actor",
  306. "decisionUse": "not-recorded",
  307. "detailRef": f"event:{integer(event.get('id')) or 0}",
  308. }
  309. )
  310. unique: dict[tuple[Any, Any], dict[str, Any]] = {}
  311. for item in values:
  312. unique[(item.get("label"), item.get("detailRef"))] = item
  313. return list(unique.values())