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
@@ -0,0 +1,469 @@
1
+ """OpenAI Agents SDK Instrumentation Exporter for AgentOps
2
+
3
+ This module handles the conversion of Agents SDK spans to OpenTelemetry spans.
4
+ It manages the complete span lifecycle, attribute application, and proper span hierarchy.
5
+
6
+ See the README.md in this directory for complete documentation on:
7
+ - Span lifecycle management approach
8
+ - Serialization rules for attributes
9
+ - Structured attribute handling
10
+ - Semantic conventions usage
11
+
12
+ IMPORTANT FOR TESTING:
13
+ - Tests should verify attribute existence using MessageAttributes constants
14
+ - Do not check for the presence of SpanAttributes.LLM_COMPLETIONS
15
+ - Verify individual content/tool attributes instead of root attributes
16
+ """
17
+
18
+ import json
19
+ from typing import Any, Dict, Optional
20
+
21
+ from opentelemetry import trace, context as context_api
22
+ from opentelemetry.trace import get_tracer, SpanKind, Status, StatusCode, NonRecordingSpan
23
+ from opentelemetry import trace as trace_api
24
+ from opentelemetry.sdk.trace import Span
25
+
26
+ from agentops.logging import logger
27
+ from agentops.semconv import (
28
+ CoreAttributes,
29
+ )
30
+
31
+ from agentops.instrumentation.common.attributes import (
32
+ get_base_trace_attributes,
33
+ get_base_span_attributes,
34
+ )
35
+
36
+ from agentops.instrumentation.agentic.openai_agents import LIBRARY_NAME, LIBRARY_VERSION
37
+ from agentops.instrumentation.agentic.openai_agents.attributes.common import (
38
+ get_span_attributes,
39
+ )
40
+
41
+
42
+ def log_otel_trace_id(span_type):
43
+ """Log the OpenTelemetry trace ID for debugging and correlation purposes.
44
+
45
+ The hexadecimal OTel trace ID is essential for querying the backend database
46
+ and correlating local debugging logs with server-side trace data. This ID
47
+ is different from the Agents SDK trace_id and is the primary key used in
48
+ observability systems and the AgentOps dashboard.
49
+
50
+ This function retrieves the current OpenTelemetry trace ID directly from the
51
+ active span context and formats it as a 32-character hex string.
52
+
53
+ Args:
54
+ span_type: The type of span being exported for logging context
55
+
56
+ Returns:
57
+ str or None: The OpenTelemetry trace ID as a hex string, or None if unavailable
58
+ """
59
+ current_span = trace.get_current_span()
60
+ if hasattr(current_span, "get_span_context"):
61
+ ctx = current_span.get_span_context()
62
+ if hasattr(ctx, "trace_id") and ctx.trace_id:
63
+ # Convert trace_id to 32-character hex string as shown in the API
64
+ otel_trace_id = f"{ctx.trace_id:032x}" if isinstance(ctx.trace_id, int) else str(ctx.trace_id)
65
+ logger.debug(f"[SPAN] Export | Type: {span_type} | TRACE ID: {otel_trace_id}")
66
+ return otel_trace_id
67
+
68
+ logger.debug(f"[SPAN] Export | Type: {span_type} | NO TRACE ID AVAILABLE")
69
+ return None
70
+
71
+
72
+ def get_span_kind(span: Any) -> SpanKind:
73
+ """Determine the appropriate span kind based on span type."""
74
+ span_data = span.span_data
75
+ span_type = span_data.__class__.__name__
76
+
77
+ if span_type == "AgentSpanData":
78
+ return SpanKind.CONSUMER
79
+ elif span_type in ["FunctionSpanData", "GenerationSpanData", "ResponseSpanData"]:
80
+ return SpanKind.CLIENT
81
+ else:
82
+ return SpanKind.INTERNAL
83
+
84
+
85
+ def get_span_name(span: Any) -> str:
86
+ """Get the name of the span based on its type and attributes."""
87
+ span_data = span.span_data
88
+ span_type = span_data.__class__.__name__
89
+
90
+ if hasattr(span_data, "name") and span_data.name:
91
+ return span_data.name
92
+ else:
93
+ return span_type.replace("SpanData", "").lower() # fallback
94
+
95
+
96
+ def _get_span_lookup_key(trace_id: str, span_id: str) -> str:
97
+ """Generate a unique lookup key for spans based on trace and span IDs.
98
+
99
+ This key is used to track spans in the exporter and allows for efficient
100
+ lookups and management of spans during their lifecycle.
101
+
102
+ Args:
103
+ trace_id: The trace ID for the current span
104
+ span_id: The span ID for the current span
105
+
106
+ Returns:
107
+ str: A unique lookup key for the span
108
+ """
109
+ return f"span:{trace_id}:{span_id}"
110
+
111
+
112
+ class OpenAIAgentsExporter:
113
+ """Exporter for Agents SDK traces and spans that forwards them to OpenTelemetry.
114
+
115
+ This exporter is responsible for:
116
+ 1. Creating and configuring spans
117
+ 2. Setting span attributes based on data from the processor
118
+ 3. Managing the span lifecycle
119
+ 4. Using semantic conventions for attribute naming
120
+ 5. Interacting with the OpenTelemetry API
121
+ 6. Tracking spans to allow updating them when tasks complete
122
+ """
123
+
124
+ def __init__(self, tracer_provider=None):
125
+ self.tracer_provider = tracer_provider
126
+ # Dictionary to track active spans by their SDK span ID
127
+ # Allows us to reference spans later during task completion
128
+ self._active_spans = {}
129
+ # Dictionary to track spans by trace/span ID for faster lookups
130
+ self._span_map = {}
131
+
132
+ def export_trace(self, trace: Any) -> None:
133
+ """
134
+ Handle exporting the trace.
135
+ """
136
+ tracer = get_tracer(LIBRARY_NAME, LIBRARY_VERSION, self.tracer_provider)
137
+ trace_id = getattr(trace, "trace_id", "unknown")
138
+
139
+ if not hasattr(trace, "trace_id"):
140
+ logger.debug("Cannot export trace: missing trace_id")
141
+ return
142
+
143
+ # Determine if this is a trace end event using status field
144
+ # We use the status field to determine if this is an end event
145
+ is_end_event = hasattr(trace, "status") and trace.status == StatusCode.OK.name
146
+ trace_lookup_key = _get_span_lookup_key(trace_id, trace_id)
147
+ attributes = get_base_trace_attributes(trace)
148
+
149
+ # For end events, check if we already have the span
150
+ if is_end_event and trace_lookup_key in self._span_map:
151
+ existing_span = self._span_map[trace_lookup_key]
152
+
153
+ span_is_ended = False
154
+ if isinstance(existing_span, Span) and hasattr(existing_span, "_end_time"):
155
+ span_is_ended = existing_span._end_time is not None
156
+
157
+ if not span_is_ended:
158
+ # Update with core attributes
159
+ for key, value in attributes.items():
160
+ existing_span.set_attribute(key, value)
161
+
162
+ # Handle error if present
163
+ if hasattr(trace, "error") and trace.error:
164
+ self._handle_span_error(trace, existing_span)
165
+ # Set status to OK if no error
166
+ else:
167
+ existing_span.set_status(Status(StatusCode.OK))
168
+
169
+ existing_span.end()
170
+
171
+ # Clean up our tracking resources
172
+ self._active_spans.pop(trace_id, None)
173
+ self._span_map.pop(trace_lookup_key, None)
174
+ return
175
+
176
+ # Create span directly instead of using context manager
177
+ span = tracer.start_span(name=trace.name, kind=SpanKind.INTERNAL, attributes=attributes)
178
+
179
+ # Add any additional trace attributes
180
+ if hasattr(trace, "group_id") and trace.group_id:
181
+ span.set_attribute(CoreAttributes.GROUP_ID, trace.group_id)
182
+
183
+ if hasattr(trace, "metadata") and trace.metadata:
184
+ for key, value in trace.metadata.items():
185
+ if isinstance(value, (str, int, float, bool)):
186
+ span.set_attribute(f"trace.metadata.{key}", value)
187
+
188
+ # Record error if present
189
+ if hasattr(trace, "error") and trace.error:
190
+ self._handle_span_error(trace, span)
191
+
192
+ # For start events, store the span for later reference
193
+ if not is_end_event:
194
+ self._span_map[trace_lookup_key] = span
195
+ self._active_spans[trace_id] = {
196
+ "span": span,
197
+ "span_type": "TraceSpan",
198
+ "trace_id": trace_id,
199
+ "parent_id": None,
200
+ }
201
+ else:
202
+ span.end()
203
+
204
+ def _get_parent_context(self, trace_id: str, span_id: str, parent_id: Optional[str] = None) -> Any:
205
+ """Find the parent span context for proper span nesting.
206
+
207
+ This method checks:
208
+ 1. First for an explicit parent ID in our span tracking dictionary
209
+ 2. Then checks if the trace span is the parent
210
+ 3. Falls back to the current active span context if no parent is found
211
+
212
+ Args:
213
+ trace_id: The trace ID for the current span
214
+ span_id: The span ID for the current span
215
+ parent_id: Optional parent span ID to look up
216
+
217
+ Returns:
218
+ The OpenTelemetry span context to use as parent
219
+ """
220
+ parent_span_ctx = None
221
+
222
+ if parent_id:
223
+ # Try to find the parent span in our tracking dictionary
224
+ parent_lookup_key = f"span:{trace_id}:{parent_id}"
225
+ if parent_lookup_key in self._span_map:
226
+ parent_span = self._span_map[parent_lookup_key]
227
+ # Get the context from the parent span if it exists
228
+ if hasattr(parent_span, "get_span_context"):
229
+ parent_span_ctx = parent_span.get_span_context()
230
+
231
+ # If parent not found by span ID, check if trace span should be the parent
232
+ if not parent_span_ctx and parent_id is None:
233
+ # Try using the trace span as parent
234
+ trace_lookup_key = _get_span_lookup_key(trace_id, trace_id)
235
+
236
+ if trace_lookup_key in self._span_map:
237
+ trace_span = self._span_map[trace_lookup_key]
238
+ if hasattr(trace_span, "get_span_context"):
239
+ parent_span_ctx = trace_span.get_span_context()
240
+
241
+ # If we couldn't find the parent by ID, use the current span context as parent
242
+ if not parent_span_ctx:
243
+ # Get the current span context from the context API
244
+ ctx = context_api.get_current()
245
+ parent_span_ctx = trace_api.get_current_span(ctx).get_span_context()
246
+
247
+ return parent_span_ctx
248
+
249
+ def _create_span_with_parent(
250
+ self, name: str, kind: SpanKind, attributes: Dict[str, Any], parent_ctx: Any, end_immediately: bool = False
251
+ ) -> Any:
252
+ """Create a span with the specified parent context.
253
+
254
+ This centralizes span creation with proper parent nesting.
255
+
256
+ Args:
257
+ name: The name for the new span
258
+ kind: The span kind (CLIENT, SERVER, etc.)
259
+ attributes: The attributes to set on the span
260
+ parent_ctx: The parent context to use for nesting
261
+ end_immediately: Whether to end the span immediately
262
+
263
+ Returns:
264
+ The newly created span
265
+ """
266
+ # Get tracer from provider
267
+ tracer = get_tracer(LIBRARY_NAME, LIBRARY_VERSION, self.tracer_provider)
268
+
269
+ # Create span with context so we get proper nesting
270
+ with trace_api.use_span(NonRecordingSpan(parent_ctx), end_on_exit=False):
271
+ span = tracer.start_span(name=name, kind=kind, attributes=attributes)
272
+
273
+ # Optionally end the span immediately
274
+ if end_immediately:
275
+ span.end()
276
+
277
+ return span
278
+
279
+ def export_span(self, span: Any) -> None:
280
+ """Export a span to OpenTelemetry, creating or updating as needed.
281
+
282
+ This method decides whether to create a new span or update an existing one
283
+ based on whether this is a start or end event for a given span ID.
284
+
285
+ For start events:
286
+ - Create a new span and store it for later updates
287
+ - Leave status as UNSET (in progress)
288
+ - Do not end the span
289
+ - Properly set parent span reference for nesting
290
+
291
+ For end events:
292
+ - Look for an existing span to update
293
+ - If found and not ended, update with final data and end it
294
+ - If not found or already ended, create a new complete span with all data
295
+ - End the span with proper status
296
+ """
297
+ if not hasattr(span, "span_data"):
298
+ return
299
+
300
+ span_data = span.span_data
301
+ span_type = span_data.__class__.__name__
302
+ span_id = getattr(span, "span_id", "unknown")
303
+ trace_id = getattr(span, "trace_id", "unknown")
304
+ parent_id = getattr(span, "parent_id", None)
305
+
306
+ # Check if this is a span end event
307
+ is_end_event = hasattr(span, "status") and span.status == StatusCode.OK.name
308
+
309
+ # Unique lookup key for this span
310
+ span_lookup_key = _get_span_lookup_key(trace_id, span_id)
311
+
312
+ attributes = get_base_span_attributes(span)
313
+ span_attributes = get_span_attributes(span_data)
314
+ attributes.update(span_attributes)
315
+
316
+ if is_end_event:
317
+ # Update all attributes for end events
318
+ attributes.update(span_attributes)
319
+
320
+ # Log the trace ID for debugging and correlation with AgentOps API
321
+ log_otel_trace_id(span_type)
322
+
323
+ # For start events, create a new span and store it (don't end it)
324
+ if not is_end_event:
325
+ # Process the span based on its type
326
+ # TODO span_name should come from the attributes module
327
+ span_name = get_span_name(span)
328
+ span_kind = get_span_kind(span)
329
+
330
+ # Get parent context for proper nesting
331
+ parent_span_ctx = self._get_parent_context(trace_id, span_id, parent_id)
332
+
333
+ # Create the span with proper parent context
334
+ otel_span = self._create_span_with_parent(
335
+ name=span_name, kind=span_kind, attributes=attributes, parent_ctx=parent_span_ctx
336
+ )
337
+
338
+ # Store the span for later reference
339
+ if not isinstance(otel_span, NonRecordingSpan):
340
+ self._span_map[span_lookup_key] = otel_span
341
+ self._active_spans[span_id] = {
342
+ "span": otel_span,
343
+ "span_type": span_type,
344
+ "trace_id": trace_id,
345
+ "parent_id": parent_id,
346
+ }
347
+
348
+ # Handle any error information
349
+ self._handle_span_error(span, otel_span)
350
+ # DO NOT end the span for start events - we want to keep it open for updates
351
+ return
352
+
353
+ # For end events, check if we already have the span
354
+ if span_lookup_key in self._span_map:
355
+ existing_span = self._span_map[span_lookup_key]
356
+
357
+ span_is_ended = False
358
+ if isinstance(existing_span, Span) and hasattr(existing_span, "_end_time"):
359
+ span_is_ended = existing_span._end_time is not None
360
+
361
+ if not span_is_ended:
362
+ # Update with final attributes
363
+ for key, value in attributes.items():
364
+ existing_span.set_attribute(key, value)
365
+
366
+ existing_span.set_status(
367
+ Status(StatusCode.OK if getattr(span, "status", "OK") == "OK" else StatusCode.ERROR)
368
+ )
369
+ self._handle_span_error(span, existing_span)
370
+ existing_span.end()
371
+ # Span already ended, create a new one (should be rare if logic is correct)
372
+ else:
373
+ logger.warning(
374
+ f"[Exporter] SDK span_id: {span_id} (END event) - Attempting to end an ALREADY ENDED span: {span_lookup_key}. Creating a new one instead."
375
+ )
376
+ self.create_span(span, span_type, attributes, is_already_ended=True)
377
+ # No existing span found for end event, create a new one
378
+ else:
379
+ logger.warning(
380
+ f"[Exporter] SDK span_id: {span_id} (END event) - No active span found for end event: {span_lookup_key}. Creating a new one."
381
+ )
382
+ self.create_span(span, span_type, attributes, is_already_ended=True)
383
+
384
+ self._active_spans.pop(span_id, None)
385
+ self._span_map.pop(span_lookup_key, None)
386
+
387
+ def create_span(
388
+ self, span: Any, span_type: str, attributes: Dict[str, Any], is_already_ended: bool = False
389
+ ) -> None:
390
+ """Create a new span with the provided data and end it immediately.
391
+
392
+ This method creates a span using the appropriate parent context, applies
393
+ all attributes, and ends it immediately since it's for spans that are
394
+ already in an ended state.
395
+
396
+ Args:
397
+ span: The span data from the Agents SDK
398
+ span_type: The type of span being created
399
+ attributes: The attributes to set on the span
400
+ """
401
+ parent_ctx = None
402
+ if hasattr(span, "parent_id") and span.parent_id:
403
+ parent_ctx = self._get_parent_context(
404
+ getattr(span, "trace_id", "unknown"), getattr(span, "id", "unknown"), span.parent_id
405
+ )
406
+
407
+ name = get_span_name(span)
408
+ kind = get_span_kind(span)
409
+
410
+ self._create_span_with_parent(
411
+ name=name, kind=kind, attributes=attributes, parent_ctx=parent_ctx, end_immediately=True
412
+ )
413
+
414
+ def _handle_span_error(self, span: Any, otel_span: Any) -> None:
415
+ """Handle error information from spans."""
416
+ if hasattr(span, "error") and span.error:
417
+ # Set status to error
418
+ status = Status(StatusCode.ERROR)
419
+ otel_span.set_status(status)
420
+
421
+ # Determine error message - handle various error formats
422
+ error_message = "Unknown error"
423
+ error_data = {}
424
+ error_type = "AgentError"
425
+
426
+ # Handle different error formats
427
+ if isinstance(span.error, dict):
428
+ error_message = span.error.get("message", span.error.get("error", "Unknown error"))
429
+ error_data = span.error.get("data", {})
430
+ # Extract error type if available
431
+ if "type" in span.error:
432
+ error_type = span.error["type"]
433
+ elif "code" in span.error:
434
+ error_type = span.error["code"]
435
+ elif isinstance(span.error, str):
436
+ error_message = span.error
437
+ elif hasattr(span.error, "message"):
438
+ error_message = span.error.message
439
+ # Use type() for more reliable class name access
440
+ error_type = type(span.error).__name__
441
+ elif hasattr(span.error, "__str__"):
442
+ # Fallback to string representation
443
+ error_message = str(span.error)
444
+
445
+ # Record the exception with proper error data
446
+ try:
447
+ exception = Exception(error_message)
448
+ error_data_json = json.dumps(error_data) if error_data else "{}"
449
+ otel_span.record_exception(
450
+ exception=exception,
451
+ attributes={"error.data": error_data_json},
452
+ )
453
+ except Exception as e:
454
+ # If JSON serialization fails, use simpler approach
455
+ logger.warning(f"Error serializing error data: {e}")
456
+ otel_span.record_exception(Exception(error_message))
457
+
458
+ # Set error attributes
459
+ otel_span.set_attribute(CoreAttributes.ERROR_TYPE, error_type)
460
+ otel_span.set_attribute(CoreAttributes.ERROR_MESSAGE, error_message)
461
+
462
+ def cleanup(self):
463
+ """Clean up any outstanding spans during shutdown.
464
+
465
+ This ensures we don't leak span resources when the exporter is shutdown.
466
+ """
467
+ # Clear all tracking dictionaries
468
+ self._active_spans.clear()
469
+ self._span_map.clear()
@@ -0,0 +1,107 @@
1
+ """OpenAI Agents SDK Instrumentation for AgentOps
2
+
3
+ This module provides instrumentation for the OpenAI Agents SDK, leveraging its built-in
4
+ tracing API for observability. It captures detailed information about agent execution,
5
+ tool usage, LLM requests, and token metrics.
6
+
7
+ The implementation uses a clean separation between exporters and processors. The exporter
8
+ translates Agent spans into OpenTelemetry spans with appropriate semantic conventions.
9
+
10
+ The processor implements the tracing interface, collects metrics, and manages timing data.
11
+
12
+ We use the built-in add_trace_processor hook for all functionality. Streaming support
13
+ would require monkey-patching the run method of `Runner`, but doesn't really get us
14
+ more data than we already have, since the `Response` object is always passed to us
15
+ from the `agents.tracing` module.
16
+
17
+ TODO Calls to the OpenAI API are not available in this tracing context, so we may
18
+ need to monkey-patch the `openai` from here to get that data. While we do have
19
+ separate instrumentation for the OpenAI API, in order to get it to nest with the
20
+ spans we create here, it's probably easier (or even required) that we incorporate
21
+ that here as well.
22
+ """
23
+
24
+ from typing import Collection
25
+
26
+ from opentelemetry import trace
27
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore
28
+ from agentops.instrumentation.agentic.openai_agents import LIBRARY_VERSION
29
+
30
+ from agentops.logging import logger
31
+ from agentops.instrumentation.agentic.openai_agents.processor import OpenAIAgentsProcessor
32
+ from agentops.instrumentation.agentic.openai_agents.exporter import OpenAIAgentsExporter
33
+
34
+
35
+ class OpenAIAgentsInstrumentor(BaseInstrumentor):
36
+ """An instrumentor for OpenAI Agents SDK that uses the built-in tracing API."""
37
+
38
+ _processor = None
39
+ _exporter = None
40
+ _default_processor = None
41
+
42
+ def __init__(self):
43
+ super().__init__()
44
+ self._tracer = None
45
+ self._is_instrumented_instance_flag = False
46
+
47
+ def instrumentation_dependencies(self) -> Collection[str]:
48
+ """Return packages required for instrumentation."""
49
+ return ["openai-agents >= 0.0.1"]
50
+
51
+ def _instrument(self, **kwargs):
52
+ """Instrument the OpenAI Agents SDK."""
53
+ if self._is_instrumented_instance_flag:
54
+ logger.debug("OpenAI Agents SDK already instrumented. Skipping.")
55
+ return
56
+
57
+ tracer_provider = kwargs.get("tracer_provider")
58
+ if self._tracer is None:
59
+ logger.debug("OpenAI Agents SDK tracer is None, creating new tracer.")
60
+ self._tracer = trace.get_tracer("agentops.instrumentation.openai_agents", LIBRARY_VERSION)
61
+
62
+ try:
63
+ self._exporter = OpenAIAgentsExporter(tracer_provider=tracer_provider)
64
+ self._processor = OpenAIAgentsProcessor(
65
+ exporter=self._exporter,
66
+ )
67
+
68
+ # Replace the default processor with our processor
69
+ from agents import set_trace_processors
70
+ from agents.tracing.processors import default_processor
71
+
72
+ if getattr(self, "_default_processor", None) is None:
73
+ self._default_processor = default_processor()
74
+
75
+ # Store reference to default processor for later restoration
76
+ set_trace_processors([self._processor])
77
+ self._is_instrumented_instance_flag = True
78
+
79
+ except Exception as e:
80
+ logger.warning(f"Failed to instrument OpenAI Agents SDK: {e}", exc_info=True)
81
+
82
+ def _uninstrument(self, **kwargs):
83
+ """Remove instrumentation from OpenAI Agents SDK."""
84
+ if not self._is_instrumented_instance_flag:
85
+ logger.debug("OpenAI Agents SDK not currently instrumented. Skipping uninstrument.")
86
+ return
87
+ try:
88
+ # Clean up any active spans in the exporter
89
+ if hasattr(self, "_exporter") and self._exporter:
90
+ if hasattr(self._exporter, "cleanup"):
91
+ self._exporter.cleanup()
92
+
93
+ # Put back the default processor
94
+ from agents import set_trace_processors
95
+
96
+ if hasattr(self, "_default_processor") and self._default_processor:
97
+ set_trace_processors([self._default_processor])
98
+ self._default_processor = None
99
+ else:
100
+ logger.warning("OpenAI Agents SDK has no default processor to restore.")
101
+ self._processor = None
102
+ self._exporter = None
103
+
104
+ self._is_instrumented_instance_flag = False
105
+ logger.info("Successfully removed OpenAI Agents SDK instrumentation")
106
+ except Exception as e:
107
+ logger.warning(f"Failed to uninstrument OpenAI Agents SDK: {e}", exc_info=True)
@@ -0,0 +1,58 @@
1
+ from typing import Any
2
+ from opentelemetry.trace import StatusCode
3
+ from agentops.logging import logger
4
+
5
+
6
+ class OpenAIAgentsProcessor:
7
+ """Processor for OpenAI Agents SDK traces.
8
+
9
+ This processor implements the TracingProcessor interface from the Agents SDK
10
+ and converts trace events to OpenTelemetry spans and metrics.
11
+
12
+ The processor does NOT directly create OpenTelemetry spans.
13
+ It delegates span creation to the OpenAIAgentsExporter.
14
+ """
15
+
16
+ def __init__(self, exporter=None):
17
+ self.exporter = exporter
18
+
19
+ def on_trace_start(self, sdk_trace: Any) -> None:
20
+ """Called when a trace starts in the Agents SDK."""
21
+
22
+ logger.debug(f"[agentops.instrumentation.openai_agents] Trace started: {sdk_trace}")
23
+ self.exporter.export_trace(sdk_trace)
24
+
25
+ def on_trace_end(self, sdk_trace: Any) -> None:
26
+ """Called when a trace ends in the Agents SDK."""
27
+
28
+ # Mark this as an end event
29
+ # This is used by the exporter to determine whether to create or update a trace
30
+ sdk_trace.status = StatusCode.OK.name
31
+
32
+ logger.debug(f"[agentops.instrumentation.openai_agents] Trace ended: {sdk_trace}")
33
+ self.exporter.export_trace(sdk_trace)
34
+
35
+ def on_span_start(self, span: Any) -> None:
36
+ """Called when a span starts in the Agents SDK."""
37
+
38
+ logger.debug(f"[agentops.instrumentation.openai_agents] Span started: {span}")
39
+ self.exporter.export_span(span)
40
+
41
+ def on_span_end(self, span: Any) -> None:
42
+ """Called when a span ends in the Agents SDK."""
43
+
44
+ # Mark this as an end event
45
+ # This is used by the exporter to determine whether to create or update a span
46
+ span.status = StatusCode.OK.name
47
+
48
+ logger.debug(f"[agentops.instrumentation.openai_agents] Span ended: {span}")
49
+ self.exporter.export_span(span)
50
+
51
+ def shutdown(self) -> None:
52
+ """Called when the application stops."""
53
+ pass
54
+
55
+ def force_flush(self) -> None:
56
+ """Forces an immediate flush of all queued spans/traces."""
57
+ # We don't queue spans so this is a no-op
58
+ pass