agentmetrics-crewai 0.1.0__tar.gz

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.
@@ -0,0 +1,59 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ .venv/
6
+ .env
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .pytest_cache/
13
+ htmlcov/
14
+ .coverage
15
+ coverage.xml
16
+
17
+ # Node / JS
18
+ node_modules/
19
+ .next/
20
+ .turbo/
21
+ dist/
22
+ build/
23
+ *.tsbuildinfo
24
+ .pnpm-store/
25
+
26
+ # Env files
27
+ .env
28
+ .env.local
29
+ .env.production
30
+ .env.*.local
31
+ api/.env.local
32
+ dashboard/.env.local
33
+
34
+ # Build artifacts inside packages
35
+ packages/python/dist/
36
+ packages/python/*.egg-info/
37
+ packages/js/dist/
38
+ packages/js/node_modules/
39
+
40
+ # Internal docs — never public
41
+ .internal/
42
+ PLAN.md
43
+ CODE.md
44
+
45
+ # OS
46
+ .DS_Store
47
+ Thumbs.db
48
+
49
+ # IDE
50
+ .vscode/
51
+ .idea/
52
+ *.swp
53
+
54
+ # Docker
55
+ *.log
56
+ .internal
57
+
58
+ # Local data (SQLite DB when running without Docker)
59
+ data/
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentmetrics-crewai
3
+ Version: 0.1.0
4
+ Summary: AgentMetrics observability integration for CrewAI agents
5
+ Project-URL: Homepage, https://github.com/andalabx/agentmetrics
6
+ Project-URL: Repository, https://github.com/andalabx/agentmetrics
7
+ License: MIT
8
+ Keywords: agentmetrics,ai-agents,crewai,monitoring,observability
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: agentmetrics>=0.1.3
11
+ Requires-Dist: crewai>=0.80.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # agentmetrics-crewai
15
+
16
+ [![PyPI](https://img.shields.io/pypi/v/agentmetrics-crewai?color=6366f1&label=pypi&logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-crewai)
17
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../../LICENSE)
18
+
19
+ AgentMetrics integration for [CrewAI](https://docs.crewai.com). Instantiate the listener once at startup and every `crew.kickoff()` reports back to your dashboard automatically showing latency, cost, token counts, tool calls, and errors, with zero changes to your crew code.
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install agentmetrics-crewai
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Quickstart
32
+
33
+ ```python
34
+ from agentmetrics_crewai import AgentMetricsListener
35
+
36
+ # register once at startup, covers all crews in the process
37
+ AgentMetricsListener(
38
+ agent_id="my-crew",
39
+ base_url="http://localhost:8099",
40
+ )
41
+
42
+ # Run your crew as normal
43
+ result = MyCrew().kickoff()
44
+ ```
45
+
46
+ ---
47
+
48
+ ## API
49
+
50
+ ### `AgentMetricsListener(agent_id, base_url)`
51
+
52
+ | Parameter | Default | Description |
53
+ |---|---|---|
54
+ | `agent_id` | `"crewai-agent"` | Fallback label if the crew has no `crew_name` |
55
+ | `base_url` | `"http://localhost:8099"` | AgentMetrics server address |
56
+
57
+ Instantiating the class auto-registers event handlers on the global `crewai_event_bus` with no further setup needed, and the listener handles concurrent kickoffs correctly via `source_fingerprint` tracking.
58
+
59
+ ### `.flush(timeout=10.0)`
60
+
61
+ Blocks until all in-flight HTTP requests complete. Call before process exit in scripts.
62
+
63
+ ---
64
+
65
+ ## What gets tracked
66
+
67
+ Each `kickoff()` call emits one event to `/v1/events` on completion or failure:
68
+
69
+ | Field | Description |
70
+ |---|---|
71
+ | `status` | `success` or `failed` |
72
+ | `duration_ms` | Wall-clock kickoff duration |
73
+ | `input_tokens` / `output_tokens` | Aggregated across all LLM calls |
74
+ | `cache_read_tokens` / `cache_write_tokens` | Cache token counts |
75
+ | `llm_calls` | Number of LLM requests in the kickoff |
76
+ | `tool_calls` / `tool_errors` | Tool usage counts |
77
+ | `tool_names` | Set of tools invoked |
78
+ | `model` | Model name from the first LLM call |
79
+ | `estimated_cost_usd` | Computed from token counts and model pricing |
80
+ | `error` | First 500 chars of the error message on failure |
81
+
82
+ ---
83
+
84
+ ## License
85
+
86
+ [MIT](../../LICENSE)
@@ -0,0 +1,73 @@
1
+ # agentmetrics-crewai
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/agentmetrics-crewai?color=6366f1&label=pypi&logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-crewai)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../../LICENSE)
5
+
6
+ AgentMetrics integration for [CrewAI](https://docs.crewai.com). Instantiate the listener once at startup and every `crew.kickoff()` reports back to your dashboard automatically showing latency, cost, token counts, tool calls, and errors, with zero changes to your crew code.
7
+
8
+ ---
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install agentmetrics-crewai
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from agentmetrics_crewai import AgentMetricsListener
22
+
23
+ # register once at startup, covers all crews in the process
24
+ AgentMetricsListener(
25
+ agent_id="my-crew",
26
+ base_url="http://localhost:8099",
27
+ )
28
+
29
+ # Run your crew as normal
30
+ result = MyCrew().kickoff()
31
+ ```
32
+
33
+ ---
34
+
35
+ ## API
36
+
37
+ ### `AgentMetricsListener(agent_id, base_url)`
38
+
39
+ | Parameter | Default | Description |
40
+ |---|---|---|
41
+ | `agent_id` | `"crewai-agent"` | Fallback label if the crew has no `crew_name` |
42
+ | `base_url` | `"http://localhost:8099"` | AgentMetrics server address |
43
+
44
+ Instantiating the class auto-registers event handlers on the global `crewai_event_bus` with no further setup needed, and the listener handles concurrent kickoffs correctly via `source_fingerprint` tracking.
45
+
46
+ ### `.flush(timeout=10.0)`
47
+
48
+ Blocks until all in-flight HTTP requests complete. Call before process exit in scripts.
49
+
50
+ ---
51
+
52
+ ## What gets tracked
53
+
54
+ Each `kickoff()` call emits one event to `/v1/events` on completion or failure:
55
+
56
+ | Field | Description |
57
+ |---|---|
58
+ | `status` | `success` or `failed` |
59
+ | `duration_ms` | Wall-clock kickoff duration |
60
+ | `input_tokens` / `output_tokens` | Aggregated across all LLM calls |
61
+ | `cache_read_tokens` / `cache_write_tokens` | Cache token counts |
62
+ | `llm_calls` | Number of LLM requests in the kickoff |
63
+ | `tool_calls` / `tool_errors` | Tool usage counts |
64
+ | `tool_names` | Set of tools invoked |
65
+ | `model` | Model name from the first LLM call |
66
+ | `estimated_cost_usd` | Computed from token counts and model pricing |
67
+ | `error` | First 500 chars of the error message on failure |
68
+
69
+ ---
70
+
71
+ ## License
72
+
73
+ [MIT](../../LICENSE)
@@ -0,0 +1,4 @@
1
+ from agentmetrics_crewai.listener import AgentMetricsListener
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["AgentMetricsListener"]
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import time
5
+ import uuid
6
+ from typing import Any
7
+
8
+ from agentmetrics.http_client import HttpClient
9
+ from agentmetrics.tracker import _estimate_cost
10
+ from crewai.utilities.events import crewai_event_bus
11
+ from crewai.utilities.events.base_event_listener import BaseEventListener
12
+ from crewai.utilities.events.types.crew_events import (
13
+ CrewKickoffCompletedEvent,
14
+ CrewKickoffFailedEvent,
15
+ CrewKickoffStartedEvent,
16
+ )
17
+ from crewai.utilities.events.types.llm_events import (
18
+ LLMCallCompletedEvent,
19
+ LLMCallFailedEvent,
20
+ )
21
+ from crewai.utilities.events.types.tool_usage_events import (
22
+ ToolUsageErrorEvent,
23
+ ToolUsageFinishedEvent,
24
+ )
25
+
26
+
27
+ class _KickoffState:
28
+ __slots__ = (
29
+ "agent_id",
30
+ "cache_read_tokens",
31
+ "cache_write_tokens",
32
+ "error",
33
+ "input_tokens",
34
+ "llm_calls",
35
+ "model",
36
+ "output_tokens",
37
+ "start_ms",
38
+ "status",
39
+ "tool_calls",
40
+ "tool_errors",
41
+ "tool_names",
42
+ "trace_id",
43
+ )
44
+
45
+ def __init__(self, agent_id: str) -> None:
46
+ self.trace_id = str(uuid.uuid4())
47
+ self.agent_id = agent_id
48
+ self.start_ms = time.monotonic()
49
+ self.input_tokens = 0
50
+ self.output_tokens = 0
51
+ self.cache_read_tokens: int | None = None # CrewAI does not expose cache token breakdown
52
+ self.cache_write_tokens: int | None = None
53
+ self.llm_calls = 0
54
+ self.tool_calls = 0
55
+ self.tool_errors = 0
56
+ self.tool_names: set[str] = set()
57
+ self.model: str | None = None
58
+ self.status = "success"
59
+ self.error: str | None = None
60
+
61
+
62
+ class AgentMetricsListener(BaseEventListener):
63
+ """
64
+ CrewAI event listener that sends a run summary to AgentMetrics
65
+ after each crew kickoff completes or fails.
66
+
67
+ Instantiating this class is enough - it auto-registers globally.
68
+
69
+ Usage::
70
+
71
+ from agentmetrics_crewai import AgentMetricsListener
72
+
73
+ AgentMetricsListener(api_key="am_...")
74
+ result = MyCrew().kickoff()
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ api_key: str,
80
+ agent_id: str = "crewai-agent",
81
+ base_url: str = "http://localhost:8099",
82
+ ) -> None:
83
+ self._client = HttpClient(api_key=api_key, base_url=base_url)
84
+ self._agent_id = agent_id
85
+ self._lock = threading.Lock()
86
+ # run_key (UUID str) → KickoffState (tracks concurrent kickoffs)
87
+ self._active: dict[str, _KickoffState] = {}
88
+ # source_fingerprint → run_key; allows other events to find their state
89
+ self._fingerprint_map: dict[str, str] = {}
90
+ super().__init__() # calls setup_listeners
91
+
92
+ def setup_listeners(self, bus: Any) -> None:
93
+
94
+ @crewai_event_bus.on(CrewKickoffStartedEvent)
95
+ def on_kickoff_started(source: Any, event: CrewKickoffStartedEvent) -> None:
96
+ # INT-02: use a UUID per kickoff as the run key to avoid fingerprint collisions
97
+ run_key = str(uuid.uuid4())
98
+ fp = event.source_fingerprint or run_key
99
+ name = event.crew_name or self._agent_id
100
+ with self._lock:
101
+ self._active[run_key] = _KickoffState(name)
102
+ self._fingerprint_map[fp] = run_key
103
+
104
+ def _state_for_event(event: Any) -> _KickoffState | None:
105
+ fp = event.source_fingerprint or ""
106
+ with self._lock:
107
+ run_key = self._fingerprint_map.get(fp)
108
+ if run_key is None:
109
+ return None
110
+ return self._active.get(run_key)
111
+
112
+ def _pop_state_for_event(event: Any) -> _KickoffState | None:
113
+ fp = event.source_fingerprint or ""
114
+ with self._lock:
115
+ run_key = self._fingerprint_map.pop(fp, None)
116
+ if run_key is None:
117
+ return None
118
+ return self._active.pop(run_key, None)
119
+
120
+ @crewai_event_bus.on(LLMCallCompletedEvent)
121
+ def on_llm_completed(source: Any, event: LLMCallCompletedEvent) -> None:
122
+ state = _state_for_event(event)
123
+ if state is None:
124
+ return
125
+ state.llm_calls += 1
126
+ usage = event.usage or {}
127
+ state.input_tokens += usage.get("prompt_tokens", 0) or usage.get("input_tokens", 0) or 0
128
+ state.output_tokens += usage.get("completion_tokens", 0) or usage.get("output_tokens", 0) or 0
129
+ # cache_read_tokens / cache_write_tokens remain None — CrewAI does not expose cache breakdown
130
+ if not state.model and event.model:
131
+ state.model = event.model
132
+
133
+ @crewai_event_bus.on(LLMCallFailedEvent)
134
+ def on_llm_failed(source: Any, event: LLMCallFailedEvent) -> None:
135
+ state = _state_for_event(event)
136
+ if state:
137
+ state.status = "failed"
138
+
139
+ @crewai_event_bus.on(ToolUsageFinishedEvent)
140
+ def on_tool_finished(source: Any, event: ToolUsageFinishedEvent) -> None:
141
+ state = _state_for_event(event)
142
+ if state is None:
143
+ return
144
+ state.tool_calls += 1
145
+ if event.tool_name:
146
+ state.tool_names.add(event.tool_name)
147
+
148
+ @crewai_event_bus.on(ToolUsageErrorEvent)
149
+ def on_tool_error(source: Any, event: ToolUsageErrorEvent) -> None:
150
+ state = _state_for_event(event)
151
+ if state is None:
152
+ return
153
+ state.tool_calls += 1
154
+ # INT-03: use getattr with None check instead of hasattr
155
+ if getattr(event, "error", None) is not None:
156
+ state.tool_errors += 1
157
+ if event.tool_name:
158
+ state.tool_names.add(event.tool_name)
159
+
160
+ @crewai_event_bus.on(CrewKickoffCompletedEvent)
161
+ def on_kickoff_completed(source: Any, event: CrewKickoffCompletedEvent) -> None:
162
+ state = _pop_state_for_event(event)
163
+ if state:
164
+ self._emit(state)
165
+
166
+ @crewai_event_bus.on(CrewKickoffFailedEvent)
167
+ def on_kickoff_failed(source: Any, event: CrewKickoffFailedEvent) -> None:
168
+ state = _pop_state_for_event(event)
169
+ if state:
170
+ state.status = "failed"
171
+ state.error = str(event.error)[:500] if event.error else None
172
+ self._emit(state)
173
+
174
+ def _emit(self, state: _KickoffState) -> None:
175
+ duration_ms = (time.monotonic() - state.start_ms) * 1000
176
+ est = _estimate_cost(
177
+ state.model,
178
+ state.input_tokens, state.output_tokens,
179
+ state.cache_read_tokens, state.cache_write_tokens,
180
+ )
181
+ payload: dict[str, Any] = {
182
+ "event_id": str(uuid.uuid4()),
183
+ "trace_id": state.trace_id,
184
+ "agent_id": state.agent_id,
185
+ "platform": "crewai",
186
+ "event_name": "agent_end",
187
+ "ts": int(time.time() * 1000),
188
+ "redaction_policy_version": "v1-strict",
189
+ "status": state.status,
190
+ "duration_ms": round(duration_ms, 2),
191
+ "tool_calls": state.tool_calls,
192
+ "tool_errors": state.tool_errors,
193
+ "tool_names": list(state.tool_names),
194
+ "llm_calls": state.llm_calls,
195
+ "input_tokens": state.input_tokens,
196
+ "output_tokens": state.output_tokens,
197
+ "cache_read_tokens": None, # CrewAI does not expose cache token breakdown
198
+ "cache_write_tokens": None,
199
+ }
200
+ if state.model:
201
+ payload["model"] = state.model
202
+ if state.error:
203
+ payload["error"] = state.error
204
+ if est is not None:
205
+ payload["estimated_cost_usd"] = est
206
+ self._client.fire_and_forget(payload)
207
+
208
+ def flush(self, timeout: float = 10.0) -> None:
209
+ self._client.flush(timeout=timeout)
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentmetrics-crewai"
7
+ version = "0.1.0"
8
+ description = "AgentMetrics observability integration for CrewAI agents"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["crewai", "agentmetrics", "observability", "ai-agents", "monitoring"]
13
+ dependencies = [
14
+ "agentmetrics>=0.1.3",
15
+ "crewai>=0.80.0",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/andalabx/agentmetrics"
20
+ Repository = "https://github.com/andalabx/agentmetrics"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["agentmetrics_crewai"]
24
+
25
+ [tool.uv.sources]
26
+ agentmetrics = { workspace = true }