Source: ContextManager.js

import ContextWindowManager from './ContextWindowManager.js'
import logger from 'loglevel'

/**
 * Manages context windows and summaries for LLM interactions
 */
export default class ContextManager {
    constructor(options = {}) {
        this.maxTokens = options.maxTokens || 8192
        this.maxTimeWindow = options.maxTimeWindow || 24 * 60 * 60 * 1000 // 24 hours
        this.relevanceThreshold = options.relevanceThreshold || 0.7
        this.maxContextSize = options.maxContextSize || 5
        this.contextBuffer = []

        this.windowManager = new ContextWindowManager({
            maxWindowSize: this.maxTokens,
            minWindowSize: Math.floor(this.maxTokens / 4),
            overlapRatio: options.overlapRatio || 0.1
        })
    }

    /**
     * Add interaction to context buffer with similarity score
     */
    addToContext(interaction, similarity = 1.0) {
        if (!interaction || typeof interaction !== 'object') {
            logger.warn('Invalid interaction provided to context')
            return
        }

        this.contextBuffer.push({
            ...interaction,
            similarity,
            addedAt: Date.now()
        })

        if (this.contextBuffer.length > this.maxContextSize * 2) {
            this.pruneContext()
        }
    }

    /**
     * Remove old or low-relevance items from context
     */
    pruneContext() {
        const now = Date.now()
        this.contextBuffer = this.contextBuffer
            .filter(item => {
                const age = now - item.addedAt
                return age < this.maxTimeWindow && item.similarity >= this.relevanceThreshold
            })
            .sort((a, b) => b.similarity - a.similarity)
            .slice(0, this.maxContextSize)
    }

    /**
     * Create a concise summary of interactions grouped by concept
     */
    summarizeContext(interactions) {
        if (!Array.isArray(interactions) || interactions.length === 0) {
            return ''
        }

        const groupedInteractions = {}

        for (const interaction of interactions) {
            if (!interaction) continue

            const mainConcept = interaction.concepts?.[0] || 'general'
            if (!groupedInteractions[mainConcept]) {
                groupedInteractions[mainConcept] = []
            }
            groupedInteractions[mainConcept].push(interaction)
        }

        const summaries = []
        for (const [concept, group] of Object.entries(groupedInteractions)) {
            if (group.length === 1) {
                summaries.push(this.formatSingleInteraction(group[0]))
            } else {
                summaries.push(this.formatGroupSummary(concept, group))
            }
        }

        return summaries.join('\n\n')
    }

    /**
     * Format a single interaction for display
     */
    formatSingleInteraction(interaction) {
        if (!interaction?.prompt || !interaction?.output) {
            return ''
        }

        return `Q: ${interaction.prompt}\nA: ${interaction.output}`
    }

    /**
     * Create summary for a group of related interactions
     */
    formatGroupSummary(concept, interactions) {
        if (!Array.isArray(interactions) || interactions.length === 0) {
            return ''
        }

        const summaryLines = interactions
            .slice(0, 3) // Limit examples per group
            .map(i => {
                if (!i?.prompt || !i?.output) return null
                const truncatedOutput = i.output.substring(0, 50)
                return `- ${i.prompt} → ${truncatedOutput}${truncatedOutput.length < i.output.length ? '...' : ''}`
            })
            .filter(Boolean)

        if (summaryLines.length === 0) return ''

        return `Topic: ${concept}\n${summaryLines.join('\n')}`
    }

    /**
     * Build complete context including history and current prompt
     */
    buildContext(currentPrompt, retrievals = [], recentInteractions = [], options = {}) {
        if (!currentPrompt) {
            logger.warn('No current prompt provided to buildContext')
            return ''
        }

        this.pruneContext()

        // Add new relevant interactions
        retrievals?.forEach(retrieval => {
            if (retrieval?.interaction) {
                this.addToContext(retrieval.interaction, retrieval.similarity)
            }
        })

        // Add recent interactions
        recentInteractions?.forEach(interaction => {
            if (interaction) {
                this.addToContext(interaction, 0.9)
            }
        })

        const contextParts = []

        // Add system context if provided
        if (options.systemContext) {
            contextParts.push(`System Context: ${options.systemContext}`)
        }

        // Add summarized historical context
        const historicalContext = this.summarizeContext(
            this.contextBuffer.slice(0, this.maxContextSize)
        )

        if (historicalContext) {
            contextParts.push('Relevant Context:', historicalContext)
        }

        const fullContext = contextParts.join('\n\n')

        // Process through window manager if needed
        if (this.windowManager.estimateTokens(fullContext) > this.maxTokens) {
            const windows = this.windowManager.processContext(fullContext)
            return this.windowManager.mergeOverlappingContent(windows)
        }

        return fullContext
    }
}