inspector_projection.py 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646
  1. from __future__ import annotations
  2. import re
  3. from typing import Any
  4. from .business_detail_text import (
  5. assessment_text,
  6. batches_markdown,
  7. branch_task_sections,
  8. branch_task_text,
  9. branches_markdown,
  10. business_text,
  11. decisions_markdown,
  12. event_business_sections,
  13. source_markdown,
  14. clean_report,
  15. )
  16. from .sanitizer import sanitize
  17. from .retrieval_detail_projection import (
  18. is_retrieval_detail_event,
  19. project_retrieval_event,
  20. )
  21. def project_round_detail(
  22. script_build_id: int,
  23. round_: dict[str, Any],
  24. bundle: dict[str, Any] | None = None,
  25. ) -> dict[str, Any]:
  26. raw_round = next(
  27. (
  28. item
  29. for item in (bundle or {}).get("rounds", [])
  30. if int(item.get("round_index") or 0) == int(round_.get("roundIndex") or 0)
  31. ),
  32. None,
  33. )
  34. sections = [
  35. _section(
  36. "goal",
  37. "本轮目标",
  38. (raw_round or {}).get("goal")
  39. or (round_.get("summary", {}).get("goal") or {}).get("headline"),
  40. ),
  41. _section("plan", "当前保存的规划", _plan_text(round_.get("plan") or {})),
  42. _section("branches", "候选方案", branches_markdown(round_.get("branches") or [])),
  43. _section(
  44. "decisions",
  45. "候选评审与主 Agent 决策",
  46. _convergence_markdown(round_.get("convergenceBatches") or []),
  47. ),
  48. _section(
  49. "evaluation",
  50. "主脚本整体评审",
  51. _node_value(round_.get("overallEvaluation")) or "未找到明确整轮评审结论",
  52. ),
  53. _section("result", "本轮产出", _node_value(round_.get("result"))),
  54. ]
  55. result = round_.get("result") or {}
  56. changes = None
  57. if result.get("hasChanges"):
  58. changes = {
  59. "summary": _node_value(result),
  60. "sections": [
  61. item
  62. for item in [
  63. _section(
  64. "content",
  65. "内容方案",
  66. _round_content_changes(round_.get("branches") or []),
  67. ),
  68. _section(
  69. "domain",
  70. "领域信息",
  71. _round_domain_changes(round_.get("branches") or []),
  72. ),
  73. ]
  74. if item
  75. ],
  76. "artifactRef": "artifact:base:current",
  77. "exactness": "current-only",
  78. }
  79. return _payload(
  80. sections,
  81. {
  82. "scriptBuildId": script_build_id,
  83. "round": raw_round,
  84. "multipathDecisions": [
  85. item
  86. for item in (bundle or {}).get("multipathDecisions", [])
  87. if int(item.get("round_index") or 0) == int(round_.get("roundIndex") or 0)
  88. ],
  89. },
  90. changes,
  91. )
  92. def project_activity_detail(
  93. script_build_id: int,
  94. activity_id: str,
  95. view: dict[str, Any],
  96. bundle: dict[str, Any],
  97. event_detail: dict[str, Any] | None = None,
  98. ) -> dict[str, Any]:
  99. if activity_id == "run:final-result":
  100. return _final_result_detail(script_build_id, view, bundle)
  101. missing_convergence = _find_missing_convergence(view, activity_id)
  102. if missing_convergence is not None:
  103. node, batch = missing_convergence
  104. return _payload(
  105. [
  106. _section(
  107. "status",
  108. str(node.get("title") or "记录状态"),
  109. _node_value(node),
  110. "summary",
  111. )
  112. ],
  113. {
  114. "scriptBuildId": script_build_id,
  115. "roundIndex": batch.get("roundIndex"),
  116. "branchIds": batch.get("branchIds") or [],
  117. "association": batch.get("association"),
  118. "unscopedReviewRefs": node.get("unscopedReviewRefs") or [],
  119. },
  120. )
  121. decision = _find_decision_projection(view, activity_id)
  122. if decision is not None:
  123. return _decision_detail_payload(
  124. script_build_id, decision, bundle, activity_id, event_detail
  125. )
  126. if activity_id.startswith("domain-info:"):
  127. record_id = _suffix_int(activity_id)
  128. row = _find_by_id(bundle.get("domainInfo") or [], record_id)
  129. if not row:
  130. raise KeyError(activity_id)
  131. return _payload(
  132. [
  133. _section("fact", "已核实领域事实", row.get("content")),
  134. _section("verification", "核实说明", row.get("note")),
  135. _section("source", "信息出处", _source_value(row.get("source"))),
  136. _section(
  137. "use",
  138. "用途与存疑状态",
  139. "已作为领域事实独立保存;是否进入具体候选脚本文案未记录。",
  140. ),
  141. ],
  142. {"scriptBuildId": script_build_id, "table": "script_build_domain_info", "record": row},
  143. {
  144. "summary": "这条领域事实已经独立写入领域信息库。",
  145. "sections": [],
  146. "exactness": "exact",
  147. },
  148. )
  149. if activity_id.startswith("retrieval-direct:"):
  150. group = _find_retrieval_item(view, "directToolGroups", activity_id)
  151. if not group:
  152. raise KeyError(activity_id)
  153. calls = group.get("calls") or []
  154. payload = _payload(
  155. [
  156. _section("purpose", "读取方式", "实现 Agent 直接调用只读工具,补齐创作所需的上下文。"),
  157. _section("sources", "读取了什么", _direct_sources_markdown(group)),
  158. _section("result", "得到什么", _direct_result_markdown(calls), "summary"),
  159. ],
  160. {
  161. "scriptBuildId": script_build_id,
  162. "retrievalGroup": group,
  163. "events": _events_for_ids(bundle, [item.get("eventId") for item in calls]),
  164. },
  165. )
  166. payload.update(
  167. {
  168. "detailKind": "retrieval-group",
  169. "retrievalKind": "direct-tools",
  170. "activities": [
  171. {
  172. "id": item.get("id") or item.get("detailRef"),
  173. "kind": "tool",
  174. "label": item.get("businessLabel") or "读取相关信息",
  175. "status": item.get("status") or "unknown",
  176. "summary": item.get("resultSummary") or "",
  177. "detailRef": item.get("detailRef"),
  178. }
  179. for item in calls
  180. ],
  181. }
  182. )
  183. return payload
  184. if activity_id.startswith("retrieval-agent:"):
  185. run = _find_retrieval_item(view, "agentRuns", activity_id)
  186. if not run:
  187. raise KeyError(activity_id)
  188. event = event_detail or _event_by_id(bundle, run.get("eventId"))
  189. screening = run.get("screening") or {}
  190. full_task = _event_nested_value(event, "input", "task")
  191. raw_screening_text = _event_nested_value(event, "output", "summary")
  192. if not raw_screening_text:
  193. raw_screening_text = screening.get("preview")
  194. screening_text = clean_report(raw_screening_text)
  195. payload = _payload(
  196. [
  197. _section("task", "完整取数任务", full_task or run.get("taskPreview") or "取数目标未记录"),
  198. _section("queries", "查询过程", _agent_query_markdown(run)),
  199. _section("hits", "原始命中", _agent_hit_markdown(run)),
  200. _section(
  201. "screening",
  202. "初筛整理",
  203. screening_text or _screening_fallback(screening.get("state")),
  204. "report",
  205. ),
  206. ],
  207. {
  208. "scriptBuildId": script_build_id,
  209. "agentRun": run,
  210. "agentEvent": event,
  211. "queryEvents": _events_for_ids(bundle, [item.get("eventId") for item in run.get("attempts") or []]),
  212. },
  213. )
  214. activities = [
  215. {
  216. "id": item.get("id") or item.get("detailRef"),
  217. "kind": "query",
  218. "label": item.get("queryLabel") or "查询内容详见详情",
  219. "status": item.get("status") or "unknown",
  220. "resultCount": item.get("resultCount"),
  221. "detailRef": item.get("detailRef"),
  222. }
  223. for item in run.get("attempts") or []
  224. ]
  225. payload.update(
  226. {
  227. "detailKind": "retrieval-agent",
  228. "retrievalKind": "agent",
  229. "activities": activities,
  230. }
  231. )
  232. if not activities:
  233. if script_build_id <= 433:
  234. payload["missingActivityNotice"] = "该历史运行没有结构化查询明细。"
  235. elif run.get("agentType") == "retrieve_data_pattern_relation":
  236. payload["missingActivityNotice"] = (
  237. "本次运行未保存可下钻的结构化查询记录。"
  238. )
  239. return payload
  240. round_index, branch_id, role = _activity_parts(activity_id)
  241. round_ = _find_round(view, round_index) if round_index is not None else None
  242. if round_ is None:
  243. raise KeyError(activity_id)
  244. if branch_id is None:
  245. if role is None:
  246. return project_round_detail(script_build_id, round_, bundle)
  247. node = _round_node(round_, role)
  248. if not node:
  249. raise KeyError(activity_id)
  250. raw_round = next(
  251. (
  252. item
  253. for item in bundle.get("rounds") or []
  254. if int(item.get("round_index") or 0) == round_index
  255. ),
  256. None,
  257. )
  258. if role == "result":
  259. return _round_result_detail(script_build_id, round_, raw_round, bundle)
  260. return _node_detail(script_build_id, node, raw_round)
  261. branch = next(
  262. (
  263. item
  264. for item in round_.get("branches") or []
  265. if int(item.get("branchId") or 0) == branch_id
  266. ),
  267. None,
  268. )
  269. raw_branch = next(
  270. (
  271. item
  272. for item in bundle.get("branches") or []
  273. if int(item.get("branch_id") or 0) == branch_id
  274. ),
  275. None,
  276. )
  277. if not branch:
  278. raise KeyError(activity_id)
  279. if role is None:
  280. return _branch_detail(script_build_id, branch, raw_branch, bundle)
  281. if role == "retrieval":
  282. return _retrieval_stage_detail(script_build_id, branch, raw_branch, bundle)
  283. node = _branch_node(branch, role)
  284. if not node:
  285. raise KeyError(activity_id)
  286. if role == "output":
  287. return _branch_output_detail(script_build_id, branch, raw_branch, bundle)
  288. if role == "task":
  289. return _branch_task_detail(
  290. script_build_id,
  291. branch,
  292. raw_branch,
  293. bundle,
  294. round_index=round_index,
  295. branch_id=branch_id,
  296. )
  297. return _node_detail(script_build_id, node, raw_branch)
  298. def project_event_detail(
  299. script_build_id: int,
  300. event: dict[str, Any],
  301. *,
  302. run_status: str | None = None,
  303. ) -> dict[str, Any]:
  304. if is_retrieval_detail_event(event):
  305. payload = project_retrieval_event(event, run_status=run_status)
  306. payload["technical"] = sanitize(
  307. {
  308. "source": {
  309. "scriptBuildId": script_build_id,
  310. "table": "script_build_event / script_build_event_body",
  311. "detailRef": f"event:{event.get('id')}",
  312. },
  313. **_event_technical_groups(event),
  314. },
  315. max_text=None,
  316. )
  317. return sanitize(payload, max_text=None)
  318. if (
  319. event.get("event_name") == "think_and_plan"
  320. and event.get("agent_role") == "main"
  321. and int(event.get("agent_depth") or 0) == 0
  322. ):
  323. raw_input = event.get("input")
  324. content = raw_input.get("content") if isinstance(raw_input, dict) else None
  325. data = content if isinstance(content, dict) else event.get("inputData")
  326. data = data if isinstance(data, dict) else {}
  327. return _payload(
  328. [
  329. _section(
  330. "summary",
  331. "规划概要",
  332. data.get("thought_summary"),
  333. "summary",
  334. visual_role="key-result",
  335. ),
  336. _section(
  337. "thought",
  338. "完整思考",
  339. data.get("thought"),
  340. "report",
  341. visual_role="section",
  342. ),
  343. _section(
  344. "plan",
  345. "执行计划",
  346. data.get("plan"),
  347. visual_role="section",
  348. ),
  349. _section(
  350. "action",
  351. "下一步",
  352. data.get("action"),
  353. visual_role="reason",
  354. ),
  355. ],
  356. {
  357. "scriptBuildId": script_build_id,
  358. "table": "script_build_event / script_build_event_body",
  359. "event": event,
  360. },
  361. )
  362. return _payload(
  363. event_business_sections(event),
  364. {"scriptBuildId": script_build_id, "table": "script_build_event / script_build_event_body", "event": event},
  365. )
  366. def project_artifact_detail(
  367. script_build_id: int, snapshot_ref: str, artifact: dict[str, Any]
  368. ) -> dict[str, Any]:
  369. paragraphs, elements, links = _normalized_artifact_records(artifact)
  370. exactness = "current-only" if "base:current" in snapshot_ref else _artifact_exactness(artifact)
  371. context = artifact.get("artifactContext") if isinstance(artifact.get("artifactContext"), dict) else {}
  372. context = {
  373. "type": context.get("type") or ("final" if "base:current" in snapshot_ref else "candidate"),
  374. **context,
  375. "exactness": exactness,
  376. }
  377. candidate = context.get("type") == "candidate"
  378. scale_title = "候选脚本规模" if candidate else "主脚本规模"
  379. content_summary = "候选脚本内容" if candidate else "当前主脚本内容"
  380. payload = _payload(
  381. [
  382. _section("summary", scale_title, f"{len(paragraphs)} 个段落、{len(elements)} 个元素、{len(links)} 条关联"),
  383. ],
  384. {
  385. "scriptBuildId": script_build_id,
  386. "snapshotRef": snapshot_ref,
  387. "sourceOrigin": artifact.get("sourceOrigin") or "database",
  388. "exactness": exactness,
  389. "artifactContext": context,
  390. "recordCounts": {
  391. "paragraphs": len(paragraphs),
  392. "elements": len(elements),
  393. "links": len(links),
  394. },
  395. },
  396. {
  397. "summary": content_summary,
  398. "sections": [
  399. _section(
  400. "counts",
  401. "内容规模",
  402. f"{len(paragraphs)} 个段落、{len(elements)} 个元素、{len(links)} 条关联",
  403. ),
  404. ],
  405. "artifactRef": snapshot_ref,
  406. "exactness": exactness,
  407. },
  408. )
  409. payload["scriptTable"] = sanitize(
  410. _script_table_projection(snapshot_ref, artifact, exactness),
  411. max_text=None,
  412. )
  413. payload["artifactContext"] = sanitize(context, max_text=None)
  414. return payload
  415. _ELEMENT_REFERENCE = re.compile(r"\[([^\]:]+):(\d+)\]")
  416. _SCRIPT_SECTIONS = (
  417. ("theme", "主题"),
  418. ("form", "形式"),
  419. ("function", "作用"),
  420. ("feeling", "感受"),
  421. )
  422. def _script_table_projection(
  423. snapshot_ref: str, artifact: dict[str, Any], exactness: str
  424. ) -> dict[str, Any]:
  425. snapshot = artifact.get("snapshot") if isinstance(artifact.get("snapshot"), dict) else artifact
  426. snapshot = snapshot if isinstance(snapshot, dict) else {}
  427. paragraphs, elements, links = _normalized_artifact_records(artifact)
  428. element_names: dict[int, str] = {}
  429. for element in elements:
  430. element_id = _as_int(element.get("effectiveId") or element.get("id"))
  431. row_id = _as_int(element.get("rowId") or element.get("id"))
  432. name = str(element.get("name") or "").strip()
  433. if element_id is not None:
  434. element_names[element_id] = name
  435. if row_id is not None:
  436. element_names[row_id] = name
  437. paragraph_rows = _ordered_paragraphs(
  438. [_script_paragraph(row, element_names) for row in paragraphs]
  439. )
  440. paragraph_lookup: dict[int, dict[str, Any]] = {}
  441. for paragraph in paragraph_rows:
  442. for key in ("id", "rowId"):
  443. value = _as_int(paragraph.get(key))
  444. if value is not None:
  445. paragraph_lookup[value] = paragraph
  446. linked_paragraphs: dict[int, list[dict[str, Any]]] = {}
  447. for link in links:
  448. element_id = _as_int(link.get("element_id") or link.get("elementId"))
  449. paragraph_id = _as_int(link.get("paragraph_id") or link.get("paragraphId"))
  450. paragraph = paragraph_lookup.get(paragraph_id) if paragraph_id is not None else None
  451. if element_id is None or paragraph is None:
  452. continue
  453. linked_paragraphs.setdefault(element_id, []).append(
  454. {
  455. "id": paragraph.get("id"),
  456. "index": paragraph.get("index"),
  457. "name": paragraph.get("name"),
  458. }
  459. )
  460. element_rows = []
  461. for element in elements:
  462. element_id = element.get("effectiveId") or element.get("id")
  463. row_id = element.get("rowId") or element.get("id")
  464. link_keys = {
  465. value
  466. for value in (_as_int(row_id), _as_int(element_id))
  467. if value is not None
  468. }
  469. linked = [
  470. paragraph
  471. for key in link_keys
  472. for paragraph in linked_paragraphs.get(key, [])
  473. ]
  474. element_rows.append(
  475. {
  476. "id": element_id,
  477. "rowId": row_id,
  478. "name": element.get("name") or "未命名元素",
  479. "primaryDimension": element.get("dimension_primary"),
  480. "secondaryDimension": element.get("dimension_secondary"),
  481. "paragraphs": _unique_records(linked, "id"),
  482. }
  483. )
  484. element_rows.sort(key=lambda row: (str(row.get("name") or ""), _sort_number(row.get("id"))))
  485. return {
  486. "snapshotRef": snapshot_ref,
  487. "exactness": exactness,
  488. "sourceOrigin": artifact.get("sourceOrigin") or snapshot.get("sourceOrigin") or "database",
  489. "counts": {
  490. "paragraphs": len(paragraph_rows),
  491. "elements": len(element_rows),
  492. "links": len(links),
  493. },
  494. "maxDepth": (
  495. max((int(row.get("depth") or 0) for row in paragraph_rows), default=0) + 1
  496. if paragraph_rows
  497. else 0
  498. ),
  499. "paragraphs": paragraph_rows,
  500. "elements": element_rows,
  501. }
  502. def _normalized_artifact_records(
  503. artifact: dict[str, Any],
  504. ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
  505. """Normalize current overlays and frozen branch snapshots to one table shape.
  506. Current overlays store a flat ``paragraphs`` list plus top-level
  507. ``paragraphElements``. Frozen ``content_snapshot`` rows store second-level
  508. paragraphs under ``sub_paragraphs`` and links under each paragraph's
  509. ``linked_elements``. The visualizer must preserve both without pretending
  510. that a truncated top-level list is the complete candidate script.
  511. """
  512. snapshot = artifact.get("snapshot") if isinstance(artifact.get("snapshot"), dict) else artifact
  513. snapshot = snapshot if isinstance(snapshot, dict) else {}
  514. paragraphs: list[dict[str, Any]] = []
  515. seen_paragraphs: set[tuple[str, str]] = set()
  516. def visit(row: Any, parent_id: Any = None) -> None:
  517. if not isinstance(row, dict):
  518. return
  519. current = dict(row)
  520. children = current.pop("sub_paragraphs", None) or current.pop("subParagraphs", None) or []
  521. if parent_id is not None and current.get("parent_id") is None and current.get("parentId") is None:
  522. current["parent_id"] = parent_id
  523. row_id = current.get("rowId") or current.get("id")
  524. key = (str(row_id), str(current.get("baseRefId") or current.get("base_ref_id") or ""))
  525. if key not in seen_paragraphs:
  526. seen_paragraphs.add(key)
  527. paragraphs.append(current)
  528. effective_parent = current.get("effectiveId") or current.get("id") or row_id
  529. for child in children if isinstance(children, list) else []:
  530. visit(child, effective_parent)
  531. for paragraph in snapshot.get("paragraphs") or []:
  532. visit(paragraph)
  533. elements = [item for item in snapshot.get("elements") or [] if isinstance(item, dict)]
  534. links: list[dict[str, Any]] = []
  535. seen_links: set[tuple[str, str]] = set()
  536. def append_link(link: Any, paragraph_id: Any = None) -> None:
  537. if not isinstance(link, dict):
  538. return
  539. current = dict(link)
  540. if paragraph_id is not None and current.get("paragraph_id") is None and current.get("paragraphId") is None:
  541. current["paragraph_id"] = paragraph_id
  542. element_id = current.get("element_id") or current.get("elementId")
  543. linked_paragraph = current.get("paragraph_id") or current.get("paragraphId")
  544. if element_id is None or linked_paragraph is None:
  545. return
  546. key = (str(linked_paragraph), str(element_id))
  547. if key in seen_links:
  548. return
  549. seen_links.add(key)
  550. links.append(current)
  551. for link in snapshot.get("paragraphElements") or snapshot.get("paragraph_elements") or []:
  552. append_link(link)
  553. for paragraph in paragraphs:
  554. paragraph_id = paragraph.get("effectiveId") or paragraph.get("id") or paragraph.get("rowId")
  555. nested_links = paragraph.get("linked_elements") or paragraph.get("linkedElements") or []
  556. if isinstance(nested_links, list) and nested_links:
  557. for link in nested_links:
  558. append_link(link, paragraph_id)
  559. else:
  560. for element_id in paragraph.get("linked_element_ids") or paragraph.get("linkedElementIds") or []:
  561. append_link({"element_id": element_id}, paragraph_id)
  562. return paragraphs, elements, links
  563. def _script_paragraph(row: dict[str, Any], element_names: dict[int, str]) -> dict[str, Any]:
  564. full_description = str(row.get("full_description") or "")
  565. return {
  566. "id": row.get("effectiveId") or row.get("id"),
  567. "rowId": row.get("rowId") or row.get("id"),
  568. "parentId": row.get("parent_id"),
  569. "depth": 0,
  570. "index": row.get("paragraph_index"),
  571. "level": row.get("level"),
  572. "name": row.get("name") or "未命名段落",
  573. "contentRange": _content_range(row.get("content_range")),
  574. "sections": {
  575. key: {
  576. "label": label,
  577. "description": row.get(key),
  578. "descriptionParts": _description_parts(
  579. str(row.get(key) or ""), element_names
  580. ),
  581. "dimensions": _dimensions(row.get(f"{key}_elements")),
  582. }
  583. for key, label in _SCRIPT_SECTIONS
  584. },
  585. "description": row.get("description"),
  586. "fullDescription": full_description,
  587. "fullDescriptionParts": _description_parts(full_description, element_names),
  588. }
  589. def _ordered_paragraphs(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
  590. """Keep every child next to its real parent without guessing from level labels."""
  591. by_reference: dict[int, dict[str, Any]] = {}
  592. for row in rows:
  593. for value in (row.get("rowId"), row.get("id")):
  594. reference = _as_int(value)
  595. if reference is not None:
  596. by_reference[reference] = row
  597. children: dict[int, list[dict[str, Any]]] = {}
  598. roots: list[dict[str, Any]] = []
  599. for row in rows:
  600. parent = by_reference.get(_as_int(row.get("parentId")))
  601. if parent is None or parent is row:
  602. row["parentId"] = None
  603. roots.append(row)
  604. continue
  605. row["parentId"] = parent.get("id")
  606. children.setdefault(id(parent), []).append(row)
  607. ordered: list[dict[str, Any]] = []
  608. visited: set[int] = set()
  609. def visit(row: dict[str, Any], depth: int, ancestry: set[int]) -> None:
  610. marker = id(row)
  611. if marker in visited:
  612. return
  613. if marker in ancestry:
  614. row["parentId"] = None
  615. depth = 0
  616. visited.add(marker)
  617. row["depth"] = depth
  618. ordered.append(row)
  619. next_ancestry = {*ancestry, marker}
  620. for child in sorted(children.get(marker, []), key=_paragraph_sort_key):
  621. visit(child, depth + 1, next_ancestry)
  622. for root in sorted(roots, key=_paragraph_sort_key):
  623. visit(root, 0, set())
  624. for row in sorted(rows, key=_paragraph_sort_key):
  625. if id(row) not in visited:
  626. row["parentId"] = None
  627. visit(row, 0, set())
  628. return ordered
  629. def _paragraph_sort_key(row: dict[str, Any]) -> tuple[Any, ...]:
  630. return (_sort_number(row.get("index")), _sort_number(row.get("id")))
  631. def _content_range(value: Any) -> list[str]:
  632. if not isinstance(value, dict):
  633. return [_display_value(value)] if value not in (None, "") else []
  634. tags: list[str] = []
  635. for key, item in value.items():
  636. if key == "标题":
  637. tags.append("标题")
  638. elif key == "图片" and isinstance(item, list):
  639. if not item:
  640. continue
  641. values = [str(entry) for entry in item]
  642. tags.append(f"图{values[0]}" if len(values) == 1 else f"图{values[0]}-{values[-1]}")
  643. elif key == "视频":
  644. tags.append(_display_value(item))
  645. elif key == "正文":
  646. body = "\n".join(str(entry) for entry in item) if isinstance(item, list) else _display_value(item)
  647. tags.append(f"正文: {body}")
  648. else:
  649. rendered = ",".join(str(entry) for entry in item) if isinstance(item, list) else _display_value(item)
  650. tags.append(f"{key}: {rendered}")
  651. return tags
  652. def _dimensions(value: Any) -> list[dict[str, Any]]:
  653. rows = value if isinstance(value, list) else [value] if isinstance(value, dict) else []
  654. return [
  655. {
  656. "type": row.get("维度类型") or row.get("dimension_type"),
  657. "name": row.get("维度") or row.get("dimension"),
  658. "value": row.get("原子点") or row.get("value"),
  659. **({"id": row.get("id")} if row.get("id") is not None else {}),
  660. }
  661. for row in rows
  662. if isinstance(row, dict)
  663. ]
  664. def _description_parts(value: str, element_names: dict[int, str]) -> list[dict[str, Any]]:
  665. parts: list[dict[str, Any]] = []
  666. cursor = 0
  667. for match in _ELEMENT_REFERENCE.finditer(value):
  668. if match.start() > cursor:
  669. parts.append({"type": "text", "text": value[cursor : match.start()]})
  670. element_id = int(match.group(2))
  671. name = element_names.get(element_id)
  672. parts.append(
  673. {
  674. "type": "element",
  675. "id": element_id,
  676. "name": name,
  677. "resolved": name is not None,
  678. "text": match.group(0),
  679. }
  680. )
  681. cursor = match.end()
  682. if name:
  683. duplicate = re.match(r"\s*" + re.escape(name), value[cursor:])
  684. if duplicate:
  685. cursor += duplicate.end()
  686. if cursor < len(value):
  687. parts.append({"type": "text", "text": value[cursor:]})
  688. return parts
  689. def _display_value(value: Any) -> str:
  690. if value is True:
  691. return "包含"
  692. if value is False:
  693. return "不包含"
  694. if value is None:
  695. return ""
  696. return str(value)
  697. def _as_int(value: Any) -> int | None:
  698. try:
  699. return int(value) if value is not None else None
  700. except (TypeError, ValueError):
  701. return None
  702. def _sort_number(value: Any) -> tuple[int, str]:
  703. number = _as_int(value)
  704. return (0, str(number).zfill(12)) if number is not None else (1, str(value or ""))
  705. def _unique_records(rows: list[dict[str, Any]], key: str) -> list[dict[str, Any]]:
  706. seen: set[Any] = set()
  707. result = []
  708. for row in rows:
  709. value = row.get(key)
  710. if value in seen:
  711. continue
  712. seen.add(value)
  713. result.append(row)
  714. return result
  715. def _branch_detail(script_build_id: int, branch: dict[str, Any], raw: Any, bundle: dict[str, Any]) -> dict[str, Any]:
  716. facts = _branch_facts(branch, bundle)
  717. return _payload(
  718. [
  719. _section("task", "实现任务", branch_task_text(branch.get("summary", {}).get("task"))),
  720. _section("data", "数据取舍", decisions_markdown([item.get("decision") for item in branch.get("dataDecisions") or []])),
  721. _section("output", "候选产出", branch.get("summary", {}).get("output")),
  722. ],
  723. {"scriptBuildId": script_build_id, "branch": raw},
  724. {
  725. "summary": branch.get("summary", {}).get("output"),
  726. "sections": [_section("facts", "领域事实", _domain_facts_markdown(facts))] if facts else [],
  727. "artifactRef": branch.get("output", {}).get("snapshotRef"),
  728. "exactness": _output_exactness(branch.get("output") or {}),
  729. }
  730. if branch.get("output", {}).get("node", {}).get("hasChanges")
  731. else None,
  732. )
  733. def _branch_task_detail(
  734. script_build_id: int,
  735. branch: dict[str, Any],
  736. raw: Any,
  737. bundle: dict[str, Any],
  738. *,
  739. round_index: int,
  740. branch_id: int,
  741. ) -> dict[str, Any]:
  742. task_event = next(
  743. (
  744. event
  745. for event in reversed(bundle.get("events") or [])
  746. if str(event.get("event_type") or "") == "agent_invoke"
  747. and str(event.get("event_name") or "") == "script_implementer"
  748. and int(event.get("round_index") or 0) == round_index
  749. and int(event.get("branch_id") or 0) == branch_id
  750. ),
  751. None,
  752. )
  753. event_input = (task_event or {}).get("inputData")
  754. event_task = event_input.get("task") if isinstance(event_input, dict) else None
  755. raw_task = (raw or {}).get("impl_task") if isinstance(raw, dict) else None
  756. complete_task = branch_task_text(
  757. event_task
  758. or raw_task
  759. or branch.get("summary", {}).get("task")
  760. )
  761. task_sections = branch_task_sections(
  762. event_task
  763. or raw_task
  764. or branch.get("summary", {}).get("task")
  765. )
  766. visual_roles = {
  767. "goal": ("summary", "key-result"),
  768. "materials": ("report", "evidence"),
  769. "avoid": ("report", "notice"),
  770. "context": ("report", "evidence"),
  771. }
  772. return _payload(
  773. [
  774. _section(
  775. f"task-{section['kind']}-{index}",
  776. section["title"],
  777. section["content"],
  778. visual_roles.get(section["kind"], ("default", "section"))[0],
  779. visual_role=visual_roles.get(section["kind"], ("default", "section"))[1],
  780. presentation="document" if section["kind"] in {"materials", "constraints", "avoid", "context"} else "prose",
  781. )
  782. for index, section in enumerate(task_sections, 1)
  783. ] or [_section("task-notes-1", "任务说明", complete_task)],
  784. {
  785. "scriptBuildId": script_build_id,
  786. "branch": raw,
  787. "taskEvent": task_event,
  788. "taskSource": (
  789. "script_build_event.inputData.task"
  790. if event_task
  791. else "script_build_branch.impl_task"
  792. if raw_task
  793. else "execution-view summary"
  794. ),
  795. },
  796. )
  797. def _branch_output_detail(script_build_id: int, branch: dict[str, Any], raw: Any, bundle: dict[str, Any]) -> dict[str, Any]:
  798. output = branch.get("output") or {}
  799. facts = _branch_facts(branch, bundle)
  800. changes = {
  801. "summary": output.get("summary"),
  802. "sections": [_section("facts", "已核实领域事实", _domain_facts_markdown(facts))] if facts else [],
  803. "artifactRef": output.get("snapshotRef"),
  804. "exactness": _output_exactness(output),
  805. }
  806. return _payload(
  807. [
  808. _section("output", "形成了什么", output.get("summary")),
  809. _section("assessment", "实现者说明", assessment_text((raw or {}).get("self_assessment")), "report"),
  810. ],
  811. {"scriptBuildId": script_build_id, "branch": raw, "output": output},
  812. changes if output.get("node", {}).get("hasChanges") else None,
  813. )
  814. def _retrieval_stage_detail(script_build_id: int, branch: dict[str, Any], raw: Any, bundle: dict[str, Any]) -> dict[str, Any]:
  815. stage = branch.get("retrievalStage") or {}
  816. direct = stage.get("directToolGroups") or []
  817. agents = stage.get("agentRuns") or []
  818. return _payload(
  819. [
  820. _section(
  821. "purpose",
  822. "取数目的",
  823. branch_task_text(branch.get("summary", {}).get("task"))
  824. or "为当前实现任务补齐可用的上游信息。",
  825. ),
  826. _section("mode", "执行方式", _retrieval_mode_text(stage.get("observedMode"))),
  827. _section("waves", "执行波次", _retrieval_waves_markdown(stage.get("waves") or [])),
  828. _section("direct", "工具取数", _direct_groups_markdown(direct)),
  829. _section("agents", "Agent 取数", _agent_runs_markdown(agents)),
  830. _section("result", "阶段结果与失败", _retrieval_stage_result(stage)),
  831. ],
  832. {
  833. "scriptBuildId": script_build_id,
  834. "branch": raw,
  835. "retrievalStage": stage,
  836. "note": "取数和初筛来自结构化运行事件;正式数据取舍来自独立业务表,不在本详情内反推。",
  837. },
  838. )
  839. def _node_detail(script_build_id: int, node: dict[str, Any], technical: Any) -> dict[str, Any]:
  840. fields = []
  841. primary = node.get("card", {}).get("primary")
  842. if primary:
  843. fields.append(_section("primary", primary.get("label") or node.get("title"), primary.get("value")))
  844. for line in node.get("card", {}).get("secondary") or []:
  845. fields.append(_section(str(line.get("key")), line.get("label") or "补充说明", line.get("value")))
  846. return _payload(fields, {"scriptBuildId": script_build_id, "record": technical})
  847. def _round_result_detail(
  848. script_build_id: int,
  849. round_: dict[str, Any],
  850. raw_round: dict[str, Any] | None,
  851. bundle: dict[str, Any],
  852. ) -> dict[str, Any]:
  853. result = round_.get("result") or {}
  854. branches = round_.get("branches") or []
  855. round_index = int(round_.get("roundIndex") or 0)
  856. facts = [
  857. item
  858. for item in bundle.get("domainInfo") or []
  859. if int(item.get("round_index") or 0) == round_index
  860. ]
  861. evaluation = round_.get("overallEvaluation") or {}
  862. next_step = next(
  863. (
  864. line.get("value")
  865. for line in (result.get("card") or {}).get("secondary") or []
  866. if line.get("key") == "next"
  867. ),
  868. None,
  869. )
  870. sections = [
  871. _section(
  872. "goal",
  873. "本轮目标完成情况",
  874. "\n".join(
  875. part
  876. for part in (
  877. business_text((raw_round or {}).get("goal")),
  878. f"收口状态:{result.get('status') or round_.get('state') or '未记录'}",
  879. )
  880. if part
  881. ),
  882. ),
  883. _section("outcomes", "方案处置", _branch_outcomes_markdown(branches)),
  884. _section("facts", "领域事实", _domain_facts_markdown(facts) or "本轮没有新增领域事实记录。"),
  885. _section(
  886. "evaluation",
  887. "整体评审",
  888. _node_value(evaluation) or "未找到明确的本轮整体评审结论。",
  889. ),
  890. _section(
  891. "changes",
  892. "主脚本变化",
  893. _round_content_changes(branches),
  894. ),
  895. _section("next", "下一步", next_step or "下一步未记录。"),
  896. ]
  897. changes = None
  898. if result.get("hasChanges"):
  899. changes = {
  900. "summary": _node_value(result),
  901. "sections": [
  902. _section("content", "内容方案", _round_content_changes(branches)),
  903. _section("domain", "领域信息", f"本轮独立保存 {len(facts)} 条领域事实。"),
  904. ],
  905. "artifactRef": "artifact:base:current",
  906. "exactness": "current-only",
  907. }
  908. return _payload(
  909. sections,
  910. {
  911. "scriptBuildId": script_build_id,
  912. "round": raw_round,
  913. "resultProjection": result,
  914. "note": "本轮主脚本历史快照无法完整还原,产物入口指向当前主脚本。",
  915. },
  916. changes,
  917. )
  918. def _final_result_detail(
  919. script_build_id: int, view: dict[str, Any], bundle: dict[str, Any]
  920. ) -> dict[str, Any]:
  921. final = view.get("finalResult") or {}
  922. record = bundle.get("record") or {}
  923. branches = final.get("branchCounts") or {}
  924. artifact = final.get("artifact") or {}
  925. status = str(final.get("status") or record.get("status") or "unknown")
  926. conclusion = {
  927. "success": "构建已完成。",
  928. "completed": "构建已完成。",
  929. "partial": "构建部分完成。",
  930. "failed": "构建失败。",
  931. "stopped": "构建已停止。",
  932. "running": "构建仍在进行。",
  933. }.get(status.lower(), f"构建状态:{status}。")
  934. summary_or_error = final.get("summary")
  935. if final.get("errorMessage"):
  936. summary_or_error = f"失败原因:{final['errorMessage']}"
  937. sections = [
  938. _section("conclusion", "构建结论", conclusion, "summary"),
  939. _section("summary", "构建摘要或失败原因", summary_or_error or "未另外记录构建摘要。"),
  940. _section(
  941. "statistics",
  942. "轮次与方案统计",
  943. (
  944. f"共 {int(final.get('roundCount') or 0)} 轮,"
  945. f"{int(branches.get('total') or 0)} 个方案;"
  946. f"采用 {int(branches.get('merged') or 0)},"
  947. f"暂存 {int(branches.get('parked') or 0)},"
  948. f"未采用 {int(branches.get('discarded') or 0)},"
  949. f"待处理 {int(branches.get('open') or 0)}。"
  950. ),
  951. ),
  952. _section(
  953. "artifact",
  954. "最终主脚本规模",
  955. (
  956. f"{int(artifact.get('paragraphCount') or 0)} 个段落、"
  957. f"{int(artifact.get('elementCount') or 0)} 个元素、"
  958. f"{int(artifact.get('linkCount') or 0)} 条关联。"
  959. ),
  960. ),
  961. _section(
  962. "version",
  963. "版本说明",
  964. "当前只能确认数据库保存的最新主脚本,不将它伪装成某一轮结束时的历史快照。",
  965. ),
  966. ]
  967. return _payload(
  968. sections,
  969. {
  970. "scriptBuildId": script_build_id,
  971. "table": "script_build_record",
  972. "record": record,
  973. "finalResultProjection": final,
  974. },
  975. {
  976. "summary": "打开当前主脚本。",
  977. "sections": [],
  978. "artifactRef": artifact.get("snapshotRef") or "artifact:base:current",
  979. "exactness": "current-only",
  980. },
  981. )
  982. def _find_decision_projection(
  983. view: dict[str, Any], detail_ref: str
  984. ) -> dict[str, Any] | None:
  985. candidates: list[dict[str, Any]] = []
  986. objective = view.get("objective") or {}
  987. if isinstance(objective.get("decision"), dict):
  988. candidates.append(objective["decision"])
  989. for round_ in view.get("rounds") or []:
  990. for node in (
  991. round_.get("goal"),
  992. (round_.get("plan") or {}).get("node"),
  993. round_.get("overallEvaluation"),
  994. ):
  995. if isinstance(node, dict) and isinstance(node.get("decision"), dict):
  996. candidates.append(node["decision"])
  997. for branch in round_.get("branches") or []:
  998. for item in branch.get("dataDecisions") or []:
  999. projection = item.get("decisionProjection")
  1000. if isinstance(projection, dict):
  1001. candidates.append(projection)
  1002. output_node = (branch.get("output") or {}).get("node") or {}
  1003. if isinstance(output_node.get("decision"), dict):
  1004. candidates.append(output_node["decision"])
  1005. for batch in round_.get("convergenceBatches") or []:
  1006. review = batch.get("review") or {}
  1007. decision = batch.get("decision") or {}
  1008. if isinstance(review.get("decisionProjection"), dict):
  1009. candidates.append(review["decisionProjection"])
  1010. if isinstance(decision.get("decisionProjection"), dict):
  1011. candidates.append(decision["decisionProjection"])
  1012. return next(
  1013. (
  1014. item
  1015. for item in candidates
  1016. if detail_ref in {item.get("id"), item.get("detailRef")}
  1017. ),
  1018. None,
  1019. )
  1020. def _find_missing_convergence(
  1021. view: dict[str, Any], detail_ref: str
  1022. ) -> tuple[dict[str, Any], dict[str, Any]] | None:
  1023. for round_ in view.get("rounds") or []:
  1024. for batch in round_.get("convergenceBatches") or []:
  1025. for key in ("missingReview", "missingDecision"):
  1026. node = batch.get(key)
  1027. if isinstance(node, dict) and detail_ref in {
  1028. node.get("id"),
  1029. node.get("detailRef"),
  1030. }:
  1031. return node, batch
  1032. return None
  1033. def _decision_detail_payload(
  1034. script_build_id: int,
  1035. decision: dict[str, Any],
  1036. bundle: dict[str, Any],
  1037. activity_id: str,
  1038. event_detail: dict[str, Any] | None = None,
  1039. ) -> dict[str, Any]:
  1040. detail = decision.get("detail") if isinstance(decision.get("detail"), dict) else {}
  1041. blocks = [
  1042. block
  1043. for block in detail.get("blocks") or []
  1044. if isinstance(block, dict)
  1045. ]
  1046. for notice in decision.get("notices") or []:
  1047. if not isinstance(notice, dict) or not notice.get("message"):
  1048. continue
  1049. blocks.append(
  1050. {
  1051. "type": "notice",
  1052. "level": "warning" if "missing" in str(notice.get("code")) else "info",
  1053. "title": "记录说明",
  1054. "value": notice["message"],
  1055. "visualRole": "notice",
  1056. "presentation": "prose",
  1057. }
  1058. )
  1059. event_refs = [
  1060. ref
  1061. for ref in [
  1062. *(decision.get("eventRefs") or []),
  1063. *(decision.get("technicalRefs") or []),
  1064. ]
  1065. if isinstance(ref, str) and ref.startswith("event:")
  1066. ]
  1067. event_refs = list(dict.fromkeys(event_refs))
  1068. technical: dict[str, Any] = {
  1069. "source": {
  1070. "scriptBuildId": script_build_id,
  1071. "detailRef": activity_id,
  1072. "technicalRefs": event_refs,
  1073. },
  1074. "association": {
  1075. "completeness": decision.get("completeness"),
  1076. "notices": decision.get("notices") or [],
  1077. },
  1078. "rawDecisionProjection": decision,
  1079. "rawBusinessRecord": _decision_raw_record(bundle, activity_id),
  1080. }
  1081. if event_refs:
  1082. technical["runtimeEvents"] = _events_for_ids(
  1083. bundle, [_suffix_int(ref) for ref in event_refs]
  1084. )
  1085. if event_detail:
  1086. technical.update(_event_technical_groups(event_detail))
  1087. payload: dict[str, Any] = {
  1088. "detailKind": "agent-decision",
  1089. "decisionType": decision.get("decisionType"),
  1090. "actor": decision.get("actor"),
  1091. "authority": decision.get("authority"),
  1092. "question": decision.get("question"),
  1093. "promptRef": decision.get("promptRef"),
  1094. "blocks": sanitize(blocks, max_text=None),
  1095. "technical": sanitize(technical, max_text=None),
  1096. }
  1097. artifact_ref = decision.get("artifactRef")
  1098. if artifact_ref:
  1099. payload["changes"] = {
  1100. "summary": (decision.get("body") or {}).get("output")
  1101. or "已形成可查看的候选产出。",
  1102. "sections": [],
  1103. "artifactRef": artifact_ref,
  1104. "exactness": "historical-snapshot",
  1105. }
  1106. return payload
  1107. def _event_technical_groups(event: dict[str, Any]) -> dict[str, Any]:
  1108. """Split a lazily loaded event body into the Inspector's technical groups.
  1109. The execution bundle only carries bounded previews. Once a user opens an
  1110. event-backed decision, the full (sanitized) body belongs here rather than
  1111. being silently replaced by those previews.
  1112. """
  1113. metadata = {
  1114. key: value
  1115. for key, value in event.items()
  1116. if key not in {"input", "output", "duration_ms"}
  1117. }
  1118. groups: dict[str, Any] = {
  1119. "input": event.get("input"),
  1120. "output": event.get("output"),
  1121. "rawEvent": metadata,
  1122. }
  1123. if event.get("duration_ms") is not None:
  1124. groups["cost"] = {"durationMs": event.get("duration_ms")}
  1125. return groups
  1126. def _decision_raw_record(bundle: dict[str, Any], activity_id: str) -> Any:
  1127. if activity_id.startswith("data-decision:"):
  1128. return _find_by_id(bundle.get("dataDecisions") or [], _suffix_int(activity_id))
  1129. if activity_id.startswith("multipath-decision:"):
  1130. return _find_by_id(
  1131. bundle.get("multipathDecisions") or [], _suffix_int(activity_id)
  1132. )
  1133. if activity_id.startswith("event:"):
  1134. return _event_by_id(bundle, _suffix_int(activity_id))
  1135. if activity_id.startswith("creative:"):
  1136. event_id = _suffix_int(activity_id)
  1137. event = _event_by_id(bundle, event_id)
  1138. branch_id = int((event or {}).get("branch_id") or 0)
  1139. branch = next(
  1140. (
  1141. item
  1142. for item in bundle.get("branches") or []
  1143. if int(item.get("branch_id") or 0) == branch_id
  1144. ),
  1145. None,
  1146. )
  1147. return {
  1148. "anchorEvent": event,
  1149. "candidateSnapshot": (branch or {}).get("candidate_snapshot"),
  1150. }
  1151. if activity_id == "run:objective":
  1152. return bundle.get("record")
  1153. round_index, _, role = _activity_parts(activity_id)
  1154. if role in {"goal", "plan"}:
  1155. return next(
  1156. (
  1157. item
  1158. for item in bundle.get("rounds") or []
  1159. if int(item.get("round_index") or 0) == int(round_index or 0)
  1160. ),
  1161. None,
  1162. )
  1163. return None
  1164. def _payload(sections: list[dict[str, Any] | None], technical: Any, changes: dict[str, Any] | None = None) -> dict[str, Any]:
  1165. payload: dict[str, Any] = {
  1166. "businessSections": [item for item in sections if item],
  1167. "technical": sanitize(technical, max_text=None),
  1168. }
  1169. if changes:
  1170. changes["sections"] = [item for item in changes.get("sections", []) if item]
  1171. payload["changes"] = sanitize(changes, max_text=None)
  1172. return payload
  1173. def _section(
  1174. section_id: str,
  1175. title: str,
  1176. content: Any,
  1177. variant: str = "default",
  1178. *,
  1179. visual_role: str | None = None,
  1180. presentation: str = "prose",
  1181. collapsible: bool = False,
  1182. default_open: bool = False,
  1183. ) -> dict[str, Any] | None:
  1184. if content is None or content == "" or content == []:
  1185. return None
  1186. if not isinstance(content, str):
  1187. return None
  1188. content = business_text(content)
  1189. if not content:
  1190. return None
  1191. return {
  1192. "id": section_id,
  1193. "title": title,
  1194. "content": sanitize(content, max_text=None),
  1195. "variant": variant,
  1196. "visualRole": visual_role or ("key-result" if variant == "summary" else "section"),
  1197. "presentation": presentation,
  1198. "collapsible": collapsible,
  1199. "defaultOpen": default_open,
  1200. }
  1201. def _items_section(
  1202. section_id: str,
  1203. title: str,
  1204. items: list[dict[str, Any] | None],
  1205. *,
  1206. fallback: Any = None,
  1207. variant: str = "default",
  1208. visual_role: str | None = None,
  1209. collapsible: bool = False,
  1210. default_open: bool = False,
  1211. ) -> dict[str, Any] | None:
  1212. values = [item for item in items if item]
  1213. if not values and fallback in (None, "", []):
  1214. return None
  1215. content = "\n".join(
  1216. f"{item.get('label')}:{item.get('value')}" for item in values
  1217. ) or str(fallback)
  1218. section = _section(
  1219. section_id,
  1220. title,
  1221. content,
  1222. variant,
  1223. visual_role=visual_role,
  1224. presentation="list",
  1225. collapsible=collapsible,
  1226. default_open=default_open,
  1227. )
  1228. if section is not None and values:
  1229. section["items"] = sanitize(values, max_text=None)
  1230. return section
  1231. def _detail_item(label: Any, value: Any) -> dict[str, Any] | None:
  1232. if value in (None, "", []):
  1233. return None
  1234. return {
  1235. "label": sanitize(business_text(str(label)), max_text=None),
  1236. "value": sanitize(business_text(str(value)), max_text=None),
  1237. }
  1238. def _activity_parts(activity_id: str) -> tuple[int | None, int | None, str | None]:
  1239. parts = activity_id.split(":")
  1240. if len(parts) < 2 or parts[0] != "round":
  1241. return None, None, None
  1242. try:
  1243. round_index = int(parts[1])
  1244. except ValueError:
  1245. return None, None, None
  1246. if len(parts) >= 4 and parts[2] == "branch":
  1247. try:
  1248. branch_id = int(parts[3])
  1249. except ValueError:
  1250. return round_index, None, None
  1251. return round_index, branch_id, parts[4] if len(parts) > 4 else None
  1252. return round_index, None, parts[2] if len(parts) > 2 else None
  1253. def _round_node(round_: dict[str, Any], role: str) -> dict[str, Any] | None:
  1254. if role == "goal":
  1255. return round_.get("goal")
  1256. if role == "plan":
  1257. return (round_.get("plan") or {}).get("node")
  1258. if role == "result":
  1259. return round_.get("result")
  1260. if role == "evaluation":
  1261. return round_.get("overallEvaluation")
  1262. return None
  1263. def _branch_node(branch: dict[str, Any], role: str) -> dict[str, Any] | None:
  1264. if role == "task":
  1265. return branch.get("task")
  1266. if role == "output":
  1267. return branch.get("output", {}).get("node")
  1268. return None
  1269. def _convergence_markdown(batches: list[dict[str, Any]]) -> str:
  1270. if not batches:
  1271. return "未找到候选评审或主 Agent 多路决策记录。"
  1272. rows: list[str] = []
  1273. for index, batch in enumerate(batches, 1):
  1274. review = batch.get("review") or {}
  1275. decision = batch.get("decision") or {}
  1276. branch_ids = batch.get("branchIds") or []
  1277. rows.append(
  1278. f"### 第 {index} 批 · "
  1279. + ("、".join(f"方案 {value}" for value in branch_ids) or "方案范围未记录")
  1280. )
  1281. rows.append(
  1282. f"**多方案评审:** {review.get('recommendation') or review.get('comparison') or review.get('conclusion') or '评审结论未记录'}"
  1283. )
  1284. rows.append(
  1285. f"**主 Agent 决策:** {business_text(decision.get('decision')) or '决策尚未记录'}"
  1286. )
  1287. return "\n\n".join(rows)
  1288. def _find_retrieval_item(view: dict[str, Any], collection: str, item_id: str) -> dict[str, Any] | None:
  1289. for round_ in view.get("rounds") or []:
  1290. for branch in round_.get("branches") or []:
  1291. for item in (branch.get("retrievalStage") or {}).get(collection) or []:
  1292. if item.get("id") == item_id:
  1293. return item
  1294. return None
  1295. def _event_by_id(bundle: dict[str, Any], event_id: Any) -> dict[str, Any] | None:
  1296. return next(
  1297. (item for item in bundle.get("events") or [] if int(item.get("id") or 0) == int(event_id or 0)),
  1298. None,
  1299. )
  1300. def _events_for_ids(bundle: dict[str, Any], event_ids: list[Any]) -> list[dict[str, Any]]:
  1301. wanted = {int(value) for value in event_ids if value is not None}
  1302. return [item for item in bundle.get("events") or [] if int(item.get("id") or 0) in wanted]
  1303. def _event_nested_value(
  1304. event: dict[str, Any] | None, side: str, key: str
  1305. ) -> Any:
  1306. if not isinstance(event, dict):
  1307. return None
  1308. aliases = (
  1309. ("inputData", "input_data")
  1310. if side == "input"
  1311. else ("agentOutputData", "outputData", "output_data")
  1312. )
  1313. for alias in aliases:
  1314. value = event.get(alias)
  1315. if isinstance(value, dict) and value.get(key) not in (None, ""):
  1316. return value.get(key)
  1317. wrapped = event.get(side)
  1318. if isinstance(wrapped, dict):
  1319. content = wrapped.get("content")
  1320. if isinstance(content, dict):
  1321. return content.get(key)
  1322. if key == "summary" and isinstance(content, str):
  1323. return content
  1324. return None
  1325. def _direct_sources_markdown(group: dict[str, Any]) -> str:
  1326. rows = group.get("sources") or []
  1327. return "\n".join(
  1328. f"- {item.get('businessLabel') or '相关信息'}:{int(item.get('callCount') or 0)} 次"
  1329. for item in rows
  1330. ) or "未记录具体读取项"
  1331. def _direct_result_markdown(calls: list[dict[str, Any]]) -> str:
  1332. success = sum(1 for item in calls if item.get("status") == "success")
  1333. failure = sum(1 for item in calls if item.get("status") == "failure")
  1334. running = sum(1 for item in calls if item.get("status") == "running")
  1335. interrupted = sum(1 for item in calls if item.get("status") == "interrupted")
  1336. return f"共 {len(calls)} 次读取:{success} 次完成,{failure} 次失败,{running} 次进行中,{interrupted} 次因构建中断。"
  1337. def _agent_query_markdown(run: dict[str, Any]) -> str:
  1338. summary = run.get("querySummary") or {}
  1339. total = int(summary.get("total") or 0)
  1340. if total == 0 and not (run.get("attempts") or []):
  1341. return "Agent 已完成,没有发起查询。"
  1342. header = (
  1343. f"共 {total} 次查询:"
  1344. f"{int(summary.get('hit') or 0)} 次命中,"
  1345. f"{int(summary.get('empty') or 0)} 次无结果,"
  1346. f"{int(summary.get('failure') or 0)} 次失败,"
  1347. f"{int(summary.get('interrupted') or 0)} 次因构建中断。"
  1348. )
  1349. rows = []
  1350. for index, attempt in enumerate(run.get("attempts") or [], 1):
  1351. query = attempt.get("queryLabel") or attempt.get("queryPreview") or attempt.get("businessLabel") or "查询"
  1352. label = {"hit": "命中", "empty": "无结果", "failure": "失败", "running": "进行中", "interrupted": "构建中断", "unknown": "结果数不明"}.get(attempt.get("status"), "结果数不明")
  1353. count = attempt.get("resultCount")
  1354. suffix = f"· {count} 条" if isinstance(count, int) and count > 0 else f"· {label}"
  1355. rows.append(f"{index:02d}. {query} {suffix}")
  1356. return "\n".join([header, *rows])
  1357. def _agent_hit_markdown(run: dict[str, Any]) -> str:
  1358. attempts = run.get("attempts") or []
  1359. hits = [item for item in attempts if item.get("status") == "hit"]
  1360. empty = [item for item in attempts if item.get("status") == "empty"]
  1361. failures = [
  1362. item
  1363. for item in attempts
  1364. if item.get("status") in {"failure", "interrupted"}
  1365. ]
  1366. rows = [
  1367. f"- {item.get('queryLabel') or '查询'}:{int(item.get('resultCount') or 0)} 条"
  1368. for item in hits
  1369. ]
  1370. rows.append(f"成功命中 {len(hits)} 次,成功为空 {len(empty)} 次,技术失败或中断 {len(failures)} 次。")
  1371. return "\n".join(rows)
  1372. def _screening_fallback(state: Any) -> str:
  1373. return {"running": "Agent 仍在整理查询结果。", "missing": "未找到 Agent 的初筛整理记录。"}.get(str(state), "初筛整理未记录。")
  1374. def _retrieval_mode_text(mode: Any) -> str:
  1375. return {
  1376. "single": "本次只有一个取数操作。",
  1377. "sequential": "各项取数依次执行。",
  1378. "parallel": "各项取数在同一时间段并行执行。",
  1379. "mixed": "本次同时存在并行取数和后续取数。",
  1380. "unknown": "运行时间记录不足,无法确认执行先后。",
  1381. }.get(str(mode), "执行方式未记录。")
  1382. def _retrieval_waves_markdown(waves: list[dict[str, Any]]) -> str:
  1383. if not waves:
  1384. return "运行时间或并发信息不足,无法安全还原执行波次。"
  1385. rows = []
  1386. for index, wave in enumerate(waves, 1):
  1387. operations = wave.get("operations") or wave.get("operationIds") or []
  1388. rows.append(
  1389. f"- 第 {index} 波:{len(operations)} 个取数操作"
  1390. )
  1391. return "\n".join(rows)
  1392. def _retrieval_stage_result(stage: dict[str, Any]) -> str:
  1393. direct_calls = [
  1394. call
  1395. for group in stage.get("directToolGroups") or []
  1396. for call in group.get("calls") or []
  1397. ]
  1398. attempts = [
  1399. attempt
  1400. for run in stage.get("agentRuns") or []
  1401. for attempt in run.get("attempts") or []
  1402. ]
  1403. failures = sum(
  1404. 1
  1405. for item in [*direct_calls, *attempts]
  1406. if item.get("status") in {"failure", "interrupted"}
  1407. )
  1408. hits = sum(1 for item in attempts if item.get("status") == "hit")
  1409. empty = sum(1 for item in attempts if item.get("status") == "empty")
  1410. return (
  1411. f"阶段状态:{stage.get('status') or '未记录'}。"
  1412. f"工具读取 {len(direct_calls)} 次,Agent 查询 {len(attempts)} 次;"
  1413. f"命中 {hits} 次,成功为空 {empty} 次,失败或中断 {failures} 次。"
  1414. )
  1415. def _direct_groups_markdown(groups: list[dict[str, Any]]) -> str:
  1416. if not groups:
  1417. return "本次未记录实现 Agent 直接读取工具。"
  1418. return "\n".join(
  1419. f"- 第 {index} 组:{'、'.join(item.get('businessLabel') or '相关信息' for item in group.get('sources') or [])}({int(group.get('callCount') or 0)} 次)"
  1420. for index, group in enumerate(groups, 1)
  1421. )
  1422. def _agent_runs_markdown(runs: list[dict[str, Any]]) -> str:
  1423. if not runs:
  1424. return "本次未记录委派取数 Agent。"
  1425. return "\n".join(
  1426. f"- {item.get('businessLabel') or '取数 Agent'}:{int((item.get('querySummary') or {}).get('total') or 0)} 次查询,初筛{'已记录' if (item.get('screening') or {}).get('state') == 'available' else '未完整记录'}"
  1427. for item in runs
  1428. )
  1429. def _find_round(view: dict[str, Any], round_index: int | None) -> dict[str, Any] | None:
  1430. return next((item for item in view.get("rounds") or [] if int(item.get("roundIndex") or 0) == int(round_index or 0)), None)
  1431. def _find_by_id(rows: list[dict[str, Any]], record_id: int) -> dict[str, Any] | None:
  1432. return next((item for item in rows if int(item.get("id") or 0) == record_id), None)
  1433. def _suffix_int(value: str) -> int:
  1434. try:
  1435. return int(value.rsplit(":", 1)[1])
  1436. except (IndexError, ValueError) as exc:
  1437. raise KeyError(value) from exc
  1438. def _node_value(node: dict[str, Any] | None) -> str | None:
  1439. return ((node or {}).get("card") or {}).get("primary", {}).get("value")
  1440. def _plan_text(plan: dict[str, Any]) -> str:
  1441. paths = plan.get("paths") or []
  1442. parts = [f"方式:{plan.get('mode') or '未记录'}", f"当前保存 {len(paths)} 个方案"]
  1443. if plan.get("note"):
  1444. parts.append(f"说明:{plan['note']}")
  1445. revisions = int(plan.get("runtimeRevisionCount") or 0)
  1446. if revisions > 1:
  1447. parts.append(f"运行事件中找到 {revisions} 次规划记录;历史版本只在技术详情中查看。")
  1448. return "\n".join(parts)
  1449. def _round_content_changes(branches: list[dict[str, Any]]) -> str:
  1450. merged = [item for item in branches if item.get("pathType") != "领域信息" and item.get("status") == "merged"]
  1451. return f"确认采用 {len(merged)} 个内容方案;无法据此还原本轮结束时的完整主脚本快照。"
  1452. def _branch_outcomes_markdown(branches: list[dict[str, Any]]) -> str:
  1453. if not branches:
  1454. return "本轮没有可展示的方案处置记录。"
  1455. rows = []
  1456. for item in branches:
  1457. outcome = item.get("outcome") or {}
  1458. line = (
  1459. f"- 方案 {item.get('branchId')}:"
  1460. f"{outcome.get('label') or item.get('status') or '状态未记录'}"
  1461. )
  1462. if outcome.get("reasoning"):
  1463. line += f"。{business_text(outcome.get('reasoning'))}"
  1464. rows.append(line)
  1465. return "\n".join(rows)
  1466. def _round_domain_changes(branches: list[dict[str, Any]]) -> str:
  1467. count = sum(int(item.get("output", {}).get("factCount") or 0) for item in branches if item.get("pathType") == "领域信息")
  1468. return f"新增 {count} 条独立保存的领域事实。"
  1469. def _branch_facts(branch: dict[str, Any], bundle: dict[str, Any]) -> list[dict[str, Any]]:
  1470. refs = set(branch.get("output", {}).get("factRefs") or [])
  1471. return [item for item in bundle.get("domainInfo") or [] if f"domain-info:{item.get('id')}" in refs]
  1472. def _output_exactness(output: dict[str, Any]) -> str:
  1473. if output.get("type") == "domain-info":
  1474. return "exact"
  1475. historical = output.get("accuracy", {}).get("historical")
  1476. return "historical-snapshot" if historical == "exact" else "current-only" if output.get("snapshotRef") else "unknown"
  1477. def _artifact_exactness(artifact: dict[str, Any]) -> str:
  1478. return "historical-snapshot" if artifact.get("historicalAccuracy") == "exact" else "current-only"
  1479. def _domain_facts_markdown(rows: list[dict[str, Any]]) -> str:
  1480. values = [business_text(row.get("content")) for row in rows]
  1481. return "\n".join(f"- {value}" for value in values if value)
  1482. def _source_value(value: Any) -> str:
  1483. if isinstance(value, str):
  1484. return value
  1485. if isinstance(value, list):
  1486. return source_markdown(value)
  1487. if isinstance(value, dict):
  1488. return source_markdown([value]) or business_text(value.get("url") or value.get("name"))
  1489. return ""