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