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.
- agentops/__init__.py +0 -0
- agentops/client/api/base.py +28 -30
- agentops/client/api/versions/v3.py +29 -25
- agentops/client/api/versions/v4.py +87 -46
- agentops/client/client.py +98 -29
- agentops/client/http/README.md +87 -0
- agentops/client/http/http_client.py +126 -172
- agentops/config.py +8 -2
- agentops/instrumentation/OpenTelemetry.md +133 -0
- agentops/instrumentation/README.md +167 -0
- agentops/instrumentation/__init__.py +13 -1
- agentops/instrumentation/agentic/ag2/__init__.py +18 -0
- agentops/instrumentation/agentic/ag2/instrumentor.py +922 -0
- agentops/instrumentation/agentic/agno/__init__.py +19 -0
- agentops/instrumentation/agentic/agno/attributes/__init__.py +20 -0
- agentops/instrumentation/agentic/agno/attributes/agent.py +250 -0
- agentops/instrumentation/agentic/agno/attributes/metrics.py +214 -0
- agentops/instrumentation/agentic/agno/attributes/storage.py +158 -0
- agentops/instrumentation/agentic/agno/attributes/team.py +195 -0
- agentops/instrumentation/agentic/agno/attributes/tool.py +210 -0
- agentops/instrumentation/agentic/agno/attributes/workflow.py +254 -0
- agentops/instrumentation/agentic/agno/instrumentor.py +1313 -0
- agentops/instrumentation/agentic/crewai/LICENSE +201 -0
- agentops/instrumentation/agentic/crewai/NOTICE.md +10 -0
- agentops/instrumentation/agentic/crewai/__init__.py +6 -0
- agentops/instrumentation/agentic/crewai/crewai_span_attributes.py +335 -0
- agentops/instrumentation/agentic/crewai/instrumentation.py +535 -0
- agentops/instrumentation/agentic/crewai/version.py +1 -0
- agentops/instrumentation/agentic/google_adk/__init__.py +19 -0
- agentops/instrumentation/agentic/google_adk/instrumentor.py +68 -0
- agentops/instrumentation/agentic/google_adk/patch.py +767 -0
- agentops/instrumentation/agentic/haystack/__init__.py +1 -0
- agentops/instrumentation/agentic/haystack/instrumentor.py +186 -0
- agentops/instrumentation/agentic/langgraph/__init__.py +3 -0
- agentops/instrumentation/agentic/langgraph/attributes.py +54 -0
- agentops/instrumentation/agentic/langgraph/instrumentation.py +598 -0
- agentops/instrumentation/agentic/langgraph/version.py +1 -0
- agentops/instrumentation/agentic/openai_agents/README.md +156 -0
- agentops/instrumentation/agentic/openai_agents/SPANS.md +145 -0
- agentops/instrumentation/agentic/openai_agents/TRACING_API.md +144 -0
- agentops/instrumentation/agentic/openai_agents/__init__.py +30 -0
- agentops/instrumentation/agentic/openai_agents/attributes/common.py +549 -0
- agentops/instrumentation/agentic/openai_agents/attributes/completion.py +172 -0
- agentops/instrumentation/agentic/openai_agents/attributes/model.py +58 -0
- agentops/instrumentation/agentic/openai_agents/attributes/tokens.py +275 -0
- agentops/instrumentation/agentic/openai_agents/exporter.py +469 -0
- agentops/instrumentation/agentic/openai_agents/instrumentor.py +107 -0
- agentops/instrumentation/agentic/openai_agents/processor.py +58 -0
- agentops/instrumentation/agentic/smolagents/README.md +88 -0
- agentops/instrumentation/agentic/smolagents/__init__.py +12 -0
- agentops/instrumentation/agentic/smolagents/attributes/agent.py +354 -0
- agentops/instrumentation/agentic/smolagents/attributes/model.py +205 -0
- agentops/instrumentation/agentic/smolagents/instrumentor.py +286 -0
- agentops/instrumentation/agentic/smolagents/stream_wrapper.py +258 -0
- agentops/instrumentation/agentic/xpander/__init__.py +15 -0
- agentops/instrumentation/agentic/xpander/context.py +112 -0
- agentops/instrumentation/agentic/xpander/instrumentor.py +877 -0
- agentops/instrumentation/agentic/xpander/trace_probe.py +86 -0
- agentops/instrumentation/agentic/xpander/version.py +3 -0
- agentops/instrumentation/common/README.md +65 -0
- agentops/instrumentation/common/attributes.py +1 -2
- agentops/instrumentation/providers/anthropic/__init__.py +24 -0
- agentops/instrumentation/providers/anthropic/attributes/__init__.py +23 -0
- agentops/instrumentation/providers/anthropic/attributes/common.py +64 -0
- agentops/instrumentation/providers/anthropic/attributes/message.py +541 -0
- agentops/instrumentation/providers/anthropic/attributes/tools.py +231 -0
- agentops/instrumentation/providers/anthropic/event_handler_wrapper.py +90 -0
- agentops/instrumentation/providers/anthropic/instrumentor.py +146 -0
- agentops/instrumentation/providers/anthropic/stream_wrapper.py +436 -0
- agentops/instrumentation/providers/google_genai/README.md +33 -0
- agentops/instrumentation/providers/google_genai/__init__.py +24 -0
- agentops/instrumentation/providers/google_genai/attributes/__init__.py +25 -0
- agentops/instrumentation/providers/google_genai/attributes/chat.py +125 -0
- agentops/instrumentation/providers/google_genai/attributes/common.py +88 -0
- agentops/instrumentation/providers/google_genai/attributes/model.py +284 -0
- agentops/instrumentation/providers/google_genai/instrumentor.py +170 -0
- agentops/instrumentation/providers/google_genai/stream_wrapper.py +238 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/__init__.py +28 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/__init__.py +27 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/attributes.py +277 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/attributes/common.py +104 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/instrumentor.py +162 -0
- agentops/instrumentation/providers/ibm_watsonx_ai/stream_wrapper.py +302 -0
- agentops/instrumentation/providers/mem0/__init__.py +45 -0
- agentops/instrumentation/providers/mem0/common.py +377 -0
- agentops/instrumentation/providers/mem0/instrumentor.py +270 -0
- agentops/instrumentation/providers/mem0/memory.py +430 -0
- agentops/instrumentation/providers/openai/__init__.py +21 -0
- agentops/instrumentation/providers/openai/attributes/__init__.py +7 -0
- agentops/instrumentation/providers/openai/attributes/common.py +55 -0
- agentops/instrumentation/providers/openai/attributes/response.py +607 -0
- agentops/instrumentation/providers/openai/config.py +36 -0
- agentops/instrumentation/providers/openai/instrumentor.py +312 -0
- agentops/instrumentation/providers/openai/stream_wrapper.py +941 -0
- agentops/instrumentation/providers/openai/utils.py +44 -0
- agentops/instrumentation/providers/openai/v0.py +176 -0
- agentops/instrumentation/providers/openai/v0_wrappers.py +483 -0
- agentops/instrumentation/providers/openai/wrappers/__init__.py +30 -0
- agentops/instrumentation/providers/openai/wrappers/assistant.py +277 -0
- agentops/instrumentation/providers/openai/wrappers/chat.py +259 -0
- agentops/instrumentation/providers/openai/wrappers/completion.py +109 -0
- agentops/instrumentation/providers/openai/wrappers/embeddings.py +94 -0
- agentops/instrumentation/providers/openai/wrappers/image_gen.py +75 -0
- agentops/instrumentation/providers/openai/wrappers/responses.py +191 -0
- agentops/instrumentation/providers/openai/wrappers/shared.py +81 -0
- agentops/instrumentation/utilities/concurrent_futures/__init__.py +10 -0
- agentops/instrumentation/utilities/concurrent_futures/instrumentation.py +206 -0
- agentops/integration/callbacks/dspy/__init__.py +11 -0
- agentops/integration/callbacks/dspy/callback.py +471 -0
- agentops/integration/callbacks/langchain/README.md +59 -0
- agentops/integration/callbacks/langchain/__init__.py +15 -0
- agentops/integration/callbacks/langchain/callback.py +791 -0
- agentops/integration/callbacks/langchain/utils.py +54 -0
- agentops/legacy/crewai.md +121 -0
- agentops/logging/instrument_logging.py +4 -0
- agentops/sdk/README.md +220 -0
- agentops/sdk/core.py +75 -32
- agentops/sdk/descriptors/classproperty.py +28 -0
- agentops/sdk/exporters.py +152 -33
- agentops/semconv/README.md +125 -0
- agentops/semconv/span_kinds.py +0 -2
- agentops/validation.py +102 -63
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/METADATA +30 -40
- mseep_agentops-0.4.23.dist-info/RECORD +178 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/WHEEL +1 -2
- mseep_agentops-0.4.18.dist-info/RECORD +0 -94
- mseep_agentops-0.4.18.dist-info/top_level.txt +0 -2
- tests/conftest.py +0 -10
- tests/unit/client/__init__.py +0 -1
- tests/unit/client/test_http_adapter.py +0 -221
- tests/unit/client/test_http_client.py +0 -206
- tests/unit/conftest.py +0 -54
- tests/unit/sdk/__init__.py +0 -1
- tests/unit/sdk/instrumentation_tester.py +0 -207
- tests/unit/sdk/test_attributes.py +0 -392
- tests/unit/sdk/test_concurrent_instrumentation.py +0 -468
- tests/unit/sdk/test_decorators.py +0 -763
- tests/unit/sdk/test_exporters.py +0 -241
- tests/unit/sdk/test_factory.py +0 -1188
- tests/unit/sdk/test_internal_span_processor.py +0 -397
- tests/unit/sdk/test_resource_attributes.py +0 -35
- tests/unit/test_config.py +0 -82
- tests/unit/test_context_manager.py +0 -777
- tests/unit/test_events.py +0 -27
- tests/unit/test_host_env.py +0 -54
- tests/unit/test_init_py.py +0 -501
- tests/unit/test_serialization.py +0 -433
- tests/unit/test_session.py +0 -676
- tests/unit/test_user_agent.py +0 -34
- tests/unit/test_validation.py +0 -405
- {tests → agentops/instrumentation/agentic/openai_agents/attributes}/__init__.py +0 -0
- /tests/unit/__init__.py → /agentops/instrumentation/providers/openai/attributes/tools.py +0 -0
- {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.23.dist-info}/licenses/LICENSE +0 -0
tests/unit/test_user_agent.py
DELETED
@@ -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"
|
tests/unit/test_validation.py
DELETED
@@ -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
|
File without changes
|
File without changes
|
File without changes
|