← back to Designer Wallcoverings

DW-Agents/dw-agents/agent-ai-expenses/ai-expenses-tracker.ts

742 lines

/**
 * DW-Agents: AI Expenses Tracker
 *
 * Real-time monitoring of Claude and Gemini API usage and costs across all agents
 * - Track API calls with full token details
 * - Calculate costs in real-time using current pricing
 * - Budget alerts and usage analytics
 * - Cost optimization recommendations
 * - Agent-by-agent breakdown
 *
 * Port: 9951
 */

import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import { chatMiddleware } from '../shared-chat-integration';
import { AgentMemory } from '../shared-memory-system';
import { TaskReporter } from '../shared-task-reporter';
import fetch from 'node-fetch';
import * as fs from 'fs';
import * as path from 'path';

const { getUniversalHeader, getThemeStyles } = require('../shared-ui-components.js');

const app = express();
const PORT = process.env.PORT || 9951;

// No authentication required - this agent accepts API calls from other agents
app.use(cookieParser());

// Add inter-agent chat widget
app.use(chatMiddleware({
  agentId: 'ai-expenses',
  agentName: 'AI Expenses Tracker',
  port: PORT,
  category: 'agent'
}));

// Anthropic AI client
const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY || '',
});

// Initialize Agent Memory
const agentMemory = new AgentMemory('ai-expenses');
console.log('📚 Agent memory initialized');

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Session configuration
declare module 'express-session' {
  interface SessionData {
    chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
  }
}

app.use(
  session({
    secret: 'dw-ai-expenses-secret-2025',
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: false,
      httpOnly: true,
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    },
  })
);

// Pricing (as of January 2025)
const PRICING = {
  claude: {
    'claude-3-5-sonnet-20241022': {
      input: 3.00 / 1_000_000,
      output: 15.00 / 1_000_000,
      name: 'Claude 3.5 Sonnet'
    },
    'claude-3-5-haiku-20241022': {
      input: 0.80 / 1_000_000,
      output: 4.00 / 1_000_000,
      name: 'Claude 3.5 Haiku'
    },
    'claude-3-opus-20240229': {
      input: 15.00 / 1_000_000,
      output: 75.00 / 1_000_000,
      name: 'Claude 3 Opus'
    }
  },
  gemini: {
    'gemini-3.5-flash': {
      input: 0.00,
      output: 0.00,
      name: 'Gemini 2.0 Flash (Preview)'
    },
    'gemini-1.5-pro': {
      input: 1.25 / 1_000_000,
      output: 5.00 / 1_000_000,
      name: 'Gemini 1.5 Pro'
    },
    'gemini-1.5-flash': {
      input: 0.075 / 1_000_000,
      output: 0.30 / 1_000_000,
      name: 'Gemini 1.5 Flash'
    }
  }
};

// Budget thresholds (load from memory)
let budgetThresholds = {
  daily: 10.00,
  weekly: 50.00,
  monthly: 200.00
};

// Load saved thresholds from memory
const savedThresholds = agentMemory.search('budget threshold');
if (savedThresholds.length > 0) {
  try {
    budgetThresholds = JSON.parse(savedThresholds[0].content);
    console.log('💰 Loaded budget thresholds from memory');
  } catch (e) {
    console.log('💰 Using default budget thresholds');
  }
}

// Data structures
interface APICall {
  timestamp: Date;
  agent: string;
  model: string;
  provider: 'claude' | 'gemini';
  inputTokens: number;
  outputTokens: number;
  cost: number;
  endpoint: string;
}

interface DailySummary {
  date: string;
  totalCost: number;
  totalCalls: number;
  totalInputTokens: number;
  totalOutputTokens: number;
  byProvider: {
    claude: { calls: number; cost: number; tokens: number };
    gemini: { calls: number; cost: number; tokens: number };
  };
  byAgent: Record<string, { calls: number; cost: number }>;
}

// In-memory storage (persistent to file)
let apiCalls: APICall[] = [];
let dailySummaries: Record<string, DailySummary> = {};
let alertsSent: string[] = [];

// Activity log
const activityLog: Array<{
  timestamp: Date;
  action: string;
  details: string;
}> = [];

function logActivity(action: string, details: string) {
  activityLog.unshift({
    timestamp: new Date(),
    action,
    details,
  });
  if (activityLog.length > 100) activityLog.pop();
}

// Data persistence
const DATA_FILE = path.join(__dirname, 'expenses-data.json');

