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
agentops/sdk/exporters.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# Define a separate class for the authenticated OTLP exporter
|
2
2
|
# This is imported conditionally to avoid dependency issues
|
3
|
-
|
3
|
+
import threading
|
4
|
+
from typing import Callable, Dict, Optional, Sequence
|
5
|
+
import time
|
4
6
|
|
5
7
|
import requests
|
6
|
-
from opentelemetry.exporter.otlp.proto.http import Compression
|
7
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
8
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter, Compression
|
8
9
|
from opentelemetry.sdk.trace import ReadableSpan
|
9
10
|
from opentelemetry.sdk.trace.export import SpanExportResult
|
10
11
|
|
@@ -14,64 +15,182 @@ from agentops.logging import logger
|
|
14
15
|
|
15
16
|
class AuthenticatedOTLPExporter(OTLPSpanExporter):
|
16
17
|
"""
|
17
|
-
OTLP exporter with JWT authentication support.
|
18
|
+
OTLP exporter with dynamic JWT authentication support.
|
18
19
|
|
19
|
-
This exporter
|
20
|
-
|
21
|
-
|
20
|
+
This exporter allows for updating JWT tokens dynamically without recreating
|
21
|
+
the exporter. It maintains a reference to a JWT token that can be updated
|
22
|
+
by external code, and automatically includes the latest token in requests.
|
22
23
|
"""
|
23
24
|
|
24
25
|
def __init__(
|
25
26
|
self,
|
26
27
|
endpoint: str,
|
27
|
-
jwt: str,
|
28
|
+
jwt: Optional[str] = None,
|
29
|
+
jwt_provider: Optional[Callable[[], Optional[str]]] = None,
|
28
30
|
headers: Optional[Dict[str, str]] = None,
|
29
31
|
timeout: Optional[int] = None,
|
30
32
|
compression: Optional[Compression] = None,
|
31
33
|
**kwargs,
|
32
34
|
):
|
33
|
-
|
34
|
-
|
35
|
-
# self._session = HttpClient.get_authenticated_session(endpoint, api_key)
|
36
|
-
|
37
|
-
# Initialize the parent class
|
38
|
-
super().__init__(
|
39
|
-
endpoint=endpoint,
|
40
|
-
headers={
|
41
|
-
"Authorization": f"Bearer {jwt}",
|
42
|
-
}, # Base headers
|
43
|
-
timeout=timeout,
|
44
|
-
compression=compression,
|
45
|
-
# session=self._session, # Use our authenticated session
|
46
|
-
)
|
35
|
+
"""
|
36
|
+
Initialize the authenticated OTLP exporter.
|
47
37
|
|
48
|
-
|
38
|
+
Args:
|
39
|
+
endpoint: The OTLP endpoint URL
|
40
|
+
jwt: Initial JWT token (optional)
|
41
|
+
jwt_provider: Function to get JWT token dynamically (optional)
|
42
|
+
headers: Additional headers to include
|
43
|
+
timeout: Request timeout
|
44
|
+
compression: Compression type
|
45
|
+
**kwargs: Additional arguments (stored but not passed to parent)
|
49
46
|
"""
|
50
|
-
|
47
|
+
# Store JWT-related parameters separately
|
48
|
+
self._jwt = jwt
|
49
|
+
self._jwt_provider = jwt_provider
|
50
|
+
self._lock = threading.Lock()
|
51
|
+
self._last_auth_failure = 0
|
52
|
+
self._auth_failure_threshold = 60 # Don't retry auth failures more than once per minute
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
+
# Store any additional kwargs for potential future use
|
55
|
+
self._custom_kwargs = kwargs
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
+
# Filter headers to prevent override of critical headers
|
58
|
+
filtered_headers = self._filter_user_headers(headers) if headers else None
|
59
|
+
|
60
|
+
# Initialize parent with only known parameters
|
61
|
+
parent_kwargs = {}
|
62
|
+
if filtered_headers is not None:
|
63
|
+
parent_kwargs["headers"] = filtered_headers
|
64
|
+
if timeout is not None:
|
65
|
+
parent_kwargs["timeout"] = timeout
|
66
|
+
if compression is not None:
|
67
|
+
parent_kwargs["compression"] = compression
|
68
|
+
|
69
|
+
super().__init__(endpoint=endpoint, **parent_kwargs)
|
70
|
+
|
71
|
+
def _get_current_jwt(self) -> Optional[str]:
|
72
|
+
"""Get the current JWT token from the provider or stored JWT."""
|
73
|
+
if self._jwt_provider:
|
74
|
+
try:
|
75
|
+
return self._jwt_provider()
|
76
|
+
except Exception as e:
|
77
|
+
logger.warning(f"Failed to get JWT token: {e}")
|
78
|
+
return self._jwt
|
79
|
+
|
80
|
+
def _filter_user_headers(self, headers: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
|
81
|
+
"""Filter user-supplied headers to prevent override of critical headers."""
|
82
|
+
if not headers:
|
83
|
+
return None
|
57
84
|
|
58
|
-
|
59
|
-
|
85
|
+
# Define critical headers that cannot be overridden by user-supplied headers
|
86
|
+
PROTECTED_HEADERS = {
|
87
|
+
"authorization",
|
88
|
+
"content-type",
|
89
|
+
"user-agent",
|
90
|
+
"x-api-key",
|
91
|
+
"api-key",
|
92
|
+
"bearer",
|
93
|
+
"x-auth-token",
|
94
|
+
"x-session-token",
|
95
|
+
}
|
96
|
+
|
97
|
+
filtered_headers = {}
|
98
|
+
for key, value in headers.items():
|
99
|
+
if key.lower() not in PROTECTED_HEADERS:
|
100
|
+
filtered_headers[key] = value
|
101
|
+
|
102
|
+
return filtered_headers if filtered_headers else None
|
103
|
+
|
104
|
+
def _prepare_headers(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
105
|
+
"""Prepare headers with current JWT token."""
|
106
|
+
# Start with base headers
|
107
|
+
prepared_headers = dict(self._headers)
|
108
|
+
|
109
|
+
# Add any additional headers, but only allow non-critical headers
|
110
|
+
filtered_headers = self._filter_user_headers(headers)
|
111
|
+
if filtered_headers:
|
112
|
+
prepared_headers.update(filtered_headers)
|
113
|
+
|
114
|
+
# Add current JWT token if available (this ensures Authorization cannot be overridden)
|
115
|
+
jwt_token = self._get_current_jwt()
|
116
|
+
if jwt_token:
|
117
|
+
prepared_headers["Authorization"] = f"Bearer {jwt_token}"
|
118
|
+
|
119
|
+
return prepared_headers
|
120
|
+
|
121
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
60
122
|
"""
|
123
|
+
Export spans with dynamic JWT authentication.
|
124
|
+
|
125
|
+
This method overrides the parent's export to ensure we always use
|
126
|
+
the latest JWT token and handle authentication failures gracefully.
|
127
|
+
"""
|
128
|
+
# Check if we should skip due to recent auth failure
|
129
|
+
with self._lock:
|
130
|
+
current_time = time.time()
|
131
|
+
if self._last_auth_failure > 0 and current_time - self._last_auth_failure < self._auth_failure_threshold:
|
132
|
+
logger.debug("Skipping export due to recent authentication failure")
|
133
|
+
return SpanExportResult.FAILURE
|
134
|
+
|
61
135
|
try:
|
62
|
-
|
136
|
+
# Get current JWT and prepare headers
|
137
|
+
current_headers = self._prepare_headers()
|
138
|
+
|
139
|
+
# Temporarily update the session headers for this request
|
140
|
+
original_headers = dict(self._session.headers)
|
141
|
+
self._session.headers.update(current_headers)
|
142
|
+
|
143
|
+
try:
|
144
|
+
# Call parent export method
|
145
|
+
result = super().export(spans)
|
146
|
+
|
147
|
+
# Reset auth failure timestamp on success
|
148
|
+
if result == SpanExportResult.SUCCESS:
|
149
|
+
with self._lock:
|
150
|
+
self._last_auth_failure = 0
|
151
|
+
|
152
|
+
return result
|
153
|
+
|
154
|
+
finally:
|
155
|
+
# Restore original headers
|
156
|
+
self._session.headers.clear()
|
157
|
+
self._session.headers.update(original_headers)
|
158
|
+
|
159
|
+
except requests.exceptions.HTTPError as e:
|
160
|
+
if e.response and e.response.status_code in (401, 403):
|
161
|
+
# Authentication error - record timestamp and warn
|
162
|
+
with self._lock:
|
163
|
+
self._last_auth_failure = time.time()
|
164
|
+
|
165
|
+
logger.warning(
|
166
|
+
f"Authentication failed during span export: {e}. "
|
167
|
+
f"Will retry in {self._auth_failure_threshold} seconds."
|
168
|
+
)
|
169
|
+
return SpanExportResult.FAILURE
|
170
|
+
else:
|
171
|
+
logger.error(f"HTTP error during span export: {e}")
|
172
|
+
return SpanExportResult.FAILURE
|
173
|
+
|
63
174
|
except AgentOpsApiJwtExpiredException as e:
|
64
|
-
#
|
65
|
-
|
175
|
+
# JWT expired - record timestamp and warn
|
176
|
+
with self._lock:
|
177
|
+
self._last_auth_failure = time.time()
|
178
|
+
|
179
|
+
logger.warning(
|
180
|
+
f"JWT token expired during span export: {e}. Will retry in {self._auth_failure_threshold} seconds."
|
181
|
+
)
|
66
182
|
return SpanExportResult.FAILURE
|
183
|
+
|
67
184
|
except ApiServerException as e:
|
68
185
|
# Server-side error
|
69
186
|
logger.error(f"API server error during span export: {e}")
|
70
187
|
return SpanExportResult.FAILURE
|
188
|
+
|
71
189
|
except requests.RequestException as e:
|
72
190
|
# Network or HTTP error
|
73
191
|
logger.error(f"Network error during span export: {e}")
|
74
192
|
return SpanExportResult.FAILURE
|
193
|
+
|
75
194
|
except Exception as e:
|
76
195
|
# Any other error
|
77
196
|
logger.error(f"Unexpected error during span export: {e}")
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# OpenTelemetry Semantic Conventions for Generative AI Systems
|
2
|
+
|
3
|
+
This module provides semantic conventions for telemetry data in AI and LLM systems, following OpenTelemetry GenAI conventions where applicable.
|
4
|
+
|
5
|
+
## Core Conventions
|
6
|
+
|
7
|
+
### Agent Attributes (`agent.py`)
|
8
|
+
```python
|
9
|
+
from agentops.semconv import AgentAttributes
|
10
|
+
|
11
|
+
AgentAttributes.AGENT_NAME # Agent name
|
12
|
+
AgentAttributes.AGENT_ROLE # Agent role/type
|
13
|
+
AgentAttributes.AGENT_ID # Unique agent identifier
|
14
|
+
```
|
15
|
+
|
16
|
+
### Tool Attributes (`tool.py`)
|
17
|
+
```python
|
18
|
+
from agentops.semconv import ToolAttributes, ToolStatus
|
19
|
+
|
20
|
+
ToolAttributes.TOOL_NAME # Tool name
|
21
|
+
ToolAttributes.TOOL_PARAMETERS # Tool input parameters
|
22
|
+
ToolAttributes.TOOL_RESULT # Tool execution result
|
23
|
+
ToolAttributes.TOOL_STATUS # Tool execution status
|
24
|
+
|
25
|
+
# Tool status values
|
26
|
+
ToolStatus.EXECUTING # Tool is executing
|
27
|
+
ToolStatus.SUCCEEDED # Tool completed successfully
|
28
|
+
ToolStatus.FAILED # Tool execution failed
|
29
|
+
```
|
30
|
+
|
31
|
+
### Workflow Attributes (`workflow.py`)
|
32
|
+
```python
|
33
|
+
from agentops.semconv import WorkflowAttributes
|
34
|
+
|
35
|
+
WorkflowAttributes.WORKFLOW_NAME # Workflow name
|
36
|
+
WorkflowAttributes.WORKFLOW_TYPE # Workflow type
|
37
|
+
WorkflowAttributes.WORKFLOW_STEP_NAME # Step name
|
38
|
+
WorkflowAttributes.WORKFLOW_STEP_STATUS # Step status
|
39
|
+
```
|
40
|
+
|
41
|
+
### LLM/GenAI Attributes (`span_attributes.py`)
|
42
|
+
Following OpenTelemetry GenAI conventions:
|
43
|
+
|
44
|
+
```python
|
45
|
+
from agentops.semconv import SpanAttributes
|
46
|
+
|
47
|
+
# Request attributes
|
48
|
+
SpanAttributes.LLM_REQUEST_MODEL # Model name (e.g., "gpt-4")
|
49
|
+
SpanAttributes.LLM_REQUEST_TEMPERATURE # Temperature setting
|
50
|
+
SpanAttributes.LLM_REQUEST_MAX_TOKENS # Max tokens to generate
|
51
|
+
|
52
|
+
# Response attributes
|
53
|
+
SpanAttributes.LLM_RESPONSE_MODEL # Model that generated response
|
54
|
+
SpanAttributes.LLM_RESPONSE_FINISH_REASON # Why generation stopped
|
55
|
+
|
56
|
+
# Token usage
|
57
|
+
SpanAttributes.LLM_USAGE_PROMPT_TOKENS # Input tokens
|
58
|
+
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS # Output tokens
|
59
|
+
SpanAttributes.LLM_USAGE_TOTAL_TOKENS # Total tokens
|
60
|
+
```
|
61
|
+
|
62
|
+
### Message Attributes (`message.py`)
|
63
|
+
For chat-based interactions:
|
64
|
+
|
65
|
+
```python
|
66
|
+
from agentops.semconv import MessageAttributes
|
67
|
+
|
68
|
+
# Prompt messages (indexed)
|
69
|
+
MessageAttributes.PROMPT_ROLE.format(i=0) # Role at index 0
|
70
|
+
MessageAttributes.PROMPT_CONTENT.format(i=0) # Content at index 0
|
71
|
+
|
72
|
+
# Completion messages (indexed)
|
73
|
+
MessageAttributes.COMPLETION_ROLE.format(i=0) # Role at index 0
|
74
|
+
MessageAttributes.COMPLETION_CONTENT.format(i=0) # Content at index 0
|
75
|
+
|
76
|
+
# Tool calls (indexed)
|
77
|
+
MessageAttributes.TOOL_CALL_NAME.format(i=0) # Tool name
|
78
|
+
MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0) # Tool arguments
|
79
|
+
```
|
80
|
+
|
81
|
+
### Core Attributes (`core.py`)
|
82
|
+
```python
|
83
|
+
from agentops.semconv import CoreAttributes
|
84
|
+
|
85
|
+
CoreAttributes.TRACE_ID # Trace identifier
|
86
|
+
CoreAttributes.SPAN_ID # Span identifier
|
87
|
+
CoreAttributes.PARENT_ID # Parent span identifier
|
88
|
+
CoreAttributes.TAGS # User-defined tags
|
89
|
+
```
|
90
|
+
|
91
|
+
## Usage Guidelines
|
92
|
+
|
93
|
+
1. **Follow OpenTelemetry conventions** - Use `gen_ai.*` prefixed attributes for LLM operations
|
94
|
+
2. **Use indexed attributes for collections** - Messages, tool calls, etc. should use `.format(i=index)`
|
95
|
+
3. **Prefer specific over generic** - Use `SpanAttributes.LLM_REQUEST_MODEL` over custom attributes
|
96
|
+
4. **Document custom attributes** - If you need provider-specific attributes, document them clearly
|
97
|
+
|
98
|
+
## Provider-Specific Conventions
|
99
|
+
|
100
|
+
### OpenAI
|
101
|
+
- `SpanAttributes.LLM_OPENAI_RESPONSE_SYSTEM_FINGERPRINT`
|
102
|
+
- `SpanAttributes.LLM_OPENAI_API_VERSION`
|
103
|
+
|
104
|
+
### LangChain
|
105
|
+
- `LangChainAttributes.CHAIN_TYPE`
|
106
|
+
- `LangChainAttributes.TOOL_NAME`
|
107
|
+
|
108
|
+
## Metrics (`meters.py`)
|
109
|
+
|
110
|
+
Standard metrics for instrumentation:
|
111
|
+
|
112
|
+
```python
|
113
|
+
from agentops.semconv import Meters
|
114
|
+
|
115
|
+
Meters.LLM_TOKEN_USAGE # Token usage histogram
|
116
|
+
Meters.LLM_OPERATION_DURATION # Operation duration histogram
|
117
|
+
Meters.LLM_COMPLETIONS_EXCEPTIONS # Exception counter
|
118
|
+
```
|
119
|
+
|
120
|
+
## Best Practices
|
121
|
+
|
122
|
+
1. **Consistency** - Use the same attributes across instrumentations
|
123
|
+
2. **Completeness** - Capture essential attributes for debugging
|
124
|
+
3. **Performance** - Avoid capturing large payloads as attributes
|
125
|
+
4. **Privacy** - Be mindful of sensitive data in attributes
|
agentops/semconv/span_kinds.py
CHANGED
@@ -13,7 +13,6 @@ class AgentOpsSpanKindValues(Enum):
|
|
13
13
|
AGENT = "agent"
|
14
14
|
TOOL = "tool"
|
15
15
|
LLM = "llm"
|
16
|
-
TEAM = "team"
|
17
16
|
CHAIN = "chain"
|
18
17
|
TEXT = "text"
|
19
18
|
GUARDRAIL = "guardrail"
|
@@ -42,7 +41,6 @@ class SpanKind:
|
|
42
41
|
AGENT = AgentOpsSpanKindValues.AGENT.value
|
43
42
|
TOOL = AgentOpsSpanKindValues.TOOL.value
|
44
43
|
LLM = AgentOpsSpanKindValues.LLM.value
|
45
|
-
TEAM = AgentOpsSpanKindValues.TEAM.value
|
46
44
|
UNKNOWN = AgentOpsSpanKindValues.UNKNOWN.value
|
47
45
|
CHAIN = AgentOpsSpanKindValues.CHAIN.value
|
48
46
|
TEXT = AgentOpsSpanKindValues.TEXT.value
|
agentops/validation.py
CHANGED
@@ -5,12 +5,15 @@ This module provides functions to validate that spans have been sent to AgentOps
|
|
5
5
|
using the public API. This is useful for testing and verification purposes.
|
6
6
|
"""
|
7
7
|
|
8
|
+
import asyncio
|
9
|
+
import os
|
8
10
|
import time
|
9
|
-
import requests
|
10
11
|
from typing import Optional, Dict, List, Any, Tuple
|
11
12
|
|
12
|
-
|
13
|
+
import requests
|
14
|
+
|
13
15
|
from agentops.exceptions import ApiServerException
|
16
|
+
from agentops.logging import logger
|
14
17
|
|
15
18
|
|
16
19
|
class ValidationError(Exception):
|
@@ -19,40 +22,84 @@ class ValidationError(Exception):
|
|
19
22
|
pass
|
20
23
|
|
21
24
|
|
22
|
-
def get_jwt_token(api_key: Optional[str] = None) -> str:
|
25
|
+
async def get_jwt_token(api_key: Optional[str] = None) -> str:
|
23
26
|
"""
|
24
|
-
Exchange API key for JWT token.
|
27
|
+
Exchange API key for JWT token asynchronously.
|
25
28
|
|
26
29
|
Args:
|
27
30
|
api_key: Optional API key. If not provided, uses AGENTOPS_API_KEY env var.
|
28
31
|
|
29
32
|
Returns:
|
30
|
-
JWT bearer token
|
33
|
+
JWT bearer token, or None if failed
|
31
34
|
|
32
|
-
|
33
|
-
|
35
|
+
Note:
|
36
|
+
This function never throws exceptions - all errors are handled gracefully
|
34
37
|
"""
|
35
|
-
|
36
|
-
|
38
|
+
try:
|
39
|
+
if api_key is None:
|
40
|
+
from agentops import get_client
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
client = get_client()
|
43
|
+
if client and client.config.api_key:
|
44
|
+
api_key = client.config.api_key
|
45
|
+
else:
|
46
|
+
api_key = os.getenv("AGENTOPS_API_KEY")
|
47
|
+
if not api_key:
|
48
|
+
logger.warning("No API key provided and AGENTOPS_API_KEY environment variable not set")
|
49
|
+
return None
|
50
|
+
|
51
|
+
# Use a separate aiohttp session for validation to avoid conflicts
|
52
|
+
import aiohttp
|
53
|
+
|
54
|
+
async with aiohttp.ClientSession() as session:
|
55
|
+
async with session.post(
|
56
|
+
"https://api.agentops.ai/public/v1/auth/access_token",
|
57
|
+
json={"api_key": api_key},
|
58
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
59
|
+
) as response:
|
60
|
+
if response.status >= 400:
|
61
|
+
logger.warning(f"Failed to get JWT token: HTTP {response.status} - backend may be unavailable")
|
62
|
+
return None
|
63
|
+
|
64
|
+
response_data = await response.json()
|
65
|
+
|
66
|
+
if "bearer" not in response_data:
|
67
|
+
logger.warning("Failed to get JWT token: No bearer token in response")
|
68
|
+
return None
|
69
|
+
|
70
|
+
return response_data["bearer"]
|
71
|
+
|
72
|
+
except Exception as e:
|
73
|
+
logger.warning(f"Failed to get JWT token: {e} - continuing without authentication")
|
74
|
+
return None
|
75
|
+
|
76
|
+
|
77
|
+
def get_jwt_token_sync(api_key: Optional[str] = None) -> Optional[str]:
|
78
|
+
"""
|
79
|
+
Synchronous wrapper for get_jwt_token - runs async function in event loop.
|
43
80
|
|
44
|
-
|
45
|
-
|
46
|
-
raise ValueError("No API key provided and AGENTOPS_API_KEY environment variable not set")
|
81
|
+
Args:
|
82
|
+
api_key: Optional API key. If not provided, uses AGENTOPS_API_KEY env var.
|
47
83
|
|
84
|
+
Returns:
|
85
|
+
JWT bearer token, or None if failed
|
86
|
+
|
87
|
+
Note:
|
88
|
+
This function never throws exceptions - all errors are handled gracefully
|
89
|
+
"""
|
48
90
|
try:
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
91
|
+
import concurrent.futures
|
92
|
+
|
93
|
+
# Always run in a separate thread to avoid event loop issues
|
94
|
+
def run_in_thread():
|
95
|
+
return asyncio.run(get_jwt_token(api_key))
|
96
|
+
|
97
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
98
|
+
future = executor.submit(run_in_thread)
|
99
|
+
return future.result()
|
100
|
+
except Exception as e:
|
101
|
+
logger.warning(f"Failed to get JWT token synchronously: {e}")
|
102
|
+
return None
|
56
103
|
|
57
104
|
|
58
105
|
def get_trace_details(trace_id: str, jwt_token: str) -> Dict[str, Any]:
|
@@ -121,60 +168,36 @@ def check_llm_spans(spans: List[Dict[str, Any]]) -> Tuple[bool, List[str]]:
|
|
121
168
|
|
122
169
|
for span in spans:
|
123
170
|
span_name = span.get("span_name", "unnamed_span")
|
124
|
-
|
125
|
-
# Check span attributes for LLM span kind
|
126
171
|
span_attributes = span.get("span_attributes", {})
|
127
172
|
is_llm_span = False
|
128
173
|
|
129
|
-
# If we have span_attributes, check them
|
130
174
|
if span_attributes:
|
131
|
-
#
|
132
|
-
span_kind =
|
133
|
-
|
134
|
-
|
135
|
-
if isinstance(span_attributes, dict):
|
175
|
+
# Check for LLM span kind - handle both flat and nested structures
|
176
|
+
span_kind = span_attributes.get("agentops.span.kind", "")
|
177
|
+
if not span_kind:
|
178
|
+
# Check nested structure: agentops.span.kind or agentops -> span -> kind
|
136
179
|
agentops_attrs = span_attributes.get("agentops", {})
|
137
180
|
if isinstance(agentops_attrs, dict):
|
138
|
-
|
139
|
-
if isinstance(
|
140
|
-
span_kind =
|
141
|
-
|
142
|
-
# Structure 2: Direct in span_attributes
|
143
|
-
if not span_kind and isinstance(span_attributes, dict):
|
144
|
-
# Try looking for agentops.span.kind as a flattened key
|
145
|
-
span_kind = span_attributes.get("agentops.span.kind", "")
|
146
|
-
|
147
|
-
# Structure 3: Look for SpanAttributes.AGENTOPS_SPAN_KIND
|
148
|
-
if not span_kind and isinstance(span_attributes, dict):
|
149
|
-
from agentops.semconv import SpanAttributes
|
181
|
+
span_attrs = agentops_attrs.get("span", {})
|
182
|
+
if isinstance(span_attrs, dict):
|
183
|
+
span_kind = span_attrs.get("kind", "")
|
150
184
|
|
151
|
-
span_kind = span_attributes.get(SpanAttributes.AGENTOPS_SPAN_KIND, "")
|
152
|
-
|
153
|
-
# Check if this is an LLM span by span kind
|
154
185
|
is_llm_span = span_kind == "llm"
|
155
186
|
|
156
|
-
# Alternative check: Look for gen_ai
|
157
|
-
|
158
|
-
if not is_llm_span and isinstance(span_attributes, dict):
|
187
|
+
# Alternative check: Look for gen_ai attributes
|
188
|
+
if not is_llm_span:
|
159
189
|
gen_ai_attrs = span_attributes.get("gen_ai", {})
|
160
190
|
if isinstance(gen_ai_attrs, dict):
|
161
|
-
# If we have prompt or completion data, it's an LLM span
|
162
191
|
if "prompt" in gen_ai_attrs or "completion" in gen_ai_attrs:
|
163
192
|
is_llm_span = True
|
164
193
|
|
165
|
-
# Check for
|
166
|
-
if not is_llm_span
|
167
|
-
|
168
|
-
|
169
|
-
# Check for LLM request type - try both gen_ai.* and llm.* prefixes
|
170
|
-
# The instrumentation sets gen_ai.* but the API might return llm.*
|
171
|
-
llm_request_type = span_attributes.get(SpanAttributes.LLM_REQUEST_TYPE, "")
|
194
|
+
# Check for LLM request type
|
195
|
+
if not is_llm_span:
|
196
|
+
llm_request_type = span_attributes.get("gen_ai.request.type", "")
|
172
197
|
if not llm_request_type:
|
173
|
-
#
|
198
|
+
# Also check for older llm.request.type format
|
174
199
|
llm_request_type = span_attributes.get("llm.request.type", "")
|
175
|
-
|
176
|
-
# Check if it's a chat or completion request (the main LLM types)
|
177
|
-
if llm_request_type in [LLMRequestTypeValues.CHAT.value, LLMRequestTypeValues.COMPLETION.value]:
|
200
|
+
if llm_request_type in ["chat", "completion"]:
|
178
201
|
is_llm_span = True
|
179
202
|
|
180
203
|
if is_llm_span:
|
@@ -242,7 +265,19 @@ def validate_trace_spans(
|
|
242
265
|
raise ValueError("No trace ID found. Provide either trace_id or trace_context parameter.")
|
243
266
|
|
244
267
|
# Get JWT token
|
245
|
-
jwt_token =
|
268
|
+
jwt_token = get_jwt_token_sync(api_key)
|
269
|
+
if not jwt_token:
|
270
|
+
logger.warning("Could not obtain JWT token - validation will be skipped")
|
271
|
+
return {
|
272
|
+
"trace_id": trace_id,
|
273
|
+
"span_count": 0,
|
274
|
+
"spans": [],
|
275
|
+
"has_llm_spans": False,
|
276
|
+
"llm_span_names": [],
|
277
|
+
"metrics": None,
|
278
|
+
"validation_skipped": True,
|
279
|
+
"reason": "No JWT token available",
|
280
|
+
}
|
246
281
|
|
247
282
|
logger.info(f"Validating spans for trace ID: {trace_id}")
|
248
283
|
|
@@ -335,6 +370,10 @@ def print_validation_summary(result: Dict[str, Any]) -> None:
|
|
335
370
|
print("🔍 AgentOps Span Validation Results")
|
336
371
|
print("=" * 50)
|
337
372
|
|
373
|
+
if result.get("validation_skipped"):
|
374
|
+
print(f"⚠️ Validation skipped: {result.get('reason', 'Unknown reason')}")
|
375
|
+
return
|
376
|
+
|
338
377
|
print(f"✅ Found {result['span_count']} span(s) in trace")
|
339
378
|
|
340
379
|
if result.get("has_llm_spans"):
|