/**
 * RAG-based Tool Selection Module
 *
 * Implements semantic tool selection using TF-IDF and cosine similarity
 * to reduce prompt bloat and improve tool selection accuracy.
 *
 * Based on research from:
 * - RAG-MCP (arXiv:2505.03275)
 * - ToolLLM (ICLR'24)
 *
 * Key improvements:
 * - Reduces prompt tokens by ~50%
 * - Improves tool selection accuracy from ~13% to ~43%+ with many tools
 * - Adaptive thresholds based on success metrics
 * - LRU cache for repeated queries
 */
/**
 * Simple LRU Cache implementation
 */
class LRUCache {
    cache;
    maxSize;
    ttlMs;
    constructor(maxSize = 100, ttlMs = 5 * 60 * 1000) {
        this.cache = new Map();
        this.maxSize = maxSize;
        this.ttlMs = ttlMs;
    }
    get(key) {
        const entry = this.cache.get(key);
        if (!entry)
            return undefined;
        // Check TTL
        if (Date.now() - entry.timestamp > this.ttlMs) {
            this.cache.delete(key);
            return undefined;
        }
        // Update access count and move to end (most recently used)
        entry.accessCount++;
        this.cache.delete(key);
        this.cache.set(key, entry);
        return entry.value;
    }
    set(key, value) {
        // Remove oldest if at capacity
        if (this.cache.size >= this.maxSize) {
            const firstKey = this.cache.keys().next().value;
            if (firstKey !== undefined) {
                this.cache.delete(firstKey);
            }
        }
        this.cache.set(key, {
            value,
            timestamp: Date.now(),
            accessCount: 1
        });
    }
    has(key) {
        return this.get(key) !== undefined;
    }
    clear() {
        this.cache.clear();
    }
    get size() {
        return this.cache.size;
    }
    getStats() {
        return {
            size: this.cache.size,
            hitRate: 0 // Would need to track hits/misses
        };
    }
}
import { TOOL_METADATA, CATEGORY_KEYWORDS } from "./metadata.js";
/**
 * RAG-based Tool Selector.
 *
 * Selects the most relevant tools for a given query to reduce context window usage
 * and improve LLM adherence to tool definitions.
 *
 * Uses a hybrid approach:
 * 1. **TF-IDF Scoring**: Matches query terms against tool keywords and descriptions.
 * 2. **Category Classification**: Classifies intent (e.g., "edit file" -> `file_write`) and boosts relevant tools.
 * 3. **Adaptive Thresholding**: Adjusts inclusion threshold based on success metrics to balance precision/recall.
 *
 * Includes LRU caching for performance.
 */