function loadData() {
  try {
    if (fs.existsSync(DATA_FILE)) {
      const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
      apiCalls = data.apiCalls.map((call: any) => ({
        ...call,
        timestamp: new Date(call.timestamp)
      }));
      dailySummaries = data.dailySummaries;
      alertsSent = data.alertsSent || [];
      console.log(`📊 Loaded ${apiCalls.length} API calls from storage`);
      logActivity('System', `Loaded ${apiCalls.length} historical API calls`);
    }
  } catch (error) {
    console.error('Error loading data:', error);
    logActivity('Error', 'Failed to load historical data');
  }
}

function saveData() {
  try {
    fs.writeFileSync(DATA_FILE, JSON.stringify({ apiCalls, dailySummaries, alertsSent }, null, 2));
  } catch (error) {
    console.error('Error saving data:', error);
  }
}

// Calculate cost for an API call
function calculateCost(provider: 'claude' | 'gemini', model: string, inputTokens: number, outputTokens: number): number {
  const pricing = PRICING[provider][model];
  if (!pricing) return 0;
  return (inputTokens * pricing.input) + (outputTokens * pricing.output);
}

// Check budget alerts
async function checkBudgetAlerts(stats: any) {
  const today = new Date().toISOString().split('T')[0];
  const alertKey = `alert-${today}`;

  if (alertsSent.includes(alertKey)) return;

  if (stats.today.cost >= budgetThresholds.daily) {
    const task = new TaskReporter(
      'Daily AI Budget Alert',
      `Daily AI expenses have reached $${stats.today.cost.toFixed(2)} (threshold: $${budgetThresholds.daily})`,
      'ai-expenses',
      'high'
    );
    await task.complete(`Budget: $${stats.today.cost.toFixed(2)} | Calls: ${stats.today.calls}`);

    alertsSent.push(alertKey);
    logActivity('Alert', `Daily budget threshold exceeded: $${stats.today.cost.toFixed(2)}`);
  }
}

// Track an API call
async function trackAPICall(agent: string, provider: 'claude' | 'gemini', model: string, inputTokens: number, outputTokens: number, endpoint: string = '/messages') {
  const cost = calculateCost(provider, model, inputTokens, outputTokens);

  const call: APICall = {
    timestamp: new Date(),
    agent,
    model,
    provider,
    inputTokens,
    outputTokens,
    cost,
    endpoint
  };

  apiCalls.push(call);
  updateDailySummary(call);
  saveData();

  logActivity('API Call', `${agent}: ${provider} ${model} - $${cost.toFixed(6)}`);

  // Check budget alerts
  const stats = getStats();
  await checkBudgetAlerts(stats);

  return call;
}

// Update daily summary
function updateDailySummary(call: APICall) {
  const dateKey = call.timestamp.toISOString().split('T')[0];

  if (!dailySummaries[dateKey]) {
    dailySummaries[dateKey] = {
      date: dateKey,
      totalCost: 0,
      totalCalls: 0,
      totalInputTokens: 0,
      totalOutputTokens: 0,
      byProvider: {
        claude: { calls: 0, cost: 0, tokens: 0 },
        gemini: { calls: 0, cost: 0, tokens: 0 }
      },
      byAgent: {}
    };
  }

  const summary = dailySummaries[dateKey];
  summary.totalCost += call.cost;
  summary.totalCalls++;
  summary.totalInputTokens += call.inputTokens;
  summary.totalOutputTokens += call.outputTokens;

  summary.byProvider[call.provider].calls++;
  summary.byProvider[call.provider].cost += call.cost;
  summary.byProvider[call.provider].tokens += call.inputTokens + call.outputTokens;

  if (!summary.byAgent[call.agent]) {
    summary.byAgent[call.agent] = { calls: 0, cost: 0 };
  }
  summary.byAgent[call.agent].calls++;
  summary.byAgent[call.agent].cost += call.cost;
}

// Get stats
function getStats() {
  const now = new Date();
  const today = now.toISOString().split('T')[0];
  const last7Days = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
  const last30Days = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

  const todayCalls = apiCalls.filter(c => c.timestamp.toISOString().split('T')[0] === today);
  const last7DaysCalls = apiCalls.filter(c => c.timestamp >= last7Days);
  const last30DaysCalls = apiCalls.filter(c => c.timestamp >= last30Days);

  return {
    today: {
      calls: todayCalls.length,
      cost: todayCalls.reduce((sum, c) => sum + c.cost, 0),
      tokens: todayCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
    },
    last7Days: {
      calls: last7DaysCalls.length,
      cost: last7DaysCalls.reduce((sum, c) => sum + c.cost, 0),
      tokens: last7DaysCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
    },
    last30Days: {
      calls: last30DaysCalls.length,
      cost: last30DaysCalls.reduce((sum, c) => sum + c.cost, 0),
      tokens: last30DaysCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
    },
    allTime: {
      calls: apiCalls.length,
      cost: apiCalls.reduce((sum, c) => sum + c.cost, 0),
      tokens: apiCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
    }
  };
}

