mseep-agentops 0.4.18__py3-none-any.whl → 0.4.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. agentops/__init__.py +0 -0
  2. agentops/client/api/base.py +28 -30
  3. agentops/client/api/versions/v3.py +29 -25
  4. agentops/client/api/versions/v4.py +87 -46
  5. agentops/client/client.py +98 -29
  6. agentops/client/http/README.md +87 -0
  7. agentops/client/http/http_client.py +126 -172
  8. agentops/config.py +8 -2
  9. agentops/instrumentation/OpenTelemetry.md +133 -0
  10. agentops/instrumentation/README.md +167 -0
  11. agentops/instrumentation/__init__.py +13 -1
  12. agentops/instrumentation/agentic/ag2/__init__.py +18 -0
  13. agentops/instrumentation/agentic/ag2/instrumentor.py +922 -0
  14. agentops/instrumentation/agentic/agno/__init__.py +19 -0
  15. agentops/instrumentation/agentic/agno/attributes/__init__.py +20 -0
  16. agentops/instrumentation/agentic/agno/attributes/agent.py +250 -0
  17. agentops/instrumentation/agentic/agno/attributes/metrics.py +214 -0
  18. agentops/instrumentation/agentic/agno/attributes/storage.py +158 -0
  19. agentops/instrumentation/agentic/agno/attributes/team.py +195 -0
  20. agentops/instrumentation/agentic/agno/attributes/tool.py +210 -0
  21. agentops/instrumentation/agentic/agno/attributes/workflow.py +254 -0
  22. agentops/instrumentation/agentic/agno/instrumentor.py +1313 -0
  23. agentops/instrumentation/agentic/crewai/LICENSE +201 -0
  24. agentops/instrumentation/agentic/crewai/NOTICE.md +10 -0
  25. agentops/instrumentation/agentic/crewai/__init__.py +6 -0
  26. agentops/instrumentation/agentic/crewai/crewai_span_attributes.py +335 -0
  27. agentops/instrumentation/agentic/crewai/instrumentation.py +535 -0
  28. agentops/instrumentation/agentic/crewai/version.py +1 -0
  29. agentops/instrumentation/agentic/google_adk/__init__.py +19 -0
  30. agentops/instrumentation/agentic/google_adk/instrumentor.py +68 -0
  31. agentops/instrumentation/agentic/google_adk/patch.py +767 -0
  32. agentops/instrumentation/agentic/haystack/__init__.py +1 -0
  33. agentops/instrumentation/agentic/haystack/instrumentor.py +186 -0
  34. agentops/instrumentation/agentic/langgraph/__init__.py +3 -0
  35. agentops/instrumentation/agentic/langgraph/attributes.py +54 -0
  36. agentops/instrumentation/agentic/langgraph/instrumentation.py +598 -0
  37. agentops/instrumentation/agentic/langgraph/version.py +1 -0
  38. agentops/instrumentation/agentic/openai_agents/README.md +156 -0
  39. agentops/instrumentation/agentic/openai_agents/SPANS.md +145 -0
  40. agentops/instrumentation/agentic/openai_agents/TRACING_API.md +144 -0
  41. agentops/instrumentation/agentic/openai_agents/__init__.py +30 -0
  42. agentops/instrumentation/agentic/openai_agents/attributes/common.py +549 -0
  43. agentops/instrumentation/agentic/openai_agents/attributes/completion.py +172 -0
  44. agentops/instrumentation/agentic/openai_agents/attributes/model.py +58 -0
  45. agentops/instrumentation/agentic/openai_agents/attributes/tokens.py +275 -0
  46. agentops/instrumentation/agentic/openai_agents/exporter.py +469 -0
  47. agentops/instrumentation/agentic/openai_agents/instrumentor.py +107 -0
  48. agentops/instrumentation/agentic/openai_agents/processor.py +58 -0
  49. agentops/instrumentation/agentic/smolagents/README.md +88 -0
  50. agentops/instrumentation/agentic/smolagents/__init__.py +12 -0
  51. agentops/instrumentation/agentic/smolagents/attributes/agent.py +354 -0
  52. agentops/instrumentation/agentic/smolagents/attributes/model.py +205 -0
  53. agentops/instrumentation/agentic/smolagents/instrumentor.py +286 -0
  54. agentops/instrumentation/agentic/smolagents/stream_wrapper.py +258 -0
  55. agentops/instrumentation/agentic/xpander/__init__.py +15 -0
  56. agentops/instrumentation/agentic/xpander/context.py +112 -0
  57. agentops/instrumentation/agentic/xpander/instrumentor.py +877 -0
  58. agentops/instrumentation/agentic/xpander/trace_probe.py +86 -0
  59. agentops/instrumentation/agentic/xpander/version.py +3 -0
  60. agentops/instrumentation/common/README.md +65 -0
  61. agentops/instrumentation/common/attributes.py +1 -2
  62. agentops/instrumentation/providers/anthropic/__init__.py +24 -0
  63. agentops/instrumentation/providers/anthropic/attributes/__init__.py +23 -0
  64. agentops/instrumentation/providers/anthropic/attributes/common.py +64 -0
  65. agentops/instrumentation/providers/anthropic/attributes/message.py +541 -0
  66. agentops/instrumentation/providers/anthropic/attributes/tools.py +231 -0
  67. agentops/instrumentation/providers/anthropic/event_handler_wrapper.py +90 -0
  68. agentops/instrumentation/providers/anthropic/instrumentor.py +146 -0
  69. agentops/instrumentation/providers/anthropic/stream_wrapper.py +436 -0
  70. agentops/instrumentation/providers/google_genai/README.md +33 -0
  71. agentops/instrumentation/providers/google_genai/__init__.py +24 -0
  72. agentops/instrumentation/providers/google_genai/attributes/__init__.py +25 -0
  73. agentops/instrumentation/providers/google_genai/attributes/chat.py +125 -0
  74. agentops/instrumentation/providers/google_genai/attributes/common.py +88 -0
  75. agentops/instrumentation/providers/google_genai/attributes/model.py +284 -0
  76. agentops/instrumentation/providers/google_genai/instrumentor.py +170 -0
  77. agentops/instrumentation/providers/google_genai/stream_wrapper.py +238 -0
  78. agentops/instrumentation/providers/ibm_watsonx_ai/__init__.py +28 -0
  79. agentops/instrumentation/providers/ibm_watsonx_ai/attributes/__init__.py +27 -0
  80. agentops/instrumentation/providers/ibm_watsonx_ai/attributes/attributes.py +277 -0
  81. agentops/instrumentation/providers/ibm_watsonx_ai/attributes/common.py +104 -0
  82. agentops/instrumentation/providers/ibm_watsonx_ai/instrumentor.py +162 -0
  83. agentops/instrumentation/providers/ibm_watsonx_ai/stream_wrapper.py +302 -0
  84. agentops/instrumentation/providers/mem0/__init__.py +45 -0
  85. agentops/instrumentation/providers/mem0/common.py +377 -0
  86. agentops/instrumentation/providers/mem0/instrumentor.py +270 -0
  87. agentops/instrumentation/providers/mem0/memory.py +430 -0
  88. agentops/instrumentation/providers/openai/__init__.py +21 -0
  89. agentops/instrumentation/providers/openai/attributes/__init__.py +7 -0
  90. agentops/instrumentation/providers/openai/attributes/common.py +55 -0
  91. agentops/instrumentation/providers/openai/attributes/response.py +607 -0
  92. agentops/instrumentation/providers/openai/config.py +36 -0
  93. agentops/instrumentation/providers/openai/instrumentor.py +312 -0
  94. agentops/instrumentation/providers/openai/stream_wrapper.py +941 -0
  95. agentops/instrumentation/providers/openai/utils.py +44 -0
  96. agentops/instrumentation/providers/openai/v0.py +176 -0
  97. agentops/instrumentation/providers/openai/v0_wrappers.py +483 -0
  98. agentops/instrumentation/providers/openai/wrappers/__init__.py +30 -0
  99. agentops/instrumentation/providers/openai/wrappers/assistant.py +277 -0
  100. agentops/instrumentation/providers/openai/wrappers/chat.py +259 -0
  101. agentops/instrumentation/providers/openai/wrappers/completion.py +109 -0
  102. agentops/instrumentation/providers/openai/wrappers/embeddings.py +94 -0
  103. agentops/instrumentation/providers/openai/wrappers/image_gen.py +75 -0
  104. agentops/instrumentation/providers/openai/wrappers/responses.py +191 -0
  105. agentops/instrumentation/providers/openai/wrappers/shared.py +81 -0
  106. agentops/instrumentation/utilities/concurrent_futures/__init__.py +10 -0
  107. agentops/instrumentation/utilities/concurrent_futures/instrumentation.py +206 -0
  108. agentops/integration/callbacks/dspy/__init__.py +11 -0
  109. agentops/integration/callbacks/dspy/callback.py +471 -0
  110. agentops/integration/callbacks/langchain/README.md +59 -0
  111. agentops/integration/callbacks/langchain/__init__.py +15 -0
  112. agentops/integration/callbacks/langchain/callback.py +791 -0
  113. agentops/integration/callbacks/langchain/utils.py +54 -0
  114. agentops/legacy/crewai.md +121 -0
  115. agentops/logging/instrument_logging.py +4 -0
  116. agentops/sdk/README.md +220 -0
  117. agentops/sdk/core.py +75 -32
  118. agentops/sdk/descriptors/classproperty.py +28 -0
  119. agentops/sdk/exporters.py +152 -33
  120. agentops/semconv/README.md +125 -0
  121. agentops/semconv/span_kinds.py +0 -2
  122. agentops/validation.py +102 -63
  123. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/METADATA +30 -40
  124. mseep_agentops-0.4.23.dist-info/RECORD +178 -0
  125. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/WHEEL +1 -2
  126. mseep_agentops-0.4.18.dist-info/RECORD +0 -94
  127. mseep_agentops-0.4.18.dist-info/top_level.txt +0 -2
  128. tests/conftest.py +0 -10
  129. tests/unit/client/__init__.py +0 -1
  130. tests/unit/client/test_http_adapter.py +0 -221
  131. tests/unit/client/test_http_client.py +0 -206
  132. tests/unit/conftest.py +0 -54
  133. tests/unit/sdk/__init__.py +0 -1
  134. tests/unit/sdk/instrumentation_tester.py +0 -207
  135. tests/unit/sdk/test_attributes.py +0 -392
  136. tests/unit/sdk/test_concurrent_instrumentation.py +0 -468
  137. tests/unit/sdk/test_decorators.py +0 -763
  138. tests/unit/sdk/test_exporters.py +0 -241
  139. tests/unit/sdk/test_factory.py +0 -1188
  140. tests/unit/sdk/test_internal_span_processor.py +0 -397
  141. tests/unit/sdk/test_resource_attributes.py +0 -35
  142. tests/unit/test_config.py +0 -82
  143. tests/unit/test_context_manager.py +0 -777
  144. tests/unit/test_events.py +0 -27
  145. tests/unit/test_host_env.py +0 -54
  146. tests/unit/test_init_py.py +0 -501
  147. tests/unit/test_serialization.py +0 -433
  148. tests/unit/test_session.py +0 -676
  149. tests/unit/test_user_agent.py +0 -34
  150. tests/unit/test_validation.py +0 -405
  151. {tests → agentops/instrumentation/agentic/openai_agents/attributes}/__init__.py +0 -0
  152. /tests/unit/__init__.py → /agentops/instrumentation/providers/openai/attributes/tools.py +0 -0
  153. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/licenses/LICENSE +0 -0
