contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
410 lines (402 loc) • 17.4 kB
JavaScript
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;
}
}