// Initialize
loadData();

// Main dashboard
app.get('/', (req, res) => {
  const stats = getStats();
  const today = new Date().toISOString().split('T')[0];
  const todaySummary = dailySummaries[today];
  const memoryContext = agentMemory.getContextSummary();

  res.send(`
<!DOCTYPE html>
<html>
<head>
  <title>AI Expenses Tracker - DW-Agents</title>
  ${getThemeStyles('professional-dark')}
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: var(--font-primary);
      background: var(--color-background);
      color: var(--color-text-primary);
      padding: 20px;
      min-height: 100vh;
    }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: 20px;
      margin: 30px 0;
    }

    .stat-card {
      background: var(--color-surface);
      border: 1px solid var(--color-neutral200);
      border-radius: var(--radius-lg);
      padding: 25px;
      box-shadow: var(--shadow-md);
    }

    .stat-card h3 {
      color: var(--color-text-secondary);
      font-size: var(--font-size-sm);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      margin-bottom: 10px;
      font-weight: var(--font-weight-semibold);
    }

    .stat-value {
      font-size: var(--font-size-3xl);
      font-weight: var(--font-weight-bold);
      color: var(--color-primary);
      margin-bottom: 5px;
    }

    .stat-value.cost {
      color: var(--color-success);
    }

    .stat-value.warning {
      color: var(--color-warning);
    }

    .stat-label {
      color: var(--color-text-secondary);
      font-size: var(--font-size-sm);
    }

    .budget-alert {
      background: rgba(234, 179, 8, 0.1);
      border: 2px solid var(--color-warning);
      border-radius: var(--radius-lg);
      padding: 20px;
      margin: 20px 0;
    }

    .table-container {
      background: var(--color-surface);
      border: 1px solid var(--color-neutral200);
      border-radius: var(--radius-lg);
      padding: 25px;
      margin: 30px 0;
      overflow-x: auto;
    }

    table {
      width: 100%;
      border-collapse: collapse;
    }

    th, td {
      padding: 12px;
      text-align: left;
      border-bottom: 1px solid var(--color-neutral200);
    }

    th {
      background: var(--color-neutral100);
      font-weight: var(--font-weight-semibold);
      color: var(--color-text-primary);
      position: sticky;
      top: 0;
    }

    tr:hover {
      background: var(--color-neutral100);
    }

    .provider-badge {
      display: inline-block;
      padding: 4px 12px;
      border-radius: var(--radius-full);
      font-size: var(--font-size-sm);
      font-weight: var(--font-weight-medium);
    }

    .provider-claude {
      background: rgba(138, 99, 210, 0.2);
      color: #8a63d2;
    }

    .provider-gemini {
      background: rgba(66, 133, 244, 0.2);
      color: #4285f4;
    }

    .refresh-btn {
      background: var(--color-primary);
      color: var(--color-text-inverse);
      border: none;
      padding: 12px 24px;
      border-radius: var(--radius-md);
      font-weight: var(--font-weight-semibold);
      cursor: pointer;
      transition: all 0.2s;
    }

    .refresh-btn:hover {
      background: var(--color-secondary);
      transform: translateY(-2px);
    }
  </style>
</head>
<body>
  ${getUniversalHeader('AI Expenses Tracker', 9951)}

  <div style="max-width: 1400px; margin: 0 auto;">
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
      <div>
        <h1 style="font-size: var(--font-size-2xl); margin-bottom: 8px;">AI API Expenses Dashboard</h1>
        <p style="color: var(--color-text-secondary);">Real-time monitoring of Claude and Gemini API usage across all 30+ agents</p>
      </div>
      <button class="refresh-btn" onclick="location.reload()">🔄 Refresh</button>
    </div>

    ${stats.today.cost >= budgetThresholds.daily ? `
    <div class="budget-alert">
      <h3 style="color: var(--color-warning); margin-bottom: 10px;">⚠️ Daily Budget Alert</h3>
      <p>Today's expenses ($${stats.today.cost.toFixed(2)}) have exceeded the daily threshold of $${budgetThresholds.daily.toFixed(2)}</p>
    </div>
    ` : ''}

    <div class="stats-grid">
      <div class="stat-card">
        <h3>Today</h3>
        <div class="stat-value cost ${stats.today.cost >= budgetThresholds.daily ? 'warning' : ''}">$${stats.today.cost.toFixed(4)}</div>
        <div class="stat-label">${stats.today.calls} calls • ${stats.today.tokens.toLocaleString()} tokens</div>
        <div class="stat-label" style="margin-top: 8px;">Budget: $${budgetThresholds.daily} daily</div>
      </div>

      <div class="stat-card">
        <h3>Last 7 Days</h3>
        <div class="stat-value cost">$${stats.last7Days.cost.toFixed(2)}</div>
        <div class="stat-label">${stats.last7Days.calls} calls • ${stats.last7Days.tokens.toLocaleString()} tokens</div>
        <div class="stat-label" style="margin-top: 8px;">Avg: $${(stats.last7Days.cost / 7).toFixed(2)}/day</div>
      </div>

      <div class="stat-card">
        <h3>Last 30 Days</h3>
        <div class="stat-value cost">$${stats.last30Days.cost.toFixed(2)}</div>
        <div class="stat-label">${stats.last30Days.calls} calls • ${stats.last30Days.tokens.toLocaleString()} tokens</div>
        <div class="stat-label" style="margin-top: 8px;">Budget: $${budgetThresholds.monthly} monthly</div>
      </div>

      <div class="stat-card">
        <h3>All Time</h3>
        <div class="stat-value cost">$${stats.allTime.cost.toFixed(2)}</div>
        <div class="stat-label">${stats.allTime.calls} calls • ${stats.allTime.tokens.toLocaleString()} tokens</div>
        <div class="stat-label" style="margin-top: 8px;">Tracking since deployment</div>
      </div>
    </div>

    ${todaySummary ? `
    <div class="table-container">
      <h2 style="margin-bottom: 20px; font-size: var(--font-size-xl);">📊 Today's Usage by Agent</h2>
      <table>
        <thead>
          <tr>
            <th>Agent</th>
            <th>API Calls</th>
            <th>Cost</th>
            <th>% of Today</th>
          </tr>
        </thead>
        <tbody>
          ${Object.entries(todaySummary.byAgent)
            .sort((a, b) => b[1].cost - a[1].cost)
            .map(([agent, data]) => `
              <tr>
                <td><strong>${agent}</strong></td>
                <td>${data.calls}</td>
                <td>$${data.cost.toFixed(4)}</td>
                <td>${((data.cost / todaySummary.totalCost) * 100).toFixed(1)}%</td>
              </tr>
            `).join('')}
        </tbody>
      </table>
    </div>
    ` : '<div class="table-container"><p style="color: var(--color-text-secondary);">No API calls recorded today yet.</p></div>'}

    <div class="table-container">
      <h2 style="margin-bottom: 20px; font-size: var(--font-size-xl);">📝 Recent API Calls</h2>
      ${apiCalls.length > 0 ? `
      <table>
        <thead>
          <tr>
            <th>Time</th>
            <th>Agent</th>
            <th>Provider</th>
            <th>Model</th>
            <th>Input</th>
            <th>Output</th>
            <th>Cost</th>
          </tr>
        </thead>
        <tbody>
          ${apiCalls.slice(-50).reverse().map(call => `
            <tr>
              <td>${new Date(call.timestamp).toLocaleTimeString()}</td>
              <td>${call.agent}</td>
              <td><span class="provider-badge provider-${call.provider}">${call.provider.toUpperCase()}</span></td>
              <td>${PRICING[call.provider][call.model]?.name || call.model}</td>
              <td>${call.inputTokens.toLocaleString()}</td>
              <td>${call.outputTokens.toLocaleString()}</td>
              <td>$${call.cost.toFixed(6)}</td>
            </tr>
          `).join('')}
        </tbody>
      </table>
      ` : '<p style="color: var(--color-text-secondary);">No API calls recorded yet.</p>'}
    </div>

    <div class="table-container">
      <h2 style="margin-bottom: 20px; font-size: var(--font-size-xl);">💡 Recent Activity</h2>
      ${activityLog.length > 0 ? `
      <table>
        <thead>
          <tr>
            <th>Time</th>
            <th>Action</th>
            <th>Details</th>
          </tr>
        </thead>
        <tbody>
          ${activityLog.slice(0, 20).map(log => `
            <tr>
              <td>${new Date(log.timestamp).toLocaleTimeString()}</td>
              <td><strong>${log.action}</strong></td>
              <td>${log.details}</td>
            </tr>
          `).join('')}
        </tbody>
      </table>
      ` : '<p style="color: var(--color-text-secondary);">No activity logged yet.</p>'}
    </div>
  </div>

  <script>
    // Auto-refresh every 30 seconds
    setTimeout(() => location.reload(), 30000);
  </script>
</body>
</html>
  `);
});

