opentelemetry-instrumentation-crewai 0.36.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.1
2
+ Name: opentelemetry-instrumentation-crewai
3
+ Version: 0.36.0
4
+ Summary: OpenTelemetry crewAI instrumentation
5
+ Home-page: https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-crewai
6
+ License: Apache-2.0
7
+ Author: Gal Kleinman
8
+ Author-email: gal@traceloop.com
9
+ Requires-Python: >=3.10,<=3.13
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Provides-Extra: instruments
16
+ Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
17
+ Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
18
+ Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
19
+ Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.2)
20
+ Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-crewai
21
+ Description-Content-Type: text/markdown
22
+
23
+ # OpenTelemetry CrewAI Instrumentation
24
+
25
+ <a href="https://pypi.org/project/opentelemetry-instrumentation-crewai/">
26
+ <img src="https://badge.fury.io/py/opentelemetry-instrumentation-crewai.svg">
27
+ </a>
28
+
29
+ This library allows tracing agentic workflows implemented with crewAI framework [crewAI library](https://github.com/crewAIInc/crewAI).
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install opentelemetry-instrumentation-crewai
35
+ ```
36
+
37
+ ## Example usage
38
+
39
+ ```python
40
+ from opentelemetry.instrumentation.crewai import CrewAIInstrumentor
41
+
42
+ CrewAIInstrumentor().instrument()
43
+ ```
44
+
45
+ ## Privacy
46
+
47
+ **By default, this instrumentation logs prompts, completions, and embeddings to span attributes**. This gives you a clear visibility into how your LLM application is working, and can make it easy to debug and evaluate the quality of the outputs.
48
+
49
+ However, you may want to disable this logging for privacy reasons, as they may contain highly sensitive data from your users. You may also simply want to reduce the size of your traces.
50
+
51
+ To disable logging, set the `TRACELOOP_TRACE_CONTENT` environment variable to `false`.
52
+
53
+ ```bash
54
+ TRACELOOP_TRACE_CONTENT=false
55
+ ```
56
+
@@ -0,0 +1,33 @@
1
+ # OpenTelemetry CrewAI Instrumentation
2
+
3
+ <a href="https://pypi.org/project/opentelemetry-instrumentation-crewai/">
4
+ <img src="https://badge.fury.io/py/opentelemetry-instrumentation-crewai.svg">
5
+ </a>
6
+
7
+ This library allows tracing agentic workflows implemented with crewAI framework [crewAI library](https://github.com/crewAIInc/crewAI).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install opentelemetry-instrumentation-crewai
13
+ ```
14
+
15
+ ## Example usage
16
+
17
+ ```python
18
+ from opentelemetry.instrumentation.crewai import CrewAIInstrumentor
19
+
20
+ CrewAIInstrumentor().instrument()
21
+ ```
22
+
23
+ ## Privacy
24
+
25
+ **By default, this instrumentation logs prompts, completions, and embeddings to span attributes**. This gives you a clear visibility into how your LLM application is working, and can make it easy to debug and evaluate the quality of the outputs.
26
+
27
+ However, you may want to disable this logging for privacy reasons, as they may contain highly sensitive data from your users. You may also simply want to reduce the size of your traces.
28
+
29
+ To disable logging, set the `TRACELOOP_TRACE_CONTENT` environment variable to `false`.
30
+
31
+ ```bash
32
+ TRACELOOP_TRACE_CONTENT=false
33
+ ```
@@ -0,0 +1,5 @@
1
+ """OpenTelemetry CrewAI instrumentation"""
2
+ from opentelemetry.instrumentation.crewai.version import __version__
3
+ from opentelemetry.instrumentation.crewai.instrumentation import CrewAIInstrumentor
4
+
5
+ __all__ = ["CrewAIInstrumentor", "__version__"]
@@ -0,0 +1,150 @@
1
+ from opentelemetry.trace import Span
2
+ import json
3
+
4
+
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
10
+
11
+
12
+ class CrewAISpanAttributes:
13
+ def __init__(self, span: Span, instance) -> None:
14
+ self.span = span
15
+ self.instance = instance
16
+ self.crew = {"tasks": [], "agents": [], "llms": []}
17
+ self.process_instance()
18
+
19
+ def process_instance(self):
20
+ instance_type = self.instance.__class__.__name__
21
+ method_mapping = {
22
+ "Crew": self._process_crew,
23
+ "Agent": self._process_agent,
24
+ "Task": self._process_task,
25
+ "LLM": self._process_llm,
26
+ }
27
+ method = method_mapping.get(instance_type)
28
+ if method:
29
+ method()
30
+
31
+ 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):
52
+ for key, value in self.instance.__dict__.items():
53
+ if value is None:
54
+ continue
55
+ if key == "tasks":
56
+ self._parse_tasks(value)
57
+ elif key == "agents":
58
+ self._parse_agents(value)
59
+ elif key == "llms":
60
+ self._parse_llms(value)
61
+ else:
62
+ self.crew[key] = str(value)
63
+
64
+ def _populate_agent_attributes(self):
65
+ return self._extract_attributes(self.instance)
66
+
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
72
+
73
+ def _populate_llm_attributes(self):
74
+ return self._extract_attributes(self.instance)
75
+
76
+ def _parse_agents(self, agents):
77
+ self.crew["agents"] = [
78
+ self._extract_agent_data(agent) for agent in agents if agent is not None
79
+ ]
80
+
81
+ def _parse_tasks(self, tasks):
82
+ self.crew["tasks"] = [
83
+ {
84
+ "agent": task.agent.role if task.agent else None,
85
+ "description": task.description,
86
+ "async_execution": task.async_execution,
87
+ "expected_output": task.expected_output,
88
+ "human_input": task.human_input,
89
+ "tools": task.tools,
90
+ "output_file": task.output_file,
91
+ }
92
+ for task in tasks
93
+ ]
94
+
95
+ def _parse_llms(self, llms):
96
+ self.crew["tasks"] = [
97
+ {
98
+ "temperature": llm.temperature,
99
+ "max_tokens": llm.max_tokens,
100
+ "max_completion_tokens": llm.max_completion_tokens,
101
+ "top_p": llm.top_p,
102
+ "n": llm.n,
103
+ "seed": llm.seed,
104
+ "base_url": llm.base_url,
105
+ "api_version": llm.api_version, }
106
+ for llm in llms
107
+ ]
108
+
109
+ def _extract_agent_data(self, agent):
110
+ model = (
111
+ getattr(agent.llm, "model", None)
112
+ or getattr(agent.llm, "model_name", None)
113
+ or ""
114
+ )
115
+
116
+ return {
117
+ "id": str(agent.id),
118
+ "role": agent.role,
119
+ "goal": agent.goal,
120
+ "backstory": agent.backstory,
121
+ "cache": agent.cache,
122
+ "config": agent.config,
123
+ "verbose": agent.verbose,
124
+ "allow_delegation": agent.allow_delegation,
125
+ "tools": agent.tools,
126
+ "max_iter": agent.max_iter,
127
+ "llm": str(model), }
128
+
129
+ def _extract_attributes(self, obj):
130
+ attributes = {}
131
+ for key, value in obj.__dict__.items():
132
+ if value is None:
133
+ continue
134
+ if key == "tools":
135
+ attributes[key] = self._serialize_tools(value)
136
+ else:
137
+ attributes[key] = str(value)
138
+ return attributes
139
+
140
+ def _serialize_tools(self, tools):
141
+ return json.dumps(
142
+ [
143
+ {k: v for k, v in vars(tool).items() if v is not None and k in ["name", "description"]}
144
+ for tool in tools
145
+ ]
146
+ )
147
+
148
+ def _set_attribute(self, key, value):
149
+ if value:
150
+ set_span_attribute(self.span, key, str(value) if isinstance(value, list) else value)
@@ -0,0 +1,127 @@
1
+ from typing import Collection
2
+ from wrapt import wrap_function_wrapper
3
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
4
+ from opentelemetry.trace import SpanKind, get_tracer
5
+ from opentelemetry.trace.status import Status, StatusCode
6
+ from opentelemetry.instrumentation.crewai.version import __version__
7
+ from opentelemetry.semconv_ai import SpanAttributes, TraceloopSpanKindValues
8
+ from .crewai_span_attributes import CrewAISpanAttributes, set_span_attribute
9
+
10
+ _instruments = ("crewai >= 0.70.0",)
11
+
12
+
13
+ class CrewAIInstrumentor(BaseInstrumentor):
14
+
15
+ def instrumentation_dependencies(self) -> Collection[str]:
16
+ return _instruments
17
+
18
+ def _instrument(self, **kwargs):
19
+ tracer_provider = kwargs.get("tracer_provider")
20
+ tracer = get_tracer(__name__, __version__, tracer_provider)
21
+
22
+ wrap_function_wrapper("crewai.crew", "Crew.kickoff", wrap_kickoff(tracer))
23
+ wrap_function_wrapper("crewai.agent", "Agent.execute_task", wrap_agent_execute_task(tracer))
24
+ wrap_function_wrapper("crewai.task", "Task.execute_sync", wrap_task_execute(tracer))
25
+ wrap_function_wrapper("crewai.llm", "LLM.call", wrap_llm_call(tracer))
26
+
27
+ def _uninstrument(self, **kwargs):
28
+ pass
29
+
30
+
31
+ def with_tracer_wrapper(func):
32
+ """Helper for providing tracer for wrapper functions."""
33
+
34
+ def _with_tracer(tracer):
35
+ def wrapper(wrapped, instance, args, kwargs):
36
+ return func(tracer, wrapped, instance, args, kwargs)
37
+ return wrapper
38
+ return _with_tracer
39
+
40
+
41
+ @with_tracer_wrapper
42
+ def wrap_kickoff(tracer, wrapped, instance, args, kwargs):
43
+ with tracer.start_as_current_span(
44
+ "crewai.workflow",
45
+ kind=SpanKind.INTERNAL,
46
+ attributes={
47
+ }
48
+ ) as span:
49
+ try:
50
+ CrewAISpanAttributes(span=span, instance=instance)
51
+ result = wrapped(*args, **kwargs)
52
+ if result:
53
+ class_name = instance.__class__.__name__
54
+ span.set_attribute(f"crewai.{class_name.lower()}.result", str(result))
55
+ span.set_status(Status(StatusCode.OK))
56
+ if class_name == "Crew":
57
+ for attr in ["tasks_output", "token_usage", "usage_metrics"]:
58
+ if hasattr(result, attr):
59
+ span.set_attribute(f"crewai.crew.{attr}", str(getattr(result, attr)))
60
+ return result
61
+ except Exception as ex:
62
+ span.set_status(Status(StatusCode.ERROR, str(ex)))
63
+ raise
64
+
65
+
66
+ @with_tracer_wrapper
67
+ def wrap_agent_execute_task(tracer, wrapped, instance, args, kwargs):
68
+ agent_name = instance.role if hasattr(instance, "role") else "agent"
69
+ with tracer.start_as_current_span(
70
+ f"{agent_name}.agent",
71
+ kind=SpanKind.CLIENT,
72
+ attributes={
73
+ SpanAttributes.TRACELOOP_SPAN_KIND: TraceloopSpanKindValues.AGENT.value,
74
+ }
75
+ ) as span:
76
+ try:
77
+ CrewAISpanAttributes(span=span, instance=instance)
78
+ result = wrapped(*args, **kwargs)
79
+ set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, str(instance.llm.model))
80
+ span.set_status(Status(StatusCode.OK))
81
+ return result
82
+ except Exception as ex:
83
+ span.set_status(Status(StatusCode.ERROR, str(ex)))
84
+ raise
85
+
86
+
87
+ @with_tracer_wrapper
88
+ def wrap_task_execute(tracer, wrapped, instance, args, kwargs):
89
+ task_name = instance.description if hasattr(instance, "description") else "task"
90
+
91
+ with tracer.start_as_current_span(
92
+ f"{task_name}.task",
93
+ kind=SpanKind.CLIENT,
94
+ attributes={
95
+ SpanAttributes.TRACELOOP_SPAN_KIND: TraceloopSpanKindValues.TASK.value,
96
+ }
97
+ ) as span:
98
+ try:
99
+ CrewAISpanAttributes(span=span, instance=instance)
100
+ result = wrapped(*args, **kwargs)
101
+ set_span_attribute(span, SpanAttributes.TRACELOOP_ENTITY_OUTPUT, str(result))
102
+ span.set_status(Status(StatusCode.OK))
103
+ return result
104
+ except Exception as ex:
105
+ span.set_status(Status(StatusCode.ERROR, str(ex)))
106
+ raise
107
+
108
+
109
+ @with_tracer_wrapper
110
+ def wrap_llm_call(tracer, wrapped, instance, args, kwargs):
111
+ llm = instance.model if hasattr(instance, "model") else "llm"
112
+ with tracer.start_as_current_span(
113
+ f"{llm}.llm",
114
+ kind=SpanKind.CLIENT,
115
+ attributes={
116
+ }
117
+ ) as span:
118
+ try:
119
+ CrewAISpanAttributes(span=span, instance=instance)
120
+ result = wrapped(*args, **kwargs)
121
+ set_span_attribute(span, SpanAttributes.LLM_SYSTEM, "crewai")
122
+ set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, str(instance.model))
123
+ span.set_status(Status(StatusCode.OK))
124
+ return result
125
+ except Exception as ex:
126
+ span.set_status(Status(StatusCode.ERROR, str(ex)))
127
+ raise
@@ -0,0 +1,49 @@
1
+ [tool.coverage.run]
2
+ branch = true
3
+ source = ["opentelemetry/instrumentation/crewai"]
4
+
5
+ [tool.coverage.report]
6
+ exclude_lines = ['if TYPE_CHECKING:']
7
+ show_missing = true
8
+
9
+ [tool.poetry]
10
+ name = "opentelemetry-instrumentation-crewai"
11
+ version = "0.36.0"
12
+ description = "OpenTelemetry crewAI instrumentation"
13
+ authors = ["Gal Kleinman <gal@traceloop.com>", "Nir Gazit <nir@traceloop.com>"]
14
+ repository = "https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-crewai"
15
+ license = "Apache-2.0"
16
+ readme = "README.md"
17
+
18
+ [[tool.poetry.packages]]
19
+ include = "opentelemetry/instrumentation/crewai"
20
+
21
+ [tool.poetry.dependencies]
22
+ python = ">=3.10,<=3.13"
23
+ opentelemetry-api = "^1.28.0"
24
+ opentelemetry-instrumentation = ">=0.50b0"
25
+ opentelemetry-semantic-conventions = ">=0.50b0"
26
+ opentelemetry-semantic-conventions-ai = "0.4.2"
27
+
28
+ [tool.poetry.group.dev.dependencies]
29
+ autopep8 = "^2.2.0"
30
+ flake8 = "7.1.1"
31
+ pytest = "^8.2.2"
32
+ pytest-sugar = "1.0.0"
33
+
34
+ [tool.poetry.group.test.dependencies]
35
+ crewai = "^0.80.0"
36
+ pytest = "^8.2.2"
37
+ pytest-sugar = "1.0.0"
38
+ pytest-recording = "^0.13.1"
39
+ opentelemetry-sdk = "^1.27.0"
40
+
41
+ [build-system]
42
+ requires = ["poetry-core"]
43
+ build-backend = "poetry.core.masonry.api"
44
+
45
+ [tool.poetry.extras]
46
+ instruments = ["crewai"]
47
+
48
+ [tool.poetry.plugins."opentelemetry_instrumentor"]
49
+ crewai = "opentelemetry.instrumentation.crewai:CrewAIInstrumentor"