codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"""Repository for Token Repository operations.
|
|
2
|
+
|
|
3
|
+
Extracted from monolithic Database class for better maintainability.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from codeframe.core.models import (
|
|
12
|
+
CallType,
|
|
13
|
+
)
|
|
14
|
+
from codeframe.platform_store.repositories.base import BaseRepository
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from codeframe.core.models import TokenUsage
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TokenRepository(BaseRepository):
|
|
23
|
+
"""Repository for token repository operations."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_token_usage(self, token_usage: "TokenUsage") -> int:
|
|
27
|
+
"""Save a token usage record to the database.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
token_usage: TokenUsage model instance
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Database ID of the created record
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> from codeframe.core.models import TokenUsage, CallType
|
|
37
|
+
>>> usage = TokenUsage(
|
|
38
|
+
... task_id=27,
|
|
39
|
+
... agent_id="backend-001",
|
|
40
|
+
... project_id=1,
|
|
41
|
+
... model_name="claude-sonnet-4-5",
|
|
42
|
+
... input_tokens=1000,
|
|
43
|
+
... output_tokens=500,
|
|
44
|
+
... estimated_cost_usd=0.0105,
|
|
45
|
+
... call_type=CallType.TASK_EXECUTION
|
|
46
|
+
... )
|
|
47
|
+
>>> usage_id = db.save_token_usage(usage)
|
|
48
|
+
"""
|
|
49
|
+
if self._sync_lock is not None:
|
|
50
|
+
with self._sync_lock:
|
|
51
|
+
cursor = self.conn.cursor()
|
|
52
|
+
cursor.execute(
|
|
53
|
+
"""
|
|
54
|
+
INSERT INTO token_usage (
|
|
55
|
+
task_id, agent_id, project_id, model_name,
|
|
56
|
+
input_tokens, output_tokens, estimated_cost_usd,
|
|
57
|
+
actual_cost_usd, call_type, timestamp
|
|
58
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
|
+
""",
|
|
60
|
+
(
|
|
61
|
+
token_usage.task_id,
|
|
62
|
+
token_usage.agent_id,
|
|
63
|
+
token_usage.project_id,
|
|
64
|
+
token_usage.model_name,
|
|
65
|
+
token_usage.input_tokens,
|
|
66
|
+
token_usage.output_tokens,
|
|
67
|
+
token_usage.estimated_cost_usd,
|
|
68
|
+
token_usage.actual_cost_usd,
|
|
69
|
+
(
|
|
70
|
+
token_usage.call_type.value
|
|
71
|
+
if isinstance(token_usage.call_type, CallType)
|
|
72
|
+
else token_usage.call_type
|
|
73
|
+
),
|
|
74
|
+
token_usage.timestamp.isoformat(),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
self.conn.commit()
|
|
78
|
+
return cursor.lastrowid
|
|
79
|
+
else:
|
|
80
|
+
cursor = self.conn.cursor()
|
|
81
|
+
cursor.execute(
|
|
82
|
+
"""
|
|
83
|
+
INSERT INTO token_usage (
|
|
84
|
+
task_id, agent_id, project_id, model_name,
|
|
85
|
+
input_tokens, output_tokens, estimated_cost_usd,
|
|
86
|
+
actual_cost_usd, call_type, timestamp
|
|
87
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
88
|
+
""",
|
|
89
|
+
(
|
|
90
|
+
token_usage.task_id,
|
|
91
|
+
token_usage.agent_id,
|
|
92
|
+
token_usage.project_id,
|
|
93
|
+
token_usage.model_name,
|
|
94
|
+
token_usage.input_tokens,
|
|
95
|
+
token_usage.output_tokens,
|
|
96
|
+
token_usage.estimated_cost_usd,
|
|
97
|
+
token_usage.actual_cost_usd,
|
|
98
|
+
(
|
|
99
|
+
token_usage.call_type.value
|
|
100
|
+
if isinstance(token_usage.call_type, CallType)
|
|
101
|
+
else token_usage.call_type
|
|
102
|
+
),
|
|
103
|
+
token_usage.timestamp.isoformat(),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
self.conn.commit()
|
|
107
|
+
return cursor.lastrowid
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_token_usage(
|
|
112
|
+
self,
|
|
113
|
+
project_id: Optional[int] = None,
|
|
114
|
+
agent_id: Optional[str] = None,
|
|
115
|
+
start_date: Optional[datetime] = None,
|
|
116
|
+
end_date: Optional[datetime] = None,
|
|
117
|
+
) -> List[Dict[str, Any]]:
|
|
118
|
+
"""Get token usage records with optional filtering.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
project_id: Filter by project ID (optional)
|
|
122
|
+
agent_id: Filter by agent ID (optional)
|
|
123
|
+
start_date: Filter by start date (inclusive, optional)
|
|
124
|
+
end_date: Filter by end date (inclusive, optional)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of token usage records as dictionaries
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> # Get all usage for a project
|
|
131
|
+
>>> usage = db.get_token_usage(project_id=1)
|
|
132
|
+
>>>
|
|
133
|
+
>>> # Get usage for an agent in a date range
|
|
134
|
+
>>> from datetime import datetime, timedelta
|
|
135
|
+
>>> start = datetime.now() - timedelta(days=7)
|
|
136
|
+
>>> usage = db.get_token_usage(agent_id="backend-001", start_date=start)
|
|
137
|
+
"""
|
|
138
|
+
cursor = self.conn.cursor()
|
|
139
|
+
|
|
140
|
+
# Build query with filters
|
|
141
|
+
query = "SELECT * FROM token_usage WHERE 1=1"
|
|
142
|
+
params = []
|
|
143
|
+
|
|
144
|
+
if project_id is not None:
|
|
145
|
+
query += " AND project_id = ?"
|
|
146
|
+
params.append(project_id)
|
|
147
|
+
|
|
148
|
+
if agent_id is not None:
|
|
149
|
+
query += " AND agent_id = ?"
|
|
150
|
+
params.append(agent_id)
|
|
151
|
+
|
|
152
|
+
if start_date is not None:
|
|
153
|
+
query += " AND timestamp >= ?"
|
|
154
|
+
params.append(start_date.isoformat())
|
|
155
|
+
|
|
156
|
+
if end_date is not None:
|
|
157
|
+
query += " AND timestamp <= ?"
|
|
158
|
+
params.append(end_date.isoformat())
|
|
159
|
+
|
|
160
|
+
query += " ORDER BY timestamp DESC"
|
|
161
|
+
|
|
162
|
+
cursor.execute(query, params)
|
|
163
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_task_token_summary(self, task_id: int) -> Dict[str, Any]:
|
|
168
|
+
"""Get aggregated token usage summary for a single task.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
task_id: Task ID to summarize
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Dictionary with aggregated token data:
|
|
175
|
+
{
|
|
176
|
+
"task_id": int,
|
|
177
|
+
"total_input_tokens": int,
|
|
178
|
+
"total_output_tokens": int,
|
|
179
|
+
"total_tokens": int,
|
|
180
|
+
"total_cost_usd": float,
|
|
181
|
+
"call_count": int,
|
|
182
|
+
}
|
|
183
|
+
"""
|
|
184
|
+
cursor = self.conn.cursor()
|
|
185
|
+
cursor.execute(
|
|
186
|
+
"""
|
|
187
|
+
SELECT
|
|
188
|
+
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
|
|
189
|
+
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
|
|
190
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
|
191
|
+
COALESCE(SUM(estimated_cost_usd), 0.0) as total_cost_usd,
|
|
192
|
+
COUNT(*) as call_count
|
|
193
|
+
FROM token_usage
|
|
194
|
+
WHERE task_id = ?
|
|
195
|
+
""",
|
|
196
|
+
(task_id,),
|
|
197
|
+
)
|
|
198
|
+
row = cursor.fetchone()
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"task_id": task_id,
|
|
202
|
+
"total_input_tokens": row["total_input_tokens"],
|
|
203
|
+
"total_output_tokens": row["total_output_tokens"],
|
|
204
|
+
"total_tokens": row["total_tokens"],
|
|
205
|
+
"total_cost_usd": row["total_cost_usd"],
|
|
206
|
+
"call_count": row["call_count"],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
def get_batch_token_usage(
|
|
210
|
+
self,
|
|
211
|
+
task_ids: List[int],
|
|
212
|
+
start_date: Optional[datetime] = None,
|
|
213
|
+
end_date: Optional[datetime] = None,
|
|
214
|
+
) -> List[Dict[str, Any]]:
|
|
215
|
+
"""Get token usage records filtered by a list of task IDs.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
task_ids: List of task IDs to filter by
|
|
219
|
+
start_date: Optional start of date range (inclusive)
|
|
220
|
+
end_date: Optional end of date range (inclusive)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of token usage records as dictionaries
|
|
224
|
+
"""
|
|
225
|
+
if not task_ids:
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
cursor = self.conn.cursor()
|
|
229
|
+
placeholders = ",".join("?" for _ in task_ids)
|
|
230
|
+
query = f"SELECT * FROM token_usage WHERE task_id IN ({placeholders})"
|
|
231
|
+
params: list = list(task_ids)
|
|
232
|
+
|
|
233
|
+
if start_date is not None:
|
|
234
|
+
query += " AND timestamp >= ?"
|
|
235
|
+
params.append(start_date.isoformat())
|
|
236
|
+
|
|
237
|
+
if end_date is not None:
|
|
238
|
+
query += " AND timestamp <= ?"
|
|
239
|
+
params.append(end_date.isoformat())
|
|
240
|
+
|
|
241
|
+
query += " ORDER BY timestamp DESC"
|
|
242
|
+
|
|
243
|
+
cursor.execute(query, params)
|
|
244
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
245
|
+
|
|
246
|
+
def get_workspace_token_usage(
|
|
247
|
+
self,
|
|
248
|
+
start_date: Optional[datetime] = None,
|
|
249
|
+
end_date: Optional[datetime] = None,
|
|
250
|
+
) -> List[Dict[str, Any]]:
|
|
251
|
+
"""Get all token usage records across the workspace.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
start_date: Optional start of date range (inclusive)
|
|
255
|
+
end_date: Optional end of date range (inclusive)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of token usage records as dictionaries
|
|
259
|
+
"""
|
|
260
|
+
cursor = self.conn.cursor()
|
|
261
|
+
query = "SELECT * FROM token_usage WHERE 1=1"
|
|
262
|
+
params: list = []
|
|
263
|
+
|
|
264
|
+
if start_date is not None:
|
|
265
|
+
query += " AND timestamp >= ?"
|
|
266
|
+
params.append(start_date.isoformat())
|
|
267
|
+
|
|
268
|
+
if end_date is not None:
|
|
269
|
+
query += " AND timestamp <= ?"
|
|
270
|
+
params.append(end_date.isoformat())
|
|
271
|
+
|
|
272
|
+
query += " ORDER BY timestamp DESC"
|
|
273
|
+
|
|
274
|
+
cursor.execute(query, params)
|
|
275
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
276
|
+
|
|
277
|
+
def get_costs_summary(self, days: int) -> Dict[str, Any]:
|
|
278
|
+
"""Aggregate token_usage costs into daily buckets for analytics.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
days: Number of trailing days to include in the summary.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dictionary with keys:
|
|
285
|
+
total_spend_usd: float — sum of estimated_cost_usd in window
|
|
286
|
+
total_tasks: int — distinct task_id count (excludes NULL)
|
|
287
|
+
avg_cost_per_task: float — total_spend_usd / total_tasks (0 if no tasks)
|
|
288
|
+
daily: list of {"date": "YYYY-MM-DD", "cost_usd": float}
|
|
289
|
+
— one entry per day in the window, oldest first,
|
|
290
|
+
zero-filled for days with no spend.
|
|
291
|
+
"""
|
|
292
|
+
if days <= 0:
|
|
293
|
+
raise ValueError("days must be a positive integer")
|
|
294
|
+
|
|
295
|
+
now_utc = datetime.now(timezone.utc)
|
|
296
|
+
# Inclusive window starting at midnight UTC, `days` calendar days back.
|
|
297
|
+
# Use a space-separated, offset-free format so lexicographic comparison
|
|
298
|
+
# works against both `CURRENT_TIMESTAMP` defaults ("YYYY-MM-DD HH:MM:SS")
|
|
299
|
+
# and Python `.isoformat()` outputs ("YYYY-MM-DDTHH:MM:SS+00:00").
|
|
300
|
+
end_date = now_utc.date()
|
|
301
|
+
start_date = end_date - timedelta(days=days - 1)
|
|
302
|
+
start_iso = start_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
303
|
+
# Exclusive upper bound = midnight after today, so the daily chart and
|
|
304
|
+
# the KPI cards always cover the same set of rows even if some records
|
|
305
|
+
# are future-dated (clock skew, bad seed data).
|
|
306
|
+
end_iso = (end_date + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S")
|
|
307
|
+
|
|
308
|
+
cursor = self.conn.cursor()
|
|
309
|
+
|
|
310
|
+
# Totals over the window. total_spend includes NULL-task records so it
|
|
311
|
+
# matches the chart; total_tasks only counts records linked to a task.
|
|
312
|
+
cursor.execute(
|
|
313
|
+
"""
|
|
314
|
+
SELECT
|
|
315
|
+
COALESCE(SUM(estimated_cost_usd), 0.0) AS total_spend,
|
|
316
|
+
COUNT(DISTINCT CASE WHEN task_id IS NOT NULL THEN task_id END) AS task_count
|
|
317
|
+
FROM token_usage
|
|
318
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
319
|
+
""",
|
|
320
|
+
(start_iso, end_iso),
|
|
321
|
+
)
|
|
322
|
+
totals = cursor.fetchone()
|
|
323
|
+
total_spend = float(totals["total_spend"] or 0.0)
|
|
324
|
+
total_tasks = int(totals["task_count"] or 0)
|
|
325
|
+
avg_cost = (total_spend / total_tasks) if total_tasks > 0 else 0.0
|
|
326
|
+
|
|
327
|
+
# Daily aggregation — group by calendar date in UTC
|
|
328
|
+
cursor.execute(
|
|
329
|
+
"""
|
|
330
|
+
SELECT
|
|
331
|
+
DATE(timestamp) AS day,
|
|
332
|
+
COALESCE(SUM(estimated_cost_usd), 0.0) AS cost
|
|
333
|
+
FROM token_usage
|
|
334
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
335
|
+
GROUP BY DATE(timestamp)
|
|
336
|
+
""",
|
|
337
|
+
(start_iso, end_iso),
|
|
338
|
+
)
|
|
339
|
+
by_day: Dict[str, float] = {row["day"]: float(row["cost"] or 0.0) for row in cursor.fetchall()}
|
|
340
|
+
|
|
341
|
+
daily: List[Dict[str, Any]] = []
|
|
342
|
+
for offset in range(days):
|
|
343
|
+
d = start_date + timedelta(days=offset)
|
|
344
|
+
iso = d.isoformat()
|
|
345
|
+
daily.append({"date": iso, "cost_usd": by_day.get(iso, 0.0)})
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
"total_spend_usd": total_spend,
|
|
349
|
+
"total_tasks": total_tasks,
|
|
350
|
+
"avg_cost_per_task": avg_cost,
|
|
351
|
+
"daily": daily,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
def _window_iso_bounds(self, days: int) -> tuple[str, str]:
|
|
355
|
+
"""Return inclusive start / exclusive end ISO strings for a `days` window.
|
|
356
|
+
|
|
357
|
+
Mirrors get_costs_summary's bounds so the per-task and per-agent
|
|
358
|
+
aggregations cover the same rows. Space-separated, offset-free format
|
|
359
|
+
works against both ``CURRENT_TIMESTAMP`` defaults and ``.isoformat()``.
|
|
360
|
+
"""
|
|
361
|
+
if days <= 0:
|
|
362
|
+
raise ValueError("days must be a positive integer")
|
|
363
|
+
end_date = datetime.now(timezone.utc).date()
|
|
364
|
+
start_date = end_date - timedelta(days=days - 1)
|
|
365
|
+
start_iso = start_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
366
|
+
end_iso = (end_date + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S")
|
|
367
|
+
return start_iso, end_iso
|
|
368
|
+
|
|
369
|
+
def get_top_tasks_by_cost(
|
|
370
|
+
self,
|
|
371
|
+
days: int,
|
|
372
|
+
limit: int = 10,
|
|
373
|
+
) -> List[Dict[str, Any]]:
|
|
374
|
+
"""Aggregate spend per task and return the top N by cost.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
days: Trailing window in days.
|
|
378
|
+
limit: Maximum number of tasks to return.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
List of dicts, sorted by total_cost_usd DESC:
|
|
382
|
+
{
|
|
383
|
+
"task_id": <native value from token_usage.task_id>,
|
|
384
|
+
"agent_id": str,
|
|
385
|
+
"input_tokens": int,
|
|
386
|
+
"output_tokens": int,
|
|
387
|
+
"total_cost_usd": float,
|
|
388
|
+
}
|
|
389
|
+
Excludes rows where task_id IS NULL. The reported ``agent_id`` is
|
|
390
|
+
the agent that made the most calls for that task (ties broken
|
|
391
|
+
arbitrarily). ``task_id`` is returned as stored — SQLite preserves
|
|
392
|
+
the inserted type, so v2 UUID strings come back as strings and v1
|
|
393
|
+
integers come back as integers.
|
|
394
|
+
"""
|
|
395
|
+
if limit <= 0:
|
|
396
|
+
raise ValueError("limit must be a positive integer")
|
|
397
|
+
start_iso, end_iso = self._window_iso_bounds(days)
|
|
398
|
+
|
|
399
|
+
cursor = self.conn.cursor()
|
|
400
|
+
cursor.execute(
|
|
401
|
+
"""
|
|
402
|
+
SELECT
|
|
403
|
+
task_id,
|
|
404
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
405
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
406
|
+
COALESCE(SUM(estimated_cost_usd), 0.0) AS total_cost_usd
|
|
407
|
+
FROM token_usage
|
|
408
|
+
WHERE task_id IS NOT NULL
|
|
409
|
+
AND timestamp >= ?
|
|
410
|
+
AND timestamp < ?
|
|
411
|
+
GROUP BY task_id
|
|
412
|
+
ORDER BY total_cost_usd DESC
|
|
413
|
+
LIMIT ?
|
|
414
|
+
""",
|
|
415
|
+
(start_iso, end_iso, limit),
|
|
416
|
+
)
|
|
417
|
+
rows = cursor.fetchall()
|
|
418
|
+
|
|
419
|
+
# TODO(perf): the dominant-agent lookup is N+1 against the limit.
|
|
420
|
+
# Acceptable at limit=10 (analytics view) and even limit=1000 (badge
|
|
421
|
+
# map for a board). Fold into a single CTE if the cap grows further.
|
|
422
|
+
result: List[Dict[str, Any]] = []
|
|
423
|
+
for row in rows:
|
|
424
|
+
task_id = row["task_id"]
|
|
425
|
+
# Find the most-used agent for this task in the same window.
|
|
426
|
+
cursor.execute(
|
|
427
|
+
"""
|
|
428
|
+
SELECT agent_id, COUNT(*) AS calls
|
|
429
|
+
FROM token_usage
|
|
430
|
+
WHERE task_id = ?
|
|
431
|
+
AND timestamp >= ?
|
|
432
|
+
AND timestamp < ?
|
|
433
|
+
GROUP BY agent_id
|
|
434
|
+
ORDER BY calls DESC
|
|
435
|
+
LIMIT 1
|
|
436
|
+
""",
|
|
437
|
+
(task_id, start_iso, end_iso),
|
|
438
|
+
)
|
|
439
|
+
agent_row = cursor.fetchone()
|
|
440
|
+
agent_id = agent_row["agent_id"] if agent_row else ""
|
|
441
|
+
|
|
442
|
+
result.append({
|
|
443
|
+
"task_id": task_id,
|
|
444
|
+
"agent_id": agent_id,
|
|
445
|
+
"input_tokens": int(row["input_tokens"] or 0),
|
|
446
|
+
"output_tokens": int(row["output_tokens"] or 0),
|
|
447
|
+
"total_cost_usd": float(row["total_cost_usd"] or 0.0),
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
def get_costs_by_agent(self, days: int) -> Dict[str, Any]:
|
|
453
|
+
"""Aggregate spend per agent over a trailing `days` window.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
days: Trailing window in days.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
{
|
|
460
|
+
"by_agent": [
|
|
461
|
+
{
|
|
462
|
+
"agent_id": str,
|
|
463
|
+
"input_tokens": int,
|
|
464
|
+
"output_tokens": int,
|
|
465
|
+
"total_cost_usd": float,
|
|
466
|
+
"call_count": int,
|
|
467
|
+
},
|
|
468
|
+
...
|
|
469
|
+
],
|
|
470
|
+
"total_input_tokens": int,
|
|
471
|
+
"total_output_tokens": int,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
Includes records with NULL ``task_id`` — calls without a task still
|
|
475
|
+
attribute to an agent. Sorted by total_cost_usd DESC.
|
|
476
|
+
"""
|
|
477
|
+
start_iso, end_iso = self._window_iso_bounds(days)
|
|
478
|
+
|
|
479
|
+
cursor = self.conn.cursor()
|
|
480
|
+
cursor.execute(
|
|
481
|
+
"""
|
|
482
|
+
SELECT
|
|
483
|
+
agent_id,
|
|
484
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
485
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
486
|
+
COALESCE(SUM(estimated_cost_usd), 0.0) AS total_cost_usd,
|
|
487
|
+
COUNT(*) AS call_count
|
|
488
|
+
FROM token_usage
|
|
489
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
490
|
+
GROUP BY agent_id
|
|
491
|
+
ORDER BY total_cost_usd DESC
|
|
492
|
+
""",
|
|
493
|
+
(start_iso, end_iso),
|
|
494
|
+
)
|
|
495
|
+
rows = cursor.fetchall()
|
|
496
|
+
|
|
497
|
+
by_agent: List[Dict[str, Any]] = []
|
|
498
|
+
total_input = 0
|
|
499
|
+
total_output = 0
|
|
500
|
+
for row in rows:
|
|
501
|
+
inp = int(row["input_tokens"] or 0)
|
|
502
|
+
out = int(row["output_tokens"] or 0)
|
|
503
|
+
by_agent.append({
|
|
504
|
+
"agent_id": row["agent_id"],
|
|
505
|
+
"input_tokens": inp,
|
|
506
|
+
"output_tokens": out,
|
|
507
|
+
"total_cost_usd": float(row["total_cost_usd"] or 0.0),
|
|
508
|
+
"call_count": int(row["call_count"] or 0),
|
|
509
|
+
})
|
|
510
|
+
total_input += inp
|
|
511
|
+
total_output += out
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
"by_agent": by_agent,
|
|
515
|
+
"total_input_tokens": total_input,
|
|
516
|
+
"total_output_tokens": total_output,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
def get_project_costs_aggregate(self, project_id: int) -> Dict[str, Any]:
|
|
520
|
+
"""Get aggregated cost statistics for a project.
|
|
521
|
+
|
|
522
|
+
This is a convenience method that aggregates costs by agent and model
|
|
523
|
+
in a single database query for better performance.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
project_id: Project ID
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Dictionary with aggregated costs:
|
|
530
|
+
{
|
|
531
|
+
"total_cost": float,
|
|
532
|
+
"total_tokens": int,
|
|
533
|
+
"by_agent": {...},
|
|
534
|
+
"by_model": {...}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
>>> stats = db.get_project_costs_aggregate(project_id=1)
|
|
539
|
+
>>> print(f"Total: ${stats['total_cost']:.2f}")
|
|
540
|
+
"""
|
|
541
|
+
cursor = self.conn.cursor()
|
|
542
|
+
|
|
543
|
+
# Get overall totals
|
|
544
|
+
cursor.execute(
|
|
545
|
+
"""
|
|
546
|
+
SELECT
|
|
547
|
+
COALESCE(SUM(estimated_cost_usd), 0) as total_cost,
|
|
548
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
|
549
|
+
COUNT(*) as total_calls
|
|
550
|
+
FROM token_usage
|
|
551
|
+
WHERE project_id = ?
|
|
552
|
+
""",
|
|
553
|
+
(project_id,),
|
|
554
|
+
)
|
|
555
|
+
totals = cursor.fetchone()
|
|
556
|
+
|
|
557
|
+
# Get breakdown by agent
|
|
558
|
+
cursor.execute(
|
|
559
|
+
"""
|
|
560
|
+
SELECT
|
|
561
|
+
agent_id,
|
|
562
|
+
SUM(estimated_cost_usd) as cost,
|
|
563
|
+
SUM(input_tokens + output_tokens) as tokens,
|
|
564
|
+
COUNT(*) as calls
|
|
565
|
+
FROM token_usage
|
|
566
|
+
WHERE project_id = ?
|
|
567
|
+
GROUP BY agent_id
|
|
568
|
+
ORDER BY cost DESC
|
|
569
|
+
""",
|
|
570
|
+
(project_id,),
|
|
571
|
+
)
|
|
572
|
+
by_agent = [dict(row) for row in cursor.fetchall()]
|
|
573
|
+
|
|
574
|
+
# Get breakdown by model
|
|
575
|
+
cursor.execute(
|
|
576
|
+
"""
|
|
577
|
+
SELECT
|
|
578
|
+
model_name,
|
|
579
|
+
SUM(estimated_cost_usd) as cost,
|
|
580
|
+
SUM(input_tokens + output_tokens) as tokens,
|
|
581
|
+
COUNT(*) as calls
|
|
582
|
+
FROM token_usage
|
|
583
|
+
WHERE project_id = ?
|
|
584
|
+
GROUP BY model_name
|
|
585
|
+
ORDER BY cost DESC
|
|
586
|
+
""",
|
|
587
|
+
(project_id,),
|
|
588
|
+
)
|
|
589
|
+
by_model = [dict(row) for row in cursor.fetchall()]
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
"total_cost": totals["total_cost"],
|
|
593
|
+
"total_tokens": totals["total_tokens"],
|
|
594
|
+
"total_calls": totals["total_calls"],
|
|
595
|
+
"by_agent": by_agent,
|
|
596
|
+
"by_model": by_model,
|
|
597
|
+
}
|
|
598
|
+
|