// API endpoint to track expenses (called by other agents)
app.post('/api/track', async (req, res) => {
  const { agent, provider, model, inputTokens, outputTokens, endpoint } = req.body;

  if (!agent || !provider || !model || inputTokens === undefined || outputTokens === undefined) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  const call = await trackAPICall(agent, provider, model, inputTokens, outputTokens, endpoint);

  res.json({
    success: true,
    cost: call.cost,
    totalToday: getStats().today.cost
  });
});

// API endpoints
app.get('/api/stats', (req, res) => {
  res.json(getStats());
});

app.get('/api/pricing', (req, res) => {
  res.json(PRICING);
});

app.get('/api/activity', (req, res) => {
  res.json(activityLog.slice(0, 50));
});

// Chat endpoint with memory integration
app.post('/api/chat', async (req, res) => {
  try {
    const { message } = req.body;

    if (!message) {
      return res.status(400).json({ error: 'Message is required' });
    }

    // Handle memory commands
    if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
      const parts = message.split(':');
      const type = parts[0].toLowerCase();
      const content = parts.slice(1).join(':').trim();
      const memType = type === 'remember' ? 'note' : type as any;
      agentMemory.add(content, memType);
      return res.json({ response: `✓ Saved to memory: ${content}` });
    }

    // Handle budget threshold updates
    if (message.toLowerCase().includes('set budget') || message.toLowerCase().includes('set threshold')) {
      const match = message.match(/daily\s+\$?(\d+(?:\.\d+)?)|weekly\s+\$?(\d+(?:\.\d+)?)|monthly\s+\$?(\d+(?:\.\d+)?)/i);
      if (match) {
        if (match[1]) budgetThresholds.daily = parseFloat(match[1]);
        if (match[2]) budgetThresholds.weekly = parseFloat(match[2]);
        if (match[3]) budgetThresholds.monthly = parseFloat(match[3]);

        agentMemory.add(JSON.stringify(budgetThresholds), 'preference');
        return res.json({
          response: `✓ Updated budget thresholds:\nDaily: $${budgetThresholds.daily}\nWeekly: $${budgetThresholds.weekly}\nMonthly: $${budgetThresholds.monthly}`
        });
      }
    }

    const stats = getStats();
    const memoryContext = agentMemory.getContextSummary();

    const response = await anthropic.messages.create({
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 2000,
      messages: [{
        role: 'user',
        content: `You are the AI Expenses Tracker agent for Designer Wallcoverings. You monitor and analyze API usage and costs across all 30+ agents.

Current Statistics:
- Today: $${stats.today.cost.toFixed(4)} (${stats.today.calls} calls, ${stats.today.tokens.toLocaleString()} tokens)
- Last 7 days: $${stats.last7Days.cost.toFixed(2)} (${stats.last7Days.calls} calls)
- Last 30 days: $${stats.last30Days.cost.toFixed(2)} (${stats.last30Days.calls} calls)
- All time: $${stats.allTime.cost.toFixed(2)} (${stats.allTime.calls} total calls)

Budget Thresholds:
- Daily: $${budgetThresholds.daily}
- Weekly: $${budgetThresholds.weekly}
- Monthly: $${budgetThresholds.monthly}

Memory Context:
${memoryContext}

User question: ${message}

Provide helpful analysis, recommendations, and cost optimization suggestions. If the user asks about specific agents or time periods, analyze the data and provide insights.`
      }]
    });

    // Track this API call
    const usage = response.usage;
    await trackAPICall('ai-expenses', 'claude', 'claude-3-5-sonnet-20241022', usage.input_tokens, usage.output_tokens, '/messages');

    res.json({
      response: response.content[0].type === 'text' ? response.content[0].text : ''
    });
  } catch (error) {
    console.error('Chat error:', error);
    res.status(500).json({ error: 'Failed to process message' });
  }
});

app.listen(PORT, '0.0.0.0', () => {
  console.log('');
  console.log('💰 AI Expenses Tracker');
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  console.log('🌍 External: http://45.61.58.125:' + PORT);
  console.log('📊 Tracking: Claude & Gemini API usage');
  console.log('💵 Real-time: Cost calculations');
  console.log('🎯 Budget Alerts: Enabled');
  console.log('💾 Memory System: Active');
  console.log('');
  console.log(`📈 Loaded ${apiCalls.length} historical API calls`);
  console.log(`💰 Budget - Daily: $${budgetThresholds.daily} | Weekly: $${budgetThresholds.weekly} | Monthly: $${budgetThresholds.monthly}`);
  console.log('✅ AI Expenses Tracker ready...');
});