Skip to content

Commit b1b500a

Browse files
committed
Add SmoLAgents instrumentation
1 parent a3d41f3 commit b1b500a

8 files changed

Lines changed: 663 additions & 0 deletions

File tree

agentops/instrumentation/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ def get_instance(self) -> BaseInstrumentor:
8787
class_name="AG2Instrumentor",
8888
provider_import_name="autogen",
8989
),
90+
InstrumentorLoader(
91+
module_name="agentops.instrumentation.smolagents",
92+
class_name="SmoLAgentsInstrumentor",
93+
provider_import_name="smolagents",
94+
),
9095
]
9196

9297

agentops/instrumentation/common/attributes.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,27 @@ def get_base_span_attributes(span: Any) -> AttributeMap:
252252
attributes[CoreAttributes.PARENT_ID] = parent_id
253253

254254
return attributes
255+
256+
257+
def extract_token_usage(response: Any) -> Dict[str, int]:
258+
"""Extract token usage information from a response.
259+
260+
Args:
261+
response: The response object to extract token usage from
262+
263+
Returns:
264+
Dictionary containing token usage information
265+
"""
266+
usage = {}
267+
268+
# Try to extract token counts from response
269+
if hasattr(response, "usage"):
270+
usage_data = response.usage
271+
if hasattr(usage_data, "prompt_tokens"):
272+
usage["prompt_tokens"] = usage_data.prompt_tokens
273+
if hasattr(usage_data, "completion_tokens"):
274+
usage["completion_tokens"] = usage_data.completion_tokens
275+
if hasattr(usage_data, "total_tokens"):
276+
usage["total_tokens"] = usage_data.total_tokens
277+
278+
return usage
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# SmoLAgents Instrumentation
2+
3+
This module provides OpenTelemetry instrumentation for the SmoLAgents framework. It captures telemetry data from model operations, agent executions, and tool usage.
4+
5+
## Features
6+
7+
- Model operation tracking
8+
- Text generation
9+
- Token usage
10+
- Streaming responses
11+
- Latency metrics
12+
13+
- Agent execution monitoring
14+
- Step-by-step execution
15+
- Planning phases
16+
- Tool usage
17+
- Execution time
18+
19+
- Tool usage analytics
20+
- Tool call patterns
21+
- Success/failure rates
22+
- Execution time
23+
- Error tracking
24+
25+
## Usage
26+
27+
```python
28+
from agentops import init
29+
from agentops.instrumentation.smolagents import SmoLAgentsInstrumentor
30+
31+
# Initialize AgentOps with your API key
32+
init(api_key="your-api-key")
33+
34+
# The instrumentation will be automatically activated
35+
# All SmoLAgents operations will now be tracked
36+
```
37+
38+
## Metrics Collected
39+
40+
1. Token Usage
41+
- Input tokens
42+
- Output tokens
43+
- Total tokens per operation
44+
45+
2. Timing Metrics
46+
- Operation duration
47+
- Time to first token (streaming)
48+
- Tool execution time
49+
- Planning phase duration
50+
51+
3. Agent Metrics
52+
- Step counts
53+
- Planning steps
54+
- Tools used
55+
- Success/failure rates
56+
57+
4. Error Tracking
58+
- Generation errors
59+
- Tool execution errors
60+
- Parsing errors
61+
62+
## Architecture
63+
64+
The instrumentation is built on OpenTelemetry and follows the same pattern as other AgentOps instrumentors:
65+
66+
1. Attribute Extractors
67+
- Model attributes
68+
- Agent attributes
69+
- Tool call attributes
70+
71+
2. Wrappers
72+
- Method wrappers for sync operations
73+
- Stream wrappers for async operations
74+
- Context propagation handling
75+
76+
3. Metrics
77+
- Histograms for distributions
78+
- Counters for events
79+
- Custom attributes for filtering
80+
81+
## Contributing
82+
83+
When adding new features or modifying existing ones:
84+
85+
1. Follow the established pattern for attribute extraction
86+
2. Maintain context propagation
87+
3. Add appropriate error handling
88+
4. Update tests and documentation
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""SmoLAgents instrumentation for AgentOps."""
2+
from .instrumentor import SmoLAgentsInstrumentor
3+
4+
LIBRARY_NAME = "smolagents"
5+
LIBRARY_VERSION = "1.16.0"
6+
7+
__all__ = ["SmoLAgentsInstrumentor"]
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Attribute extractors for SmoLAgents agent operations."""
2+
3+
from typing import Any, Dict, Optional, Tuple
4+
import uuid
5+
import time
6+
7+
from agentops.instrumentation.common.attributes import get_common_attributes
8+
from agentops.semconv.agent import AgentAttributes
9+
from agentops.semconv.tool import ToolAttributes
10+
11+
12+
def get_agent_attributes(
13+
args: Optional[Tuple] = None,
14+
kwargs: Optional[Dict] = None,
15+
return_value: Optional[Any] = None,
16+
) -> Dict[str, Any]:
17+
"""Extract attributes from an agent execution.
18+
19+
Args:
20+
args: Optional tuple of positional arguments
21+
kwargs: Optional dict of keyword arguments
22+
return_value: Optional return value from the wrapped function
23+
24+
Returns:
25+
Dict containing extracted attributes
26+
"""
27+
attributes = get_common_attributes()
28+
29+
# Extract agent info from instance
30+
if args and len(args) > 0:
31+
instance = args[0]
32+
agent_type = instance.__class__.__name__
33+
tools = [t.name for t in instance.tools] if hasattr(instance, "tools") else []
34+
attributes.update(
35+
{
36+
AgentAttributes.AGENT_ID: str(uuid.uuid4()),
37+
AgentAttributes.AGENT_NAME: agent_type,
38+
AgentAttributes.AGENT_ROLE: "executor",
39+
AgentAttributes.AGENT_TOOLS: tools,
40+
}
41+
)
42+
43+
# Extract task from kwargs or args
44+
task = kwargs.get("task", args[1] if len(args) > 1 else "unknown") if kwargs else "unknown"
45+
attributes[AgentAttributes.AGENT_REASONING] = task
46+
47+
return attributes
48+
49+
50+
def get_tool_call_attributes(
51+
args: Optional[Tuple] = None,
52+
kwargs: Optional[Dict] = None,
53+
return_value: Optional[Any] = None,
54+
) -> Dict[str, Any]:
55+
"""Extract attributes from a tool call.
56+
57+
Args:
58+
args: Optional tuple of positional arguments
59+
kwargs: Optional dict of keyword arguments
60+
return_value: Optional return value from the wrapped function
61+
62+
Returns:
63+
Dict containing extracted attributes
64+
"""
65+
attributes = get_common_attributes()
66+
67+
# Extract tool info from instance and args
68+
if args and len(args) > 0:
69+
instance = args[0]
70+
tool_name = instance.name if hasattr(instance, "name") else "unknown"
71+
tool_description = instance.description if hasattr(instance, "description") else "unknown"
72+
73+
# Get arguments from args/kwargs
74+
arguments = {}
75+
if len(args) > 1:
76+
arguments = args[1]
77+
elif kwargs:
78+
arguments = kwargs
79+
80+
# Track execution time and success
81+
start_time = time.time()
82+
error = None
83+
84+
try:
85+
if return_value is not None:
86+
execution_time = time.time() - start_time
87+
attributes.update(
88+
{
89+
ToolAttributes.TOOL_ID: str(uuid.uuid4()),
90+
ToolAttributes.TOOL_NAME: tool_name,
91+
ToolAttributes.TOOL_DESCRIPTION: tool_description,
92+
ToolAttributes.TOOL_PARAMETERS: arguments,
93+
ToolAttributes.TOOL_STATUS: "success",
94+
ToolAttributes.TOOL_RESULT: str(return_value),
95+
"tool.execution_time": execution_time,
96+
}
97+
)
98+
except Exception as e:
99+
error = str(e)
100+
attributes.update(
101+
{
102+
ToolAttributes.TOOL_ID: str(uuid.uuid4()),
103+
ToolAttributes.TOOL_NAME: tool_name,
104+
ToolAttributes.TOOL_DESCRIPTION: tool_description,
105+
ToolAttributes.TOOL_PARAMETERS: arguments,
106+
ToolAttributes.TOOL_STATUS: "error",
107+
ToolAttributes.TOOL_ERROR: error,
108+
}
109+
)
110+
111+
return attributes
112+
113+
114+
def get_planning_step_attributes(
115+
args: Optional[Tuple] = None,
116+
kwargs: Optional[Dict] = None,
117+
return_value: Optional[Any] = None,
118+
) -> Dict[str, Any]:
119+
"""Extract attributes from a planning step.
120+
121+
Args:
122+
args: Optional tuple of positional arguments
123+
kwargs: Optional dict of keyword arguments
124+
return_value: Optional return value from the wrapped function
125+
126+
Returns:
127+
Dict containing extracted attributes
128+
"""
129+
attributes = get_common_attributes()
130+
131+
# Extract step info from kwargs
132+
if kwargs:
133+
step_number = kwargs.get("step_number", 0)
134+
is_first_step = kwargs.get("is_first_step", False)
135+
task = kwargs.get("task", "unknown")
136+
137+
attributes.update(
138+
{
139+
AgentAttributes.AGENT_REASONING: task,
140+
"planning.step_number": step_number,
141+
"planning.is_first_step": is_first_step,
142+
}
143+
)
144+
145+
return attributes

0 commit comments

Comments
 (0)