htmlgraph 0.26.12__py3-none-any.whl → 0.26.14__py3-none-any.whl
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.
- htmlgraph/__init__.py +1 -1
- htmlgraph/analytics/__init__.py +6 -1
- htmlgraph/analytics/cost_analyzer.py +387 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/analytics.py +19 -0
- htmlgraph/hooks/posttooluse.py +42 -0
- {htmlgraph-0.26.12.dist-info → htmlgraph-0.26.14.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.12.dist-info → htmlgraph-0.26.14.dist-info}/RECORD +17 -15
- {htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.12.dist-info → htmlgraph-0.26.14.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.12.dist-info → htmlgraph-0.26.14.dist-info}/entry_points.txt +0 -0
htmlgraph/__init__.py
CHANGED
htmlgraph/analytics/__init__.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Analytics modules for HtmlGraph.
|
|
3
3
|
|
|
4
|
-
Provides work type analysis, dependency analytics, cross-session analytics,
|
|
4
|
+
Provides work type analysis, dependency analytics, cross-session analytics, CLI analytics,
|
|
5
|
+
and cost attribution analysis for OTEL ROI.
|
|
5
6
|
"""
|
|
6
7
|
|
|
8
|
+
from htmlgraph.analytics.cost_analyzer import CostAnalyzer
|
|
9
|
+
from htmlgraph.analytics.cost_reporter import CostReporter
|
|
7
10
|
from htmlgraph.analytics.cross_session import CrossSessionAnalytics
|
|
8
11
|
from htmlgraph.analytics.dependency import DependencyAnalytics
|
|
9
12
|
from htmlgraph.analytics.work_type import Analytics
|
|
@@ -12,4 +15,6 @@ __all__ = [
|
|
|
12
15
|
"Analytics",
|
|
13
16
|
"DependencyAnalytics",
|
|
14
17
|
"CrossSessionAnalytics",
|
|
18
|
+
"CostAnalyzer",
|
|
19
|
+
"CostReporter",
|
|
15
20
|
]
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CostAnalyzer for OTEL ROI Analysis - Phase 1.
|
|
3
|
+
|
|
4
|
+
Analyzes cost attribution of Task() delegations vs direct tool execution.
|
|
5
|
+
Provides insights into which delegations are most expensive and their ROI.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
1. get_task_delegations() - Query all task_delegation events with hierarchy
|
|
9
|
+
2. calculate_task_cost(event_id) - Sum token costs of all child tool calls
|
|
10
|
+
3. get_cost_by_subagent_type() - Group costs by subagent type
|
|
11
|
+
4. get_cost_by_tool_type() - Show which tools cost most
|
|
12
|
+
5. get_roi_stats() - Calculate parallelization savings and benefits
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from htmlgraph.analytics.cost_analyzer import CostAnalyzer
|
|
16
|
+
analyzer = CostAnalyzer()
|
|
17
|
+
delegations = analyzer.get_task_delegations_with_costs()
|
|
18
|
+
print(f"Total delegation cost: ${delegations['total_cost_usd']:.2f}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import sqlite3
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from htmlgraph.cigs.cost import CostCalculator
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TaskDelegation:
|
|
32
|
+
"""Represents a single Task delegation with cost analysis."""
|
|
33
|
+
|
|
34
|
+
event_id: str
|
|
35
|
+
session_id: str
|
|
36
|
+
timestamp: datetime
|
|
37
|
+
subagent_type: str
|
|
38
|
+
parent_event_id: str | None
|
|
39
|
+
tool_count: int = 0
|
|
40
|
+
total_cost_tokens: int = 0
|
|
41
|
+
child_events: list[dict[str, Any]] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
"""Convert to dictionary."""
|
|
45
|
+
return {
|
|
46
|
+
"event_id": self.event_id,
|
|
47
|
+
"session_id": self.session_id,
|
|
48
|
+
"timestamp": self.timestamp.isoformat(),
|
|
49
|
+
"subagent_type": self.subagent_type,
|
|
50
|
+
"parent_event_id": self.parent_event_id,
|
|
51
|
+
"tool_count": self.tool_count,
|
|
52
|
+
"total_cost_tokens": self.total_cost_tokens,
|
|
53
|
+
"child_events": self.child_events,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class CostBreakdown:
|
|
59
|
+
"""Cost breakdown analysis."""
|
|
60
|
+
|
|
61
|
+
by_subagent: dict[str, int] = field(default_factory=dict)
|
|
62
|
+
by_tool: dict[str, int] = field(default_factory=dict)
|
|
63
|
+
total_cost_tokens: int = 0
|
|
64
|
+
total_delegations: int = 0
|
|
65
|
+
avg_cost_per_delegation: float = 0.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ROIStats:
|
|
70
|
+
"""Return-on-Investment statistics."""
|
|
71
|
+
|
|
72
|
+
total_delegation_cost: int = 0
|
|
73
|
+
estimated_direct_cost: int = 0
|
|
74
|
+
estimated_savings: int = 0
|
|
75
|
+
savings_percentage: float = 0.0
|
|
76
|
+
avg_parallelization_factor: float = 1.0
|
|
77
|
+
context_preservation_benefit: float = 0.0
|
|
78
|
+
total_delegations: int = 0
|
|
79
|
+
avg_cost_per_delegation: float = 0.0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CostAnalyzer:
|
|
83
|
+
"""
|
|
84
|
+
Analyze cost attribution of Task delegations.
|
|
85
|
+
|
|
86
|
+
Queries the agent_events database to calculate:
|
|
87
|
+
- Total cost of each Task delegation (sum of child tool calls)
|
|
88
|
+
- Cost breakdown by subagent type and tool type
|
|
89
|
+
- ROI statistics comparing direct vs delegated execution
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, graph_dir: Path | None = None):
|
|
93
|
+
"""
|
|
94
|
+
Initialize CostAnalyzer.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
graph_dir: Root directory for HtmlGraph (defaults to .htmlgraph)
|
|
98
|
+
"""
|
|
99
|
+
if graph_dir is None:
|
|
100
|
+
graph_dir = Path.cwd() / ".htmlgraph"
|
|
101
|
+
|
|
102
|
+
self.graph_dir = Path(graph_dir)
|
|
103
|
+
self.db_path = self.graph_dir / "htmlgraph.db"
|
|
104
|
+
self.cost_calculator = CostCalculator()
|
|
105
|
+
|
|
106
|
+
if not self.db_path.exists():
|
|
107
|
+
raise FileNotFoundError(f"Database not found at {self.db_path}")
|
|
108
|
+
|
|
109
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
110
|
+
"""Get database connection with row factory."""
|
|
111
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
112
|
+
conn.row_factory = sqlite3.Row
|
|
113
|
+
return conn
|
|
114
|
+
|
|
115
|
+
def get_task_delegations(self) -> list[TaskDelegation]:
|
|
116
|
+
"""
|
|
117
|
+
Query all task_delegation events from the database.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of TaskDelegation objects ordered by timestamp (newest first)
|
|
121
|
+
"""
|
|
122
|
+
conn = self._get_connection()
|
|
123
|
+
try:
|
|
124
|
+
cursor = conn.cursor()
|
|
125
|
+
cursor.execute(
|
|
126
|
+
"""
|
|
127
|
+
SELECT
|
|
128
|
+
event_id,
|
|
129
|
+
session_id,
|
|
130
|
+
timestamp,
|
|
131
|
+
subagent_type,
|
|
132
|
+
parent_event_id
|
|
133
|
+
FROM agent_events
|
|
134
|
+
WHERE event_type = 'task_delegation'
|
|
135
|
+
ORDER BY timestamp DESC
|
|
136
|
+
"""
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
delegations = []
|
|
140
|
+
for row in cursor.fetchall():
|
|
141
|
+
timestamp = (
|
|
142
|
+
datetime.fromisoformat(row["timestamp"])
|
|
143
|
+
if isinstance(row["timestamp"], str)
|
|
144
|
+
else row["timestamp"]
|
|
145
|
+
)
|
|
146
|
+
delegations.append(
|
|
147
|
+
TaskDelegation(
|
|
148
|
+
event_id=row["event_id"],
|
|
149
|
+
session_id=row["session_id"],
|
|
150
|
+
timestamp=timestamp,
|
|
151
|
+
subagent_type=row["subagent_type"] or "unknown",
|
|
152
|
+
parent_event_id=row["parent_event_id"],
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return delegations
|
|
157
|
+
finally:
|
|
158
|
+
conn.close()
|
|
159
|
+
|
|
160
|
+
def calculate_task_cost(self, event_id: str) -> tuple[int, list[dict[str, Any]]]:
|
|
161
|
+
"""
|
|
162
|
+
Calculate total cost of a Task delegation.
|
|
163
|
+
|
|
164
|
+
Sums all token costs of child tool calls using cost_tokens field.
|
|
165
|
+
Falls back to CIGS cost estimation if cost_tokens is not available.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
event_id: Task delegation event ID
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Tuple of (total_cost_tokens, child_events_list)
|
|
172
|
+
"""
|
|
173
|
+
conn = self._get_connection()
|
|
174
|
+
try:
|
|
175
|
+
cursor = conn.cursor()
|
|
176
|
+
|
|
177
|
+
# Get all children of this task
|
|
178
|
+
cursor.execute(
|
|
179
|
+
"""
|
|
180
|
+
SELECT
|
|
181
|
+
event_id,
|
|
182
|
+
tool_name,
|
|
183
|
+
cost_tokens,
|
|
184
|
+
input_summary,
|
|
185
|
+
output_summary,
|
|
186
|
+
timestamp
|
|
187
|
+
FROM agent_events
|
|
188
|
+
WHERE parent_event_id = ?
|
|
189
|
+
AND event_type IN ('tool_call', 'tool_result')
|
|
190
|
+
ORDER BY timestamp ASC
|
|
191
|
+
""",
|
|
192
|
+
(event_id,),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
total_cost = 0
|
|
196
|
+
child_events = []
|
|
197
|
+
|
|
198
|
+
for row in cursor.fetchall():
|
|
199
|
+
cost = row["cost_tokens"] if row["cost_tokens"] else 0
|
|
200
|
+
|
|
201
|
+
# If no stored cost, estimate based on tool type
|
|
202
|
+
if cost == 0 and row["tool_name"]:
|
|
203
|
+
cost = self.cost_calculator.predict_cost(row["tool_name"], {})
|
|
204
|
+
|
|
205
|
+
total_cost += cost
|
|
206
|
+
|
|
207
|
+
child_events.append(
|
|
208
|
+
{
|
|
209
|
+
"event_id": row["event_id"],
|
|
210
|
+
"tool_name": row["tool_name"],
|
|
211
|
+
"cost_tokens": cost,
|
|
212
|
+
"timestamp": row["timestamp"],
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return total_cost, child_events
|
|
217
|
+
finally:
|
|
218
|
+
conn.close()
|
|
219
|
+
|
|
220
|
+
def get_task_delegations_with_costs(self) -> dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Get all task delegations with calculated costs.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Dictionary with:
|
|
226
|
+
- delegations: List of TaskDelegation with costs
|
|
227
|
+
- total_cost_tokens: Sum of all delegation costs
|
|
228
|
+
- total_delegations: Count of delegations
|
|
229
|
+
- by_subagent_type: Cost breakdown by subagent
|
|
230
|
+
- by_tool_type: Cost breakdown by tool
|
|
231
|
+
"""
|
|
232
|
+
delegations = self.get_task_delegations()
|
|
233
|
+
|
|
234
|
+
total_cost = 0
|
|
235
|
+
by_subagent: dict[str, int] = {}
|
|
236
|
+
by_tool: dict[str, int] = {}
|
|
237
|
+
|
|
238
|
+
for delegation in delegations:
|
|
239
|
+
cost, child_events = self.calculate_task_cost(delegation.event_id)
|
|
240
|
+
delegation.total_cost_tokens = cost
|
|
241
|
+
delegation.child_events = child_events
|
|
242
|
+
delegation.tool_count = len(child_events)
|
|
243
|
+
|
|
244
|
+
total_cost += cost
|
|
245
|
+
|
|
246
|
+
# Track by subagent type
|
|
247
|
+
subagent = delegation.subagent_type
|
|
248
|
+
by_subagent[subagent] = by_subagent.get(subagent, 0) + cost
|
|
249
|
+
|
|
250
|
+
# Track by tool type
|
|
251
|
+
for child in child_events:
|
|
252
|
+
tool = child["tool_name"] or "unknown"
|
|
253
|
+
by_tool[tool] = by_tool.get(tool, 0) + child["cost_tokens"]
|
|
254
|
+
|
|
255
|
+
# Convert to USD (approximation: 1M tokens ~ $3 for input, $6 for output)
|
|
256
|
+
# Average: ~$4.50 per 1M tokens
|
|
257
|
+
total_cost_usd = total_cost * 0.0000045
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"delegations": delegations,
|
|
261
|
+
"total_cost_tokens": total_cost,
|
|
262
|
+
"total_cost_usd": total_cost_usd,
|
|
263
|
+
"total_delegations": len(delegations),
|
|
264
|
+
"avg_cost_per_delegation": (
|
|
265
|
+
total_cost / len(delegations) if delegations else 0
|
|
266
|
+
),
|
|
267
|
+
"by_subagent_type": by_subagent,
|
|
268
|
+
"by_tool_type": by_tool,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def get_cost_by_subagent_type(self) -> dict[str, int]:
|
|
272
|
+
"""
|
|
273
|
+
Group delegation costs by subagent type.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dictionary mapping subagent_type to total tokens spent
|
|
277
|
+
"""
|
|
278
|
+
data = self.get_task_delegations_with_costs()
|
|
279
|
+
result = data.get("by_subagent_type", {})
|
|
280
|
+
if isinstance(result, dict):
|
|
281
|
+
return result
|
|
282
|
+
return {}
|
|
283
|
+
|
|
284
|
+
def get_cost_by_tool_type(self) -> dict[str, int]:
|
|
285
|
+
"""
|
|
286
|
+
Show which tools cost most across all delegations.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dictionary mapping tool_name to total tokens spent
|
|
290
|
+
"""
|
|
291
|
+
data = self.get_task_delegations_with_costs()
|
|
292
|
+
result = data.get("by_tool_type", {})
|
|
293
|
+
if isinstance(result, dict):
|
|
294
|
+
return result
|
|
295
|
+
return {}
|
|
296
|
+
|
|
297
|
+
def get_roi_stats(self) -> ROIStats:
|
|
298
|
+
"""
|
|
299
|
+
Calculate ROI statistics comparing delegation vs direct execution.
|
|
300
|
+
|
|
301
|
+
Assumptions:
|
|
302
|
+
- Direct execution: Tokens spent directly on main agent
|
|
303
|
+
- Delegated execution: Tokens in child subagents (already counted)
|
|
304
|
+
- Savings: Context preservation + parallelization benefits
|
|
305
|
+
- Parallelization factor: 1.2-1.5x (subagents can work more efficiently)
|
|
306
|
+
- Context preservation: ~30% token savings from better focus
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
ROIStats with cost and savings analysis
|
|
310
|
+
"""
|
|
311
|
+
data = self.get_task_delegations_with_costs()
|
|
312
|
+
|
|
313
|
+
total_delegation_cost = data["total_cost_tokens"]
|
|
314
|
+
total_delegations = data["total_delegations"]
|
|
315
|
+
|
|
316
|
+
# Estimate direct execution cost
|
|
317
|
+
# Assumption: direct execution would cost 2.5x due to context overhead
|
|
318
|
+
estimated_direct_cost = int(total_delegation_cost * 2.5)
|
|
319
|
+
|
|
320
|
+
# Estimate savings
|
|
321
|
+
# Parallelization benefit: 1.2x efficiency
|
|
322
|
+
# Context preservation: 30% savings
|
|
323
|
+
parallelization_factor = 1.2
|
|
324
|
+
context_benefit = 0.30
|
|
325
|
+
|
|
326
|
+
estimated_savings = int(
|
|
327
|
+
estimated_direct_cost
|
|
328
|
+
- (total_delegation_cost * parallelization_factor * (1.0 - context_benefit))
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
savings_percentage = (
|
|
332
|
+
(estimated_savings / estimated_direct_cost * 100)
|
|
333
|
+
if estimated_direct_cost > 0
|
|
334
|
+
else 0.0
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return ROIStats(
|
|
338
|
+
total_delegation_cost=total_delegation_cost,
|
|
339
|
+
estimated_direct_cost=estimated_direct_cost,
|
|
340
|
+
estimated_savings=estimated_savings,
|
|
341
|
+
savings_percentage=savings_percentage,
|
|
342
|
+
avg_parallelization_factor=parallelization_factor,
|
|
343
|
+
context_preservation_benefit=context_benefit,
|
|
344
|
+
total_delegations=total_delegations,
|
|
345
|
+
avg_cost_per_delegation=(
|
|
346
|
+
total_delegation_cost / total_delegations
|
|
347
|
+
if total_delegations > 0
|
|
348
|
+
else 0.0
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def get_top_delegations(self, limit: int = 10) -> list[TaskDelegation]:
|
|
353
|
+
"""
|
|
354
|
+
Get the most expensive Task delegations.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
limit: Number of delegations to return (default: 10)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of TaskDelegation sorted by cost (descending)
|
|
361
|
+
"""
|
|
362
|
+
data = self.get_task_delegations_with_costs()
|
|
363
|
+
delegations = data["delegations"]
|
|
364
|
+
|
|
365
|
+
# Sort by cost descending
|
|
366
|
+
sorted_delegations = sorted(
|
|
367
|
+
delegations, key=lambda d: d.total_cost_tokens, reverse=True
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return sorted_delegations[:limit]
|
|
371
|
+
|
|
372
|
+
def get_cost_breakdown(self) -> CostBreakdown:
|
|
373
|
+
"""
|
|
374
|
+
Get comprehensive cost breakdown.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
CostBreakdown with by_subagent and by_tool analysis
|
|
378
|
+
"""
|
|
379
|
+
data = self.get_task_delegations_with_costs()
|
|
380
|
+
|
|
381
|
+
return CostBreakdown(
|
|
382
|
+
by_subagent=data["by_subagent_type"],
|
|
383
|
+
by_tool=data["by_tool_type"],
|
|
384
|
+
total_cost_tokens=data["total_cost_tokens"],
|
|
385
|
+
total_delegations=data["total_delegations"],
|
|
386
|
+
avg_cost_per_delegation=data["avg_cost_per_delegation"],
|
|
387
|
+
)
|
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CostReporter for OTEL ROI Analysis - Phase 1.
|
|
3
|
+
|
|
4
|
+
Generates interactive HTML dashboards showing cost analysis of Task delegations.
|
|
5
|
+
Creates visualizations of delegation costs, ROI, and optimization recommendations.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Top 10 most expensive Task delegations
|
|
9
|
+
- Cost breakdown by subagent type and tool type
|
|
10
|
+
- ROI analysis comparing delegation vs direct execution
|
|
11
|
+
- Interactive charts using Chart.js
|
|
12
|
+
- Dark theme matching HtmlGraph visual style
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from htmlgraph.analytics.cost_analyzer import CostAnalyzer
|
|
16
|
+
from htmlgraph.analytics.cost_reporter import CostReporter
|
|
17
|
+
|
|
18
|
+
analyzer = CostAnalyzer()
|
|
19
|
+
reporter = CostReporter()
|
|
20
|
+
html = reporter.generate_dashboard(analyzer)
|
|
21
|
+
reporter.save_dashboard(html, "cost-analysis.html")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from htmlgraph.analytics.cost_analyzer import CostAnalyzer
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CostReporter:
|
|
33
|
+
"""Generate interactive HTML dashboards from cost analysis data."""
|
|
34
|
+
|
|
35
|
+
THEME = {
|
|
36
|
+
"bg_primary": "#1a1a2e",
|
|
37
|
+
"bg_secondary": "#16213e",
|
|
38
|
+
"text_primary": "#e0e0e0",
|
|
39
|
+
"text_secondary": "#b0b0b0",
|
|
40
|
+
"accent_primary": "#0f3460",
|
|
41
|
+
"accent_secondary": "#e94560",
|
|
42
|
+
"success": "#4caf50",
|
|
43
|
+
"warning": "#ff9800",
|
|
44
|
+
"error": "#f44336",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
"""Initialize CostReporter."""
|
|
49
|
+
self.generated_at = datetime.now()
|
|
50
|
+
|
|
51
|
+
def generate_dashboard(self, analyzer: CostAnalyzer) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Generate complete interactive HTML dashboard.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
analyzer: CostAnalyzer instance with cost data
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Complete HTML document as string
|
|
60
|
+
"""
|
|
61
|
+
# Gather data
|
|
62
|
+
cost_breakdown = analyzer.get_cost_breakdown()
|
|
63
|
+
roi_stats = analyzer.get_roi_stats()
|
|
64
|
+
top_delegations = analyzer.get_top_delegations(10)
|
|
65
|
+
all_delegations = analyzer.get_task_delegations_with_costs()
|
|
66
|
+
|
|
67
|
+
# Build dashboard components
|
|
68
|
+
html_parts = [
|
|
69
|
+
self._html_header(),
|
|
70
|
+
self._html_styles(),
|
|
71
|
+
self._html_body_open(),
|
|
72
|
+
self._html_header_section(roi_stats),
|
|
73
|
+
self._html_summary_cards(roi_stats, cost_breakdown),
|
|
74
|
+
self._html_charts_section(cost_breakdown, all_delegations),
|
|
75
|
+
self._html_top_delegations_table(top_delegations),
|
|
76
|
+
self._html_insights_section(roi_stats, cost_breakdown),
|
|
77
|
+
self._html_footer_section(),
|
|
78
|
+
self._html_body_close(),
|
|
79
|
+
self._html_scripts(cost_breakdown, all_delegations),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
return "\n".join(html_parts)
|
|
83
|
+
|
|
84
|
+
def save_dashboard(self, html: str, path: str | Path) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Save HTML dashboard to file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
html: HTML content to save
|
|
90
|
+
path: File path to save to
|
|
91
|
+
"""
|
|
92
|
+
path = Path(path)
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
path.write_text(html, encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
# ===== HTML Generation Methods =====
|
|
97
|
+
|
|
98
|
+
def _html_header(self) -> str:
|
|
99
|
+
"""Generate HTML document header."""
|
|
100
|
+
return """<!DOCTYPE html>
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head>
|
|
103
|
+
<meta charset="UTF-8">
|
|
104
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
105
|
+
<title>Cost Attribution Dashboard - HtmlGraph OTEL ROI Analysis</title>
|
|
106
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
|
107
|
+
</head>"""
|
|
108
|
+
|
|
109
|
+
def _html_styles(self) -> str:
|
|
110
|
+
"""Generate CSS styles."""
|
|
111
|
+
return f"""<style>
|
|
112
|
+
* {{
|
|
113
|
+
margin: 0;
|
|
114
|
+
padding: 0;
|
|
115
|
+
box-sizing: border-box;
|
|
116
|
+
}}
|
|
117
|
+
|
|
118
|
+
body {{
|
|
119
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
120
|
+
background-color: {self.THEME["bg_primary"]};
|
|
121
|
+
color: {self.THEME["text_primary"]};
|
|
122
|
+
line-height: 1.6;
|
|
123
|
+
}}
|
|
124
|
+
|
|
125
|
+
a {{
|
|
126
|
+
color: {self.THEME["accent_secondary"]};
|
|
127
|
+
text-decoration: none;
|
|
128
|
+
}}
|
|
129
|
+
|
|
130
|
+
a:hover {{
|
|
131
|
+
text-decoration: underline;
|
|
132
|
+
}}
|
|
133
|
+
|
|
134
|
+
.container {{
|
|
135
|
+
max-width: 1400px;
|
|
136
|
+
margin: 0 auto;
|
|
137
|
+
padding: 20px;
|
|
138
|
+
}}
|
|
139
|
+
|
|
140
|
+
.header {{
|
|
141
|
+
border-bottom: 2px solid {self.THEME["accent_primary"]};
|
|
142
|
+
padding-bottom: 20px;
|
|
143
|
+
margin-bottom: 30px;
|
|
144
|
+
}}
|
|
145
|
+
|
|
146
|
+
.header h1 {{
|
|
147
|
+
font-size: 2.5em;
|
|
148
|
+
margin-bottom: 10px;
|
|
149
|
+
color: {self.THEME["text_primary"]};
|
|
150
|
+
}}
|
|
151
|
+
|
|
152
|
+
.header .subtitle {{
|
|
153
|
+
color: {self.THEME["text_secondary"]};
|
|
154
|
+
font-size: 0.95em;
|
|
155
|
+
}}
|
|
156
|
+
|
|
157
|
+
.header-meta {{
|
|
158
|
+
margin-top: 15px;
|
|
159
|
+
display: flex;
|
|
160
|
+
gap: 20px;
|
|
161
|
+
flex-wrap: wrap;
|
|
162
|
+
font-size: 0.9em;
|
|
163
|
+
color: {self.THEME["text_secondary"]};
|
|
164
|
+
}}
|
|
165
|
+
|
|
166
|
+
.cards {{
|
|
167
|
+
display: grid;
|
|
168
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
169
|
+
gap: 20px;
|
|
170
|
+
margin-bottom: 40px;
|
|
171
|
+
}}
|
|
172
|
+
|
|
173
|
+
.card {{
|
|
174
|
+
background-color: {self.THEME["bg_secondary"]};
|
|
175
|
+
border: 1px solid {self.THEME["accent_primary"]};
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
padding: 25px;
|
|
178
|
+
transition: all 0.3s ease;
|
|
179
|
+
}}
|
|
180
|
+
|
|
181
|
+
.card:hover {{
|
|
182
|
+
border-color: {self.THEME["accent_secondary"]};
|
|
183
|
+
transform: translateY(-5px);
|
|
184
|
+
box-shadow: 0 10px 30px rgba(233, 69, 96, 0.2);
|
|
185
|
+
}}
|
|
186
|
+
|
|
187
|
+
.card-label {{
|
|
188
|
+
color: {self.THEME["text_secondary"]};
|
|
189
|
+
font-size: 0.85em;
|
|
190
|
+
text-transform: uppercase;
|
|
191
|
+
letter-spacing: 0.5px;
|
|
192
|
+
margin-bottom: 10px;
|
|
193
|
+
}}
|
|
194
|
+
|
|
195
|
+
.card-value {{
|
|
196
|
+
font-size: 2.5em;
|
|
197
|
+
font-weight: bold;
|
|
198
|
+
margin-bottom: 8px;
|
|
199
|
+
color: {self.THEME["text_primary"]};
|
|
200
|
+
}}
|
|
201
|
+
|
|
202
|
+
.card-unit {{
|
|
203
|
+
color: {self.THEME["text_secondary"]};
|
|
204
|
+
font-size: 0.9em;
|
|
205
|
+
font-weight: normal;
|
|
206
|
+
}}
|
|
207
|
+
|
|
208
|
+
.card-meta {{
|
|
209
|
+
color: {self.THEME["text_secondary"]};
|
|
210
|
+
font-size: 0.85em;
|
|
211
|
+
margin-top: 12px;
|
|
212
|
+
padding-top: 12px;
|
|
213
|
+
border-top: 1px solid {self.THEME["accent_primary"]};
|
|
214
|
+
}}
|
|
215
|
+
|
|
216
|
+
.status-good {{
|
|
217
|
+
color: {self.THEME["success"]};
|
|
218
|
+
}}
|
|
219
|
+
|
|
220
|
+
.status-warning {{
|
|
221
|
+
color: {self.THEME["warning"]};
|
|
222
|
+
}}
|
|
223
|
+
|
|
224
|
+
.status-error {{
|
|
225
|
+
color: {self.THEME["error"]};
|
|
226
|
+
}}
|
|
227
|
+
|
|
228
|
+
.charts {{
|
|
229
|
+
display: grid;
|
|
230
|
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
231
|
+
gap: 30px;
|
|
232
|
+
margin-bottom: 40px;
|
|
233
|
+
}}
|
|
234
|
+
|
|
235
|
+
.chart-container {{
|
|
236
|
+
background-color: {self.THEME["bg_secondary"]};
|
|
237
|
+
border: 1px solid {self.THEME["accent_primary"]};
|
|
238
|
+
border-radius: 8px;
|
|
239
|
+
padding: 20px;
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
.chart-title {{
|
|
243
|
+
font-size: 1.3em;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
margin-bottom: 20px;
|
|
246
|
+
color: {self.THEME["text_primary"]};
|
|
247
|
+
}}
|
|
248
|
+
|
|
249
|
+
.chart-canvas {{
|
|
250
|
+
max-height: 400px;
|
|
251
|
+
}}
|
|
252
|
+
|
|
253
|
+
.table-section {{
|
|
254
|
+
margin-bottom: 40px;
|
|
255
|
+
}}
|
|
256
|
+
|
|
257
|
+
.table-section h2 {{
|
|
258
|
+
font-size: 1.5em;
|
|
259
|
+
margin-bottom: 20px;
|
|
260
|
+
color: {self.THEME["text_primary"]};
|
|
261
|
+
}}
|
|
262
|
+
|
|
263
|
+
.table-wrapper {{
|
|
264
|
+
overflow-x: auto;
|
|
265
|
+
background-color: {self.THEME["bg_secondary"]};
|
|
266
|
+
border: 1px solid {self.THEME["accent_primary"]};
|
|
267
|
+
border-radius: 8px;
|
|
268
|
+
}}
|
|
269
|
+
|
|
270
|
+
table {{
|
|
271
|
+
width: 100%;
|
|
272
|
+
border-collapse: collapse;
|
|
273
|
+
}}
|
|
274
|
+
|
|
275
|
+
th {{
|
|
276
|
+
background-color: {self.THEME["accent_primary"]};
|
|
277
|
+
padding: 15px;
|
|
278
|
+
text-align: left;
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
color: {self.THEME["text_primary"]};
|
|
281
|
+
border-bottom: 2px solid {self.THEME["accent_secondary"]};
|
|
282
|
+
}}
|
|
283
|
+
|
|
284
|
+
td {{
|
|
285
|
+
padding: 12px 15px;
|
|
286
|
+
border-bottom: 1px solid {self.THEME["accent_primary"]};
|
|
287
|
+
color: {self.THEME["text_primary"]};
|
|
288
|
+
}}
|
|
289
|
+
|
|
290
|
+
tr:hover {{
|
|
291
|
+
background-color: {self.THEME["accent_primary"]};
|
|
292
|
+
}}
|
|
293
|
+
|
|
294
|
+
.insights {{
|
|
295
|
+
background-color: {self.THEME["bg_secondary"]};
|
|
296
|
+
border: 1px solid {self.THEME["accent_primary"]};
|
|
297
|
+
border-radius: 8px;
|
|
298
|
+
padding: 25px;
|
|
299
|
+
margin-bottom: 40px;
|
|
300
|
+
}}
|
|
301
|
+
|
|
302
|
+
.insights h2 {{
|
|
303
|
+
font-size: 1.5em;
|
|
304
|
+
margin-bottom: 20px;
|
|
305
|
+
color: {self.THEME["text_primary"]};
|
|
306
|
+
}}
|
|
307
|
+
|
|
308
|
+
.insight {{
|
|
309
|
+
margin-bottom: 20px;
|
|
310
|
+
padding: 15px;
|
|
311
|
+
background-color: {self.THEME["bg_primary"]};
|
|
312
|
+
border-left: 4px solid {self.THEME["accent_secondary"]};
|
|
313
|
+
border-radius: 4px;
|
|
314
|
+
}}
|
|
315
|
+
|
|
316
|
+
.insight-title {{
|
|
317
|
+
font-weight: 600;
|
|
318
|
+
color: {self.THEME["accent_secondary"]};
|
|
319
|
+
margin-bottom: 8px;
|
|
320
|
+
}}
|
|
321
|
+
|
|
322
|
+
.insight-text {{
|
|
323
|
+
color: {self.THEME["text_secondary"]};
|
|
324
|
+
line-height: 1.6;
|
|
325
|
+
}}
|
|
326
|
+
|
|
327
|
+
.footer {{
|
|
328
|
+
text-align: center;
|
|
329
|
+
padding-top: 20px;
|
|
330
|
+
border-top: 1px solid {self.THEME["accent_primary"]};
|
|
331
|
+
color: {self.THEME["text_secondary"]};
|
|
332
|
+
font-size: 0.85em;
|
|
333
|
+
}}
|
|
334
|
+
|
|
335
|
+
@media (max-width: 768px) {{
|
|
336
|
+
.charts {{
|
|
337
|
+
grid-template-columns: 1fr;
|
|
338
|
+
}}
|
|
339
|
+
|
|
340
|
+
.header h1 {{
|
|
341
|
+
font-size: 1.8em;
|
|
342
|
+
}}
|
|
343
|
+
|
|
344
|
+
.card-value {{
|
|
345
|
+
font-size: 2em;
|
|
346
|
+
}}
|
|
347
|
+
|
|
348
|
+
table {{
|
|
349
|
+
font-size: 0.9em;
|
|
350
|
+
}}
|
|
351
|
+
|
|
352
|
+
th, td {{
|
|
353
|
+
padding: 10px;
|
|
354
|
+
}}
|
|
355
|
+
}}
|
|
356
|
+
</style>"""
|
|
357
|
+
|
|
358
|
+
def _html_body_open(self) -> str:
|
|
359
|
+
"""Generate opening body tag."""
|
|
360
|
+
return "<body>"
|
|
361
|
+
|
|
362
|
+
def _html_body_close(self) -> str:
|
|
363
|
+
"""Generate closing body tag."""
|
|
364
|
+
return "</body></html>"
|
|
365
|
+
|
|
366
|
+
def _html_header_section(self, roi_stats: Any) -> str:
|
|
367
|
+
"""Generate dashboard header section."""
|
|
368
|
+
generated_at = self.generated_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
369
|
+
|
|
370
|
+
return f"""<div class="container">
|
|
371
|
+
<div class="header">
|
|
372
|
+
<h1>Cost Attribution Dashboard</h1>
|
|
373
|
+
<p class="subtitle">OTEL ROI Analysis - Phase 1 MVP</p>
|
|
374
|
+
<div class="header-meta">
|
|
375
|
+
<span>Generated: <strong>{generated_at}</strong></span>
|
|
376
|
+
<span>Total Delegations: <strong>{roi_stats.total_delegations}</strong></span>
|
|
377
|
+
<span>Status: <strong class="status-good">✓ Active</strong></span>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def _html_summary_cards(self, roi_stats: Any, cost_breakdown: Any) -> str:
|
|
383
|
+
"""Generate summary metric cards."""
|
|
384
|
+
total_cost_usd = roi_stats.total_delegation_cost * 0.0000045
|
|
385
|
+
|
|
386
|
+
return f"""<div class="cards">
|
|
387
|
+
<div class="card">
|
|
388
|
+
<div class="card-label">Total Delegation Cost</div>
|
|
389
|
+
<div class="card-value">${total_cost_usd:.2f}<span class="card-unit"> USD</span></div>
|
|
390
|
+
<div class="card-meta">{roi_stats.total_delegation_cost:,} tokens</div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<div class="card">
|
|
394
|
+
<div class="card-label">Estimated Direct Cost</div>
|
|
395
|
+
<div class="card-value">${roi_stats.estimated_direct_cost * 0.0000045:.2f}<span class="card-unit"> USD</span></div>
|
|
396
|
+
<div class="card-meta">{roi_stats.estimated_direct_cost:,} tokens (2.5x overhead)</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div class="card">
|
|
400
|
+
<div class="card-label">Estimated Savings</div>
|
|
401
|
+
<div class="card-value"><span class="status-good">${roi_stats.estimated_savings * 0.0000045:.2f}</span></div>
|
|
402
|
+
<div class="card-meta"><span class="status-good">{roi_stats.savings_percentage:.0f}% reduction</span></div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div class="card">
|
|
406
|
+
<div class="card-label">Avg Cost per Delegation</div>
|
|
407
|
+
<div class="card-value">{roi_stats.avg_cost_per_delegation:,.0f}<span class="card-unit"> tokens</span></div>
|
|
408
|
+
<div class="card-meta">Across {roi_stats.total_delegations} delegations</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def _html_charts_section(self, cost_breakdown: Any, all_delegations: Any) -> str:
|
|
414
|
+
"""Generate charts section with visualizations."""
|
|
415
|
+
# Prepare data for charts
|
|
416
|
+
subagent_labels = list(cost_breakdown.by_subagent.keys())
|
|
417
|
+
subagent_data = list(cost_breakdown.by_subagent.values())
|
|
418
|
+
|
|
419
|
+
tool_labels = list(cost_breakdown.by_tool.keys())
|
|
420
|
+
tool_data = list(cost_breakdown.by_tool.values())
|
|
421
|
+
|
|
422
|
+
# Embed data as JSON for JavaScript
|
|
423
|
+
subagent_labels_json = json.dumps(subagent_labels)
|
|
424
|
+
subagent_data_json = json.dumps(subagent_data)
|
|
425
|
+
tool_labels_json = json.dumps(tool_labels)
|
|
426
|
+
tool_data_json = json.dumps(tool_data)
|
|
427
|
+
|
|
428
|
+
return f"""<div class="charts">
|
|
429
|
+
<div class="chart-container">
|
|
430
|
+
<div class="chart-title">Cost by Subagent Type</div>
|
|
431
|
+
<canvas id="chartBySubagent" class="chart-canvas"></canvas>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div class="chart-container">
|
|
435
|
+
<div class="chart-title">Cost by Tool Type</div>
|
|
436
|
+
<canvas id="chartByTool" class="chart-canvas"></canvas>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<script>
|
|
441
|
+
window.chartData = {{
|
|
442
|
+
subagent_labels: {subagent_labels_json},
|
|
443
|
+
subagent_data: {subagent_data_json},
|
|
444
|
+
tool_labels: {tool_labels_json},
|
|
445
|
+
tool_data: {tool_data_json}
|
|
446
|
+
}};
|
|
447
|
+
</script>
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
def _html_top_delegations_table(self, top_delegations: list[Any]) -> str:
|
|
451
|
+
"""Generate top delegations table."""
|
|
452
|
+
table_rows = ""
|
|
453
|
+
|
|
454
|
+
for i, delegation in enumerate(top_delegations, 1):
|
|
455
|
+
cost_usd = delegation.total_cost_tokens * 0.0000045
|
|
456
|
+
timestamp = delegation.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
457
|
+
|
|
458
|
+
table_rows += f""" <tr>
|
|
459
|
+
<td>{i}</td>
|
|
460
|
+
<td>{timestamp}</td>
|
|
461
|
+
<td><strong>{delegation.subagent_type}</strong></td>
|
|
462
|
+
<td>{delegation.tool_count}</td>
|
|
463
|
+
<td>{delegation.total_cost_tokens:,}</td>
|
|
464
|
+
<td>${cost_usd:.2f}</td>
|
|
465
|
+
<td><code style="font-size: 0.85em; color: #b0b0b0;">{delegation.event_id[:16]}...</code></td>
|
|
466
|
+
</tr>
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
return f"""<div class="table-section">
|
|
470
|
+
<h2>Top 10 Most Expensive Delegations</h2>
|
|
471
|
+
<div class="table-wrapper">
|
|
472
|
+
<table>
|
|
473
|
+
<thead>
|
|
474
|
+
<tr>
|
|
475
|
+
<th>#</th>
|
|
476
|
+
<th>Timestamp</th>
|
|
477
|
+
<th>Subagent Type</th>
|
|
478
|
+
<th>Tool Count</th>
|
|
479
|
+
<th>Cost (tokens)</th>
|
|
480
|
+
<th>Cost (USD)</th>
|
|
481
|
+
<th>Event ID</th>
|
|
482
|
+
</tr>
|
|
483
|
+
</thead>
|
|
484
|
+
<tbody>
|
|
485
|
+
{table_rows} </tbody>
|
|
486
|
+
</table>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
def _html_insights_section(self, roi_stats: Any, cost_breakdown: Any) -> str:
|
|
492
|
+
"""Generate insights and recommendations."""
|
|
493
|
+
insights = []
|
|
494
|
+
|
|
495
|
+
# Insight 1: ROI summary
|
|
496
|
+
if roi_stats.savings_percentage > 40:
|
|
497
|
+
insights.append(
|
|
498
|
+
(
|
|
499
|
+
"Strong Delegation ROI",
|
|
500
|
+
f"Your delegation strategy is effective. By properly delegating work, "
|
|
501
|
+
f"you're achieving approximately {roi_stats.savings_percentage:.0f}% cost savings "
|
|
502
|
+
f"compared to direct execution.",
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
else:
|
|
506
|
+
insights.append(
|
|
507
|
+
(
|
|
508
|
+
"Delegation Opportunity",
|
|
509
|
+
f"Current delegation strategy shows {roi_stats.savings_percentage:.0f}% potential savings. "
|
|
510
|
+
f"Consider delegating more complex work to specialized subagents.",
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Insight 2: Most expensive subagent
|
|
515
|
+
if cost_breakdown.by_subagent:
|
|
516
|
+
top_subagent = max(cost_breakdown.by_subagent.items(), key=lambda x: x[1])
|
|
517
|
+
insights.append(
|
|
518
|
+
(
|
|
519
|
+
f"Highest Cost Subagent: {top_subagent[0]}",
|
|
520
|
+
f"This subagent type accounts for {top_subagent[1]:,} tokens. "
|
|
521
|
+
f"Review the work being delegated here for optimization opportunities.",
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Insight 3: Most expensive tool
|
|
526
|
+
if cost_breakdown.by_tool:
|
|
527
|
+
top_tool = max(cost_breakdown.by_tool.items(), key=lambda x: x[1])
|
|
528
|
+
insights.append(
|
|
529
|
+
(
|
|
530
|
+
f"Highest Cost Tool: {top_tool[0]}",
|
|
531
|
+
f"This tool is consuming {top_tool[1]:,} tokens. "
|
|
532
|
+
f"Consider batching operations or using more efficient approaches.",
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Insight 4: Parallelization benefit
|
|
537
|
+
insights.append(
|
|
538
|
+
(
|
|
539
|
+
"Parallelization Benefit",
|
|
540
|
+
f"Subagent delegation enables {roi_stats.avg_parallelization_factor:.1f}x efficiency gain "
|
|
541
|
+
f"through parallel execution and focused context.",
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
insights_html = ""
|
|
546
|
+
for title, text in insights:
|
|
547
|
+
insights_html += f""" <div class="insight">
|
|
548
|
+
<div class="insight-title">{title}</div>
|
|
549
|
+
<div class="insight-text">{text}</div>
|
|
550
|
+
</div>
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
return f"""<div class="insights">
|
|
554
|
+
<h2>Insights & Recommendations</h2>
|
|
555
|
+
{insights_html} </div>
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
def _html_footer_section(self) -> str:
|
|
559
|
+
"""Generate footer section."""
|
|
560
|
+
return """ <div class="footer">
|
|
561
|
+
<p>HtmlGraph Cost Attribution Dashboard | OTEL ROI Analysis Phase 1 | <a href="https://code.claude.com">Claude Code</a></p>
|
|
562
|
+
</div>
|
|
563
|
+
</div>"""
|
|
564
|
+
|
|
565
|
+
def _html_scripts(self, cost_breakdown: Any, all_delegations: Any) -> str:
|
|
566
|
+
"""Generate JavaScript for interactive charts."""
|
|
567
|
+
return """<script>
|
|
568
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
569
|
+
const data = window.chartData || {
|
|
570
|
+
subagent_labels: [],
|
|
571
|
+
subagent_data: [],
|
|
572
|
+
tool_labels: [],
|
|
573
|
+
tool_data: []
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const chartOptions = {
|
|
577
|
+
responsive: true,
|
|
578
|
+
maintainAspectRatio: true,
|
|
579
|
+
plugins: {
|
|
580
|
+
legend: {
|
|
581
|
+
labels: {
|
|
582
|
+
color: '#e0e0e0',
|
|
583
|
+
font: { size: 12 }
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
tooltip: {
|
|
587
|
+
backgroundColor: 'rgba(26, 26, 46, 0.8)',
|
|
588
|
+
titleColor: '#e0e0e0',
|
|
589
|
+
bodyColor: '#b0b0b0',
|
|
590
|
+
borderColor: '#0f3460',
|
|
591
|
+
borderWidth: 1
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
scales: {
|
|
595
|
+
y: {
|
|
596
|
+
ticks: { color: '#b0b0b0' },
|
|
597
|
+
grid: { color: '#16213e' }
|
|
598
|
+
},
|
|
599
|
+
x: {
|
|
600
|
+
ticks: { color: '#b0b0b0' },
|
|
601
|
+
grid: { color: '#16213e' }
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Doughnut chart - Cost by Subagent Type
|
|
607
|
+
if (data.subagent_labels.length > 0) {
|
|
608
|
+
const ctxSubagent = document.getElementById('chartBySubagent');
|
|
609
|
+
if (ctxSubagent) {
|
|
610
|
+
new Chart(ctxSubagent, {
|
|
611
|
+
type: 'doughnut',
|
|
612
|
+
data: {
|
|
613
|
+
labels: data.subagent_labels,
|
|
614
|
+
datasets: [{
|
|
615
|
+
label: 'Cost (tokens)',
|
|
616
|
+
data: data.subagent_data,
|
|
617
|
+
backgroundColor: [
|
|
618
|
+
'#e94560',
|
|
619
|
+
'#ff9800',
|
|
620
|
+
'#ff6f00',
|
|
621
|
+
'#d84315',
|
|
622
|
+
'#c62828',
|
|
623
|
+
'#0f3460'
|
|
624
|
+
],
|
|
625
|
+
borderColor: '#1a1a2e',
|
|
626
|
+
borderWidth: 2
|
|
627
|
+
}]
|
|
628
|
+
},
|
|
629
|
+
options: {
|
|
630
|
+
...chartOptions,
|
|
631
|
+
plugins: {
|
|
632
|
+
...chartOptions.plugins,
|
|
633
|
+
legend: {
|
|
634
|
+
...chartOptions.plugins.legend,
|
|
635
|
+
position: 'bottom'
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Bar chart - Cost by Tool
|
|
644
|
+
if (data.tool_labels.length > 0) {
|
|
645
|
+
const ctxTool = document.getElementById('chartByTool');
|
|
646
|
+
if (ctxTool) {
|
|
647
|
+
new Chart(ctxTool, {
|
|
648
|
+
type: 'bar',
|
|
649
|
+
data: {
|
|
650
|
+
labels: data.tool_labels,
|
|
651
|
+
datasets: [{
|
|
652
|
+
label: 'Cost (tokens)',
|
|
653
|
+
data: data.tool_data,
|
|
654
|
+
backgroundColor: '#0f3460',
|
|
655
|
+
borderColor: '#e94560',
|
|
656
|
+
borderWidth: 2,
|
|
657
|
+
borderRadius: 4
|
|
658
|
+
}]
|
|
659
|
+
},
|
|
660
|
+
options: {
|
|
661
|
+
...chartOptions,
|
|
662
|
+
indexAxis: 'y',
|
|
663
|
+
plugins: {
|
|
664
|
+
...chartOptions.plugins,
|
|
665
|
+
legend: {
|
|
666
|
+
...chartOptions.plugins.legend,
|
|
667
|
+
display: true
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
</script>"""
|
|
Binary file
|
htmlgraph/cli/analytics.py
CHANGED
|
@@ -96,6 +96,25 @@ def _register_cigs_commands(subparsers: _SubParsersAction) -> None:
|
|
|
96
96
|
cost_dashboard.add_argument("--output", help="Custom output path")
|
|
97
97
|
cost_dashboard.set_defaults(func=CostDashboardCommand.from_args)
|
|
98
98
|
|
|
99
|
+
# cigs roi-analysis (Phase 1 OTEL ROI)
|
|
100
|
+
roi_analysis = cigs_subparsers.add_parser(
|
|
101
|
+
"roi-analysis", help="OTEL ROI analysis - cost attribution of Task delegations"
|
|
102
|
+
)
|
|
103
|
+
roi_analysis.add_argument(
|
|
104
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
105
|
+
)
|
|
106
|
+
roi_analysis.add_argument(
|
|
107
|
+
"--save", action="store_true", help="Save to .htmlgraph/cost-analysis.html"
|
|
108
|
+
)
|
|
109
|
+
roi_analysis.add_argument(
|
|
110
|
+
"--open", action="store_true", help="Open in browser after generation"
|
|
111
|
+
)
|
|
112
|
+
roi_analysis.add_argument(
|
|
113
|
+
"--json", action="store_true", help="Output JSON instead of HTML"
|
|
114
|
+
)
|
|
115
|
+
roi_analysis.add_argument("--output", help="Custom output path")
|
|
116
|
+
# roi_analysis.set_defaults(func=OTELROIAnalysisCommand.from_args) # TODO: Implement OTELROIAnalysisCommand
|
|
117
|
+
|
|
99
118
|
# cigs status
|
|
100
119
|
cigs_status = cigs_subparsers.add_parser("status", help="Show CIGS status")
|
|
101
120
|
cigs_status.add_argument(
|
htmlgraph/hooks/posttooluse.py
CHANGED
|
@@ -49,6 +49,48 @@ async def run_event_tracking(
|
|
|
49
49
|
Returns:
|
|
50
50
|
Event tracking response: {"continue": True, "hookSpecificOutput": {...}}
|
|
51
51
|
"""
|
|
52
|
+
# ============ DEBUG: Log Task() responses ============
|
|
53
|
+
import json
|
|
54
|
+
|
|
55
|
+
tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
|
|
56
|
+
if tool_name == "Task":
|
|
57
|
+
tool_response = hook_input.get("tool_response", {})
|
|
58
|
+
|
|
59
|
+
print("\n" + "=" * 60, file=sys.stderr)
|
|
60
|
+
print("DEBUG: Task() PostToolUse Hook Input", file=sys.stderr)
|
|
61
|
+
print("=" * 60, file=sys.stderr)
|
|
62
|
+
|
|
63
|
+
print(f"\nTool Name: {tool_name}", file=sys.stderr)
|
|
64
|
+
print(f"Hook Type: {hook_type}", file=sys.stderr)
|
|
65
|
+
|
|
66
|
+
if isinstance(tool_response, dict):
|
|
67
|
+
print("\nTool Response Type: dict", file=sys.stderr)
|
|
68
|
+
print(f"Tool Response Keys: {list(tool_response.keys())}", file=sys.stderr)
|
|
69
|
+
|
|
70
|
+
# Check for task_id field
|
|
71
|
+
if "task_id" in tool_response:
|
|
72
|
+
task_id = tool_response.get("task_id")
|
|
73
|
+
print(f"\n✅ FOUND task_id: {task_id}", file=sys.stderr)
|
|
74
|
+
else:
|
|
75
|
+
print("\n❌ task_id NOT FOUND in response", file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
# Print full response for inspection
|
|
78
|
+
print("\nFull Tool Response:", file=sys.stderr)
|
|
79
|
+
try:
|
|
80
|
+
response_str = json.dumps(tool_response, indent=2, default=str)
|
|
81
|
+
print(response_str, file=sys.stderr)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"Could not serialize response: {e}", file=sys.stderr)
|
|
84
|
+
print(f"Raw response: {tool_response}", file=sys.stderr)
|
|
85
|
+
else:
|
|
86
|
+
print(
|
|
87
|
+
f"\nTool Response Type: {type(tool_response).__name__}", file=sys.stderr
|
|
88
|
+
)
|
|
89
|
+
print(f"Tool Response Value: {tool_response}", file=sys.stderr)
|
|
90
|
+
|
|
91
|
+
print("\n" + "=" * 60 + "\n", file=sys.stderr)
|
|
92
|
+
# ============ END DEBUG ============
|
|
93
|
+
|
|
52
94
|
try:
|
|
53
95
|
loop = asyncio.get_event_loop()
|
|
54
96
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: htmlgraph
|
|
3
|
-
Version: 0.26.
|
|
3
|
+
Version: 0.26.14
|
|
4
4
|
Summary: HTML is All You Need - Graph database on web standards
|
|
5
5
|
Project-URL: Homepage, https://github.com/Shakes-tzd/htmlgraph
|
|
6
6
|
Project-URL: Documentation, https://github.com/Shakes-tzd/htmlgraph#readme
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
htmlgraph/__init__.py,sha256=
|
|
1
|
+
htmlgraph/__init__.py,sha256=aFjqr9RP4Lzx2kZCoH8PRKdbaMd4an9tAnvEG9xLapo,5718
|
|
2
2
|
htmlgraph/agent_detection.py,sha256=wEmrDv4hssPX2OkEnJZBHPbalxcaloiJF_hOOow_5WE,3511
|
|
3
3
|
htmlgraph/agent_registry.py,sha256=Usa_35by7p5gtpvHO7K3AcGimnorw-FzgPVa3cWTQ58,9448
|
|
4
4
|
htmlgraph/agents.py,sha256=Yvu6x1nOfrW2WhRTAHiCuSpvqoVJXx1Mkzd59kwEczw,33466
|
|
@@ -69,8 +69,10 @@ htmlgraph/work_type_utils.py,sha256=exA3FnuSmVMMMSBhPYDW-Bq5jGaTDjX7jkckCvnVZ_s,
|
|
|
69
69
|
htmlgraph/.htmlgraph/.session-warning-state.json,sha256=X7xLzJc57pWuOLyg5iN7D9hp0CvaYY6UWBq8ImcIBmg,92
|
|
70
70
|
htmlgraph/.htmlgraph/agents.json,sha256=dKaiSsd1L7iaQa1_s9lCpNEZH-1S_n0CfzmCHFoWyvc,1391
|
|
71
71
|
htmlgraph/.htmlgraph/htmlgraph.db,sha256=gto2pndMBw97gljvwPywDlTUzYdoa_FnNJs3AsS9_SI,262144
|
|
72
|
-
htmlgraph/analytics/__init__.py,sha256=
|
|
72
|
+
htmlgraph/analytics/__init__.py,sha256=jVcxE-9eGupT8IC5YMh8SqPc_w4k7GpPK0WfXj6SE4M,607
|
|
73
73
|
htmlgraph/analytics/cli.py,sha256=aSnxkvNdrdukytqSirpFtcuiNRbg3Dt-hSpUsDPF9jo,14771
|
|
74
|
+
htmlgraph/analytics/cost_analyzer.py,sha256=tNVLRKesS6thCr89Xw6Bjz5K6Drn-pagiIjiF16hgQo,12854
|
|
75
|
+
htmlgraph/analytics/cost_reporter.py,sha256=_KumnYUYTr3yEHwBGGgjXSHR5IGPLDqjfuZHpfbeNi8,19917
|
|
74
76
|
htmlgraph/analytics/cross_session.py,sha256=BmTNqyt1qYjKbq5sXDYGxp85G2XB2aHkIvdqRgSIQBw,20494
|
|
75
77
|
htmlgraph/analytics/dependency.py,sha256=7Qc5xe0b1QE_P6-l4Z0WtstEmEXc4ZGNbyIEBMoABys,25757
|
|
76
78
|
htmlgraph/analytics/work_type.py,sha256=SrZvY_RVq70uvzl3Ky7I3LHgOMduW_0vdSsk5yUZRG4,19293
|
|
@@ -123,15 +125,15 @@ htmlgraph/cigs/reporter.py,sha256=DgfHjD_ufP6a9Fmfm_-Ro50bDCQrNC6BZYl3dYNl86o,24
|
|
|
123
125
|
htmlgraph/cigs/tracker.py,sha256=OcyXY7tGxbRCtlLX8Pg2bVtCtzPJtvh1VOtt4jbL9SI,10502
|
|
124
126
|
htmlgraph/cli/__init__.py,sha256=0r1gr-uzhMS9_wz88IjGf5ma4bDK-zTzUX6p8Wq5n2U,1031
|
|
125
127
|
htmlgraph/cli/__main__.py,sha256=lkTNsdh3X-vgKz_jzQe0IQmZY5PWlMjGBuaQR-gsqf8,137
|
|
126
|
-
htmlgraph/cli/analytics.py,sha256=
|
|
128
|
+
htmlgraph/cli/analytics.py,sha256=KRw40n8lq_TOJno0eM97qsMiVZpmU6R7YN0dKKI4gwQ,35654
|
|
127
129
|
htmlgraph/cli/base.py,sha256=OjybqWTQfLtQD5wBbz8IDDkE-2N6jpcXYdOq5BXza2I,20248
|
|
128
130
|
htmlgraph/cli/constants.py,sha256=S7hAih2Ka64c0sWqOdq7W762VRoBHri8w8XDDWqHCkE,7088
|
|
129
131
|
htmlgraph/cli/core.py,sha256=JhRL1iZhN3x4IeeVQkK4GnfAV7JQm5JDU6hG473GBOs,28878
|
|
130
132
|
htmlgraph/cli/main.py,sha256=rSSSErlmFHrREbAydtytj3up8M_dQ9jFgYL1OmrSQas,4108
|
|
131
133
|
htmlgraph/cli/models.py,sha256=7dEAwsroH2J_84Bq6i9Q0RLJsKQ9rNbX_daOfHdrb78,16033
|
|
132
|
-
htmlgraph/cli/.htmlgraph/.session-warning-state.json,sha256=
|
|
134
|
+
htmlgraph/cli/.htmlgraph/.session-warning-state.json,sha256=jKw9n3GDSPzcHwZZnRoW7eOJEbQPQ3FKpvo3kMY7lHw,93
|
|
133
135
|
htmlgraph/cli/.htmlgraph/agents.json,sha256=hVmmvoL4CDTIrhxH7VaM-jSDCj8kszWsUFY_LHae9DQ,1391
|
|
134
|
-
htmlgraph/cli/.htmlgraph/htmlgraph.db,sha256=
|
|
136
|
+
htmlgraph/cli/.htmlgraph/htmlgraph.db,sha256=Ipk3K21EQyTT5hkimtL1dBiwHve-3zdpS4yP1wMMU-4,1343488
|
|
135
137
|
htmlgraph/cli/templates/__init__.py,sha256=7eh8oC8rce689HGuuYqJEMCElJCtg084xOMHZti2XL8,37
|
|
136
138
|
htmlgraph/cli/templates/cost_dashboard.py,sha256=2FRaujRUYEDfA18c0QFju28yidfaVnMFH4pdcBaQ7Ao,11313
|
|
137
139
|
htmlgraph/cli/work/__init__.py,sha256=yFK8G85F-Z9df-3LBM-3NbjCgidtLEVhkYGO1uf4QuI,4570
|
|
@@ -201,7 +203,7 @@ htmlgraph/hooks/post-commit.sh,sha256=if65jNGZnEWsZPq_iYDNYunrZ1cmjPUEUbh6_4vfpO
|
|
|
201
203
|
htmlgraph/hooks/post-merge.sh,sha256=gq-EeFLhDUVp-J2jyWMBVFcB0pdmH54Wu1SW_Gn-s2I,541
|
|
202
204
|
htmlgraph/hooks/post_tool_use_failure.py,sha256=DHkJtuAOg5KSLfFZ1O-kePwaqmtNkbGQSEn4NplzvD8,8381
|
|
203
205
|
htmlgraph/hooks/post_tool_use_handler.py,sha256=e0L8X4sU2H167HH96CgV6iYojd02AI9uyF8m4A1a-kU,8345
|
|
204
|
-
htmlgraph/hooks/posttooluse.py,sha256=
|
|
206
|
+
htmlgraph/hooks/posttooluse.py,sha256=HOxW9lgJCnRLJRsdB7PFfAJkPWv-NiLjkF4wcBnjVUE,14606
|
|
205
207
|
htmlgraph/hooks/pre-commit.sh,sha256=gTpbnHIBFxpAl7-REhXoS0NI4Pmlqo9pQEMEngTAU_A,3865
|
|
206
208
|
htmlgraph/hooks/pre-push.sh,sha256=rNnkG8YmDtyk7OuJHOcbOYQR3MYFneaG6_w2X-Hl8Hs,660
|
|
207
209
|
htmlgraph/hooks/pretooluse.py,sha256=5mW-hMmJZlFvahDfA9yCfPrtaN6yAlWTOchirE-zZdU,27327
|
|
@@ -250,12 +252,12 @@ htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4be
|
|
|
250
252
|
htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
|
|
251
253
|
htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
|
|
252
254
|
htmlgraph/templates/orchestration-view.html,sha256=DlS7LlcjH0oO_KYILjuF1X42t8QhKLH4F85rkO54alY,10472
|
|
253
|
-
htmlgraph-0.26.
|
|
254
|
-
htmlgraph-0.26.
|
|
255
|
-
htmlgraph-0.26.
|
|
256
|
-
htmlgraph-0.26.
|
|
257
|
-
htmlgraph-0.26.
|
|
258
|
-
htmlgraph-0.26.
|
|
259
|
-
htmlgraph-0.26.
|
|
260
|
-
htmlgraph-0.26.
|
|
261
|
-
htmlgraph-0.26.
|
|
255
|
+
htmlgraph-0.26.14.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
|
|
256
|
+
htmlgraph-0.26.14.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
|
|
257
|
+
htmlgraph-0.26.14.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
|
|
258
|
+
htmlgraph-0.26.14.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
|
|
259
|
+
htmlgraph-0.26.14.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
|
|
260
|
+
htmlgraph-0.26.14.dist-info/METADATA,sha256=BOzq9w86UFDhZgsaZCIMYf8Cp4nqHf8_rWEC_-7EOF4,10237
|
|
261
|
+
htmlgraph-0.26.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
262
|
+
htmlgraph-0.26.14.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
|
|
263
|
+
htmlgraph-0.26.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/templates/AGENTS.md.template
RENAMED
|
File without changes
|
{htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/templates/CLAUDE.md.template
RENAMED
|
File without changes
|
{htmlgraph-0.26.12.data → htmlgraph-0.26.14.data}/data/htmlgraph/templates/GEMINI.md.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|