mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: initial conversation memory system implementation
- Add comprehensive memory service architecture mirroring CodeIndexManager pattern - Implement temporal intelligence with fact categorization (infrastructure, architecture, debugging, pattern) - Add memory_search tool for retrieving relevant conversation context - Create service factory, orchestrator, and search service stubs - Add configuration management for memory system settings - Implement cache manager for persistent memory storage - Define interfaces for fact extraction, conflict resolution, and temporal management Based on detailed planning documents: - Temporal intelligence for managing fact lifecycle - Battle-tested patterns from mem0, Graphiti, and Potpie - Workspace isolation for project-specific memories - Non-invasive integration with minimal codebase changes This is an initial implementation that provides the foundation for the conversation memory feature as described in issue #7537.
This commit is contained in:
parent
01458f1646
commit
60bbf31428
12 changed files with 1482 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ export const toolNames = [
|
|||
"new_task",
|
||||
"fetch_instructions",
|
||||
"codebase_search",
|
||||
"memory_search",
|
||||
"update_todo_list",
|
||||
"generate_image",
|
||||
] as const
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ import { getNewTaskDescription } from "./new-task"
|
|||
import { getCodebaseSearchDescription } from "./codebase-search"
|
||||
import { getUpdateTodoListDescription } from "./update-todo-list"
|
||||
import { getGenerateImageDescription } from "./generate-image"
|
||||
import { getMemorySearchDescription } from "./memory-search"
|
||||
import { CodeIndexManager } from "../../../services/code-index/manager"
|
||||
import { ConversationMemoryManager } from "../../../services/conversation-memory/manager"
|
||||
|
||||
// Map of tool names to their description functions
|
||||
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
|
||||
|
|
@ -50,6 +52,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
|
|||
use_mcp_tool: (args) => getUseMcpToolDescription(args),
|
||||
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
|
||||
codebase_search: (args) => getCodebaseSearchDescription(args),
|
||||
memory_search: (args) => getMemorySearchDescription(args),
|
||||
switch_mode: () => getSwitchModeDescription(),
|
||||
new_task: (args) => getNewTaskDescription(args),
|
||||
insert_content: (args) => getInsertContentDescription(args),
|
||||
|
|
@ -126,6 +129,12 @@ export function getToolDescriptionsForMode(
|
|||
tools.delete("codebase_search")
|
||||
}
|
||||
|
||||
// Conditionally exclude memory_search if feature is disabled or not configured
|
||||
const memoryManager = ConversationMemoryManager.getCurrentWorkspaceManager()
|
||||
if (!memoryManager || !memoryManager.isFeatureEnabled || !memoryManager.isInitialized) {
|
||||
tools.delete("memory_search")
|
||||
}
|
||||
|
||||
// Conditionally exclude update_todo_list if disabled in settings
|
||||
if (settings?.todoListEnabled === false) {
|
||||
tools.delete("update_todo_list")
|
||||
|
|
@ -171,5 +180,6 @@ export {
|
|||
getInsertContentDescription,
|
||||
getSearchAndReplaceDescription,
|
||||
getCodebaseSearchDescription,
|
||||
getMemorySearchDescription,
|
||||
getGenerateImageDescription,
|
||||
}
|
||||
|
|
|
|||
44
src/core/prompts/tools/memory-search.ts
Normal file
44
src/core/prompts/tools/memory-search.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getMemorySearchDescription(args: ToolArgs): string {
|
||||
return `## memory_search
|
||||
Description: Search conversation memory for past technical decisions, patterns, and project context. Retrieves relevant memories about infrastructure, architecture, debugging issues, and learned patterns for the current workspace.
|
||||
|
||||
Parameters:
|
||||
- query: (required) Natural language search query. Use the user's exact wording when possible.
|
||||
- category: (optional) Filter by category: infrastructure, architecture, pattern, or debugging
|
||||
- tags: (optional) Comma-separated tags to filter results (e.g., "auth,cookies")
|
||||
- limit: (optional) Maximum number of results to return (default: 10)
|
||||
|
||||
Usage:
|
||||
<memory_search>
|
||||
<query>Your natural language query here</query>
|
||||
<category>architecture</category>
|
||||
<tags>auth,cookies</tags>
|
||||
<limit>6</limit>
|
||||
</memory_search>
|
||||
|
||||
Examples:
|
||||
|
||||
1. Search for authentication decisions:
|
||||
<memory_search>
|
||||
<query>authentication approach</query>
|
||||
<category>architecture</category>
|
||||
</memory_search>
|
||||
|
||||
2. Find debugging patterns:
|
||||
<memory_search>
|
||||
<query>CORS error fixes</query>
|
||||
<category>pattern</category>
|
||||
</memory_search>
|
||||
|
||||
3. General project context search:
|
||||
<memory_search>
|
||||
<query>database configuration</query>
|
||||
</memory_search>
|
||||
|
||||
Output format:
|
||||
ARCHITECTURE: Database access via dependency injection (replaces singleton) (2025-07-08)
|
||||
PATTERN: Avoid N+1 queries in SQLAlchemy using selectinload (derived from incident) (2025-05-21)
|
||||
`
|
||||
}
|
||||
167
src/services/conversation-memory/cache-manager.ts
Normal file
167
src/services/conversation-memory/cache-manager.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { ConversationFact } from "./interfaces"
|
||||
|
||||
export interface MemoryCacheData {
|
||||
version: string
|
||||
lastUpdated: string
|
||||
facts: ConversationFact[]
|
||||
metadata: {
|
||||
workspacePath: string
|
||||
totalFacts: number
|
||||
categories: Record<string, number>
|
||||
}
|
||||
}
|
||||
|
||||
export class ConversationMemoryCacheManager {
|
||||
private readonly cacheFileName = "conversation-memory-cache.json"
|
||||
private readonly cacheVersion = "1.0.0"
|
||||
private cacheFilePath: string
|
||||
private memoryCache: Map<string, ConversationFact> = new Map()
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly workspacePath: string,
|
||||
) {
|
||||
// Store cache in VS Code's global storage
|
||||
const storageUri = context.globalStorageUri
|
||||
this.cacheFilePath = path.join(storageUri.fsPath, this.getCacheFileName())
|
||||
}
|
||||
|
||||
private getCacheFileName(): string {
|
||||
// Create unique cache file name per workspace
|
||||
const workspaceHash = Buffer.from(this.workspacePath).toString("base64").replace(/[/+=]/g, "_")
|
||||
return `${workspaceHash}-${this.cacheFileName}`
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
// Ensure storage directory exists
|
||||
const storageDir = path.dirname(this.cacheFilePath)
|
||||
await fs.mkdir(storageDir, { recursive: true })
|
||||
|
||||
// Load existing cache if available
|
||||
await this.loadCache()
|
||||
}
|
||||
|
||||
public async loadCache(): Promise<void> {
|
||||
try {
|
||||
const cacheContent = await fs.readFile(this.cacheFilePath, "utf8")
|
||||
const cacheData: MemoryCacheData = JSON.parse(cacheContent)
|
||||
|
||||
// Validate cache version
|
||||
if (cacheData.version !== this.cacheVersion) {
|
||||
console.log(
|
||||
`Cache version mismatch. Expected ${this.cacheVersion}, got ${cacheData.version}. Clearing cache.`,
|
||||
)
|
||||
await this.clearCacheFile()
|
||||
return
|
||||
}
|
||||
|
||||
// Load facts into memory
|
||||
this.memoryCache.clear()
|
||||
for (const fact of cacheData.facts) {
|
||||
// Convert date strings back to Date objects
|
||||
fact.reference_time = new Date(fact.reference_time)
|
||||
fact.ingestion_time = new Date(fact.ingestion_time)
|
||||
if (fact.superseded_at) fact.superseded_at = new Date(fact.superseded_at)
|
||||
if (fact.resolved_at) fact.resolved_at = new Date(fact.resolved_at)
|
||||
if (fact.last_confirmed) fact.last_confirmed = new Date(fact.last_confirmed)
|
||||
|
||||
this.memoryCache.set(fact.id, fact)
|
||||
}
|
||||
|
||||
console.log(`Loaded ${this.memoryCache.size} facts from cache`)
|
||||
} catch (error) {
|
||||
// Cache doesn't exist or is corrupted - start fresh
|
||||
console.log("No valid cache found, starting with empty memory")
|
||||
this.memoryCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
public async saveCache(): Promise<void> {
|
||||
try {
|
||||
const facts = Array.from(this.memoryCache.values())
|
||||
|
||||
// Calculate category statistics
|
||||
const categories: Record<string, number> = {}
|
||||
for (const fact of facts) {
|
||||
categories[fact.category] = (categories[fact.category] || 0) + 1
|
||||
}
|
||||
|
||||
const cacheData: MemoryCacheData = {
|
||||
version: this.cacheVersion,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
facts,
|
||||
metadata: {
|
||||
workspacePath: this.workspacePath,
|
||||
totalFacts: facts.length,
|
||||
categories,
|
||||
},
|
||||
}
|
||||
|
||||
await fs.writeFile(this.cacheFilePath, JSON.stringify(cacheData, null, 2), "utf8")
|
||||
console.log(`Saved ${facts.length} facts to cache`)
|
||||
} catch (error) {
|
||||
console.error("Failed to save memory cache:", error)
|
||||
}
|
||||
}
|
||||
|
||||
public async clearCacheFile(): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(this.cacheFilePath)
|
||||
this.memoryCache.clear()
|
||||
console.log("Memory cache cleared")
|
||||
} catch (error) {
|
||||
// File might not exist, which is fine
|
||||
if ((error as any).code !== "ENOENT") {
|
||||
console.error("Failed to clear memory cache:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache operations
|
||||
public getFact(id: string): ConversationFact | undefined {
|
||||
return this.memoryCache.get(id)
|
||||
}
|
||||
|
||||
public setFact(fact: ConversationFact): void {
|
||||
this.memoryCache.set(fact.id, fact)
|
||||
}
|
||||
|
||||
public deleteFact(id: string): boolean {
|
||||
return this.memoryCache.delete(id)
|
||||
}
|
||||
|
||||
public getAllFacts(): ConversationFact[] {
|
||||
return Array.from(this.memoryCache.values())
|
||||
}
|
||||
|
||||
public getFactsByCategory(category: string): ConversationFact[] {
|
||||
return Array.from(this.memoryCache.values()).filter((fact) => fact.category === category)
|
||||
}
|
||||
|
||||
public getFactCount(): number {
|
||||
return this.memoryCache.size
|
||||
}
|
||||
|
||||
// Periodic save
|
||||
private saveTimer: NodeJS.Timeout | undefined
|
||||
|
||||
public scheduleSave(delayMs: number = 5000): void {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer)
|
||||
}
|
||||
|
||||
this.saveTimer = setTimeout(() => {
|
||||
this.saveCache()
|
||||
}, delayMs)
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer)
|
||||
}
|
||||
await this.saveCache()
|
||||
}
|
||||
}
|
||||
177
src/services/conversation-memory/config-manager.ts
Normal file
177
src/services/conversation-memory/config-manager.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import * as vscode from "vscode"
|
||||
import { ContextProxy } from "../../core/config/ContextProxy"
|
||||
|
||||
export interface ConversationMemoryConfig {
|
||||
// Basic enablement
|
||||
enabled: boolean
|
||||
provider: "inherit" | "openai" | "ollama" | "anthropic" | "custom"
|
||||
|
||||
// When provider !== 'inherit', these are used:
|
||||
memoryLLMProvider?: string
|
||||
memoryModelId?: string
|
||||
memoryLLMOptions?: Record<string, any>
|
||||
|
||||
// Memory-specific settings
|
||||
autoExtraction: boolean
|
||||
processingMode: "realtime" | "background" | "manual"
|
||||
factRetentionDays: number
|
||||
maxFactsPerConversation: number
|
||||
conflictResolutionMode: "aggressive" | "conservative" | "manual"
|
||||
|
||||
// Token and cost budgets
|
||||
promptBudgetTokens: number
|
||||
memoryToolDefaultLimit: number
|
||||
dailyProcessingBudgetUSD: number
|
||||
}
|
||||
|
||||
export class ConversationMemoryConfigManager {
|
||||
private currentConfig: ConversationMemoryConfig | undefined
|
||||
private contextProxy: ContextProxy
|
||||
|
||||
constructor(contextProxy: ContextProxy) {
|
||||
this.contextProxy = contextProxy
|
||||
}
|
||||
|
||||
public async loadConfiguration(): Promise<{ requiresRestart: boolean }> {
|
||||
const previousConfig = this.currentConfig
|
||||
|
||||
// Load memory-specific configuration
|
||||
const memoryConfig = vscode.workspace.getConfiguration("roo.conversationMemory")
|
||||
const codeIndexConfig = vscode.workspace.getConfiguration("roo.codeIndex")
|
||||
|
||||
// Determine inheritance mode
|
||||
const inheritanceMode = memoryConfig.get("provider", "inherit")
|
||||
|
||||
if (inheritanceMode === "inherit") {
|
||||
// Inherit from code indexing settings
|
||||
this.currentConfig = {
|
||||
enabled: memoryConfig.get("enabled", false),
|
||||
provider: "inherit",
|
||||
|
||||
// Inherit LLM settings from code indexing
|
||||
memoryLLMProvider: codeIndexConfig.get("embedderProvider"),
|
||||
memoryModelId: codeIndexConfig.get("modelId"),
|
||||
memoryLLMOptions: {
|
||||
...codeIndexConfig.get("openAiOptions", {}),
|
||||
...codeIndexConfig.get("ollamaOptions", {}),
|
||||
...codeIndexConfig.get("geminiOptions", {}),
|
||||
...codeIndexConfig.get("mistralOptions", {}),
|
||||
},
|
||||
|
||||
// Memory-specific settings
|
||||
autoExtraction: memoryConfig.get("autoExtraction", true),
|
||||
processingMode: memoryConfig.get("processingMode", "background"),
|
||||
factRetentionDays: memoryConfig.get("retentionDays", 90),
|
||||
maxFactsPerConversation: memoryConfig.get("maxFacts", 10),
|
||||
conflictResolutionMode: memoryConfig.get("conflictResolutionMode", "conservative"),
|
||||
|
||||
// Budgets
|
||||
promptBudgetTokens: memoryConfig.get("promptBudgetTokens", 400),
|
||||
memoryToolDefaultLimit: memoryConfig.get("memoryToolDefaultLimit", 10),
|
||||
dailyProcessingBudgetUSD: memoryConfig.get("dailyProcessingBudgetUSD", 1.0),
|
||||
}
|
||||
} else {
|
||||
// Independent configuration
|
||||
this.currentConfig = {
|
||||
enabled: memoryConfig.get("enabled", false),
|
||||
provider: memoryConfig.get("provider", "inherit"),
|
||||
memoryLLMProvider: memoryConfig.get("memoryLLMProvider"),
|
||||
memoryModelId: memoryConfig.get("memoryModelId"),
|
||||
memoryLLMOptions: memoryConfig.get("memoryLLMOptions", {}),
|
||||
|
||||
// Memory-specific settings
|
||||
autoExtraction: memoryConfig.get("autoExtraction", true),
|
||||
processingMode: memoryConfig.get("processingMode", "background"),
|
||||
factRetentionDays: memoryConfig.get("retentionDays", 90),
|
||||
maxFactsPerConversation: memoryConfig.get("maxFacts", 10),
|
||||
conflictResolutionMode: memoryConfig.get("conflictResolutionMode", "conservative"),
|
||||
|
||||
// Budgets
|
||||
promptBudgetTokens: memoryConfig.get("promptBudgetTokens", 400),
|
||||
memoryToolDefaultLimit: memoryConfig.get("memoryToolDefaultLimit", 10),
|
||||
dailyProcessingBudgetUSD: memoryConfig.get("dailyProcessingBudgetUSD", 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if restart is required
|
||||
const requiresRestart = this.hasSignificantChanges(previousConfig, this.currentConfig)
|
||||
|
||||
return { requiresRestart }
|
||||
}
|
||||
|
||||
public getConfig(): ConversationMemoryConfig {
|
||||
if (!this.currentConfig) {
|
||||
throw new Error("Configuration not loaded. Call loadConfiguration() first.")
|
||||
}
|
||||
return this.currentConfig
|
||||
}
|
||||
|
||||
public get isFeatureEnabled(): boolean {
|
||||
return this.currentConfig?.enabled ?? false
|
||||
}
|
||||
|
||||
public get isFeatureConfigured(): boolean {
|
||||
if (!this.currentConfig?.enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if we have necessary LLM configuration
|
||||
if (this.currentConfig.provider === "inherit") {
|
||||
// Check if code indexing is configured
|
||||
const codeIndexConfig = vscode.workspace.getConfiguration("roo.codeIndex")
|
||||
return codeIndexConfig.get("enabled", false) && codeIndexConfig.get("embedderProvider") !== undefined
|
||||
}
|
||||
|
||||
// For independent configuration, check if provider is set
|
||||
return this.currentConfig.memoryLLMProvider !== undefined
|
||||
}
|
||||
|
||||
public validateDependencies(): { isValid: boolean; errors: string[]; warnings: string[] } {
|
||||
const codeIndexConfig = vscode.workspace.getConfiguration("roo.codeIndex")
|
||||
const memoryConfig = vscode.workspace.getConfiguration("roo.conversationMemory")
|
||||
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
// Memory system can work independently or inherit from code indexing
|
||||
if (memoryConfig.get("provider") === "inherit") {
|
||||
// Check if code indexing is enabled when inheriting
|
||||
if (!codeIndexConfig.get("enabled", false) && memoryConfig.get("enabled", false)) {
|
||||
warnings.push(
|
||||
"Conversation memory is set to inherit from code indexing, but code indexing is disabled. Consider enabling code indexing or using independent configuration.",
|
||||
)
|
||||
}
|
||||
|
||||
// Check embedder configuration
|
||||
const provider = codeIndexConfig.get("embedderProvider")
|
||||
if (!provider && memoryConfig.get("provider") === "inherit") {
|
||||
errors.push(
|
||||
"No embedder provider configured for code indexing. Configure code indexing first or set independent memory provider.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
private hasSignificantChanges(
|
||||
oldConfig: ConversationMemoryConfig | undefined,
|
||||
newConfig: ConversationMemoryConfig,
|
||||
): boolean {
|
||||
if (!oldConfig) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for changes that require restart
|
||||
return (
|
||||
oldConfig.provider !== newConfig.provider ||
|
||||
oldConfig.memoryLLMProvider !== newConfig.memoryLLMProvider ||
|
||||
oldConfig.memoryModelId !== newConfig.memoryModelId ||
|
||||
JSON.stringify(oldConfig.memoryLLMOptions) !== JSON.stringify(newConfig.memoryLLMOptions)
|
||||
)
|
||||
}
|
||||
}
|
||||
130
src/services/conversation-memory/interfaces/index.ts
Normal file
130
src/services/conversation-memory/interfaces/index.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Core interfaces for the conversation memory system
|
||||
|
||||
export interface ConversationFact {
|
||||
id: string
|
||||
content: string
|
||||
category: FactCategory
|
||||
confidence: number
|
||||
|
||||
// Bi-temporal tracking (Graphiti pattern)
|
||||
reference_time: Date // When the fact/decision happened
|
||||
ingestion_time: Date // When we recorded it
|
||||
|
||||
// Lifecycle tracking (mem0 pattern)
|
||||
superseded_by?: string // ID of fact that replaces this
|
||||
superseded_at?: Date // When it was superseded
|
||||
resolved?: boolean // For debugging facts
|
||||
resolved_at?: Date // When it was resolved
|
||||
derived_from?: string // For patterns derived from incidents
|
||||
derived_pattern_created?: boolean // For debugging incidents after promotion
|
||||
last_confirmed?: Date // Last validation
|
||||
|
||||
// Context
|
||||
workspace_path: string
|
||||
project_context: ProjectContext
|
||||
conversation_context: string
|
||||
|
||||
// Vector storage
|
||||
embedding: number[]
|
||||
|
||||
// Metadata
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export enum FactCategory {
|
||||
INFRASTRUCTURE = "infrastructure", // Core tech stack - persistent
|
||||
ARCHITECTURE = "architecture", // Design decisions - evolving
|
||||
DEBUGGING = "debugging", // Temporary problems - expire
|
||||
PATTERN = "pattern", // Solution wisdom - persistent (including promoted incident lessons)
|
||||
}
|
||||
|
||||
export interface ProjectContext {
|
||||
language: "typescript" | "python" | "rust" | "go" | "java" | "unknown"
|
||||
framework?: string
|
||||
workspaceName: string
|
||||
packageManager?: "npm" | "yarn" | "pnpm" | "pip" | "cargo" | "maven"
|
||||
}
|
||||
|
||||
export interface MemoryAction {
|
||||
type: "ADD" | "UPDATE" | "DELETE" | "NONE"
|
||||
fact: CategorizedFact
|
||||
target_id?: string // For UPDATE/DELETE
|
||||
reasoning: string
|
||||
}
|
||||
|
||||
export interface CategorizedFact {
|
||||
content: string
|
||||
category: FactCategory
|
||||
confidence: number
|
||||
embedding: number[]
|
||||
reference_time?: Date
|
||||
context_description?: string
|
||||
reasoning?: string
|
||||
tags?: string[]
|
||||
subtype?: string
|
||||
}
|
||||
|
||||
export interface ConversationEpisode {
|
||||
messages: Message[]
|
||||
reference_time: Date
|
||||
workspace_path: string
|
||||
context_description: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: "user" | "assistant" | "system"
|
||||
content: string
|
||||
timestamp?: Date
|
||||
}
|
||||
|
||||
export interface FactConflictGroup {
|
||||
newFact: CategorizedFact
|
||||
existingFacts: ConversationFact[]
|
||||
}
|
||||
|
||||
export interface MemoryStatus {
|
||||
systemState: string
|
||||
systemMessage: string
|
||||
processedEpisodes: number
|
||||
totalEpisodes: number
|
||||
}
|
||||
|
||||
export interface MemorySearchOptions {
|
||||
limit?: number
|
||||
category?: FactCategory
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
// Service interfaces
|
||||
export interface IFactExtractor {
|
||||
extractFacts(messages: Message[], projectContext: ProjectContext): Promise<CategorizedFact[]>
|
||||
}
|
||||
|
||||
export interface IConflictResolver {
|
||||
resolveConflicts(
|
||||
newFacts: CategorizedFact[],
|
||||
existingFacts: ConversationFact[],
|
||||
context: ProjectContext,
|
||||
): Promise<MemoryAction[]>
|
||||
}
|
||||
|
||||
export interface ITemporalManager {
|
||||
cleanupExpiredFacts(): Promise<void>
|
||||
calculateTemporalScore(fact: ConversationFact): number
|
||||
markFactResolved(factId: string): Promise<void>
|
||||
supersedeFact(oldFactId: string, newFactId: string): Promise<void>
|
||||
promoteResolvedDebuggingToPattern(fact: ConversationFact, episode?: ConversationEpisode): Promise<void>
|
||||
}
|
||||
|
||||
export interface IConversationProcessor {
|
||||
processEpisode(episode: ConversationEpisode): Promise<void>
|
||||
}
|
||||
|
||||
export interface IMemoryVectorStore {
|
||||
insert(embeddings: number[][], ids: string[], payloads: any[]): Promise<void>
|
||||
search(query: string, embedding: number[], limit: number, filter?: any): Promise<any[]>
|
||||
get(id: string): Promise<any | null>
|
||||
update(id: string, vector: number[], payload: any): Promise<void>
|
||||
delete(id: string): Promise<void>
|
||||
clear(): Promise<void>
|
||||
}
|
||||
452
src/services/conversation-memory/manager.ts
Normal file
452
src/services/conversation-memory/manager.ts
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
import * as vscode from "vscode"
|
||||
import { createHash } from "crypto"
|
||||
import { ContextProxy } from "../../core/config/ContextProxy"
|
||||
import {
|
||||
ConversationFact,
|
||||
ConversationEpisode,
|
||||
Message,
|
||||
MemoryStatus,
|
||||
MemorySearchOptions,
|
||||
ProjectContext,
|
||||
} from "./interfaces"
|
||||
import { ConversationMemoryConfigManager } from "./config-manager"
|
||||
import { ConversationMemoryStateManager } from "./state-manager"
|
||||
import { ConversationMemoryServiceFactory } from "./service-factory"
|
||||
import { ConversationMemoryOrchestrator } from "./orchestrator"
|
||||
import { ConversationMemorySearchService } from "./search-service"
|
||||
import { ConversationMemoryCacheManager } from "./cache-manager"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
||||
export class ConversationMemoryManager {
|
||||
// --- Singleton Implementation (mirrors CodeIndexManager) ---
|
||||
private static instances = new Map<string, ConversationMemoryManager>()
|
||||
|
||||
// Service dependencies - following CodeIndex patterns
|
||||
private _configManager: ConversationMemoryConfigManager | undefined
|
||||
private readonly _stateManager: ConversationMemoryStateManager
|
||||
private _serviceFactory: ConversationMemoryServiceFactory | undefined
|
||||
private _orchestrator: ConversationMemoryOrchestrator | undefined
|
||||
private _searchService: ConversationMemorySearchService | undefined
|
||||
private _cacheManager: ConversationMemoryCacheManager | undefined
|
||||
|
||||
// Project context for this workspace
|
||||
private _projectContext: ProjectContext | undefined
|
||||
|
||||
// Flag to prevent race conditions during error recovery
|
||||
private _isRecoveringFromError = false
|
||||
|
||||
public static getInstance(
|
||||
context: vscode.ExtensionContext,
|
||||
workspacePath?: string,
|
||||
): ConversationMemoryManager | undefined {
|
||||
// Exact same workspace discovery logic as CodeIndexManager
|
||||
if (!workspacePath) {
|
||||
const activeEditor = vscode.window.activeTextEditor
|
||||
if (activeEditor) {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)
|
||||
workspacePath = workspaceFolder?.uri.fsPath
|
||||
}
|
||||
|
||||
if (!workspacePath) {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
workspacePath = workspaceFolders[0].uri.fsPath
|
||||
}
|
||||
}
|
||||
|
||||
if (!ConversationMemoryManager.instances.has(workspacePath)) {
|
||||
ConversationMemoryManager.instances.set(
|
||||
workspacePath,
|
||||
new ConversationMemoryManager(workspacePath, context),
|
||||
)
|
||||
}
|
||||
return ConversationMemoryManager.instances.get(workspacePath)!
|
||||
}
|
||||
|
||||
public static async initializeAll(context: vscode.ExtensionContext): Promise<void> {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders || []
|
||||
|
||||
for (const folder of workspaceFolders) {
|
||||
try {
|
||||
const manager = ConversationMemoryManager.getInstance(context, folder.uri.fsPath)
|
||||
if (manager) {
|
||||
const contextProxy = new ContextProxy(context)
|
||||
await manager.initialize(contextProxy)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Memory manager initialization failed for ${folder.uri.fsPath}:`, error)
|
||||
// Continue - don't break other workspaces
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for workspace changes
|
||||
vscode.workspace.onDidChangeWorkspaceFolders(async (event) => {
|
||||
// Handle workspace additions
|
||||
for (const added of event.added) {
|
||||
try {
|
||||
const manager = ConversationMemoryManager.getInstance(context, added.uri.fsPath)
|
||||
if (manager) {
|
||||
const contextProxy = new ContextProxy(context)
|
||||
await manager.initialize(contextProxy)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to initialize memory manager for ${added.uri.fsPath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle workspace removals
|
||||
for (const removed of event.removed) {
|
||||
const manager = ConversationMemoryManager.instances.get(removed.uri.fsPath)
|
||||
if (manager) {
|
||||
manager.dispose()
|
||||
ConversationMemoryManager.instances.delete(removed.uri.fsPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static getCurrentWorkspaceManager(): ConversationMemoryManager | undefined {
|
||||
const activeEditor = vscode.window.activeTextEditor
|
||||
if (!activeEditor) return undefined
|
||||
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)
|
||||
if (!workspaceFolder) return undefined
|
||||
|
||||
return this.instances.get(workspaceFolder.uri.fsPath)
|
||||
}
|
||||
|
||||
public static disposeAll(): void {
|
||||
for (const instance of ConversationMemoryManager.instances.values()) {
|
||||
instance.dispose()
|
||||
}
|
||||
ConversationMemoryManager.instances.clear()
|
||||
}
|
||||
|
||||
private readonly workspacePath: string
|
||||
private readonly context: vscode.ExtensionContext
|
||||
|
||||
// Private constructor for singleton pattern
|
||||
private constructor(workspacePath: string, context: vscode.ExtensionContext) {
|
||||
this.workspacePath = workspacePath
|
||||
this.context = context
|
||||
this._stateManager = new ConversationMemoryStateManager()
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
public get onProgressUpdate() {
|
||||
return this._stateManager.onProgressUpdate
|
||||
}
|
||||
|
||||
private assertInitialized() {
|
||||
if (!this._configManager || !this._orchestrator || !this._searchService || !this._cacheManager) {
|
||||
throw new Error("ConversationMemoryManager not initialized. Call initialize() first.")
|
||||
}
|
||||
}
|
||||
|
||||
public get isFeatureEnabled(): boolean {
|
||||
return this._configManager?.isFeatureEnabled ?? false
|
||||
}
|
||||
|
||||
public get isFeatureConfigured(): boolean {
|
||||
return this._configManager?.isFeatureConfigured ?? false
|
||||
}
|
||||
|
||||
public get isInitialized(): boolean {
|
||||
try {
|
||||
this.assertInitialized()
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> {
|
||||
// 1. ConfigManager Initialization and Configuration Loading
|
||||
if (!this._configManager) {
|
||||
this._configManager = new ConversationMemoryConfigManager(contextProxy)
|
||||
}
|
||||
const { requiresRestart } = await this._configManager.loadConfiguration()
|
||||
|
||||
// 2. Check if feature is enabled
|
||||
if (!this.isFeatureEnabled) {
|
||||
if (this._orchestrator) {
|
||||
this._orchestrator.stopProcessing()
|
||||
}
|
||||
return { requiresRestart }
|
||||
}
|
||||
|
||||
// 3. Detect project context
|
||||
this._projectContext = await this.detectProjectContext()
|
||||
|
||||
// 4. CacheManager Initialization
|
||||
if (!this._cacheManager) {
|
||||
this._cacheManager = new ConversationMemoryCacheManager(this.context, this.workspacePath)
|
||||
await this._cacheManager.initialize()
|
||||
}
|
||||
|
||||
// 5. Determine if Core Services Need Recreation
|
||||
const needsServiceRecreation = !this._serviceFactory || requiresRestart
|
||||
|
||||
if (needsServiceRecreation) {
|
||||
await this._recreateServices()
|
||||
}
|
||||
|
||||
return { requiresRestart }
|
||||
}
|
||||
|
||||
public async searchMemory(query: string, options?: MemorySearchOptions): Promise<ConversationFact[]> {
|
||||
if (!this.isFeatureEnabled) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
this.assertInitialized()
|
||||
return await this._searchService!.searchMemory(query, options)
|
||||
} catch (error) {
|
||||
console.warn("Memory search failed:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public async processConversation(messages: Message[]): Promise<void> {
|
||||
if (!this.isFeatureEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.assertInitialized()
|
||||
|
||||
const episode: ConversationEpisode = {
|
||||
messages,
|
||||
reference_time: new Date(),
|
||||
workspace_path: this.workspacePath,
|
||||
context_description: "Interactive conversation",
|
||||
}
|
||||
|
||||
await this._orchestrator!.processConversationEpisode(episode)
|
||||
} catch (error) {
|
||||
console.warn("Memory processing failed:", error)
|
||||
// Never throw - don't break existing flows
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentMemoryStatus(): MemoryStatus {
|
||||
return this._stateManager.getCurrentStatus()
|
||||
}
|
||||
|
||||
public async recoverFromError(): Promise<void> {
|
||||
// Prevent race conditions from multiple rapid recovery attempts
|
||||
if (this._isRecoveringFromError) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isRecoveringFromError = true
|
||||
try {
|
||||
// Clear error state
|
||||
this._stateManager.setSystemState("Standby", "")
|
||||
} catch (error) {
|
||||
console.error("Failed to clear error state during recovery:", error)
|
||||
} finally {
|
||||
// Force re-initialization by clearing service instances
|
||||
this._configManager = undefined
|
||||
this._serviceFactory = undefined
|
||||
this._orchestrator = undefined
|
||||
this._searchService = undefined
|
||||
|
||||
this._isRecoveringFromError = false
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._orchestrator) {
|
||||
this._orchestrator.stopProcessing()
|
||||
}
|
||||
this._stateManager.dispose()
|
||||
}
|
||||
|
||||
public async clearMemoryData(): Promise<void> {
|
||||
if (!this.isFeatureEnabled) {
|
||||
return
|
||||
}
|
||||
this.assertInitialized()
|
||||
await this._orchestrator!.clearMemoryData()
|
||||
await this._cacheManager!.clearCacheFile()
|
||||
}
|
||||
|
||||
// --- Private Helpers ---
|
||||
|
||||
private async detectProjectContext(): Promise<ProjectContext> {
|
||||
const workspaceFiles = await this.scanWorkspaceFiles()
|
||||
|
||||
let language: ProjectContext["language"] = "unknown"
|
||||
let framework: string | undefined
|
||||
let packageManager: ProjectContext["packageManager"]
|
||||
|
||||
if (workspaceFiles.includes("package.json")) {
|
||||
language = "typescript"
|
||||
packageManager = workspaceFiles.includes("yarn.lock")
|
||||
? "yarn"
|
||||
: workspaceFiles.includes("pnpm-lock.yaml")
|
||||
? "pnpm"
|
||||
: "npm"
|
||||
|
||||
// Framework detection from package.json
|
||||
try {
|
||||
const packageJsonPath = path.join(this.workspacePath, "package.json")
|
||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"))
|
||||
|
||||
if (packageJson.dependencies?.react || packageJson.devDependencies?.react) {
|
||||
framework = "react"
|
||||
} else if (packageJson.dependencies?.next || packageJson.devDependencies?.next) {
|
||||
framework = "nextjs"
|
||||
} else if (packageJson.dependencies?.express || packageJson.devDependencies?.express) {
|
||||
framework = "express"
|
||||
} else if (packageJson.dependencies?.vue || packageJson.devDependencies?.vue) {
|
||||
framework = "vue"
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors reading package.json
|
||||
}
|
||||
} else if (workspaceFiles.includes("requirements.txt") || workspaceFiles.includes("pyproject.toml")) {
|
||||
language = "python"
|
||||
packageManager = "pip"
|
||||
|
||||
// Framework detection
|
||||
try {
|
||||
const requirementsPath = path.join(this.workspacePath, "requirements.txt")
|
||||
const requirements = await fs.readFile(requirementsPath, "utf8")
|
||||
|
||||
if (requirements.includes("django")) framework = "django"
|
||||
else if (requirements.includes("fastapi")) framework = "fastapi"
|
||||
else if (requirements.includes("flask")) framework = "flask"
|
||||
else if (requirements.includes("pandas")) framework = "data-science"
|
||||
} catch (error) {
|
||||
// Ignore errors reading requirements
|
||||
}
|
||||
} else if (workspaceFiles.includes("Cargo.toml")) {
|
||||
language = "rust"
|
||||
packageManager = "cargo"
|
||||
} else if (workspaceFiles.includes("go.mod")) {
|
||||
language = "go"
|
||||
} else if (workspaceFiles.includes("pom.xml")) {
|
||||
language = "java"
|
||||
packageManager = "maven"
|
||||
}
|
||||
|
||||
return {
|
||||
language,
|
||||
framework,
|
||||
packageManager,
|
||||
workspaceName: path.basename(this.workspacePath),
|
||||
}
|
||||
}
|
||||
|
||||
private async scanWorkspaceFiles(): Promise<string[]> {
|
||||
try {
|
||||
const files = await fs.readdir(this.workspacePath)
|
||||
return files
|
||||
} catch (error) {
|
||||
console.error("Failed to scan workspace files:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async _recreateServices(): Promise<void> {
|
||||
// Stop processing if it exists
|
||||
if (this._orchestrator) {
|
||||
this._orchestrator.stopProcessing()
|
||||
}
|
||||
|
||||
// Clear existing services to ensure clean state
|
||||
this._orchestrator = undefined
|
||||
this._searchService = undefined
|
||||
|
||||
// (Re)Initialize service factory
|
||||
this._serviceFactory = new ConversationMemoryServiceFactory(
|
||||
this._configManager!,
|
||||
this.workspacePath,
|
||||
this._cacheManager!,
|
||||
this._projectContext!,
|
||||
)
|
||||
|
||||
// Create service instances
|
||||
const services = await this._serviceFactory.createServices(this.context)
|
||||
|
||||
// Validate configuration before proceeding
|
||||
const validationResult = await this._serviceFactory.validateConfiguration()
|
||||
if (!validationResult.valid) {
|
||||
const errorMessage = validationResult.error || "Memory service configuration validation failed"
|
||||
this._stateManager.setSystemState("Error", errorMessage)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// (Re)Initialize orchestrator
|
||||
this._orchestrator = new ConversationMemoryOrchestrator(
|
||||
this._configManager!,
|
||||
this._stateManager,
|
||||
this.workspacePath,
|
||||
this._cacheManager!,
|
||||
services.vectorStore,
|
||||
services.factExtractor,
|
||||
services.conflictResolver,
|
||||
services.temporalManager,
|
||||
services.conversationProcessor,
|
||||
)
|
||||
|
||||
// (Re)Initialize search service
|
||||
this._searchService = new ConversationMemorySearchService(
|
||||
this._configManager!,
|
||||
this._stateManager,
|
||||
services.embedder,
|
||||
services.vectorStore,
|
||||
services.temporalManager,
|
||||
)
|
||||
|
||||
// Clear any error state after successful recreation
|
||||
this._stateManager.setSystemState("Standby", "")
|
||||
}
|
||||
|
||||
public async handleSettingsChange(): Promise<void> {
|
||||
if (this._configManager) {
|
||||
const { requiresRestart } = await this._configManager.loadConfiguration()
|
||||
|
||||
const isFeatureEnabled = this.isFeatureEnabled
|
||||
const isFeatureConfigured = this.isFeatureConfigured
|
||||
|
||||
// If feature is disabled, stop the service
|
||||
if (!isFeatureEnabled) {
|
||||
if (this._orchestrator) {
|
||||
this._orchestrator.stopProcessing()
|
||||
}
|
||||
this._stateManager.setSystemState("Standby", "Conversation memory is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresRestart && isFeatureEnabled && isFeatureConfigured) {
|
||||
try {
|
||||
// Ensure cacheManager is initialized before recreating services
|
||||
if (!this._cacheManager) {
|
||||
this._cacheManager = new ConversationMemoryCacheManager(this.context, this.workspacePath)
|
||||
await this._cacheManager.initialize()
|
||||
}
|
||||
|
||||
// Recreate services with new configuration
|
||||
await this._recreateServices()
|
||||
} catch (error) {
|
||||
console.error("Failed to recreate memory services:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique collection name for this workspace
|
||||
public getMemoryCollectionName(): string {
|
||||
const hash = createHash("sha256").update(this.workspacePath).digest("hex")
|
||||
return `ws-${hash.substring(0, 16)}-memory`
|
||||
}
|
||||
}
|
||||
118
src/services/conversation-memory/orchestrator.ts
Normal file
118
src/services/conversation-memory/orchestrator.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { ConversationMemoryConfigManager } from "./config-manager"
|
||||
import { ConversationMemoryStateManager } from "./state-manager"
|
||||
import { ConversationMemoryCacheManager } from "./cache-manager"
|
||||
import {
|
||||
IMemoryVectorStore,
|
||||
IFactExtractor,
|
||||
IConflictResolver,
|
||||
ITemporalManager,
|
||||
IConversationProcessor,
|
||||
ConversationEpisode,
|
||||
ConversationFact,
|
||||
FactCategory,
|
||||
} from "./interfaces"
|
||||
|
||||
export class ConversationMemoryOrchestrator {
|
||||
private isProcessing = false
|
||||
|
||||
constructor(
|
||||
private readonly configManager: ConversationMemoryConfigManager,
|
||||
private readonly stateManager: ConversationMemoryStateManager,
|
||||
private readonly workspacePath: string,
|
||||
private readonly cacheManager: ConversationMemoryCacheManager,
|
||||
private readonly vectorStore: IMemoryVectorStore,
|
||||
private readonly factExtractor: IFactExtractor,
|
||||
private readonly conflictResolver: IConflictResolver,
|
||||
private readonly temporalManager: ITemporalManager,
|
||||
private readonly conversationProcessor: IConversationProcessor,
|
||||
) {}
|
||||
|
||||
public get state(): string {
|
||||
return this.isProcessing ? "Processing" : "Standby"
|
||||
}
|
||||
|
||||
public async processConversationEpisode(episode: ConversationEpisode): Promise<void> {
|
||||
if (this.isProcessing) {
|
||||
console.log("Already processing a conversation episode")
|
||||
return
|
||||
}
|
||||
|
||||
this.isProcessing = true
|
||||
this.stateManager.setProcessingState("Processing conversation episode")
|
||||
|
||||
try {
|
||||
// 1. Extract facts from the conversation
|
||||
const projectContext = {
|
||||
language: "typescript" as const,
|
||||
workspaceName: this.workspacePath,
|
||||
framework: undefined,
|
||||
packageManager: "npm" as const,
|
||||
}
|
||||
|
||||
const newFacts = await this.factExtractor.extractFacts(episode.messages, projectContext)
|
||||
|
||||
if (newFacts.length === 0) {
|
||||
console.log("No facts extracted from conversation")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Find conflicting facts via vector similarity
|
||||
const existingFacts = this.cacheManager.getAllFacts()
|
||||
|
||||
// 3. Resolve conflicts
|
||||
const memoryActions = await this.conflictResolver.resolveConflicts(newFacts, existingFacts, projectContext)
|
||||
|
||||
// 4. Apply memory actions
|
||||
for (const action of memoryActions) {
|
||||
if (action.type === "ADD") {
|
||||
const fact: ConversationFact = {
|
||||
id: this.generateFactId(),
|
||||
content: action.fact.content,
|
||||
category: action.fact.category,
|
||||
confidence: action.fact.confidence,
|
||||
reference_time: episode.reference_time,
|
||||
ingestion_time: new Date(),
|
||||
workspace_path: this.workspacePath,
|
||||
project_context: projectContext,
|
||||
conversation_context: episode.context_description,
|
||||
embedding: action.fact.embedding,
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
// Store in cache and vector store
|
||||
this.cacheManager.setFact(fact)
|
||||
await this.vectorStore.insert([fact.embedding], [fact.id], [fact])
|
||||
}
|
||||
// TODO: Handle UPDATE and DELETE actions
|
||||
}
|
||||
|
||||
// 5. Cleanup expired facts
|
||||
await this.temporalManager.cleanupExpiredFacts()
|
||||
|
||||
// 6. Save cache
|
||||
await this.cacheManager.saveCache()
|
||||
|
||||
this.stateManager.setSystemState("Standby", "Processing complete")
|
||||
} catch (error) {
|
||||
console.error("Error processing conversation episode:", error)
|
||||
this.stateManager.setError(`Processing failed: ${error}`)
|
||||
} finally {
|
||||
this.isProcessing = false
|
||||
}
|
||||
}
|
||||
|
||||
public stopProcessing(): void {
|
||||
this.isProcessing = false
|
||||
this.stateManager.setSystemState("Standby", "Processing stopped")
|
||||
}
|
||||
|
||||
public async clearMemoryData(): Promise<void> {
|
||||
await this.vectorStore.clear()
|
||||
await this.cacheManager.clearCacheFile()
|
||||
this.stateManager.setSystemState("Standby", "Memory data cleared")
|
||||
}
|
||||
|
||||
private generateFactId(): string {
|
||||
return `fact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
}
|
||||
135
src/services/conversation-memory/search-service.ts
Normal file
135
src/services/conversation-memory/search-service.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { ConversationMemoryConfigManager } from "./config-manager"
|
||||
import { ConversationMemoryStateManager } from "./state-manager"
|
||||
import { IEmbedder } from "../code-index/interfaces"
|
||||
import { IMemoryVectorStore, ITemporalManager, ConversationFact, MemorySearchOptions, FactCategory } from "./interfaces"
|
||||
|
||||
export class ConversationMemorySearchService {
|
||||
constructor(
|
||||
private readonly configManager: ConversationMemoryConfigManager,
|
||||
private readonly stateManager: ConversationMemoryStateManager,
|
||||
private readonly embedder: IEmbedder,
|
||||
private readonly vectorStore: IMemoryVectorStore,
|
||||
private readonly temporalManager: ITemporalManager,
|
||||
) {}
|
||||
|
||||
public async searchMemory(query: string, options?: MemorySearchOptions): Promise<ConversationFact[]> {
|
||||
try {
|
||||
// 1. Generate query embedding
|
||||
const embeddingResponse = await this.embedder.createEmbeddings([query])
|
||||
const queryEmbedding = embeddingResponse.embeddings[0]
|
||||
|
||||
// 2. Build search filter
|
||||
const filter: any = {}
|
||||
if (options?.category) {
|
||||
filter.category = options.category
|
||||
}
|
||||
if (options?.tags && options.tags.length > 0) {
|
||||
filter.tags = { $in: options.tags }
|
||||
}
|
||||
|
||||
// 3. Search vector store
|
||||
const limit = options?.limit || 10
|
||||
const rawResults = await this.vectorStore.search(
|
||||
query,
|
||||
queryEmbedding,
|
||||
limit * 2, // Get more results for temporal filtering
|
||||
filter,
|
||||
)
|
||||
|
||||
// 4. Apply temporal scoring and filtering
|
||||
const scoredResults = rawResults.map((result) => {
|
||||
const fact = result.payload as ConversationFact
|
||||
const temporalScore = this.temporalManager.calculateTemporalScore(fact)
|
||||
return {
|
||||
fact,
|
||||
temporalScore,
|
||||
similarityScore: result.score || 0,
|
||||
}
|
||||
})
|
||||
|
||||
// 5. Filter by temporal relevance and sort
|
||||
const relevantResults = scoredResults
|
||||
.filter((result) => result.temporalScore > 0.3) // Minimum relevance threshold
|
||||
.sort((a, b) => {
|
||||
// Combine similarity and temporal scores
|
||||
const aScore = a.similarityScore * 0.7 + a.temporalScore * 0.3
|
||||
const bScore = b.similarityScore * 0.7 + b.temporalScore * 0.3
|
||||
return bScore - aScore
|
||||
})
|
||||
.slice(0, limit)
|
||||
.map((result) => result.fact)
|
||||
|
||||
return relevantResults
|
||||
} catch (error) {
|
||||
this.stateManager.setError(`Memory search failed: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async getRelevantMemoryForPrompt(userMessage: string, maxTokens: number = 400): Promise<string> {
|
||||
// Heuristics for automatic memory retrieval
|
||||
const messageLower = userMessage.toLowerCase()
|
||||
|
||||
let category: FactCategory | undefined
|
||||
let searchQuery = userMessage
|
||||
|
||||
// Detect intent from message
|
||||
if (messageLower.includes("error") || messageLower.includes("bug") || messageLower.includes("fix")) {
|
||||
category = FactCategory.DEBUGGING
|
||||
} else if (
|
||||
messageLower.includes("architecture") ||
|
||||
messageLower.includes("design") ||
|
||||
messageLower.includes("approach")
|
||||
) {
|
||||
category = FactCategory.ARCHITECTURE
|
||||
} else if (
|
||||
messageLower.includes("database") ||
|
||||
messageLower.includes("deploy") ||
|
||||
messageLower.includes("setup")
|
||||
) {
|
||||
category = FactCategory.INFRASTRUCTURE
|
||||
}
|
||||
|
||||
// Search for relevant memories
|
||||
const memories = await this.searchMemory(searchQuery, {
|
||||
category,
|
||||
limit: 6,
|
||||
})
|
||||
|
||||
if (memories.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Format memories for prompt injection
|
||||
const formattedMemories = memories.map((fact) => {
|
||||
const date = fact.reference_time.toLocaleDateString()
|
||||
const categoryLabel = fact.category.toUpperCase()
|
||||
let annotation = ""
|
||||
|
||||
if (fact.superseded_by) {
|
||||
annotation = " (superseded)"
|
||||
} else if (fact.resolved) {
|
||||
annotation = " (resolved)"
|
||||
} else if (fact.derived_from) {
|
||||
annotation = " (derived from incident)"
|
||||
}
|
||||
|
||||
return `- ${categoryLabel}: ${fact.content}${annotation} (${date})`
|
||||
})
|
||||
|
||||
const memorySection = `# Relevant Memory (auto)\n${formattedMemories.join("\n")}`
|
||||
|
||||
// Simple token estimation (rough approximation)
|
||||
const estimatedTokens = memorySection.length / 4
|
||||
if (estimatedTokens > maxTokens) {
|
||||
// Truncate if too long
|
||||
const truncatedMemories = formattedMemories.slice(
|
||||
0,
|
||||
Math.floor(formattedMemories.length * ((maxTokens * 4) / memorySection.length)),
|
||||
)
|
||||
return `# Relevant Memory (auto)\n${truncatedMemories.join("\n")}`
|
||||
}
|
||||
|
||||
return memorySection
|
||||
}
|
||||
}
|
||||
164
src/services/conversation-memory/service-factory.ts
Normal file
164
src/services/conversation-memory/service-factory.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import * as vscode from "vscode"
|
||||
import { ConversationMemoryConfigManager } from "./config-manager"
|
||||
import { ConversationMemoryCacheManager } from "./cache-manager"
|
||||
import {
|
||||
ProjectContext,
|
||||
IFactExtractor,
|
||||
IConflictResolver,
|
||||
ITemporalManager,
|
||||
IConversationProcessor,
|
||||
IMemoryVectorStore,
|
||||
} from "./interfaces"
|
||||
import { IEmbedder } from "../code-index/interfaces"
|
||||
|
||||
export interface ConversationMemoryServices {
|
||||
embedder: IEmbedder
|
||||
vectorStore: IMemoryVectorStore
|
||||
factExtractor: IFactExtractor
|
||||
conflictResolver: IConflictResolver
|
||||
temporalManager: ITemporalManager
|
||||
conversationProcessor: IConversationProcessor
|
||||
}
|
||||
|
||||
export class ConversationMemoryServiceFactory {
|
||||
constructor(
|
||||
private readonly configManager: ConversationMemoryConfigManager,
|
||||
private readonly workspacePath: string,
|
||||
private readonly cacheManager: ConversationMemoryCacheManager,
|
||||
private readonly projectContext: ProjectContext,
|
||||
) {}
|
||||
|
||||
public async createServices(context: vscode.ExtensionContext): Promise<ConversationMemoryServices> {
|
||||
// TODO: Implement actual service creation
|
||||
// For now, return stub implementations
|
||||
|
||||
const embedder = this.createEmbedder()
|
||||
const vectorStore = this.createVectorStore()
|
||||
const factExtractor = this.createFactExtractor()
|
||||
const conflictResolver = this.createConflictResolver()
|
||||
const temporalManager = this.createTemporalManager()
|
||||
const conversationProcessor = this.createConversationProcessor()
|
||||
|
||||
return {
|
||||
embedder,
|
||||
vectorStore,
|
||||
factExtractor,
|
||||
conflictResolver,
|
||||
temporalManager,
|
||||
conversationProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
public async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
// TODO: Implement configuration validation
|
||||
// Check if LLM provider is configured, embedder is available, etc.
|
||||
|
||||
const config = this.configManager.getConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
return { valid: false, error: "Conversation memory is not enabled" }
|
||||
}
|
||||
|
||||
// For now, assume configuration is valid if enabled
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
private createEmbedder(): IEmbedder {
|
||||
// TODO: Create actual embedder based on configuration
|
||||
// For now, return a stub
|
||||
return {
|
||||
embed: async (text: string) => {
|
||||
// Return a dummy embedding vector
|
||||
return new Array(384).fill(0).map(() => Math.random())
|
||||
},
|
||||
embedBatch: async (texts: string[]) => {
|
||||
return texts.map(() => new Array(384).fill(0).map(() => Math.random()))
|
||||
},
|
||||
createEmbeddings: async (texts: string[]) => {
|
||||
return texts.map(() => new Array(384).fill(0).map(() => Math.random()))
|
||||
},
|
||||
validateConfiguration: async () => {
|
||||
return { valid: true }
|
||||
},
|
||||
embedderInfo: {
|
||||
model: "stub",
|
||||
dimensions: 384,
|
||||
maxInputTokens: 8192,
|
||||
},
|
||||
} as IEmbedder
|
||||
}
|
||||
|
||||
private createVectorStore(): IMemoryVectorStore {
|
||||
// TODO: Create actual vector store (Qdrant integration)
|
||||
// For now, return an in-memory stub
|
||||
const store = new Map<string, { vector: number[]; payload: any }>()
|
||||
|
||||
return {
|
||||
insert: async (embeddings: number[][], ids: string[], payloads: any[]) => {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
store.set(ids[i], { vector: embeddings[i], payload: payloads[i] })
|
||||
}
|
||||
},
|
||||
search: async (query: string, embedding: number[], limit: number, filter?: any) => {
|
||||
// Return all items for now (no actual similarity search)
|
||||
return Array.from(store.values()).slice(0, limit)
|
||||
},
|
||||
get: async (id: string) => {
|
||||
return store.get(id) || null
|
||||
},
|
||||
update: async (id: string, vector: number[], payload: any) => {
|
||||
store.set(id, { vector, payload })
|
||||
},
|
||||
delete: async (id: string) => {
|
||||
store.delete(id)
|
||||
},
|
||||
clear: async () => {
|
||||
store.clear()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createFactExtractor(): IFactExtractor {
|
||||
// TODO: Implement LLM-based fact extraction
|
||||
return {
|
||||
extractFacts: async (messages, projectContext) => {
|
||||
// Stub implementation
|
||||
return []
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createConflictResolver(): IConflictResolver {
|
||||
// TODO: Implement LLM-based conflict resolution
|
||||
return {
|
||||
resolveConflicts: async (newFacts, existingFacts, context) => {
|
||||
// Stub implementation - just add all new facts
|
||||
return newFacts.map((fact) => ({
|
||||
type: "ADD" as const,
|
||||
fact,
|
||||
reasoning: "No conflict detection implemented yet",
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createTemporalManager(): ITemporalManager {
|
||||
// TODO: Implement temporal lifecycle management
|
||||
return {
|
||||
cleanupExpiredFacts: async () => {},
|
||||
calculateTemporalScore: (fact) => fact.confidence,
|
||||
markFactResolved: async (factId) => {},
|
||||
supersedeFact: async (oldFactId, newFactId) => {},
|
||||
promoteResolvedDebuggingToPattern: async (fact, episode) => {},
|
||||
}
|
||||
}
|
||||
|
||||
private createConversationProcessor(): IConversationProcessor {
|
||||
// TODO: Implement conversation processing pipeline
|
||||
return {
|
||||
processEpisode: async (episode) => {
|
||||
console.log("Processing conversation episode:", episode.context_description)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/services/conversation-memory/state-manager.ts
Normal file
82
src/services/conversation-memory/state-manager.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import * as vscode from "vscode"
|
||||
import { MemoryStatus } from "./interfaces"
|
||||
|
||||
export interface ProgressData {
|
||||
processedEpisodes: number
|
||||
totalEpisodes: number
|
||||
}
|
||||
|
||||
export interface ProgressUpdate {
|
||||
state: string
|
||||
message: string
|
||||
progress?: ProgressData
|
||||
}
|
||||
|
||||
export class ConversationMemoryStateManager {
|
||||
private _systemState: string = "Standby"
|
||||
private _systemMessage: string = ""
|
||||
private _progressData: ProgressData = { processedEpisodes: 0, totalEpisodes: 0 }
|
||||
|
||||
// Event emitter for progress updates - matches CodeIndex pattern
|
||||
private _onProgressUpdate = new vscode.EventEmitter<ProgressUpdate>()
|
||||
public readonly onProgressUpdate = this._onProgressUpdate.event
|
||||
|
||||
public getCurrentStatus(): MemoryStatus {
|
||||
return {
|
||||
systemState: this._systemState,
|
||||
systemMessage: this._systemMessage,
|
||||
processedEpisodes: this._progressData.processedEpisodes,
|
||||
totalEpisodes: this._progressData.totalEpisodes,
|
||||
}
|
||||
}
|
||||
|
||||
public setSystemState(state: string, message: string): void {
|
||||
this._systemState = state
|
||||
this._systemMessage = message
|
||||
|
||||
this._onProgressUpdate.fire({
|
||||
state: this._systemState,
|
||||
message: this._systemMessage,
|
||||
progress: this._progressData,
|
||||
})
|
||||
}
|
||||
|
||||
public setProcessingState(message: string, progress?: ProgressData): void {
|
||||
this._systemState = "Processing"
|
||||
this._systemMessage = message
|
||||
if (progress) {
|
||||
this._progressData = progress
|
||||
}
|
||||
|
||||
this._onProgressUpdate.fire({
|
||||
state: this._systemState,
|
||||
message: this._systemMessage,
|
||||
progress: this._progressData,
|
||||
})
|
||||
}
|
||||
|
||||
public setError(errorMessage: string): void {
|
||||
this._systemState = "Error"
|
||||
this._systemMessage = errorMessage
|
||||
|
||||
this._onProgressUpdate.fire({
|
||||
state: this._systemState,
|
||||
message: this._systemMessage,
|
||||
progress: this._progressData,
|
||||
})
|
||||
}
|
||||
|
||||
public updateProgress(processedEpisodes: number, totalEpisodes: number): void {
|
||||
this._progressData = { processedEpisodes, totalEpisodes }
|
||||
|
||||
this._onProgressUpdate.fire({
|
||||
state: this._systemState,
|
||||
message: this._systemMessage,
|
||||
progress: this._progressData,
|
||||
})
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._onProgressUpdate.dispose()
|
||||
}
|
||||
}
|
||||
|
|
@ -196,6 +196,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
|
|||
insert_content: "insert content",
|
||||
search_and_replace: "search and replace",
|
||||
codebase_search: "codebase search",
|
||||
memory_search: "memory search",
|
||||
update_todo_list: "update todo list",
|
||||
generate_image: "generate images",
|
||||
} as const
|
||||
|
|
@ -210,6 +211,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
|
|||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"codebase_search",
|
||||
"memory_search",
|
||||
],
|
||||
},
|
||||
edit: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue