| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- from __future__ import annotations
- import json
- from types import SimpleNamespace
- from langchain.agents import create_agent
- from langchain_core.language_models.fake_chat_models import (
- FakeMessagesListChatModel,
- )
- from langchain_core.messages import (
- AIMessage,
- HumanMessage,
- SystemMessage,
- ToolMessage,
- )
- from production_build_agents.observability.readable_events import (
- BusinessReadableFlowCollector,
- readable_value,
- tool_title,
- )
- from production_build_agents.observability.business_views import (
- business_response_title,
- )
- from production_build_agents.agents.invocation import build_agent_run_config
- from production_build_agents.run.langgraph_checkpointer import (
- create_in_memory_checkpointer,
- )
- class _ModuleContext:
- def __init__(self) -> None:
- self.events: list[tuple[int, str, dict]] = []
- self.messages: list[tuple[list, int]] = []
- def emit_at(self, idx: int, type_: str, **fields) -> None:
- self.events.append((idx, type_, fields))
- def record_messages(self, messages: list, *, idx: int) -> None:
- self.messages.append((messages, idx))
- class _Run:
- def __init__(self) -> None:
- self.usage = (0, 0)
- def account(self, input_tokens: int, output_tokens: int) -> None:
- self.usage = (input_tokens, output_tokens)
- def test_readable_value_turns_json_into_business_sentences() -> None:
- text = readable_value(
- '{"success": true, "task_id": "Task2", '
- '"status": "delivered", "output_path": "/run/result.json"}'
- )
- assert "结果:成功" in text
- assert "Task:Task2" in text
- assert "状态:delivered" in text
- assert not text.startswith("{")
- def test_fenced_global_data_plan_is_rendered_as_business_logic() -> None:
- plan = {
- "schema_version": "0.3",
- "plan_id": "global-data-plan",
- "plan_version": 1,
- "goal": "准备正式生产所需的共享素材与约束",
- "stage_requirements": [
- {
- "requirement_id": "Requirement1",
- "description": "确认人物基准图真实可用",
- "artifact_expectations": [
- {
- "artifact_type": "image",
- "minimum_count": 1,
- "source_asset_ids": ["SourceAsset-" + "a" * 64],
- }
- ],
- }
- ],
- "tasks": [
- {
- "task_id": "Task1",
- "objective": "检查并采纳人物基准图",
- "skill_id": "reference-inspection",
- "expectation_ids": ["Requirement1-Expectation1"],
- "depends_on": [],
- }
- ],
- "revision_summary": "先验收共享素材,再进入正式生产",
- }
- text = readable_value(
- "```json\n"
- + json.dumps(plan, ensure_ascii=False)
- + "\n```"
- )
- assert "规划目标:准备正式生产所需的共享素材与约束" in text
- assert "Requirement1|确认人物基准图真实可用" in text
- assert "Task1|检查并采纳人物基准图" in text
- assert "stage_requirements" not in text
- assert "```json" not in text
- def test_validator_candidate_without_verdict_has_business_title() -> None:
- candidate = {
- "task_id": "Task3",
- "criterion_results": [
- {
- "criterion_id": "Criterion1",
- "passed": True,
- "reason": "媒体证据完整",
- }
- ],
- "summary": "任务满足验收要求",
- }
- text = readable_value(json.dumps(candidate, ensure_ascii=False))
- assert business_response_title(candidate) == "模型形成 Task 验收结论"
- assert "业务摘要:任务满足验收要求" in text
- assert "criterion_results" not in text
- def test_tool_names_have_business_titles() -> None:
- assert tool_title("read_production_brief") == "读取 Production Brief"
- assert tool_title("assemble_segment_media") == "组装 Segment 媒体"
- assert tool_title("custom_business_action") == "Custom Business Action"
- def test_llm_tool_decision_card_is_readable_and_keeps_raw_messages() -> None:
- run = _Run()
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(run, auto_module=False)
- collector._frame = lambda *_args: (context, 3)
- collector._pending_msgs["llm-1"] = [
- AIMessage(content="上一步上下文")
- ]
- collector._model_of["llm-1"] = "test-model"
- message = AIMessage(
- content="",
- tool_calls=[
- {
- "id": "call-1",
- "name": "read_production_brief",
- "args": {"source_paths": ["subject.title", "shots"]},
- }
- ],
- usage_metadata={
- "input_tokens": 20,
- "output_tokens": 5,
- "total_tokens": 25,
- },
- )
- response = SimpleNamespace(
- generations=[[SimpleNamespace(message=message)]],
- llm_output={},
- )
- collector.on_llm_end(response, run_id="llm-1")
- _, type_, event = context.events[0]
- assert type_ == "llm"
- assert event["label"] == "模型决定调用:读取 Production Brief"
- assert "读取 Production Brief" in event["content"]
- assert event["payload"]["tool_calls"][0]["args_summary"] == (
- "来源:subject.title、shots"
- )
- assert context.messages[0][0][-1] is message
- assert run.usage == (20, 5)
- def test_llm_start_snapshot_and_terminal_are_joinable_without_raw_content() -> None:
- run = _Run()
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(run, auto_module=False)
- collector._frame = lambda *_args: (context, 3)
- collector.on_chat_model_start(
- {"name": "test-model"},
- [[
- SystemMessage(content="系统提示词秘密"),
- HumanMessage(content="用户输入秘密"),
- ]],
- run_id="llm-context",
- metadata={
- "business_run_id": "run-1",
- "agent_run_id": "agent-1",
- "domain": "production",
- "role": "segment_executor",
- "segment_id": "Segment1",
- "plan_version": 1,
- },
- invocation_params={"tools": []},
- )
- response_message = AIMessage(
- content="模型输出秘密",
- usage_metadata={
- "input_tokens": 30,
- "output_tokens": 5,
- "total_tokens": 35,
- },
- )
- collector.on_llm_end(
- SimpleNamespace(
- generations=[[SimpleNamespace(message=response_message)]],
- llm_output={},
- ),
- run_id="llm-context",
- )
- assert [event[1] for event in context.events] == ["note", "llm"]
- start = context.events[0][2]["payload"]
- terminal = context.events[1][2]["payload"]["context_metrics"]
- assert start["event_kind"] == "model_context_start"
- assert terminal["event_kind"] == "model_context_terminal"
- assert start["physical_call_id"] == terminal["physical_call_id"]
- assert start["identity"]["segment_id"] == "Segment1"
- assert terminal["usage"]["input_tokens"] == 30
- assert len(context.messages) == 1
- serialized_start = json.dumps(start, ensure_ascii=False)
- assert "系统提示词秘密" not in serialized_start
- assert "用户输入秘密" not in serialized_start
- def test_llm_failure_closes_started_context_and_keeps_input_once() -> None:
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(_Run(), auto_module=False)
- collector._frame = lambda *_args: (context, 4)
- collector.on_chat_model_start(
- {"name": "test-model"},
- [[HumanMessage(content="will fail")]],
- run_id="llm-failure",
- metadata={
- "business_run_id": "run-1",
- "agent_run_id": "agent-1",
- "role": "planner",
- },
- )
- collector.on_llm_error(
- TimeoutError("provider timeout"),
- run_id="llm-failure",
- )
- assert [event[1] for event in context.events] == ["note", "llm"]
- failure = context.events[1][2]
- assert failure["ok"] is False
- terminal = failure["payload"]["context_metrics"]
- assert terminal["status"] == "failure"
- assert terminal["error_type"] == "TimeoutError"
- assert len(context.messages) == 1
- assert len(context.messages[0][0]) == 1
- def test_langgraph_config_metadata_reaches_real_model_callback() -> None:
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(_Run(), auto_module=False)
- collector._frame = lambda *_args: (context, 2)
- model = FakeMessagesListChatModel(
- responses=[AIMessage(content="done")],
- callbacks=[collector],
- )
- agent = create_agent(
- model=model,
- tools=[],
- system_prompt="system",
- checkpointer=create_in_memory_checkpointer(),
- )
- config = build_agent_run_config(
- business_run_id="run-1",
- agent_run_id="run-1:Segment1:v1:validator",
- domain="production",
- role="segment_validator",
- invocation_mode="VALIDATE",
- segment_id="Segment1",
- plan_version=1,
- )
- agent.invoke(
- {"messages": [{"role": "user", "content": "validate"}]},
- config,
- )
- start = next(
- fields["payload"]
- for _, type_, fields in context.events
- if type_ == "note"
- )
- assert start["identity"] == config["metadata"]
- def test_llm_plan_card_has_business_title_and_keeps_raw_message() -> None:
- run = _Run()
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(run, auto_module=False)
- collector._frame = lambda *_args: (context, 4)
- collector._pending_msgs["llm-plan"] = [
- AIMessage(content="规划上下文")
- ]
- plan_json = (
- '```json\n{"schema_version":"0.3","plan_id":"plan-1",'
- '"plan_version":1,"goal":"准备共享素材",'
- '"stage_requirements":[],"tasks":[]}\n```'
- )
- message = AIMessage(content=plan_json)
- response = SimpleNamespace(
- generations=[[SimpleNamespace(message=message)]],
- llm_output={},
- )
- collector.on_llm_end(response, run_id="llm-plan")
- _, type_, event = context.events[0]
- assert type_ == "llm"
- assert event["label"] == "初版候选 · Global Data 规划"
- assert event["content"].startswith("规划目标:准备共享素材")
- assert "```json" not in event["content"]
- assert event["payload"]["business_phase"] == "initial_candidate"
- assert context.messages[0][0][-1] is message
- def test_contract_correction_is_labeled_before_upload() -> None:
- run = _Run()
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(run, auto_module=False)
- collector._frame = lambda *_args: (context, 5)
- collector._pending_msgs["llm-correction"] = [
- HumanMessage(content='{"task_id":"Task3"}'),
- AIMessage(content='{"summary":"初版"}'),
- HumanMessage(
- content=(
- "上一版未通过运行时校验。错误:"
- "missing_binding_evidence: 该 Expectation 必须声明工具证据。"
- )
- ),
- ]
- corrected = AIMessage(
- content=json.dumps(
- {
- "task_id": "Task3",
- "artifacts": [{"uri": "/run/image.png"}],
- "artifact_binding_claims": [
- {
- "expectation_id": "Expectation1",
- "evidence_tool_call_ids": ["call-1"],
- }
- ],
- "summary": "已补充工具证据",
- },
- ensure_ascii=False,
- ),
- usage_metadata={
- "input_tokens": 30,
- "output_tokens": 8,
- "total_tokens": 38,
- },
- )
- response = SimpleNamespace(
- generations=[[SimpleNamespace(message=corrected)]],
- llm_output={},
- )
- collector.on_llm_end(response, run_id="llm-correction")
- _, type_, event = context.events[0]
- assert type_ == "llm"
- assert event["label"] == "合同校正 · 第 1 次"
- assert event["content"].startswith(
- "校正原因:Artifact Binding 缺少工具调用证据\n校正结果:"
- )
- assert event["payload"]["business_phase"] == "contract_correction"
- assert event["payload"]["attempt"] == 2
- assert event["payload"]["previous_candidate_ok"] is False
- assert event["payload"]["correction_reason_codes"] == [
- "missing_binding_evidence"
- ]
- def test_media_review_is_labeled_and_reports_scope() -> None:
- run = _Run()
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(run, auto_module=False)
- collector._frame = lambda *_args: (context, 6)
- collector._pending_msgs["llm-media-review"] = [
- HumanMessage(content='{"task_id":"Task3"}'),
- HumanMessage(
- content=[
- {
- "type": "text",
- "text": (
- "最终媒体证据自检:"
- '{"artifact_uris":["/run/image.png","/run/video.mp4"]}'
- ),
- },
- {
- "type": "image_url",
- "image_url": {"url": "data:image/png;base64,AA=="},
- },
- ]
- ),
- ]
- reviewed = AIMessage(
- content=json.dumps(
- {
- "task_id": "Task3",
- "artifacts": [
- {"uri": "/run/image.png"},
- {"uri": "/run/video.mp4"},
- ],
- "artifact_binding_claims": [],
- "summary": "媒体内容与制作要求一致",
- },
- ensure_ascii=False,
- )
- )
- response = SimpleNamespace(
- generations=[[SimpleNamespace(message=reviewed)]],
- llm_output={},
- )
- collector.on_llm_end(response, run_id="llm-media-review")
- _, type_, event = context.events[0]
- assert type_ == "llm"
- assert event["label"] == "媒体证据复核"
- assert event["content"].startswith(
- "复核范围:2 个媒体产物\n复核结果:"
- )
- assert event["payload"]["business_phase"] == "media_evidence_review"
- assert event["payload"]["reviewed_media_count"] == 2
- def test_correction_tool_call_keeps_action_and_adds_phase() -> None:
- run = _Run()
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(run, auto_module=False)
- collector._frame = lambda *_args: (context, 7)
- collector._pending_msgs["llm-correction-tool"] = [
- HumanMessage(
- content="上一版未通过运行时格式校验。错误:缺少字段。"
- )
- ]
- message = AIMessage(
- content="重新检查图片",
- tool_calls=[
- {
- "id": "call-2",
- "name": "view_images",
- "args": {"image_sources": ["/run/image.png"]},
- }
- ],
- )
- response = SimpleNamespace(
- generations=[[SimpleNamespace(message=message)]],
- llm_output={},
- )
- collector.on_llm_end(response, run_id="llm-correction-tool")
- _, type_, event = context.events[0]
- assert type_ == "llm"
- assert event["label"] == "校正中 · 模型决定调用:查看图片"
- assert event["payload"]["business_phase"] == "contract_correction"
- assert event["payload"]["tool_calls"][0]["name"] == "view_images"
- def test_tool_result_card_uses_summary_instead_of_raw_json() -> None:
- context = _ModuleContext()
- collector = BusinessReadableFlowCollector(_Run(), auto_module=False)
- collector._frame = lambda *_args: (context, 2)
- collector._pending_tool["tool-1"] = {
- "name": "probe_media",
- "args": {"artifact_uri": "/run/segment.mp4"},
- "tok": None,
- }
- output = ToolMessage(
- content=(
- '{"success": true, "status": "ready", '
- '"duration_ms": 1250, "video_url": "https://media/segment.mp4"}'
- ),
- tool_call_id="call-1",
- name="probe_media",
- )
- collector.on_tool_end(output, run_id="tool-1")
- _, type_, event = context.events[0]
- assert type_ == "tool"
- assert event["label"] == "检查媒体技术信息 · 已返回"
- assert event["tool_args"] == "Artifact:/run/segment.mp4"
- assert "结果:成功" in event["tool_result"]
- assert "状态:ready" in event["tool_result"]
- assert not event["tool_result"].startswith("{")
- contribution = event["payload"]["context_contribution"]
- assert contribution["message_role"] == "tool"
- assert contribution["serialized_bytes"] > 0
- assert "segment.mp4" not in json.dumps(contribution)
|