UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

593 lines (587 loc) 23.9 kB
import { FileService } from "./fileService.js"; import { IntelligentFileService } from "./IntelligentFileService.js"; import { ClassifyFileTool } from "../tools/file/ClassifyFileTool.js"; import { LLMFactory } from "./llm/LLMFactory.js"; import fs from 'fs/promises'; import path from 'path'; import crypto from 'crypto'; export class ProjectContextService { constructor(workingDirectory) { this.contextCachePath = './.config/project-context'; this.contextVersion = '1.0.0'; this.fileService = new FileService(workingDirectory); this.intelligentFileService = new IntelligentFileService(workingDirectory || process.cwd()); this.initializeLLM(); } async initializeLLM() { try { const provider = await LLMFactory.getConfiguredProvider(); this.llm = provider || undefined; } catch (error) { console.warn('No LLM provider configured for ProjectContextService'); this.llm = undefined; } } /** * Build comprehensive project context */ async buildProjectContext(forceRefresh = false) { const workingDirectory = process.cwd(); // Check for cached context first if (!forceRefresh) { const cachedContext = await this.loadCachedContext(); if (cachedContext && this.isContextValid(cachedContext)) { return cachedContext; } } console.log('🔍 Building comprehensive project context...'); // Build basic context const relevantFiles = await this.findRelevantFiles(workingDirectory); const projectStructure = await this.analyzeProjectStructure(workingDirectory); // Build dependency graph const dependencyGraph = await this.buildDependencyGraph(relevantFiles); // Build semantic relationships (if LLM is available) const semanticRelationships = this.llm ? await this.buildSemanticRelationships(relevantFiles) : []; const projectContext = { workingDirectory, relevantFiles, projectMetadata: { analyzedAt: Date.now(), fileCount: relevantFiles.length, totalDirectories: projectStructure.totalDirectories, contextVersion: this.contextVersion }, projectStructure, semanticRelationships, dependencyGraph, lastAnalyzed: Date.now(), contextVersion: this.contextVersion }; // Cache the context await this.cacheContext(projectContext); console.log(`✅ Project context built: ${relevantFiles.length} files, ${semanticRelationships.length} semantic relationships`); return projectContext; } /** * Analyze project structure */ async analyzeProjectStructure(rootDirectory) { const directories = []; const filesByType = {}; let totalFiles = 0; let totalDirectories = 0; const analyzeDirectory = async (dirPath) => { try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const relativePath = path.relative(rootDirectory, dirPath); let fileCount = 0; let subdirectoryCount = 0; for (const entry of entries) { if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { subdirectoryCount++; totalDirectories++; await analyzeDirectory(fullPath); } else { fileCount++; totalFiles++; const ext = path.extname(entry.name).toLowerCase(); if (!filesByType[ext]) filesByType[ext] = []; filesByType[ext].push(path.relative(rootDirectory, fullPath)); } } if (relativePath) { // Don't include root directory itself directories.push({ path: relativePath, name: path.basename(dirPath), fileCount, subdirectoryCount, purpose: await this.determinePurpose(dirPath, entries.map(e => e.name)), importance: this.calculateDirectoryImportance(fileCount, subdirectoryCount) }); } } catch (error) { console.warn(`Failed to analyze directory ${dirPath}:`, error); } }; await analyzeDirectory(rootDirectory); return { rootDirectory, directories, filesByType, totalFiles, totalDirectories, analyzedAt: Date.now() }; } /** * Build dependency graph from file relationships */ async buildDependencyGraph(files) { const nodes = []; const edges = []; const relationshipCounts = new Map(); console.log('📊 Building dependency graph...'); // Analyze each file for relationships for (const filePath of files) { try { const classifyTool = new ClassifyFileTool({ workingDirectory: process.cwd(), llm: this.llm }); const result = await classifyTool.execute({ filePath, includeRelationships: true }); if (result.success && result.data.relationships) { const relationships = result.data.relationships; // Create node nodes.push({ filePath, nodeType: this.determineNodeType(filePath), importance: relationships.length, // Will be recalculated later metadata: { fileType: path.extname(filePath), size: (await fs.stat(filePath)).size, lastModified: (await fs.stat(filePath)).mtime } }); // Create edges for (const rel of relationships) { edges.push({ source: filePath, target: rel.targetFile, edgeType: rel.type, strength: rel.confidence, metadata: { description: rel.description } }); // Count relationships for importance calculation relationshipCounts.set(filePath, (relationshipCounts.get(filePath) || 0) + 1); relationshipCounts.set(rel.targetFile, (relationshipCounts.get(rel.targetFile) || 0) + 1); } } } catch (error) { console.warn(`Failed to analyze relationships for ${filePath}:`, error); } } // Recalculate node importance based on connections for (const node of nodes) { node.importance = (relationshipCounts.get(node.filePath) || 0) / Math.max(files.length, 1); } // Build clusters const clusters = await this.buildDependencyClusters(nodes, edges); return { nodes, edges, clusters, analyzedAt: Date.now() }; } /** * Build semantic relationships using LLM analysis */ async buildSemanticRelationships(files) { if (!this.llm) return []; console.log('🧠 Building semantic relationships...'); const relationships = []; // Analyze files in batches to avoid overwhelming the LLM const batchSize = 5; for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); const batchRelationships = await this.analyzeSemanticRelationshipsBatch(batch); relationships.push(...batchRelationships); } return relationships; } /** * Analyze semantic relationships for a batch of files */ async analyzeSemanticRelationshipsBatch(files) { if (!this.llm) return []; try { // Read file contents for analysis const fileContents = await Promise.all(files.map(async (filePath) => { try { const content = await this.fileService.readFile(filePath); return { filePath, content: content.substring(0, 1000) }; // Limit content for analysis } catch { return { filePath, content: '' }; } })); const prompt = this.buildSemanticAnalysisPrompt(fileContents); const response = await this.llm.executePrompt(prompt); return this.parseSemanticRelationships(response.content); } catch (error) { console.warn('Failed to analyze semantic relationships:', error); return []; } } /** * Build prompt for semantic relationship analysis */ buildSemanticAnalysisPrompt(fileContents) { const fileList = fileContents.map(f => `${f.filePath}:\n${f.content}`).join('\n\n---\n\n'); return `Analyze the semantic relationships between these files. Look for conceptual connections, shared themes, functional relationships, and hierarchical structures. Files to analyze: ${fileList} For each meaningful relationship, provide: RELATIONSHIP: [sourceFile]|[targetFile]|[type]|[strength]|[description]|[keywords]|[confidence] Types: conceptual, functional, hierarchical, temporal, thematic Strength: 0.0-1.0 (how strong the relationship is) Confidence: 0.0-1.0 (how confident you are in this relationship) Keywords: comma-separated relevant keywords Description: brief explanation of the relationship Focus on meaningful relationships, not just superficial similarities.`; } /** * 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) { relationships.push({ sourceFile: parts[0].trim(), targetFile: parts[1].trim(), relationshipType: parts[2].trim(), strength: parseFloat(parts[3].trim()) || 0.5, description: parts[4].trim(), keywords: parts[5].trim().split(',').map(k => k.trim()), confidence: parseFloat(parts[6]?.trim()) || 0.5 }); } } } return relationships; } /** * Find relevant files in the project */ async findRelevantFiles(workingDirectory) { 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(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(workingDirectory); return files; } /** * Check if cached context is still valid */ isContextValid(context) { if (!context.lastAnalyzed || !context.contextVersion) return false; // Context is valid for 1 hour const maxAge = 60 * 60 * 1000; // 1 hour in milliseconds const age = Date.now() - context.lastAnalyzed; return age < maxAge && context.contextVersion === this.contextVersion; } /** * Load cached project context */ async loadCachedContext() { try { const contextPath = path.join(this.contextCachePath, 'project-context.json'); const content = await this.fileService.readFile(contextPath); const context = JSON.parse(content); // Restore Map objects that were serialized if (context.fileRelationships && Array.isArray(context.fileRelationships)) { context.fileRelationships = new Map(context.fileRelationships); } return context; } catch { return null; } } /** * Cache project context to disk */ async cacheContext(context) { try { const contextPath = path.join(this.contextCachePath, 'project-context.json'); // Serialize Map objects for JSON storage const serializable = { ...context, fileRelationships: context.fileRelationships ? Array.from(context.fileRelationships.entries()) : undefined }; await this.fileService.saveFile({ path: contextPath, content: JSON.stringify(serializable, null, 2) }); } catch (error) { console.warn('Failed to cache project context:', error); } } /** * Get project context summary for display */ async getContextSummary() { const context = await this.buildProjectContext(); return { fileCount: context.relevantFiles.length, directoryCount: context.projectStructure?.totalDirectories || 0, relationshipCount: context.semanticRelationships?.length || 0, clusterCount: context.dependencyGraph?.clusters.length || 0, lastAnalyzed: context.lastAnalyzed ? new Date(context.lastAnalyzed) : null }; } /** * Get files related to a specific file */ async getRelatedFiles(filePath, maxResults = 10) { const context = await this.buildProjectContext(); const directDependencies = []; const semanticRelated = []; const clusterMembers = []; // Find direct dependencies if (context.dependencyGraph) { const edges = context.dependencyGraph.edges.filter(edge => edge.source === filePath || edge.target === filePath); for (const edge of edges) { const relatedFile = edge.source === filePath ? edge.target : edge.source; if (!directDependencies.includes(relatedFile)) { directDependencies.push(relatedFile); } } } // Find semantic relationships if (context.semanticRelationships) { const relationships = context.semanticRelationships .filter(rel => rel.sourceFile === filePath || rel.targetFile === filePath) .sort((a, b) => b.strength - a.strength) .slice(0, maxResults); for (const rel of relationships) { const relatedFile = rel.sourceFile === filePath ? rel.targetFile : rel.sourceFile; if (!semanticRelated.includes(relatedFile)) { semanticRelated.push(relatedFile); } } } // Find cluster members if (context.dependencyGraph) { const cluster = context.dependencyGraph.clusters.find(cluster => cluster.files.includes(filePath)); if (cluster) { clusterMembers.push(...cluster.files.filter(f => f !== filePath)); } } return { directDependencies: directDependencies.slice(0, maxResults), semanticRelated: semanticRelated.slice(0, maxResults), clusterMembers: clusterMembers.slice(0, maxResults) }; } /** * Clear cached context */ async clearCache() { try { const contextPath = path.join(this.contextCachePath, 'project-context.json'); await this.fileService.deleteFile(contextPath); } catch { // Cache file might not exist, that's okay } } /** * Build dependency clusters */ async buildDependencyClusters(nodes, edges) { const clusters = []; const visited = new Set(); // Simple clustering based on file paths and relationships for (const node of nodes) { if (visited.has(node.filePath)) continue; const cluster = await this.buildClusterFromNode(node, nodes, edges, visited); if (cluster.files.length > 1) { // Only include clusters with multiple files clusters.push(cluster); } } return clusters; } /** * Build a cluster starting from a specific node */ async buildClusterFromNode(startNode, allNodes, allEdges, visited) { const clusterFiles = new Set([startNode.filePath]); const queue = [startNode.filePath]; visited.add(startNode.filePath); // BFS to find connected files while (queue.length > 0) { const currentFile = queue.shift(); // Find all connected files const connectedEdges = allEdges.filter(edge => edge.source === currentFile || edge.target === currentFile); for (const edge of connectedEdges) { const connectedFile = edge.source === currentFile ? edge.target : edge.source; if (!visited.has(connectedFile) && this.shouldIncludeInCluster(edge)) { clusterFiles.add(connectedFile); queue.push(connectedFile); visited.add(connectedFile); } } } const files = Array.from(clusterFiles); const directoryPath = this.findCommonDirectory(files); return { id: crypto.randomUUID(), name: directoryPath || 'Mixed', files, purpose: await this.determineClusterPurpose(files), cohesion: this.calculateClusterCohesion(files, allEdges) }; } /** * Determine if an edge should include files in the same cluster */ shouldIncludeInCluster(edge) { // Strong relationships that indicate files should be clustered together const strongRelationships = ['imports', 'extends', 'includes']; return strongRelationships.includes(edge.edgeType) && edge.strength > 0.5; } /** * Find common directory path for a set of 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) || '.'; } /** * Calculate cluster cohesion (how tightly related files are) */ calculateClusterCohesion(files, edges) { if (files.length < 2) return 1.0; const internalEdges = edges.filter(edge => files.includes(edge.source) && files.includes(edge.target)); const maxPossibleEdges = files.length * (files.length - 1); // Directed graph return maxPossibleEdges > 0 ? internalEdges.length / maxPossibleEdges : 0; } /** * Determine cluster purpose */ async determineClusterPurpose(files) { // Simple heuristic based on file types and directory names const commonDir = this.findCommonDirectory(files); const dirName = path.basename(commonDir); // Common directory name patterns const purposePatterns = { 'components': 'UI Components', 'services': 'Business Logic Services', 'utils': 'Utility Functions', 'types': 'Type Definitions', 'tests': 'Test Files', 'docs': 'Documentation', 'config': 'Configuration', 'tools': 'Development Tools', 'api': 'API Layer', 'routes': 'Routing Logic' }; return purposePatterns[dirName.toLowerCase()] || `${dirName} Module`; } /** * Determine node type based on file path and content */ determineNodeType(filePath) { const ext = path.extname(filePath).toLowerCase(); const basename = path.basename(filePath, ext).toLowerCase(); if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) { if (basename.includes('component') || basename.includes('page')) { return 'component'; } if (basename.includes('service') || basename.includes('api')) { return 'module'; } } if (['.md', '.txt', '.json', '.yaml', '.yml'].includes(ext)) { return 'resource'; } return 'file'; } /** * Determine directory purpose using AI if available */ async determinePurpose(dirPath, fileNames) { if (!this.llm || fileNames.length === 0) return undefined; try { const prompt = `Analyze this directory and determine its purpose: Directory: ${path.basename(dirPath)} Files: ${fileNames.slice(0, 10).join(', ')}${fileNames.length > 10 ? '...' : ''} Provide a brief purpose description (max 50 characters):`; const response = await this.llm.executePrompt(prompt); return response.content.trim().substring(0, 50); } catch { return undefined; } } /** * Calculate directory importance */ calculateDirectoryImportance(fileCount, subdirectoryCount) { // Simple heuristic: more files and subdirectories = higher importance const totalItems = fileCount + subdirectoryCount; return Math.min(totalItems / 20, 1.0); // Normalize to 0-1 scale } }