UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

163 lines (161 loc) 5.79 kB
import Database from 'better-sqlite3'; import path from 'path'; import { homedir } from 'os'; import { existsSync, mkdirSync } from 'fs'; export class LocalAnalyticsService { db; constructor() { const capsuleDir = path.join(homedir(), '.capsule'); if (!existsSync(capsuleDir)) { mkdirSync(capsuleDir, { recursive: true }); } const dbPath = path.join(capsuleDir, 'analytics.db'); this.db = new Database(dbPath); this.initializeDatabase(); } initializeDatabase() { this.db.exec(` CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL, properties TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, session_id TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp); CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS model_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, model TEXT NOT NULL, provider TEXT NOT NULL, prompt_tokens INTEGER NOT NULL, completion_tokens INTEGER NOT NULL, total_tokens INTEGER NOT NULL, cost REAL NOT NULL, duration INTEGER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, session_id TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_model_usage_timestamp ON model_usage(timestamp); CREATE INDEX IF NOT EXISTS idx_model_usage_model ON model_usage(model); `); this.db.exec(` CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, started_at DATETIME DEFAULT CURRENT_TIMESTAMP, ended_at DATETIME, total_queries INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, total_cost REAL DEFAULT 0 ); `); } async track(event) { const stmt = this.db.prepare(` INSERT INTO events (event, properties, timestamp, session_id) VALUES (?, ?, ?, ?) `); stmt.run(event.event, JSON.stringify(event.properties || {}), event.timestamp.toISOString(), event.sessionId); if (event.event === 'model_completion' && event.properties) { this.trackModelUsage(event); } } trackModelUsage(event) { const props = event.properties; const stmt = this.db.prepare(` INSERT INTO model_usage ( model, provider, prompt_tokens, completion_tokens, total_tokens, cost, duration, session_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run(props.model, props.provider, props.promptTokens || 0, props.completionTokens || 0, props.totalTokens || 0, props.cost || 0, props.duration || 0, event.sessionId); const updateStmt = this.db.prepare(` UPDATE sessions SET total_queries = total_queries + 1, total_tokens = total_tokens + ?, total_cost = total_cost + ? WHERE id = ? `); updateStmt.run(props.totalTokens || 0, props.cost || 0, event.sessionId); } async getInsights(timeframe = 'week') { const days = timeframe === 'day' ? 1 : timeframe === 'week' ? 7 : 30; const since = new Date(); since.setDate(since.getDate() - days); const totals = this.db.prepare(` SELECT COUNT(DISTINCT session_id) as total_sessions, COUNT(*) as total_queries, AVG(duration) as avg_response_time FROM model_usage WHERE timestamp > ? `).get(since.toISOString()); const modelUsage = this.db.prepare(` SELECT model, COUNT(*) as count FROM model_usage WHERE timestamp > ? GROUP BY model `).all(since.toISOString()); const modelUsageMap = {}; modelUsage.forEach(row => { modelUsageMap[row.model] = row.count; }); const errors = this.db.prepare(` SELECT COUNT(*) as error_count FROM events WHERE event = 'error' AND timestamp > ? `).get(since.toISOString()); const errorRate = totals.total_queries > 0 ? errors.error_count / totals.total_queries : 0; const dailyStats = this.db.prepare(` SELECT DATE(timestamp) as date, COUNT(*) as queries, SUM(total_tokens) as tokens, SUM(cost) as cost FROM model_usage WHERE timestamp > ? GROUP BY DATE(timestamp) ORDER BY date DESC `).all(since.toISOString()); return { totalSessions: totals.total_sessions || 0, totalQueries: totals.total_queries || 0, modelUsage: modelUsageMap, averageResponseTime: totals.avg_response_time || 0, errorRate, dailyStats: dailyStats.map(row => ({ date: row.date, queries: row.queries, tokens: row.tokens || 0, cost: row.cost || 0 })) }; } async exportData() { const events = this.db.prepare('SELECT * FROM events').all(); const modelUsage = this.db.prepare('SELECT * FROM model_usage').all(); const sessions = this.db.prepare('SELECT * FROM sessions').all(); return { events, modelUsage, sessions, exportedAt: new Date().toISOString() }; } startSession(sessionId) { const stmt = this.db.prepare('INSERT OR IGNORE INTO sessions (id) VALUES (?)'); stmt.run(sessionId); } endSession(sessionId) { const stmt = this.db.prepare('UPDATE sessions SET ended_at = CURRENT_TIMESTAMP WHERE id = ?'); stmt.run(sessionId); } } //# sourceMappingURL=analytics-service.js.map