decision_projection.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. from __future__ import annotations
  2. import re
  3. from typing import Any, Iterable, Mapping
  4. from .business_detail_text import business_text
  5. _SUCCESS_STATUSES = {"ok", "success", "succeeded", "completed", "complete"}
  6. _DECISION_BASIS_TITLE = "根据以下信息做出的决策"
  7. _RELATIONS = {
  8. "direct-read": "read-by-actor",
  9. "read-by-actor": "read-by-actor",
  10. "returned-report": "returned-to-actor",
  11. "returned-to-actor": "returned-to-actor",
  12. "persisted-record": "persisted-before-decision",
  13. "persisted-before-decision": "persisted-before-decision",
  14. "standing-constraint": "standing-constraint",
  15. }
  16. class AgentDecisionProjector:
  17. """Create the V8 decision union from already-associated, plain records.
  18. The projector deliberately does not scan event timelines or infer causal
  19. links. Callers must first establish ownership/association, then pass those
  20. facts here. In particular an observed read remains a read unless its input
  21. explicitly records ``decisionUse=explicit-basis``.
  22. """
  23. def direction(self, payload: Mapping[str, Any]) -> dict[str, Any]:
  24. subtype = _machine(payload.get("subtype")) or "direction"
  25. actor = _actor(payload.get("actor"), default_role="main", default_label="主 Agent")
  26. decision_items = _strings(
  27. payload.get("decisionItems") or payload.get("decisions")
  28. )
  29. reasoning = _text(payload.get("reasoning") or payload.get("explicitReasoning"))
  30. constraints = _strings(payload.get("constraints"))
  31. inputs = _inputs(payload.get("inputs"))
  32. primary = decision_items[0] if decision_items else _text(payload.get("decision"))
  33. secondary = []
  34. if inputs:
  35. secondary.append(_card_line("inputs", _DECISION_BASIS_TITLE, _input_labels(inputs)))
  36. if constraints:
  37. secondary.append(_card_line("constraints", "关键约束", _join(constraints, 2)))
  38. if reasoning:
  39. secondary.append(_card_line("reasoning", "决策理由", reasoning))
  40. body = {
  41. "type": "direction",
  42. "decisionItems": decision_items,
  43. "reasoning": reasoning,
  44. "constraints": constraints,
  45. }
  46. if subtype == "implementation-plan":
  47. implementation_plan = _implementation_plan(payload.get("implementationPlan"), reasoning)
  48. body["implementationPlan"] = implementation_plan
  49. blocks = [
  50. _implementation_plan_block(implementation_plan),
  51. _items_block(_DECISION_BASIS_TITLE, [_input_business_item(item) for item in inputs], visual_role="evidence"),
  52. _items_block("关键约束", constraints),
  53. ]
  54. elif subtype == "objective" and _source_text(payload.get("sourceDocument")):
  55. source_document = _source_text(payload.get("sourceDocument"))
  56. body["sourceDocument"] = source_document
  57. blocks = [
  58. _source_document_block(
  59. "当前保存的创作方向",
  60. source_document,
  61. ),
  62. _items_block(
  63. _DECISION_BASIS_TITLE,
  64. [_input_business_item(item) for item in inputs],
  65. visual_role="evidence",
  66. ),
  67. ]
  68. elif subtype == "round-goal" and _source_text(payload.get("sourceDocument")):
  69. source_document = _source_text(payload.get("sourceDocument"))
  70. body["sourceDocument"] = source_document
  71. blocks = [
  72. _source_document_block(
  73. "本轮目标构成",
  74. source_document,
  75. ),
  76. _items_block(
  77. _DECISION_BASIS_TITLE,
  78. [_input_business_item(item) for item in inputs],
  79. visual_role="evidence",
  80. ),
  81. _summary_block("目标来源", reasoning, visual_role="reason"),
  82. ]
  83. else:
  84. blocks = [
  85. _decision_items_block("最终方向", decision_items or ([primary] if primary else []), visual_role="key-result"),
  86. _items_block(_DECISION_BASIS_TITLE, [_input_business_item(item) for item in inputs], visual_role="evidence"),
  87. _items_block("关键约束", constraints),
  88. _summary_block("明确理由", reasoning, visual_role="reason"),
  89. ]
  90. return _decision(
  91. payload,
  92. decision_type="direction",
  93. subtype=subtype,
  94. actor=actor,
  95. authority="final",
  96. question=_text(payload.get("question")) or _direction_question(subtype),
  97. inputs=inputs,
  98. body=body,
  99. primary_label=_text(payload.get("primaryLabel")) or "最终方向",
  100. primary=primary,
  101. secondary=secondary,
  102. blocks=blocks,
  103. )
  104. def data_tradeoff(self, row: Mapping[str, Any]) -> dict[str, Any]:
  105. inputs = _inputs(row.get("inputs") or _source_inputs(row.get("sources")))
  106. decision_text = _text(row.get("decision"))
  107. reasoning = _text(row.get("reasoning"))
  108. scope_items = _strings(row.get("scopeItems"))
  109. body = {
  110. "type": "tradeoff",
  111. "scopeItems": scope_items,
  112. "decision": decision_text,
  113. "selected": _strings(row.get("selected")),
  114. "combined": _strings(row.get("combined")),
  115. "parked": _strings(row.get("parked")),
  116. "rejected": _strings(row.get("rejected")),
  117. "reasoning": reasoning,
  118. }
  119. secondary = []
  120. if inputs:
  121. secondary.append(_card_line("scope", "参与判断", f"{len(inputs)} 类信息"))
  122. if reasoning:
  123. secondary.append(_card_line("reasoning", "理由", reasoning))
  124. return _decision(
  125. row,
  126. decision_type="tradeoff",
  127. subtype="data-tradeoff",
  128. actor=_actor(
  129. row.get("actor"),
  130. default_role="implementer",
  131. default_label="实现 Agent",
  132. ),
  133. authority="implementation-scope",
  134. question=_text(row.get("question")) or "哪些信息用于当前实现方案?",
  135. inputs=inputs,
  136. body=body,
  137. primary_label="取舍结论",
  138. primary=decision_text,
  139. secondary=secondary,
  140. blocks=[
  141. _items_block("取舍对象", scope_items),
  142. _items_block("参与判断", [_input_business_item(item) for item in inputs], visual_role="evidence", collapsible=True),
  143. _summary_block("明确取舍", decision_text, visual_role="key-result"),
  144. _summary_block("理由", reasoning, visual_role="reason"),
  145. ],
  146. )
  147. def multipath_tradeoff(self, row: Mapping[str, Any]) -> dict[str, Any]:
  148. branch_ids = _unique_values(row.get("branchIds") or row.get("branch_ids"))
  149. inputs = _inputs(row.get("inputs"))
  150. decision_text = _text(row.get("decision"))
  151. reasoning = _text(row.get("reasoning"))
  152. selected = _strings(row.get("selected"))
  153. rejected = _strings(row.get("rejected"))
  154. body = {
  155. "type": "tradeoff",
  156. "scopeItems": _strings(row.get("scopeItems")),
  157. "decision": decision_text,
  158. "selected": selected,
  159. "combined": _strings(row.get("combined")),
  160. "parked": _strings(row.get("parked")),
  161. "rejected": rejected,
  162. "reasoning": reasoning,
  163. # Identity is useful to flow wiring but is not rendered as business copy.
  164. "branchIds": branch_ids,
  165. "reviewComparison": row.get("reviewComparison"),
  166. }
  167. secondary = []
  168. if branch_ids:
  169. secondary.append(
  170. _card_line("scope", "取舍范围", f"比较 {len(branch_ids)} 个候选方案")
  171. )
  172. if reasoning:
  173. secondary.append(_card_line("reasoning", "决策理由", reasoning))
  174. if row.get("reviewComparison"):
  175. secondary.append(_card_line("review", "评审对照", "可在详情中查看"))
  176. return _decision(
  177. row,
  178. decision_type="tradeoff",
  179. subtype="multipath-tradeoff",
  180. actor=_actor(row.get("actor"), default_role="main", default_label="主 Agent"),
  181. authority="final",
  182. question=_text(row.get("question")) or "多个候选方案最终如何处理?",
  183. inputs=inputs,
  184. body=body,
  185. primary_label="最终决定",
  186. primary=decision_text,
  187. secondary=secondary,
  188. blocks=[
  189. _summary_block(
  190. "取舍对象",
  191. f"{len(branch_ids)} 个候选方案" if branch_ids else None,
  192. ),
  193. _items_block("参与判断", [_input_business_item(item) for item in inputs], visual_role="evidence", collapsible=True),
  194. _summary_block("最终决定", decision_text, visual_role="key-result"),
  195. _summary_block("决策理由", reasoning, visual_role="reason"),
  196. _comparison_block(row.get("reviewComparison")),
  197. ],
  198. )
  199. def evaluation(self, payload: Mapping[str, Any]) -> dict[str, Any]:
  200. subtype = _machine(payload.get("subtype")) or "overall-evaluation"
  201. is_multipath = subtype == "multipath-evaluation"
  202. inputs = _inputs(payload.get("inputs"))
  203. conclusion = _text(payload.get("conclusion"))
  204. recommendation = _text(payload.get("recommendation"))
  205. subjects = _strings(payload.get("subjects"))
  206. criteria = _strings(payload.get("criteria"))
  207. item_conclusions = _business_dicts(payload.get("itemConclusions"))
  208. achievements = _strings(payload.get("achievements"))
  209. problems = _business_dicts(payload.get("problems"))
  210. branch_evaluations = _mapping_list(payload.get("branchEvaluations"))
  211. comparison_rows = _mapping_list(payload.get("comparisonRows"))
  212. body = {
  213. "type": "evaluation",
  214. "subjects": subjects,
  215. "criteria": criteria,
  216. "itemConclusions": item_conclusions,
  217. "achievements": achievements,
  218. "problems": problems,
  219. "branchEvaluations": branch_evaluations,
  220. "comparisonRows": comparison_rows,
  221. "conclusion": conclusion,
  222. "recommendation": recommendation,
  223. "nextGoal": _text(payload.get("nextGoal")),
  224. }
  225. secondary = []
  226. if achievements:
  227. secondary.append(_card_line("achievement", "已达成", _join(achievements, 2)))
  228. if problems:
  229. secondary.append(
  230. _card_line("problems", "需要关注", _problem_summary(problems), "warning")
  231. )
  232. if recommendation:
  233. secondary.append(_card_line("recommendation", "评审建议", recommendation))
  234. primary = conclusion or _item_conclusion_summary(item_conclusions) or recommendation
  235. if is_multipath and branch_evaluations:
  236. detail_blocks = [
  237. _evaluation_branches_block(branch_evaluations),
  238. _evaluation_matrix_block(comparison_rows) or _items_block("评审标准", criteria),
  239. _summary_block("评审建议", recommendation),
  240. ]
  241. else:
  242. detail_blocks = [
  243. _items_block("评审对象", subjects),
  244. _items_block("评审标准", criteria),
  245. _items_block("逐项结论", item_conclusions),
  246. _items_block("已达成", achievements),
  247. _items_block("问题", problems),
  248. _summary_block("总结论", conclusion, visual_role="key-result"),
  249. _summary_block("评审建议", recommendation),
  250. ]
  251. return _decision(
  252. payload,
  253. decision_type="evaluation",
  254. subtype=subtype,
  255. actor=_actor(
  256. payload.get("actor"),
  257. default_role=("multipath-evaluator" if is_multipath else "overall-evaluator"),
  258. default_label=("多方案评审 Agent" if is_multipath else "整体评审 Agent"),
  259. ),
  260. authority="recommendation",
  261. question=_text(payload.get("question"))
  262. or ("哪些候选方案更值得采用?" if is_multipath else "当前主脚本是否达到目标?"),
  263. inputs=inputs,
  264. body=body,
  265. primary_label="评审结论",
  266. primary=primary,
  267. secondary=secondary,
  268. blocks=detail_blocks,
  269. )
  270. def creative(self, payload: Mapping[str, Any]) -> dict[str, Any] | None:
  271. """Project an implementer creative decision only from a safe event link.
  272. An artifact reference may be attached to a real creative record, but it
  273. is never sufficient to create that record. This avoids inferring the
  274. implementer's plan from the eventual candidate script.
  275. """
  276. if payload.get("safeLink") is not True:
  277. return None
  278. event = payload.get("event")
  279. event = event if isinstance(event, Mapping) else payload
  280. if not _is_successful_think_and_plan(event):
  281. return None
  282. raw_input = event.get("inputData") or event.get("input")
  283. if not isinstance(raw_input, Mapping):
  284. return None
  285. thought = _text(
  286. payload.get("reasoning")
  287. or raw_input.get("thought")
  288. or raw_input.get("thought_summary")
  289. )
  290. steps = _creative_steps(payload.get("steps"))
  291. actions = _strings(payload.get("actions"))
  292. if not steps:
  293. fallback_action = _text(raw_input.get("action"))
  294. fallback_plan = _source_text(raw_input.get("plan"))
  295. if fallback_action or fallback_plan or thought:
  296. steps = [{
  297. "stepIndex": 1,
  298. "action": fallback_action or "创作处理",
  299. "summary": _text(raw_input.get("thought_summary")),
  300. "plan": fallback_plan,
  301. "reasoning": _source_text(raw_input.get("thought")),
  302. }]
  303. if not actions:
  304. actions = [str(step["action"]) for step in steps if step.get("action")]
  305. if not any((thought, actions, steps)):
  306. return None
  307. task = _text(payload.get("task"))
  308. explicit_basis = _strings(payload.get("basis"))
  309. uncertainties = _strings(payload.get("uncertainties"))
  310. body = {
  311. "type": "creative",
  312. "task": task,
  313. "basis": explicit_basis,
  314. "actions": actions,
  315. "steps": steps,
  316. "reasoning": thought,
  317. "output": _text(payload.get("output")),
  318. "uncertainties": uncertainties,
  319. }
  320. secondary = []
  321. projected_payload = dict(payload)
  322. projected_payload.setdefault("id", f"creative-event:{event.get('id')}")
  323. return _decision(
  324. projected_payload,
  325. decision_type="creative",
  326. subtype="creative-handling",
  327. actor=_actor(
  328. payload.get("actor"),
  329. default_role="implementer",
  330. default_label="实现 Agent",
  331. ),
  332. authority="implementation-scope",
  333. question=_text(payload.get("question")) or "当前实现方案如何形成候选产出?",
  334. inputs=_inputs(payload.get("inputs")),
  335. body=body,
  336. primary_label="创作处理",
  337. primary=actions[0] if actions else thought,
  338. secondary=secondary,
  339. blocks=[
  340. _items_block("明确依据", explicit_basis, visual_role="evidence", collapsible=True),
  341. _creative_process_block(steps) or _items_block("创作处理", actions),
  342. _summary_block("产出的候选表", body["output"], visual_role="key-result"),
  343. _items_block("存疑项", uncertainties, visual_role="notice"),
  344. ],
  345. )
  346. def _decision(
  347. payload: Mapping[str, Any],
  348. *,
  349. decision_type: str,
  350. subtype: str,
  351. actor: dict[str, str],
  352. authority: str,
  353. question: str,
  354. inputs: list[dict[str, Any]],
  355. body: dict[str, Any],
  356. primary_label: str,
  357. primary: str | None,
  358. secondary: list[dict[str, Any]],
  359. blocks: list[dict[str, Any] | None],
  360. ) -> dict[str, Any]:
  361. completeness = _completeness(payload.get("completeness"), primary)
  362. notices = _notices(payload.get("notices"))
  363. if not primary and not any(item.get("code") == "record-missing" for item in notices):
  364. notices.append({"code": "record-missing", "message": "没有找到结构化决策结论。"})
  365. decision_id = _machine(payload.get("id") or payload.get("detailRef")) or f"{subtype}:unknown"
  366. detail_ref = _machine(payload.get("detailRef")) or decision_id
  367. result = {
  368. "id": decision_id,
  369. "semanticKind": "decision",
  370. "decisionType": decision_type,
  371. "subtype": subtype,
  372. "actor": actor,
  373. "authority": authority,
  374. "question": question,
  375. "inputs": inputs,
  376. "body": body,
  377. "card": {
  378. "primary": {
  379. "label": primary_label,
  380. "value": primary or "决策结论未记录",
  381. },
  382. "secondary": [item for item in secondary if item.get("value")][:3],
  383. },
  384. "detailRef": detail_ref,
  385. "promptRef": _machine(payload.get("promptRef")),
  386. "artifactRef": _machine(payload.get("artifactRef")),
  387. "completeness": completeness,
  388. "notices": notices,
  389. "detail": {
  390. "detailKind": "agent-decision",
  391. "decisionType": decision_type,
  392. "actor": actor,
  393. "authority": authority,
  394. "question": question,
  395. "blocks": [block for block in blocks if block],
  396. },
  397. }
  398. return result
  399. def _inputs(value: Any) -> list[dict[str, Any]]:
  400. if not isinstance(value, Iterable) or isinstance(value, (str, bytes, Mapping)):
  401. return []
  402. result: list[dict[str, Any]] = []
  403. for raw in value:
  404. if not isinstance(raw, Mapping):
  405. continue
  406. observed = _RELATIONS.get(
  407. _text(raw.get("observedRelation") or raw.get("relation")) or "",
  408. "persisted-before-decision",
  409. )
  410. explicit_use = _machine(raw.get("decisionUse"))
  411. decision_use = explicit_use if explicit_use in {"explicit-basis", "not-recorded"} else "not-recorded"
  412. if not (raw.get("observedRelation") or raw.get("relation")):
  413. continue
  414. label = _text(raw.get("label") or raw.get("name") or raw.get("type"))
  415. if not label:
  416. continue
  417. result.append(
  418. {
  419. "label": label,
  420. "summary": _text(raw.get("summary")),
  421. "observedRelation": observed,
  422. "decisionUse": decision_use,
  423. "detailRef": _text(raw.get("detailRef")),
  424. }
  425. )
  426. return result
  427. def _source_inputs(value: Any) -> list[dict[str, Any]]:
  428. if not isinstance(value, list):
  429. return []
  430. result = []
  431. for index, item in enumerate(value, 1):
  432. if isinstance(item, Mapping):
  433. label = _text(
  434. item.get("label")
  435. or item.get("data_type")
  436. or item.get("source_type")
  437. or item.get("type")
  438. )
  439. summary = _text(
  440. item.get("summary")
  441. or item.get("data_content")
  442. or item.get("title")
  443. or item.get("name")
  444. )
  445. explicit_use = item.get("decisionUse")
  446. else:
  447. label, summary, explicit_use = _text(item), None, None
  448. result.append(
  449. {
  450. "label": label or f"信息来源 {index}",
  451. "summary": summary,
  452. "observedRelation": "persisted-before-decision",
  453. "decisionUse": explicit_use or "not-recorded",
  454. }
  455. )
  456. return result
  457. def _actor(value: Any, *, default_role: str, default_label: str) -> dict[str, str]:
  458. if isinstance(value, Mapping):
  459. return {
  460. "role": _machine(value.get("role")) or default_role,
  461. "label": _text(value.get("label")) or default_label,
  462. }
  463. return {"role": default_role, "label": default_label}
  464. def _summary_block(
  465. title: str,
  466. value: Any,
  467. *,
  468. visual_role: str = "section",
  469. presentation: str = "prose",
  470. ) -> dict[str, Any] | None:
  471. text = _text(value)
  472. return {
  473. "type": "summary",
  474. "title": title,
  475. "value": text,
  476. "visualRole": visual_role,
  477. "presentation": presentation,
  478. } if text else None
  479. def _source_document_block(title: str, value: Any) -> dict[str, Any] | None:
  480. text = _source_text(value)
  481. return {
  482. "type": "summary",
  483. "title": title,
  484. "value": text,
  485. "visualRole": "key-result",
  486. "presentation": "document",
  487. } if text else None
  488. def _items_block(
  489. title: str,
  490. items: Any,
  491. *,
  492. visual_role: str = "section",
  493. presentation: str = "list",
  494. collapsible: bool = False,
  495. default_open: bool = False,
  496. ) -> dict[str, Any] | None:
  497. if not isinstance(items, list) or not items:
  498. return None
  499. return {
  500. "type": "items",
  501. "title": title,
  502. "items": items,
  503. "visualRole": visual_role,
  504. "presentation": presentation,
  505. "collapsible": collapsible,
  506. "defaultOpen": default_open,
  507. }
  508. def _creative_process_block(steps: list[dict[str, Any]]) -> dict[str, Any] | None:
  509. if not steps:
  510. return None
  511. return {
  512. "type": "creative-process",
  513. "title": "创作处理",
  514. "steps": steps,
  515. "visualRole": "section",
  516. "presentation": "process",
  517. }
  518. def _evaluation_branches_block(branches: list[dict[str, Any]]) -> dict[str, Any] | None:
  519. if not branches:
  520. return None
  521. return {
  522. "type": "evaluation-branches",
  523. "title": "逐方案评审",
  524. "branches": branches,
  525. "visualRole": "section",
  526. "presentation": "grouped-review",
  527. }
  528. def _evaluation_matrix_block(rows: list[dict[str, Any]]) -> dict[str, Any] | None:
  529. if not rows:
  530. return None
  531. return {
  532. "type": "evaluation-matrix",
  533. "title": "评审标准与方案对比",
  534. "rows": rows,
  535. "visualRole": "section",
  536. "presentation": "matrix",
  537. }
  538. def _decision_items_block(
  539. title: str,
  540. items: list[str],
  541. *,
  542. visual_role: str = "section",
  543. ) -> dict[str, Any] | None:
  544. if not items:
  545. return None
  546. if len(items) == 1:
  547. return _summary_block(title, items[0], visual_role=visual_role)
  548. return _items_block(title, items, visual_role=visual_role)
  549. def _implementation_plan(value: Any, reasoning: str | None) -> dict[str, Any]:
  550. plan = value if isinstance(value, Mapping) else {}
  551. routes = plan.get("routes") if isinstance(plan.get("routes"), list) else []
  552. return {
  553. "mode": _text(plan.get("mode")) or _text(plan.get("rawMode")),
  554. "reasoning": _text(plan.get("reasoning")) or reasoning,
  555. "routes": [
  556. {
  557. "pathIndex": route.get("pathIndex") or index,
  558. "pathType": _business_text(route.get("pathType")),
  559. "action": _business_text(route.get("action")),
  560. "target": _business_text(route.get("target")),
  561. "method": _business_text(route.get("method")),
  562. "emphasis": _business_text(route.get("emphasis")),
  563. }
  564. for index, route in enumerate(routes, 1)
  565. if isinstance(route, Mapping)
  566. ],
  567. }
  568. def _implementation_plan_block(plan: Mapping[str, Any]) -> dict[str, Any] | None:
  569. routes = plan.get("routes") if isinstance(plan.get("routes"), list) else []
  570. if not routes and not plan.get("reasoning"):
  571. return None
  572. return {
  573. "type": "implementation-plan",
  574. "title": "本轮实施路线",
  575. "visualRole": "section",
  576. "presentation": "plan",
  577. "mode": plan.get("mode"),
  578. "routes": routes,
  579. "reasoning": plan.get("reasoning"),
  580. }
  581. def _item_conclusion_summary(items: list[dict[str, Any]]) -> str | None:
  582. summaries = []
  583. for item in items[:3]:
  584. subject = _text(item.get("subject") or item.get("label"))
  585. conclusion = _text(item.get("conclusion") or item.get("value"))
  586. if subject and conclusion:
  587. summaries.append(f"{subject}:{conclusion}")
  588. if not summaries:
  589. return None
  590. suffix = f";另有 {len(items) - 3} 项" if len(items) > 3 else ""
  591. return ";".join(summaries) + suffix
  592. def _comparison_block(value: Any) -> dict[str, Any] | None:
  593. if not isinstance(value, list) or not value:
  594. return None
  595. return {
  596. "type": "comparison",
  597. "title": "评审建议与最终决定",
  598. "visualRole": "section",
  599. "presentation": "comparison",
  600. "rows": _business_value(value),
  601. }
  602. def _input_business_item(item: Mapping[str, Any]) -> dict[str, str]:
  603. result = {"label": str(item.get("label") or "信息")}
  604. summary = _business_text(item.get("summary"))
  605. if summary:
  606. result["value"] = summary
  607. relation = {
  608. "read-by-actor": "决策前直接读取",
  609. "returned-to-actor": "评审返回给决策者",
  610. "persisted-before-decision": "此前已保存",
  611. "standing-constraint": "持续约束",
  612. }.get(str(item.get("observedRelation") or ""))
  613. use = (
  614. "本轮必须遵守"
  615. if item.get("observedRelation") == "standing-constraint"
  616. else "明确作为决策依据"
  617. if item.get("decisionUse") == "explicit-basis"
  618. else "是否采用未记录"
  619. )
  620. result["note"] = ";".join(part for part in (relation, use) if part)
  621. return result
  622. def _business_text(value: Any) -> str | None:
  623. text = business_text(value)
  624. return text or None
  625. def _card_line(key: str, label: str, value: Any, tone: str | None = None) -> dict[str, Any]:
  626. line = {"key": key, "label": label, "value": _preview(value, 160)}
  627. if tone:
  628. line["tone"] = tone
  629. return line
  630. def _notices(value: Any) -> list[dict[str, str]]:
  631. if not isinstance(value, list):
  632. return []
  633. result = []
  634. for item in value:
  635. if isinstance(item, Mapping):
  636. code = _machine(item.get("code"))
  637. message = _text(item.get("message"))
  638. else:
  639. code, message = "notice", _text(item)
  640. if code and message:
  641. result.append({"code": code, "message": message})
  642. return result
  643. def _business_dicts(value: Any) -> list[Any]:
  644. if not isinstance(value, list):
  645. return []
  646. result: list[Any] = []
  647. for item in value:
  648. if isinstance(item, Mapping):
  649. cleaned = {
  650. key: _business_value(item[key])
  651. for key in (
  652. "label",
  653. "subject",
  654. "conclusion",
  655. "summary",
  656. "severity",
  657. "recommendation",
  658. "achievements",
  659. "problems",
  660. )
  661. if item.get(key) not in (None, "")
  662. }
  663. if cleaned:
  664. result.append(cleaned)
  665. elif text := _text(item):
  666. result.append(text)
  667. return result
  668. def _mapping_list(value: Any) -> list[dict[str, Any]]:
  669. if not isinstance(value, list):
  670. return []
  671. return [
  672. _business_value(dict(item))
  673. for item in value
  674. if isinstance(item, Mapping)
  675. ]
  676. def _creative_steps(value: Any) -> list[dict[str, Any]]:
  677. if not isinstance(value, list):
  678. return []
  679. result: list[dict[str, Any]] = []
  680. for fallback_index, item in enumerate(value, start=1):
  681. if not isinstance(item, Mapping):
  682. continue
  683. action = _text(item.get("action"))
  684. plan = _source_text(item.get("plan"))
  685. summary = _text(item.get("summary"))
  686. reasoning = _source_text(item.get("reasoning"))
  687. if not any((action, plan, summary, reasoning)):
  688. continue
  689. result.append(
  690. {
  691. "stepIndex": int(item.get("stepIndex") or fallback_index),
  692. "action": action or f"步骤 {fallback_index}",
  693. "summary": summary,
  694. "plan": plan,
  695. "reasoning": reasoning,
  696. "eventRef": _text(item.get("eventRef")),
  697. }
  698. )
  699. return result
  700. def _problem_summary(problems: list[Any]) -> str:
  701. values = []
  702. for item in problems[:2]:
  703. if isinstance(item, Mapping):
  704. values.append(_text(item.get("summary") or item.get("label")) or "待处理问题")
  705. else:
  706. values.append(str(item))
  707. return _join(values, 2)
  708. def _input_labels(inputs: list[dict[str, Any]]) -> str:
  709. return _join([str(item["label"]) for item in inputs], 3)
  710. def _direction_question(subtype: str) -> str:
  711. return {
  712. "objective": "这次构建要形成什么创作方向?",
  713. "round-goal": "本轮优先解决什么问题?",
  714. "implementation-plan": "本轮应如何组织实现方案?",
  715. }.get(subtype, "这一步确定了什么方向?")
  716. def _is_successful_think_and_plan(event: Mapping[str, Any]) -> bool:
  717. return (
  718. str(event.get("event_type") or "") == "tool_call"
  719. and str(event.get("event_name") or "") == "think_and_plan"
  720. and str(event.get("agent_role") or "")
  721. in {"script_implementer", "implementer"}
  722. and str(event.get("status") or "").lower() in _SUCCESS_STATUSES
  723. )
  724. def _completeness(value: Any, primary: str | None) -> str:
  725. explicit = _machine(value)
  726. if explicit in {"complete", "partial", "missing"}:
  727. return explicit
  728. return "complete" if primary else "missing"
  729. def _strings(value: Any) -> list[str]:
  730. if not isinstance(value, list):
  731. return []
  732. return [text for item in value if (text := _text(item))]
  733. def _unique_values(value: Any) -> list[Any]:
  734. if not isinstance(value, list):
  735. return []
  736. result = []
  737. for item in value:
  738. if item not in result:
  739. result.append(item)
  740. return result
  741. def _join(values: list[str], limit: int) -> str:
  742. kept = [value for value in values if value][:limit]
  743. suffix = f" · 另有 {len(values) - limit} 项" if len(values) > limit else ""
  744. return " · ".join(kept) + suffix
  745. def _preview(value: Any, limit: int) -> str:
  746. text = _text(value) or ""
  747. if len(text) <= limit:
  748. return text
  749. cut = text[:limit]
  750. boundary = max(cut.rfind(mark) for mark in "。!?;")
  751. if boundary >= max(24, limit // 2):
  752. return cut[: boundary + 1]
  753. return cut.rstrip() + "…"
  754. def _text(value: Any) -> str | None:
  755. if value is None:
  756. return None
  757. if isinstance(value, (dict, list, tuple, set)):
  758. return None
  759. text = str(value).replace("\\n", "\n")
  760. text = re.sub(r"[`*_#]+", "", text)
  761. text = re.sub(r"\s+", " ", text).strip(" ::")
  762. return _business_copy(text) or None
  763. def _source_text(value: Any) -> str | None:
  764. """Keep authoritative saved Markdown intact for source-document rendering."""
  765. if value is None or isinstance(value, (dict, list, tuple, set)):
  766. return None
  767. text = str(value).replace("\\n", "\n").strip()
  768. return text or None
  769. def _business_value(value: Any) -> Any:
  770. if isinstance(value, str):
  771. return _business_copy(value)
  772. if isinstance(value, list):
  773. return [_business_value(item) for item in value]
  774. if isinstance(value, Mapping):
  775. return {key: _business_value(item) for key, item in value.items()}
  776. return value
  777. def _business_copy(text: str) -> str:
  778. """Translate machine-facing tokens without deleting ordinary numbers."""
  779. replacements = (
  780. (r"\bretrieve_data_decode_case\b|\bretrievedatadecodecase\b", "解构案例取数"),
  781. (r"\bretrieve_data_knowledge\b|\bretrievedataknowledge\b", "知识取数"),
  782. (r"\bget_account_script_persona_points\b|\bgetaccountscriptpersonapoints\b", "账号人设"),
  783. (r"\bget_account_script_section_patterns\b", "账号段落模式"),
  784. (r"\bget_topic_detail\b", "选题内容"),
  785. (r"\bget_script_snapshot\b", "当前主脚本"),
  786. (r"\brecord_data_decision\b", "记录数据取舍"),
  787. (r"\bthink_and_plan\b", "实现思考与计划"),
  788. )
  789. for pattern, label in replacements:
  790. text = re.sub(pattern, label, text, flags=re.IGNORECASE)
  791. text = re.sub(
  792. r"\bpost_?id\s*[:=]?(?:\s*[A-Za-z0-9-]+)?",
  793. "账号内容样本",
  794. text,
  795. flags=re.IGNORECASE,
  796. )
  797. text = re.sub(r"\bscript_build_id\s*[:=]\s*\d+\b", "", text, flags=re.IGNORECASE)
  798. text = re.sub(r"\bround_index\s*[:=]\s*(\d+)\b", r"第 \1 轮", text, flags=re.IGNORECASE)
  799. text = re.sub(r"\bbranch[_\s-]*(\d+)\b", r"方案 \1", text, flags=re.IGNORECASE)
  800. text = re.sub(r"\bP(\d+)\b", r"段落 \1", text)
  801. text = re.sub(r"\bD(\d+)\b", r"评估维度 \1", text)
  802. text = re.sub(r"\bbase\b", "主脚本", text, flags=re.IGNORECASE)
  803. return re.sub(r"\s+", " ", text).strip(" ::")
  804. def _machine(value: Any) -> str | None:
  805. if value is None or isinstance(value, (dict, list, tuple, set)):
  806. return None
  807. text = str(value).strip()
  808. return text or None