/** * Contextual Thinking Tool * * Provides a way for the LLM agent to expose its reasoning process to the user, * showing how it explores knowledge and reaches conclusions. This makes the * agent's thinking more transparent and allows users to understand the context * behind answers. * * Features: * - Capture and structure the agent's thinking steps * - Visualize reasoning chains for complex queries * - Expose confidence levels for different assertions * - Show how different sources of evidence are weighed */ import log from "../../log.js"; import aiServiceManager from "../ai_service_manager.js"; import { AGENT_TOOL_PROMPTS } from '../constants/llm_prompt_constants.js'; /** * Represents a single reasoning step taken by the agent */ export interface ThinkingStep { id: string; content: string; type: 'observation' | 'hypothesis' | 'question' | 'evidence' | 'conclusion'; confidence?: number; sources?: string[]; parentId?: string; children?: string[]; metadata?: Record; } /** * Contains the full reasoning process */ export interface ThinkingProcess { id: string; query: string; steps: ThinkingStep[]; status: 'in_progress' | 'completed'; startTime: number; endTime?: number; } export class ContextualThinkingTool { private static thinkingCounter = 0; private static stepCounter = 0; private activeProcId?: string; private processes: Record = {}; /** * Start a new thinking process for a query * * @param query The user's query * @returns The created thinking process ID */ startThinking(query: string): string { const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`); this.processes[thinkingId] = { id: thinkingId, query, steps: [], status: 'in_progress', startTime: Date.now() }; // Set as active process this.activeProcId = thinkingId; // Initialize with some starter thinking steps this.addThinkingStep(thinkingId, { type: 'observation', content: AGENT_TOOL_PROMPTS.CONTEXTUAL_THINKING.STARTING_ANALYSIS(query) }); this.addThinkingStep(thinkingId, { type: 'question', content: AGENT_TOOL_PROMPTS.CONTEXTUAL_THINKING.KEY_COMPONENTS }); this.addThinkingStep(thinkingId, { type: 'observation', content: AGENT_TOOL_PROMPTS.CONTEXTUAL_THINKING.BREAKING_DOWN }); return thinkingId; } /** * Add a thinking step to a process * * @param processId The ID of the process to add to * @param step The thinking step to add * @returns The ID of the added step */ addThinkingStep( processId: string, step: Omit, parentId?: string ): string { const process = this.processes[processId]; if (!process) { throw new Error(`Thinking process ${processId} not found`); } // Create full step with ID const fullStep: ThinkingStep = { id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`, ...step, parentId }; // Add to process steps process.steps.push(fullStep); // If this step has a parent, update the parent's children list if (parentId) { const parentStep = process.steps.find(s => s.id === parentId); if (parentStep) { if (!parentStep.children) { parentStep.children = []; } parentStep.children.push(fullStep.id); } } // Log the step addition with more detail log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`); return fullStep.id; } /** * Complete the current thinking process * * @param processId The ID of the process to complete (defaults to active process) * @returns The completed thinking process */ completeThinking(processId?: string): ThinkingProcess | null { const id = processId || this.activeProcId; if (!id || !this.processes[id]) { log.error(`Thinking process ${id} not found`); return null; } this.processes[id].status = 'completed'; this.processes[id].endTime = Date.now(); if (id === this.activeProcId) { this.activeProcId = undefined; } return this.processes[id]; } /** * Get a thinking process by ID */ getThinkingProcess(processId: string): ThinkingProcess | null { return this.processes[processId] || null; } /** * Get the active thinking process */ getActiveThinkingProcess(): ThinkingProcess | null { if (!this.activeProcId) return null; return this.processes[this.activeProcId] || null; } /** * Visualize the thinking process as HTML for display in the UI * * @param thinkingId The ID of the thinking process to visualize * @returns HTML representation of the thinking process */ visualizeThinking(thinkingId: string): string { log.info(`Visualizing thinking process: thinkingId=${thinkingId}`); const process = this.getThinkingProcess(thinkingId); if (!process) { log.info(`No thinking process found for id: ${thinkingId}`); return "
No thinking process found
"; } log.info(`Found thinking process with ${process.steps.length} steps for query: "${process.query.substring(0, 50)}..."`); let html = "
"; html += `

Reasoning Process

`; html += `
${process.query}
`; // Show overall time taken for the thinking process const duration = process.endTime ? Math.round((process.endTime - process.startTime) / 1000) : Math.round((Date.now() - process.startTime) / 1000); html += `
Analysis took ${duration} seconds
`; // Create a more structured visualization with indentation for parent-child relationships const renderStep = (step: ThinkingStep, level: number = 0) => { const indent = level * 20; // 20px indentation per level let stepHtml = `
`; // Add an icon based on step type const icon = this.getStepIcon(step.type); stepHtml += ` `; // Add the step content stepHtml += step.content; // Show confidence if available if (step.metadata?.confidence) { const confidence = Math.round((step.metadata.confidence as number) * 100); stepHtml += ` (Confidence: ${confidence}%)`; } // Show sources if available if (step.sources && step.sources.length > 0) { stepHtml += `
Sources: ${step.sources.join(', ')}
`; } stepHtml += `
`; return stepHtml; }; // Helper function to render a step and all its children recursively const renderStepWithChildren = (stepId: string, level: number = 0) => { const step = process.steps.find(s => s.id === stepId); if (!step) return ''; let html = renderStep(step, level); if (step.children && step.children.length > 0) { for (const childId of step.children) { html += renderStepWithChildren(childId, level + 1); } } return html; }; // Render top-level steps and their children const topLevelSteps = process.steps.filter(s => !s.parentId); for (const step of topLevelSteps) { html += renderStep(step); if (step.children && step.children.length > 0) { for (const childId of step.children) { html += renderStepWithChildren(childId, 1); } } } html += "
"; return html; } /** * Get an appropriate icon for a thinking step type */ private getStepIcon(type: string): string { switch (type) { case 'observation': return 'bx-search'; case 'hypothesis': return 'bx-bulb'; case 'evidence': return 'bx-list-check'; case 'conclusion': return 'bx-check-circle'; default: return 'bx-message-square-dots'; } } /** * Get a plain text summary of the thinking process * * @param thinkingId The ID of the thinking process to summarize * @returns Text summary of the thinking process */ getThinkingSummary(thinkingId: string): string { const process = this.getThinkingProcess(thinkingId); if (!process) { log.error(`No thinking process found for id: ${thinkingId}`); return "No thinking process available."; } let summary = `## Reasoning Process for Query: "${process.query}"\n\n`; // Group steps by type for better organization const observations = process.steps.filter(s => s.type === 'observation'); const questions = process.steps.filter(s => s.type === 'question'); const hypotheses = process.steps.filter(s => s.type === 'hypothesis'); const evidence = process.steps.filter(s => s.type === 'evidence'); const conclusions = process.steps.filter(s => s.type === 'conclusion'); log.info(`Generating thinking summary with: ${observations.length} observations, ${questions.length} questions, ${hypotheses.length} hypotheses, ${evidence.length} evidence, ${conclusions.length} conclusions`); // Add observations if (observations.length > 0) { summary += "### Observations:\n"; observations.forEach(step => { summary += `- ${step.content}\n`; }); summary += "\n"; } // Add questions if (questions.length > 0) { summary += "### Questions Considered:\n"; questions.forEach(step => { summary += `- ${step.content}\n`; }); summary += "\n"; } // Add hypotheses if (hypotheses.length > 0) { summary += "### Hypotheses:\n"; hypotheses.forEach(step => { summary += `- ${step.content}\n`; }); summary += "\n"; } // Add evidence if (evidence.length > 0) { summary += "### Evidence Gathered:\n"; evidence.forEach(step => { summary += `- ${step.content}\n`; }); summary += "\n"; } // Add conclusions if (conclusions.length > 0) { summary += "### Conclusions:\n"; conclusions.forEach(step => { summary += `- ${step.content}\n`; }); summary += "\n"; } log.info(`Generated thinking summary with ${summary.length} characters`); return summary; } /** * Reset the active thinking process */ resetActiveThinking(): void { this.activeProcId = undefined; } /** * Generate a unique ID for a thinking process */ private generateProcessId(): string { return `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; } /** * Generate a unique ID for a thinking step */ private generateStepId(): string { return `step_${Date.now()}_${ContextualThinkingTool.stepCounter++}`; } /** * Format duration between two timestamps */ private formatDuration(start: number, end: number): string { const durationMs = end - start; if (durationMs < 1000) { return `${durationMs}ms`; } else if (durationMs < 60000) { return `${Math.round(durationMs / 1000)}s`; } else { return `${Math.round(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`; } } /** * Recursively render a step and its children */ private renderStepTree(step: ThinkingStep, allSteps: ThinkingStep[]): string { const typeIcons: Record = { 'observation': '🔍', 'hypothesis': '🤔', 'question': '❓', 'evidence': '📋', 'conclusion': '✅' }; const icon = typeIcons[step.type] || '•'; const confidenceDisplay = step.confidence !== undefined ? `${Math.round(step.confidence * 100)}%` : ''; let html = `
${icon} ${step.type} ${confidenceDisplay}
${step.content}
`; // Add sources if available if (step.sources && step.sources.length > 0) { html += `
Sources: ${step.sources.join(', ')}
`; } // Recursively render children if (step.children && step.children.length > 0) { html += `
`; for (const childId of step.children) { const childStep = allSteps.find(s => s.id === childId); if (childStep) { html += this.renderStepTree(childStep, allSteps); } } html += `
`; } html += `
`; return html; } } export default ContextualThinkingTool;