@@ -1,34 +0,0 @@
1
- import pytest
2
- import requests
3
- from unittest.mock import patch
4
- from agentops.client.http.http_client import HttpClient
5
- from agentops.helpers.version import get_agentops_version
6
-
7
-
8
- @pytest.fixture(autouse=True)
9
- def reset_http_client_session():
10
- # Reset the cached session before each test
11
- HttpClient._session = None
12
-
13
-
14
- def test_user_agent_header():
15
- with patch("requests.Session", wraps=requests.Session):
16
- session = HttpClient.get_session()
17
- expected_version = get_agentops_version() or "unknown"
18
- expected_user_agent = f"agentops-python/{expected_version}"
19
- # Check the session's headers directly
20
- assert "User-Agent" in session.headers
21
- assert session.headers["User-Agent"] == expected_user_agent
22
-
23
-
24
- def test_user_agent_header_content():
25
- with patch("requests.Session", wraps=requests.Session):
26
- session = HttpClient.get_session()
27
- # Check the session's headers directly
28
- assert "User-Agent" in session.headers
29
- assert "Connection" in session.headers
30
- assert "Keep-Alive" in session.headers
31
- assert "Content-Type" in session.headers
32
- assert session.headers["Connection"] == "keep-alive"
33
- assert session.headers["Keep-Alive"] == "timeout=10, max=1000"
34
- assert session.headers["Content-Type"] == "application/json"
@@ -1,405 +0,0 @@
1
- """
2
- Unit tests for the AgentOps validation module.
3
- """
4
-
5
- import pytest
6
- from unittest.mock import patch, Mock
7
- import requests
8
-
9
- from agentops.validation import (
10
- get_jwt_token,
11
- get_trace_details,
12
- check_llm_spans,
13
- validate_trace_spans,
14
- ValidationError,
15
- print_validation_summary,
16
- )
17
- from agentops.exceptions import ApiServerException
18
-
19
-
20
- class TestGetJwtToken:
21
- """Test JWT token exchange functionality."""
22
-
23
- @patch("agentops.validation.requests.post")
24
- def test_get_jwt_token_success(self, mock_post):
25
- """Test successful JWT token retrieval."""
26
- mock_response = Mock()
27
- mock_response.status_code = 200
28
- mock_response.json.return_value = {"bearer": "test-token"}
29
- mock_post.return_value = mock_response
30
-
31
- token = get_jwt_token("test-api-key")
32
- assert token == "test-token"
33
-
34
- mock_post.assert_called_once_with(
35
- "https://api.agentops.ai/public/v1/auth/access_token", json={"api_key": "test-api-key"}, timeout=10
36
- )
37
-
38
- @patch("agentops.validation.requests.post")
39
- def test_get_jwt_token_failure(self, mock_post):
40
- """Test JWT token retrieval failure."""
41
- mock_response = Mock()
42
- mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Unauthorized")
43
- mock_post.return_value = mock_response
44
-
45
- with pytest.raises(ApiServerException, match="Failed to get JWT token"):
46
- get_jwt_token("invalid-api-key")
47
-
48
- @patch("os.getenv")
49
- @patch("agentops.get_client")
50
- @patch("agentops.validation.requests.post")
51
- def test_get_jwt_token_from_env(self, mock_post, mock_get_client, mock_getenv):
52
- """Test JWT token retrieval using environment variable."""
53
- mock_get_client.return_value = None
54
- mock_getenv.return_value = "env-api-key"
55
-
56
- mock_response = Mock()
57
- mock_response.status_code = 200
58
- mock_response.json.return_value = {"bearer": "env-token"}
59
- mock_post.return_value = mock_response
60
-
61
- token = get_jwt_token()
62
- assert token == "env-token"
63
-
64
- mock_getenv.assert_called_once_with("AGENTOPS_API_KEY")
65
-
66
-
67
- class TestGetTraceDetails:
68
- """Test trace details retrieval."""
69
-
70
- @patch("agentops.validation.requests.get")
71
- def test_get_trace_details_success(self, mock_get):
72
- """Test successful trace details retrieval."""
73
- mock_response = Mock()
74
- mock_response.status_code = 200
75
- mock_response.json.return_value = {"trace_id": "test-trace", "spans": [{"span_name": "test-span"}]}
76
- mock_get.return_value = mock_response
77
-
78
- details = get_trace_details("test-trace", "test-token")
79
- assert details["trace_id"] == "test-trace"
80
- assert len(details["spans"]) == 1
81
-
82
- mock_get.assert_called_once_with(
83
- "https://api.agentops.ai/public/v1/traces/test-trace",
84
- headers={"Authorization": "Bearer test-token"},
85
- timeout=10,
86
- )
87
-
88
- @patch("agentops.validation.requests.get")
89
- def test_get_trace_details_failure(self, mock_get):
90
- """Test trace details retrieval failure."""
91
- mock_response = Mock()
92
- mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")
93
- mock_get.return_value = mock_response
94
-
95
- with pytest.raises(ApiServerException, match="Failed to get trace details"):
96
- get_trace_details("invalid-trace", "test-token")
97
-
98
-
99
- class TestCheckLlmSpans:
100
- """Test LLM span checking."""
101
-
102
- def test_check_llm_spans_found(self):
103
- """Test when LLM spans are found."""
104
- spans = [
105
- {"span_name": "OpenAI Chat Completion", "span_attributes": {"agentops.span.kind": "llm"}},
106
- {"span_name": "Some other span"},
107
- {"span_name": "anthropic.messages.create", "span_attributes": {"agentops": {"span": {"kind": "llm"}}}},
108
- ]
109
-
110
- has_llm, llm_names = check_llm_spans(spans)
111
- assert has_llm is True
112
- assert len(llm_names) == 2
113
- assert "OpenAI Chat Completion" in llm_names
114
- assert "anthropic.messages.create" in llm_names
115
-
116
- def test_check_llm_spans_not_found(self):
117
- """Test when no LLM spans are found."""
118
- spans = [{"span_name": "database.query"}, {"span_name": "http.request"}]
119
-
120
- has_llm, llm_names = check_llm_spans(spans)
121
- assert has_llm is False
122
- assert len(llm_names) == 0
123
-
124
- def test_check_llm_spans_empty(self):
125
- """Test with empty spans list."""
126
- has_llm, llm_names = check_llm_spans([])
127
- assert has_llm is False
128
- assert len(llm_names) == 0
129
-
130
- def test_check_llm_spans_with_request_type(self):
131
- """Test when LLM spans are identified by LLM_REQUEST_TYPE attribute."""
132
- from agentops.semconv import SpanAttributes, LLMRequestTypeValues
133
-
134
- spans = [
135
- {
136
- "span_name": "openai.chat.completion",
137
- "span_attributes": {SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
138
- },
139
- {
140
- "span_name": "anthropic.messages.create",
141
- "span_attributes": {SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
142
- },
143
- {
144
- "span_name": "llm.completion",
145
- "span_attributes": {SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value},
146
- },
147
- {
148
- "span_name": "embedding.create",
149
- "span_attributes": {SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.EMBEDDING.value},
150
- },
151
- {"span_name": "database.query"},
152
- ]
153
-
154
- has_llm, llm_names = check_llm_spans(spans)
155
- assert has_llm is True
156
- assert len(llm_names) == 3 # Only chat and completion types count as LLM
157
- assert "openai.chat.completion" in llm_names
158
- assert "anthropic.messages.create" in llm_names
159
- assert "llm.completion" in llm_names
160
- assert "embedding.create" not in llm_names # Embeddings are not LLM spans
161
-
162
- def test_check_llm_spans_real_world(self):
163
- """Test with real-world span structures from OpenAI and Anthropic."""
164
- from agentops.semconv import SpanAttributes, LLMRequestTypeValues
165
-
166
- # This simulates what we actually get from the OpenAI and Anthropic instrumentations
167
- spans = [
168
- {
169
- "span_name": "openai.chat.completion",
170
- "span_attributes": {
171
- SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value,
172
- SpanAttributes.LLM_SYSTEM: "OpenAI",
173
- SpanAttributes.LLM_REQUEST_MODEL: "gpt-4",
174
- },
175
- },
176
- {
177
- "span_name": "anthropic.messages.create",
178
- "span_attributes": {
179
- SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value,
180
- SpanAttributes.LLM_SYSTEM: "Anthropic",
181
- SpanAttributes.LLM_REQUEST_MODEL: "claude-3-opus-20240229",
182
- },
183
- },
184
- ]
185
-
186
- has_llm, llm_names = check_llm_spans(spans)
187
- assert has_llm is True
188
- assert len(llm_names) == 2
189
- assert "openai.chat.completion" in llm_names
190
- assert "anthropic.messages.create" in llm_names
191
-
192
-
193
- class TestValidateTraceSpans:
194
- """Test the main validation function."""
195
-
196
- @patch("agentops.validation.get_jwt_token")
197
- @patch("agentops.validation.get_trace_details")
198
- @patch("agentops.validation.get_trace_metrics")
199
- def test_validate_trace_spans_success(self, mock_metrics, mock_details, mock_token):
200
- """Test successful validation."""
201
- mock_token.return_value = "test-token"
202
- mock_details.return_value = {
203
- "spans": [
204
- {"span_name": "OpenAI Chat Completion", "span_attributes": {"agentops.span.kind": "llm"}},
205
- {"span_name": "Other span"},
206
- ]
207
- }
208
- mock_metrics.return_value = {"total_tokens": 100, "total_cost": "0.0025"}
209
-
210
- result = validate_trace_spans(trace_id="test-trace")
211
-
212
- assert result["trace_id"] == "test-trace"
213
- assert result["span_count"] == 2
214
- assert result["has_llm_spans"] is True
215
- # LLM activity can be confirmed via metrics or span inspection
216
- assert result["metrics"]["total_tokens"] == 100
217
-
218
- @patch("agentops.validation.get_jwt_token")
219
- @patch("agentops.validation.get_trace_details")
220
- @patch("agentops.validation.get_trace_metrics")
221
- def test_validate_trace_spans_success_via_metrics(self, mock_metrics, mock_details, mock_token):
222
- """Test successful validation when LLM activity is confirmed via metrics."""
223
- mock_token.return_value = "test-token"
224
- mock_details.return_value = {
225
- "spans": [
226
- {
227
- "span_name": "openai.chat.completion",
228
- "span_attributes": {}, # No specific LLM attributes
229
- },
230
- {"span_name": "Other span"},
231
- ]
232
- }
233
- # But we have token usage, proving LLM activity
234
- mock_metrics.return_value = {"total_tokens": 1066, "total_cost": "0.0006077"}
235
-
236
- result = validate_trace_spans(trace_id="test-trace")
237
-
238
- assert result["trace_id"] == "test-trace"
239
- assert result["span_count"] == 2
240
- assert result["has_llm_spans"] is True # Confirmed via metrics
241
- assert result["metrics"]["total_tokens"] == 1066
242
-
243
- @patch("agentops.validation.get_jwt_token")
244
- @patch("agentops.validation.get_trace_details")
245
- @patch("agentops.validation.get_trace_metrics")
246
- def test_validate_trace_spans_no_llm(self, mock_metrics, mock_details, mock_token):
247
- """Test validation failure when no LLM spans found and no token usage."""
248
- mock_token.return_value = "test-token"
249
- mock_details.return_value = {"spans": [{"span_name": "database.query"}]}
250
- # No token usage either
251
- mock_metrics.return_value = {"total_tokens": 0, "total_cost": "0.0000"}
252
-
253
- with pytest.raises(ValidationError, match="No LLM activity detected"):
254
- validate_trace_spans(trace_id="test-trace", check_llm=True)
255
-
256
- @patch("agentops.validation.get_jwt_token")
257
- @patch("agentops.validation.get_trace_details")
258
- @patch("agentops.validation.get_trace_metrics")
259
- def test_validate_trace_spans_retry(self, mock_metrics, mock_details, mock_token):
260
- """Test validation with retries."""
261
- mock_token.return_value = "test-token"
262
-
263
- # First two calls return empty, third returns spans
264
- mock_details.side_effect = [
265
- {"spans": []},
266
- {"spans": []},
267
- {"spans": [{"span_name": "OpenAI Chat Completion", "span_attributes": {"agentops.span.kind": "llm"}}]},
268
- ]
269
-
270
- # Mock metrics for the successful attempt
271
- mock_metrics.return_value = {"total_tokens": 100, "total_cost": "0.0025"}
272
-
273
- result = validate_trace_spans(trace_id="test-trace", max_retries=3, retry_delay=0.01)
274
-
275
- assert result["span_count"] == 1
276
- assert mock_details.call_count == 3
277
-
278
- @patch("opentelemetry.trace.get_current_span")
279
- def test_validate_trace_spans_no_trace_id(self, mock_get_current_span):
280
- """Test validation without trace ID."""
281
- # Mock get_current_span to return None
282
- mock_get_current_span.return_value = None
283
-
284
- with pytest.raises(ValueError, match="No trace ID found"):
285
- validate_trace_spans()
286
-
287
- @patch("opentelemetry.trace.get_current_span")
288
- @patch("agentops.validation.get_jwt_token")
289
- @patch("agentops.validation.get_trace_details")
290
- @patch("agentops.validation.get_trace_metrics")
291
- def test_validate_trace_spans_from_current_span(self, mock_metrics, mock_details, mock_token, mock_get_span):
292
- """Test extracting trace ID from current span."""
293
- # Mock the current span
294
- mock_span_context = Mock()
295
- mock_span_context.trace_id = 12345678901234567890
296
-
297
- mock_span = Mock()
298
- mock_span.get_span_context.return_value = mock_span_context
299
-
300
- mock_get_span.return_value = mock_span
301
-
302
- mock_token.return_value = "test-token"
303
- mock_details.return_value = {
304
- "spans": [{"span_name": "OpenAI Chat Completion", "span_attributes": {"agentops.span.kind": "llm"}}]
305
- }
306
- mock_metrics.return_value = {"total_tokens": 100, "total_cost": "0.0025"}
307
-
308
- result = validate_trace_spans()
309
- assert result["trace_id"] == "0000000000000000ab54a98ceb1f0ad2" # hex format of trace ID
310
-
311
-
312
- class TestPrintValidationSummary:
313
- """Test validation summary printing."""
314
-
315
- def test_print_validation_summary(self, capsys):
316
- """Test printing validation summary."""
317
- result = {
318
- "span_count": 3,
319
- "has_llm_spans": True,
320
- "llm_span_names": ["OpenAI Chat", "Claude Message"],
321
- "metrics": {"total_tokens": 150, "prompt_tokens": 100, "completion_tokens": 50, "total_cost": "0.0030"},
322
- }
323
-
324
- print_validation_summary(result)
325
-
326
- captured = capsys.readouterr()
327
- assert "Found 3 span(s)" in captured.out
328
- assert "OpenAI Chat" in captured.out
329
- assert "Total tokens: 150" in captured.out
330
- assert "Total cost: $0.0030" in captured.out
331
- assert "✅ Validation successful!" in captured.out
332
-
333
- def test_print_validation_summary_metrics_only(self, capsys):
334
- """Test printing validation summary when LLM activity confirmed via metrics only."""
335
- result = {
336
- "span_count": 2,
337
- "has_llm_spans": True,
338
- "llm_span_names": [], # No specific LLM span names found
339
- "metrics": {
340
- "total_tokens": 1066,
341
- "prompt_tokens": 800,
342
- "completion_tokens": 266,
343
- "total_cost": "0.0006077",
344
- },
345
- }
346
-
347
- print_validation_summary(result)
348
-
349
- captured = capsys.readouterr()
350
- assert "Found 2 span(s)" in captured.out
351
- assert "LLM activity confirmed via token usage metrics" in captured.out
352
- assert "Total tokens: 1066" in captured.out
353
- assert "Total cost: $0.0006077" in captured.out
354
- assert "✅ Validation successful!" in captured.out
355
-
356
- def test_print_validation_summary_llm_prefix(self, capsys):
357
- """Test with spans using llm.* prefix (as returned by API)."""
358
- result = {
359
- "span_count": 1,
360
- "has_llm_spans": True,
361
- "llm_span_names": ["openai.chat.completion"],
362
- "metrics": {"total_tokens": 150, "prompt_tokens": 100, "completion_tokens": 50, "total_cost": "0.0030"},
363
- }
364
-
365
- print_validation_summary(result)
366
-
367
- captured = capsys.readouterr()
368
- assert "Found 1 span(s)" in captured.out
369
- assert "openai.chat.completion" in captured.out
370
- assert "✅ Validation successful!" in captured.out
371
-
372
-
373
- class TestCheckLlmSpansWithLlmPrefix:
374
- """Test LLM span checking with llm.* prefix attributes."""
375
-
376
- def test_check_llm_spans_with_llm_prefix(self):
377
- """Test when spans use llm.request.type instead of gen_ai.request.type."""
378
- spans = [
379
- {
380
- "span_name": "openai.chat.completion",
381
- "span_attributes": {
382
- "llm.request.type": "chat",
383
- "llm.system": "OpenAI",
384
- "llm.request.model": "gpt-4",
385
- "llm.usage.total_tokens": 150,
386
- },
387
- },
388
- {
389
- "span_name": "anthropic.messages.create",
390
- "span_attributes": {
391
- "llm.request.type": "chat",
392
- "llm.system": "Anthropic",
393
- "llm.request.model": "claude-3-opus",
394
- "llm.usage.total_tokens": 300,
395
- },
396
- },
397
- {"span_name": "embedding.create", "span_attributes": {"llm.request.type": "embedding"}},
398
- {"span_name": "database.query"},
399
- ]
400
-
401
- has_llm, llm_names = check_llm_spans(spans)
402
- assert has_llm is True
403
- assert len(llm_names) == 2 # Only chat types
404
- assert "openai.chat.completion" in llm_names
405
- assert "anthropic.messages.create" in llm_names