htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CostAnalyzer for HtmlGraph - Token cost tracking from HtmlGraph events.
|
|
3
|
+
|
|
4
|
+
Reads HtmlGraph events from .htmlgraph/ directories and calculates costs
|
|
5
|
+
based on token usage and standard Claude pricing models.
|
|
6
|
+
|
|
7
|
+
Design:
|
|
8
|
+
- Reads spike HTML files and session event files
|
|
9
|
+
- Extracts token usage from event metadata
|
|
10
|
+
- Calculates costs per event using standard Claude pricing
|
|
11
|
+
- Groups costs by: subagent_type, tool_name, event_type
|
|
12
|
+
- Calculates aggregates: total cost, cost per delegation, cost per spike
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Standard Claude pricing (tokens per 1M tokens)
|
|
26
|
+
CLAUDE_PRICING: dict[str, dict[str, float]] = {
|
|
27
|
+
"claude-3.5-sonnet": {"input": 3.0, "output": 15.0}, # $3/$15 per 1M
|
|
28
|
+
"claude-3-opus": {"input": 15.0, "output": 75.0}, # $15/$75 per 1M
|
|
29
|
+
"claude-3-haiku": {"input": 0.25, "output": 1.25}, # $0.25/$1.25 per 1M
|
|
30
|
+
"claude-3-sonnet": {"input": 3.0, "output": 15.0}, # $3/$15 per 1M (alias)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Default model if not specified
|
|
34
|
+
DEFAULT_MODEL = "claude-3.5-sonnet"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TokenCostBreakdown:
|
|
39
|
+
"""Token cost breakdown for an event."""
|
|
40
|
+
|
|
41
|
+
event_id: str
|
|
42
|
+
event_type: str
|
|
43
|
+
timestamp: str
|
|
44
|
+
input_tokens: int = 0
|
|
45
|
+
output_tokens: int = 0
|
|
46
|
+
input_cost: float = 0.0
|
|
47
|
+
output_cost: float = 0.0
|
|
48
|
+
total_cost: float = 0.0
|
|
49
|
+
model: str = DEFAULT_MODEL
|
|
50
|
+
subagent_type: str | None = None
|
|
51
|
+
tool_name: str | None = None
|
|
52
|
+
success: bool = True
|
|
53
|
+
notes: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class CostAnalyzerResult:
|
|
58
|
+
"""Result from cost analysis."""
|
|
59
|
+
|
|
60
|
+
total_events: int = 0
|
|
61
|
+
total_input_tokens: int = 0
|
|
62
|
+
total_output_tokens: int = 0
|
|
63
|
+
total_cost: float = 0.0
|
|
64
|
+
cost_by_model: dict[str, float] = field(default_factory=dict)
|
|
65
|
+
cost_by_subagent: dict[str, float] = field(default_factory=dict)
|
|
66
|
+
cost_by_tool: dict[str, float] = field(default_factory=dict)
|
|
67
|
+
cost_by_event_type: dict[str, float] = field(default_factory=dict)
|
|
68
|
+
event_breakdowns: list[TokenCostBreakdown] = field(default_factory=list)
|
|
69
|
+
direct_execution_cost: float = 0.0
|
|
70
|
+
estimated_savings: float = 0.0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CostAnalyzer:
|
|
74
|
+
"""Analyze token costs from HtmlGraph events."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, htmlgraph_dir: Path | None = None) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Initialize CostAnalyzer.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
htmlgraph_dir: Path to .htmlgraph directory. Defaults to ./.htmlgraph
|
|
82
|
+
"""
|
|
83
|
+
if htmlgraph_dir is None:
|
|
84
|
+
htmlgraph_dir = Path.cwd() / ".htmlgraph"
|
|
85
|
+
self.htmlgraph_dir = Path(htmlgraph_dir)
|
|
86
|
+
self.events: list[dict[str, Any]] = []
|
|
87
|
+
self.spike_data: dict[str, dict[str, Any]] = {}
|
|
88
|
+
self.result = CostAnalyzerResult()
|
|
89
|
+
|
|
90
|
+
def analyze_events(self) -> CostAnalyzerResult:
|
|
91
|
+
"""
|
|
92
|
+
Analyze events and return cost breakdown.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
CostAnalyzerResult with complete cost analysis
|
|
96
|
+
"""
|
|
97
|
+
self.result = CostAnalyzerResult()
|
|
98
|
+
|
|
99
|
+
# Load all events
|
|
100
|
+
self._load_events()
|
|
101
|
+
|
|
102
|
+
if not self.events:
|
|
103
|
+
logger.warning("No events found in .htmlgraph directory")
|
|
104
|
+
return self.result
|
|
105
|
+
|
|
106
|
+
# Process each event
|
|
107
|
+
cost_breakdowns: list[TokenCostBreakdown] = []
|
|
108
|
+
|
|
109
|
+
for event in self.events:
|
|
110
|
+
breakdown = self._calculate_event_cost(event)
|
|
111
|
+
if breakdown:
|
|
112
|
+
cost_breakdowns.append(breakdown)
|
|
113
|
+
|
|
114
|
+
# Update result with event data
|
|
115
|
+
self.result.event_breakdowns = cost_breakdowns
|
|
116
|
+
self.result.total_events = len(cost_breakdowns)
|
|
117
|
+
|
|
118
|
+
# Aggregate costs
|
|
119
|
+
self._aggregate_costs(cost_breakdowns)
|
|
120
|
+
|
|
121
|
+
# Calculate savings
|
|
122
|
+
self.result.estimated_savings = max(
|
|
123
|
+
0, self.result.direct_execution_cost - self.result.total_cost
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return self.result
|
|
127
|
+
|
|
128
|
+
def _load_events(self) -> None:
|
|
129
|
+
"""Load all events from .htmlgraph/events and .htmlgraph/spikes directories."""
|
|
130
|
+
self.events = []
|
|
131
|
+
|
|
132
|
+
# Load from events directory (JSONL files)
|
|
133
|
+
events_dir = self.htmlgraph_dir / "events"
|
|
134
|
+
if events_dir.exists():
|
|
135
|
+
for jsonl_file in events_dir.glob("*.jsonl"):
|
|
136
|
+
try:
|
|
137
|
+
with open(jsonl_file) as f:
|
|
138
|
+
for line in f:
|
|
139
|
+
if line.strip():
|
|
140
|
+
try:
|
|
141
|
+
event = json.loads(line)
|
|
142
|
+
self.events.append(event)
|
|
143
|
+
except json.JSONDecodeError as e:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"Failed to parse JSON line in {jsonl_file}: {e}"
|
|
146
|
+
)
|
|
147
|
+
except OSError as e:
|
|
148
|
+
logger.warning(f"Failed to read {jsonl_file}: {e}")
|
|
149
|
+
|
|
150
|
+
logger.info(f"Loaded {len(self.events)} events from .htmlgraph/events")
|
|
151
|
+
|
|
152
|
+
def _calculate_event_cost(self, event: dict[str, Any]) -> TokenCostBreakdown | None:
|
|
153
|
+
"""
|
|
154
|
+
Calculate cost for a single event.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
event: Event dictionary
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
TokenCostBreakdown or None if no token info
|
|
161
|
+
"""
|
|
162
|
+
event_id = event.get("event_id", "unknown")
|
|
163
|
+
event_type = event.get("tool", "unknown")
|
|
164
|
+
timestamp = event.get("timestamp", "")
|
|
165
|
+
success = event.get("success", True)
|
|
166
|
+
|
|
167
|
+
# Extract token counts from various possible locations
|
|
168
|
+
input_tokens = self._extract_tokens(event, "input")
|
|
169
|
+
output_tokens = self._extract_tokens(event, "output")
|
|
170
|
+
|
|
171
|
+
# If no explicit token counts, estimate from text length
|
|
172
|
+
if input_tokens == 0 and output_tokens == 0:
|
|
173
|
+
input_tokens, output_tokens = self._estimate_tokens_from_event(event)
|
|
174
|
+
|
|
175
|
+
# Determine model
|
|
176
|
+
model = event.get("model", DEFAULT_MODEL)
|
|
177
|
+
if not self._is_valid_model(model):
|
|
178
|
+
model = DEFAULT_MODEL
|
|
179
|
+
|
|
180
|
+
# Get subagent type if available
|
|
181
|
+
subagent_type = event.get("subagent_type")
|
|
182
|
+
|
|
183
|
+
# Get tool name
|
|
184
|
+
tool_name = event.get("tool")
|
|
185
|
+
|
|
186
|
+
# Calculate cost
|
|
187
|
+
pricing = CLAUDE_PRICING.get(model) or CLAUDE_PRICING[DEFAULT_MODEL]
|
|
188
|
+
input_cost = (input_tokens / 1_000_000) * pricing["input"]
|
|
189
|
+
output_cost = (output_tokens / 1_000_000) * pricing["output"]
|
|
190
|
+
total_cost = input_cost + output_cost
|
|
191
|
+
|
|
192
|
+
breakdown = TokenCostBreakdown(
|
|
193
|
+
event_id=event_id,
|
|
194
|
+
event_type=event_type,
|
|
195
|
+
timestamp=timestamp,
|
|
196
|
+
input_tokens=input_tokens,
|
|
197
|
+
output_tokens=output_tokens,
|
|
198
|
+
input_cost=input_cost,
|
|
199
|
+
output_cost=output_cost,
|
|
200
|
+
total_cost=total_cost,
|
|
201
|
+
model=model,
|
|
202
|
+
subagent_type=subagent_type,
|
|
203
|
+
tool_name=tool_name,
|
|
204
|
+
success=success,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return breakdown
|
|
208
|
+
|
|
209
|
+
def _extract_tokens(self, event: dict[str, Any], token_type: str) -> int:
|
|
210
|
+
"""
|
|
211
|
+
Extract token counts from event.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
event: Event dictionary
|
|
215
|
+
token_type: "input" or "output"
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Token count or 0 if not found
|
|
219
|
+
"""
|
|
220
|
+
# Check direct fields
|
|
221
|
+
if f"{token_type}_tokens" in event:
|
|
222
|
+
try:
|
|
223
|
+
return int(event[f"{token_type}_tokens"])
|
|
224
|
+
except (ValueError, TypeError):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# Check metadata
|
|
228
|
+
if "metadata" in event:
|
|
229
|
+
meta = event["metadata"]
|
|
230
|
+
if isinstance(meta, dict):
|
|
231
|
+
if f"{token_type}_tokens" in meta:
|
|
232
|
+
try:
|
|
233
|
+
return int(meta[f"{token_type}_tokens"])
|
|
234
|
+
except (ValueError, TypeError):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
# Check payload
|
|
238
|
+
if "payload" in event:
|
|
239
|
+
payload = event["payload"]
|
|
240
|
+
if isinstance(payload, dict):
|
|
241
|
+
if f"{token_type}_tokens" in payload:
|
|
242
|
+
try:
|
|
243
|
+
return int(payload[f"{token_type}_tokens"])
|
|
244
|
+
except (ValueError, TypeError):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
def _estimate_tokens_from_event(self, event: dict[str, Any]) -> tuple[int, int]:
|
|
250
|
+
"""
|
|
251
|
+
Estimate tokens from event text content.
|
|
252
|
+
|
|
253
|
+
Rough estimate: ~4 characters = 1 token (conservative)
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
event: Event dictionary
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Tuple of (input_tokens, output_tokens) estimates
|
|
260
|
+
"""
|
|
261
|
+
input_estimate = 0
|
|
262
|
+
output_estimate = 0
|
|
263
|
+
|
|
264
|
+
# Estimate from summary
|
|
265
|
+
summary = event.get("summary", "")
|
|
266
|
+
if summary:
|
|
267
|
+
input_estimate += len(summary) // 4
|
|
268
|
+
|
|
269
|
+
# Estimate from findings or results
|
|
270
|
+
for field_name in ["findings", "result", "output", "response", "payload"]:
|
|
271
|
+
if field_name in event:
|
|
272
|
+
content = event[field_name]
|
|
273
|
+
if isinstance(content, str):
|
|
274
|
+
output_estimate += len(content) // 4
|
|
275
|
+
elif isinstance(content, dict):
|
|
276
|
+
# Rough estimate for dict size
|
|
277
|
+
output_estimate += len(json.dumps(content)) // 4
|
|
278
|
+
|
|
279
|
+
return input_estimate, output_estimate
|
|
280
|
+
|
|
281
|
+
def _is_valid_model(self, model: str) -> bool:
|
|
282
|
+
"""Check if model is in pricing table."""
|
|
283
|
+
return model in CLAUDE_PRICING
|
|
284
|
+
|
|
285
|
+
def _aggregate_costs(self, breakdowns: list[TokenCostBreakdown]) -> None:
|
|
286
|
+
"""Aggregate costs by various dimensions."""
|
|
287
|
+
cost_by_model: dict[str, float] = defaultdict(float)
|
|
288
|
+
cost_by_subagent: dict[str, float] = defaultdict(float)
|
|
289
|
+
cost_by_tool: dict[str, float] = defaultdict(float)
|
|
290
|
+
cost_by_event_type: dict[str, float] = defaultdict(float)
|
|
291
|
+
|
|
292
|
+
total_input = 0
|
|
293
|
+
total_output = 0
|
|
294
|
+
total_cost = 0.0
|
|
295
|
+
|
|
296
|
+
for breakdown in breakdowns:
|
|
297
|
+
# Aggregate by model
|
|
298
|
+
cost_by_model[breakdown.model] += breakdown.total_cost
|
|
299
|
+
|
|
300
|
+
# Aggregate by subagent
|
|
301
|
+
if breakdown.subagent_type:
|
|
302
|
+
cost_by_subagent[breakdown.subagent_type] += breakdown.total_cost
|
|
303
|
+
|
|
304
|
+
# Aggregate by tool
|
|
305
|
+
if breakdown.tool_name:
|
|
306
|
+
cost_by_tool[breakdown.tool_name] += breakdown.total_cost
|
|
307
|
+
|
|
308
|
+
# Aggregate by event type
|
|
309
|
+
cost_by_event_type[breakdown.event_type] += breakdown.total_cost
|
|
310
|
+
|
|
311
|
+
# Totals
|
|
312
|
+
total_input += breakdown.input_tokens
|
|
313
|
+
total_output += breakdown.output_tokens
|
|
314
|
+
total_cost += breakdown.total_cost
|
|
315
|
+
|
|
316
|
+
self.result.cost_by_model = dict(cost_by_model)
|
|
317
|
+
self.result.cost_by_subagent = dict(cost_by_subagent)
|
|
318
|
+
self.result.cost_by_tool = dict(cost_by_tool)
|
|
319
|
+
self.result.cost_by_event_type = dict(cost_by_event_type)
|
|
320
|
+
self.result.total_input_tokens = total_input
|
|
321
|
+
self.result.total_output_tokens = total_output
|
|
322
|
+
self.result.total_cost = total_cost
|
|
323
|
+
|
|
324
|
+
# Estimate direct execution cost (assume 50% more without delegation)
|
|
325
|
+
self.result.direct_execution_cost = total_cost * 1.5
|
|
326
|
+
|
|
327
|
+
def get_cost_by_subagent(self) -> dict[str, float]:
|
|
328
|
+
"""
|
|
329
|
+
Get total cost grouped by subagent type.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dictionary mapping subagent_type to total cost
|
|
333
|
+
"""
|
|
334
|
+
return self.result.cost_by_subagent
|
|
335
|
+
|
|
336
|
+
def get_cost_by_tool(self) -> dict[str, float]:
|
|
337
|
+
"""
|
|
338
|
+
Get total cost grouped by tool name.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Dictionary mapping tool_name to total cost
|
|
342
|
+
"""
|
|
343
|
+
return self.result.cost_by_tool
|
|
344
|
+
|
|
345
|
+
def get_delegation_costs(self) -> list[dict[str, Any]]:
|
|
346
|
+
"""
|
|
347
|
+
Get cost breakdown per delegation.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of dicts with delegation costs
|
|
351
|
+
"""
|
|
352
|
+
delegations: dict[str, dict[str, Any]] = defaultdict(
|
|
353
|
+
lambda: {
|
|
354
|
+
"count": 0,
|
|
355
|
+
"total_cost": 0.0,
|
|
356
|
+
"input_tokens": 0,
|
|
357
|
+
"output_tokens": 0,
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
for breakdown in self.result.event_breakdowns:
|
|
362
|
+
if breakdown.subagent_type:
|
|
363
|
+
key = breakdown.subagent_type
|
|
364
|
+
delegations[key]["count"] += 1
|
|
365
|
+
delegations[key]["total_cost"] += breakdown.total_cost
|
|
366
|
+
delegations[key]["input_tokens"] += breakdown.input_tokens
|
|
367
|
+
delegations[key]["output_tokens"] += breakdown.output_tokens
|
|
368
|
+
|
|
369
|
+
# Convert to list and calculate averages
|
|
370
|
+
result = []
|
|
371
|
+
for subagent_type, data in delegations.items():
|
|
372
|
+
result.append(
|
|
373
|
+
{
|
|
374
|
+
"subagent_type": subagent_type,
|
|
375
|
+
"count": data["count"],
|
|
376
|
+
"total_cost": round(data["total_cost"], 4),
|
|
377
|
+
"average_cost": round(data["total_cost"] / data["count"], 4),
|
|
378
|
+
"input_tokens": data["input_tokens"],
|
|
379
|
+
"output_tokens": data["output_tokens"],
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return sorted(result, key=lambda x: x["total_cost"], reverse=True)
|
|
384
|
+
|
|
385
|
+
def estimate_direct_execution_cost(self, delegation_cost: float) -> float:
|
|
386
|
+
"""
|
|
387
|
+
Estimate what cost would be without delegation optimization.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
delegation_cost: Cost with delegations
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Estimated cost with direct execution (roughly 50% more)
|
|
394
|
+
"""
|
|
395
|
+
return delegation_cost * 1.5
|
|
396
|
+
|
|
397
|
+
def get_cost_summary(self) -> dict[str, Any]:
|
|
398
|
+
"""
|
|
399
|
+
Get a summary of all costs.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Dictionary with cost summary
|
|
403
|
+
"""
|
|
404
|
+
return {
|
|
405
|
+
"total_events": self.result.total_events,
|
|
406
|
+
"total_input_tokens": self.result.total_input_tokens,
|
|
407
|
+
"total_output_tokens": self.result.total_output_tokens,
|
|
408
|
+
"total_cost": round(self.result.total_cost, 4),
|
|
409
|
+
"direct_execution_cost_estimate": round(
|
|
410
|
+
self.result.direct_execution_cost, 4
|
|
411
|
+
),
|
|
412
|
+
"estimated_savings": round(self.result.estimated_savings, 4),
|
|
413
|
+
"cost_by_model": {
|
|
414
|
+
k: round(v, 4) for k, v in self.result.cost_by_model.items()
|
|
415
|
+
},
|
|
416
|
+
"cost_by_subagent": {
|
|
417
|
+
k: round(v, 4) for k, v in self.result.cost_by_subagent.items()
|
|
418
|
+
},
|
|
419
|
+
"cost_by_tool": {
|
|
420
|
+
k: round(v, 4) for k, v in self.result.cost_by_tool.items()
|
|
421
|
+
},
|
|
422
|
+
"cost_by_event_type": {
|
|
423
|
+
k: round(v, 4) for k, v in self.result.cost_by_event_type.items()
|
|
424
|
+
},
|
|
425
|
+
"delegation_costs": self.get_delegation_costs(),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
def export_to_json(self, output_path: Path) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Export cost analysis to JSON file.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
output_path: Path to write JSON file
|
|
434
|
+
"""
|
|
435
|
+
summary = self.get_cost_summary()
|
|
436
|
+
with open(output_path, "w") as f:
|
|
437
|
+
json.dump(summary, f, indent=2)
|
|
438
|
+
logger.info(f"Exported cost analysis to {output_path}")
|