UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

410 lines (402 loc) 17.4 kB
import { BaseTool } from '../base/BaseTool.js'; import { IntelligentFileService } from '../../services/IntelligentFileService.js'; import fs from 'fs/promises'; import path from 'path'; export class SemanticRelationshipTool extends BaseTool { constructor(context) { super('semantic_relationship', 'Analyze semantic relationships between files using LLM-powered content understanding', context); this.intelligentFileService = new IntelligentFileService(context.workingDirectory); } async execute(params) { try { if (!this.context.llm) { return this.createErrorResult('LLM provider is required for semantic analysis'); } const { files, relationshipTypes = ['conceptual', 'functional', 'hierarchical', 'temporal', 'thematic'], minConfidence = 0.3, maxRelationships = 100, includeKeywords = true, batchSize = 5 } = params; console.log('🧠 Starting semantic relationship analysis...'); // Get files to analyze const filesToAnalyze = files || await this.findRelevantFiles(); if (filesToAnalyze.length === 0) { return this.createErrorResult('No files found to analyze'); } // Analyze relationships in batches const relationships = await this.analyzeSemanticRelationships(filesToAnalyze, relationshipTypes, batchSize); // Filter by confidence and limit results const filteredRelationships = relationships .filter(rel => rel.confidence >= minConfidence) .sort((a, b) => b.strength - a.strength) .slice(0, maxRelationships); // Build summary const summary = await this.buildSummary(filteredRelationships, filesToAnalyze); // Extract keywords if requested const keywords = includeKeywords ? await this.extractKeywords(filesToAnalyze, filteredRelationships) : { globalKeywords: [], fileKeywords: {} }; const result = { relationships: filteredRelationships, summary, keywords }; console.log(`✅ Semantic analysis complete: ${filteredRelationships.length} relationships found`); return this.createSuccessResult(result, { analyzedFiles: filesToAnalyze.length, totalRelationships: relationships.length, filteredRelationships: filteredRelationships.length, minConfidence, relationshipTypes }); } catch (error) { console.error('Semantic analysis failed:', error); return this.createErrorResult(`Semantic analysis failed: ${error.message}`); } } /** * Analyze semantic relationships between files */ async analyzeSemanticRelationships(files, relationshipTypes, batchSize) { const allRelationships = []; // Process files in batches to avoid overwhelming the LLM for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); console.log(`📊 Analyzing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(files.length / batchSize)}`); const batchRelationships = await this.analyzeBatch(batch, relationshipTypes); allRelationships.push(...batchRelationships); } return allRelationships; } /** * Analyze a batch of files for semantic relationships */ async analyzeBatch(files, relationshipTypes) { try { // Read file contents const fileContents = await Promise.all(files.map(async (filePath) => { try { const content = await this.intelligentFileService.readFile(filePath); return { filePath, content: this.extractRelevantContent(content, filePath), summary: await this.generateFileSummary(filePath, content) }; } catch { return { filePath, content: '', summary: '' }; } })); // Filter out empty files const validFiles = fileContents.filter(f => f.content.length > 0); if (validFiles.length < 2) return []; // Generate semantic analysis prompt const prompt = this.buildSemanticAnalysisPrompt(validFiles, relationshipTypes); // Get LLM analysis const response = await this.context.llm.executePrompt(prompt, { temperature: 0.3, // Lower temperature for more consistent analysis maxTokens: 2000 }); // Parse relationships from response return this.parseSemanticRelationships(response.content); } catch (error) { console.warn('Failed to analyze batch:', error); return []; } } /** * Extract relevant content from file for analysis */ extractRelevantContent(content, filePath) { const ext = path.extname(filePath).toLowerCase(); // For code files, focus on comments, function names, and key structures if (['.ts', '.js', '.tsx', '.jsx', '.py', '.java'].includes(ext)) { const lines = content.split('\n'); const relevantLines = lines.filter(line => { const trimmed = line.trim(); return trimmed.includes('//') || trimmed.includes('/*') || trimmed.includes('function') || trimmed.includes('class') || trimmed.includes('interface') || trimmed.includes('export') || trimmed.includes('import'); }); // Include first 20 lines and relevant lines, limit to 1000 chars const combined = [...lines.slice(0, 20), ...relevantLines].join('\n'); return combined.substring(0, 1000); } // For markdown and text files, use first portion return content.substring(0, 1500); } /** * Generate a brief summary of the file */ async generateFileSummary(filePath, content) { if (!this.context.llm || content.length < 100) return ''; try { const prompt = `Briefly summarize the purpose and main concepts of this file in 1-2 sentences: File: ${path.basename(filePath)} Content: ${content.substring(0, 500)}... Summary:`; const response = await this.context.llm.executePrompt(prompt, { temperature: 0.3, maxTokens: 100 }); return response.content.trim(); } catch { return ''; } } /** * Build prompt for semantic relationship analysis */ buildSemanticAnalysisPrompt(fileContents, relationshipTypes) { const fileDescriptions = fileContents.map(f => `FILE: ${f.filePath} SUMMARY: ${f.summary} CONTENT: ${f.content} `).join('\n---\n'); return `Analyze the semantic relationships between these files. Look for meaningful connections beyond just imports or references. ${fileDescriptions} Relationship Types to Consider: - conceptual: Files that deal with similar concepts or ideas - functional: Files that serve similar functions or purposes - hierarchical: Files that have parent-child or container-contained relationships - temporal: Files that represent different stages or time-based sequences - thematic: Files that share common themes or subject matter For each meaningful relationship, provide: RELATIONSHIP: [sourceFile]|[targetFile]|[type]|[strength]|[description]|[keywords]|[confidence] Where: - sourceFile: path of the first file - targetFile: path of the second file - type: one of ${relationshipTypes.join(', ')} - strength: 0.0-1.0 (how strong the relationship is) - description: brief explanation of the relationship - keywords: comma-separated relevant keywords that connect the files - confidence: 0.0-1.0 (how confident you are in this relationship) Focus on meaningful, non-obvious relationships. Avoid superficial connections.`; } /** * Parse semantic relationships from LLM response */ parseSemanticRelationships(response) { const relationships = []; const lines = response.split('\n'); for (const line of lines) { if (line.startsWith('RELATIONSHIP:')) { const parts = line.replace('RELATIONSHIP:', '').split('|'); if (parts.length >= 6) { const keywords = parts[5] ? parts[5].trim().split(',').map(k => k.trim()).filter(k => k) : []; relationships.push({ sourceFile: parts[0].trim(), targetFile: parts[1].trim(), relationshipType: parts[2].trim(), strength: Math.max(0, Math.min(1, parseFloat(parts[3].trim()) || 0.5)), description: parts[4].trim(), keywords, confidence: Math.max(0, Math.min(1, parseFloat(parts[6]?.trim()) || 0.5)) }); } } } return relationships; } /** * Build analysis summary */ async buildSummary(relationships, files) { // Count relationships by type const relationshipsByType = {}; let totalConfidence = 0; for (const rel of relationships) { relationshipsByType[rel.relationshipType] = (relationshipsByType[rel.relationshipType] || 0) + 1; totalConfidence += rel.confidence; } const averageConfidence = relationships.length > 0 ? totalConfidence / relationships.length : 0; // Find strongest relationships const strongestRelationships = relationships .sort((a, b) => b.strength - a.strength) .slice(0, 5); // Build conceptual clusters const conceptualClusters = await this.buildConceptualClusters(relationships, files); return { totalRelationships: relationships.length, relationshipsByType, averageConfidence, strongestRelationships, conceptualClusters }; } /** * Build conceptual clusters from relationships */ async buildConceptualClusters(relationships, files) { const clusters = []; const visited = new Set(); // Group files by strong conceptual relationships for (const file of files) { if (visited.has(file)) continue; const cluster = this.buildClusterFromFile(file, relationships, visited); if (cluster.files.length > 1) { clusters.push(cluster); } } return clusters; } /** * Build a conceptual cluster starting from a specific file */ buildClusterFromFile(startFile, relationships, visited) { const clusterFiles = new Set([startFile]); const queue = [startFile]; const allKeywords = new Set(); visited.add(startFile); // BFS to find conceptually related files while (queue.length > 0) { const currentFile = queue.shift(); // Find strong conceptual relationships const relatedRels = relationships.filter(rel => (rel.sourceFile === currentFile || rel.targetFile === currentFile) && rel.relationshipType === 'conceptual' && rel.strength > 0.6); for (const rel of relatedRels) { const relatedFile = rel.sourceFile === currentFile ? rel.targetFile : rel.sourceFile; if (!visited.has(relatedFile)) { clusterFiles.add(relatedFile); queue.push(relatedFile); visited.add(relatedFile); // Collect keywords rel.keywords.forEach(keyword => allKeywords.add(keyword)); } } } const files = Array.from(clusterFiles); const centralConcepts = Array.from(allKeywords).slice(0, 5); return { id: `cluster-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, name: this.generateClusterName(files, centralConcepts), files, centralConcepts, cohesionScore: this.calculateClusterCohesion(files, relationships), description: this.generateClusterDescription(files, centralConcepts) }; } /** * Generate a name for a conceptual cluster */ generateClusterName(files, concepts) { if (concepts.length > 0) { return concepts.slice(0, 2).join(' & ') + ' Cluster'; } // Fallback to common directory const commonDir = this.findCommonDirectory(files); return commonDir ? `${path.basename(commonDir)} Module` : 'Mixed Cluster'; } /** * Generate a description for a conceptual cluster */ generateClusterDescription(files, concepts) { const fileCount = files.length; const conceptList = concepts.slice(0, 3).join(', '); return `A cluster of ${fileCount} files related to ${conceptList || 'common functionality'}`; } /** * Calculate cluster cohesion based on internal relationships */ calculateClusterCohesion(files, relationships) { if (files.length < 2) return 1.0; const internalRels = relationships.filter(rel => files.includes(rel.sourceFile) && files.includes(rel.targetFile)); const maxPossibleRels = files.length * (files.length - 1); return maxPossibleRels > 0 ? internalRels.length / maxPossibleRels : 0; } /** * Extract keywords from files and relationships */ async extractKeywords(files, relationships) { // Global keywords from relationships const keywordFreq = new Map(); for (const rel of relationships) { for (const keyword of rel.keywords) { keywordFreq.set(keyword, (keywordFreq.get(keyword) || 0) + 1); } } const globalKeywords = Array.from(keywordFreq.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 20) .map(([keyword, frequency]) => ({ keyword, frequency })); // File-specific keywords const fileKeywords = {}; for (const file of files) { const fileRels = relationships.filter(rel => rel.sourceFile === file || rel.targetFile === file); const keywords = new Set(); for (const rel of fileRels) { rel.keywords.forEach(k => keywords.add(k)); } fileKeywords[file] = Array.from(keywords).slice(0, 10); } return { globalKeywords, fileKeywords }; } /** * Find common directory path for files */ findCommonDirectory(files) { if (files.length === 0) return ''; if (files.length === 1) return path.dirname(files[0]); const paths = files.map(f => path.dirname(f).split(path.sep)); const commonParts = []; for (let i = 0; i < paths[0].length; i++) { const part = paths[0][i]; if (paths.every(p => p[i] === part)) { commonParts.push(part); } else { break; } } return commonParts.join(path.sep) || '.'; } /** * Find relevant files for analysis */ async findRelevantFiles() { const relevantExtensions = [ '.md', '.mdx', '.txt', '.json', '.yaml', '.yml', '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.html', '.css', '.scss', '.sass', '.less', '.php', '.rb', '.go', '.rs', '.swift', '.kt' ]; const skipDirs = ['node_modules', 'dist', 'build', '.git', '.next', 'coverage']; const files = []; const scanDirectory = async (dirPath) => { try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.')) continue; const fullPath = path.join(dirPath, entry.name); const relativePath = path.relative(this.context.workingDirectory, fullPath); if (entry.isDirectory()) { if (!skipDirs.includes(entry.name)) { await scanDirectory(fullPath); } } else { const ext = path.extname(entry.name).toLowerCase(); if (relevantExtensions.includes(ext)) { files.push(relativePath); } } } } catch (error) { console.warn(`Failed to scan directory ${dirPath}:`, error); } }; await scanDirectory(this.context.workingDirectory); return files; } }