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.
- agentmetrics_crewai-0.1.0/.gitignore +59 -0
- agentmetrics_crewai-0.1.0/PKG-INFO +86 -0
- agentmetrics_crewai-0.1.0/README.md +73 -0
- agentmetrics_crewai-0.1.0/agentmetrics_crewai/__init__.py +4 -0
- agentmetrics_crewai-0.1.0/agentmetrics_crewai/listener.py +209 -0
- agentmetrics_crewai-0.1.0/pyproject.toml +26 -0
|
@@ -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
|
+
[](https://pypi.org/project/agentmetrics-crewai)
|
|
17
|
+
[](../../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
|
+
[](https://pypi.org/project/agentmetrics-crewai)
|
|
4
|
+
[](../../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,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 }
|