/** * Enhanced Tool Integration * * Integrates tool preview, feedback, and error recovery into the LLM chat experience. */ import server from "../../services/server.js"; import { ToolPreviewUI, type ExecutionPlanData, type UserApproval } from "./tool_preview_ui.js"; import { ToolFeedbackUI, type ToolProgressData, type ToolStepData } from "./tool_feedback_ui.js"; /** * Enhanced tool integration configuration */ export interface EnhancedToolConfig { enablePreview?: boolean; enableFeedback?: boolean; enableErrorRecovery?: boolean; requireConfirmation?: boolean; autoApproveTimeout?: number; showHistory?: boolean; showStatistics?: boolean; } /** * Default configuration */ const DEFAULT_CONFIG: EnhancedToolConfig = { enablePreview: true, enableFeedback: true, enableErrorRecovery: true, requireConfirmation: true, autoApproveTimeout: 30000, // 30 seconds showHistory: true, showStatistics: true }; /** * Enhanced Tool Integration Manager */ export class EnhancedToolIntegration { private config: EnhancedToolConfig; private previewUI?: ToolPreviewUI; private feedbackUI?: ToolFeedbackUI; private container: HTMLElement; private eventHandlers: Map = new Map(); private activeExecutions: Set = new Set(); constructor(container: HTMLElement, config?: Partial) { this.container = container; this.config = { ...DEFAULT_CONFIG, ...config }; this.initialize(); } /** * Initialize the integration */ private initialize(): void { // Create UI containers this.createUIContainers(); // Initialize UI components if (this.config.enablePreview) { const previewContainer = this.container.querySelector('.tool-preview-area') as HTMLElement; if (previewContainer) { this.previewUI = new ToolPreviewUI(previewContainer); } } if (this.config.enableFeedback) { const feedbackContainer = this.container.querySelector('.tool-feedback-area') as HTMLElement; if (feedbackContainer) { this.feedbackUI = new ToolFeedbackUI(feedbackContainer); // Set up history and stats containers if enabled if (this.config.showHistory) { const historyContainer = this.container.querySelector('.tool-history-area') as HTMLElement; if (historyContainer) { this.feedbackUI.setHistoryContainer(historyContainer); } } if (this.config.showStatistics) { const statsContainer = this.container.querySelector('.tool-stats-area') as HTMLElement; if (statsContainer) { this.feedbackUI.setStatsContainer(statsContainer); this.loadStatistics(); } } } } // Load initial data this.loadActiveExecutions(); this.loadCircuitBreakerStatus(); } /** * Create UI containers */ private createUIContainers(): void { // Add enhanced tool UI areas if they don't exist if (!this.container.querySelector('.tool-preview-area')) { const previewArea = document.createElement('div'); previewArea.className = 'tool-preview-area mb-3'; this.container.appendChild(previewArea); } if (!this.container.querySelector('.tool-feedback-area')) { const feedbackArea = document.createElement('div'); feedbackArea.className = 'tool-feedback-area mb-3'; this.container.appendChild(feedbackArea); } if (this.config.showHistory && !this.container.querySelector('.tool-history-area')) { const historySection = document.createElement('div'); historySection.className = 'tool-history-section mt-3'; historySection.innerHTML = `
Execution History
`; this.container.appendChild(historySection); } if (this.config.showStatistics && !this.container.querySelector('.tool-stats-area')) { const statsSection = document.createElement('div'); statsSection.className = 'tool-stats-section mt-3'; statsSection.innerHTML = `
Tool Statistics
`; this.container.appendChild(statsSection); } } /** * Handle tool preview request */ public async handleToolPreview(toolCalls: any[]): Promise { if (!this.config.enablePreview || !this.previewUI) { // Auto-approve if preview is disabled return { planId: `auto-${Date.now()}`, approved: true }; } try { // Get preview from server const response = await server.post('api/llm-tools/preview', { toolCalls }); if (!response) { console.error('Failed to get tool preview'); return null; } // Show preview and wait for user approval return new Promise((resolve) => { let timeoutId: number | undefined; const handleApproval = (approval: UserApproval) => { if (timeoutId) { clearTimeout(timeoutId); } // Send approval to server server.post(`api/llm-tools/preview/${approval.planId}/approval`, approval) .catch(error => console.error('Failed to record approval:', error)); resolve(approval); }; // Show preview UI this.previewUI!.showPreview(response, handleApproval); // Auto-approve after timeout if configured if (this.config.autoApproveTimeout && response.requiresConfirmation) { timeoutId = window.setTimeout(() => { const autoApproval: UserApproval = { planId: response.id, approved: true }; handleApproval(autoApproval); }, this.config.autoApproveTimeout); } }); } catch (error) { console.error('Error handling tool preview:', error); return null; } } /** * Start tool execution tracking */ public startToolExecution( executionId: string, toolName: string, displayName?: string ): void { if (!this.config.enableFeedback || !this.feedbackUI) { return; } this.activeExecutions.add(executionId); this.feedbackUI.startExecution(executionId, toolName, displayName); } /** * Update tool execution progress */ public updateToolProgress(data: ToolProgressData): void { if (!this.config.enableFeedback || !this.feedbackUI) { return; } this.feedbackUI.updateProgress(data); } /** * Add tool execution step */ public addToolStep(data: ToolStepData): void { if (!this.config.enableFeedback || !this.feedbackUI) { return; } this.feedbackUI.addStep(data); } /** * Complete tool execution */ public completeToolExecution( executionId: string, status: 'success' | 'error' | 'cancelled' | 'timeout', result?: any, error?: string ): void { if (!this.config.enableFeedback || !this.feedbackUI) { return; } this.activeExecutions.delete(executionId); this.feedbackUI.completeExecution(executionId, status, result, error); // Refresh statistics if (this.config.showStatistics) { setTimeout(() => this.loadStatistics(), 1000); } } /** * Cancel tool execution */ public async cancelToolExecution(executionId: string, reason?: string): Promise { try { const response = await server.post(`api/llm-tools/executions/${executionId}/cancel`, { reason }); if (response?.success) { this.completeToolExecution(executionId, 'cancelled', undefined, reason); return true; } } catch (error) { console.error('Failed to cancel execution:', error); } return false; } /** * Load active executions */ private async loadActiveExecutions(): Promise { if (!this.config.enableFeedback) { return; } try { const executions = await server.get('api/llm-tools/executions/active'); if (executions && Array.isArray(executions)) { executions.forEach(exec => { if (!this.activeExecutions.has(exec.id)) { this.startToolExecution(exec.id, exec.toolName); // Restore progress if available if (exec.progress) { this.updateToolProgress({ executionId: exec.id, ...exec.progress }); } } }); } } catch (error) { console.error('Failed to load active executions:', error); } } /** * Load execution statistics */ private async loadStatistics(): Promise { if (!this.config.showStatistics) { return; } try { const stats = await server.get('api/llm-tools/executions/stats'); if (stats) { this.displayStatistics(stats); } } catch (error) { console.error('Failed to load statistics:', error); } } /** * Display statistics */ private displayStatistics(stats: any): void { const container = this.container.querySelector('.tool-stats-area') as HTMLElement; if (!container) return; container.innerHTML = `
${stats.totalExecutions}
Total
${stats.successfulExecutions}
Success
${stats.failedExecutions}
Failed
${this.formatDuration(stats.averageDuration)}
Avg Time
`; // Add tool-specific statistics if available if (stats.toolStatistics && Object.keys(stats.toolStatistics).length > 0) { const toolStatsHtml = Object.entries(stats.toolStatistics) .map(([toolName, toolStats]: [string, any]) => ` ${toolName} ${toolStats.count} ${toolStats.successRate}% ${this.formatDuration(toolStats.averageDuration)} `).join(''); container.innerHTML += `
Per-Tool Statistics
${toolStatsHtml}
Tool Count Success Avg Time
`; } } /** * Load circuit breaker status */ private async loadCircuitBreakerStatus(): Promise { try { const statuses = await server.get('api/llm-tools/circuit-breakers'); if (statuses && Array.isArray(statuses)) { this.displayCircuitBreakerStatus(statuses); } } catch (error) { console.error('Failed to load circuit breaker status:', error); } } /** * Display circuit breaker status */ private displayCircuitBreakerStatus(statuses: any[]): void { const openBreakers = statuses.filter(s => s.state === 'open'); const halfOpenBreakers = statuses.filter(s => s.state === 'half_open'); if (openBreakers.length > 0 || halfOpenBreakers.length > 0) { const alertContainer = document.createElement('div'); alertContainer.className = 'circuit-breaker-alerts mb-3'; if (openBreakers.length > 0) { alertContainer.innerHTML += `
Circuit Breakers Open: ${openBreakers.map(b => b.toolName).join(', ')}
`; } if (halfOpenBreakers.length > 0) { alertContainer.innerHTML += `
Circuit Breakers Half-Open: ${halfOpenBreakers.map(b => b.toolName).join(', ')}
`; } // Add to container const existingAlerts = this.container.querySelector('.circuit-breaker-alerts'); if (existingAlerts) { existingAlerts.replaceWith(alertContainer); } else { this.container.insertBefore(alertContainer, this.container.firstChild); } // Add reset handler const resetBtn = alertContainer.querySelector('.reset-breakers-btn'); resetBtn?.addEventListener('click', () => this.resetAllCircuitBreakers(openBreakers)); } } /** * Reset all circuit breakers */ private async resetAllCircuitBreakers(breakers: any[]): Promise { for (const breaker of breakers) { try { await server.post(`api/llm-tools/circuit-breakers/${breaker.toolName}/reset`, {}); } catch (error) { console.error(`Failed to reset circuit breaker for ${breaker.toolName}:`, error); } } // Reload status this.loadCircuitBreakerStatus(); } /** * Format duration */ private formatDuration(milliseconds: number): string { if (!milliseconds || milliseconds === 0) return '0ms'; if (milliseconds < 1000) { return `${Math.round(milliseconds)}ms`; } else if (milliseconds < 60000) { return `${(milliseconds / 1000).toFixed(1)}s`; } else { const minutes = Math.floor(milliseconds / 60000); const seconds = Math.floor((milliseconds % 60000) / 1000); return `${minutes}m ${seconds}s`; } } /** * Clean up resources */ public dispose(): void { this.eventHandlers.clear(); this.activeExecutions.clear(); if (this.feedbackUI) { this.feedbackUI.clear(); } } } /** * Create enhanced tool integration */ export function createEnhancedToolIntegration( container: HTMLElement, config?: Partial ): EnhancedToolIntegration { return new EnhancedToolIntegration(container, config); }