UNPKG

pit-manager

Version:

Centralized prompt management system for Human Behavior AI agents

530 lines (441 loc) 18.3 kB
#!/usr/bin/env tsx /** * Visualize aggregate chain tracking statistics in ASCII tree format * Shows hierarchical agent relationships and average metrics per chain execution */ import { createClient } from '@supabase/supabase-js'; import * as dotenv from 'dotenv'; import * as path from 'path'; // Load environment variables dotenv.config({ path: path.resolve(process.cwd(), '.env') }); interface ChainExecutionGroup { id: string; group_id: string; chain_id: string; repo_id: string; session_id?: string; process_id?: string; start_time: string; end_time?: string; status: string; total_executions: number; total_tokens: number; metadata: any; } interface Execution { id: string; tag: string; tokens: any; chain_group_id: string; parent_execution_id?: string; } interface AgentStats { callsPerChain: number[]; tokensPerChain: number[]; } interface AgentNode { name: string; calls: number; totalTokens: number; stats: AgentStats; children: Map<string, AgentNode>; } class ChainVisualizationAnalyzer { private supabaseClient: any; private repoId: string; constructor() { const url = process.env.PIT_SUPABASE_URL; const serviceKey = process.env.PIT_SUPABASE_SERVICE_ROLE_KEY; const repoKey = process.env.PIT_REPO_KEY; if (!url || !serviceKey || !repoKey) { throw new Error('Missing required environment variables: PIT_SUPABASE_URL, PIT_SUPABASE_SERVICE_ROLE_KEY, PIT_REPO_KEY'); } this.supabaseClient = createClient(url, serviceKey); // Extract repo_id from repo_key by validating it this.repoId = ''; } async initialize() { // Validate repo key to get repo_id const repoKey = process.env.PIT_REPO_KEY!; const keyHash = this.hashRepoKey(repoKey); const { data, error } = await this.supabaseClient .from('repo_keys') .select('repo_id') .eq('key_hash', keyHash) .single(); if (error || !data) { throw new Error('Invalid repository key'); } this.repoId = data.repo_id; console.log('Repository ID:', this.repoId); } private hashRepoKey(repoKey: string): string { const crypto = require('crypto'); return crypto.createHash('sha256').update(repoKey).digest('hex'); } async analyzeChain(chainId: string) { console.log(`\nAnalyzing chain: ${chainId}\n`); console.log('=' .repeat(80)); // Get all chain execution groups for this chain const { data: chainGroups, error: groupError } = await this.supabaseClient .from('chain_execution_groups') .select('*') .eq('repo_id', this.repoId) .eq('chain_id', chainId); // Removed status filter to include all chains for demonstration if (groupError || !chainGroups || chainGroups.length === 0) { console.log('No chain execution groups found for this chain.'); return; } const completedGroups = chainGroups.filter(g => g.status === 'completed'); const runningGroups = chainGroups.filter(g => g.status === 'running'); const failedGroups = chainGroups.filter(g => g.status === 'failed'); console.log(`Found ${chainGroups.length} chain execution(s):`); console.log(` - Completed: ${completedGroups.length}`); console.log(` - Running: ${runningGroups.length}`); console.log(` - Failed: ${failedGroups.length}\n`); // Build agent hierarchy const rootNode: AgentNode = { name: chainId, calls: chainGroups.length, totalTokens: 0, stats: { callsPerChain: [], tokensPerChain: [] }, children: new Map() }; // Process each chain group separately to track per-chain stats const chainResults: Map<string, Map<string, { calls: number, tokens: number }>> = new Map(); for (const group of chainGroups) { const chainStats = await this.processChainGroupForStats(group); chainResults.set(group.group_id, chainStats); } // Build logical hierarchy based on code analysis since parent_execution_id is not working yet this.buildLogicalHierarchy(rootNode, chainResults); // Debug: Show what we collected console.log('Agents captured:', Array.from(rootNode.children.keys()).join(', ')); console.log(); // Generate and print ASCII tree this.printTree(rootNode, chainGroups.length, '', true, true); } private async processChainGroupForStats(group: ChainExecutionGroup): Promise<Map<string, { calls: number, tokens: number }>> { // Get all executions for this chain group const { data: executions, error } = await this.supabaseClient .from('executions') .select('*') .eq('chain_group_id', group.id); if (error || !executions) { console.warn(`Failed to get executions for group ${group.group_id}`); return new Map(); } // Count calls and tokens per agent for this chain const agentStats = new Map<string, { calls: number, tokens: number }>(); for (const exec of executions) { const agentName = exec.tag || 'unknown-agent'; if (!agentStats.has(agentName)) { agentStats.set(agentName, { calls: 0, tokens: 0 }); } const stats = agentStats.get(agentName)!; stats.calls++; // Add tokens from execution const tokens = exec.tokens || {}; const totalTokens = typeof tokens === 'object' ? (tokens.total || 0) : 0; stats.tokens += totalTokens; } return agentStats; } private buildAggregatedStats(rootNode: AgentNode, chainResults: Map<string, Map<string, { calls: number, tokens: number }>>) { // Get all unique agent names const allAgents = new Set<string>(); chainResults.forEach(chainStats => { chainStats.forEach((_, agentName) => allAgents.add(agentName)); }); // Build nodes for each agent for (const agentName of allAgents) { const node: AgentNode = { name: agentName, calls: 0, totalTokens: 0, stats: { callsPerChain: [], tokensPerChain: [] }, children: new Map() }; // Collect stats from each chain chainResults.forEach(chainStats => { const agentStats = chainStats.get(agentName); if (agentStats) { node.stats.callsPerChain.push(agentStats.calls); node.stats.tokensPerChain.push(agentStats.tokens); node.calls += agentStats.calls; node.totalTokens += agentStats.tokens; } else { // Agent didn't run in this chain node.stats.callsPerChain.push(0); node.stats.tokensPerChain.push(0); } }); rootNode.children.set(agentName, node); } } private buildLogicalHierarchy(rootNode: AgentNode, chainResults: Map<string, Map<string, { calls: number, tokens: number }>>) { // Based on code analysis, the logical hierarchy should be: // line-analysis-agent (multiple chunks) - ROOT level agents // ├── summary-agent (child of last line-analysis chunk) // │ └── title-agent (child of summary-agent) // │ └── product-features-agent (child of title-agent) // ├── create-raw-tickets-agent (child of last line-analysis chunk, multiple instances) // │ └── check-duplicate-agent (child of create-raw-tickets-agent) // ├── notification-filter-agent (child of last line-analysis chunk) // └── slack-summary-agent (child of last line-analysis chunk) // Get all unique agent names const allAgents = new Set<string>(); chainResults.forEach(chainStats => { chainStats.forEach((_, agentName) => allAgents.add(agentName)); }); // Build stats for all agents first const agentStats = new Map<string, AgentNode>(); for (const agentName of allAgents) { const node: AgentNode = { name: agentName, calls: 0, totalTokens: 0, stats: { callsPerChain: [], tokensPerChain: [] }, children: new Map() }; // Collect stats from each chain chainResults.forEach(chainStats => { const agentStat = chainStats.get(agentName); if (agentStat) { node.stats.callsPerChain.push(agentStat.calls); node.stats.tokensPerChain.push(agentStat.tokens); node.calls += agentStat.calls; node.totalTokens += agentStat.tokens; } else { // Agent didn't run in this chain node.stats.callsPerChain.push(0); node.stats.tokensPerChain.push(0); } }); agentStats.set(agentName, node); } // Build logical hierarchy based on code analysis const getOrCreateNode = (name: string) => agentStats.get(name) || { name, calls: 0, totalTokens: 0, stats: { callsPerChain: [], tokensPerChain: [] }, children: new Map() }; // Start with line-analysis-agent as root level (multiple chunks) if (agentStats.has('line-analysis-agent')) { const lineAnalysisNode = getOrCreateNode('line-analysis-agent'); rootNode.children.set('line-analysis-agent', lineAnalysisNode); // summary-agent is child of line-analysis-agent if (agentStats.has('summary-agent')) { const summaryNode = getOrCreateNode('summary-agent'); lineAnalysisNode.children.set('summary-agent', summaryNode); // title-agent is child of summary-agent if (agentStats.has('title-agent')) { const titleNode = getOrCreateNode('title-agent'); summaryNode.children.set('title-agent', titleNode); // product-features-agent is child of title-agent if (agentStats.has('product-features-agent')) { const productFeaturesNode = getOrCreateNode('product-features-agent'); titleNode.children.set('product-features-agent', productFeaturesNode); } } } // create-raw-tickets-agent is also child of line-analysis-agent if (agentStats.has('create-raw-tickets-agent')) { const createTicketsNode = getOrCreateNode('create-raw-tickets-agent'); lineAnalysisNode.children.set('create-raw-tickets-agent', createTicketsNode); // check-duplicate-agent is child of create-raw-tickets-agent if (agentStats.has('check-duplicate-agent')) { const checkDuplicateNode = getOrCreateNode('check-duplicate-agent'); createTicketsNode.children.set('check-duplicate-agent', checkDuplicateNode); } } // notification-filter-agent is child of line-analysis-agent if (agentStats.has('notification-filter-agent')) { const notificationFilterNode = getOrCreateNode('notification-filter-agent'); lineAnalysisNode.children.set('notification-filter-agent', notificationFilterNode); } // slack-summary-agent is child of line-analysis-agent if (agentStats.has('slack-summary-agent')) { const slackSummaryNode = getOrCreateNode('slack-summary-agent'); lineAnalysisNode.children.set('slack-summary-agent', slackSummaryNode); } // workflow-assignment-agent is child of line-analysis-agent if (agentStats.has('workflow-assignment-agent')) { const workflowAssignmentNode = getOrCreateNode('workflow-assignment-agent'); lineAnalysisNode.children.set('workflow-assignment-agent', workflowAssignmentNode); } } // Add any remaining agents that weren't part of the logical hierarchy as root level for (const [agentName, node] of agentStats) { if (!this.isAgentInHierarchy(rootNode, agentName)) { rootNode.children.set(agentName, node); } } } private isAgentInHierarchy(rootNode: AgentNode, agentName: string): boolean { // Check if agent is already placed in the hierarchy const checkNode = (node: AgentNode): boolean => { if (node.name === agentName) return true; for (const child of node.children.values()) { if (checkNode(child)) return true; } return false; }; return checkNode(rootNode); } private async processChainGroup(group: ChainExecutionGroup, rootNode: AgentNode) { // Get all executions for this chain group const { data: executions, error } = await this.supabaseClient .from('executions') .select('*') .eq('chain_group_id', group.id); if (error || !executions) { console.warn(`Failed to get executions for group ${group.group_id}`); return; } // Build parent-child relationships const executionMap = new Map<string, Execution>(); const childrenMap = new Map<string, string[]>(); for (const exec of executions) { executionMap.set(exec.id, exec); if (exec.parent_execution_id) { if (!childrenMap.has(exec.parent_execution_id)) { childrenMap.set(exec.parent_execution_id, []); } childrenMap.get(exec.parent_execution_id)!.push(exec.id); } } // Find root executions (no parent) const rootExecutions = executions.filter(e => !e.parent_execution_id); // Process each root execution recursively for (const rootExec of rootExecutions) { this.addToHierarchy(rootNode, rootExec, executionMap, childrenMap, []); } } private addToHierarchy( parentNode: AgentNode, execution: Execution, executionMap: Map<string, Execution>, childrenMap: Map<string, string[]>, path: string[] ) { const agentName = execution.tag || 'unknown-agent'; // Get or create child node if (!parentNode.children.has(agentName)) { parentNode.children.set(agentName, { name: agentName, calls: 0, totalTokens: 0, children: new Map() }); } const node = parentNode.children.get(agentName)!; node.calls++; // Add tokens from execution const tokens = execution.tokens || {}; const totalTokens = typeof tokens === 'object' ? (tokens.total || 0) : 0; node.totalTokens += totalTokens; // Process children const children = childrenMap.get(execution.id) || []; for (const childId of children) { const childExec = executionMap.get(childId); if (childExec && !path.includes(childId)) { // Avoid cycles this.addToHierarchy(node, childExec, executionMap, childrenMap, [...path, childId]); } } } private calculateVariance(values: number[]): number { if (values.length <= 1) return 0; const mean = values.reduce((a, b) => a + b, 0) / values.length; const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / values.length; return variance; } private printTree(node: AgentNode, totalChainExecutions: number, prefix = '', isLast = true, isRoot = false) { // Print current node if (isRoot) { // Root node console.log(`${node.name} (executed ${totalChainExecutions} separate times)`); console.log(''); } else { const connector = isLast ? '└── ' : '├── '; // Calculate statistics const callsPerChain = node.stats.callsPerChain; const tokensPerChain = node.stats.tokensPerChain; const avgCalls = callsPerChain.length > 0 ? (callsPerChain.reduce((a, b) => a + b, 0) / callsPerChain.length) : 0; const avgTokensPerCall = node.calls > 0 ? (node.totalTokens / node.calls) : 0; const avgTokensPerChain = tokensPerChain.length > 0 ? (tokensPerChain.reduce((a, b) => a + b, 0) / tokensPerChain.length) : 0; const callsVariance = this.calculateVariance(callsPerChain); const tokensVariance = this.calculateVariance(tokensPerChain); // Main line with averages console.log( `${prefix}${connector}${node.name} ` + `(avg ${avgCalls.toFixed(1)} calls/chain, σ²=${callsVariance.toFixed(1)}) ` + `(avg ${Math.round(avgTokensPerCall).toLocaleString()} tokens/call) ` + `(avg ${Math.round(avgTokensPerChain).toLocaleString()} tokens/chain)` ); // Show per-chain breakdown indented if (callsPerChain.length > 0) { const detailPrefix = prefix + (isLast ? ' ' : '│ ') + ' '; console.log(`${detailPrefix}Per-chain calls: [${callsPerChain.join(', ')}]`); console.log(`${detailPrefix}Per-chain tokens: [${tokensPerChain.map(t => Math.round(t).toLocaleString()).join(', ')}]`); } } // Print children const children = Array.from(node.children.values()) .sort((a, b) => { // Sort by average calls per chain (descending) const aAvg = a.stats.callsPerChain.length > 0 ? (a.stats.callsPerChain.reduce((x, y) => x + y, 0) / a.stats.callsPerChain.length) : 0; const bAvg = b.stats.callsPerChain.length > 0 ? (b.stats.callsPerChain.reduce((x, y) => x + y, 0) / b.stats.callsPerChain.length) : 0; return bAvg - aAvg; }); children.forEach((child, index) => { const isLastChild = index === children.length - 1; const extension = isRoot ? '' : (isLast ? ' ' : '│ '); this.printTree(child, totalChainExecutions, prefix + extension, isLastChild, false); }); } async listAvailableChains() { const { data: chains, error } = await this.supabaseClient .from('chain_execution_groups') .select('chain_id, status') .eq('repo_id', this.repoId); if (error || !chains) { console.log('No chains found.'); return []; } // Get unique chain IDs const uniqueChains = [...new Set(chains.map(c => c.chain_id))]; console.log('\nAvailable chains:'); console.log('-----------------'); uniqueChains.forEach(chain => { console.log(` - ${chain}`); }); console.log(); return uniqueChains; } } // Main execution async function main() { try { const analyzer = new ChainVisualizationAnalyzer(); await analyzer.initialize(); // Get chain ID from command line or use default const chainId = process.argv[2] || 'session-analysis-pipeline'; if (chainId === '--list') { await analyzer.listAvailableChains(); } else { await analyzer.analyzeChain(chainId); } } catch (error) { console.error('Error:', error); process.exit(1); } } main();