Skip to main content
Glama
agentMatcher.ts10.5 kB
import { Task } from '../types/index.js'; import { Agent } from './agentLoader.js'; interface KeywordMapping { keywords: string[]; weight: number; } interface AgentKeywordMap { [agentType: string]: KeywordMapping; } // Define keyword mappings for common agent types const AGENT_KEYWORD_MAPPINGS: AgentKeywordMap = { frontend: { keywords: [ 'frontend', 'front-end', 'ui', 'ux', 'user interface', 'react', 'vue', 'angular', 'svelte', 'css', 'html', 'javascript', 'typescript', 'jsx', 'tsx', 'component', 'styling', 'layout', 'responsive', 'design', 'dom', 'browser', 'client-side', 'webpack', 'vite', 'sass', 'scss', 'tailwind', 'material-ui', 'bootstrap', 'animation', 'accessibility', 'a11y' ], weight: 1.0 }, backend: { keywords: [ 'backend', 'back-end', 'server', 'api', 'rest', 'graphql', 'node', 'nodejs', 'express', 'fastify', 'koa', 'python', 'django', 'flask', 'java', 'spring', 'ruby', 'rails', 'php', 'laravel', 'golang', 'go', 'rust', 'microservice', 'authentication', 'authorization', 'jwt', 'oauth', 'middleware', 'routing', 'controller', 'service', 'repository' ], weight: 1.0 }, database: { keywords: [ 'database', 'db', 'sql', 'nosql', 'mysql', 'postgresql', 'postgres', 'mongodb', 'redis', 'elasticsearch', 'cassandra', 'dynamodb', 'sqlite', 'oracle', 'mssql', 'schema', 'table', 'query', 'index', 'migration', 'orm', 'sequelize', 'typeorm', 'mongoose', 'prisma', 'knex', 'join', 'transaction', 'stored procedure', 'trigger', 'view', 'normalization' ], weight: 1.0 }, fullstack: { keywords: [ 'fullstack', 'full-stack', 'full stack', 'end-to-end', 'e2e', 'mern', 'mean', 'lamp', 'integration', 'architecture', 'system design', 'deployment', 'devops', 'docker', 'kubernetes', 'k8s', 'ci/cd', 'pipeline', 'aws', 'azure', 'gcp', 'cloud', 'serverless', 'lambda' ], weight: 1.2 // Slightly higher weight for fullstack as it's more comprehensive }, mobile: { keywords: [ 'mobile', 'ios', 'android', 'react native', 'flutter', 'swift', 'kotlin', 'objective-c', 'java', 'xamarin', 'ionic', 'cordova', 'native', 'hybrid', 'pwa', 'progressive web app', 'responsive', 'touch', 'gesture', 'notification', 'offline', 'app store', 'play store' ], weight: 1.0 }, testing: { keywords: [ 'test', 'testing', 'qa', 'quality assurance', 'unit test', 'integration test', 'e2e test', 'end-to-end test', 'jest', 'mocha', 'chai', 'cypress', 'selenium', 'playwright', 'puppeteer', 'tdd', 'bdd', 'coverage', 'mock', 'stub', 'spy', 'assertion', 'test suite', 'test case', 'regression', 'smoke test', 'load test' ], weight: 1.0 }, security: { keywords: [ 'security', 'secure', 'vulnerability', 'penetration', 'pentest', 'owasp', 'xss', 'csrf', 'sql injection', 'authentication', 'authorization', 'encryption', 'ssl', 'tls', 'https', 'certificate', 'token', 'session', 'cookie', 'cors', 'firewall', 'waf', 'ids', 'ips', 'audit', 'compliance', 'gdpr', 'pci' ], weight: 1.1 // Slightly higher weight for security as it's critical }, data: { keywords: [ 'data', 'analytics', 'analysis', 'ml', 'machine learning', 'ai', 'artificial intelligence', 'deep learning', 'neural network', 'tensorflow', 'pytorch', 'scikit-learn', 'pandas', 'numpy', 'jupyter', 'notebook', 'visualization', 'chart', 'graph', 'd3', 'plotly', 'tableau', 'powerbi', 'etl', 'pipeline', 'transformation', 'mining', 'science' ], weight: 1.0 } }; /** * Calculate the relevance score for a given text against a set of keywords */ function calculateKeywordScore(text: string, keywords: string[], weight: number, agentType?: string): number { const lowerText = text.toLowerCase(); let score = 0; let matchCount = 0; for (const keyword of keywords) { const lowerKeyword = keyword.toLowerCase(); // Check for whole word matches (with word boundaries) // Escape special regex characters in the keyword const escapedKeyword = lowerKeyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`\\b${escapedKeyword}\\b`, 'gi'); const matches = lowerText.match(regex); if (matches) { matchCount += matches.length; // Give higher score for exact matches vs partial matches let keywordScore = matches.length * weight; // Special bonus for primary keywords that strongly indicate the agent type if (agentType === 'testing') { // Check for testing-specific keywords and phrases if (['test', 'testing', 'unit test', 'unit tests', 'jest', 'mocha', 'qa', 'test suite'].includes(lowerKeyword)) { keywordScore *= 3; // Triple score for primary testing keywords } // Additional boost if the text contains "write" or "create" with "test" if (lowerText.includes('write') || lowerText.includes('create')) { keywordScore *= 1.5; } } else if (agentType === 'fullstack' && ['fullstack', 'full-stack', 'full stack'].includes(lowerKeyword)) { keywordScore *= 2; // Double score for primary fullstack keywords } score += keywordScore; } } // Bonus for multiple different keywords matched const uniqueMatchedKeywords = keywords.filter(keyword => { const escapedKeyword = keyword.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(`\\b${escapedKeyword}\\b`, 'i').test(lowerText); }).length; if (uniqueMatchedKeywords > 1) { score += (uniqueMatchedKeywords - 1) * 0.5 * weight; } return score; } /** * Find the best matching agent type based on task content */ function findBestAgentType(task: Task): { type: string; score: number } | undefined { const combinedText = `${task.name} ${task.description || ''} ${task.notes || ''} ${task.implementationGuide || ''}`; if (!combinedText.trim()) { return undefined; } const scores: Array<{ type: string; score: number }> = []; for (const [agentType, mapping] of Object.entries(AGENT_KEYWORD_MAPPINGS)) { const score = calculateKeywordScore(combinedText, mapping.keywords, mapping.weight, agentType); if (score > 0) { scores.push({ type: agentType, score }); } } if (scores.length === 0) { return undefined; } // Sort by score descending and return the best match scores.sort((a, b) => b.score - a.score); // Only return if the score is significant enough (threshold) const bestMatch = scores[0]; if (bestMatch.score >= 1.0) { return bestMatch; } return undefined; } /** * Match a task to the most suitable agent from available agents */ export function matchAgentToTask(task: Task, availableAgents: Agent[]): string | undefined { // Handle edge cases if (!task || !availableAgents || availableAgents.length === 0) { return undefined; } // Find the best agent type for this task const bestAgentType = findBestAgentType(task); if (!bestAgentType) { return undefined; } // Try to find an agent that matches the type // Look for agents with the type keyword in their name let matchedAgent = availableAgents.find(agent => agent.name.toLowerCase().includes(bestAgentType.type.toLowerCase()) ); if (matchedAgent) { return matchedAgent.name; } // If no exact type match, look for agents with matching keywords in their name const typeKeywords = AGENT_KEYWORD_MAPPINGS[bestAgentType.type]?.keywords || []; // Score all agents based on keyword matches in their names const agentScores = availableAgents.map(agent => { const agentNameLower = agent.name.toLowerCase(); let agentScore = 0; // Check each keyword against agent name for (const keyword of typeKeywords) { const escapedKeyword = keyword.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`\\b${escapedKeyword}\\b`, 'i'); if (regex.test(agentNameLower)) { agentScore += 2; // Higher weight for name matches } } return { agent, score: agentScore }; }).filter(item => item.score > 0); // Sort by score and return the best match if (agentScores.length > 0) { agentScores.sort((a, b) => b.score - a.score); return agentScores[0].agent.name; } // Check description if no name matches for (const agent of availableAgents) { // Check description if available if (agent.description) { const descriptionLower = agent.description.toLowerCase(); for (const keyword of typeKeywords) { const escapedKeyword = keyword.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`\\b${escapedKeyword}\\b`, 'i'); if (regex.test(descriptionLower)) { return agent.name; } } } } // If still no match but we have a general-purpose agent, use it const generalAgent = availableAgents.find(agent => agent.name.toLowerCase().includes('general') ); if (generalAgent && bestAgentType.score >= 2.0) { // Only use general agent if the task has a strong match to some domain return generalAgent.name; } return undefined; } /** * Get suggested agent type for a task (for UI display) */ export function getSuggestedAgentType(task: Task): string | undefined { const bestMatch = findBestAgentType(task); return bestMatch?.type; } /** * Get keyword match details for debugging/UI */ export function getKeywordMatchDetails(task: Task): Array<{ type: string; score: number; matchedKeywords: string[] }> { const combinedText = `${task.name} ${task.description || ''} ${task.notes || ''} ${task.implementationGuide || ''}`.toLowerCase(); const results = []; for (const [agentType, mapping] of Object.entries(AGENT_KEYWORD_MAPPINGS)) { const matchedKeywords = mapping.keywords.filter(keyword => { const escapedKeyword = keyword.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(`\\b${escapedKeyword}\\b`, 'i').test(combinedText); }); if (matchedKeywords.length > 0) { const score = calculateKeywordScore(combinedText, mapping.keywords, mapping.weight, agentType); results.push({ type: agentType, score, matchedKeywords }); } } return results.sort((a, b) => b.score - a.score); }

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/cjo4m06/mcp-shrimp-task-manager'

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