test_evaluation_runtime.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import asyncio
  2. import json
  3. import runpy
  4. from datetime import UTC, datetime
  5. from pathlib import Path
  6. from types import SimpleNamespace
  7. from uuid import UUID, uuid4
  8. import pytest
  9. from langchain_core.messages import HumanMessage, message_to_dict
  10. from test_after_sales import scenario # noqa: F401
  11. from test_agent_runtime import tool_run
  12. from zhifu.evaluation import AgentManifest, digest, write_report
  13. from zhifu.modules.agent_runtime.config import GRAPH_VERSION, SYSTEM_PROMPT, AgentSettings
  14. from zhifu.modules.agent_runtime.models import AgentRun
  15. from zhifu.modules.agent_runtime.runtime import build_tool_graph
  16. from zhifu.modules.agent_runtime.worker import claim
  17. runner = runpy.run_path("scripts/evaluate_agent_runs.py")
  18. @pytest.mark.integration
  19. async def test_persistent_call_limit_survives_restart_and_preserves_output_budget(scenario): # noqa: F811
  20. path = Path(".work") / f"allowance-validation-{uuid4().hex}.json"
  21. review = {"reviewer": "字段校验", "reviewed_at": datetime.now(UTC),
  22. "evidence_path": "字段校验不作为人工审核记录", "evidence_sha256": "a" * 64}
  23. manifest = AgentManifest(dataset_sha256="a" * 64, source_sha256="b" * 64,
  24. split="development", release_sha256="c" * 64, graph_version=GRAPH_VERSION,
  25. model_name="validation", max_calls=3, max_output_tokens=128,
  26. max_total_output_tokens=256, total_timeout_seconds=30,
  27. cases=[{"case_id": "validation", "tenant_id": uuid4(), "run_id": uuid4(),
  28. "review": review, "environment_evidence": review}])
  29. state = scenario["app"].state.state
  30. args = SimpleNamespace(output=path, execute=True)
  31. dataset = SimpleNamespace(version="validation")
  32. identity = digest(manifest.model_dump(mode="json"))
  33. budget_key = "evaluation-budget:" + identity
  34. async def reserve_one():
  35. lock = state.lock("evaluation:" + identity, timeout=30, blocking_timeout=5)
  36. async with lock:
  37. report = await runner["initialize_report"](args, dataset, manifest, {}, state)
  38. await asyncio.to_thread(write_report, path, report)
  39. await runner["PersistentAllowance"](manifest, report, path, state).take()
  40. return report["reserved_calls"]
  41. try:
  42. args.execute = False
  43. initial = await runner["initialize_report"](args, dataset, manifest, {}, state)
  44. assert initial["deadline"] == 0
  45. await asyncio.to_thread(write_report, path, initial)
  46. args.execute = True
  47. assert sorted(await asyncio.gather(reserve_one(), reserve_one())) == [1, 2]
  48. assert int(await state.hget(budget_key, "calls")) == 2
  49. with pytest.raises(ValueError, match="限额"):
  50. await reserve_one()
  51. args.output = path.with_suffix(".other.json")
  52. with pytest.raises(ValueError, match="绑定其他"):
  53. await runner["initialize_report"](args, dataset, manifest, {}, state)
  54. args.output = path
  55. await asyncio.to_thread(path.unlink)
  56. with pytest.raises(ValueError, match="报告文件缺失"):
  57. await runner["initialize_report"](args, dataset, manifest, {}, state)
  58. finally:
  59. await state.delete(budget_key, "evaluation:" + identity)
  60. await asyncio.to_thread(path.unlink, missing_ok=True)
  61. @pytest.mark.integration
  62. async def test_targeted_claim_and_real_tool_observation(scenario): # noqa: F811
  63. sessions = scenario["app"].state.sessions
  64. first = await tool_run(scenario, "get_order", {"order_id": str(scenario["order_id"])})
  65. target = await tool_run(scenario, "get_order", {"order_id": str(scenario["order_id"])})
  66. settings = AgentSettings()
  67. snapshot = {"model_name": settings.model_name, "model_url": settings.model_url,
  68. "graph_version": GRAPH_VERSION, "system_prompt": SYSTEM_PROMPT,
  69. "max_output_tokens": 512}
  70. prompt = "查询当前订单"
  71. async with sessions.begin() as session:
  72. run = await session.get(AgentRun, target.id)
  73. run.model_name, run.graph_version = settings.model_name, GRAPH_VERSION
  74. run.release_snapshot = snapshot
  75. run.messages = [message_to_dict(HumanMessage(content=prompt)), *run.messages]
  76. assert await claim(sessions, 2, run_ids=[]) is None
  77. run_id, token = await claim(sessions, 2, run_ids=[target.id])
  78. assert run_id == target.id
  79. async with asyncio.timeout(10):
  80. await build_tool_graph(sessions).ainvoke({"run_id": run_id, "token": token})
  81. binding = SimpleNamespace(run_id=UUID(bytes=target.id), tenant_id=scenario["tenant"])
  82. manifest = SimpleNamespace(graph_version=GRAPH_VERSION, model_name=settings.model_name,
  83. release_sha256=digest(snapshot), max_output_tokens=512)
  84. observation = await runner["observe"](sessions, binding, manifest, {"prompt": prompt})
  85. assert observation["attempts"] == []
  86. assert observation["messages"][-1]["type"] == "tool"
  87. assert json.loads(observation["messages"][-1]["data"]["content"])["order_id"] == str(
  88. scenario["order_id"])
  89. async with sessions() as session:
  90. assert (await session.get(AgentRun, first.id)).status == "QUEUED"
  91. manifest.release_sha256 = "0" * 64
  92. with pytest.raises(ValueError, match="发布参数"):
  93. await runner["observe"](sessions, binding, manifest, {"prompt": prompt})