@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
375 lines • 15.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextManager = void 0;
const documents_1 = require("@langchain/core/documents");
const text_splitter_1 = require("langchain/text_splitter");
const crypto_1 = require("crypto");
const logger_1 = __importDefault(require("../../../utils/logger"));
class ContextManager {
config;
contextWindows = new Map();
contextChains = new Map();
textSplitter;
cleanupTimer = null;
constructor(config) {
this.config = config;
this.textSplitter = new text_splitter_1.RecursiveCharacterTextSplitter({
chunkSize: config.defaultChunkSize,
chunkOverlap: config.chunkOverlap,
separators: ['\n\n', '\n', ' ', '']
});
this.startCleanupTimer();
}
async addCodeContext(codeContext, priority = 1) {
try {
const contextId = this.generateContextId(codeContext);
// Convert code context to documents
const documents = await this.codeContextToDocuments(codeContext);
// Calculate total tokens (rough estimation)
const totalTokens = documents.reduce((sum, doc) => sum + Math.ceil(doc.pageContent.length / 4), 0);
// Create context window
const contextWindow = {
id: contextId,
documents,
totalTokens,
maxTokens: this.config.maxTokensPerWindow,
priority,
createdAt: new Date(),
lastAccessed: new Date()
};
// Check if we need to evict old contexts
await this.manageContextCapacity();
// Store context window
this.contextWindows.set(contextId, contextWindow);
logger_1.default.info('Code context added', {
contextId,
filePath: codeContext.filePath,
documentsCount: documents.length,
totalTokens,
priority
});
return contextId;
}
catch (error) {
logger_1.default.error('Failed to add code context', { error, filePath: codeContext.filePath });
throw error;
}
}
async getRelevantContext(queryContext, maxTokens) {
try {
const relevantDocuments = [];
let currentTokens = 0;
// Get contexts sorted by relevance and priority
const sortedContexts = this.getSortedContexts(queryContext);
for (const contextWindow of sortedContexts) {
// Update last accessed time
contextWindow.lastAccessed = new Date();
// Filter documents by relevance to query
const relevantDocs = await this.filterDocumentsByRelevance(contextWindow.documents, queryContext);
for (const doc of relevantDocs) {
const docTokens = Math.ceil(doc.pageContent.length / 4);
if (currentTokens + docTokens <= maxTokens) {
relevantDocuments.push(doc);
currentTokens += docTokens;
}
else {
// Try to fit a truncated version
const remainingTokens = maxTokens - currentTokens;
if (remainingTokens > 100) { // Only if we have meaningful space left
const truncatedContent = doc.pageContent.substring(0, remainingTokens * 4);
relevantDocuments.push(new documents_1.Document({
pageContent: truncatedContent + '...',
metadata: { ...doc.metadata, truncated: true }
}));
currentTokens = maxTokens;
}
break;
}
}
if (currentTokens >= maxTokens)
break;
}
logger_1.default.debug('Retrieved relevant context', {
documentsCount: relevantDocuments.length,
totalTokens: currentTokens,
maxTokens,
query: queryContext.query
});
return relevantDocuments;
}
catch (error) {
logger_1.default.error('Failed to get relevant context', { error, query: queryContext.query });
throw error;
}
}
async updateContextChain(sessionId, queryContext, response) {
try {
if (!this.contextChains.has(sessionId)) {
this.contextChains.set(sessionId, {
id: sessionId,
messages: [],
context: {
codeFiles: [],
relevantDocs: [],
userIntent: queryContext.intent,
sessionId
},
reasoning: {
chain: [],
confidence: 0,
sources: []
}
});
}
const chain = this.contextChains.get(sessionId);
// Add user message
chain.messages.push({
role: 'user',
content: queryContext.query,
timestamp: new Date(),
metadata: {
intent: queryContext.intent,
scope: queryContext.scope,
filePath: queryContext.filePath
}
});
// Add assistant response
chain.messages.push({
role: 'assistant',
content: response,
timestamp: new Date()
});
// Update context information
if (queryContext.filePath && !chain.context.codeFiles.includes(queryContext.filePath)) {
chain.context.codeFiles.push(queryContext.filePath);
}
// Keep only last 20 messages to prevent memory bloat
if (chain.messages.length > 20) {
chain.messages = chain.messages.slice(-20);
}
logger_1.default.debug('Context chain updated', {
sessionId,
messagesCount: chain.messages.length,
codeFilesCount: chain.context.codeFiles.length
});
}
catch (error) {
logger_1.default.error('Failed to update context chain', { error, sessionId });
throw error;
}
}
getContextChain(sessionId) {
return this.contextChains.get(sessionId);
}
async removeContext(contextId) {
const removed = this.contextWindows.delete(contextId);
if (removed) {
logger_1.default.info('Context removed', { contextId });
}
return removed;
}
async clearExpiredContexts() {
const now = new Date();
const expiredContexts = [];
for (const [contextId, contextWindow] of this.contextWindows) {
const ageInMinutes = (now.getTime() - contextWindow.lastAccessed.getTime()) / (1000 * 60);
// Remove contexts not accessed for more than 30 minutes
if (ageInMinutes > 30) {
expiredContexts.push(contextId);
}
}
expiredContexts.forEach(contextId => {
this.contextWindows.delete(contextId);
});
if (expiredContexts.length > 0) {
logger_1.default.info('Expired contexts cleared', { count: expiredContexts.length });
}
return expiredContexts.length;
}
getContextStats() {
let totalDocuments = 0;
let totalTokens = 0;
for (const contextWindow of this.contextWindows.values()) {
totalDocuments += contextWindow.documents.length;
totalTokens += contextWindow.totalTokens;
}
return {
totalContexts: this.contextWindows.size,
totalDocuments,
totalTokens,
averageTokensPerContext: this.contextWindows.size > 0 ? totalTokens / this.contextWindows.size : 0,
memoryUsage: process.memoryUsage().heapUsed
};
}
async codeContextToDocuments(codeContext) {
const documents = [];
// Main file content
const mainDoc = new documents_1.Document({
pageContent: codeContext.content,
metadata: {
source: codeContext.filePath,
type: 'code',
language: codeContext.language,
filePath: codeContext.filePath,
lastModified: codeContext.metadata.lastModified,
size: codeContext.metadata.size,
complexity: codeContext.metadata.complexity,
testCoverage: codeContext.metadata.testCoverage
}
});
// Split main content if it's too large
const splitDocs = await this.textSplitter.splitDocuments([mainDoc]);
documents.push(...splitDocs);
// Function documentation
for (const func of codeContext.functions) {
const funcDoc = new documents_1.Document({
pageContent: `Function: ${func.name}\nSignature: ${func.signature}\n${func.docstring || ''}`,
metadata: {
source: codeContext.filePath,
type: 'function',
language: codeContext.language,
functionName: func.name,
startLine: func.startLine,
endLine: func.endLine
}
});
documents.push(funcDoc);
}
// Class documentation
for (const cls of codeContext.classes) {
const classDoc = new documents_1.Document({
pageContent: `Class: ${cls.name}\nMethods: ${cls.methods.join(', ')}\nProperties: ${cls.properties.join(', ')}`,
metadata: {
source: codeContext.filePath,
type: 'class',
language: codeContext.language,
className: cls.name,
startLine: cls.startLine,
endLine: cls.endLine
}
});
documents.push(classDoc);
}
// Import/dependency information
if (codeContext.imports.length > 0 || codeContext.dependencies.length > 0) {
const importsContent = [
'Imports:',
...codeContext.imports.map(imp => `- ${imp.module}: ${imp.items.join(', ')}${imp.alias ? ` as ${imp.alias}` : ''}`),
'Dependencies:',
...codeContext.dependencies.map(dep => `- ${dep}`)
].join('\n');
const importsDoc = new documents_1.Document({
pageContent: importsContent,
metadata: {
source: codeContext.filePath,
type: 'imports',
language: codeContext.language
}
});
documents.push(importsDoc);
}
return documents;
}
getSortedContexts(queryContext) {
const contexts = Array.from(this.contextWindows.values());
return contexts.sort((a, b) => {
// Calculate relevance score
let scoreA = a.priority;
let scoreB = b.priority;
// Boost score for file-specific queries
if (queryContext.filePath) {
const hasFileA = a.documents.some(doc => doc.metadata.filePath === queryContext.filePath);
const hasFileB = b.documents.some(doc => doc.metadata.filePath === queryContext.filePath);
if (hasFileA)
scoreA += 10;
if (hasFileB)
scoreB += 10;
}
// Boost score for language-specific queries
if (queryContext.language) {
const hasLangA = a.documents.some(doc => doc.metadata.language === queryContext.language);
const hasLangB = b.documents.some(doc => doc.metadata.language === queryContext.language);
if (hasLangA)
scoreA += 5;
if (hasLangB)
scoreB += 5;
}
// Apply time decay
const now = new Date().getTime();
const ageA = (now - a.lastAccessed.getTime()) / (1000 * 60); // minutes
const ageB = (now - b.lastAccessed.getTime()) / (1000 * 60);
scoreA *= Math.exp(-ageA * this.config.priorityDecayRate);
scoreB *= Math.exp(-ageB * this.config.priorityDecayRate);
return scoreB - scoreA; // Higher score first
});
}
async filterDocumentsByRelevance(documents, queryContext) {
// Simple relevance filtering based on metadata and content
return documents.filter(doc => {
// Always include if file path matches
if (queryContext.filePath && doc.metadata.filePath === queryContext.filePath) {
return true;
}
// Include if language matches
if (queryContext.language && doc.metadata.language === queryContext.language) {
return true;
}
// Include if content seems relevant (simple keyword matching)
const queryLower = queryContext.query.toLowerCase();
const contentLower = doc.pageContent.toLowerCase();
// Check for query terms in content
const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 2);
const relevantTerms = queryTerms.filter(term => contentLower.includes(term));
return relevantTerms.length > 0;
});
}
async manageContextCapacity() {
if (this.contextWindows.size >= this.config.maxContextWindows) {
// Remove least recently used contexts
const contexts = Array.from(this.contextWindows.entries())
.sort(([, a], [, b]) => a.lastAccessed.getTime() - b.lastAccessed.getTime());
const toRemove = contexts.slice(0, Math.ceil(this.config.maxContextWindows * 0.2)); // Remove 20%
toRemove.forEach(([contextId]) => {
this.contextWindows.delete(contextId);
});
logger_1.default.info('Context capacity managed', {
removed: toRemove.length,
remaining: this.contextWindows.size
});
}
}
generateContextId(codeContext) {
const data = {
filePath: codeContext.filePath,
lastModified: codeContext.metadata.lastModified.toISOString(),
size: codeContext.metadata.size
};
return (0, crypto_1.createHash)('md5').update(JSON.stringify(data)).digest('hex');
}
startCleanupTimer() {
this.cleanupTimer = setInterval(async () => {
try {
await this.clearExpiredContexts();
}
catch (error) {
logger_1.default.error('Context cleanup failed', { error });
}
}, this.config.cleanupInterval);
// Ensure timer doesn't prevent process exit
this.cleanupTimer.unref();
}
async shutdown() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
this.contextWindows.clear();
this.contextChains.clear();
logger_1.default.info('Context manager shutdown complete');
}
}
exports.ContextManager = ContextManager;
//# sourceMappingURL=ContextManager.js.map