Skip to main content
Glama
beam-search.js23.8 kB
/** * Beam Search Pattern Handler Implementation * * Maintains multiple promising paths simultaneously, exploring them * in parallel with periodic evaluation and pruning. */ import { v4 as uuidv4 } from 'uuid'; export class BeamSearchHandler { defaultConfig = { expansionStrategy: 'breadth', branchingFactor: 3, scoreAggregation: 'average', allowMerging: true, mergeThreshold: 0.8, pruningStrategy: 'relative', keepPrunedPaths: true, diversityBonus: 0.1 }; defaultConvergenceCriteria = { maxGenerations: 10, minScoreImprovement: 0.01, consensusThreshold: 0.7, targetScore: 0.9, timeLimit: 60000 }; /** * Initialize a new Beam Search session */ initializeSession(beamWidth = 5, config) { const sessionId = uuidv4(); const timestamp = new Date().toISOString(); // Create initial path const initialPathId = uuidv4(); const initialPath = { id: initialPathId, nodeIds: [], currentScore: 0, status: 'active', metadata: { createdAt: timestamp, updatedAt: timestamp, scoreHistory: [0] } }; return { sessionId, patternType: 'beam', iteration: 0, nextStepNeeded: true, createdAt: timestamp, updatedAt: timestamp, beamWidth, currentGeneration: 0, nodes: new Map(), paths: new Map([[initialPathId, initialPath]]), activePaths: [initialPathId], evaluationFunction: 'default', convergenceCriteria: this.defaultConvergenceCriteria, config: { ...this.defaultConfig, ...config }, stats: { totalPaths: 1, activePaths: 1, completedPaths: 0, prunedPaths: 0, mergedPaths: 0, nodesExplored: 0, averagePathLength: 0, bestScore: 0, bestScoreGeneration: 0 } }; } /** * Generate next generation of nodes */ generateNextGeneration(session) { const newNodes = []; const generation = ++session.currentGeneration; // Expand each active path session.activePaths.forEach(pathId => { const path = session.paths.get(pathId); if (path && path.status === 'active') { const expandedNodes = this.expandPath(pathId, session); newNodes.push(...expandedNodes); } }); session.stats.nodesExplored += newNodes.length; return newNodes; } /** * Expand a specific path */ expandPath(pathId, session) { const path = session.paths.get(pathId); if (!path) return []; const newNodes = []; const newPaths = []; // Generate children based on branching factor for (let i = 0; i < session.config.branchingFactor; i++) { const nodeId = uuidv4(); const node = { id: nodeId, content: `Generation ${session.currentGeneration}, Branch ${i + 1}`, timestamp: new Date().toISOString(), pathIds: [], generationNumber: session.currentGeneration, localScore: Math.random(), // Simple random score for demo cumulativeScore: path.currentScore, metadata: { generationTime: Date.now() } }; // Create new path for this expansion const newPathId = uuidv4(); const newPath = { id: newPathId, nodeIds: [...path.nodeIds, nodeId], currentScore: 0, // Will be updated by evaluation status: 'active', metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), scoreHistory: [...(path.metadata?.scoreHistory || []), 0] } }; node.pathIds.push(newPathId); session.nodes.set(nodeId, node); session.paths.set(newPathId, newPath); newNodes.push(node); newPaths.push(newPath); } // Mark original path as completed if it was expanded if (newPaths.length > 0) { path.status = 'completed'; session.stats.completedPaths++; session.stats.activePaths--; } // Add new paths to active paths (will be pruned later) session.activePaths.push(...newPaths.map(p => p.id)); session.stats.totalPaths += newPaths.length; session.stats.activePaths += newPaths.length; return newNodes; } /** * Evaluate and score paths */ evaluatePaths(session) { const scores = new Map(); session.activePaths.forEach(pathId => { const path = session.paths.get(pathId); if (path && path.status === 'active') { const score = this.calculatePathScore(path, session); scores.set(pathId, score); path.currentScore = score; // Update score history if (path.metadata) { path.metadata.scoreHistory?.push(score); path.metadata.updatedAt = new Date().toISOString(); } // Update best score if (score > session.stats.bestScore) { session.stats.bestScore = score; session.stats.bestScoreGeneration = session.currentGeneration; } } }); return scores; } /** * Calculate score for a path */ calculatePathScore(path, session) { const components = { baseScore: 0, finalScore: 0 }; // Base score from node scores let totalScore = 0; let count = 0; path.nodeIds.forEach(nodeId => { const node = session.nodes.get(nodeId); if (node) { totalScore += node.localScore; count++; } }); switch (session.config.scoreAggregation) { case 'sum': components.baseScore = totalScore; break; case 'average': components.baseScore = count > 0 ? totalScore / count : 0; break; case 'max': components.baseScore = path.nodeIds.reduce((max, nodeId) => { const node = session.nodes.get(nodeId); return node ? Math.max(max, node.localScore) : max; }, 0); break; case 'weighted': // Weight recent nodes more heavily let weightedSum = 0; let weightSum = 0; path.nodeIds.forEach((nodeId, index) => { const node = session.nodes.get(nodeId); if (node) { const weight = Math.pow(0.9, path.nodeIds.length - index - 1); weightedSum += node.localScore * weight; weightSum += weight; } }); components.baseScore = weightSum > 0 ? weightedSum / weightSum : 0; break; } // Apply diversity bonus if (session.config.diversityBonus) { const diversity = this.calculatePathDiversity(path, session); components.diversityBonus = diversity * session.config.diversityBonus; } // Apply length adjustment const lengthPenalty = Math.max(0, 1 - (path.nodeIds.length / 20)); components.lengthAdjustment = lengthPenalty * 0.1; // Calculate final score components.finalScore = components.baseScore + (components.diversityBonus || 0) + (components.lengthAdjustment || 0); return components.finalScore; } /** * Calculate diversity of a path compared to other active paths */ calculatePathDiversity(path, session) { if (session.activePaths.length <= 1) return 1; let totalSimilarity = 0; let comparisons = 0; session.activePaths.forEach(otherPathId => { if (otherPathId !== path.id) { const otherPath = session.paths.get(otherPathId); if (otherPath) { const similarity = this.calculatePathSimilarity(path, otherPath); totalSimilarity += similarity; comparisons++; } } }); return comparisons > 0 ? 1 - (totalSimilarity / comparisons) : 1; } /** * Calculate similarity between two paths */ calculatePathSimilarity(path1, path2) { const set1 = new Set(path1.nodeIds); const set2 = new Set(path2.nodeIds); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return union.size > 0 ? intersection.size / union.size : 0; } /** * Prune paths to maintain beam width */ prunePaths(session) { const prunedPathIds = []; // Only prune if we exceed beam width if (session.activePaths.length <= session.beamWidth) { return prunedPathIds; } // Score all active paths const scores = this.evaluatePaths(session); // Sort paths by score const sortedPaths = session.activePaths .map(pathId => ({ pathId, score: scores.get(pathId) || 0 })) .sort((a, b) => b.score - a.score); // Determine pruning threshold based on strategy let threshold; switch (session.config.pruningStrategy) { case 'absolute': threshold = 0.3; // Fixed threshold break; case 'relative': // Keep top beam_width paths if (sortedPaths.length > session.beamWidth) { threshold = sortedPaths[session.beamWidth].score; } else { threshold = 0; } break; case 'adaptive': // Adaptive threshold based on score distribution const avgScore = sortedPaths.reduce((sum, p) => sum + p.score, 0) / sortedPaths.length; threshold = avgScore * 0.8; break; default: threshold = 0; } // Prune paths below threshold const newActivePaths = []; sortedPaths.forEach((pathData, index) => { if (index < session.beamWidth && pathData.score >= threshold) { newActivePaths.push(pathData.pathId); } else { const path = session.paths.get(pathData.pathId); if (path) { path.status = 'pruned'; if (path.metadata) { path.metadata.pruningGeneration = session.currentGeneration; path.metadata.pruningReason = `Score ${pathData.score} below threshold ${threshold}`; } prunedPathIds.push(pathData.pathId); // Remove from paths if not keeping pruned if (!session.config.keepPrunedPaths) { session.paths.delete(pathData.pathId); } } } }); session.activePaths = newActivePaths; session.stats.prunedPaths += prunedPathIds.length; session.stats.activePaths = newActivePaths.length; return prunedPathIds; } /** * Check for convergence */ checkConvergence(session) { const criteria = session.convergenceCriteria; if (!criteria) return false; // Check max generations if (criteria.maxGenerations && session.currentGeneration >= criteria.maxGenerations) { return true; } // Check target score if (criteria.targetScore && session.stats.bestScore >= criteria.targetScore) { return true; } // Check consensus if (criteria.consensusThreshold && session.activePaths.length > 0) { const consensus = this.calculateConsensus(session); if (consensus >= criteria.consensusThreshold) { return true; } } // Check score improvement if (criteria.minScoreImprovement && session.currentGeneration > 2) { const improvement = this.calculateScoreImprovement(session); if (improvement < criteria.minScoreImprovement) { return true; } } return false; } /** * Calculate consensus among active paths */ calculateConsensus(session) { if (session.activePaths.length <= 1) return 1; // Check if paths are converging to similar solutions const paths = session.activePaths.map(id => session.paths.get(id)).filter(Boolean); let totalSimilarity = 0; let comparisons = 0; for (let i = 0; i < paths.length; i++) { for (let j = i + 1; j < paths.length; j++) { totalSimilarity += this.calculatePathSimilarity(paths[i], paths[j]); comparisons++; } } return comparisons > 0 ? totalSimilarity / comparisons : 0; } /** * Calculate score improvement over recent generations */ calculateScoreImprovement(session) { const recentGenerations = 3; const startGen = Math.max(0, session.currentGeneration - recentGenerations); // Get best scores from recent generations const recentScores = []; session.paths.forEach(path => { if (path.metadata?.scoreHistory) { const history = path.metadata.scoreHistory; for (let i = startGen; i <= session.currentGeneration && i < history.length; i++) { recentScores.push(history[i]); } } }); if (recentScores.length < 2) return 1; // Calculate improvement const oldMax = Math.max(...recentScores.slice(0, Math.floor(recentScores.length / 2))); const newMax = Math.max(...recentScores.slice(Math.floor(recentScores.length / 2))); return oldMax > 0 ? (newMax - oldMax) / oldMax : 0; } /** * Merge similar paths */ mergePaths(pathIds, session) { if (pathIds.length < 2) { throw new Error('Need at least 2 paths to merge'); } const paths = pathIds.map(id => session.paths.get(id)).filter(Boolean); if (paths.length !== pathIds.length) { throw new Error('Some paths not found'); } // Create merged path const mergedPathId = uuidv4(); const mergedNodeIds = this.mergeNodeSequences(paths.map(p => p.nodeIds)); const avgScore = paths.reduce((sum, p) => sum + p.currentScore, 0) / paths.length; const mergedPath = { id: mergedPathId, nodeIds: mergedNodeIds, currentScore: avgScore, status: 'active', metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), scoreHistory: [avgScore] } }; // Update nodes to reference merged path mergedNodeIds.forEach(nodeId => { const node = session.nodes.get(nodeId); if (node) { node.pathIds.push(mergedPathId); } }); // Mark original paths as merged pathIds.forEach(pathId => { const path = session.paths.get(pathId); if (path) { path.status = 'merged'; if (path.metadata) { path.metadata.mergedIntoPathId = mergedPathId; } // Remove from active paths const index = session.activePaths.indexOf(pathId); if (index !== -1) { session.activePaths.splice(index, 1); } } }); // Add merged path session.paths.set(mergedPathId, mergedPath); session.activePaths.push(mergedPathId); session.stats.mergedPaths += pathIds.length; return mergedPath; } /** * Merge node sequences from multiple paths */ mergeNodeSequences(sequences) { if (sequences.length === 0) return []; if (sequences.length === 1) return sequences[0]; // Simple merge: take consensus at each position const maxLength = Math.max(...sequences.map(s => s.length)); const merged = []; for (let i = 0; i < maxLength; i++) { const nodesAtPosition = sequences .filter(seq => seq.length > i) .map(seq => seq[i]); if (nodesAtPosition.length > 0) { // Take most common node at this position const counts = new Map(); nodesAtPosition.forEach(node => { counts.set(node, (counts.get(node) || 0) + 1); }); let maxCount = 0; let mostCommon = nodesAtPosition[0]; counts.forEach((count, node) => { if (count > maxCount) { maxCount = count; mostCommon = node; } }); merged.push(mostCommon); } } return merged; } /** * Get best path */ getBestPath(session) { if (session.paths.size === 0) return null; let bestPath = null; let bestScore = -Infinity; session.paths.forEach(path => { if (path.currentScore > bestScore) { bestScore = path.currentScore; bestPath = path; } }); return bestPath; } /** * Calculate diversity across all active paths */ calculateDiversity(session) { if (session.activePaths.length <= 1) return 0; const paths = session.activePaths.map(id => session.paths.get(id)).filter(Boolean); let totalDiversity = 0; let comparisons = 0; for (let i = 0; i < paths.length; i++) { for (let j = i + 1; j < paths.length; j++) { totalDiversity += 1 - this.calculatePathSimilarity(paths[i], paths[j]); comparisons++; } } return comparisons > 0 ? totalDiversity / comparisons : 0; } /** * Export to sequential thinking format */ exportToSequentialFormat(session) { const bestPath = this.getBestPath(session); if (!bestPath) return []; const thoughts = []; bestPath.nodeIds.forEach((nodeId, index) => { const node = session.nodes.get(nodeId); if (node) { thoughts.push({ thought: node.content, thoughtNumber: index + 1, totalThoughts: bestPath.nodeIds.length, nextThoughtNeeded: index < bestPath.nodeIds.length - 1 }); } }); return thoughts; } /** * Import from sequential thinking format */ importFromSequentialFormat(thoughts) { const session = this.initializeSession(); // Create a single path from the thoughts const pathId = uuidv4(); const path = { id: pathId, nodeIds: [], currentScore: 0, status: 'active', metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), scoreHistory: [] } }; thoughts.forEach((thought, index) => { const nodeId = uuidv4(); const node = { id: nodeId, content: thought.thought, timestamp: new Date().toISOString(), pathIds: [pathId], generationNumber: index, localScore: 0.5 }; session.nodes.set(nodeId, node); path.nodeIds.push(nodeId); }); session.paths.set(pathId, path); session.activePaths = [pathId]; return session; } /** * Run one iteration of beam search */ runIteration(session) { // Generate next generation this.generateNextGeneration(session); // Evaluate paths this.evaluatePaths(session); // Prune to maintain beam width this.prunePaths(session); // Check for similar paths to merge if (session.config.allowMerging) { this.attemptPathMerging(session); } // Update statistics this.updateStatistics(session); // Check convergence if (this.checkConvergence(session)) { session.nextStepNeeded = false; } session.iteration++; session.updatedAt = new Date().toISOString(); } /** * Attempt to merge similar paths */ attemptPathMerging(session) { if (!session.config.mergeThreshold) return; const paths = session.activePaths.map(id => session.paths.get(id)).filter(Boolean); const merged = new Set(); for (let i = 0; i < paths.length; i++) { if (merged.has(paths[i].id)) continue; const similarPaths = [paths[i].id]; for (let j = i + 1; j < paths.length; j++) { if (merged.has(paths[j].id)) continue; const similarity = this.calculatePathSimilarity(paths[i], paths[j]); if (similarity >= session.config.mergeThreshold) { similarPaths.push(paths[j].id); merged.add(paths[j].id); } } if (similarPaths.length > 1) { this.mergePaths(similarPaths, session); similarPaths.forEach(id => merged.add(id)); } } } /** * Update session statistics */ updateStatistics(session) { // Calculate average path length let totalLength = 0; let pathCount = 0; session.paths.forEach(path => { if (path.status === 'active' || path.status === 'completed') { totalLength += path.nodeIds.length; pathCount++; } }); session.stats.averagePathLength = pathCount > 0 ? totalLength / pathCount : 0; // Calculate score variance const scores = session.activePaths.map(id => { const path = session.paths.get(id); return path ? path.currentScore : 0; }); if (scores.length > 0) { const mean = scores.reduce((sum, s) => sum + s, 0) / scores.length; const variance = scores.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / scores.length; session.stats.scoreVariance = variance; } } } export default BeamSearchHandler; //# sourceMappingURL=beam-search.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/waldzellai/clearthought-onepointfive'

If you have feedback or need assistance with the MCP directory API, please join our Discord server