| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- from copy import deepcopy
- from datetime import datetime, timezone
- from app.runtime_event_projection import RuntimeEventProjector
- def _implementer_events():
- return [
- {"id": 10, "event_seq": 1, "event_type": "agent_invoke", "event_name": "script_implementer", "round_index": 1, "branch_id": 7, "status": "ok", "started_at": "2026-07-13T10:00:00", "ended_at": "2026-07-13T10:02:00"},
- {"id": 11, "event_seq": 2, "event_type": "tool_call", "event_name": "get_topic_detail", "agent_role": "script_implementer", "scope_event_id": 10, "round_index": 1, "branch_id": 7, "status": "ok", "started_at": "2026-07-13T10:00:01", "ended_at": "2026-07-13T10:00:02"},
- {"id": 20, "event_seq": 3, "event_type": "agent_invoke", "event_name": "retrieve_data_decode_case", "parent_event_id": 10, "scope_event_id": 10, "round_index": 1, "branch_id": 7, "status": "ok", "inputData": {"task": "查找叙事案例"}, "agentOutputData": {"summary": "初筛出两个案例"}, "started_at": "2026-07-13T10:00:10", "ended_at": "2026-07-13T10:00:30"},
- {"id": 21, "event_seq": 4, "event_type": "tool_call", "event_name": "think_and_plan", "scope_event_id": 20, "parent_event_id": 20, "status": "ok", "started_at": "2026-07-13T10:00:11", "ended_at": "2026-07-13T10:00:12"},
- {"id": 22, "event_seq": 5, "event_type": "tool_call", "event_name": "search_script_decode_case", "scope_event_id": 20, "parent_event_id": 20, "status": "ok", "inputData": {"keyword": "叙事"}, "output_preview": '{"count": 2}', "started_at": "2026-07-13T10:00:13", "ended_at": "2026-07-13T10:00:17"},
- {"id": 23, "event_seq": 6, "event_type": "tool_call", "event_name": "search_script_decode_case", "scope_event_id": 20, "parent_event_id": 20, "status": "ok", "inputData": {"keyword": "构图"}, "output_preview": '{"count": 0}', "started_at": "2026-07-13T10:00:18", "ended_at": "2026-07-13T10:00:22"},
- ]
- def test_explicit_event_tree_separates_direct_tools_agent_queries_and_screening():
- projection = RuntimeEventProjector().project(_implementer_events(), valid_rounds={1}, valid_branches={7})
- stage = projection["retrievalStagesByBranch"][7]
- assert stage["observedMode"] == "sequential"
- assert stage["directToolGroups"][0]["sources"] == [{"businessLabel": "选题", "callCount": 1}]
- assert len(stage["agentRuns"]) == 1
- run = stage["agentRuns"][0]
- assert run["id"] == "retrieval-agent:20"
- assert run["taskPreview"] == "查找叙事案例"
- assert run["screening"]["preview"] == "初筛出两个案例"
- assert run["querySummary"] == {"total": 2, "hit": 1, "empty": 1, "failure": 0, "unknown": 0}
- assert [item["eventId"] for item in run["attempts"]] == [22, 23]
- def test_same_agent_type_invoked_twice_stays_two_instances():
- events = _implementer_events()
- second = deepcopy(events[2])
- second.update({"id": 30, "event_seq": 7, "started_at": "2026-07-13T10:00:40", "ended_at": "2026-07-13T10:00:50"})
- events.append(second)
- projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
- assert [item["id"] for item in projection["retrievalStagesByBranch"][7]["agentRuns"]] == ["retrieval-agent:20", "retrieval-agent:30"]
- def test_missing_parent_is_unassigned_instead_of_text_matched():
- event = {"id": 3, "event_type": "agent_invoke", "event_name": "retrieve_data_knowledge", "round_index": 1, "branch_id": 7, "inputData": {"task": "文字里写了方案 7"}}
- projection = RuntimeEventProjector().project([event], valid_rounds={1}, valid_branches={7})
- assert projection["retrievalStagesByBranch"] == {}
- assert projection["unassigned"][0]["eventId"] == 3
- assert projection["unassigned"][0]["association"] == "missing-parent-implementer"
- def test_parallel_and_mixed_modes_come_from_time_overlap():
- events = _implementer_events()
- first_agent = events[2]
- direct = events[1]
- direct["ended_at"] = "2026-07-13T10:00:20"
- first_agent["started_at"] = "2026-07-13T10:00:10"
- projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
- assert projection["retrievalStagesByBranch"][7]["observedMode"] == "parallel"
- later = deepcopy(first_agent)
- later.update({"id": 40, "event_seq": 9, "started_at": "2026-07-13T10:00:40", "ended_at": "2026-07-13T10:00:50"})
- events.append(later)
- projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
- assert projection["retrievalStagesByBranch"][7]["observedMode"] == "mixed"
- def test_unknown_time_does_not_claim_parallelism():
- events = _implementer_events()
- events[2]["started_at"] = None
- projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
- stage = projection["retrievalStagesByBranch"][7]
- assert stage["observedMode"] == "unknown"
- assert stage["waves"] == []
- def test_failed_direct_read_makes_stage_partial():
- events = _implementer_events()
- events[1]["status"] = "error"
- projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7})
- stage = projection["retrievalStagesByBranch"][7]
- assert stage["directToolGroups"][0]["failureCount"] == 1
- assert stage["status"] == "partial"
- def test_running_implementer_without_retrieval_calls_is_preparing_not_missing():
- events = [{
- "id": 10, "event_seq": 1, "event_type": "agent_invoke",
- "event_name": "script_implementer", "round_index": 1,
- "branch_id": 7, "status": "running",
- "started_at": "2026-07-14T12:00:00", "ended_at": None,
- }]
- projection = RuntimeEventProjector().project(
- events,
- valid_rounds={1},
- valid_branches={7},
- captured_at=datetime(2026, 7, 14, 4, 0, 5, tzinfo=timezone.utc),
- )
- stage = projection["retrievalStagesByBranch"][7]
- assert stage["status"] == "running"
- assert stage["observedMode"] == "unknown"
- def test_terminal_run_reclassifies_unfinished_retrieval_as_interrupted():
- events = _implementer_events()
- events[0].update({"status": "running", "ended_at": None})
- events[1].update({"status": "running", "ended_at": None})
- events[2].update({"status": "running", "ended_at": None})
- events[4].update({"status": "running", "ended_at": None})
- projection = RuntimeEventProjector().project(
- events,
- valid_rounds={1},
- valid_branches={7},
- run_status="failed",
- )
- stage = projection["retrievalStagesByBranch"][7]
- assert stage["status"] == "partial"
- assert stage["directToolGroups"][0]["calls"][0]["status"] == "interrupted"
- assert stage["agentRuns"][0]["status"] == "interrupted"
- assert stage["agentRuns"][0]["attempts"][0]["status"] == "interrupted"
- def test_running_naive_database_time_uses_asia_shanghai_before_wave_projection():
- events = _implementer_events()
- events[0].update({"status": "running", "ended_at": None})
- events[1].update({"status": "running", "started_at": "2026-07-14T12:00:00", "ended_at": None})
- events[2].update({"status": "running", "started_at": "2026-07-14T12:00:02", "ended_at": None})
- projection = RuntimeEventProjector().project(
- events,
- valid_rounds={1},
- valid_branches={7},
- captured_at=datetime(2026, 7, 14, 4, 0, 5, tzinfo=timezone.utc),
- )
- stage = projection["retrievalStagesByBranch"][7]
- assert stage["observedMode"] == "parallel"
- assert stage["startedAt"] == "2026-07-14T04:00:00+00:00"
- assert stage["endedAt"] == "2026-07-14T04:00:05+00:00"
- def test_query_output_error_precedes_empty_count_and_unstructured_object_is_unknown():
- events = _implementer_events()
- events[4]["output_preview"] = '{"error":"knowledge unavailable","items":[]}'
- events[5]["output_preview"] = '{"message":"query accepted"}'
- stage = RuntimeEventProjector().project(
- events, valid_rounds={1}, valid_branches={7}
- )["retrievalStagesByBranch"][7]
- assert [attempt["status"] for attempt in stage["agentRuns"][0]["attempts"]] == ["failure", "unknown"]
- assert stage["agentRuns"][0]["querySummary"] == {
- "total": 2, "hit": 0, "empty": 0, "failure": 1, "unknown": 1,
- }
- def test_agent_card_screening_preview_uses_business_cleaning():
- events = _implementer_events()
- events[2]["agentOutputData"] = {
- "summary": "## 委托任务完成\n\n### 初筛结果\n- 找到可用案例 post_id=abc123\n\n**执行统计**:\n- Tokens: 1200\n- 成本: $0.1"
- }
- stage = RuntimeEventProjector().project(
- events, valid_rounds={1}, valid_branches={7}
- )["retrievalStagesByBranch"][7]
- preview = stage["agentRuns"][0]["screening"]["preview"]
- assert "可用案例" in preview
- assert "post_id" not in preview
- assert "abc123" not in preview
- assert "Tokens" not in preview
- assert "成本" not in preview
- def test_creative_business_projection_translates_runtime_tool_names():
- events = [
- {
- "id": 10,
- "event_seq": 1,
- "event_type": "agent_invoke",
- "event_name": "script_implementer",
- "round_index": 1,
- "branch_id": 7,
- "status": "ok",
- "inputData": {
- "task": "路序号:1 action:增 target:烹饪贴士 path_type:内容 目标:补齐烹饪贴士与总结。"
- },
- },
- {
- "id": 11,
- "event_seq": 2,
- "event_type": "tool_call",
- "event_name": "think_and_plan",
- "agent_role": "script_implementer",
- "scope_event_id": 10,
- "round_index": 1,
- "branch_id": 7,
- "status": "ok",
- "inputData": {
- "thought": "缺少直接证据,先保留为待验证推导。",
- "action": "record_data_decision",
- "plan": "1. 调用 get_topic_detail。\n2. 委派 retrieve_data_knowledge。\n3. 调用 batch_update_script_paragraphs。",
- },
- },
- ]
- decision = RuntimeEventProjector().project(
- events, valid_rounds={1}, valid_branches={7}
- )["creativeByBranch"][7]
- assert decision["body"]["task"] == "补齐烹饪贴士与总结。"
- assert decision["body"]["actions"] == ["记录数据取舍"]
- assert decision["body"]["steps"] == [
- {
- "stepIndex": 1,
- "action": "记录数据取舍",
- "summary": None,
- "plan": "1. 读取选题。\n2. 委派知识 Agent取数。\n3. 更新候选脚本内容。",
- "reasoning": "缺少直接证据,先保留为待验证推导。",
- "eventRef": "event:11",
- }
- ]
- process_block = decision["detail"]["blocks"][0]
- assert process_block["type"] == "creative-process"
- assert process_block["steps"][0]["action"] == "记录数据取舍"
- business = str(decision["detail"]["blocks"])
- assert "record_data_decision" not in business
- assert "get_topic_detail" not in business
- assert "batch_update_script_paragraphs" not in business
- assert "path_type" not in business
- def test_creative_projection_keeps_upstream_and_actual_mutation_refs_separate():
- events = [
- {
- "id": 100,
- "event_seq": 1,
- "event_type": "agent_invoke",
- "event_name": "script_implementer",
- "round_index": 1,
- "branch_id": 7,
- "status": "ok",
- "inputData": {"task": "目标:补齐正文。"},
- },
- {
- "id": 101,
- "event_seq": 2,
- "event_type": "tool_call",
- "event_name": "get_script_snapshot",
- "agent_role": "script_implementer",
- "scope_event_id": 100,
- "round_index": 1,
- "branch_id": 7,
- "status": "ok",
- },
- {
- "id": 102,
- "event_seq": 3,
- "event_type": "tool_call",
- "event_name": "think_and_plan",
- "agent_role": "script_implementer",
- "scope_event_id": 100,
- "round_index": 1,
- "branch_id": 7,
- "status": "ok",
- "inputData": {"action": "更新候选正文", "thought": "先根据已有结构补写。"},
- },
- {
- "id": 103,
- "event_seq": 4,
- "event_type": "tool_call",
- "event_name": "batch_update_script_paragraphs",
- "agent_role": "script_implementer",
- "scope_event_id": 100,
- "round_index": 1,
- "branch_id": 7,
- "status": "ok",
- },
- ]
- decision = RuntimeEventProjector().project(
- events, valid_rounds={1}, valid_branches={7}
- )["creativeByBranch"][7]
- assert decision["eventRefs"] == ["event:101", "event:102", "event:103"]
- assert decision["body"]["availableUpstream"][0]["decisionUse"] == "not-recorded"
- assert decision["body"]["runtimeActions"][0]["label"] == "更新候选脚本内容"
- blocks = {item["title"]: item for item in decision["detail"]["blocks"]}
- upstream_item = blocks["可确认的上游信息"]["items"][0]
- assert "无法确认是否被本次创作采用" in upstream_item["note"]
- assert blocks["实际脚本改动"]["items"][0]["detailRef"] == "event:103"
- def test_exact_evaluator_names_create_two_different_business_stages():
- events = [
- {
- "id": 80, "event_seq": 1, "event_type": "agent_invoke",
- "event_name": "script_multipath_evaluator", "round_index": 1,
- "status": "ok", "inputData": {"task": "比较 branch_id=7, branch_id=8"},
- "agentOutputData": {"summary": "### 分支评估 branch_id=7\n**该支结论**:通过\n**目标推进**:结构完整。\n\n### 分支评估 branch_id=8\n**该支结论**:部分通过\n**目标推进**:仍需补充。\n\n### 对比与建议\n**采纳建议**:优先采用 branch_id=7。"},
- "started_at": "2026-07-13T10:03:00", "ended_at": "2026-07-13T10:03:10",
- },
- {
- "id": 81, "event_seq": 2, "event_type": "agent_invoke",
- "event_name": "script_evaluator", "round_index": 1,
- "status": "ok", "agentOutputData": {"summary": "### 整体结论:部分通过"},
- "started_at": "2026-07-13T10:04:00", "ended_at": "2026-07-13T10:04:10",
- },
- {
- "id": 82, "event_seq": 3, "event_type": "agent_invoke",
- "event_name": "some_future_evaluator", "round_index": 1,
- "status": "ok", "started_at": "2026-07-13T10:05:00", "ended_at": "2026-07-13T10:05:10",
- },
- ]
- projection = RuntimeEventProjector().project(events, valid_rounds={1}, valid_branches={7, 8})
- review = projection["multipathReviewsByRound"][1][0]
- assert review["branchIds"] == [7, 8]
- assert [item["conclusion"] for item in review["branchConclusions"]] == ["通过", "部分通过"]
- assert "优先采用" in review["recommendation"]
- assert projection["overallEvaluationsByRound"][1][0]["eventId"] == 81
- assert "evaluationsByBranch" not in projection
- assert "evaluationsByRound" not in projection
- def test_multipath_evaluator_parses_one_labelled_comma_separated_branch_list():
- event = {
- "id": 3687,
- "event_seq": 1,
- "event_type": "agent_invoke",
- "event_name": "script_multipath_evaluator",
- "round_index": 2,
- "status": "ok",
- "inputData": {
- "task": "评估对象:branch_id = 4, 5, 6, 7(分工,各填充一段)"
- },
- "agentOutputData": {
- "summary": "\n".join(
- f"### 分支评估 branch_id={branch_id}\n**该支结论**:通过"
- for branch_id in (4, 5, 6, 7)
- )
- },
- "started_at": "2026-07-14T17:17:09",
- "ended_at": "2026-07-14T17:18:06",
- }
- projection = RuntimeEventProjector().project(
- [event], valid_rounds={2}, valid_branches={4, 5, 6, 7}
- )
- assert projection["multipathReviewsByRound"][2][0]["branchIds"] == [4, 5, 6, 7]
- def test_facade_builds_one_index_and_merges_projector_outputs():
- received = []
- class RetrievalSpy:
- def project(self, index, **kwargs):
- received.append(index)
- return {
- "retrievalStagesByBranch": {7: {"id": "retrieval"}},
- "unassigned": [{"id": "retrieval-unassigned"}],
- }
- class EvaluationSpy:
- def project(self, index, **kwargs):
- received.append(index)
- return {
- "multipathReviewsByRound": {1: [{"id": "review"}]},
- "overallEvaluationsByRound": {1: [{"id": "evaluation"}]},
- "unassigned": [{"id": "evaluation-unassigned"}],
- }
- class MainAgentSpy:
- def project(self, index, **kwargs):
- received.append(index)
- return {
- "objective": {"stage": "objective"},
- "roundGoalsByRound": {},
- "implementationPlansByRound": {},
- }
- projection = RuntimeEventProjector(
- retrieval_projector=RetrievalSpy(),
- evaluation_projector=EvaluationSpy(),
- main_agent_projector=MainAgentSpy(),
- ).project(
- [{
- "id": 9,
- "event_seq": 1,
- "event_type": "tool_call",
- "event_name": "record_multipath_plan",
- "round_index": 1,
- "status": "ok",
- }],
- valid_rounds={1},
- valid_branches={7},
- )
- assert len({id(index) for index in received}) == 1
- assert projection["retrievalStagesByBranch"][7]["id"] == "retrieval"
- assert projection["multipathReviewsByRound"][1][0]["id"] == "review"
- assert projection["overallEvaluationsByRound"][1][0]["id"] == "evaluation"
- assert projection["mainAgentDecisions"]["objective"]["stage"] == "objective"
- assert "planRevisionsByRound" not in projection
- assert projection["unassigned"] == [
- {"id": "retrieval-unassigned"},
- {"id": "evaluation-unassigned"},
- ]
|