mseep-agentops 0.4.18__py3-none-any.whl → 0.4.22__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.22.dist-info}/METADATA +30 -40
  124. mseep_agentops-0.4.22.dist-info/RECORD +178 -0
  125. {mseep_agentops-0.4.18.dist-info → mseep_agentops-0.4.22.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.22.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
- from typing import Dict, Optional, Sequence
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 automatically handles JWT authentication and token refresh
20
- for telemetry data sent to the AgentOps API using a dedicated HTTP session
21
- with authentication retry logic built in.
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
- # TODO: Implement re-authentication
34
- # FIXME: endpoint here is not "endpoint" from config
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
- def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
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
- Export spans with automatic authentication handling
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
- The authentication and retry logic is now handled by the underlying
53
- HTTP session adapter, so we just need to call the parent export method.
54
+ # Store any additional kwargs for potential future use
55
+ self._custom_kwargs = kwargs
54
56
 
55
- Args:
56
- spans: The list of spans to export
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
- Returns:
59
- The result of the export
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
- return super().export(spans)
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
- # Authentication token expired or invalid
65
- logger.warning(f"Authentication error during span export: {e}")
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
@@ -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
- from agentops.logging import logger
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
- Raises:
33
- ApiServerException: If token exchange fails
35
+ Note:
36
+ This function never throws exceptions - all errors are handled gracefully
34
37
  """
35
- if api_key is None:
36
- from agentops import get_client
38
+ try:
39
+ if api_key is None:
40
+ from agentops import get_client
37
41
 
38
- client = get_client()
39
- if client and client.config.api_key:
40
- api_key = client.config.api_key
41
- else:
42
- import os
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
- api_key = os.getenv("AGENTOPS_API_KEY")
45
- if not api_key:
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
- response = requests.post(
50
- "https://api.agentops.ai/public/v1/auth/access_token", json={"api_key": api_key}, timeout=10
51
- )
52
- response.raise_for_status()
53
- return response.json()["bearer"]
54
- except requests.exceptions.RequestException as e:
55
- raise ApiServerException(f"Failed to get JWT token: {e}")
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
- # Try different possible structures for span kind
132
- span_kind = None
133
-
134
- # Structure 1: span_attributes.agentops.span.kind
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
- span_info = agentops_attrs.get("span", {})
139
- if isinstance(span_info, dict):
140
- span_kind = span_info.get("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.prompt or gen_ai.completion attributes
157
- # These are standard semantic conventions for LLM spans
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 LLM_REQUEST_TYPE attribute (used by provider instrumentations)
166
- if not is_llm_span and isinstance(span_attributes, dict):
167
- from agentops.semconv import SpanAttributes, LLMRequestTypeValues
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
- # Try the llm.* prefix version
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 = get_jwt_token(api_key)
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"):