export class ToolSelector {
    toolIndex;
    idfScores;
    documentFrequency;
    totalDocuments;
    // Metrics tracking
    metrics;
    requestHistory = [];
    maxHistorySize = 1000;
    // Adaptive threshold
    baseMinScore = 0.5;
    adaptiveMinScore = 0.5;
    adaptationRate = 0.1;
    // Classification cache
    classificationCache;
    selectionCache;
    constructor() {
        this.toolIndex = new Map();
        this.idfScores = new Map();
        this.documentFrequency = new Map();
        this.totalDocuments = TOOL_METADATA.length;
        // Initialize metrics
        this.metrics = {
            totalSelections: 0,
            successfulSelections: 0,
            missedTools: 0,
            missedToolNames: new Map(),
            successRate: 1.0,
            lastUpdated: new Date()
        };
        // Initialize caches
        this.classificationCache = new LRUCache(100, 5 * 60 * 1000);
        this.selectionCache = new LRUCache(50, 2 * 60 * 1000);
        this.buildIndex();
    }
    /**
     * Build the TF-IDF index from tool metadata
     */
    buildIndex() {
        // Register tools
        for (const metadata of TOOL_METADATA) {
            this.toolIndex.set(metadata.name, metadata);
        }
        // Calculate document frequency for each keyword
        for (const metadata of TOOL_METADATA) {
            const uniqueKeywords = new Set(metadata.keywords.map(k => k.toLowerCase()));
            for (const keyword of uniqueKeywords) {
                this.documentFrequency.set(keyword, (this.documentFrequency.get(keyword) || 0) + 1);
            }
        }
        // Calculate IDF scores
        for (const [keyword, df] of this.documentFrequency) {
            const idf = Math.log(this.totalDocuments / (df + 1)) + 1;
            this.idfScores.set(keyword, idf);
        }
    }
    /**
     * Tokenize and normalize a query string
     */
    tokenize(text) {
        return text
            .toLowerCase()
            .replace(/[^\w\s]/g, ' ')
            .split(/\s+/)
            .filter(token => token.length > 1);
    }
    /**
     * Calculate TF-IDF score for a query against a tool
     */
    calculateTFIDF(queryTokens, metadata) {
        const toolKeywords = new Set(metadata.keywords.map(k => k.toLowerCase()));
        let score = 0;
        // Calculate term frequency in query
        const queryTF = new Map();
        for (const token of queryTokens) {
            queryTF.set(token, (queryTF.get(token) || 0) + 1);
        }
        // Calculate TF-IDF score
        for (const [token, tf] of queryTF) {
            // Check exact match
            if (toolKeywords.has(token)) {
                const idf = this.idfScores.get(token) || 1;
                score += tf * idf * 2; // Boost exact matches
            }
            // Check partial match (substring)
            for (const keyword of toolKeywords) {
                if (keyword.includes(token) || token.includes(keyword)) {
                    const idf = this.idfScores.get(keyword) || 1;
                    score += tf * idf * 0.5; // Lower weight for partial matches
                }
            }
        }
        // Apply priority boost
        score *= (1 + metadata.priority * 0.1);
        return score;
    }
    /**
     * Classify a user query into tool categories (with caching)
     */
    classifyQuery(query) {
        // Check cache first
        const cacheKey = query.toLowerCase().trim();
        const cached = this.classificationCache.get(cacheKey);
        if (cached) {
            return cached;
        }
        const tokens = this.tokenize(query);
        const queryLower = query.toLowerCase();
        const categoryScores = new Map();
        const detectedKeywords = [];
        // Score each category
        for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
            let score = 0;
            for (const keyword of keywords) {
                if (queryLower.includes(keyword)) {
                    score += 2; // Phrase match
                    detectedKeywords.push(keyword);
                }
                // Check token overlap
                const keywordTokens = keyword.split(' ');
                for (const kt of keywordTokens) {
                    if (tokens.includes(kt)) {
                        score += 1;
                    }
                }
            }
            if (score > 0) {
                categoryScores.set(category, score);
            }
        }
        // Sort categories by score
        const sortedCategories = Array.from(categoryScores.entries())
            .sort((a, b) => b[1] - a[1])
            .map(([cat]) => cat);
        // Calculate confidence (based on score distribution)
        const maxScore = Math.max(...categoryScores.values(), 1);
        const confidence = Math.min(maxScore / 10, 1);
        // Detect if multiple tools might be needed
        const requiresMultipleTools = sortedCategories.length > 1 ||
            queryLower.includes(' and ') ||
            queryLower.includes(' then ') ||
            queryLower.includes(' after ');
        // If no categories detected, return defaults
        if (sortedCategories.length === 0) {
            const defaultResult = {
                categories: ['file_read', 'file_search', 'system'],
                confidence: 0.3,
                keywords: tokens,
                requiresMultipleTools: false
            };
            // Cache the default result too
            this.classificationCache.set(cacheKey, defaultResult);
            return defaultResult;
        }
        const result = {
            categories: sortedCategories.slice(0, 3), // Top 3 categories
            confidence,
            keywords: [...new Set(detectedKeywords)],
            requiresMultipleTools
        };
        // Cache the result
        this.classificationCache.set(cacheKey, result);
        return result;
    }
    /**
     * Select the most relevant tools for a given query.
     *
     * @param query - The user's natural language query
     * @param allTools - List of all available tools
     * @param options - Configuration options
     * @returns Selection result containing the filtered list of tools
     */
    selectTools(query, allTools, options = {}) {
        const { maxTools = 10, minScore = this.baseMinScore, includeCategories, excludeCategories, alwaysInclude = ['view_file', 'bash'], // Core tools always included
        useAdaptiveThreshold = true } = options;
        // Use adaptive threshold if enabled and we have enough data
        const effectiveMinScore = useAdaptiveThreshold && this.metrics.totalSelections > 10
            ? this.adaptiveMinScore
            : minScore;
        const classification = this.classifyQuery(query);
        const queryTokens = this.tokenize(query);
        const scores = new Map();
        // Create a map of tool name to CodeBuddyTool for quick lookup
        const toolMap = new Map();
        for (const tool of allTools) {
            toolMap.set(tool.function.name, tool);
        }
        // Score each tool
        for (const tool of allTools) {
            const toolName = tool.function.name;
            const metadata = this.toolIndex.get(toolName);
            if (metadata) {
                // Check category filters
                if (includeCategories && !includeCategories.includes(metadata.category)) {
                    continue;
                }
                if (excludeCategories && excludeCategories.includes(metadata.category)) {
                    continue;
                }
                // Calculate TF-IDF score
                let score = this.calculateTFIDF(queryTokens, metadata);
                // Boost if category matches classification
                if (classification.categories.includes(metadata.category)) {
                    const categoryRank = classification.categories.indexOf(metadata.category);
                    score *= (1 + (3 - categoryRank) * 0.3); // Higher boost for top categories
                }
                // Additional boost for always-include tools
                if (alwaysInclude.includes(toolName)) {
                    score = Math.max(score, effectiveMinScore + 0.1);
                }
                scores.set(toolName, score);
            }
            else {
                // MCP or unknown tool - use description-based scoring
                const descTokens = this.tokenize(tool.function.description);
                let score = 0;
                for (const token of queryTokens) {
                    if (descTokens.includes(token)) {
                        score += 1;
                    }
                }
                scores.set(toolName, score);
            }
        }
        // Sort tools by score
        const sortedTools = Array.from(scores.entries())
            .sort((a, b) => b[1] - a[1]);
        // Select top tools
        const selectedToolNames = [];
        // First, add always-include tools if they have any relevance
        for (const name of alwaysInclude) {
            if (toolMap.has(name)) {
                selectedToolNames.push(name);
            }
        }
        // Then add high-scoring tools
        for (const [name, score] of sortedTools) {
            if (selectedToolNames.length >= maxTools)
                break;
            if (score < effectiveMinScore && !alwaysInclude.includes(name))
                continue;
            if (!selectedToolNames.includes(name)) {
                selectedToolNames.push(name);
            }
        }
        // If we have very few tools, add some based on category
        if (selectedToolNames.length < 5) {
            for (const category of classification.categories) {
                const categoryTools = TOOL_METADATA
                    .filter(m => m.category === category)
                    .map(m => m.name);
                for (const toolName of categoryTools) {
                    if (selectedToolNames.length >= maxTools)
                        break;
                    if (toolMap.has(toolName) && !selectedToolNames.includes(toolName)) {
                        selectedToolNames.push(toolName);
                    }
                }
            }
        }
        // Build selected tools array
        const selectedTools = selectedToolNames
            .map(name => toolMap.get(name))
            .filter((t) => t !== undefined);
        // Calculate token savings (rough estimate)
        const originalTokens = this.estimateTokens(allTools);
        const reducedTokens = this.estimateTokens(selectedTools);
        return {
            selectedTools,
            scores,
            classification,
            reducedTokens,
            originalTokens
        };
    }
    /**
     * Estimate token count for tools (rough approximation)
     */
    estimateTokens(tools) {
        let tokens = 0;
        for (const tool of tools) {
            // Rough estimate: name + description + parameters
            tokens += tool.function.name.length / 4;
            tokens += tool.function.description.length / 4;
            tokens += JSON.stringify(tool.function.parameters).length / 4;
        }
        return Math.round(tokens);
    }
    /**
     * Get tool metadata by name
     */
    getToolMetadata(name) {
        return this.toolIndex.get(name);
    }
    /**
     * Register a new tool (for MCP tools)
     */
    registerTool(name, category, keywords, description, priority = 5) {
        const metadata = {
            name,
            category,
            keywords,
            priority,
            description
        };
        this.toolIndex.set(name, metadata);
        // Update document frequency and IDF
        this.totalDocuments++;
        const uniqueKeywords = new Set(keywords.map(k => k.toLowerCase()));
        for (const keyword of uniqueKeywords) {
            this.documentFrequency.set(keyword, (this.documentFrequency.get(keyword) || 0) + 1);
            const df = this.documentFrequency.get(keyword) || 1;
            const idf = Math.log(this.totalDocuments / (df + 1)) + 1;
            this.idfScores.set(keyword, idf);
        }
    }
    /**
     * Auto-register MCP tools by parsing their names and descriptions
     */
    registerMCPTool(tool) {
        const name = tool.function.name;
        const description = tool.function.description;
        // Extract keywords from name and description
        const keywords = [
            ...this.tokenize(name.replace(/^mcp__\w+__/, '')),
            ...this.tokenize(description).slice(0, 10)
        ];
        this.registerTool(name, 'mcp', keywords, description, 4);
    }
    // ============== METRICS TRACKING ==============
    /**
     * Record a tool request from the LLM
     *
     * Call this when the LLM requests a tool to track whether
     * our RAG selection correctly included it.
     *
     * @param requestedTool - The tool name requested by LLM
     * @param selectedTools - The tools that were selected by RAG
     * @param query - The original user query
     */
    recordToolRequest(requestedTool, selectedTools, query) {
        const wasSelected = selectedTools.includes(requestedTool);
        // Record the event
        const event = {
            requestedTool,
            selectedTools,
            query,
            wasSelected
        };
        // Add to history (bounded)
        this.requestHistory.push(event);
        if (this.requestHistory.length > this.maxHistorySize) {
            this.requestHistory.shift();
        }
        // Update metrics
        this.metrics.totalSelections++;
        if (wasSelected) {
            this.metrics.successfulSelections++;
        }
        else {
            this.metrics.missedTools++;
            const currentCount = this.metrics.missedToolNames.get(requestedTool) || 0;
            this.metrics.missedToolNames.set(requestedTool, currentCount + 1);
            // Adaptive threshold adjustment: lower threshold when we miss tools
            this.adaptiveMinScore = Math.max(0.1, this.adaptiveMinScore - this.adaptationRate);
        }
        // Recalculate success rate
        this.metrics.successRate = this.metrics.totalSelections > 0
            ? this.metrics.successfulSelections / this.metrics.totalSelections
            : 1.0;
        this.metrics.lastUpdated = new Date();
        // Adaptive threshold adjustment based on success rate
        if (this.metrics.totalSelections > 0 && this.metrics.totalSelections % 10 === 0) {
            this.adjustAdaptiveThreshold();
        }
    }
    /**
     * Adjust adaptive threshold based on recent performance
     */
    adjustAdaptiveThreshold() {
        const targetSuccessRate = 0.95; // Target 95% success rate
        if (this.metrics.successRate < targetSuccessRate) {
            // Lower threshold to include more tools
            this.adaptiveMinScore = Math.max(0.1, this.adaptiveMinScore - this.adaptationRate);
        }
        else if (this.metrics.successRate > 0.99 && this.adaptiveMinScore < this.baseMinScore) {
            // Raise threshold back towards base if we're doing very well
            this.adaptiveMinScore = Math.min(this.baseMinScore, this.adaptiveMinScore + this.adaptationRate * 0.5);
        }
    }
    /**
     * Get current metrics
     */
    getMetrics() {
        return { ...this.metrics };
    }
    /**
     * Get most frequently missed tools
     */
    getMostMissedTools(limit = 10) {
        return Array.from(this.metrics.missedToolNames.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, limit)
            .map(([tool, count]) => ({ tool, count }));
    }
    /**
     * Get recent request history
     */
    getRequestHistory(limit = 50) {
        return this.requestHistory.slice(-limit);
    }
    /**
     * Get current adaptive threshold
     */
    getAdaptiveThreshold() {
        return this.adaptiveMinScore;
    }
    /**
     * Manually set the adaptive threshold
     */
    setAdaptiveThreshold(threshold) {
        this.adaptiveMinScore = Math.max(0.1, Math.min(1.0, threshold));
    }
    /**
     * Reset metrics to initial state
     */
    resetMetrics() {
        this.metrics = {
            totalSelections: 0,
            successfulSelections: 0,
            missedTools: 0,
            missedToolNames: new Map(),
            successRate: 1.0,
            lastUpdated: new Date()
        };
        this.requestHistory = [];
        this.adaptiveMinScore = this.baseMinScore;
    }
    /**
     * Format metrics as a readable string
     */
    formatMetrics() {
        const metrics = this.metrics;
        const missedTools = this.getMostMissedTools(5);
        const lines = [
            '📈 Tool Selection Metrics',
            '─'.repeat(30),
            `Total Selections: ${metrics.totalSelections}`,
            `Successful: ${metrics.successfulSelections} (${(metrics.successRate * 100).toFixed(1)}%)`,
            `Missed: ${metrics.missedTools}`,
            `Adaptive Threshold: ${this.adaptiveMinScore.toFixed(2)} (base: ${this.baseMinScore})`,
            `Last Updated: ${metrics.lastUpdated.toLocaleString()}`,
        ];
        if (missedTools.length > 0) {
            lines.push('', 'Most Missed Tools:');
            missedTools.forEach(({ tool, count }) => {
                lines.push(`  • ${tool}: ${count} times`);
            });
        }
        return lines.join('\n');
    }
    // ============== CACHE MANAGEMENT ==============
    /**
     * Clear classification cache
     */
    clearClassificationCache() {
        this.classificationCache.clear();
    }
    /**
     * Clear selection cache
     */
    clearSelectionCache() {
        this.selectionCache.clear();
    }
    /**
     * Clear all caches
     */
    clearAllCaches() {
        this.classificationCache.clear();
        this.selectionCache.clear();
    }
    /**
     * Get cache statistics
     */
    getCacheStats() {
        return {
            classificationCache: { size: this.classificationCache.size },
            selectionCache: { size: this.selectionCache.size }
        };
    }
}
/**
 * Singleton instance
 */
let toolSelectorInstance = null;
export function getToolSelector() {
    if (!toolSelectorInstance) {
        toolSelectorInstance = new ToolSelector();
    }
    return toolSelectorInstance;
}
/**
 * Convenience function for tool selection
 */
export function selectRelevantTools(query, allTools, maxTools = 10) {
    return getToolSelector().selectTools(query, allTools, { maxTools });
}
/**
 * Record a tool request for metrics tracking
 */
export function recordToolRequest(requestedTool, selectedTools, query) {
    getToolSelector().recordToolRequest(requestedTool, selectedTools, query);
}
/**
 * Get tool selection metrics
 */
export function getToolSelectionMetrics() {
    return getToolSelector().getMetrics();
}
/**
 * Format metrics as string
 */
export function formatToolSelectionMetrics() {
    return getToolSelector().formatMetrics();
}
//# sourceMappingURL=tool-selector.js.map