test_flow_ledger_api.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. from fastapi.testclient import TestClient
  2. from content_agent import api
  3. from content_agent.flow_ledger_service import _FLOW_LEDGER_CACHE
  4. from content_agent.run_service import RunService
  5. def test_flow_ledger_api_returns_business_v4_ledger(tmp_path, monkeypatch):
  6. _FLOW_LEDGER_CACHE.clear()
  7. service = RunService(runtime_root=tmp_path / "runtime" / "v1")
  8. run_id = "run_flow_ledger"
  9. policy_run_id = "policy_flow_ledger"
  10. service.runtime.prepare_run(run_id)
  11. service.runtime.write_json(
  12. run_id,
  13. "source_context.json",
  14. {
  15. "run_id": run_id,
  16. "demand_content_id": "demand_123",
  17. "ext_data": {
  18. "evidence_pack": {
  19. "source_post_id": "post_001",
  20. "pattern_execution_id": 581,
  21. "category_bindings": [
  22. {
  23. "category_id": 76006,
  24. "category_path": "/理念/观念/个人观念/人生观",
  25. "category_level": 4,
  26. "category_name": "人生观",
  27. "itemset_item_id": 6055,
  28. }
  29. ],
  30. }
  31. },
  32. },
  33. )
  34. service.runtime.write_json(
  35. run_id,
  36. "pattern_seed_pack.json",
  37. {
  38. "run_id": run_id,
  39. "policy_run_id": policy_run_id,
  40. "pattern_execution_id": 581,
  41. "source_post_id": "post_001",
  42. "element_bindings": [],
  43. },
  44. )
  45. service.runtime.append_jsonl(
  46. run_id,
  47. "search_queries.jsonl",
  48. [
  49. {
  50. "run_id": run_id,
  51. "policy_run_id": policy_run_id,
  52. "search_query_id": "q_001",
  53. "search_query": "睡前拉伸",
  54. "search_query_generation_method": "seed_term",
  55. "pattern_seed_ref": {"query_source_text": "睡前拉伸", "source_post_id": "seed_001"},
  56. "query_source_terms": ["睡前拉伸"],
  57. "raw_payload": {"query_source_refs": [{"query_source_type": "seed_term"}]},
  58. },
  59. {
  60. "run_id": run_id,
  61. "policy_run_id": policy_run_id,
  62. "search_query_id": "q_002",
  63. "search_query": "肩颈放松",
  64. "search_query_generation_method": "tag_query",
  65. "raw_payload": {"parent_search_query_id": "q_001", "hashtag": "肩颈放松"},
  66. },
  67. {
  68. "run_id": run_id,
  69. "policy_run_id": policy_run_id,
  70. "search_query_id": "q_001_page_002",
  71. "search_query": "睡前拉伸",
  72. "search_query_generation_method": "query_next_page",
  73. "raw_payload": {"parent_search_query_id": "q_001"},
  74. },
  75. {
  76. "run_id": run_id,
  77. "policy_run_id": policy_run_id,
  78. "search_query_id": "q_003",
  79. "search_query": "情绪放松练习",
  80. "search_query_generation_method": "piaoquan_topic_point",
  81. "pattern_seed_ref": {"query_source_text": "情绪放松练习", "source_post_id": "post_001"},
  82. "raw_payload": {
  83. "query_source_refs": [
  84. {
  85. "query_source_type": "piaoquan_point_text",
  86. "source_ref": {
  87. "post_id": "post_888",
  88. "point_id": "point_456",
  89. "point_type": "灵感点",
  90. "point_text": "情绪放松练习",
  91. },
  92. }
  93. ]
  94. },
  95. },
  96. {
  97. "run_id": run_id,
  98. "policy_run_id": policy_run_id,
  99. "search_query_id": "q_004",
  100. "search_query": "人生观",
  101. "search_query_generation_method": "category_leaf_element",
  102. "pattern_seed_ref": {
  103. "query_source_text": "人生观",
  104. "source_post_id": "post_001",
  105. "pattern_execution_id": 581,
  106. },
  107. "raw_payload": {
  108. "query_source_refs": [
  109. {
  110. "query_source_type": "category_terminal_element",
  111. "source_ref": {
  112. "category_id": 76006,
  113. "element_name": "人生观",
  114. "itemset_item_id": 6055,
  115. },
  116. }
  117. ]
  118. },
  119. },
  120. ],
  121. )
  122. service.runtime.append_jsonl(
  123. run_id,
  124. "discovered_content_items.jsonl",
  125. [
  126. {
  127. "run_id": run_id,
  128. "policy_run_id": policy_run_id,
  129. "content_discovery_id": "content_001",
  130. "platform_content_id": "douyin_001",
  131. "search_query_id": "q_001",
  132. "platform": "douyin",
  133. "description": "睡前 5 分钟肩颈拉伸",
  134. "author_display_name": "健康教练",
  135. "tags": ["肩颈", "拉伸"],
  136. "statistics": {"digg_count": 100, "comment_count": 8, "share_count": 3},
  137. "raw_payload": {},
  138. },
  139. {
  140. "run_id": run_id,
  141. "policy_run_id": policy_run_id,
  142. "content_discovery_id": "content_002",
  143. "platform_content_id": "douyin_002",
  144. "search_query_id": "q_002",
  145. "previous_discovery_step": "hashtag_to_query",
  146. "platform": "douyin",
  147. "description": "扩展搜到的视频",
  148. "statistics": {},
  149. "raw_payload": {},
  150. },
  151. {
  152. "run_id": run_id,
  153. "policy_run_id": policy_run_id,
  154. "content_discovery_id": "content_003",
  155. "platform_content_id": "douyin_003",
  156. "search_query_id": "q_003",
  157. "platform": "douyin",
  158. "description": "情绪放松练习视频",
  159. "author_display_name": "心理教练",
  160. "statistics": {},
  161. "raw_payload": {},
  162. },
  163. {
  164. "run_id": run_id,
  165. "policy_run_id": policy_run_id,
  166. "content_discovery_id": "content_004",
  167. "platform_content_id": "douyin_004",
  168. "search_query_id": "q_001_page_002",
  169. "previous_discovery_step": "query_next_page",
  170. "platform": "douyin",
  171. "description": "翻页搜到的视频",
  172. "author_display_name": "拉伸助手",
  173. "statistics": {},
  174. "raw_payload": {},
  175. },
  176. ],
  177. )
  178. service.runtime.append_jsonl(
  179. run_id,
  180. "content_media_records.jsonl",
  181. [
  182. {
  183. "run_id": run_id,
  184. "policy_run_id": policy_run_id,
  185. "platform": "douyin",
  186. "platform_content_id": "douyin_001",
  187. "play_url": "https://example.com/raw.mp4",
  188. "oss_url": "https://oss.example.com/video.mp4",
  189. "local_path": None,
  190. "content_media_status": "oss_uploaded",
  191. "raw_payload": {"oss_object_key": "video.mp4"},
  192. }
  193. ],
  194. )
  195. service.runtime.append_jsonl(
  196. run_id,
  197. "pattern_recall_evidence.jsonl",
  198. [
  199. {
  200. "run_id": run_id,
  201. "policy_run_id": policy_run_id,
  202. "recall_evidence_id": "evidence_001",
  203. "platform_content_id": "douyin_001",
  204. "decode_status": "ok",
  205. "raw_payload": {},
  206. }
  207. ],
  208. )
  209. service.runtime.append_jsonl(
  210. run_id,
  211. "rule_decisions.jsonl",
  212. [
  213. {
  214. "run_id": run_id,
  215. "policy_run_id": policy_run_id,
  216. "decision_id": "decision_001",
  217. "search_query_id": "q_001",
  218. "decision_target_id": "content_001",
  219. "decision_action": "ADD_TO_CONTENT_POOL",
  220. "decision_reason_code": "v4_query_and_platform_pass",
  221. "score": 82,
  222. "scorecard": {
  223. "total_score": 82,
  224. "query_relevance_score": 90,
  225. "platform_performance_score": 74,
  226. "platform_performance_components": [
  227. {"field": "statistics.digg_count", "value": 100, "weight": 0.4, "normalized_score": 80},
  228. {"field": "statistics.comment_count", "value": 8, "weight": 0.2, "normalized_score": 70},
  229. ],
  230. },
  231. "decision_replay_data": {
  232. "allow_walk": True,
  233. "walk_gate_snapshot": {
  234. "score": 82,
  235. "query_relevance_score": 90,
  236. "platform_performance_score": 74,
  237. },
  238. },
  239. "raw_payload": {},
  240. },
  241. {
  242. "run_id": run_id,
  243. "policy_run_id": policy_run_id,
  244. "decision_id": "decision_003",
  245. "search_query_id": "q_003",
  246. "decision_target_id": "content_003",
  247. "decision_action": "KEEP_CONTENT_FOR_REVIEW",
  248. "decision_reason_code": "v4_score_review_needed",
  249. "score": 62,
  250. "scorecard": {
  251. "total_score": 62,
  252. "query_relevance_score": 95,
  253. "platform_performance_score": 29,
  254. },
  255. "decision_replay_data": {"allow_walk": False},
  256. "raw_payload": {},
  257. }
  258. ],
  259. )
  260. service.runtime.append_jsonl(
  261. run_id,
  262. "walk_actions.jsonl",
  263. [
  264. {
  265. "run_id": run_id,
  266. "policy_run_id": policy_run_id,
  267. "walk_action_id": "walk_001",
  268. "edge_id": "hashtag_to_query",
  269. "hashtag": "拉伸",
  270. "depth": 1,
  271. "from_node_type": "Content",
  272. "from_node_id": "douyin_001",
  273. "to_node_id": "q_002",
  274. "walk_status": "success",
  275. "reason_code": "v4_query_and_platform_pass",
  276. "walk_gate_snapshot": {
  277. "score": 82,
  278. "query_relevance_score": 90,
  279. "platform_performance_score": 74,
  280. },
  281. "raw_payload": {},
  282. },
  283. {
  284. "run_id": run_id,
  285. "policy_run_id": policy_run_id,
  286. "walk_action_id": "walk_page_001",
  287. "edge_id": "query_next_page",
  288. "depth": 1,
  289. "from_node_type": "SearchQuery",
  290. "from_node_id": "q_001",
  291. "to_node_type": "SearchQuery",
  292. "to_node_id": "q_001_page_002",
  293. "decision_id": "decision_001",
  294. "walk_status": "success",
  295. "walk_action": "fetch_next_page",
  296. "page_cursor": "10",
  297. "reason_code": "v4_query_and_platform_pass",
  298. "walk_gate_snapshot": {
  299. "score": 82,
  300. "query_relevance_score": 90,
  301. "platform_performance_score": 74,
  302. },
  303. "raw_payload": {},
  304. },
  305. ],
  306. )
  307. service.runtime.write_json(
  308. run_id,
  309. "final_output.json",
  310. {
  311. "run_id": run_id,
  312. "policy_run_id": policy_run_id,
  313. "content_assets": [{"decision_id": "decision_001", "platform_content_id": "douyin_001"}],
  314. "review_records": [{"decision_id": "decision_003", "platform_content_id": "douyin_003"}],
  315. "reject_records": [],
  316. "summary": {},
  317. },
  318. )
  319. monkeypatch.setattr(api, "service", service)
  320. client = TestClient(api.app)
  321. ledger = client.get(f"/runs/{run_id}/flow-ledger").json()
  322. assert ledger["data_origin_label"] == "本地调试缓存"
  323. assert len(ledger["rows"]) == 3
  324. assert ledger["summary"]["first_round_query_count"] == 3
  325. assert ledger["summary"]["extension_query_count"] == 2
  326. row = next(item for item in ledger["rows"] if item["id"] == "q_001")
  327. assert row["query"]["method_label"] == "Pattern 种子词"
  328. assert row["source"]["details"] == ["需求单号:demand_123", "种子词:睡前拉伸", "来源帖子:seed_001"]
  329. assert row["first_round_video_total"] == 1
  330. assert row["video_stats"]["oss_uploaded_count"] == 1
  331. assert row["rule_summary"]["pool_count"] == 1
  332. assert row["rule_summary"]["score_summary"]["items"][0]["label"] == "总分"
  333. assert row["rule_summary"]["score_summary"]["items"][0]["score_label"] == "平均 82 分"
  334. assert row["rule_summary"]["score_summary"]["items"][1]["label"] == "是否契合需求"
  335. assert row["rule_summary"]["score_summary"]["items"][1]["score_label"] == "平均 90 分"
  336. assert row["first_round_videos"][0]["decision"]["label"] == "入池"
  337. assert row["first_round_videos"][0]["media_status_label"] == "已保存"
  338. assert row["first_round_videos"][0]["platform_url"] == "https://www.douyin.com/video/douyin_001"
  339. assert row["first_round_videos"][0]["decision"]["score_items"][1]["label"] == "是否契合需求"
  340. assert row["first_round_videos"][0]["decision"]["score_items"][3]["label"] == "点赞表现"
  341. piaoquan = next(item for item in ledger["rows"] if item["id"] == "q_003")
  342. assert piaoquan["source"]["label"] == "票圈帖子具体的点"
  343. assert "帖子 ID:post_888" in piaoquan["source"]["details"]
  344. assert "内容点 ID:point_456" in piaoquan["source"]["details"]
  345. assert piaoquan["asset_summary"]["pool_count"] == 0
  346. assert piaoquan["asset_summary"]["review_count"] == 1
  347. assert piaoquan["asset_summary"]["headline"] == "待复看 1 条"
  348. category = next(item for item in ledger["rows"] if item["id"] == "q_004")
  349. assert "分类树:Pattern 执行 581" in category["source"]["details"]
  350. assert "分类路径:/理念/观念/个人观念/人生观" in category["source"]["details"]
  351. assert "所在层级:第 4 层" in category["source"]["details"]
  352. videos = client.get(f"/runs/{run_id}/flow-ledger/queries/q_001/videos").json()
  353. assert videos["summary"]["total"] == 1
  354. assert videos["videos"][0]["oss_url"] == "https://oss.example.com/video.mp4"
  355. walk = client.get(f"/runs/{run_id}/flow-ledger/queries/q_001/walk").json()
  356. assert walk["summary"]["total_actions"] == 2
  357. assert walk["summary"]["extension_query_count"] == 2
  358. assert walk["lanes"]["tag_query"][0]["edge_label"] == "从视频标签继续游走"
  359. assert walk["lanes"]["tag_query"][0]["source_video"]["title"] == "睡前 5 分钟肩颈拉伸"
  360. assert walk["lanes"]["tag_query"][0]["trigger_label"] == "标签:#拉伸"
  361. assert walk["lanes"]["tag_query"][0]["target_query"]["text"] == "肩颈放松"
  362. assert walk["lanes"]["tag_query"][0]["target_videos"][0]["title"] == "扩展搜到的视频"
  363. assert "起点:从视频《睡前 5 分钟肩颈拉伸》触发" in walk["lanes"]["tag_query"][0]["detail_lines"]
  364. assert "结果:已生成标签搜索词《肩颈放松》,带回 1 条视频。" in walk["lanes"]["tag_query"][0]["detail_lines"]
  365. assert walk["lanes"]["tag_query"][0]["score_items"][0]["score_label"] == "82 分"
  366. page_action = walk["lanes"]["query_next_page"][0]
  367. assert page_action["edge_label"] == "翻页继续找"
  368. assert page_action["source_video"] is None
  369. assert page_action["from_label"] == "从搜索词《睡前拉伸》继续"
  370. assert page_action["target_query"]["id"] == "q_001_page_002"
  371. assert page_action["target_videos"][0]["title"] == "翻页搜到的视频"
  372. assert "起点:从搜索词《睡前拉伸》继续" in page_action["detail_lines"]
  373. detail = client.get(f"/runs/{run_id}/flow-ledger/videos/douyin_001").json()
  374. assert detail["video"]["playable_url"] == "https://oss.example.com/video.mp4"
  375. assert detail["decision"]["label"] == "入池"
  376. service.runtime.append_jsonl(
  377. run_id,
  378. "search_queries.jsonl",
  379. [
  380. {
  381. "run_id": run_id,
  382. "policy_run_id": policy_run_id,
  383. "search_query_id": "q_005",
  384. "search_query": "新搜索词",
  385. "search_query_generation_method": "seed_term",
  386. "raw_payload": {},
  387. }
  388. ],
  389. )
  390. cached = client.get(f"/runs/{run_id}/flow-ledger").json()
  391. assert cached["summary"]["first_round_query_count"] == 3
  392. debug_fresh = client.get(f"/runs/{run_id}/flow-ledger?debug=true").json()
  393. assert debug_fresh["summary"]["first_round_query_count"] == 4