Skip to content

Commit 7f967db

Browse files
committed
Refactor CrewAI instrumentation to enhance span attribute management
1 parent d4be724 commit 7f967db

2 files changed

Lines changed: 197 additions & 95 deletions

File tree

Lines changed: 135 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
1-
from opentelemetry.trace import Span
1+
"""OpenTelemetry instrumentation for CrewAI."""
2+
23
import json
4+
import logging
5+
from typing import Any
6+
from opentelemetry.trace import Span
7+
8+
from agentops.semconv.span_attributes import SpanAttributes
9+
from agentops.semconv.agent import AgentAttributes
10+
from agentops.semconv.workflow import WorkflowAttributes
11+
12+
# Initialize logger for logging potential issues and operations
13+
logger = logging.getLogger(__name__)
314

15+
def _parse_tools(tools):
16+
"""Parse tools into a JSON string with name and description."""
17+
result = []
18+
for tool in tools:
19+
res = {}
20+
if hasattr(tool, "name") and tool.name is not None:
21+
res["name"] = tool.name
22+
if hasattr(tool, "description") and tool.description is not None:
23+
res["description"] = tool.description
24+
if res:
25+
result.append(res)
26+
return json.dumps(result)
427

5-
def set_span_attribute(span: Span, name, value):
6-
if value is not None:
7-
if value != "":
8-
span.set_attribute(name, value)
9-
return
28+
def set_span_attribute(span: Span, key: str, value: Any) -> None:
29+
"""Set a single attribute on a span."""
30+
if value is not None and value != "":
31+
span.set_attribute(key, value)
1032

1133

1234
class CrewAISpanAttributes:
35+
"""Manages span attributes for CrewAI instrumentation."""
36+
1337
def __init__(self, span: Span, instance) -> None:
1438
self.span = span
1539
self.instance = instance
16-
self.crew = {"tasks": [], "agents": [], "llms": []}
1740
self.process_instance()
1841

1942
def process_instance(self):
43+
"""Process the instance based on its type."""
2044
instance_type = self.instance.__class__.__name__
2145
method_mapping = {
2246
"Crew": self._process_crew,
@@ -29,26 +53,7 @@ def process_instance(self):
2953
method()
3054

3155
def _process_crew(self):
32-
self._populate_crew_attributes()
33-
for key, value in self.crew.items():
34-
self._set_attribute(f"crewai.crew.{key}", value)
35-
36-
def _process_agent(self):
37-
agent_data = self._populate_agent_attributes()
38-
for key, value in agent_data.items():
39-
self._set_attribute(f"crewai.agent.{key}", value)
40-
41-
def _process_task(self):
42-
task_data = self._populate_task_attributes()
43-
for key, value in task_data.items():
44-
self._set_attribute(f"crewai.task.{key}", value)
45-
46-
def _process_llm(self):
47-
llm_data = self._populate_llm_attributes()
48-
for key, value in llm_data.items():
49-
self._set_attribute(f"crewai.llm.{key}", value)
50-
51-
def _populate_crew_attributes(self):
56+
"""Process a Crew instance."""
5257
for key, value in self.instance.__dict__.items():
5358
if value is None:
5459
continue
@@ -59,53 +64,117 @@ def _populate_crew_attributes(self):
5964
elif key == "llms":
6065
self._parse_llms(value)
6166
else:
62-
self.crew[key] = str(value)
67+
self._set_attribute(f"crewai.crew.{key}", str(value))
6368

64-
def _populate_agent_attributes(self):
65-
return self._extract_attributes(self.instance)
69+
def _process_agent(self):
70+
"""Process an Agent instance."""
71+
agent = {}
72+
for key, value in self.instance.__dict__.items():
73+
if key == "tools":
74+
value = _parse_tools(value)
75+
if value is None:
76+
continue
77+
agent[key] = str(value)
78+
79+
# Set agent attributes using our semantic conventions
80+
self._set_attribute(AgentAttributes.AGENT_ID, agent.get('id', ''))
81+
self._set_attribute(AgentAttributes.AGENT_ROLE, agent.get('role', ''))
82+
self._set_attribute(AgentAttributes.AGENT_NAME, agent.get('name', ''))
83+
self._set_attribute(AgentAttributes.AGENT_TOOLS, agent.get('tools', ''))
84+
85+
self._set_attribute("crewai.agent.goal", agent.get('goal', ''))
86+
self._set_attribute("crewai.agent.backstory", agent.get('backstory', ''))
87+
self._set_attribute("crewai.agent.cache", agent.get('cache', ''))
88+
self._set_attribute("crewai.agent.allow_delegation", agent.get('allow_delegation', ''))
89+
self._set_attribute("crewai.agent.allow_code_execution", agent.get('allow_code_execution', ''))
90+
self._set_attribute("crewai.agent.max_retry_limit", agent.get('max_retry_limit', ''))
91+
self._set_attribute("crewai.agent.tools_results", agent.get('tools_results', ''))
6692

67-
def _populate_task_attributes(self):
68-
task_data = self._extract_attributes(self.instance)
69-
if "agent" in task_data:
70-
task_data["agent"] = self.instance.agent.role if self.instance.agent else None
71-
return task_data
93+
def _process_task(self):
94+
"""Process a Task instance."""
95+
task = {}
96+
for key, value in self.instance.__dict__.items():
97+
if value is None:
98+
continue
99+
if key == "tools":
100+
value = _parse_tools(value)
101+
task[key] = value
102+
elif key == "agent":
103+
task[key] = value.role if value else None
104+
else:
105+
task[key] = str(value)
106+
107+
# Set task attributes using our semantic conventions
108+
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_NAME, task.get('description', ''))
109+
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_TYPE, "task")
110+
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_INPUT, task.get('context', ''))
111+
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_OUTPUT, task.get('expected_output', ''))
112+
113+
self._set_attribute("crewai.task.id", task.get('id', ''))
114+
self._set_attribute("crewai.task.agent", task.get('agent', ''))
115+
self._set_attribute("crewai.task.human_input", task.get('human_input', ''))
116+
self._set_attribute("crewai.task.output", task.get('output', ''))
117+
self._set_attribute("crewai.task.processed_by_agents", str(task.get('processed_by_agents', '')))
72118

