fableforge-agent-telemetry 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.
- fableforge_agent_telemetry-0.1.0/LICENSE +21 -0
- fableforge_agent_telemetry-0.1.0/PKG-INFO +21 -0
- fableforge_agent_telemetry-0.1.0/README.md +269 -0
- fableforge_agent_telemetry-0.1.0/pyproject.toml +37 -0
- fableforge_agent_telemetry-0.1.0/setup.cfg +4 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/__init__.py +7 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/cli.py +212 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/collector.py +412 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/dashboard.py +462 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/error_tracker.py +149 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/models.py +110 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/storage.py +501 -0
- fableforge_agent_telemetry-0.1.0/src/agent_telemetry/token_tracker.py +195 -0
- fableforge_agent_telemetry-0.1.0/src/fableforge_agent_telemetry.egg-info/PKG-INFO +21 -0
- fableforge_agent_telemetry-0.1.0/src/fableforge_agent_telemetry.egg-info/SOURCES.txt +18 -0
- fableforge_agent_telemetry-0.1.0/src/fableforge_agent_telemetry.egg-info/dependency_links.txt +1 -0
- fableforge_agent_telemetry-0.1.0/src/fableforge_agent_telemetry.egg-info/entry_points.txt +2 -0
- fableforge_agent_telemetry-0.1.0/src/fableforge_agent_telemetry.egg-info/requires.txt +15 -0
- fableforge_agent_telemetry-0.1.0/src/fableforge_agent_telemetry.egg-info/top_level.txt +1 -0
- fableforge_agent_telemetry-0.1.0/tests/test_token_tracker.py +157 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 FableForge Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fableforge-agent-telemetry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Datadog for AI agents — real-time observability, token tracking, cost estimation, error rates
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: fastapi>=0.110.0
|
|
8
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
9
|
+
Requires-Dist: clickhouse-driver>=0.2.6
|
|
10
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
11
|
+
Requires-Dist: pydantic>=2.5.0
|
|
12
|
+
Requires-Dist: plotly>=5.18.0
|
|
13
|
+
Requires-Dist: click>=8.1.0
|
|
14
|
+
Requires-Dist: tiktoken>=0.6.0
|
|
15
|
+
Requires-Dist: rich>=13.7.0
|
|
16
|
+
Requires-Dist: jinja2>=3.1.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
20
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# AgentTelemetry
|
|
2
|
+
|
|
3
|
+
[](LICENSE) [](https://www.python.org/downloads/) [](tests/)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
**Datadog for AI agents** — real-time observability, token tracking, cost estimation, and error rates for autonomous agent sessions.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Multi-format trace ingestion** — Parse traces from Glint-Research, armand0e, and v-Fable formats with auto-detection
|
|
11
|
+
- **Token tracking** — Count tokens with tiktoken, estimate costs with real per-model pricing
|
|
12
|
+
- **Cost estimation** — Detailed breakdowns for Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku, GPT-4, GPT-4o, GPT-4o-mini, Qwen3-Coder
|
|
13
|
+
- **Error tracking** — Automated error classification (bash, edit, timeout, rate limit, auth, context overflow, etc.) and recovery rate calculation
|
|
14
|
+
- **Interactive dashboard** — FastAPI + Plotly charts for session timelines, heatmaps, cost pies, and error breakdowns
|
|
15
|
+
- **Dual storage** — ClickHouse for production, SQLite for local/dev with automatic fallback
|
|
16
|
+
- **CLI** — Analyze traces, start dashboards, and generate reports from the command line
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e .
|
|
22
|
+
|
|
23
|
+
# With ClickHouse support (production):
|
|
24
|
+
pip install -e ".[clickhouse]"
|
|
25
|
+
|
|
26
|
+
# Development:
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### Analyze a Trace File
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Auto-detect format and analyze
|
|
36
|
+
agenttelemetry analyze trace.jsonl
|
|
37
|
+
|
|
38
|
+
# Specify format explicitly
|
|
39
|
+
agenttelemetry analyze trace.jsonl --format glint
|
|
40
|
+
|
|
41
|
+
# Analyze without storing
|
|
42
|
+
agenttelemetry analyze trace.jsonl --no-store
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Cost Report
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Detailed cost breakdown
|
|
49
|
+
agenttelemetry cost trace.jsonl
|
|
50
|
+
|
|
51
|
+
# Output:
|
|
52
|
+
Model: gpt-4
|
|
53
|
+
Input tokens: 1,234,567 ($0.037037)
|
|
54
|
+
Output tokens: 98,765 ($0.005926)
|
|
55
|
+
Cache read: 500,000 ($0.000000)
|
|
56
|
+
Cache creation: 0 ($0.000000)
|
|
57
|
+
─────────────────────────────────────
|
|
58
|
+
Total: $0.042963
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Error Report
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Show errors with classification and recovery rates
|
|
65
|
+
agenttelemetry errors trace.jsonl
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Token Counting
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Count tokens in a string
|
|
72
|
+
agenttelemetry tokens "Hello, world!" --model gpt-4
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Dashboard
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Start the interactive dashboard
|
|
79
|
+
agenttelemetry dashboard
|
|
80
|
+
|
|
81
|
+
# Custom host/port
|
|
82
|
+
agenttelemetry dashboard --host 0.0.0.0 --port 9000
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Open `http://127.0.0.1:8088/dashboard` to see session metrics, timelines, heatmaps, and cost reports.
|
|
86
|
+
|
|
87
|
+
## Analyzing Fable5 Traces
|
|
88
|
+
|
|
89
|
+
Fable5 (v-Fable) traces can be analyzed directly:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Ingest a Fable5 session trace
|
|
93
|
+
agenttelemetry analyze ~/.fable/sessions/2025-01-15-abc123.jsonl
|
|
94
|
+
|
|
95
|
+
# View cost breakdown
|
|
96
|
+
agenttelemetry cost ~/.fable/sessions/2025-01-15-abc123.jsonl
|
|
97
|
+
|
|
98
|
+
# Check errors and recovery
|
|
99
|
+
agenttelemetry errors ~/.fable/sessions/2025-01-15-abc123.jsonl
|
|
100
|
+
|
|
101
|
+
# Start dashboard with ingested data
|
|
102
|
+
agenttelemetry dashboard
|
|
103
|
+
# Then open http://127.0.0.1:8088/dashboard
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### v-Fable Trace Format
|
|
107
|
+
|
|
108
|
+
The v-Fable format uses these fields per JSONL line:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"kind": "tool_use",
|
|
113
|
+
"timestamp": "2025-01-15T10:30:00Z",
|
|
114
|
+
"session_id": "abc123",
|
|
115
|
+
"span_id": "span-001",
|
|
116
|
+
"parent_span_id": null,
|
|
117
|
+
"tool_name": "Bash",
|
|
118
|
+
"tokens": {"prompt": 1500, "completion": 800, "cache_read": 200, "cache_write": 50},
|
|
119
|
+
"duration_ms": 2345.6,
|
|
120
|
+
"cost_usd": 0.0234,
|
|
121
|
+
"model": "claude-3.5-sonnet",
|
|
122
|
+
"status": "success",
|
|
123
|
+
"error_message": null
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Glint-Research Format
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"type": "tool_call",
|
|
132
|
+
"timestamp": "2025-01-15T10:30:00Z",
|
|
133
|
+
"session_id": "glint-session-1",
|
|
134
|
+
"span_id": "span-001",
|
|
135
|
+
"tool": "Edit",
|
|
136
|
+
"usage": {
|
|
137
|
+
"input_tokens": 2000,
|
|
138
|
+
"output_tokens": 500,
|
|
139
|
+
"cache_read_input_tokens": 300,
|
|
140
|
+
"cache_creation_input_tokens": 100
|
|
141
|
+
},
|
|
142
|
+
"duration_ms": 1500.0,
|
|
143
|
+
"model": "gpt-4o",
|
|
144
|
+
"error": null
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### armand0e Format
|
|
149
|
+
|
|
150
|
+
Uses paired `invocation`/`response` events:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{"event": "invocation", "id": "span-001", "session": "arm-session", "tool": {"name": "Write", "input": {"path": "/tmp/file.py"}}, "model": "claude-3.5-sonnet"}
|
|
154
|
+
{"event": "response", "id": "span-001", "tokens": {"in": 1500, "out": 800, "cached": 200}, "latency_ms": 1800.0, "model": "claude-3.5-sonnet"}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Pricing Reference
|
|
158
|
+
|
|
159
|
+
| Model | Input ($/1M tok) | Output ($/1M tok) | Cache Read ($/1M) | Cache Write ($/1M) |
|
|
160
|
+
|---|---|---|---|---|
|
|
161
|
+
| Claude 3.5 Sonnet | $3.00 | $15.00 | $0.30 | $3.75 |
|
|
162
|
+
| Claude 3 Opus | $15.00 | $75.00 | $1.50 | $18.75 |
|
|
163
|
+
| Claude 3 Haiku | $0.25 | $1.25 | $0.03 | $0.30 |
|
|
164
|
+
| GPT-4 | $30.00 | $60.00 | — | — |
|
|
165
|
+
| GPT-4o | $2.50 | $10.00 | $1.25 | — |
|
|
166
|
+
| GPT-4o-mini | $0.15 | $0.60 | $0.075 | — |
|
|
167
|
+
| Qwen3-Coder | $0.50 | $2.00 | $0.10 | $0.50 |
|
|
168
|
+
|
|
169
|
+
## Architecture
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
agent_telemetry/
|
|
173
|
+
├── __init__.py # Package exports
|
|
174
|
+
├── models.py # Pydantic models (Span, SessionMetrics, ToolMetrics, CostReport)
|
|
175
|
+
├── collector.py # Trace ingestion (parse_glint_trace, parse_armand0e_trace, parse_vfable_trace)
|
|
176
|
+
├── token_tracker.py # Token counting + cost estimation with real pricing
|
|
177
|
+
├── error_tracker.py # Error detection, classification, recovery rate
|
|
178
|
+
├── storage.py # ClickHouse + SQLite dual storage
|
|
179
|
+
├── dashboard.py # FastAPI dashboard with Plotly charts
|
|
180
|
+
└── cli.py # Click CLI (analyze, dashboard, cost, errors, tokens)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## API Endpoints
|
|
184
|
+
|
|
185
|
+
| Endpoint | Description |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `GET /dashboard` | Main dashboard with session list |
|
|
188
|
+
| `GET /sessions/{id}` | Session detail with metrics table |
|
|
189
|
+
| `GET /sessions/{id}/timeline` | Tool call timeline (Plotly bar chart) |
|
|
190
|
+
| `GET /sessions/{id}/heatmap` | Tool usage heatmap (Plotly) |
|
|
191
|
+
| `GET /cost/report` | Cost breakdown across all sessions |
|
|
192
|
+
| `GET /cost/report?session_id=X` | Cost breakdown for a specific session |
|
|
193
|
+
| `GET /errors/report` | Error report across all sessions |
|
|
194
|
+
| `GET /errors/report?session_id=X` | Error report for a specific session |
|
|
195
|
+
|
|
196
|
+
## Python API
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from agent_telemetry.collector import ingest_trace, calculate_metrics
|
|
200
|
+
from agent_telemetry.token_tracker import estimate_cost, count_tokens
|
|
201
|
+
from agent_telemetry.error_tracker import generate_error_report
|
|
202
|
+
from agent_telemetry.storage import TelemetryStorage
|
|
203
|
+
|
|
204
|
+
# Analyze a trace
|
|
205
|
+
spans = ingest_trace("trace.jsonl", fmt="vfable")
|
|
206
|
+
metrics = calculate_metrics(spans)
|
|
207
|
+
|
|
208
|
+
# Estimate cost
|
|
209
|
+
cost = estimate_cost(10000, 5000, model="claude-3.5-sonnet", cache_read=3000)
|
|
210
|
+
print(f"Total: ${cost.total_cost:.6f}")
|
|
211
|
+
|
|
212
|
+
# Store in database
|
|
213
|
+
storage = TelemetryStorage()
|
|
214
|
+
storage.store_spans(spans)
|
|
215
|
+
storage.store_session_metrics(metrics["session"])
|
|
216
|
+
|
|
217
|
+
# Generate error report
|
|
218
|
+
report = generate_error_report("session-123", spans=spans)
|
|
219
|
+
print(f"Errors: {report.total_errors}, Recovery rate: {report.recovery_rate:.0%}")
|
|
220
|
+
|
|
221
|
+
# Query spans
|
|
222
|
+
session_spans = storage.query_spans(session_id="session-123")
|
|
223
|
+
tool_spans = storage.query_spans(tool_name="Bash")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Install dev dependencies
|
|
230
|
+
pip install -e ".[dev]"
|
|
231
|
+
|
|
232
|
+
# Run tests
|
|
233
|
+
pytest tests/
|
|
234
|
+
|
|
235
|
+
# Run specific test file
|
|
236
|
+
pytest tests/test_token_tracker.py -v
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
242
|
+
|
|
243
|
+
## Ecosystem
|
|
244
|
+
|
|
245
|
+
Part of the [FableForge](../) ecosystem — 21 open-source projects built from 210K real agent traces:
|
|
246
|
+
|
|
247
|
+
| Project | Description |
|
|
248
|
+
| --- | --- |
|
|
249
|
+
| **[Anvil](../anvil)** | Self-verified coding agent |
|
|
250
|
+
| **[VerifyLoop](../verifyloop)** | Plan→Execute→Verify→Recover framework |
|
|
251
|
+
| **[ErrorRecovery](../error-recovery)** | Self-healing middleware (3,725 error patterns) |
|
|
252
|
+
| **[FableForge-14B](../fableforge-14b)** | The fine-tuned 14B model (4-stage training) |
|
|
253
|
+
| **[ShellWhisperer](../shell-whisperer)** | 1.5B edge agent (phone/RPi, 50ms) |
|
|
254
|
+
| **[ReasonCritic](../reason-critic)** | Verification model (130 benchmark tasks) |
|
|
255
|
+
| **[TraceCompiler](../trace-compiler)** | Compile traces → LoRA skills |
|
|
256
|
+
| **[AgentRuntime](../agent-runtime)** | Persistent agent daemon (systemd for AI) |
|
|
257
|
+
| **[AgentSwarm](../agent-swarm)** | Multi-agent from real trace transitions |
|
|
258
|
+
| **[AgentTelemetry](../agent-telemetry)** | Datadog for agents (token tracking, costs) |
|
|
259
|
+
| **[BenchAgent](../bench-agent)** | HumanEval for tool-use (107 tasks) |
|
|
260
|
+
| **[AgentDev](../agent-dev)** | VSCode extension with verification |
|
|
261
|
+
| **[TraceViz](../trace-viz)** | Trace replay visualizer (Next.js) |
|
|
262
|
+
| **[AgentSkills](../agent-skills)** | npm for agent behaviors |
|
|
263
|
+
| **[AgentCurriculum](../agent-curriculum)** | 5-stage progressive training |
|
|
264
|
+
| **[AgentFuzzer](../agent-fuzzer)** | Adversarial testing for agents |
|
|
265
|
+
| **[AgentConstitution](../agent-constitution)** | Safety guardrails from traces |
|
|
266
|
+
| **[CostOptimizer](../cost-optimizer)** | Token cost reduction (50-80%) |
|
|
267
|
+
| **[AgentProfiler](../agent-profiler)** | Behavioral fingerprinting |
|
|
268
|
+
| **[TrajectoryDistiller](../trajectory-distiller)** | Trace→training data pipeline |
|
|
269
|
+
| **[Fable5-Dataset](../fable5-dataset)** | HuggingFace dataset release |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fableforge-agent-telemetry"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Datadog for AI agents — real-time observability, token tracking, cost estimation, error rates"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi>=0.110.0",
|
|
12
|
+
"uvicorn[standard]>=0.27.0",
|
|
13
|
+
"clickhouse-driver>=0.2.6",
|
|
14
|
+
"sqlalchemy>=2.0.0",
|
|
15
|
+
"pydantic>=2.5.0",
|
|
16
|
+
"plotly>=5.18.0",
|
|
17
|
+
"click>=8.1.0",
|
|
18
|
+
"tiktoken>=0.6.0",
|
|
19
|
+
"rich>=13.7.0",
|
|
20
|
+
"jinja2>=3.1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.0.0",
|
|
26
|
+
"pytest-asyncio>=0.23.0",
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
agenttelemetry = "agent_telemetry.cli:cli"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""CLI for AgentTelemetry — analyze traces, view costs, start dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from agent_telemetry.collector import (
|
|
14
|
+
auto_detect_format,
|
|
15
|
+
calculate_metrics,
|
|
16
|
+
ingest_trace,
|
|
17
|
+
)
|
|
18
|
+
from agent_telemetry.error_tracker import classify_error, generate_error_report
|
|
19
|
+
from agent_telemetry.models import Span
|
|
20
|
+
from agent_telemetry.storage import TelemetryStorage
|
|
21
|
+
from agent_telemetry.token_tracker import estimate_cost, format_cost_table
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
@click.version_option(version="0.1.0")
|
|
28
|
+
def cli() -> None:
|
|
29
|
+
"""AgentTelemetry — Datadog for AI agents."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cli.command()
|
|
33
|
+
@click.argument("trace_file", type=click.Path(exists=True))
|
|
34
|
+
@click.option("--format", "fmt", type=click.Choice(["glint", "armand0e", "vfable", "auto"]), default="auto", help="Trace format")
|
|
35
|
+
@click.option("--store/--no-store", default=True, help="Store results in database")
|
|
36
|
+
def analyze(trace_file: str, fmt: str, store: bool) -> None:
|
|
37
|
+
"""Analyze a trace file and display metrics."""
|
|
38
|
+
if fmt == "auto":
|
|
39
|
+
fmt = auto_detect_format(trace_file)
|
|
40
|
+
console.print(f"[dim]Detected format: {fmt}[/dim]")
|
|
41
|
+
|
|
42
|
+
spans = ingest_trace(trace_file, fmt=fmt)
|
|
43
|
+
if not spans:
|
|
44
|
+
console.print("[red]No spans found in trace file.[/red]")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
console.print(f"[green]Loaded {len(spans)} spans[/green]")
|
|
48
|
+
|
|
49
|
+
metrics_result = calculate_metrics(spans)
|
|
50
|
+
session = metrics_result["session"]
|
|
51
|
+
tools = metrics_result["tools"]
|
|
52
|
+
|
|
53
|
+
console.print(f"\n[bold]Session:[/bold] {session.session_id}")
|
|
54
|
+
console.print(f"[bold]Model:[/bold] {session.model}")
|
|
55
|
+
console.print(f"[bold]Duration:[/bold] {session.duration_seconds:.1f}s")
|
|
56
|
+
|
|
57
|
+
metrics_table = Table(title="Session Metrics", show_header=True)
|
|
58
|
+
metrics_table.add_column("Metric", style="cyan")
|
|
59
|
+
metrics_table.add_column("Value", justify="right")
|
|
60
|
+
|
|
61
|
+
metrics_table.add_row("Total Tokens", f"{session.total_tokens:,}")
|
|
62
|
+
metrics_table.add_row("Total Cost", f"${session.total_cost:.6f}")
|
|
63
|
+
metrics_table.add_row("Tool Calls", str(session.tool_calls))
|
|
64
|
+
metrics_table.add_row("Errors", str(session.error_count))
|
|
65
|
+
metrics_table.add_row("Avg Duration", f"{session.avg_tool_duration_ms:.0f}ms")
|
|
66
|
+
metrics_table.add_row("P50 Duration", f"{session.p50_duration_ms:.0f}ms")
|
|
67
|
+
metrics_table.add_row("P95 Duration", f"{session.p95_duration_ms:.0f}ms")
|
|
68
|
+
metrics_table.add_row("P99 Duration", f"{session.p99_duration_ms:.0f}ms")
|
|
69
|
+
metrics_table.add_row("Cache Hit Rate", f"{session.cache_hit_rate:.1%}")
|
|
70
|
+
console.print(metrics_table)
|
|
71
|
+
|
|
72
|
+
tool_table = Table(title="Tool Metrics", show_header=True)
|
|
73
|
+
tool_table.add_column("Tool", style="cyan")
|
|
74
|
+
tool_table.add_column("Calls", justify="right")
|
|
75
|
+
tool_table.add_column("Avg ms", justify="right")
|
|
76
|
+
tool_table.add_column("P95 ms", justify="right")
|
|
77
|
+
tool_table.add_column("Error Rate", justify="right")
|
|
78
|
+
tool_table.add_column("Cost", justify="right", style="green")
|
|
79
|
+
|
|
80
|
+
for name, tm in sorted(tools.items()):
|
|
81
|
+
tool_table.add_row(
|
|
82
|
+
name,
|
|
83
|
+
str(tm.call_count),
|
|
84
|
+
f"{tm.avg_duration_ms:.0f}",
|
|
85
|
+
f"{tm.p95_duration_ms:.0f}",
|
|
86
|
+
f"{tm.error_rate:.1%}",
|
|
87
|
+
f"${tm.total_cost_usd:.6f}",
|
|
88
|
+
)
|
|
89
|
+
console.print(tool_table)
|
|
90
|
+
|
|
91
|
+
if store:
|
|
92
|
+
storage = TelemetryStorage()
|
|
93
|
+
storage.store_spans(spans)
|
|
94
|
+
storage.store_session_metrics(session)
|
|
95
|
+
console.print(f"\n[dim]Stored {len(spans)} spans in database[/dim]")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command()
|
|
99
|
+
@click.option("--host", default="127.0.0.1", help="Host to bind to")
|
|
100
|
+
@click.option("--port", default=8088, type=int, help="Port to bind to")
|
|
101
|
+
def dashboard(host: str, port: int) -> None:
|
|
102
|
+
"""Start the interactive dashboard server."""
|
|
103
|
+
import uvicorn
|
|
104
|
+
from agent_telemetry.dashboard import app
|
|
105
|
+
|
|
106
|
+
console.print(f"[green]Starting AgentTelemetry dashboard on http://{host}:{port}[/green]")
|
|
107
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
108
|
+
uvicorn.run(app, host=host, port=port)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@cli.command()
|
|
112
|
+
@click.argument("trace_file", type=click.Path(exists=True))
|
|
113
|
+
@click.option("--format", "fmt", type=click.Choice(["glint", "armand0e", "vfable", "auto"]), default="auto", help="Trace format")
|
|
114
|
+
def cost(trace_file: str, fmt: str) -> None:
|
|
115
|
+
"""Show cost breakdown for a trace file."""
|
|
116
|
+
if fmt == "auto":
|
|
117
|
+
fmt = auto_detect_format(trace_file)
|
|
118
|
+
console.print(f"[dim]Detected format: {fmt}[/dim]")
|
|
119
|
+
|
|
120
|
+
spans = ingest_trace(trace_file, fmt=fmt)
|
|
121
|
+
if not spans:
|
|
122
|
+
console.print("[red]No spans found in trace file.[/red]")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
models: dict[str, list[Span]] = {}
|
|
126
|
+
for s in spans:
|
|
127
|
+
models.setdefault(s.model, []).append(s)
|
|
128
|
+
|
|
129
|
+
breakdowns = []
|
|
130
|
+
for model, model_spans in sorted(models.items()):
|
|
131
|
+
bd = estimate_cost(
|
|
132
|
+
sum(s.input_tokens for s in model_spans),
|
|
133
|
+
sum(s.output_tokens for s in model_spans),
|
|
134
|
+
model,
|
|
135
|
+
sum(s.cache_read for s in model_spans),
|
|
136
|
+
sum(s.cache_creation for s in model_spans),
|
|
137
|
+
)
|
|
138
|
+
breakdowns.append(bd)
|
|
139
|
+
|
|
140
|
+
console.print(format_cost_table(breakdowns))
|
|
141
|
+
|
|
142
|
+
total = sum(b.total_cost for b in breakdowns)
|
|
143
|
+
console.print(f"\n[bold green]Grand Total: ${total:.6f}[/bold green]")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@cli.command()
|
|
147
|
+
@click.argument("trace_file", type=click.Path(exists=True))
|
|
148
|
+
@click.option("--format", "fmt", type=click.Choice(["glint", "armand0e", "vfable", "auto"]), default="auto", help="Trace format")
|
|
149
|
+
def errors(trace_file: str, fmt: str) -> None:
|
|
150
|
+
"""Show error report for a trace file."""
|
|
151
|
+
if fmt == "auto":
|
|
152
|
+
fmt = auto_detect_format(trace_file)
|
|
153
|
+
console.print(f"[dim]Detected format: {fmt}[/dim]")
|
|
154
|
+
|
|
155
|
+
spans = ingest_trace(trace_file, fmt=fmt)
|
|
156
|
+
if not spans:
|
|
157
|
+
console.print("[red]No spans found in trace file.[/red]")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
session_id = spans[0].session_id
|
|
161
|
+
report = generate_error_report(session_id, spans=spans)
|
|
162
|
+
|
|
163
|
+
console.print(f"\n[bold]Error Report: {session_id}[/bold]")
|
|
164
|
+
console.print(f"Total Errors: {report.total_errors}")
|
|
165
|
+
console.print(f"Recovered: {report.recovered_errors}")
|
|
166
|
+
console.print(f"Recovery Rate: {report.recovery_rate:.0%}")
|
|
167
|
+
|
|
168
|
+
if report.errors_by_type:
|
|
169
|
+
type_table = Table(title="Errors by Type", show_header=True)
|
|
170
|
+
type_table.add_column("Error Type", style="red")
|
|
171
|
+
type_table.add_column("Count", justify="right")
|
|
172
|
+
|
|
173
|
+
for etype, count in sorted(report.errors_by_type.items(), key=lambda x: -x[1]):
|
|
174
|
+
type_table.add_row(etype, str(count))
|
|
175
|
+
console.print(type_table)
|
|
176
|
+
|
|
177
|
+
if report.errors:
|
|
178
|
+
error_table = Table(title="Error Details", show_header=True)
|
|
179
|
+
error_table.add_column("Span ID", style="dim")
|
|
180
|
+
error_table.add_column("Type", style="red")
|
|
181
|
+
error_table.add_column("Tool", style="cyan")
|
|
182
|
+
error_table.add_column("Message", max_width=60)
|
|
183
|
+
error_table.add_column("Recovered")
|
|
184
|
+
|
|
185
|
+
for e in report.errors[:50]:
|
|
186
|
+
recovered = "[green]✓[/green]" if e.recovered else "[red]✗[/red]"
|
|
187
|
+
error_table.add_row(
|
|
188
|
+
e.span_id[:12] + "...",
|
|
189
|
+
e.error_type,
|
|
190
|
+
e.tool_name,
|
|
191
|
+
e.error_message[:60],
|
|
192
|
+
recovered,
|
|
193
|
+
)
|
|
194
|
+
console.print(error_table)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@cli.command()
|
|
198
|
+
@click.argument("text")
|
|
199
|
+
@click.option("--model", default="gpt-4", help="Model name for token counting")
|
|
200
|
+
def tokens(text: str, model: str) -> None:
|
|
201
|
+
"""Count tokens in a text string."""
|
|
202
|
+
from agent_telemetry.token_tracker import count_tokens
|
|
203
|
+
|
|
204
|
+
n = count_tokens(text, model)
|
|
205
|
+
console.print(f"[bold]{n:,}[/bold] tokens ({model})")
|
|
206
|
+
|
|
207
|
+
bd = estimate_cost(n, 0, model)
|
|
208
|
+
console.print(f"Input cost (no output): ${bd.input_cost:.6f}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
cli()
|