test_decision_projection_v8.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. from __future__ import annotations
  2. from app.decision_projection import AgentDecisionProjector
  3. def test_direction_keeps_observed_reads_separate_from_explicit_basis():
  4. decision = AgentDecisionProjector().direction(
  5. {
  6. "id": "run:objective",
  7. "subtype": "objective",
  8. "decisionItems": ["讲清柔性排产为什么能减少浪费"],
  9. "constraints": ["数据可信", "结构清楚"],
  10. "inputs": [
  11. {"label": "选题", "relation": "direct-read"},
  12. {
  13. "label": "账号段落模式",
  14. "relation": "direct-read",
  15. "decisionUse": "explicit-basis",
  16. },
  17. ],
  18. "promptRef": "main:direction",
  19. }
  20. )
  21. assert decision["semanticKind"] == "decision"
  22. assert decision["decisionType"] == "direction"
  23. assert decision["authority"] == "final"
  24. assert decision["inputs"][0] == {
  25. "label": "选题",
  26. "summary": None,
  27. "observedRelation": "read-by-actor",
  28. "decisionUse": "not-recorded",
  29. "detailRef": None,
  30. }
  31. assert decision["inputs"][1]["decisionUse"] == "explicit-basis"
  32. input_block = next(
  33. block for block in decision["detail"]["blocks"] if block["title"] == "根据以下信息做出的决策"
  34. )
  35. assert input_block["items"][0]["note"] == "决策前直接读取;是否采用未记录"
  36. assert input_block["items"][1]["note"] == "决策前直接读取;明确作为决策依据"
  37. assert input_block["visualRole"] == "evidence"
  38. assert input_block["presentation"] == "list"
  39. assert input_block["collapsible"] is False
  40. final_block = next(
  41. block for block in decision["detail"]["blocks"] if block["title"] == "最终方向"
  42. )
  43. assert final_block["visualRole"] == "key-result"
  44. assert len(decision["card"]["secondary"]) <= 3
  45. assert decision["detail"]["detailKind"] == "agent-decision"
  46. def test_implementation_plan_uses_dedicated_routes_and_full_evidence():
  47. full_goal = "修复结构完整性。" * 40
  48. decision = AgentDecisionProjector().direction(
  49. {
  50. "id": "round:4:plan",
  51. "subtype": "implementation-plan",
  52. "decisionItems": ["方案 1 · 改:段落 2207、2208"],
  53. "explicitReasoning": "这是收敛阶段的定点修复,不需要竞争。",
  54. "inputs": [
  55. {"label": "本轮目标", "summary": full_goal, "relation": "standing-constraint"},
  56. ],
  57. "implementationPlan": {
  58. "mode": "单路",
  59. "routes": [{
  60. "pathIndex": 1,
  61. "pathType": "内容",
  62. "action": "改",
  63. "target": "段落 2207、2208 的 form_elements",
  64. "method": "产生元素",
  65. "emphasis": "结构完整性",
  66. }],
  67. },
  68. }
  69. )
  70. route_block = decision["detail"]["blocks"][0]
  71. evidence_block = decision["detail"]["blocks"][1]
  72. assert route_block["type"] == "implementation-plan"
  73. assert route_block["routes"][0]["target"] == "段落 2207、2208 的 形式信息"
  74. assert route_block["routes"][0]["emphasis"] == "结构完整性"
  75. assert evidence_block["title"] == "根据以下信息做出的决策"
  76. assert evidence_block["visualRole"] == "evidence"
  77. assert evidence_block["collapsible"] is False
  78. assert evidence_block["items"][0]["value"] == full_goal
  79. assert evidence_block["items"][0]["note"] == "持续约束;本轮必须遵守"
  80. def test_data_tradeoff_is_implementer_scope_and_sources_are_not_auto_adopted():
  81. decision = AgentDecisionProjector().data_tradeoff(
  82. {
  83. "id": "data-decision:12",
  84. "decision": "采用两条交叉印证的数据,暂不采用单一来源估算。",
  85. "reasoning": "两类来源结论一致。",
  86. "sources": [
  87. {"type": "行业报告", "title": "报告 A"},
  88. {"type": "搜索结果", "title": "结果 B"},
  89. ],
  90. }
  91. )
  92. assert decision["decisionType"] == "tradeoff"
  93. assert decision["subtype"] == "data-tradeoff"
  94. assert decision["actor"]["role"] == "implementer"
  95. assert decision["authority"] == "implementation-scope"
  96. assert {item["decisionUse"] for item in decision["inputs"]} == {"not-recorded"}
  97. assert decision["body"]["selected"] == []
  98. assert len(decision["card"]["secondary"]) == 2
  99. blocks = {block["title"]: block for block in decision["detail"]["blocks"]}
  100. assert blocks["参与判断"]["visualRole"] == "evidence"
  101. assert blocks["明确取舍"]["visualRole"] == "key-result"
  102. assert blocks["理由"]["visualRole"] == "reason"
  103. def test_data_tradeoff_uses_real_source_type_and_content_fields():
  104. decision = AgentDecisionProjector().data_tradeoff(
  105. {
  106. "id": "data-decision:13",
  107. "decision": "组合领域信息与账号模式。",
  108. "sources": [
  109. {
  110. "data_type": "领域信息",
  111. "data_content": "米豆腐热量约 50-60kcal/100g",
  112. }
  113. ],
  114. }
  115. )
  116. source = decision["detail"]["blocks"][0]["items"][0]
  117. assert source["label"] == "领域信息"
  118. assert source["value"] == "米豆腐热量约 50-60kcal/100g"
  119. def test_multipath_tradeoff_is_main_final_and_does_not_render_internal_ids():
  120. decision = AgentDecisionProjector().multipath_tradeoff(
  121. {
  122. "id": "multipath-decision:21",
  123. "branch_ids": [3, 4],
  124. "decision": "组合两个候选方案后合入主脚本。",
  125. "reasoning": "两路内容互补。",
  126. "reviewComparison": [
  127. {
  128. "recommendation": "建议组合",
  129. "finalDecision": "组合采用",
  130. "handling": None,
  131. }
  132. ],
  133. }
  134. )
  135. assert decision["authority"] == "final"
  136. assert decision["actor"] == {"role": "main", "label": "主 Agent"}
  137. assert decision["body"]["branchIds"] == [3, 4]
  138. business_text = str(decision["card"]) + str(decision["detail"]["blocks"])
  139. assert "branch_id" not in business_text
  140. assert "比较 2 个候选方案" in business_text
  141. def test_evaluator_is_recommendation_with_structured_business_blocks():
  142. decision = AgentDecisionProjector().evaluation(
  143. {
  144. "id": "multipath-review:40",
  145. "subtype": "multipath-evaluation",
  146. "subjects": ["候选方案 A", "候选方案 B"],
  147. "criteria": ["结构完整", "证据可信"],
  148. "itemConclusions": [
  149. {"subject": "候选方案 A", "conclusion": "通过"},
  150. {"subject": "候选方案 B", "conclusion": "部分通过"},
  151. ],
  152. "achievements": ["核心结构已经形成"],
  153. "problems": [{"severity": "一般", "summary": "结尾仍需收束"}],
  154. "conclusion": "部分通过",
  155. "recommendation": "优先采用候选方案 A。",
  156. }
  157. )
  158. assert decision["decisionType"] == "evaluation"
  159. assert decision["authority"] == "recommendation"
  160. assert decision["actor"]["role"] == "multipath-evaluator"
  161. assert [block["title"] for block in decision["detail"]["blocks"]] == [
  162. "评审对象",
  163. "评审标准",
  164. "逐项结论",
  165. "已达成",
  166. "问题",
  167. "总结论",
  168. "评审建议",
  169. ]
  170. conclusion = next(block for block in decision["detail"]["blocks"] if block["title"] == "总结论")
  171. assert conclusion["visualRole"] == "key-result"
  172. def test_creative_requires_safe_think_and_plan_and_never_infers_from_artifact():
  173. projector = AgentDecisionProjector()
  174. event = {
  175. "id": 70,
  176. "event_type": "tool_call",
  177. "event_name": "think_and_plan",
  178. "agent_role": "script_implementer",
  179. "status": "ok",
  180. "inputData": {
  181. "thought": "现有证据不足,抗性淀粉只作为待验证推导。",
  182. "action": "补充烹饪贴士段",
  183. "plan": "先写可确认内容,再标注待核验项。",
  184. },
  185. }
  186. assert projector.creative({"safeLink": False, "event": event}) is None
  187. assert projector.creative({"safeLink": True, "artifactRef": "artifact:9"}) is None
  188. decision = projector.creative(
  189. {
  190. "safeLink": True,
  191. "event": event,
  192. "task": "补齐烹饪贴士",
  193. "output": "形成候选脚本段落",
  194. "uncertainties": ["抗性淀粉结论需要进一步核验"],
  195. "artifactRef": "artifact:9",
  196. "promptRef": "implementer:70",
  197. }
  198. )
  199. assert decision is not None
  200. assert decision["decisionType"] == "creative"
  201. assert decision["authority"] == "implementation-scope"
  202. assert decision["artifactRef"] == "artifact:9"
  203. assert "待验证推导" in decision["body"]["reasoning"]
  204. assert decision["body"]["uncertainties"] == ["抗性淀粉结论需要进一步核验"]
  205. assert all(item["key"] != "task" for item in decision["card"]["secondary"])
  206. assert "创作任务" not in [block["title"] for block in decision["detail"]["blocks"]]
  207. def test_missing_decision_is_explicit_without_fabricated_reasoning():
  208. decision = AgentDecisionProjector().evaluation(
  209. {"id": "overall-review:missing", "subtype": "overall-evaluation"}
  210. )
  211. assert decision["completeness"] == "missing"
  212. assert decision["card"]["primary"]["value"] == "决策结论未记录"
  213. assert decision["body"]["recommendation"] is None
  214. assert decision["notices"] == [
  215. {"code": "record-missing", "message": "没有找到结构化决策结论。"}
  216. ]
  217. def test_business_copy_translates_internal_ids_without_deleting_business_numbers():
  218. projection = AgentDecisionProjector().evaluation(
  219. {
  220. "id": "event:9",
  221. "subtype": "multipath-evaluation",
  222. "achievements": [
  223. "branch3 完成 P2,参考 postid: 67c6c723,保留领域信息 31/32。"
  224. ],
  225. "recommendation": "建议合入 base。",
  226. }
  227. )
  228. business = str(projection["detail"]["blocks"])
  229. assert "方案 3" in business
  230. assert "段落 2" in business
  231. assert "账号内容样本" in business
  232. assert "主脚本" in business
  233. assert "领域信息 31/32" in business
  234. assert "branch3" not in business
  235. assert "postid" not in business