capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
163 lines (161 loc) • 5.79 kB
JavaScript
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