73-
def _populate_llm_attributes(self):
74-
return self._extract_attributes(self.instance)
119+
def _process_llm(self):
120+
"""Process an LLM instance."""
121+
llm = {}
122+
for key, value in self.instance.__dict__.items():
123+
if value is None:
124+
continue
125+
llm[key] = str(value)
126+
127+
# Set LLM attributes using our semantic conventions
128+
self._set_attribute(SpanAttributes.LLM_REQUEST_MODEL, llm.get('model_name', ''))
129+
self._set_attribute(SpanAttributes.LLM_REQUEST_TEMPERATURE, llm.get('temperature', ''))
130+
self._set_attribute(SpanAttributes.LLM_REQUEST_MAX_TOKENS, llm.get('max_tokens', ''))
131+
self._set_attribute(SpanAttributes.LLM_REQUEST_TOP_P, llm.get('top_p', ''))
75132

76133
def _parse_agents(self, agents):
77-
self.crew["agents"] = [self._extract_agent_data(agent) for agent in agents if agent is not None]
134+
"""Parse agents into a list of dictionaries."""
135+
for agent in agents:
136+
if agent is not None:
137+
agent_data = self._extract_agent_data(agent)
138+
for key, value in agent_data.items():
139+
self._set_attribute(f"crewai.agent.{key}", value)
78140

79141
def _parse_tasks(self, tasks):
80-
self.crew["tasks"] = [
81-
{
82-
"agent": task.agent.role if task.agent else None,
83-
"description": task.description,
84-
"async_execution": task.async_execution,
85-
"expected_output": task.expected_output,
86-
"human_input": task.human_input,
87-
"tools": task.tools,
88-
"output_file": task.output_file,
89-
}
90-
for task in tasks
91-
]
142+
"""Parse tasks into a list of dictionaries."""
143+
for task in tasks:
144+
if task is not None:
145+
task_data = {
146+
"agent": task.agent.role if task.agent else None,
147+
"description": task.description,
148+
"async_execution": task.async_execution,
149+
"expected_output": task.expected_output,
150+
"human_input": task.human_input,
151+
"tools": task.tools,
152+
"output_file": task.output_file,
153+
}
154+
for key, value in task_data.items():
155+
if value is not None:
156+
self._set_attribute(f"crewai.task.{key}", str(value))
92157

93158
def _parse_llms(self, llms):
94-
self.crew["tasks"] = [
95-
{
96-
"temperature": llm.temperature,
97-
"max_tokens": llm.max_tokens,
98-
"max_completion_tokens": llm.max_completion_tokens,
99-
"top_p": llm.top_p,
100-
"n": llm.n,
101-
"seed": llm.seed,
102-
"base_url": llm.base_url,
103-
"api_version": llm.api_version,
104-
}
105-
for llm in llms
106-
]
159+
"""Parse LLMs into a list of dictionaries."""
160+
for llm in llms:
161+
if llm is not None:
162+
llm_data = {
163+
"temperature": llm.temperature,
164+
"max_tokens": llm.max_tokens,
165+
"max_completion_tokens": llm.max_completion_tokens,
166+
"top_p": llm.top_p,
167+
"n": llm.n,
168+
"seed": llm.seed,
169+
"base_url": llm.base_url,
170+
"api_version": llm.api_version,
171+
}
172+
for key, value in llm_data.items():
173+
if value is not None:
174+
self._set_attribute(f"crewai.llm.{key}", str(value))
107175

108176
def _extract_agent_data(self, agent):
177+
"""Extract data from an agent."""
109178
model = getattr(agent.llm, "model", None) or getattr(agent.llm, "model_name", None) or ""
110179

111180
return {
@@ -122,22 +191,7 @@ def _extract_agent_data(self, agent):
122191
"llm": str(model),
123192
}
124193

125-
def _extract_attributes(self, obj):
126-
attributes = {}
127-
for key, value in obj.__dict__.items():
128-
if value is None:
129-
continue
130-
if key == "tools":
131-
attributes[key] = self._serialize_tools(value)
132-
else:
133-
attributes[key] = str(value)
134-
return attributes
135-
136-
def _serialize_tools(self, tools):
137-
return json.dumps(
138-
[{k: v for k, v in vars(tool).items() if v is not None and k in ["name", "description"]} for tool in tools]
139-
)
140-
141194
def _set_attribute(self, key, value):
142-
if value:
143-
set_span_attribute(self.span, key, str(value) if isinstance(value, list) else value)
195+
"""Set an attribute on the span."""
196+
if value is not None and value != "":
197+
set_span_attribute(self.span, key, value)

0 commit comments

Comments
 (0)