test_context_metrics.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from __future__ import annotations
  2. import json
  3. from langchain_core.messages import (
  4. AIMessage,
  5. HumanMessage,
  6. SystemMessage,
  7. ToolMessage,
  8. )
  9. from production_build_agents.observability.context_metrics import (
  10. build_context_snapshot,
  11. build_terminal_metrics,
  12. summarize_tool_contribution,
  13. )
  14. def test_context_snapshot_measures_messages_tools_images_and_growth() -> None:
  15. previous = AIMessage(
  16. content="调用工具",
  17. tool_calls=[
  18. {
  19. "id": "call-1",
  20. "name": "inspect_tool",
  21. "args": {"工具参数": "不能进入指标正文"},
  22. }
  23. ],
  24. usage_metadata={
  25. "input_tokens": 120,
  26. "output_tokens": 10,
  27. "total_tokens": 130,
  28. },
  29. )
  30. messages = [
  31. SystemMessage(content="系统提示词秘密"),
  32. HumanMessage(
  33. content=[
  34. {"type": "text", "text": "中文输入"},
  35. {
  36. "type": "image_url",
  37. "image_url": {
  38. "url": "data:image/png;base64,YWJj",
  39. },
  40. },
  41. {
  42. "type": "image_url",
  43. "image_url": {
  44. "url": "https://secret.example/image.png",
  45. },
  46. },
  47. ]
  48. ),
  49. previous,
  50. ToolMessage(
  51. content='{"secret_result":"不能进入指标正文"}',
  52. tool_call_id="call-1",
  53. name="inspect_tool",
  54. ),
  55. ]
  56. snapshot = build_context_snapshot(
  57. messages,
  58. invocation_params={
  59. "tools": [
  60. {
  61. "type": "function",
  62. "function": {
  63. "name": "inspect_tool",
  64. "description": "工具说明秘密",
  65. "parameters": {"type": "object"},
  66. },
  67. }
  68. ]
  69. },
  70. identity={
  71. "business_run_id": "run-1",
  72. "agent_run_id": "agent-1",
  73. "role": "executor",
  74. },
  75. physical_call_id="physical-1",
  76. )
  77. assert snapshot["logical_call_index"] == 2
  78. assert snapshot["input"]["message_role_counts"] == {
  79. "system": 1,
  80. "human": 1,
  81. "ai": 1,
  82. "tool": 1,
  83. "other": 0,
  84. }
  85. assert snapshot["input"]["tool_messages"]["count"] == 1
  86. assert snapshot["input"]["history_tool_calls"]["count"] == 1
  87. assert snapshot["input"]["tool_schemas"]["count"] == 1
  88. assert snapshot["input"]["images"]["count"] == 2
  89. assert snapshot["input"]["images"]["decoded_bytes"] == 3
  90. assert snapshot["input"]["images"]["unknown_bytes_count"] == 1
  91. assert snapshot["previous"]["input_tokens"] == 120
  92. assert snapshot["previous"]["delta"]["message_count"] == 2
  93. serialized = json.dumps(snapshot, ensure_ascii=False)
  94. for secret in (
  95. "系统提示词秘密",
  96. "中文输入",
  97. "不能进入指标正文",
  98. "https://secret.example/image.png",
  99. "YWJj",
  100. "工具说明秘密",
  101. ):
  102. assert secret not in serialized
  103. def test_first_call_has_no_previous_delta_and_terminal_uses_real_tokens() -> None:
  104. snapshot = build_context_snapshot(
  105. [SystemMessage(content="system"), HumanMessage(content="hello")],
  106. invocation_params={},
  107. identity={"agent_run_id": "agent-1"},
  108. physical_call_id="physical-1",
  109. )
  110. output = AIMessage(
  111. content="done",
  112. usage_metadata={
  113. "input_tokens": 25,
  114. "output_tokens": 5,
  115. "total_tokens": 30,
  116. "input_token_details": {"cache_read": 4},
  117. },
  118. )
  119. terminal = build_terminal_metrics(
  120. snapshot,
  121. message=output,
  122. response=object(),
  123. status="success",
  124. duration_ms=17,
  125. transport={
  126. "observed": False,
  127. "attempt_count": None,
  128. "retry_count": None,
  129. "attempts": [],
  130. },
  131. )
  132. assert snapshot["previous"] is None
  133. assert terminal["usage"]["input_tokens"] == 25
  134. assert terminal["usage"]["input_tokens_delta"] is None
  135. assert terminal["output"]["content_bytes"] == 4
  136. assert "done" not in json.dumps(terminal)
  137. def test_tool_contribution_only_contains_size_and_hash() -> None:
  138. contribution = summarize_tool_contribution(
  139. {"secret": "tool result", "success": True}
  140. )
  141. assert contribution["message_role"] == "tool"
  142. assert contribution["serialized_bytes"] > 0
  143. assert "tool result" not in json.dumps(contribution)