contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
593 lines (587 loc) • 23.9 kB
JavaScript
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
}
}