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,286 @@
1
+ """SmoLAgents instrumentation for AgentOps."""
2
+
3
+ from typing import Dict, Any
4
+ from opentelemetry.trace import SpanKind
5
+ from opentelemetry.metrics import Meter
6
+ from wrapt import wrap_function_wrapper
7
+
8
+ from agentops.instrumentation.common import CommonInstrumentor, StandardMetrics, InstrumentorConfig
9
+ from agentops.logging import logger
10
+
11
+ # Library info for tracer/meter
12
+ LIBRARY_NAME = "agentops.instrumentation.smolagents"
13
+ LIBRARY_VERSION = "0.1.0"
14
+
15
+ # Import attribute handlers
16
+ try:
17
+ from agentops.instrumentation.agentic.smolagents.attributes.agent import (
18
+ get_agent_attributes,
19
+ get_tool_call_attributes,
20
+ get_planning_step_attributes,
21
+ get_agent_step_attributes,
22
+ get_agent_stream_attributes,
23
+ get_managed_agent_attributes,
24
+ )
25
+ from agentops.instrumentation.agentic.smolagents.attributes.model import (
26
+ get_model_attributes,
27
+ get_stream_attributes,
28
+ )
29
+ except ImportError:
30
+ # Fallback functions if imports fail
31
+ def get_agent_attributes(*args, **kwargs):
32
+ return {}
33
+
34
+ def get_tool_call_attributes(*args, **kwargs):
35
+ return {}
36
+
37
+ def get_planning_step_attributes(*args, **kwargs):
38
+ return {}
39
+
40
+ def get_agent_step_attributes(*args, **kwargs):
41
+ return {}
42
+
43
+ def get_agent_stream_attributes(*args, **kwargs):
44
+ return {}
45
+
46
+ def get_managed_agent_attributes(*args, **kwargs):
47
+ return {}
48
+
49
+ def get_model_attributes(*args, **kwargs):
50
+ return {}
51
+
52
+ def get_stream_attributes(*args, **kwargs):
53
+ return {}
54
+
55
+
56
+ class SmolagentsInstrumentor(CommonInstrumentor):
57
+ """Instrumentor for SmoLAgents library."""
58
+
59
+ def __init__(self):
60
+ """Initialize the SmoLAgents instrumentor."""
61
+ # Create instrumentor config
62
+ config = InstrumentorConfig(
63
+ library_name=LIBRARY_NAME,
64
+ library_version=LIBRARY_VERSION,
65
+ wrapped_methods=[], # We use custom wrapping
66
+ metrics_enabled=True,
67
+ dependencies=["smolagents >= 1.0.0", "litellm"],
68
+ )
69
+
70
+ super().__init__(config)
71
+
72
+ def _create_metrics(self, meter: Meter) -> Dict[str, Any]:
73
+ """Create metrics for the instrumentor.
74
+
75
+ Returns a dictionary of metric name to metric instance.
76
+ """
77
+ # Create standard metrics for LLM operations
78
+ return StandardMetrics.create_standard_metrics(meter)
79
+
80
+ def _custom_wrap(self, **kwargs):
81
+ """Apply custom wrapping for SmoLAgents.
82
+
83
+ This is called after normal wrapping, but we use it for all wrapping
84
+ since we don't have normal wrapped methods.
85
+ """
86
+ # Core agent operations
87
+ wrap_function_wrapper("smolagents.agents", "CodeAgent.run", self._agent_run_wrapper(self._tracer))
88
+ wrap_function_wrapper("smolagents.agents", "ToolCallingAgent.run", self._agent_run_wrapper(self._tracer))
89
+
90
+ # Tool calling operations
91
+ wrap_function_wrapper(
92
+ "smolagents.agents", "ToolCallingAgent.execute_tool_call", self._tool_execution_wrapper(self._tracer)
93
+ )
94
+
95
+ # Model operations with proper model name extraction
96
+ wrap_function_wrapper("smolagents.models", "LiteLLMModel.generate", self._llm_wrapper(self._tracer))
97
+ wrap_function_wrapper("smolagents.models", "LiteLLMModel.generate_stream", self._llm_wrapper(self._tracer))
98
+
99
+ logger.info("SmoLAgents instrumentation enabled")
100
+
101
+ def _agent_run_wrapper(self, tracer):
102
+ """Wrapper for agent run methods."""
103
+
104
+ def wrapper(wrapped, instance, args, kwargs):
105
+ # Get proper agent name - handle None case
106
+ agent_name = getattr(instance, "name", None)
107
+ if not agent_name: # Handle None, empty string, or missing attribute
108
+ agent_name = instance.__class__.__name__
109
+
110
+ span_name = f"{agent_name}.run"
111
+
112
+ with tracer.start_as_current_span(
113
+ span_name,
114
+ kind=SpanKind.CLIENT,
115
+ ) as span:
116
+ # Extract attributes
117
+ attributes = get_agent_attributes(args=(instance,) + args, kwargs=kwargs)
118
+
119
+ # Fix managed agents attribute
120
+ if hasattr(instance, "managed_agents") and instance.managed_agents:
121
+ managed_agent_names = []
122
+ for agent in instance.managed_agents:
123
+ name = getattr(agent, "name", None)
124
+ if not name: # Handle None case for managed agents too
125
+ name = agent.__class__.__name__
126
+ managed_agent_names.append(name)
127
+ attributes["agent.managed_agents"] = str(managed_agent_names)
128
+ else:
129
+ attributes["agent.managed_agents"] = "[]"
130
+
131
+ for key, value in attributes.items():
132
+ if value is not None:
133
+ span.set_attribute(key, value)
134
+
135
+ try:
136
+ result = wrapped(*args, **kwargs)
137
+
138
+ # Set output attribute
139
+ if result is not None:
140
+ span.set_attribute("agentops.entity.output", str(result))
141
+
142
+ return result
143
+ except Exception as e:
144
+ span.record_exception(e)
145
+ raise
146
+
147
+ return wrapper
148
+
149
+ def _tool_execution_wrapper(self, tracer):
150
+ """Wrapper for tool execution methods."""
151
+
152
+ def wrapper(wrapped, instance, args, kwargs):
153
+ # Extract tool name for better span naming
154
+ tool_name = "unknown"
155
+ if args and len(args) > 0:
156
+ tool_call = args[0]
157
+ if hasattr(tool_call, "function"):
158
+ tool_name = tool_call.function.name
159
+
160
+ span_name = f"tool.{tool_name}" if tool_name != "unknown" else "tool.execute"
161
+
162
+ with tracer.start_as_current_span(
163
+ span_name,
164
+ kind=SpanKind.CLIENT,
165
+ ) as span:
166
+ # Extract tool information from kwargs or args
167
+ tool_params = "{}"
168
+
169
+ # Try to extract tool call information
170
+ if args and len(args) > 0:
171
+ tool_call = args[0]
172
+ if hasattr(tool_call, "function"):
173
+ if hasattr(tool_call.function, "arguments"):
174
+ tool_params = str(tool_call.function.arguments)
175
+
176
+ # Extract attributes
177
+ attributes = get_tool_call_attributes(args=(instance,) + args, kwargs=kwargs)
178
+
179
+ # Override with better tool information if available
180
+ if tool_name != "unknown":
181
+ attributes["tool.name"] = tool_name
182
+ attributes["tool.parameters"] = tool_params
183
+
184
+ for key, value in attributes.items():
185
+ if value is not None:
186
+ span.set_attribute(key, value)
187
+
188
+ try:
189
+ result = wrapped(*args, **kwargs)
190
+
191
+ # Set success status and result
192
+ span.set_attribute("tool.status", "success")
193
+ if result is not None:
194
+ span.set_attribute("tool.result", str(result))
195
+
196
+ return result
197
+ except Exception as e:
198
+ span.set_attribute("tool.status", "error")
199
+ span.record_exception(e)
200
+ raise
201
+
202
+ return wrapper
203
+
204
+ def _llm_wrapper(self, tracer):
205
+ """Wrapper for LLM generation methods with proper model name extraction."""
206
+
207
+ def wrapper(wrapped, instance, args, kwargs):
208
+ # Extract model name from instance
209
+ model_name = getattr(instance, "model_id", "unknown")
210
+
211
+ # Determine if this is streaming
212
+ is_streaming = "generate_stream" in wrapped.__name__
213
+ operation = "generate_stream" if is_streaming else "generate"
214
+ span_name = f"litellm.{operation} ({model_name})" if model_name != "unknown" else f"litellm.{operation}"
215
+
216
+ with tracer.start_as_current_span(
217
+ span_name,
218
+ kind=SpanKind.CLIENT,
219
+ ) as span:
220
+ # Extract attributes
221
+ if is_streaming:
222
+ attributes = get_stream_attributes(args=(instance,) + args, kwargs=kwargs)
223
+ else:
224
+ attributes = get_model_attributes(args=(instance,) + args, kwargs=kwargs)
225
+
226
+ # Ensure model name is properly set
227
+ attributes["gen_ai.request.model"] = model_name
228
+
229
+ for key, value in attributes.items():
230
+ if value is not None:
231
+ span.set_attribute(key, value)
232
+
233
+ try:
234
+ result = wrapped(*args, **kwargs)
235
+
236
+ # Extract response attributes if available
237
+ if result and hasattr(result, "content"):
238
+ span.set_attribute("gen_ai.completion.0.content", str(result.content))
239
+ if result and hasattr(result, "token_usage"):
240
+ token_usage = result.token_usage
241
+ if hasattr(token_usage, "input_tokens"):
242
+ span.set_attribute("gen_ai.usage.prompt_tokens", token_usage.input_tokens)
243
+ if hasattr(token_usage, "output_tokens"):
244
+ span.set_attribute("gen_ai.usage.completion_tokens", token_usage.output_tokens)
245
+
246
+ return result
247
+ except Exception as e:
248
+ span.record_exception(e)
249
+ raise
250
+
251
+ return wrapper
252
+
253
+ def _custom_unwrap(self, **kwargs):
254
+ """Remove custom wrapping from SmoLAgents.
255
+
256
+ This method removes all custom wrapping we applied.
257
+ """
258
+ # Unwrap all instrumented methods
259
+ from opentelemetry.instrumentation.utils import unwrap
260
+
261
+ try:
262
+ unwrap("smolagents.agents", "CodeAgent.run")
263
+ except Exception as e:
264
+ logger.debug(f"Failed to unwrap CodeAgent.run: {e}")
265
+
266
+ try:
267
+ unwrap("smolagents.agents", "ToolCallingAgent.run")
268
+ except Exception as e:
269
+ logger.debug(f"Failed to unwrap ToolCallingAgent.run: {e}")
270
+
271
+ try:
272
+ unwrap("smolagents.agents", "ToolCallingAgent.execute_tool_call")
273
+ except Exception as e:
274
+ logger.debug(f"Failed to unwrap ToolCallingAgent.execute_tool_call: {e}")
275
+
276
+ try:
277
+ unwrap("smolagents.models", "LiteLLMModel.generate")
278
+ except Exception as e:
279
+ logger.debug(f"Failed to unwrap LiteLLMModel.generate: {e}")
280
+
281
+ try:
282
+ unwrap("smolagents.models", "LiteLLMModel.generate_stream")
283
+ except Exception as e:
284
+ logger.debug(f"Failed to unwrap LiteLLMModel.generate_stream: {e}")
285
+
286
+ logger.info("SmoLAgents instrumentation disabled")
@@ -0,0 +1,258 @@
1
+ """Stream wrapper for SmoLAgents model streaming responses."""
2
+
3
+ import time
4
+ import uuid
5
+ from typing import Any, Generator, Optional
6
+ from opentelemetry.trace import Status, StatusCode, Span
7
+
8
+ from agentops.semconv.message import MessageAttributes
9
+ from agentops.semconv.agent import AgentAttributes
10
+ from agentops.semconv.tool import ToolAttributes
11
+ from .attributes.model import get_stream_attributes
12
+ from agentops.semconv.span_attributes import SpanAttributes
13
+
14
+
15
+ def model_stream_wrapper(tracer):
16
+ """Wrapper for model streaming methods.
17
+
18
+ Args:
19
+ tracer: OpenTelemetry tracer
20
+
21
+ Returns:
22
+ Wrapped function
23
+ """
24
+
25
+ def wrapper(wrapped, instance, args, kwargs):
26
+ messages = kwargs.get("messages", [])
27
+ model_id = instance.model_id if hasattr(instance, "model_id") else "unknown"
28
+
29
+ with tracer.start_as_current_span(
30
+ name=f"{model_id}.generate_stream", attributes=get_stream_attributes(model_id=model_id, messages=messages)
31
+ ) as span:
32
+ try:
33
+ # Start streaming
34
+ stream = wrapped(*args, **kwargs)
35
+ first_token_received = False
36
+ start_time = time.time()
37
+ accumulated_text = ""
38
+
39
+ # Process stream
40
+ for chunk in stream:
41
+ if not first_token_received:
42
+ first_token_received = True
43
+ span.set_attribute("gen_ai.time_to_first_token", time.time() - start_time)
44
+
45
+ # Accumulate text and update attributes
46
+ if hasattr(chunk, "content") and chunk.content:
47
+ accumulated_text += chunk.content
48
+ span.set_attribute(MessageAttributes.COMPLETION_CONTENT.format(i=0), accumulated_text)
49
+ span.set_attribute(MessageAttributes.COMPLETION_TYPE.format(i=0), "text")
50
+
51
+ yield chunk
52
+
53
+ # Set final attributes
54
+ span.set_attribute("gen_ai.streaming_duration", time.time() - start_time)
55
+ span.set_status(Status(StatusCode.OK))
56
+
57
+ except Exception as e:
58
+ span.set_status(Status(StatusCode.ERROR, str(e)))
59
+ raise
60
+
61
+ return wrapper
62
+
63
+
64
+ def agent_stream_wrapper(tracer):
65
+ """Wrapper for agent streaming methods.
66
+
67
+ Args:
68
+ tracer: OpenTelemetry tracer
69
+
70
+ Returns:
71
+ Wrapped function
72
+ """
73
+
74
+ def wrapper(wrapped, instance, args, kwargs):
75
+ task = kwargs.get("task", args[0] if args else "unknown")
76
+ agent_type = instance.__class__.__name__
77
+ agent_id = str(uuid.uuid4())
78
+
79
+ with tracer.start_as_current_span(
80
+ name=f"{agent_type}.run_stream",
81
+ attributes={
82
+ AgentAttributes.AGENT_ID: agent_id,
83
+ AgentAttributes.AGENT_NAME: agent_type,
84
+ AgentAttributes.AGENT_ROLE: "executor",
85
+ AgentAttributes.AGENT_REASONING: task,
86
+ },
87
+ ) as span:
88
+ try:
89
+ # Initialize counters
90
+ step_count = 0
91
+ planning_steps = 0
92
+ tools_used = set()
93
+ start_time = time.time()
94
+
95
+ # Process stream
96
+ stream = wrapped(*args, **kwargs)
97
+ for step in stream:
98
+ step_count += 1
99
+
100
+ # Track step types
101
+ if hasattr(step, "type"):
102
+ if step.type == "planning":
103
+ planning_steps += 1
104
+ elif step.type == "tool_call":
105
+ tools_used.add(step.tool_name)
106
+ # Add tool-specific attributes
107
+ span.set_attribute(ToolAttributes.TOOL_NAME, step.tool_name)
108
+ if hasattr(step, "arguments"):
109
+ span.set_attribute(ToolAttributes.TOOL_PARAMETERS, step.arguments)
110
+
111
+ # Update span attributes
112
+ span.set_attribute("agent.step_count", step_count)
113
+ span.set_attribute("agent.planning_steps", planning_steps)
114
+ span.set_attribute(AgentAttributes.AGENT_TOOLS, list(tools_used))
115
+
116
+ yield step
117
+
118
+ # Set final attributes
119
+ span.set_attribute("agent.execution_time", time.time() - start_time)
120
+ span.set_status(Status(StatusCode.OK))
121
+
122
+ except Exception as e:
123
+ span.set_status(Status(StatusCode.ERROR, str(e)))
124
+ raise
125
+
126
+ return wrapper
127
+
128
+
129
+ class SmoLAgentsStreamWrapper:
130
+ """Wrapper for streaming responses from SmoLAgents models."""
131
+
132
+ def __init__(
133
+ self,
134
+ stream: Generator,
135
+ span: Span,
136
+ model_id: Optional[str] = None,
137
+ ):
138
+ """Initialize the stream wrapper.
139
+
140
+ Args:
141
+ stream: The original generator from the model
142
+ span: The OpenTelemetry span to track the stream
143
+ model_id: Optional model identifier
144
+ """
145
+ self._stream = stream
146
+ self._span = span
147
+ self._model_id = model_id
148
+ self._chunks_received = 0
149
+ self._full_content = []
150
+ self._tool_calls = []
151
+ self._current_tool_call = None
152
+ self._token_count = 0
153
+
154
+ def __iter__(self):
155
+ """Iterate over the stream."""
156
+ return self
157
+
158
+ def __next__(self):
159
+ """Get the next chunk from the stream."""
160
+ try:
161
+ chunk = next(self._stream)
162
+ self._process_chunk(chunk)
163
+ return chunk
164
+ except StopIteration:
165
+ self._finalize_stream()
166
+ raise
167
+
168
+ def _process_chunk(self, chunk: Any) -> None:
169
+ """Process a chunk from the stream.
170
+
171
+ Args:
172
+ chunk: The chunk to process
173
+ """
174
+ self._chunks_received += 1
175
+
176
+ # Handle ChatMessageStreamDelta objects
177
+ if hasattr(chunk, "content") and chunk.content:
178
+ self._full_content.append(chunk.content)
179
+
180
+ # Handle tool calls in chunks
181
+ if hasattr(chunk, "tool_calls") and chunk.tool_calls:
182
+ for tool_call in chunk.tool_calls:
183
+ if tool_call.id not in [tc["id"] for tc in self._tool_calls]:
184
+ self._tool_calls.append(
185
+ {
186
+ "id": tool_call.id,
187
+ "type": tool_call.type,
188
+ "name": tool_call.function.name,
189
+ "arguments": tool_call.function.arguments,
190
+ }
191
+ )
192
+
193
+ # Track token usage if available
194
+ if hasattr(chunk, "token_usage") and chunk.token_usage:
195
+ if hasattr(chunk.token_usage, "output_tokens"):
196
+ self._token_count += chunk.token_usage.output_tokens
197
+
198
+ # Update span with chunk information
199
+ self._span.add_event(
200
+ "stream_chunk_received",
201
+ {
202
+ "chunk_number": self._chunks_received,
203
+ "chunk_content_length": len(chunk.content) if hasattr(chunk, "content") and chunk.content else 0,
204
+ },
205
+ )
206
+
207
+ def _finalize_stream(self) -> None:
208
+ """Finalize the stream and update span attributes."""
209
+ # Combine all content chunks
210
+ full_content = "".join(self._full_content)
211
+
212
+ # Set final attributes on the span
213
+ attributes = {
214
+ MessageAttributes.COMPLETION_CONTENT.format(i=0): full_content,
215
+ "stream.chunks_received": self._chunks_received,
216
+ "stream.total_content_length": len(full_content),
217
+ }
218
+
219
+ # Add tool calls if any
220
+ if self._tool_calls:
221
+ for j, tool_call in enumerate(self._tool_calls):
222
+ attributes.update(
223
+ {
224
+ MessageAttributes.COMPLETION_TOOL_CALL_ID.format(i=0, j=j): tool_call["id"],
225
+ MessageAttributes.COMPLETION_TOOL_CALL_TYPE.format(i=0, j=j): tool_call["type"],
226
+ MessageAttributes.COMPLETION_TOOL_CALL_NAME.format(i=0, j=j): tool_call["name"],
227
+ MessageAttributes.COMPLETION_TOOL_CALL_ARGUMENTS.format(i=0, j=j): str(tool_call["arguments"]),
228
+ }
229
+ )
230
+
231
+ # Add token usage if tracked
232
+ if self._token_count > 0:
233
+ attributes[SpanAttributes.LLM_USAGE_STREAMING_TOKENS] = self._token_count
234
+
235
+ self._span.set_attributes(attributes)
236
+
237
+ def close(self) -> None:
238
+ """Close the stream wrapper."""
239
+ if hasattr(self._stream, "close"):
240
+ self._stream.close()
241
+
242
+
243
+ def wrap_stream(
244
+ stream: Generator,
245
+ span: Span,
246
+ model_id: Optional[str] = None,
247
+ ) -> SmoLAgentsStreamWrapper:
248
+ """Wrap a streaming response from a SmoLAgents model.
249
+
250
+ Args:
251
+ stream: The original generator from the model
252
+ span: The OpenTelemetry span to track the stream
253
+ model_id: Optional model identifier
254
+
255
+ Returns:
256
+ SmoLAgentsStreamWrapper: The wrapped stream
257
+ """
258
+ return SmoLAgentsStreamWrapper(stream, span, model_id)
@@ -0,0 +1,15 @@
1
+ """Xpander SDK instrumentation for AgentOps."""
2
+
3
+ from agentops.instrumentation.agentic.xpander.instrumentor import XpanderInstrumentor
4
+ from agentops.instrumentation.agentic.xpander.trace_probe import (
5
+ wrap_openai_call_for_xpander,
6
+ is_xpander_session_active,
7
+ get_active_xpander_session,
8
+ )
9
+
10
+ __all__ = [
11
+ "XpanderInstrumentor",
12
+ "wrap_openai_call_for_xpander",
13
+ "is_xpander_session_active",
14
+ "get_active_xpander_session",
15
+ ]
@@ -0,0 +1,112 @@
1
+ """Xpander context management for session tracking."""
2
+
3
+ import time
4
+ import threading
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ class XpanderContext:
9
+ """Context manager for Xpander sessions with nested conversation spans."""
10
+
11
+ def __init__(self):
12
+ self._sessions = {} # session_id -> session_data
13
+ self._workflow_spans = {} # session_id -> active workflow span
14
+ self._agent_spans = {} # session_id -> active agent span
15
+ self._conversation_spans = {} # session_id -> active conversation span
16
+ self._conversation_counters = {} # session_id -> conversation counter
17
+ self._lock = threading.Lock()
18
+
19
+ def start_session(self, session_id: str, agent_info: Dict[str, Any], workflow_span=None, agent_span=None) -> None:
20
+ """Start a new session with agent info."""
21
+ with self._lock:
22
+ self._sessions[session_id] = {
23
+ "agent_name": agent_info.get("agent_name", "unknown"),
24
+ "agent_id": agent_info.get("agent_id", "unknown"),
25
+ "task_input": agent_info.get("task_input"),
26
+ "phase": "planning",
27
+ "step_count": 0,
28
+ "total_tokens": 0,
29
+ "tools_executed": [],
30
+ "start_time": time.time(),
31
+ }
32
+ if workflow_span:
33
+ self._workflow_spans[session_id] = workflow_span
34
+ if agent_span:
35
+ self._agent_spans[session_id] = agent_span
36
+
37
+ # Initialize conversation counter
38
+ self._conversation_counters[session_id] = 0
39
+
40
+ def start_conversation(self, session_id: str, conversation_span) -> None:
41
+ """Start a new conversation within the session."""
42
+ with self._lock:
43
+ self._conversation_spans[session_id] = conversation_span
44
+ self._conversation_counters[session_id] = self._conversation_counters.get(session_id, 0) + 1
45
+
46
+ def end_conversation(self, session_id: str) -> None:
47
+ """End the current conversation."""
48
+ with self._lock:
49
+ if session_id in self._conversation_spans:
50
+ del self._conversation_spans[session_id]
51
+
52
+ def has_active_conversation(self, session_id: str) -> bool:
53
+ """Check if there's an active conversation for this session."""
54
+ with self._lock:
55
+ return session_id in self._conversation_spans
56
+
57
+ def get_conversation_counter(self, session_id: str) -> int:
58
+ """Get the current conversation counter."""
59
+ with self._lock:
60
+ return self._conversation_counters.get(session_id, 0)
61
+
62
+ def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
63
+ """Get session data."""
64
+ with self._lock:
65
+ return self._sessions.get(session_id)
66
+
67
+ def update_session(self, session_id: str, updates: Dict[str, Any]) -> None:
68
+ """Update session data."""
69
+ with self._lock:
70
+ if session_id in self._sessions:
71
+ self._sessions[session_id].update(updates)
72
+
73
+ def end_session(self, session_id: str) -> None:
74
+ """End a session."""
75
+ with self._lock:
76
+ if session_id in self._sessions:
77
+ del self._sessions[session_id]
78
+ if session_id in self._workflow_spans:
79
+ del self._workflow_spans[session_id]
80
+ if session_id in self._agent_spans:
81
+ del self._agent_spans[session_id]
82
+ if session_id in self._conversation_spans:
83
+ del self._conversation_spans[session_id]
84
+ if session_id in self._conversation_counters:
85
+ del self._conversation_counters[session_id]
86
+
87
+ def get_workflow_phase(self, session_id: str) -> str:
88
+ """Detect current workflow phase based on state."""
89
+ with self._lock:
90
+ session = self._sessions.get(session_id, {})
91
+
92
+ if session.get("tools_executed", []):
93
+ return "executing"
94
+ elif session.get("step_count", 0) > 0:
95
+ return "executing"
96
+ else:
97
+ return "planning"
98
+
99
+ def get_workflow_span(self, session_id: str):
100
+ """Get the active workflow span for a session."""
101
+ with self._lock:
102
+ return self._workflow_spans.get(session_id)
103
+
104
+ def get_agent_span(self, session_id: str):
105
+ """Get the active agent span for a session."""
106
+ with self._lock:
107
+ return self._agent_spans.get(session_id)
108
+
109
+ def get_conversation_span(self, session_id: str):
110
+ """Get the active conversation span for a session."""
111
+ with self._lock:
112
+ return self._conversation_spans.get(session_id)