EO RAG + Knowledge Sharing System 让 141 位专家形成"知识共同体" - 自动沉淀经验、智能检索、上下文注入 Overview EO RAG 系统为 Everything OpenClaw 框架添加了向量检索 + 知识共享能力,使多专家协作产生的经验能够被自动沉淀和复用。 Architecture 5-Layer Knowledge Index | Layer | Name | Source | Chunk Strategy | |-------|------|--------|----------------| | L1 | Expert Profiles | | One per expert file | | L2 | Pattern Instances | + | Section-based | | L3 | Checkpoint Results | Runtime: ETP Protocol | One per checkpoint | | L4 | Cross-Expert Decisions | Runtime: ETP Protocol | One per decision | | L5 | Session Memory | + | One per session | Directory Structure Quick Start…

)\n if (fs.existsSync(currentDir)) {\n const entries = fs.readdirSync(currentDir, { withFileTypes: true })\n const matched = entries\n .filter(entry => regex.test(entry.name))\n .map(entry => path.join(currentDir, entry.name))\n files.push(...matched)\n }\n break\n } else {\n currentDir = path.join(currentDir, part)\n }\n }\n\n // If no glob, check if it's a file\n if (!pattern.includes('*') && fs.existsSync(fullPattern)) {\n const stat = fs.statSync(fullPattern)\n if (stat.isFile() && fullPattern.endsWith('.md')) {\n files.push(fullPattern)\n }\n }\n }\n\n // Also check common locations\n const commonPaths = [\n path.join(this.config.basePath, 'expert-library'),\n path.join(this.config.basePath, '..', 'expert-library')\n ]\n\n for (const commonPath of commonPaths) {\n if (fs.existsSync(commonPath)) {\n this.findMdFilesRecursive(commonPath, files)\n }\n }\n\n return [...new Set(files)] // Deduplicate\n }\n\n private findMdFilesRecursive(dir: string, files: string[]): void {\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n this.findMdFilesRecursive(fullPath, files)\n } else if (entry.name.endsWith('.md')) {\n files.push(fullPath)\n }\n }\n } catch {\n // Ignore permission errors\n }\n }\n\n /**\n * Create a knowledge chunk from an expert profile\n */\n private async createChunk(profile: ExpertProfile, sourceFile: string): Promise\u003cKnowledgeChunk> {\n const content = this.buildChunkContent(profile)\n const now = Date.now()\n\n return {\n id: profile.id,\n content,\n chunkType: CHUNK_TYPE,\n layer: LAYER,\n metadata: {\n sourceFile,\n expertRole: profile.role,\n category: profile.category,\n capabilities: profile.capabilities,\n tools: profile.tools\n },\n createdAt: now,\n updatedAt: now\n }\n }\n\n /**\n * Build searchable content from expert profile\n */\n private buildChunkContent(profile: ExpertProfile): string {\n const parts: string[] = []\n\n parts.push(`Expert: ${profile.name}`)\n parts.push(`Role: ${profile.role}`)\n parts.push(`Category: ${profile.category}`)\n \n if (profile.description) {\n parts.push(`Description: ${profile.description}`)\n }\n \n if (profile.capabilities.length > 0) {\n parts.push(`Capabilities: ${profile.capabilities.join(', ')}`)\n }\n \n if (profile.tools.length > 0) {\n parts.push(`Tools: ${profile.tools.join(', ')}`)\n }\n \n if (profile.skills && profile.skills.length > 0) {\n parts.push(`Skills: ${profile.skills.join(', ')}`)\n }\n\n return parts.join('\\n')\n }\n\n getStats(): ExpertIndexerStats {\n return { ...this.stats }\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":9766,"content_sha256":"aa2e1086467638cfefd2ba7398390b4f354b099b3a3d628354bf871da9b4db2e"},{"filename":"src/indexers/pattern-indexer.ts","content":"/**\n * L2 Pattern Indexer\n * \n * 索引 5 种模式的定义、场景、实例\n * 来源: docs/PATTERNS.md + patterns/\n * Chunk策略: 每个模式 = 定义 + 使用场景 + 实例\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport type { \n KnowledgeChunk, \n PatternInstance, \n PatternExample,\n PatternType,\n IndexLayer,\n ChunkType,\n PatternWorkflow\n} from '../types/index.js'\nimport { LanceDBAdapter } from '../vector-store/lancedb-adapter.js'\nimport { KNOWLEDGE_PATHS } from '../config.js'\n\nconst LAYER: IndexLayer = 2\nconst CHUNK_TYPE: ChunkType = 'pattern'\n\nexport interface PatternIndexerConfig {\n basePath: string\n patternLibraryPaths?: string[]\n}\n\nexport interface PatternIndexerStats {\n layer: IndexLayer\n totalPatterns: number\n indexedCount: number\n lastIndexed: number | null\n errors: string[]\n}\n\n// Pattern type mapping\nconst PATTERN_TYPE_MAP: Record\u003cstring, PatternType> = {\n 'miniprogram': 'miniprogram',\n '小程序': 'miniprogram',\n 'website': 'website',\n '网站': 'website',\n 'web': 'website',\n 'mobile-app': 'mobile-app',\n 'app': 'mobile-app',\n '移动应用': 'mobile-app',\n 'algorithm': 'algorithm',\n '算法': 'algorithm',\n 'academic-paper': 'academic-paper',\n '论文': 'academic-paper',\n 'academic': 'academic-paper',\n 'multi-expert': 'multi-expert',\n '多专家': 'multi-expert'\n}\n\n/**\n * Parse pattern instance from markdown content\n */\nfunction parsePatternInstance(filePath: string, content: string): PatternInstance | null {\n const fileName = path.basename(filePath, '.md')\n \n // Extract frontmatter\n let name = ''\n let patternType: PatternType = 'general'\n let description = ''\n const usageScenarios: string[] = []\n const examples: PatternExample[] = []\n let confidence = 0.8\n let usageCount = 0\n\n // Try to parse frontmatter\n const frontmatterMatch = content.match(/^---\\n([\\s\\S]*?)\\n---/)\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1]\n const lines = yamlContent.split('\\n')\n for (const line of lines) {\n const colonIndex = line.indexOf(':')\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim()\n const value = line.slice(colonIndex + 1).trim()\n switch (key) {\n case 'name':\n name = value\n break\n case 'patternType':\n case 'type':\n patternType = PATTERN_TYPE_MAP[value] || 'general'\n break\n case 'description':\n description = value\n break\n case 'confidence':\n confidence = parseFloat(value) || 0.8\n break\n case 'usageCount':\n case 'usage_count':\n usageCount = parseInt(value) || 0\n break\n }\n }\n }\n }\n\n // If name not in frontmatter, extract from content\n if (!name) {\n const titleMatch = content.match(/^#\\s+(.+)/m)\n name = titleMatch?.[1]?.trim() || fileName.replace(/-/g, ' ')\n }\n\n // Extract description if not in frontmatter\n if (!description) {\n const descMatch = content.match(/>([^#]{10,200})/)\n description = descMatch?.[1]?.trim() || ''\n }\n\n // Extract usage scenarios\n const scenariosSection = content.match(/##?\\s*(?:Usage Scenarios?|使用场景|适用场景)\\s*\\n([\\s\\S]*?)(?=\\n##|\\n#|$)/i)\n if (scenariosSection) {\n scenariosSection[1].split(/[,\\n•\\-]/).forEach(scenario => {\n const trimmed = scenario.trim()\n if (trimmed && trimmed.length > 3) {\n usageScenarios.push(trimmed)\n }\n })\n }\n\n // Extract examples\n const exampleSections = content.split(/(?=##?\\s*(?:Example|实例|案例|示例))/i)\n for (const section of exampleSections) {\n const exampleTitleMatch = section.match(/##?\\s*(?:Example|实例|案例|示例)\\s*:?\\s*(.+)/i)\n const exampleOutcomeMatch = section.match(/outcome\\s*:?\\s*(success|partial|failed)/i)\n \n if (exampleTitleMatch || section.includes('```')) {\n examples.push({\n title: exampleTitleMatch?.[1]?.trim() || 'Unnamed Example',\n description: section.slice(0, 300).trim(),\n codeSnippet: extractCodeSnippet(section),\n outcome: (exampleOutcomeMatch?.[1] as 'success' | 'partial' | 'failed') || 'success'\n })\n }\n }\n\n // Extract workflow stages if present\n let workflow: PatternWorkflow | undefined\n const workflowSection = content.match(/##?\\s*(?:Workflow|工作流|流程)\\s*\\n([\\s\\S]*?)(?=\\n##|\\n#|$)/i)\n if (workflowSection) {\n const stages: PatternWorkflow['stages'] = []\n const stageMatches = workflowSection[1].matchAll(/(?:^|\\n)([^-\\n].+?)(?=\\n-{3,}|$)/g)\n let stageIndex = 0\n for (const match of stageMatches) {\n stages.push({\n id: `stage-${stageIndex++}`,\n name: match[1].trim(),\n expertRole: 'general',\n description: match[1].trim(),\n input: '',\n output: ''\n })\n }\n if (stages.length > 0) {\n workflow = { stages, checkpoints: [] }\n }\n }\n\n if (!name) return null\n\n return {\n id: `pattern-${fileName}`,\n name,\n description,\n patternType,\n definition: extractDefinition(content),\n usageScenarios,\n examples: examples.slice(0, 5), // Limit examples\n workflow,\n confidence,\n usageCount\n }\n}\n\nfunction extractCodeSnippet(content: string): string | undefined {\n const codeMatch = content.match(/```[\\w]*\\n([\\s\\S]*?)```/)\n return codeMatch?.[1]?.trim()\n}\n\nfunction extractDefinition(content: string): string {\n // Try to find definition section\n const defMatch = content.match(/##?\\s*(?:Definition|定义)\\s*\\n([\\s\\S]*?)(?=\\n##|\\n#|$)/i)\n if (defMatch) {\n return defMatch[1].trim().slice(0, 500)\n }\n \n // Fall back to first substantial paragraph\n const paraMatch = content.match(/^(?!#)([^\\n]{100,500})/)\n return paraMatch?.[1]?.trim() || content.slice(0, 300)\n}\n\n/**\n * Pattern Indexer - L2\n */\nexport class PatternIndexer {\n private adapter: LanceDBAdapter\n private config: PatternIndexerConfig\n private stats: PatternIndexerStats\n\n constructor(adapter: LanceDBAdapter, config?: PatternIndexerConfig) {\n this.adapter = adapter\n this.config = {\n basePath: config?.basePath || '',\n patternLibraryPaths: config?.patternLibraryPaths || KNOWLEDGE_PATHS.patternLibrary as string[]\n }\n this.stats = {\n layer: LAYER,\n totalPatterns: 0,\n indexedCount: 0,\n lastIndexed: null,\n errors: []\n }\n }\n\n /**\n * Index all pattern instances\n */\n async index(): Promise\u003cPatternIndexerStats> {\n const startTime = Date.now()\n const chunks: KnowledgeChunk[] = []\n this.stats.errors = []\n\n try {\n const patternFiles = await this.findPatternFiles()\n this.stats.totalPatterns = patternFiles.length\n\n for (const filePath of patternFiles) {\n try {\n const content = fs.readFileSync(filePath, 'utf-8')\n const pattern = parsePatternInstance(filePath, content)\n \n if (pattern) {\n const chunk = await this.createChunk(pattern, filePath)\n chunks.push(chunk)\n this.stats.indexedCount++\n }\n } catch (err) {\n this.stats.errors.push(`Failed to index ${filePath}: ${err}`)\n }\n }\n\n if (chunks.length > 0) {\n for (const chunk of chunks) {\n chunk.embedding = await this.adapter.generateEmbedding(chunk.content)\n }\n await this.adapter.addBatch(chunks)\n }\n\n this.stats.lastIndexed = Date.now()\n } catch (err) {\n this.stats.errors.push(`Index failed: ${err}`)\n }\n\n return this.stats\n }\n\n /**\n * Index a single pattern instance\n */\n async indexPattern(pattern: PatternInstance): Promise\u003cKnowledgeChunk | null> {\n try {\n const chunk = await this.createChunk(pattern, `runtime:${pattern.id}`)\n chunk.embedding = await this.adapter.generateEmbedding(chunk.content)\n await this.adapter.add(chunk)\n this.stats.indexedCount++\n return chunk\n } catch (err) {\n this.stats.errors.push(`Failed to index pattern ${pattern.id}: ${err}`)\n return null\n }\n }\n\n /**\n * Update pattern usage statistics\n */\n async recordPatternUsage(patternId: string, success: boolean): Promise\u003cvoid> {\n const chunk = await this.adapter.get(patternId)\n if (chunk) {\n const metadata = chunk.metadata as Record\u003cstring, unknown>\n const usageCount = (metadata.usageCount as number) || 0\n const successCount = (metadata.successCount as number) || 0\n \n metadata.usageCount = usageCount + 1\n if (success) metadata.successCount = successCount + 1\n // Recalculate confidence\n metadata.confidence = (successCount + (success ? 1 : 0)) / (usageCount + 1)\n \n chunk.metadata = metadata as any\n chunk.updatedAt = Date.now()\n await this.adapter.add(chunk)\n }\n }\n\n /**\n * Find all pattern files\n */\n private async findPatternFiles(): Promise\u003cstring[]> {\n const files: string[] = []\n \n if (!this.config.basePath) return files\n\n // Check common paths\n const commonPaths = [\n path.join(this.config.basePath, 'docs', 'PATTERNS.md'),\n path.join(this.config.basePath, 'patterns'),\n path.join(this.config.basePath, '..', 'patterns'),\n path.join(this.config.basePath, 'skills', 'patterns')\n ]\n\n for (const commonPath of commonPaths) {\n if (fs.existsSync(commonPath)) {\n const stat = fs.statSync(commonPath)\n if (stat.isFile() && commonPath.endsWith('.md')) {\n files.push(commonPath)\n } else if (stat.isDirectory()) {\n this.findMdFilesRecursive(commonPath, files)\n }\n }\n }\n\n return [...new Set(files)]\n }\n\n private findMdFilesRecursive(dir: string, files: string[]): void {\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {\n this.findMdFilesRecursive(fullPath, files)\n } else if (entry.name.endsWith('.md') && !entry.name.startsWith('.')) {\n files.push(fullPath)\n }\n }\n } catch {\n // Ignore permission errors\n }\n }\n\n /**\n * Create a knowledge chunk from a pattern instance\n */\n private async createChunk(pattern: PatternInstance, sourceFile: string): Promise\u003cKnowledgeChunk> {\n const content = this.buildChunkContent(pattern)\n const now = Date.now()\n\n return {\n id: pattern.id,\n content,\n chunkType: CHUNK_TYPE,\n layer: LAYER,\n metadata: {\n sourceFile,\n patternName: pattern.name,\n patternType: pattern.patternType,\n usageScenarios: pattern.usageScenarios,\n successRate: pattern.confidence,\n usageCount: pattern.usageCount\n },\n createdAt: now,\n updatedAt: now\n }\n }\n\n /**\n * Build searchable content from pattern instance\n */\n private buildChunkContent(pattern: PatternInstance): string {\n const parts: string[] = []\n\n parts.push(`Pattern: ${pattern.name}`)\n parts.push(`Type: ${pattern.patternType}`)\n \n if (pattern.description) {\n parts.push(`Description: ${pattern.description}`)\n }\n \n if (pattern.definition) {\n parts.push(`Definition: ${pattern.definition.slice(0, 300)}`)\n }\n \n if (pattern.usageScenarios.length > 0) {\n parts.push(`Use Cases: ${pattern.usageScenarios.slice(0, 5).join(', ')}`)\n }\n \n if (pattern.examples.length > 0) {\n parts.push(`Examples: ${pattern.examples.slice(0, 2).map(e => e.title).join('; ')}`)\n }\n\n return parts.join('\\n')\n }\n\n getStats(): PatternIndexerStats {\n return { ...this.stats }\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":11690,"content_sha256":"4f9d418c62fdd39a68388668b9cf57269040a0a7503b6d94e353f7686161aa94"},{"filename":"src/indexers/session-indexer.ts","content":"/**\n * L5 Session Indexer\n * \n * 索引会话记忆摘要\n * 来源: MEMORY.md + memory/YYYY-MM-DD.md\n * Chunk策略: 每个会话 = 任务摘要 + 关键产出 + 教训\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport type { \n KnowledgeChunk, \n SessionSummary, \n IndexLayer,\n ChunkType \n} from '../types/index.js'\nimport { LanceDBAdapter } from '../vector-store/lancedb-adapter.js'\nimport { KNOWLEDGE_PATHS } from '../config.js'\n\nconst LAYER: IndexLayer = 5\nconst CHUNK_TYPE: ChunkType = 'session'\n\nexport interface SessionIndexerConfig {\n basePath: string\n sessionMemoryPaths?: string[]\n}\n\nexport interface SessionIndexerStats {\n layer: IndexLayer\n totalSessions: number\n indexedCount: number\n lastIndexed: number | null\n errors: string[]\n}\n\nexport interface MemoryFile {\n path: string\n date: string\n content: string\n entries: MemoryEntry[]\n}\n\nexport interface MemoryEntry {\n type: 'note' | 'decision' | 'task' | 'lesson' | 'general'\n content: string\n timestamp?: number\n}\n\n/**\n * Parse memory file content into structured entries\n */\nfunction parseMemoryFile(filePath: string, content: string): MemoryFile {\n const fileName = path.basename(filePath)\n const dateMatch = fileName.match(/(\\d{4}-\\d{2}-\\d{2})/)\n const date = dateMatch?.[1] || 'unknown'\n\n const entries: MemoryEntry[] = []\n \n // Split by common section headers\n const sections = content.split(/(?=^##?\\s+|\\n##?\\s+)/m)\n \n for (const section of sections) {\n const trimmed = section.trim()\n if (!trimmed || trimmed.length \u003c 10) continue\n\n // Determine entry type\n let type: MemoryEntry['type'] = 'general'\n if (/^#.*decision|^##?\\s*decision/i.test(trimmed)) {\n type = 'decision'\n } else if (/^#.*task|^##?\\s*task/i.test(trimmed)) {\n type = 'task'\n } else if (/^#.*lesson|^##?\\s*lesson/i.test(trimmed)) {\n type = 'lesson'\n } else if (/^#.*note|^##?\\s*note|^#.*日志|^##?\\s*日志/i.test(trimmed)) {\n type = 'note'\n }\n\n // Extract key content\n const lines = trimmed.split('\\n').filter(line => !line.startsWith('#'))\n const body = lines.join(' ').trim()\n \n if (body.length > 10) {\n entries.push({ type, content: body, timestamp: extractTimestamp(trimmed) })\n }\n }\n\n return { path: filePath, date, content, entries }\n}\n\nfunction extractTimestamp(text: string): number | undefined {\n const dateMatch = text.match(/(\\d{4}-\\d{2}-\\d{2})/)\n if (dateMatch) {\n return new Date(dateMatch[1]).getTime()\n }\n return undefined\n}\n\n/**\n * Convert memory file to session summary\n */\nfunction createSessionSummary(memoryFile: MemoryFile): SessionSummary {\n const decisions = memoryFile.entries\n .filter(e => e.type === 'decision')\n .map(e => e.content)\n \n const lessons = memoryFile.entries\n .filter(e => e.type === 'lesson')\n .map(e => e.content)\n \n const tasks = memoryFile.entries\n .filter(e => e.type === 'task')\n .map(e => e.content)\n\n // Extract task type from content\n const allText = memoryFile.content.toLowerCase()\n let taskType = 'general'\n if (allText.includes('小程序') || allText.includes('miniprogram')) {\n taskType = 'miniprogram'\n } else if (allText.includes('网站') || allText.includes('website')) {\n taskType = 'website'\n } else if (allText.includes('论文') || allText.includes('paper') || allText.includes('academic')) {\n taskType = 'academic-paper'\n } else if (allText.includes('算法') || allText.includes('algorithm')) {\n taskType = 'algorithm'\n }\n\n // Determine complexity\n let complexity: 'low' | 'medium' | 'high' = 'medium'\n if (memoryFile.entries.length > 20 || memoryFile.content.length > 5000) {\n complexity = 'high'\n } else if (memoryFile.entries.length \u003c 5 && memoryFile.content.length \u003c 1000) {\n complexity = 'low'\n }\n\n // Extract experts mentioned\n const expertMentions = memoryFile.content.match(/@(planner|architect|frontend|backend|devops|qa|security|code.?reviewer)/gi) || []\n const experts = [...new Set(expertMentions.map(e => e.replace(/@/gi, '').toLowerCase()))]\n\n // Determine success\n let success = true\n if (allText.includes('失败') || allText.includes('failed') || allText.includes('error')) {\n success = false\n } else if (allText.includes('部分成功') || allText.includes('partial')) {\n success = false // partial is not full success\n }\n\n return {\n id: `session-${memoryFile.date}`,\n task: extractTaskFromContent(memoryFile.content),\n taskType,\n complexity,\n summary: createSummary(memoryFile),\n keyDecisions: decisions.slice(0, 5),\n keyOutcomes: tasks.slice(0, 5),\n lessonsLearned: lessons.slice(0, 5),\n participatingExperts: experts.slice(0, 10),\n durationMs: estimateDuration(memoryFile),\n success,\n timestamp: new Date(memoryFile.date).getTime() || Date.now(),\n relatedTaskIds: extractRelatedTasks(memoryFile.content)\n }\n}\n\nfunction extractTaskFromContent(content: string): string {\n // Try to find the main task/goal\n const taskMatch = content.match(/(?:目标|task|任务|项目)[::]\\s*(.+?)(?:\\n|$)/i)\n if (taskMatch) {\n return taskMatch[1].trim().slice(0, 200)\n }\n \n // Fall back to first substantial line\n const lines = content.split('\\n').filter(l => !l.startsWith('#') && l.trim().length > 20)\n return lines[0]?.trim().slice(0, 200) || 'Untitled session'\n}\n\nfunction createSummary(memoryFile: MemoryFile): string {\n const entryCount = memoryFile.entries.length\n const decisions = memoryFile.entries.filter(e => e.type === 'decision').length\n const lessons = memoryFile.entries.filter(e => e.type === 'lesson').length\n \n return `Session on ${memoryFile.date}: ${entryCount} entries, ${decisions} decisions, ${lessons} lessons. ${memoryFile.content.slice(0, 300)}`\n}\n\nfunction estimateDuration(memoryFile: MemoryFile): number {\n // Estimate based on content length and entry count\n const baseMs = memoryFile.entries.length * 30 * 60 * 1000 // 30 min per entry\n return Math.min(baseMs, 8 * 60 * 60 * 1000) // Cap at 8 hours\n}\n\nfunction extractRelatedTasks(content: string): string[] {\n const taskIds = content.match(/task[::]\\s*([a-zA-Z0-9_-]+)/gi) || []\n return [...new Set(taskIds.map(t => t.replace(/task[::]/gi, '').trim()))].slice(0, 10)\n}\n\n/**\n * Session Indexer - L5\n */\nexport class SessionIndexer {\n private adapter: LanceDBAdapter\n private config: SessionIndexerConfig\n private stats: SessionIndexerStats\n\n constructor(adapter: LanceDBAdapter, config?: SessionIndexerConfig) {\n this.adapter = adapter\n this.config = {\n basePath: config?.basePath || '',\n sessionMemoryPaths: config?.sessionMemoryPaths || KNOWLEDGE_PATHS.sessionMemory as string[]\n }\n this.stats = {\n layer: LAYER,\n totalSessions: 0,\n indexedCount: 0,\n lastIndexed: null,\n errors: []\n }\n }\n\n /**\n * Index all session memory files\n */\n async index(): Promise\u003cSessionIndexerStats> {\n const chunks: KnowledgeChunk[] = []\n this.stats.errors = []\n\n try {\n const memoryFiles = await this.findMemoryFiles()\n this.stats.totalSessions = memoryFiles.length\n\n for (const file of memoryFiles) {\n try {\n const memoryFile = parseMemoryFile(file.path, file.content)\n const summary = createSessionSummary(memoryFile)\n const chunk = await this.createChunk(summary, file.path)\n chunks.push(chunk)\n this.stats.indexedCount++\n } catch (err) {\n this.stats.errors.push(`Failed to index ${file.path}: ${err}`)\n }\n }\n\n if (chunks.length > 0) {\n for (const chunk of chunks) {\n chunk.embedding = await this.adapter.generateEmbedding(chunk.content)\n }\n await this.adapter.addBatch(chunks)\n }\n\n this.stats.lastIndexed = Date.now()\n } catch (err) {\n this.stats.errors.push(`Index failed: ${err}`)\n }\n\n return this.stats\n }\n\n /**\n * Index a single session summary\n */\n async indexSessionSummary(summary: SessionSummary): Promise\u003cKnowledgeChunk | null> {\n try {\n const chunk = await this.createChunk(summary, `runtime:${summary.id}`)\n chunk.embedding = await this.adapter.generateEmbedding(chunk.content)\n await this.adapter.add(chunk)\n this.stats.indexedCount++\n return chunk\n } catch (err) {\n this.stats.errors.push(`Failed to index session ${summary.id}: ${err}`)\n return null\n }\n }\n\n /**\n * Find all memory files\n */\n private async findMemoryFiles(): Promise\u003cArray\u003c{ path: string; content: string }>> {\n const files: Array\u003c{ path: string; content: string }> = []\n \n if (!this.config.basePath) return files\n\n // Expand tilde\n const basePath = this.config.basePath.replace(/^~\\//, process.env.HOME + '/')\n\n // Check for MEMORY.md\n const memoryPaths = [\n path.join(basePath, 'MEMORY.md'),\n path.join(basePath, 'memory'),\n path.join(basePath, '..', 'MEMORY.md')\n ]\n\n for (const memoryPath of memoryPaths) {\n if (fs.existsSync(memoryPath)) {\n const stat = fs.statSync(memoryPath)\n if (stat.isFile()) {\n try {\n const content = fs.readFileSync(memoryPath, 'utf-8')\n files.push({ path: memoryPath, content })\n } catch {\n // Ignore read errors\n }\n } else if (stat.isDirectory()) {\n // Read all .md files in memory directory\n this.findMdFilesRecursive(memoryPath, files)\n }\n }\n }\n\n return files\n }\n\n private findMdFilesRecursive(\n dir: string, \n files: Array\u003c{ path: string; content: string }>\n ): void {\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {\n this.findMdFilesRecursive(fullPath, files)\n } else if (entry.name.endsWith('.md') && !entry.name.startsWith('.')) {\n try {\n const content = fs.readFileSync(fullPath, 'utf-8')\n files.push({ path: fullPath, content })\n } catch {\n // Ignore read errors\n }\n }\n }\n } catch {\n // Ignore permission errors\n }\n }\n\n /**\n * Create a knowledge chunk from a session summary\n */\n private async createChunk(\n summary: SessionSummary, \n sourceFile: string\n ): Promise\u003cKnowledgeChunk> {\n const content = this.buildChunkContent(summary)\n const now = Date.now()\n\n return {\n id: summary.id,\n content,\n chunkType: CHUNK_TYPE,\n layer: LAYER,\n metadata: {\n sourceFile,\n sessionId: summary.id,\n taskType: summary.taskType,\n timestamp: summary.timestamp,\n successRate: summary.success ? 1 : 0,\n participatingExperts: summary.participatingExperts\n },\n createdAt: now,\n updatedAt: now\n }\n }\n\n /**\n * Build searchable content from session summary\n */\n private buildChunkContent(summary: SessionSummary): string {\n const parts: string[] = []\n\n parts.push(`Session: ${summary.task}`)\n parts.push(`Task Type: ${summary.taskType}`)\n parts.push(`Complexity: ${summary.complexity}`)\n \n if (summary.summary) {\n parts.push(`Summary: ${summary.summary.slice(0, 300)}`)\n }\n \n if (summary.keyDecisions.length > 0) {\n parts.push(`Key Decisions: ${summary.keyDecisions.join('; ')}`)\n }\n \n if (summary.keyOutcomes.length > 0) {\n parts.push(`Outcomes: ${summary.keyOutcomes.join('; ')}`)\n }\n \n if (summary.lessonsLearned.length > 0) {\n parts.push(`Lessons Learned: ${summary.lessonsLearned.join('; ')}`)\n }\n \n if (summary.participatingExperts.length > 0) {\n parts.push(`Experts: ${summary.participatingExperts.join(', ')}`)\n }\n\n return parts.join('\\n')\n }\n\n getStats(): SessionIndexerStats {\n return { ...this.stats }\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":11920,"content_sha256":"e32f4374ac78e9dd5442ea23cf7f1ef14e2565507bcbcc0aa40a4d54001a9151"},{"filename":"src/knowledge/conflict-resolver.ts","content":"/**\n * Conflict Resolver\n * \n * 冲突解决策略 - 当多个专家的 ETP 记录对同一问题给出矛盾建议时\n * \n * 冲突分级:\n * L1: 工具/参数冲突(如同一API用不同参数)\n * → 策略:优先最新记录 > 优先成功率高 > 优先权威专家\n * \n * L2: 架构/设计冲突(如微服务 vs 单体)\n * → 策略:记录冲突快照 → 输出给 Architect → 人工决策\n * \n * L3: 方向/优先级冲突(如安全 vs 速度)\n * → 策略:上报 Planner → 多准则决策分析(MCDA)\n */\n\nimport type {\n ConflictRecord,\n ConflictLevel,\n ConflictType,\n ConflictResolutionWeights,\n ETPRecord\n} from '../types/index.js'\nimport { CONFLICT_THRESHOLDS } from '../config.js'\n\nexport interface ConflictResolverConfig {\n weights?: ConflictResolutionWeights\n autoResolveL1?: boolean\n escalateL2?: boolean\n escalateL3?: boolean\n}\n\nconst DEFAULT_CONFIG: Required\u003cConflictResolverConfig> = {\n weights: {\n recency: 0.4,\n successRate: 0.3,\n expertAuthority: 0.2,\n contextSimilarity: 0.1\n },\n autoResolveL1: true,\n escalateL2: true,\n escalateL3: true\n}\n\n// Expert authority levels (higher = more authoritative)\nconst AUTHORITY_LEVELS: Record\u003cstring, number> = {\n 'architect': 10,\n 'planner': 9,\n 'security': 8,\n 'devops': 7,\n 'backend': 7,\n 'frontend': 6,\n 'qa': 6,\n 'code-reviewer': 5,\n 'general': 3\n}\n\nexport type ConflictDetectionType = 'exact' | 'similar' | 'semantic'\n\nexport interface ConflictCandidate {\n expertId: string\n recommendation: string\n record: ETPRecord\n confidence: number\n}\n\nexport interface ConflictAnalysis {\n conflict: ConflictRecord\n candidates: ConflictCandidate[]\n winningCandidate?: ConflictCandidate\n recommendedResolution?: string\n requiresHumanInput: boolean\n}\n\n/**\n * Conflict Resolver\n */\nexport class ConflictResolver {\n private config: Required\u003cConflictResolverConfig>\n private conflicts: Map\u003cstring, ConflictRecord[]>\n\n constructor(config?: ConflictResolverConfig) {\n this.config = { ...DEFAULT_CONFIG, ...config }\n this.conflicts = new Map()\n }\n\n // =========================================================================\n // Conflict Detection\n // =========================================================================\n\n /**\n * Detect conflicts between recommendations\n */\n detectConflict(\n taskId: string,\n recommendations: Array\u003c{ expertId: string; recommendation: string; record: ETPRecord }>\n ): ConflictRecord | null {\n if (recommendations.length \u003c 2) return null\n\n // Group by similar recommendations\n const groups = this.groupSimilarRecommendations(recommendations)\n \n // If we have multiple groups with different recommendations, it's a conflict\n if (groups.length > 1) {\n const conflictingExperts = recommendations.map(r => r.expertId)\n const options = groups.map(g => g.canonical)\n \n // Determine conflict level\n const level = this.determineConflictLevel(options)\n const type = this.inferConflictType(options)\n\n const conflict: ConflictRecord = {\n id: `conflict-${taskId}-${Date.now()}`,\n level,\n type,\n taskId,\n conflictingExperts,\n options,\n timestamp: Date.now()\n }\n\n // Store conflict\n if (!this.conflicts.has(taskId)) {\n this.conflicts.set(taskId, [])\n }\n this.conflicts.get(taskId)!.push(conflict)\n\n return conflict\n }\n\n return null\n }\n\n /**\n * Group recommendations by similarity\n */\n private groupSimilarRecommendations(\n recommendations: Array\u003c{ expertId: string; recommendation: string; record: ETPRecord }>\n ): Array\u003c{ canonical: string; members: typeof recommendations }> {\n const groups: Map\u003cstring, typeof recommendations> = new Map()\n\n for (const rec of recommendations) {\n const normalized = this.normalizeRecommendation(rec.recommendation)\n const existing = Array.from(groups.values()).find(g => \n this.calculateSimilarity(normalized, this.normalizeRecommendation(g[0].recommendation)) > 0.7\n )\n\n if (existing) {\n existing.push(rec)\n } else {\n groups.set(normalized, [rec])\n }\n }\n\n return Array.from(groups.entries()).map(([canonical, members]) => ({ canonical, members }))\n }\n\n private normalizeRecommendation(text: string): string {\n return text.toLowerCase().replace(/[^\\w]/g, '').slice(0, 50)\n }\n\n private calculateSimilarity(a: string, b: string): number {\n if (a === b) return 1\n if (a.includes(b) || b.includes(a)) return 0.8\n\n // Simple Jaccard similarity on character bigrams\n const aBigrams = new Set\u003cstring>()\n const bBigrams = new Set\u003cstring>()\n\n for (let i = 0; i \u003c a.length - 1; i++) {\n aBigrams.add(a.slice(i, i + 2))\n }\n for (let i = 0; i \u003c b.length - 1; i++) {\n bBigrams.add(b.slice(i, i + 2))\n }\n\n const intersection = new Set([...aBigrams].filter(x => bBigrams.has(x)))\n const union = new Set([...aBigrams, ...bBigrams])\n\n return intersection.size / union.size\n }\n\n /**\n * Determine conflict level based on options\n */\n private determineConflictLevel(options: string[]): ConflictLevel {\n const hasToolParam = options.some(o => \n /(api|param|config|option)/i.test(o) && options.some(p => p !== o && /(api|param|config|option)/i.test(p))\n )\n if (hasToolParam) return 'L1'\n\n const hasArchitecture = options.some(o => \n /(microservice|monolith|architecture|design|structure)/i.test(o)\n )\n if (hasArchitecture) return 'L2'\n\n return 'L3'\n }\n\n /**\n * Infer conflict type\n */\n private inferConflictType(options: string[]): ConflictType {\n if (options.some(o => /(api|param|config|method|function)/i.test(o))) {\n return 'tool-parameter'\n }\n if (options.some(o => /(microservice|monolith|architecture|layer|component)/i.test(o))) {\n return 'architecture-design'\n }\n return 'priority-direction'\n }\n\n // =========================================================================\n // Conflict Resolution\n // =========================================================================\n\n /**\n * Analyze a conflict and determine resolution\n */\n analyzeConflict(\n conflict: ConflictRecord,\n candidates: Array\u003c{ expertId: string; recommendation: string; record: ETPRecord }>\n ): ConflictAnalysis {\n const requiresHumanInput = \n (conflict.level === 'L2' && this.config.escalateL2) ||\n (conflict.level === 'L3' && this.config.escalateL3)\n\n if (conflict.level === 'L1' && this.config.autoResolveL1) {\n const winner = this.resolveL1Conflict(conflict, candidates)\n return {\n conflict,\n candidates: candidates.map(c => ({\n expertId: c.expertId,\n recommendation: c.recommendation,\n record: c.record,\n confidence: this.calculateConfidence(c.expertId, c.record)\n })),\n winningCandidate: winner,\n recommendedResolution: winner?.recommendation,\n requiresHumanInput: false\n }\n }\n\n // For L2/L3, suggest escalation\n const escalationTarget = conflict.level === 'L2' ? 'Architect' : 'Planner'\n return {\n conflict,\n candidates: candidates.map(c => ({\n expertId: c.expertId,\n recommendation: c.recommendation,\n record: c.record,\n confidence: this.calculateConfidence(c.expertId, c.record)\n })),\n recommendedResolution: `Escalate to ${escalationTarget} for decision`,\n requiresHumanInput: true\n }\n }\n\n /**\n * Resolve L1 (tool/parameter) conflicts using weighted scoring\n */\n private resolveL1Conflict(\n conflict: ConflictRecord,\n candidates: Array\u003c{ expertId: string; recommendation: string; record: ETPRecord }>\n ): ConflictCandidate | undefined {\n const scored = candidates.map(c => ({\n ...c,\n score: this.calculateWeightedScore(c.expertId, c.record)\n }))\n\n scored.sort((a, b) => b.score - a.score)\n return scored[0]\n }\n\n /**\n * Calculate weighted score for a recommendation\n */\n private calculateWeightedScore(expertId: string, record: ETPRecord): number {\n const weights = this.config.weights\n\n // Recency score (newer = higher)\n const ageMs = Date.now() - record.timestamp\n const ageDays = ageMs / (1000 * 60 * 60 * 24)\n const recencyScore = Math.max(0, 1 - ageDays / 30) // Decay over 30 days\n\n // Success rate score\n const totalAttempts = record.checkpointsPassed.length + record.checkpointsFailed.length\n const successRate = totalAttempts > 0 \n ? record.checkpointsPassed.length / totalAttempts \n : 0.5\n\n // Expert authority score\n const authorityScore = this.getExpertAuthority(expertId)\n\n // Context similarity (simplified - would need actual context matching)\n const contextScore = 0.5 // Placeholder\n\n return (\n weights.recency * recencyScore +\n weights.successRate * successRate +\n weights.expertAuthority * authorityScore +\n weights.contextSimilarity * contextScore\n )\n }\n\n /**\n * Calculate confidence for a candidate\n */\n private calculateConfidence(expertId: string, record: ETPRecord): number {\n return this.calculateWeightedScore(expertId, record)\n }\n\n /**\n * Get expert authority level\n */\n private getExpertAuthority(expertId: string): number {\n const idLower = expertId.toLowerCase()\n for (const [role, level] of Object.entries(AUTHORITY_LEVELS)) {\n if (idLower.includes(role)) {\n return level / 10 // Normalize to 0-1\n }\n }\n return AUTHORITY_LEVELS['general'] / 10\n }\n\n // =========================================================================\n // Conflict Management\n // =========================================================================\n\n /**\n * Record a conflict resolution\n */\n resolveConflict(conflictId: string, resolution: string): void {\n for (const conflicts of this.conflicts.values()) {\n const conflict = conflicts.find(c => c.id === conflictId)\n if (conflict) {\n conflict.resolution = resolution\n conflict.resolvedAt = Date.now()\n return\n }\n }\n }\n\n /**\n * Get unresolved conflicts for a task\n */\n getUnresolvedConflicts(taskId: string): ConflictRecord[] {\n return this.conflicts.get(taskId)?.filter(c => !c.resolvedAt) || []\n }\n\n /**\n * Get all conflicts\n */\n getAllConflicts(): ConflictRecord[] {\n const all: ConflictRecord[] = []\n for (const conflicts of this.conflicts.values()) {\n all.push(...conflicts)\n }\n return all\n }\n\n /**\n * Get conflict statistics\n */\n getStats(): {\n totalConflicts: number\n unresolved: number\n byLevel: Record\u003cConflictLevel, number>\n resolvedRate: number\n } {\n let total = 0\n let unresolved = 0\n const byLevel: Record\u003cConflictLevel, number> = { L1: 0, L2: 0, L3: 0 }\n\n for (const conflicts of this.conflicts.values()) {\n for (const conflict of conflicts) {\n total++\n byLevel[conflict.level]++\n if (!conflict.resolvedAt) unresolved++\n }\n }\n\n return {\n totalConflicts: total,\n unresolved,\n byLevel,\n resolvedRate: total > 0 ? (total - unresolved) / total : 1\n }\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":11195,"content_sha256":"eed700dd2faf1ebbbceb42768d3e67b41479cdf92dab904e65289c5120fa855b"},{"filename":"src/knowledge/etp-protocol.ts","content":"/**\n * Experience Transfer Protocol (ETP) Implementation\n * \n * ETP 经验传递协议 - 当专家 Agent 完成关键任务时,自动触发经验提取\n * \n * 触发时机:\n * 1. Team 完成 (team.status = 'completed')\n * 2. Checkpoint 验证失败 (记录失败原因和修复过程)\n * 3. Agent 主动通过 hook 触发 (/etp-save 指令)\n * \n * ETP Record 格式:\n * - protocol_version: '1.0'\n * - task_id: 任务标识\n * - task_type: 任务类型\n * - participating_experts: 参与专家列表\n * - key_decisions: 关键决策节点\n * - lessons: 经验教训\n * - reusable_snippets: 可复用代码片段\n * - checkpoints_passed/failed: 验证点记录\n */\n\nimport type {\n ETPRecord,\n ETPVersion,\n ETPTaskOutcome,\n ETPKeyDecision,\n ETPReusableSnippet,\n CheckpointRecord,\n DecisionRecord,\n TaskType\n} from '../types/index.js'\nimport { ETPIndexer } from '../indexers/etp-indexer.js'\nimport { LanceDBAdapter } from '../vector-store/lancedb-adapter.js'\nimport { KnowledgeGraphEngine } from './graph-engine.js'\n\nexport interface ETPConfig {\n persistPath?: string\n autoTrigger?: boolean\n triggerOnTaskComplete?: boolean\n triggerOnCheckpointFail?: boolean\n minDecisionsToRecord?: number\n}\n\nconst DEFAULT_ETP_CONFIG: Required\u003cETPConfig> = {\n persistPath: './data/rag/etp-records.json',\n autoTrigger: true,\n triggerOnTaskComplete: true,\n triggerOnCheckpointFail: true,\n minDecisionsToRecord: 1\n}\n\n// ETP event types for hooks\nexport type ETPEventType = \n | 'etp_created'\n | 'etp_indexed'\n | 'decision_recorded'\n | 'lesson_extracted'\n | 'snippet_extracted'\n\nexport interface ETPEvent {\n type: ETPEventType\n timestamp: number\n recordId?: string\n data: Record\u003cstring, unknown>\n}\n\ntype ETPEventHandler = (event: ETPEvent) => void\n\n/**\n * ETP Manager - handles ETP record creation, storage, and indexing\n */\nexport class ETPProtocol {\n private config: Required\u003cETPConfig>\n private adapter: LanceDBAdapter\n private indexer: ETPIndexer\n private graph: KnowledgeGraphEngine\n private records: Map\u003cstring, ETPRecord>\n private eventHandlers: ETPEventHandler[]\n private pendingRecords: ETPRecord[]\n\n constructor(\n adapter: LanceDBAdapter,\n graph: KnowledgeGraphEngine,\n config?: ETPConfig\n ) {\n this.config = { ...DEFAULT_ETP_CONFIG, ...config }\n this.adapter = adapter\n this.indexer = new ETPIndexer(adapter)\n this.graph = graph\n this.records = new Map()\n this.eventHandlers = []\n this.pendingRecords = []\n }\n\n // =========================================================================\n // Event Handling\n // =========================================================================\n\n onEvent(handler: ETPEventHandler): void {\n this.eventHandlers.push(handler)\n }\n\n private emit(event: ETPEvent): void {\n for (const handler of this.eventHandlers) {\n try {\n handler(event)\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n // =========================================================================\n // Record Creation\n // =========================================================================\n\n /**\n * Create a new ETP record from a task completion\n */\n createRecord(params: {\n taskId: string\n taskType: string\n taskDescription: string\n participatingExperts: string[]\n durationMs: number\n outcome: ETPTaskOutcome\n keyDecisions?: ETPKeyDecision[]\n lessons?: string[]\n reusableSnippets?: ETPReusableSnippet[]\n checkpointsPassed?: string[]\n checkpointsFailed?: string[]\n failureAnalysis?: string\n }): ETPRecord {\n const now = Date.now()\n\n const record: ETPRecord = {\n protocolVersion: '1.0',\n taskId: params.taskId,\n taskType: params.taskType,\n taskDescription: params.taskDescription,\n participatingExperts: params.participatingExperts,\n durationMs: params.durationMs,\n outcome: params.outcome,\n keyDecisions: params.keyDecisions || [],\n lessons: params.lessons || [],\n reusableSnippets: params.reusableSnippets || [],\n checkpointsPassed: params.checkpointsPassed || [],\n checkpointsFailed: params.checkpointsFailed || [],\n failureAnalysis: params.failureAnalysis,\n timestamp: now\n }\n\n // Validate record\n if (this.validateRecord(record)) {\n this.records.set(record.taskId, record)\n this.pendingRecords.push(record)\n this.emit({ type: 'etp_created', timestamp: now, recordId: record.taskId, data: record })\n }\n\n return record\n }\n\n /**\n * Validate an ETP record\n */\n private validateRecord(record: ETPRecord): boolean {\n if (!record.taskId || !record.taskType) {\n console.warn('[ETP] Invalid record: missing taskId or taskType')\n return false\n }\n if (!record.protocolVersion) {\n console.warn('[ETP] Invalid record: missing protocolVersion')\n return false\n }\n return true\n }\n\n // =========================================================================\n // Record Enrichment\n // =========================================================================\n\n /**\n * Add a decision to an existing record\n */\n addDecision(taskId: string, decision: ETPKeyDecision): void {\n const record = this.records.get(taskId)\n if (!record) {\n console.warn(`[ETP] Record not found: ${taskId}`)\n return\n }\n\n record.keyDecisions.push(decision)\n this.emit({\n type: 'decision_recorded',\n timestamp: Date.now(),\n recordId: taskId,\n data: { decision }\n })\n }\n\n /**\n * Add a lesson to an existing record\n */\n addLesson(taskId: string, lesson: string): void {\n const record = this.records.get(taskId)\n if (!record) {\n console.warn(`[ETP] Record not found: ${taskId}`)\n return\n }\n\n record.lessons.push(lesson)\n this.emit({\n type: 'lesson_extracted',\n timestamp: Date.now(),\n recordId: taskId,\n data: { lesson }\n })\n }\n\n /**\n * Add a reusable snippet to an existing record\n */\n addSnippet(taskId: string, snippet: ETPReusableSnippet): void {\n const record = this.records.get(taskId)\n if (!record) {\n console.warn(`[ETP] Record not found: ${taskId}`)\n return\n }\n\n record.reusableSnippets.push(snippet)\n this.emit({\n type: 'snippet_extracted',\n timestamp: Date.now(),\n recordId: taskId,\n data: { snippet }\n })\n }\n\n /**\n * Record a failed checkpoint\n */\n recordCheckpointFailure(taskId: string, checkpointName: string, analysis?: string): void {\n const record = this.records.get(taskId)\n if (!record) {\n // Create new record for this task\n const newRecord = this.createRecord({\n taskId,\n taskType: 'unknown',\n taskDescription: `Task: ${taskId}`,\n participatingExperts: [],\n durationMs: 0,\n outcome: 'failed'\n })\n this.addLesson(taskId, `Checkpoint failed: ${checkpointName}`)\n if (analysis) {\n newRecord.failureAnalysis = analysis\n }\n return\n }\n\n if (!record.checkpointsFailed.includes(checkpointName)) {\n record.checkpointsFailed.push(checkpointName)\n }\n if (analysis && !record.failureAnalysis) {\n record.failureAnalysis = analysis\n }\n \n // Add lesson about the failure\n this.addLesson(taskId, `Checkpoint failed: ${checkpointName}`)\n }\n\n // =========================================================================\n // Indexing\n // =========================================================================\n\n /**\n * Index all pending ETP records to vector store and knowledge graph\n */\n async indexPendingRecords(): Promise\u003cnumber> {\n let indexed = 0\n\n for (const record of this.pendingRecords) {\n try {\n // Index to vector store (via ETPIndexer)\n const chunks = await this.indexer.indexETPRecord(record)\n \n // Add to knowledge graph\n this.graph.addNode({\n id: `etp-${record.taskId}`,\n type: 'task',\n label: record.taskDescription,\n properties: {\n taskType: record.taskType,\n outcome: record.outcome,\n createdAt: record.timestamp\n }\n })\n\n // Add expert participation edges\n for (const expert of record.participatingExperts) {\n this.graph.addEdge({\n id: `expert-task-${expert}-${record.taskId}`,\n source: expert,\n target: `etp-${record.taskId}`,\n type: 'expert_participated_in',\n properties: { createdAt: record.timestamp }\n })\n }\n\n // Add decision edges\n for (const decision of record.keyDecisions) {\n this.graph.addNode({\n id: `decision-${record.taskId}-${decision.point.slice(0, 20)}`,\n type: 'decision',\n label: decision.point,\n properties: {\n chosen: decision.chosen,\n rationale: decision.rationale\n }\n })\n this.graph.addEdge({\n id: `task-decision-${record.taskId}`,\n source: `etp-${record.taskId}`,\n target: `decision-${record.taskId}-${decision.point.slice(0, 20)}`,\n type: 'task_has_decision',\n properties: { createdAt: record.timestamp }\n })\n }\n\n indexed++\n this.emit({ type: 'etp_indexed', timestamp: Date.now(), recordId: record.taskId, data: { chunksCreated: chunks.length } })\n } catch (err) {\n console.error(`[ETP] Failed to index record ${record.taskId}:`, err)\n }\n }\n\n this.pendingRecords = []\n return indexed\n }\n\n // =========================================================================\n // Record Retrieval\n // =========================================================================\n\n /**\n * Get an ETP record by task ID\n */\n getRecord(taskId: string): ETPRecord | undefined {\n return this.records.get(taskId)\n }\n\n /**\n * Get all ETP records for a specific task type\n */\n getRecordsByType(taskType: string): ETPRecord[] {\n return Array.from(this.records.values()).filter(r => r.taskType === taskType)\n }\n\n /**\n * Get all ETP records for a specific expert\n */\n getRecordsByExpert(expertId: string): ETPRecord[] {\n return Array.from(this.records.values())\n .filter(r => r.participatingExperts.includes(expertId))\n }\n\n /**\n * Get successful ETP records\n */\n getSuccessfulRecords(): ETPRecord[] {\n return Array.from(this.records.values()).filter(r => r.outcome === 'success')\n }\n\n /**\n * Get failed ETP records\n */\n getFailedRecords(): ETPRecord[] {\n return Array.from(this.records.values()).filter(r => r.outcome === 'failed')\n }\n\n // =========================================================================\n // Persistence\n // =========================================================================\n\n async save(): Promise\u003cvoid> {\n try {\n const fs = await import('fs')\n const path = await import('path')\n\n const data = {\n records: Array.from(this.records.values()),\n config: this.config\n }\n\n const dir = path.dirname(this.config.persistPath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n fs.writeFileSync(this.config.persistPath, JSON.stringify(data, null, 2))\n \n // Also save knowledge graph\n await this.graph.save()\n } catch (err) {\n console.error('[ETP] Save failed:', err)\n }\n }\n\n async load(): Promise\u003cvoid> {\n try {\n const fs = await import('fs')\n\n if (!fs.existsSync(this.config.persistPath)) return\n\n const data = JSON.parse(fs.readFileSync(this.config.persistPath, 'utf-8'))\n\n this.records.clear()\n for (const record of data.records || []) {\n this.records.set(record.taskId, record)\n }\n\n // Load knowledge graph\n await this.graph.load()\n } catch (err) {\n console.error('[ETP] Load failed:', err)\n }\n }\n\n // =========================================================================\n // Statistics\n // =========================================================================\n\n getStats(): {\n totalRecords: number\n byOutcome: Record\u003cETPTaskOutcome, number>\n byTaskType: Record\u003cstring, number>\n pendingIndexing: number\n } {\n const byOutcome: Record\u003cETPTaskOutcome, number> = {\n success: 0,\n partial: 0,\n failed: 0\n }\n const byTaskType: Record\u003cstring, number> = {}\n\n for (const record of this.records.values()) {\n byOutcome[record.outcome]++\n byTaskType[record.taskType] = (byTaskType[record.taskType] || 0) + 1\n }\n\n return {\n totalRecords: this.records.size,\n byOutcome,\n byTaskType,\n pendingIndexing: this.pendingRecords.length\n }\n }\n\n /**\n * Get lessons learned from all records\n */\n getAllLessons(): string[] {\n const lessons: string[] = []\n for (const record of this.records.values()) {\n lessons.push(...record.lessons)\n }\n return [...new Set(lessons)]\n }\n\n /**\n * Get reusable snippets\n */\n getReusableSnippets(): ETPReusableSnippet[] {\n const snippets: ETPReusableSnippet[] = []\n for (const record of this.records.values()) {\n snippets.push(...record.reusableSnippets)\n }\n return snippets\n }\n}\n\n// ============================================================================\n// Convenience Functions\n// ============================================================================\n\n/**\n * Convert CheckpointRecord to ETP format\n */\nexport function checkpointToETP(\n checkpoint: CheckpointRecord,\n taskType: string = 'unknown'\n): ETPRecord {\n return {\n protocolVersion: '1.0',\n taskId: checkpoint.taskId,\n taskType,\n taskDescription: `Checkpoint: ${checkpoint.checkpointName}`,\n participatingExperts: checkpoint.experts,\n durationMs: checkpoint.durationMs,\n outcome: checkpoint.passed ? 'success' : 'failed',\n keyDecisions: [],\n lessons: checkpoint.recommendations || [],\n reusableSnippets: [],\n checkpointsPassed: checkpoint.passed ? [checkpoint.checkpointName] : [],\n checkpointsFailed: checkpoint.passed ? [] : [checkpoint.checkpointName],\n failureAnalysis: checkpoint.failedItems?.join('; '),\n timestamp: checkpoint.timestamp\n }\n}\n\n/**\n * Convert DecisionRecord to ETP format\n */\nexport function decisionToETP(\n decision: DecisionRecord,\n taskDescription: string = ''\n): ETPRecord {\n return {\n protocolVersion: '1.0',\n taskId: decision.taskId,\n taskType: decision.taskId.split('-')[0] || 'unknown',\n taskDescription,\n participatingExperts: decision.participatingExperts,\n durationMs: 0,\n outcome: decision.outcome || 'partial',\n keyDecisions: [{\n point: decision.decisionPoint,\n alternatives: decision.alternatives,\n chosen: decision.chosen,\n rationale: decision.rationale\n }],\n lessons: [],\n reusableSnippets: [],\n checkpointsPassed: [],\n checkpointsFailed: [],\n timestamp: decision.timestamp\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":14978,"content_sha256":"6eff9b8b789b3744fb6341915ab31c488bf987f930c949d5e3d41480e64ad30d"},{"filename":"src/knowledge/graph-engine.ts","content":"/**\n * Knowledge Graph Engine (Lightweight)\n * \n * 轻量级知识图谱引擎,用于维护实体关系\n * 核心关系:\n * - [task] → [expert] (expert participated in)\n * - [expert] → [pattern] (expert uses pattern)\n * - [task] → [decision] (task has decision)\n * - [task] → [checkpoint] (checkpoint validated)\n * - [session] ↔ [session] (related sessions)\n */\n\nimport type {\n KnowledgeGraph,\n GraphNode,\n GraphEdge,\n GraphNodeType,\n GraphEdgeType,\n GraphQuery\n} from '../types/index.js'\n\nexport interface KnowledgeGraphConfig {\n persistPath?: string\n maxNodes?: number\n maxEdges?: number\n}\n\n// In-memory graph storage\ninterface GraphStore {\n nodes: Map\u003cstring, GraphNode>\n edges: Map\u003cstring, GraphEdge>\n nodeIndex: Map\u003cGraphNodeType, Set\u003cstring>>\n edgeIndex: Map\u003cGraphEdgeType, Set\u003cstring>>\n}\n\n/**\n * Lightweight Knowledge Graph Engine\n */\nexport class KnowledgeGraphEngine {\n private store: GraphStore\n private config: Required\u003cKnowledgeGraphConfig>\n\n constructor(config?: KnowledgeGraphConfig) {\n this.config = {\n persistPath: config?.persistPath || './data/rag/knowledge-graph.json',\n maxNodes: config?.maxNodes || 10000,\n maxEdges: config?.maxEdges || 50000\n }\n this.store = this.initStore()\n }\n\n private initStore(): GraphStore {\n return {\n nodes: new Map(),\n edges: new Map(),\n nodeIndex: new Map(),\n edgeIndex: new Map()\n }\n }\n\n // =========================================================================\n // Node Operations\n // =========================================================================\n\n /**\n * Add a node to the graph\n */\n addNode(node: GraphNode): void {\n if (this.store.nodes.size >= this.config.maxNodes) {\n this.evictOldNodes()\n }\n\n this.store.nodes.set(node.id, node)\n\n // Index by type\n if (!this.store.nodeIndex.has(node.type)) {\n this.store.nodeIndex.set(node.type, new Set())\n }\n this.store.nodeIndex.get(node.type)!.add(node.id)\n }\n\n /**\n * Add multiple nodes\n */\n addNodes(nodes: GraphNode[]): void {\n for (const node of nodes) {\n this.addNode(node)\n }\n }\n\n /**\n * Get a node by ID\n */\n getNode(id: string): GraphNode | undefined {\n return this.store.nodes.get(id)\n }\n\n /**\n * Get nodes by type\n */\n getNodesByType(type: GraphNodeType): GraphNode[] {\n const ids = this.store.nodeIndex.get(type)\n if (!ids) return []\n return Array.from(ids).map(id => this.store.nodes.get(id)!).filter(Boolean)\n }\n\n /**\n * Remove a node and its edges\n */\n removeNode(id: string): void {\n const node = this.store.nodes.get(id)\n if (!node) return\n\n // Remove from type index\n this.store.nodeIndex.get(node.type)?.delete(id)\n\n // Remove all edges involving this node\n const edgesToRemove = Array.from(this.store.edges.values())\n .filter(e => e.source === id || e.target === id)\n .map(e => e.id)\n\n for (const edgeId of edgesToRemove) {\n this.removeEdge(edgeId)\n }\n\n this.store.nodes.delete(id)\n }\n\n // =========================================================================\n // Edge Operations\n // =========================================================================\n\n /**\n * Add an edge to the graph\n */\n addEdge(edge: GraphEdge): void {\n if (this.store.edges.size >= this.config.maxEdges) {\n this.evictOldEdges()\n }\n\n // Ensure nodes exist\n if (!this.store.nodes.has(edge.source)) {\n this.addNode({ id: edge.source, type: 'task', label: edge.source, properties: {} })\n }\n if (!this.store.nodes.has(edge.target)) {\n this.addNode({ id: edge.target, type: 'task', label: edge.target, properties: {} })\n }\n\n this.store.edges.set(edge.id, edge)\n\n // Index by type\n if (!this.store.edgeIndex.has(edge.type)) {\n this.store.edgeIndex.set(edge.type, new Set())\n }\n this.store.edgeIndex.get(edge.type)!.add(edge.id)\n }\n\n /**\n * Add multiple edges\n */\n addEdges(edges: GraphEdge[]): void {\n for (const edge of edges) {\n this.addEdge(edge)\n }\n }\n\n /**\n * Get edges by type\n */\n getEdgesByType(type: GraphEdgeType): GraphEdge[] {\n const ids = this.store.edgeIndex.get(type)\n if (!ids) return []\n return Array.from(ids).map(id => this.store.edges.get(id)!).filter(Boolean)\n }\n\n /**\n * Remove an edge\n */\n removeEdge(id: string): void {\n const edge = this.store.edges.get(id)\n if (!edge) return\n\n this.store.edgeIndex.get(edge.type)?.delete(id)\n this.store.edges.delete(id)\n }\n\n // =========================================================================\n // Graph Queries\n // =========================================================================\n\n /**\n * Traverse graph from a root node\n */\n traverse(query: GraphQuery): { nodes: GraphNode[]; edges: GraphEdge[] } {\n const visitedNodes = new Set\u003cstring>()\n const visitedEdges = new Set\u003cstring>()\n const resultNodes: GraphNode[] = []\n const resultEdges: GraphEdge[] = []\n\n if (query.rootNodeId) {\n this.dfs(\n query.rootNodeId,\n query.depth || 2,\n query.direction || 'both',\n query.edgeTypes,\n visitedNodes,\n visitedEdges,\n resultNodes,\n resultEdges\n )\n } else if (query.rootType) {\n // Start from all nodes of root type\n const rootNodes = this.getNodesByType(query.rootType)\n for (const root of rootNodes.slice(0, 10)) { // Limit initial nodes\n this.dfs(\n root.id,\n query.depth || 2,\n query.direction || 'both',\n query.edgeTypes,\n visitedNodes,\n visitedEdges,\n resultNodes,\n resultEdges\n )\n }\n }\n\n return { nodes: resultNodes, edges: resultEdges }\n }\n\n private dfs(\n nodeId: string,\n remainingDepth: number,\n direction: 'outgoing' | 'incoming' | 'both',\n edgeTypes: GraphEdgeType[] | undefined,\n visitedNodes: Set\u003cstring>,\n visitedEdges: Set\u003cstring>,\n resultNodes: GraphNode[],\n resultEdges: GraphEdge[]\n ): void {\n if (remainingDepth \u003c 0 || visitedNodes.has(nodeId)) return\n visitedNodes.add(nodeId)\n\n const node = this.store.nodes.get(nodeId)\n if (node) {\n resultNodes.push(node)\n }\n\n const edges = Array.from(this.store.edges.values())\n .filter(e => !visitedEdges.has(e.id))\n .filter(e => {\n if (edgeTypes && !edgeTypes.includes(e.type)) return false\n return e.source === nodeId || e.target === nodeId\n })\n\n for (const edge of edges) {\n visitedEdges.add(edge.id)\n resultEdges.push(edge)\n\n const nextNodeId = edge.source === nodeId ? edge.target : edge.source\n const nextDirection = edge.source === nodeId ? 'outgoing' : 'incoming'\n\n if (direction === 'both' || direction === nextDirection) {\n this.dfs(\n nextNodeId,\n remainingDepth - 1,\n direction,\n edgeTypes,\n visitedNodes,\n visitedEdges,\n resultNodes,\n resultEdges\n )\n }\n }\n }\n\n /**\n * Find patterns: given an expert, find related patterns\n */\n findRelatedPatterns(expertId: string): GraphNode[] {\n const result = this.traverse({\n rootNodeId: expertId,\n depth: 2,\n edgeTypes: ['expert_uses_pattern', 'expert_participated_in']\n })\n return result.nodes.filter(n => n.type === 'pattern')\n }\n\n /**\n * Find experts related to a task\n */\n findTaskExperts(taskId: string): GraphNode[] {\n const result = this.traverse({\n rootNodeId: taskId,\n depth: 1,\n edgeTypes: ['expert_participated_in']\n })\n return result.nodes.filter(n => n.type === 'expert')\n }\n\n /**\n * Find related sessions\n */\n findRelatedSessions(sessionId: string): GraphNode[] {\n const result = this.traverse({\n rootNodeId: sessionId,\n depth: 2,\n edgeTypes: ['session_related_to']\n })\n return result.nodes.filter(n => n.type === 'session')\n }\n\n // =========================================================================\n // Graph Statistics\n // =========================================================================\n\n getStats(): {\n totalNodes: number\n totalEdges: number\n nodesByType: Record\u003cGraphNodeType, number>\n edgesByType: Record\u003cGraphEdgeType, number>\n } {\n const nodesByType: Record\u003cGraphNodeType, number> = {\n expert: 0, pattern: 0, task: 0, decision: 0, session: 0, checkpoint: 0\n }\n const edgesByType: Record\u003cGraphEdgeType, number> = {\n expert_uses_pattern: 0,\n task_has_decision: 0,\n expert_participated_in: 0,\n pattern_resolved: 0,\n checkpoint_validated: 0,\n session_related_to: 0\n }\n\n for (const node of this.store.nodes.values()) {\n nodesByType[node.type]++\n }\n for (const edge of this.store.edges.values()) {\n edgesByType[edge.type]++\n }\n\n return {\n totalNodes: this.store.nodes.size,\n totalEdges: this.store.edges.size,\n nodesByType,\n edgesByType\n }\n }\n\n // =========================================================================\n // Persistence (Simple JSON-based)\n // =========================================================================\n\n async save(): Promise\u003cvoid> {\n try {\n const fs = await import('fs')\n const path = await import('path')\n \n const data = {\n nodes: Array.from(this.store.nodes.values()),\n edges: Array.from(this.store.edges.values())\n }\n \n const dir = path.dirname(this.config.persistPath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n \n fs.writeFileSync(this.config.persistPath, JSON.stringify(data, null, 2))\n } catch (err) {\n console.error('[KnowledgeGraph] Save failed:', err)\n }\n }\n\n async load(): Promise\u003cvoid> {\n try {\n const fs = await import('fs')\n \n if (!fs.existsSync(this.config.persistPath)) return\n\n const data = JSON.parse(fs.readFileSync(this.config.persistPath, 'utf-8'))\n \n this.store = this.initStore()\n \n for (const node of data.nodes || []) {\n this.addNode(node)\n }\n for (const edge of data.edges || []) {\n this.addEdge(edge)\n }\n } catch (err) {\n console.error('[KnowledgeGraph] Load failed:', err)\n }\n }\n\n // =========================================================================\n // Maintenance\n // =========================================================================\n\n private evictOldNodes(): void {\n // Remove oldest 10% of nodes\n const nodes = Array.from(this.store.nodes.values())\n .sort((a, b) => {\n const aTime = (a.properties?.createdAt as number) || 0\n const bTime = (b.properties?.createdAt as number) || 0\n return aTime - bTime\n })\n \n const toRemove = Math.ceil(nodes.length * 0.1)\n for (let i = 0; i \u003c toRemove; i++) {\n this.removeNode(nodes[i].id)\n }\n }\n\n private evictOldEdges(): void {\n // Remove oldest 10% of edges\n const edges = Array.from(this.store.edges.values())\n .sort((a, b) => {\n const aTime = (a.properties?.createdAt as number) || 0\n const bTime = (b.properties?.createdAt as number) || 0\n return aTime - bTime\n })\n \n const toRemove = Math.ceil(edges.length * 0.1)\n for (let i = 0; i \u003c toRemove; i++) {\n this.removeEdge(edges[i].id)\n }\n }\n\n clear(): void {\n this.store = this.initStore()\n }\n}\n\n// Singleton\nlet defaultGraph: KnowledgeGraphEngine | null = null\n\nexport function getKnowledgeGraph(config?: KnowledgeGraphConfig): KnowledgeGraphEngine {\n if (!defaultGraph) {\n defaultGraph = new KnowledgeGraphEngine(config)\n }\n return defaultGraph\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":11722,"content_sha256":"b8f604586f2a914a1a33591b305fc2c0a48ee3e0c62df699450d93833b125f60"},{"filename":"src/retrieval/hybrid-search.ts","content":"/**\n * Hybrid Search Implementation\n * \n * 混合检索实现,结合稀疏检索(BM25)和密集检索(向量)\n * 使用 Reciprocal Rank Fusion (RRF) 进行分数融合\n */\n\nimport type { \n KnowledgeChunk, \n VectorSearchResult,\n RetrievalQuery,\n RetrievalResult,\n RetrievalStrategy,\n IndexLayer\n} from '../types/index.js'\nimport { LanceDBAdapter } from '../vector-store/lancedb-adapter.js'\nimport { RetrievalRouter } from './router.js'\n\nexport interface HybridSearchConfig {\n denseWeight?: number // Alpha weight for dense retrieval\n sparseWeight?: number // 1 - alpha weight for sparse retrieval\n rrfK?: number // RRF k parameter (default 60)\n enableHybrid?: boolean // Enable hybrid search\n}\n\nconst DEFAULT_CONFIG: Required\u003cHybridSearchConfig> = {\n denseWeight: 0.6,\n sparseWeight: 0.4,\n rrfK: 60,\n enableHybrid: true\n}\n\n// ============================================================================\n// BM25 Sparse Retrieval (Simplified Implementation)\n// ============================================================================\n\ninterface BM25Document {\n id: string\n terms: string[]\n originalContent: string\n}\n\ninterface BM25Result {\n id: string\n score: number\n}\n\n/**\n * Simple BM25 implementation for sparse retrieval\n */\nclass BM25Index {\n private documents: Map\u003cstring, BM25Document> = new Map()\n private documentFrequency: Map\u003cstring, number> = new Map()\n private avgDocLength = 0\n private k1 = 1.5\n private b = 0.75\n\n /**\n * Index documents for BM25 search\n */\n index(chunks: KnowledgeChunk[]): void {\n this.documents.clear()\n this.documentFrequency.clear()\n\n let totalLength = 0\n\n for (const chunk of chunks) {\n const terms = this.tokenize(chunk.content)\n this.documents.set(chunk.id, {\n id: chunk.id,\n terms,\n originalContent: chunk.content\n })\n totalLength += terms.length\n\n // Update document frequency\n const uniqueTerms = new Set(terms)\n for (const term of uniqueTerms) {\n this.documentFrequency.set(\n term, \n (this.documentFrequency.get(term) || 0) + 1\n )\n }\n }\n\n this.avgDocLength = totalLength / Math.max(this.documents.size, 1)\n }\n\n /**\n * Search using BM25\n */\n search(query: string, topK: number): BM25Result[] {\n const queryTerms = this.tokenize(query)\n if (queryTerms.length === 0) return []\n\n const scores: Map\u003cstring, number> = new Map()\n const N = this.documents.size\n\n for (const [docId, doc] of this.documents) {\n let score = 0\n const docLength = doc.terms.length\n\n for (const term of queryTerms) {\n const df = this.documentFrequency.get(term) || 0\n if (df === 0) continue\n\n const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1)\n \n // Term frequency in document\n const tf = doc.terms.filter(t => t === term).length\n \n // BM25 scoring formula\n const numerator = tf * (this.k1 + 1)\n const denominator = tf + this.k1 * (1 - this.b + this.b * (docLength / this.avgDocLength))\n \n score += idf * (numerator / denominator)\n }\n\n if (score > 0) {\n scores.set(docId, score)\n }\n }\n\n // Sort by score and return top K\n return Array.from(scores.entries())\n .sort((a, b) => b[1] - a[1])\n .slice(0, topK)\n .map(([id, score]) => ({ id, score }))\n }\n\n private tokenize(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s\\u4e00-\\u9fff]/g, ' ') // Keep Chinese characters\n .split(/\\s+/)\n .filter(term => term.length > 1)\n }\n}\n\n// ============================================================================\n// Hybrid Search\n// ============================================================================\n\nexport interface HybridSearchResult {\n chunks: KnowledgeChunk[]\n scores: number[]\n denseScores: number[]\n sparseScores: number[]\n totalHits: number\n queryTimeMs: number\n}\n\n/**\n * Hybrid Search Engine\n */\nexport class HybridSearchEngine {\n private adapter: LanceDBAdapter\n private router: RetrievalRouter\n private config: Required\u003cHybridSearchConfig>\n private bm25Index: BM25Index\n\n // Cache for BM25 indexing (in-memory only)\n private indexedChunks: KnowledgeChunk[] = []\n private lastIndexTime = 0\n\n constructor(\n adapter: LanceDBAdapter,\n router: RetrievalRouter,\n config?: HybridSearchConfig\n ) {\n this.adapter = adapter\n this.router = router\n this.config = { ...DEFAULT_CONFIG, ...config }\n this.bm25Index = new BM25Index()\n }\n\n /**\n * Perform hybrid search\n */\n async search(query: RetrievalQuery): Promise\u003cRetrievalResult> {\n const startTime = Date.now()\n\n // Generate query embedding\n const queryEmbedding = await this.adapter.generateEmbedding(query.query)\n\n // Get layers to search\n const layers = query.layers || [1, 2, 3, 4, 5]\n const topK = query.topK || 5\n\n // Perform dense (vector) search\n const denseResults = await this.performDenseSearch(\n queryEmbedding, \n topK * 3, // Request more for fusion\n layers\n )\n\n // Perform sparse (BM25) search if hybrid enabled\n let sparseResults: VectorSearchResult[] = []\n if (this.config.enableHybrid) {\n sparseResults = await this.performSparseSearch(query.query, topK * 3, layers)\n }\n\n // Fuse results using RRF\n const fusedResults = this.reciprocalRankFusion(\n denseResults,\n sparseResults,\n this.config.rrfK\n )\n\n // Get top K results\n const topResults = fusedResults.slice(0, topK)\n\n // Fetch full chunks for results\n const chunks: KnowledgeChunk[] = []\n const scores: number[] = []\n const denseScores: number[] = []\n const sparseScores: number[] = []\n\n for (const result of topResults) {\n const chunk = await this.adapter.get(result.id)\n if (chunk) {\n chunks.push(chunk)\n scores.push(result.fusedScore)\n denseScores.push(result.denseScore || 0)\n sparseScores.push(result.sparseScore || 0)\n }\n }\n\n return {\n chunks,\n strategy: query.strategy || 'hybrid_retrieval',\n totalHits: fusedResults.length,\n queryTimeMs: Date.now() - startTime,\n scores\n }\n }\n\n /**\n * Perform dense vector search\n */\n private async performDenseSearch(\n embedding: number[],\n limit: number,\n layers: number[]\n ): Promise\u003cArray\u003cVectorSearchResult & { denseScore: number }>> {\n const allResults: Array\u003cVectorSearchResult & { denseScore: number }> = []\n\n for (const layer of layers) {\n try {\n const results = await this.adapter.search(embedding, limit, {\n layer: layer as IndexLayer\n })\n\n for (const result of results) {\n allResults.push({\n ...result,\n denseScore: result.score\n })\n }\n } catch {\n // Ignore layer search errors\n }\n }\n\n // Deduplicate by ID, keeping highest score\n const deduplicated = new Map\u003cstring, VectorSearchResult & { denseScore: number }>()\n for (const result of allResults) {\n const existing = deduplicated.get(result.id)\n if (!existing || result.denseScore > existing.denseScore) {\n deduplicated.set(result.id, result)\n }\n }\n\n return Array.from(deduplicated.values())\n }\n\n /**\n * Perform sparse BM25 search\n */\n private async performSparseSearch(\n query: string,\n limit: number,\n layers: number[]\n ): Promise\u003cArray\u003cVectorSearchResult & { sparseScore: number }>> {\n // Check if we need to rebuild the index\n const indexAge = Date.now() - this.lastIndexTime\n if (indexAge > 5 * 60 * 1000 || this.indexedChunks.length === 0) {\n await this.rebuildBM25Index(layers)\n }\n\n // Search BM25\n const bm25Results = this.bm25Index.search(query, limit)\n\n // Convert to VectorSearchResult format\n const results: Array\u003cVectorSearchResult & { sparseScore: number }> = []\n for (const bm25Result of bm25Results) {\n const chunk = await this.adapter.get(bm25Result.id)\n if (chunk) {\n results.push({\n id: bm25Result.id,\n score: bm25Result.score,\n sparseScore: bm25Result.score,\n payload: {\n content: chunk.content,\n chunkType: chunk.chunkType,\n layer: chunk.layer,\n metadata: chunk.metadata\n }\n })\n }\n }\n\n return results\n }\n\n /**\n * Rebuild the BM25 index from vector store\n */\n private async rebuildBM25Index(layers: number[]): Promise\u003cvoid> {\n const chunks: KnowledgeChunk[] = []\n \n for (const layer of layers) {\n // Fetch chunks for this layer (simplified - in production use pagination)\n for (let i = 0; i \u003c 1000; i++) {\n // This is a simplified approach\n // In production, you'd have a better way to iterate\n }\n }\n\n // For now, we'll use an in-memory index approach\n // The BM25 index is rebuilt periodically with all indexed chunks\n this.indexedChunks = chunks\n this.bm25Index.index(chunks)\n this.lastIndexTime = Date.now()\n }\n\n /**\n * Reciprocal Rank Fusion (RRF) for combining results\n * \n * RRF formula: score(d) = Σ 1/(k + rank(d))\n * where k is a constant (default 60) and rank(d) is the rank in each result list\n */\n private reciprocalRankFusion(\n denseResults: Array\u003c{ id: string; denseScore?: number }>,\n sparseResults: Array\u003c{ id: string; sparseScore?: number }>,\n k: number\n ): Array\u003c{ id: string; fusedScore: number; denseScore?: number; sparseScore?: number }> {\n const scores = new Map\u003cstring, {\n fusedScore: number\n denseScore?: number\n sparseScore?: number\n }>()\n\n // Add dense results with RRF\n denseResults.forEach((result, rank) => {\n const rrfScore = 1 / (k + rank + 1)\n const existing = scores.get(result.id) || { fusedScore: 0 }\n existing.fusedScore += this.config.denseWeight * rrfScore\n existing.denseScore = result.denseScore\n scores.set(result.id, existing)\n })\n\n // Add sparse results with RRF\n sparseResults.forEach((result, rank) => {\n const rrfScore = 1 / (k + rank + 1)\n const existing = scores.get(result.id) || { fusedScore: 0 }\n existing.fusedScore += this.config.sparseWeight * rrfScore\n existing.sparseScore = result.sparseScore\n scores.set(result.id, existing)\n })\n\n // Sort by fused score\n return Array.from(scores.entries())\n .map(([id, data]) => ({ id, ...data }))\n .sort((a, b) => b.fusedScore - a.fusedScore)\n }\n\n /**\n * Update BM25 index with new chunks\n */\n async updateBM25Index(chunks: KnowledgeChunk[]): Promise\u003cvoid> {\n for (const chunk of chunks) {\n const existing = this.indexedChunks.findIndex(c => c.id === chunk.id)\n if (existing >= 0) {\n this.indexedChunks[existing] = chunk\n } else {\n this.indexedChunks.push(chunk)\n }\n }\n this.bm25Index.index(this.indexedChunks)\n this.lastIndexTime = Date.now()\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":10995,"content_sha256":"a6cb081df686474895b8182021c31cf1769d2f039b2b4cc9f56d6a1f3c57250a"},{"filename":"src/retrieval/reranker.ts","content":"/**\n * Reranker for Retrieval Results\n * \n * 使用 Cross-Encoder 或基于规则的 reranking 提高检索精度\n * 支持两种模式:\n * - cross-encoder: 使用神经网络模型(需要外部 API)\n * - rules-based: 基于规则的轻量级 reranking\n */\n\nimport type { KnowledgeChunk, RetrievalResult, ChunkMetadata } from '../types/index.js'\n\nexport interface RerankerConfig {\n model?: string // Cross-encoder model name\n topK?: number // Final number of results to return\n enableRules?: boolean\n}\n\nexport interface RerankedResult {\n chunks: KnowledgeChunk[]\n scores: number[]\n rerankReason?: string\n}\n\n/**\n * Reranker interface\n */\nexport interface Reranker {\n rerank(\n chunks: KnowledgeChunk[], \n query: string, \n topK?: number\n ): Promise\u003cRerankedResult>\n}\n\n/**\n * Rules-based reranker\n * Applies heuristic rules to improve relevance scoring\n */\nexport class RulesBasedReranker implements Reranker {\n private config: Required\u003cRerankerConfig>\n\n constructor(config?: RerankerConfig) {\n this.config = {\n model: config?.model || 'rules',\n topK: config?.topK || 5,\n enableRules: config?.enableRules ?? true\n }\n }\n\n async rerank(\n chunks: KnowledgeChunk[], \n query: string,\n topK?: number\n ): Promise\u003cRerankedResult> {\n const targetK = topK || this.config.topK\n const queryLower = query.toLowerCase()\n const queryTerms = queryLower.split(/\\s+/).filter(t => t.length > 1)\n\n // Score each chunk with rules\n const scored = chunks.map(chunk => {\n let score = chunk.vectorScore || 0\n let reasons: string[] = []\n\n // Rule 1: Exact keyword match boost\n const contentLower = chunk.content.toLowerCase()\n const exactMatches = queryTerms.filter(term => contentLower.includes(term)).length\n score += exactMatches * 0.1\n if (exactMatches > 0) reasons.push(`+${exactMatches} exact matches`)\n\n // Rule 2: Title/heading match boost\n const firstLine = contentLower.split('\\n')[0] || ''\n const titleMatches = queryTerms.filter(term => firstLine.includes(term)).length\n score += titleMatches * 0.15\n if (titleMatches > 0) reasons.push(`+${titleMatches} title matches`)\n\n // Rule 3: Recent content boost\n const ageMs = Date.now() - chunk.updatedAt\n const ageDays = ageMs / (1000 * 60 * 60 * 24)\n if (ageDays \u003c 30) {\n score += 0.1 * (1 - ageDays / 30)\n reasons.push('recent content boost')\n }\n\n // Rule 4: Success rate boost (for historical content)\n const metadata = chunk.metadata as ChunkMetadata\n if (metadata.successRate !== undefined && metadata.successRate > 0.8) {\n score += 0.05\n reasons.push('high success rate')\n }\n\n // Rule 5: Expert authority boost\n if (chunk.chunkType === 'expert' && metadata.expertRole) {\n // Architect and Planner get slight authority boost\n const authorityRoles = ['architect', 'planner', 'security']\n if (authorityRoles.some(r => metadata.expertRole?.toLowerCase().includes(r))) {\n score += 0.08\n reasons.push('authority role')\n }\n }\n\n // Rule 6: Query type matching\n if (this.matchesQueryType(chunk, queryLower)) {\n score += 0.2\n reasons.push('query type match')\n }\n\n return {\n chunk,\n score,\n reasons\n }\n })\n\n // Sort by adjusted score\n scored.sort((a, b) => b.score - a.score)\n\n // Return top K\n const topResults = scored.slice(0, targetK)\n\n return {\n chunks: topResults.map(s => s.chunk),\n scores: topResults.map(s => s.score),\n rerankReason: topResults.length > 0 \n ? `Reranked by rules: ${topResults[0].reasons.slice(0, 2).join(', ')}`\n : 'No reranking needed'\n }\n }\n\n private matchesQueryType(chunk: KnowledgeChunk, query: string): boolean {\n const chunkType = chunk.chunkType\n\n // Expert query\n if (/专家|expert|谁来做|who.*do/i.test(query) && chunkType === 'expert') {\n return true\n }\n\n // Pattern query\n if (/模式|pattern|流程|workflow/i.test(query) && chunkType === 'pattern') {\n return true\n }\n\n // Checkpoint query\n if (/验证|checkpoint|检查|verify|测试/i.test(query) && chunkType === 'checkpoint') {\n return true\n }\n\n // History/session query\n if (/之前|上次|历史|lessons/i.test(query) && chunkType === 'session') {\n return true\n }\n\n // Decision query\n if (/决策|决定|方案/i.test(query) && chunkType === 'decision') {\n return true\n }\n\n return false\n }\n}\n\n/**\n * Cross-Encoder reranker (placeholder for OpenAI/Cohere integration)\n */\nexport class CrossEncoderReranker implements Reranker {\n private config: RerankerConfig\n private apiKey?: string\n private endpoint?: string\n\n constructor(config: RerankerConfig = {}, apiKey?: string) {\n this.config = config\n this.apiKey = apiKey\n // Default endpoint for OpenAI-compatible cross-encoder\n this.endpoint = 'https://api.openai.com/v1/embeddings'\n }\n\n async rerank(\n chunks: KnowledgeChunk[], \n query: string,\n topK?: number\n ): Promise\u003cRerankedResult> {\n const targetK = topK || this.config.topK || 5\n\n if (!this.apiKey) {\n // Fallback to rules-based reranker\n const fallback = new RulesBasedReranker({ topK: targetK })\n return fallback.rerank(chunks, query, targetK)\n }\n\n try {\n // Create query-document pairs for scoring\n const pairs = chunks.map(chunk => [query, chunk.content])\n\n // Call cross-encoder API\n const scores = await this.callCrossEncoderAPI(pairs)\n\n // Combine with original scores\n const scored = chunks.map((chunk, i) => ({\n chunk,\n combinedScore: (chunk.vectorScore || 0) * 0.3 + scores[i] * 0.7\n }))\n\n // Sort by combined score\n scored.sort((a, b) => b.combinedScore - a.combinedScore)\n\n const topResults = scored.slice(0, targetK)\n\n return {\n chunks: topResults.map(s => s.chunk),\n scores: topResults.map(s => s.combinedScore),\n rerankReason: `Cross-encoder reranking (${this.config.model || 'default'})`\n }\n } catch (err) {\n console.error('[Reranker] Cross-encoder failed, falling back to rules:', err)\n const fallback = new RulesBasedReranker({ topK: targetK })\n return fallback.rerank(chunks, query, targetK)\n }\n }\n\n private async callCrossEncoderAPI(pairs: string[][]): Promise\u003cnumber[]> {\n // This is a simplified implementation\n // In production, use actual cross-encoder API\n // OpenAI or Cohere offers cross-encoder models\n \n // For now, return random scores as placeholder\n // Real implementation would call: POST to cross-encoder endpoint\n return pairs.map(() => Math.random())\n }\n}\n\n/**\n * Create a reranker based on configuration\n */\nexport function createReranker(config?: RerankerConfig): Reranker {\n if (config?.model && config.model !== 'rules') {\n return new CrossEncoderReranker(config)\n }\n return new RulesBasedReranker(config)\n}\n\n/**\n * Rerank retrieval results\n */\nexport async function rerankResults(\n results: RetrievalResult,\n config?: RerankerConfig\n): Promise\u003cRerankedResult> {\n const reranker = createReranker(config)\n return reranker.rerank(\n results.chunks, \n results.strategy === 'hybrid_retrieval' ? 'general' : results.strategy,\n results.chunks.length\n )\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":7390,"content_sha256":"0269b54555431c6bd0ff9895b9dddee7b644696f4a96746c1342d51a3f6cdcce"},{"filename":"src/retrieval/router.ts","content":"/**\n * Retrieval Router\n * \n * 查询路由 - 根据用户意图将查询路由到合适的检索策略\n * 支持 6 种检索策略:\n * - expert_retrieval: 专家匹配\n * - pattern_retrieval: 模式检索\n * - checkpoint_retrieval: 验证点检索\n * - history_retrieval: 历史经验检索\n * - decision_retrieval: 决策检索\n * - hybrid_retrieval: 混合检索\n */\n\nimport type { RetrievalStrategy, RetrievalQuery } from '../types/index.js'\nimport { ROUTING_PATTERNS } from '../config.js'\n\nexport interface RouterConfig {\n enableFuzzy?: boolean\n defaultStrategy?: RetrievalStrategy\n confidenceThreshold?: number\n}\n\n/**\n * Intent analysis result\n */\nexport interface IntentAnalysis {\n strategy: RetrievalStrategy\n confidence: number\n matchedKeywords: string[]\n reasoning: string\n}\n\n/**\n * Query Router - routes queries to appropriate retrieval strategies\n */\nexport class RetrievalRouter {\n private config: Required\u003cRouterConfig>\n\n constructor(config?: RouterConfig) {\n this.config = {\n enableFuzzy: config?.enableFuzzy ?? true,\n defaultStrategy: config?.defaultStrategy ?? 'hybrid_retrieval',\n confidenceThreshold: config?.confidenceThreshold ?? 0.5\n }\n }\n\n /**\n * Route a query to the appropriate retrieval strategy\n */\n route(query: string, context?: {\n taskType?: string\n currentExperts?: string[]\n }): RetrievalStrategy {\n const analysis = this.analyzeIntent(query, context)\n return analysis.strategy\n }\n\n /**\n * Analyze query intent and return detailed analysis\n */\n analyzeIntent(\n query: string, \n context?: {\n taskType?: string\n currentExperts?: string[]\n }\n ): IntentAnalysis {\n const normalizedQuery = query.toLowerCase().trim()\n const scores: Record\u003cRetrievalStrategy, number> = {\n expert_retrieval: 0,\n pattern_retrieval: 0,\n checkpoint_retrieval: 0,\n history_retrieval: 0,\n decision_retrieval: 0,\n hybrid_retrieval: 0\n }\n const matchedKeywords: Record\u003cRetrievalStrategy, string[]> = {\n expert_retrieval: [],\n pattern_retrieval: [],\n checkpoint_retrieval: [],\n history_retrieval: [],\n decision_retrieval: [],\n hybrid_retrieval: []\n }\n\n // Score each strategy based on pattern matching\n for (const [strategy, patterns] of Object.entries(ROUTING_PATTERNS)) {\n const s = strategy as keyof typeof ROUTING_PATTERNS\n for (const pattern of patterns) {\n if (typeof pattern === 'string') {\n if (normalizedQuery.includes(pattern.toLowerCase())) {\n scores[s] += 1\n matchedKeywords[s].push(pattern)\n }\n } else if (pattern.test(normalizedQuery)) {\n scores[s] += 1\n matchedKeywords[s].push(pattern.source)\n }\n }\n }\n\n // Context-aware adjustments\n if (context?.taskType) {\n scores.hybrid_retrieval += 0.5\n }\n if (context?.currentExperts && context.currentExperts.length > 0) {\n // If experts already selected, reduce expert retrieval score\n scores.expert_retrieval *= 0.5\n scores.hybrid_retrieval += 0.3\n }\n\n // Find best strategy\n let bestStrategy = this.config.defaultStrategy\n let bestScore = 0\n for (const [strategy, score] of Object.entries(scores)) {\n if (score > bestScore) {\n bestScore = score\n bestStrategy = strategy as RetrievalStrategy\n }\n }\n\n // If no clear winner, use hybrid\n if (bestScore \u003c 0.5) {\n bestStrategy = 'hybrid_retrieval'\n }\n\n const confidence = bestScore / (Math.max(...Object.values(scores)) || 1)\n\n return {\n strategy: bestStrategy,\n confidence,\n matchedKeywords: matchedKeywords[bestStrategy],\n reasoning: this.generateReasoning(bestStrategy, matchedKeywords[bestStrategy], context)\n }\n }\n\n /**\n * Generate human-readable reasoning for the routing decision\n */\n private generateReasoning(\n strategy: RetrievalStrategy,\n matchedKeywords: string[],\n context?: {\n taskType?: string\n currentExperts?: string[]\n }\n ): string {\n const parts: string[] = []\n\n parts.push(`Strategy: ${strategy}`)\n \n if (matchedKeywords.length > 0) {\n parts.push(`Matched patterns: ${matchedKeywords.join(', ')}`)\n }\n \n if (context?.taskType) {\n parts.push(`Context: taskType=${context.taskType}`)\n }\n \n if (context?.currentExperts && context.currentExperts.length > 0) {\n parts.push(`Context: ${context.currentExperts.length} experts already selected`)\n }\n\n return parts.join(' | ')\n }\n\n /**\n * Determine which layers to search based on strategy\n */\n getTargetLayers(strategy: RetrievalStrategy): number[] {\n switch (strategy) {\n case 'expert_retrieval':\n return [1] // L1 only\n case 'pattern_retrieval':\n return [2] // L2 only\n case 'checkpoint_retrieval':\n return [3] // L3 only\n case 'decision_retrieval':\n return [4] // L4 only\n case 'history_retrieval':\n return [5, 4] // L5 primary, L4 secondary\n case 'hybrid_retrieval':\n default:\n return [1, 2, 3, 4, 5] // All layers\n }\n }\n\n /**\n * Build retrieval query from raw query and strategy\n */\n buildRetrievalQuery(\n rawQuery: string,\n strategy?: RetrievalStrategy,\n context?: {\n taskType?: string\n currentExperts?: string[]\n topK?: number\n }\n ): RetrievalQuery {\n const resolvedStrategy = strategy || this.route(rawQuery, context)\n\n return {\n query: rawQuery,\n strategy: resolvedStrategy,\n topK: context?.topK || 5,\n layers: this.getTargetLayers(resolvedStrategy)\n }\n }\n}\n\n// Convenience function for quick routing\nexport function routeQuery(\n query: string, \n context?: {\n taskType?: string\n currentExperts?: string[]\n }\n): RetrievalStrategy {\n const router = new RetrievalRouter()\n return router.route(query, context)\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":5913,"content_sha256":"ffb35018ce3918475cf06b9da02800d4b880398c80921f9901d264eada597aea"},{"filename":"src/types/index.ts","content":"/**\n * EO RAG System - Core Type Definitions\n * 5层索引知识库 + 混合检索 + ETP经验传递协议\n */\n\n// ============================================================================\n// Chunk Types (L1-L5)\n// ============================================================================\n\nexport type ChunkType = 'expert' | 'pattern' | 'checkpoint' | 'decision' | 'session'\nexport type IndexLayer = 1 | 2 | 3 | 4 | 5\n\nexport interface KnowledgeChunk {\n id: string\n content: string\n chunkType: ChunkType\n layer: IndexLayer\n metadata: ChunkMetadata\n embedding?: number[]\n vectorScore?: number\n createdAt: number\n updatedAt: number\n}\n\nexport interface ChunkMetadata {\n sourceFile?: string\n expertRole?: string // L1: 专家角色\n category?: string // L1: 专家分类\n patternName?: string // L2: 模式名称\n patternType?: string // L2: 模式类型\n taskId?: string // L3/L4: 任务ID\n sessionId?: string // L5: 会话ID\n timestamp?: number\n parentSession?: string // L5: 父会话\n successRate?: number // L3: 历史成功率\n taskType?: string // L3/L4: 任务类型\n participatingExperts?: string[] // L4: 参与专家\n duration?: number // L4: 执行时长\n outcome?: 'success' | 'partial' | 'failed' // L4: 结果\n}\n\n// ============================================================================\n// L1: Expert Profile Types\n// ============================================================================\n\nexport interface ExpertProfile {\n id: string\n name: string\n role: string\n category: string\n description: string\n capabilities: string[]\n tools: string[]\n prompt?: string\n skills?: string[]\n available?: boolean\n systemPrompt?: string\n}\n\nexport interface ExpertSearchQuery {\n query: string\n category?: string\n capabilities?: string[]\n limit?: number\n}\n\nexport interface ExpertSearchResult {\n expert: ExpertProfile\n similarity: number\n matchedCapabilities: string[]\n layer: 1\n}\n\n// ============================================================================\n// L2: Pattern Types\n// ============================================================================\n\nexport interface PatternInstance {\n id: string\n name: string\n description: string\n patternType: PatternType\n definition: string\n usageScenarios: string[]\n examples: PatternExample[]\n workflow?: PatternWorkflow\n successFactors?: string[]\n failureModes?: string[]\n confidence?: number\n usageCount?: number\n}\n\nexport type PatternType = \n | 'miniprogram' \n | 'website' \n | 'mobile-app' \n | 'algorithm' \n | 'academic-paper'\n | 'multi-expert'\n | 'general'\n\nexport interface PatternExample {\n title: string\n description: string\n codeSnippet?: string\n outcome: 'success' | 'partial' | 'failed'\n lessonsLearned?: string[]\n}\n\nexport interface PatternWorkflow {\n stages: WorkflowStage[]\n checkpoints: WorkflowCheckpoint[]\n}\n\nexport interface WorkflowStage {\n id: string\n name: string\n expertRole: string\n description: string\n input: string\n output: string\n typicalDurationMs?: number\n}\n\nexport interface WorkflowCheckpoint {\n id: string\n name: string\n validationRules: string[]\n autoVerify: boolean\n}\n\nexport interface PatternSearchQuery {\n query: string\n patternType?: PatternType\n limit?: number\n}\n\nexport interface PatternSearchResult {\n pattern: PatternInstance\n similarity: number\n matchedScenarios: string[]\n layer: 2\n}\n\n// ============================================================================\n// L3: Checkpoint Types\n// ============================================================================\n\nexport interface CheckpointRecord {\n id: string\n taskId: string\n sessionId: string\n checkpointName: string\n passed: boolean\n durationMs: number\n warnings?: number\n failedItems?: string[]\n timestamp: number\n experts: string[]\n recommendations?: string[]\n}\n\nexport interface CheckpointSearchQuery {\n query: string\n taskType?: string\n passedOnly?: boolean\n limit?: number\n}\n\n// ============================================================================\n// L4: Decision Types\n// ============================================================================\n\nexport interface DecisionRecord {\n id: string\n taskId: string\n sessionId: string\n decisionPoint: string\n alternatives: string[]\n chosen: string\n rationale: string\n timestamp: number\n participatingExperts: string[]\n outcome?: 'success' | 'partial' | 'failed'\n}\n\nexport interface DecisionSearchQuery {\n query: string\n taskType?: string\n experts?: string[]\n limit?: number\n}\n\n// ============================================================================\n// L5: Session Types\n// ============================================================================\n\nexport interface SessionSummary {\n id: string\n task: string\n taskType: string\n complexity: 'low' | 'medium' | 'high'\n summary: string\n keyDecisions: string[]\n keyOutcomes: string[]\n lessonsLearned: string[]\n participatingExperts: string[]\n durationMs: number\n success: boolean\n timestamp: number\n relatedTaskIds: string[]\n}\n\nexport interface SessionSearchQuery {\n query: string\n taskType?: string\n experts?: string[]\n limit?: number\n}\n\n// ============================================================================\n// ETP Types (Experience Transfer Protocol)\n// ============================================================================\n\nexport type ETPVersion = '1.0'\n\nexport type ETPTaskOutcome = 'success' | 'partial' | 'failed'\n\nexport interface ETPKeyDecision {\n point: string\n alternatives: string[]\n chosen: string\n rationale: string\n}\n\nexport interface ETPReusableSnippet {\n description: string\n content: string\n context: string\n}\n\nexport interface ETPRecord {\n protocolVersion: ETPVersion\n taskId: string\n taskType: string\n taskDescription: string\n participatingExperts: string[]\n durationMs: number\n outcome: ETPTaskOutcome\n keyDecisions: ETPKeyDecision[]\n lessons: string[]\n reusableSnippets: ETPReusableSnippet[]\n checkpointsPassed: string[]\n checkpointsFailed: string[]\n failureAnalysis?: string\n timestamp: number\n}\n\nexport interface ETPSearchQuery {\n query: string\n taskType?: string\n experts?: string[]\n outcome?: ETPTaskOutcome\n limit?: number\n}\n\n// ============================================================================\n// Retrieval Types\n// ============================================================================\n\nexport type RetrievalStrategy = \n | 'expert_retrieval' \n | 'pattern_retrieval' \n | 'checkpoint_retrieval'\n | 'history_retrieval'\n | 'decision_retrieval'\n | 'hybrid_retrieval'\n\nexport interface RetrievalQuery {\n query: string\n strategy?: RetrievalStrategy\n topK?: number\n filters?: RetrievalFilters\n layers?: IndexLayer[]\n}\n\nexport interface RetrievalFilters {\n chunkTypes?: ChunkType[]\n taskTypes?: string[]\n expertRoles?: string[]\n patternTypes?: PatternType[]\n sessionIds?: string[]\n dateFrom?: number\n dateTo?: number\n minSuccessRate?: number\n}\n\nexport interface RetrievalResult {\n chunks: KnowledgeChunk[]\n strategy: RetrievalStrategy\n totalHits: number\n queryTimeMs: number\n scores: number[]\n}\n\n// ============================================================================\n// Knowledge Graph Types\n// ============================================================================\n\nexport type GraphNodeType = 'expert' | 'pattern' | 'task' | 'decision' | 'session' | 'checkpoint'\n\nexport interface GraphNode {\n id: string\n type: GraphNodeType\n label: string\n properties: Record\u003cstring, unknown>\n}\n\nexport type GraphEdgeType = \n | 'expert_uses_pattern'\n | 'task_has_decision'\n | 'expert_participated_in'\n | 'pattern_resolved'\n | 'checkpoint_validated'\n | 'session_related_to'\n\nexport interface GraphEdge {\n id: string\n source: string\n target: string\n type: GraphEdgeType\n properties?: Record\u003cstring, unknown>\n}\n\nexport interface KnowledgeGraph {\n nodes: GraphNode[]\n edges: GraphEdge[]\n}\n\nexport interface GraphQuery {\n rootNodeId?: string\n rootType?: GraphNodeType\n edgeTypes?: GraphEdgeType[]\n depth?: number\n direction?: 'outgoing' | 'incoming' | 'both'\n}\n\n// ============================================================================\n// Conflict Resolution Types\n// ============================================================================\n\nexport type ConflictLevel = 'L1' | 'L2' | 'L3'\nexport type ConflictType = 'tool-parameter' | 'architecture-design' | 'priority-direction'\n\nexport interface ConflictRecord {\n id: string\n level: ConflictLevel\n type: ConflictType\n taskId: string\n conflictingExperts: string[]\n options: string[]\n resolution?: string\n resolvedAt?: number\n timestamp: number\n}\n\nexport interface ConflictResolutionWeights {\n recency: number\n successRate: number\n expertAuthority: number\n contextSimilarity: number\n}\n\n// ============================================================================\n// Vector Store Types\n// ============================================================================\n\nexport type VectorStoreType = 'lancedb' | 'chromadb'\n\nexport interface VectorStoreConfig {\n type: VectorStoreType\n persistPath?: string\n embeddingModel?: string\n embeddingDimension?: number\n}\n\nexport interface VectorSearchResult {\n id: string\n score: number\n payload: Record\u003cstring, unknown>\n}\n\n// ============================================================================\n// Context Injector Types\n// ============================================================================\n\nexport interface ContextInjectionRequest {\n query: string\n sessionId?: string\n taskId?: string\n taskType?: string\n currentExperts?: string[]\n topK?: number\n}\n\nexport interface ContextInjectionResult {\n success: boolean\n injectedContent: string\n chunksUsed: number\n sources: ContextSource[]\n injectionPoint: InjectionPoint\n}\n\nexport interface ContextSource {\n chunkId: string\n chunkType: ChunkType\n content: string\n relevanceScore: number\n metadata: ChunkMetadata\n}\n\nexport type InjectionPoint = 'system_prompt' | 'user_message' | 'both'\n\n// ============================================================================\n// RAG Config Types\n// ============================================================================\n\nexport interface RAGConfig {\n enabled: boolean\n vectorStore: VectorStoreConfig\n embedding: {\n model: string\n dimension: number\n provider: 'openai' | 'local'\n }\n retrieval: {\n defaultTopK: number\n rerankTopK: number\n hybridAlpha: number\n enableReranking: boolean\n }\n indexUpdate: {\n triggers: string[]\n autoIndex: boolean\n incrementalOnly: boolean\n }\n conflictResolution: ConflictResolutionWeights\n}\n\nexport interface IndexerStats {\n layer: IndexLayer\n totalChunks: number\n lastIndexed: number | null\n indexingDurationMs?: number\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":10854,"content_sha256":"0fd98a2b7805e72aaef9c22f1f85c9b922226a18417ca59747b3b2057b322e5d"},{"filename":"src/vector-store/chromadb-adapter.ts","content":"/**\n * ChromaDB Vector Store Adapter\n * \n * EO RAG 系统的备选向量存储适配器,基于 ChromaDB\n * ChromaDB 特点:\n * - 嵌入式,零运维\n * - LangChain 原生支持\n * - 适合快速原型验证\n */\n\nimport type { \n VectorStoreConfig, \n VectorSearchResult, \n KnowledgeChunk,\n ChunkMetadata,\n IndexLayer\n} from '../types/index.js'\nimport { DEFAULT_RAG_CONFIG } from '../config.js'\n\n// Try to import ChromaDB\nlet ChromaClient: any = null\nlet chromadbAvailable = false\n\nasync function tryInitChromaDB(): Promise\u003cboolean> {\n try {\n const chromaModule = await import('chroma-js').catch(() => null) ||\n await import('chromadb').catch(() => null)\n if (chromaModule) {\n ChromaClient = chromaModule\n chromadbAvailable = true\n return true\n }\n } catch {\n // ChromaDB not available\n }\n return false\n}\n\nconst COLLECTION_NAME = 'eo_rag_knowledge'\n\n/**\n * ChromaDB Adapter implementation\n */\nexport class ChromaDBAdapter {\n private config: VectorStoreConfig\n private client: any = null\n private collection: any = null\n private isInitialized: boolean = false\n\n constructor(config?: Partial\u003cVectorStoreConfig>) {\n this.config = {\n ...DEFAULT_RAG_CONFIG.vectorStore,\n ...config,\n type: 'chromadb'\n }\n }\n\n async initialize(): Promise\u003cvoid> {\n if (this.isInitialized) return\n\n const chromadbLoaded = await tryInitChromaDB()\n \n if (chromadbLoaded && ChromaClient) {\n try {\n // Create client (ChromaDB can run in-memory or persistent mode)\n this.client = new ChromaClient({\n path: this.config.persistPath || './data/rag/chromadb'\n })\n \n // Get or create collection\n try {\n this.collection = await this.client.getCollection(COLLECTION_NAME)\n } catch {\n this.collection = await this.client.createCollection(COLLECTION_NAME, {\n metadata: { schema: 'eo_rag_v1' }\n })\n }\n \n this.isInitialized = true\n console.log('[ChromaDB] Initialized successfully')\n } catch (err) {\n console.warn('[ChromaDB] Failed to initialize, using stub:', err)\n this.isInitialized = true\n }\n } else {\n console.warn('[ChromaDB] Not available, this adapter will be non-functional')\n this.isInitialized = true\n }\n }\n\n async add(chunk: KnowledgeChunk): Promise\u003cvoid> {\n await this.initialize()\n \n if (!this.collection) {\n console.warn('[ChromaDB] No collection available, skipping add')\n return\n }\n\n try {\n await this.collection.add({\n ids: [chunk.id],\n embeddings: [chunk.embedding || []],\n documents: [chunk.content],\n metadatas: [{\n chunkType: chunk.chunkType,\n layer: chunk.layer,\n metadata: JSON.stringify(chunk.metadata),\n createdAt: chunk.createdAt,\n updatedAt: chunk.updatedAt\n }]\n })\n } catch (err) {\n console.error('[ChromaDB] Add failed:', err)\n }\n }\n\n async addBatch(chunks: KnowledgeChunk[]): Promise\u003cvoid> {\n await this.initialize()\n \n if (!this.collection) {\n console.warn('[ChromaDB] No collection available, skipping addBatch')\n return\n }\n\n try {\n await this.collection.add({\n ids: chunks.map(c => c.id),\n embeddings: chunks.map(c => c.embedding || []),\n documents: chunks.map(c => c.content),\n metadatas: chunks.map(c => ({\n chunkType: c.chunkType,\n layer: c.layer,\n metadata: JSON.stringify(c.metadata),\n createdAt: c.createdAt,\n updatedAt: c.updatedAt\n }))\n })\n } catch (err) {\n console.error('[ChromaDB] AddBatch failed:', err)\n }\n }\n\n async search(\n queryEmbedding: number[],\n limit: number,\n filters?: {\n layer?: IndexLayer\n chunkType?: string\n metadata?: Record\u003cstring, unknown>\n }\n ): Promise\u003cVectorSearchResult[]> {\n await this.initialize()\n \n if (!this.collection) {\n return []\n }\n\n try {\n // Build where filter\n const whereFilter: Record\u003cstring, unknown> = {}\n if (filters?.layer !== undefined) {\n whereFilter.layer = filters.layer\n }\n if (filters?.chunkType) {\n whereFilter.chunkType = filters.chunkType\n }\n\n const results = await this.collection.query({\n queryEmbeddings: [queryEmbedding],\n nResults: limit,\n where: Object.keys(whereFilter).length > 0 ? whereFilter : undefined\n })\n\n if (!results.ids || !results.ids[0]) {\n return []\n }\n\n return results.ids[0].map((id: string, i: number) => ({\n id,\n score: results.distances?.[0]?.[i] || 0,\n payload: {\n content: results.documents?.[0]?.[i] || '',\n chunkType: results.metadatas?.[0]?.[i]?.chunkType,\n layer: results.metadatas?.[0]?.[i]?.layer,\n metadata: JSON.parse(results.metadatas?.[0]?.[i]?.metadata || '{}')\n }\n }))\n } catch (err) {\n console.error('[ChromaDB] Search failed:', err)\n return []\n }\n }\n\n async delete(ids: string[]): Promise\u003cvoid> {\n await this.initialize()\n \n if (!this.collection) return\n\n try {\n await this.collection.delete({ ids })\n } catch (err) {\n console.error('[ChromaDB] Delete failed:', err)\n }\n }\n\n async get(id: string): Promise\u003cKnowledgeChunk | null> {\n await this.initialize()\n \n if (!this.collection) return null\n\n try {\n const results = await this.collection.get({ ids: [id] })\n \n if (!results.ids || results.ids.length === 0) {\n return null\n }\n\n return {\n id: results.ids[0],\n content: results.documents?.[0] || '',\n chunkType: results.metadatas?.[0]?.chunkType || 'unknown',\n layer: (results.metadatas?.[0]?.layer || 1) as IndexLayer,\n metadata: JSON.parse(results.metadatas?.[0]?.metadata || '{}'),\n embedding: results.embeddings?.[0],\n createdAt: results.metadatas?.[0]?.createdAt || Date.now(),\n updatedAt: results.metadatas?.[0]?.updatedAt || Date.now()\n }\n } catch (err) {\n console.error('[ChromaDB] Get failed:', err)\n return null\n }\n }\n\n async count(): Promise\u003cnumber> {\n await this.initialize()\n \n if (!this.collection) return 0\n\n try {\n const results = await this.collection.count()\n return results\n } catch {\n return 0\n }\n }\n\n async clear(): Promise\u003cvoid> {\n await this.initialize()\n \n if (!this.collection) return\n\n try {\n await this.collection.delete({ where: {} })\n } catch (err) {\n console.error('[ChromaDB] Clear failed:', err)\n }\n }\n\n async close(): Promise\u003cvoid> {\n this.client = null\n this.collection = null\n this.isInitialized = false\n }\n\n isChromaDBAvailable(): boolean {\n return chromadbAvailable\n }\n\n async generateEmbedding(text: string, dimension?: number): Promise\u003cnumber[]> {\n const dim = dimension || this.config.embeddingDimension || 1536\n // Stub implementation - use OpenAI API or local model in production\n const hash = this.simpleHash(text)\n const embedding: number[] = []\n let seed = hash\n for (let i = 0; i \u003c dim; i++) {\n seed = (seed * 1103515245 + 12345) & 0x7fffffff\n embedding.push((seed % 1000) / 1000 - 0.5)\n }\n const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0))\n return embedding.map(v => v / (norm + 1e-10))\n }\n\n private simpleHash(str: string): number {\n let hash = 0\n for (let i = 0; i \u003c str.length; i++) {\n hash = ((hash \u003c\u003c 5) - hash) + str.charCodeAt(i)\n hash = hash & hash\n }\n return Math.abs(hash)\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":7710,"content_sha256":"ebdad1f915b278215f756e44e5ddcbbb5ba3bf5e398695387dfcec7ccc33da14"},{"filename":"src/vector-store/lancedb-adapter.ts","content":"/**\n * LanceDB Vector Store Adapter\n * \n * EO RAG 系统的向量存储适配器,基于 LanceDB\n * LanceDB 特点:\n * - 嵌入式(无需独立服务)\n * - 列式存储 + 向量索引\n * - 支持 Schema Evolution\n * - 高性能检索\n */\n\nimport type { \n VectorStoreConfig, \n VectorSearchResult, \n KnowledgeChunk,\n ChunkMetadata,\n IndexLayer\n} from '../types/index.js'\nimport { DEFAULT_RAG_CONFIG } from '../config.js'\n\n// LanceDB table schema constants\nconst SCHEMA_VERSION = '1.0'\nconst TABLE_NAME = 'eo_rag_knowledge'\n\n// Try to import LanceDB, with fallback\nlet LanceDB: any = null\nlet conn: any = null\nlet lancedbAvailable = false\n\nasync function tryInitLanceDB(config: VectorStoreConfig): Promise\u003cboolean> {\n try {\n // Dynamic import to handle optional dependency\n const lancedbModule = await import('vectordb').catch(() => null) ||\n await import('lancedb').catch(() => null)\n if (lancedbModule) {\n LanceDB = lancedbModule\n lancedbAvailable = true\n return true\n }\n } catch {\n // LanceDB not available, will use in-memory fallback\n }\n return false\n}\n\n/**\n * Schema definition for LanceDB tables\n */\ninterface LanceDBSchema {\n id: string\n content: string\n chunk_type: string\n layer: number\n metadata: string // JSON serialized\n embedding: number[]\n created_at: number\n updated_at: number\n}\n\n/**\n * In-memory fallback vector store when LanceDB is not available\n * Uses simple cosine similarity for demonstration\n */\nclass InMemoryVectorStore {\n private chunks: Map\u003cstring, {\n id: string\n content: string\n chunkType: string\n layer: number\n metadata: ChunkMetadata\n embedding: number[]\n createdAt: number\n updatedAt: number\n }> = new Map()\n\n private embeddings: Map\u003cstring, number[]> = new Map()\n\n async add(chunk: KnowledgeChunk): Promise\u003cvoid> {\n if (chunk.embedding) {\n this.embeddings.set(chunk.id, chunk.embedding)\n }\n this.chunks.set(chunk.id, {\n id: chunk.id,\n content: chunk.content,\n chunkType: chunk.chunkType,\n layer: chunk.layer,\n metadata: chunk.metadata,\n embedding: chunk.embedding || [],\n createdAt: chunk.createdAt,\n updatedAt: chunk.updatedAt\n })\n }\n\n async addBatch(chunks: KnowledgeChunk[]): Promise\u003cvoid> {\n for (const chunk of chunks) {\n await this.add(chunk)\n }\n }\n\n async search(queryEmbedding: number[], limit: number, filters?: Record\u003cstring, unknown>): Promise\u003cVectorSearchResult[]> {\n const results: Array\u003c{ id: string; score: number; payload: Record\u003cstring, unknown> }> = []\n\n for (const [id, chunk] of this.chunks.entries()) {\n // Apply filters\n if (filters) {\n if (filters.layer && chunk.layer !== filters.layer) continue\n if (filters.chunkType && chunk.chunkType !== filters.chunkType) continue\n }\n\n const embedding = this.embeddings.get(id)\n if (!embedding) continue\n\n const score = this.cosineSimilarity(queryEmbedding, embedding)\n results.push({\n id,\n score,\n payload: {\n id: chunk.id,\n content: chunk.content,\n chunkType: chunk.chunkType,\n layer: chunk.layer,\n metadata: chunk.metadata,\n createdAt: chunk.createdAt,\n updatedAt: chunk.updatedAt\n }\n })\n }\n\n // Sort by score descending\n results.sort((a, b) => b.score - a.score)\n return results.slice(0, limit)\n }\n\n async delete(ids: string[]): Promise\u003cvoid> {\n for (const id of ids) {\n this.chunks.delete(id)\n this.embeddings.delete(id)\n }\n }\n\n async get(id: string): Promise\u003cKnowledgeChunk | null> {\n const chunk = this.chunks.get(id)\n if (!chunk) return null\n return {\n id: chunk.id,\n content: chunk.content,\n chunkType: chunk.chunkType as any,\n layer: chunk.layer as IndexLayer,\n metadata: chunk.metadata,\n embedding: chunk.embedding,\n createdAt: chunk.createdAt,\n updatedAt: chunk.updatedAt\n }\n }\n\n async count(): Promise\u003cnumber> {\n return this.chunks.size\n }\n\n async clear(): Promise\u003cvoid> {\n this.chunks.clear()\n this.embeddings.clear()\n }\n\n private cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length) return 0\n let dotProduct = 0\n let normA = 0\n let normB = 0\n for (let i = 0; i \u003c a.length; i++) {\n dotProduct += a[i] * b[i]\n normA += a[i] * a[i]\n normB += b[i] * b[i]\n }\n return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-10)\n }\n}\n\n/**\n * LanceDB Vector Store Adapter\n * Provides a unified interface for vector storage operations\n */\nexport class LanceDBAdapter {\n private config: VectorStoreConfig\n private table: any = null\n private inMemoryStore: InMemoryVectorStore\n private isInitialized: boolean = false\n private persistPath: string\n\n constructor(config?: Partial\u003cVectorStoreConfig>) {\n this.config = {\n ...DEFAULT_RAG_CONFIG.vectorStore,\n ...config\n }\n this.persistPath = this.config.persistPath || './data/rag/lancedb'\n this.inMemoryStore = new InMemoryVectorStore()\n }\n\n /**\n * Initialize the LanceDB connection and table\n */\n async initialize(): Promise\u003cvoid> {\n if (this.isInitialized) return\n\n const lancedbLoaded = await tryInitLanceDB(this.config)\n \n if (lancedbLoaded && LanceDB) {\n try {\n // Try to connect to LanceDB\n conn = await LanceDB.connect(this.persistPath)\n \n // Try to open existing table or create new one\n try {\n this.table = await conn.openTable(TABLE_NAME)\n } catch {\n // Table doesn't exist, create it\n await this.createTable()\n }\n \n this.isInitialized = true\n console.log('[LanceDB] Initialized with persistent storage:', this.persistPath)\n } catch (err) {\n console.warn('[LanceDB] Failed to connect with persistent storage, using in-memory:', err)\n this.isInitialized = true\n }\n } else {\n console.warn('[LanceDB] LanceDB not available, using in-memory fallback store')\n this.isInitialized = true\n }\n }\n\n /**\n * Create a new LanceDB table with the EO RAG schema\n */\n private async createTable(): Promise\u003cvoid> {\n if (!conn) return\n\n try {\n // Define schema based on what LanceDB JS supports\n const schema = {\n id: new LanceDB.Text(),\n content: new LanceDB.Text(),\n chunk_type: new LanceDB.Text(),\n layer: new LanceDB.Int32(),\n metadata: new LanceDB.JSON(),\n embedding: new LanceDB.Vector(new LanceDB.Float32(), this.config.embeddingDimension || 1536),\n created_at: new LanceDB.Timestamp(),\n updated_at: new LanceDB.Timestamp()\n }\n\n await conn.createTable(TABLE_NAME, schema)\n this.table = await conn.openTable(TABLE_NAME)\n } catch (err) {\n console.error('[LanceDB] Failed to create table:', err)\n throw err\n }\n }\n\n /**\n * Add a single knowledge chunk to the vector store\n */\n async add(chunk: KnowledgeChunk): Promise\u003cvoid> {\n await this.initialize()\n \n if (this.table) {\n await this.addToLanceDB(chunk)\n } else {\n await this.inMemoryStore.add(chunk)\n }\n }\n\n /**\n * Add multiple chunks in batch for efficiency\n */\n async addBatch(chunks: KnowledgeChunk[]): Promise\u003cvoid> {\n await this.initialize()\n \n if (this.table && !this.useInMemory) {\n await this.addBatchToLanceDB(chunks)\n } else {\n await this.inMemoryStore.addBatch(chunks)\n }\n }\n\n private get useInMemory(): boolean {\n return this.table === null\n }\n\n private async addToLanceDB(chunk: KnowledgeChunk): Promise\u003cvoid> {\n if (!this.table) return\n\n const row = {\n id: chunk.id,\n content: chunk.content,\n chunk_type: chunk.chunkType,\n layer: chunk.layer,\n metadata: JSON.stringify(chunk.metadata),\n embedding: chunk.embedding || new Array(this.config.embeddingDimension || 1536).fill(0),\n created_at: new Date(chunk.createdAt),\n updated_at: new Date(chunk.updatedAt)\n }\n\n await this.table.add([row])\n }\n\n private async addBatchToLanceDB(chunks: KnowledgeChunk[]): Promise\u003cvoid> {\n if (!this.table) return\n\n const rows = chunks.map(chunk => ({\n id: chunk.id,\n content: chunk.content,\n chunk_type: chunk.chunkType,\n layer: chunk.layer,\n metadata: JSON.stringify(chunk.metadata),\n embedding: chunk.embedding || new Array(this.config.embeddingDimension || 1536).fill(0),\n created_at: new Date(chunk.createdAt),\n updated_at: new Date(chunk.updatedAt)\n }))\n\n await this.table.add(rows)\n }\n\n /**\n * Search for similar chunks using vector similarity\n */\n async search(\n queryEmbedding: number[],\n limit: number,\n filters?: {\n layer?: IndexLayer\n chunkType?: string\n metadata?: Record\u003cstring, unknown>\n }\n ): Promise\u003cVectorSearchResult[]> {\n await this.initialize()\n\n if (this.table) {\n return this.searchLanceDB(queryEmbedding, limit, filters)\n } else {\n return this.inMemoryStore.search(queryEmbedding, limit, filters as Record\u003cstring, unknown>)\n }\n }\n\n private async searchLanceDB(\n queryEmbedding: number[],\n limit: number,\n filters?: {\n layer?: IndexLayer\n chunkType?: string\n metadata?: Record\u003cstring, unknown>\n }\n ): Promise\u003cVectorSearchResult[]> {\n if (!this.table) return []\n\n try {\n // Build query with optional filters\n let query = this.table.vectorSearch(queryEmbedding)\n \n // Apply filters if specified\n if (filters?.layer !== undefined) {\n query = query.where('layer', filters.layer)\n }\n if (filters?.chunkType) {\n query = query.where('chunk_type', filters.chunkType)\n }\n\n const results = await query.limit(limit).execute()\n \n return results.map((row: any) => ({\n id: row.id,\n score: row._distance || 0,\n payload: {\n id: row.id,\n content: row.content,\n chunkType: row.chunk_type,\n layer: row.layer,\n metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,\n createdAt: row.created_at?.getTime?.() || row.created_at,\n updatedAt: row.updated_at?.getTime?.() || row.updated_at\n }\n }))\n } catch (err) {\n console.error('[LanceDB] Search failed, falling back to in-memory:', err)\n return this.inMemoryStore.search(queryEmbedding, limit, filters as Record\u003cstring, unknown>)\n }\n }\n\n /**\n * Delete chunks by IDs\n */\n async delete(ids: string[]): Promise\u003cvoid> {\n await this.initialize()\n\n if (this.table) {\n try {\n await this.table.delete(`id IN (${ids.map(id => `\"${id}\"`).join(',')})`)\n } catch (err) {\n console.error('[LanceDB] Delete failed:', err)\n }\n }\n \n await this.inMemoryStore.delete(ids)\n }\n\n /**\n * Get a chunk by ID\n */\n async get(id: string): Promise\u003cKnowledgeChunk | null> {\n await this.initialize()\n\n if (this.table) {\n try {\n const results = await this.table.query()\n .where('id', id)\n .limit(1)\n .execute()\n \n if (results.length > 0) {\n const row = results[0]\n return {\n id: row.id,\n content: row.content,\n chunkType: row.chunk_type as any,\n layer: row.layer as IndexLayer,\n metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,\n embedding: row.embedding,\n createdAt: row.created_at?.getTime?.() || row.created_at,\n updatedAt: row.updated_at?.getTime?.() || row.updated_at\n }\n }\n } catch {\n // Fallback to in-memory\n }\n }\n\n return this.inMemoryStore.get(id)\n }\n\n /**\n * Get total count of chunks\n */\n async count(): Promise\u003cnumber> {\n await this.initialize()\n\n if (this.table) {\n try {\n const results = await this.table.query().select(['id']).execute()\n return results.length\n } catch {\n // Fallback\n }\n }\n \n return this.inMemoryStore.count()\n }\n\n /**\n * Get statistics about the vector store\n */\n async getStats(): Promise\u003c{\n totalChunks: number\n byLayer: Record\u003cnumber, number>\n byType: Record\u003cstring, number>\n lastUpdated: number | null\n }> {\n await this.initialize()\n\n const stats = {\n totalChunks: await this.count(),\n byLayer: {} as Record\u003cnumber, number>,\n byType: {} as Record\u003cstring, number>,\n lastUpdated: null as number | null\n }\n\n // This is a simplified implementation\n // In production, you'd query LanceDB directly for these aggregates\n return stats\n }\n\n /**\n * Clear all data from the vector store\n */\n async clear(): Promise\u003cvoid> {\n await this.initialize()\n\n if (this.table) {\n try {\n await this.table.delete('true') // Delete all\n } catch (err) {\n console.error('[LanceDB] Clear failed:', err)\n }\n }\n \n await this.inMemoryStore.clear()\n }\n\n /**\n * Close the connection\n */\n async close(): Promise\u003cvoid> {\n if (conn) {\n try {\n await conn.close()\n } catch {\n // Ignore close errors\n }\n }\n conn = null\n this.table = null\n this.isInitialized = false\n }\n\n /**\n * Check if LanceDB is available\n */\n isLanceDBAvailable(): boolean {\n return lancedbAvailable\n }\n\n /**\n * Generate embedding for text content\n * This is a placeholder - in production, use OpenAI API or local model\n */\n async generateEmbedding(text: string, dimension?: number): Promise\u003cnumber[]> {\n const dim = dimension || this.config.embeddingDimension || 1536\n \n // Simple hash-based pseudo-embedding for demo\n // In production, replace with actual embedding API call\n const hash = this.simpleHash(text)\n const embedding: number[] = []\n \n // Use hash to seed pseudo-random but deterministic values\n let seed = hash\n for (let i = 0; i \u003c dim; i++) {\n seed = (seed * 1103515245 + 12345) & 0x7fffffff\n embedding.push((seed % 1000) / 1000 - 0.5) // Normalize to [-0.5, 0.5]\n }\n \n // Normalize\n const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0))\n return embedding.map(v => v / (norm + 1e-10))\n }\n\n private simpleHash(str: string): number {\n let hash = 0\n for (let i = 0; i \u003c str.length; i++) {\n const char = str.charCodeAt(i)\n hash = ((hash \u003c\u003c 5) - hash) + char\n hash = hash & hash // Convert to 32-bit integer\n }\n return Math.abs(hash)\n }\n}\n\n// Singleton instance\nlet defaultAdapter: LanceDBAdapter | null = null\n\nexport function getLanceDBAdapter(config?: Partial\u003cVectorStoreConfig>): LanceDBAdapter {\n if (!defaultAdapter) {\n defaultAdapter = new LanceDBAdapter(config)\n }\n return defaultAdapter\n}\n\nexport function resetLanceDBAdapter(): void {\n if (defaultAdapter) {\n defaultAdapter.close()\n defaultAdapter = null\n }\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":15030,"content_sha256":"68cf1e05d4d176493043ac084ce185904e657805111556475bbd682620e69972"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"EO RAG + Knowledge Sharing System","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"让 141 位专家形成\"知识共同体\" - 自动沉淀经验、智能检索、上下文注入","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"EO RAG 系统为 Everything OpenClaw 框架添加了向量检索 + 知识共享能力,使多专家协作产生的经验能够被自动沉淀和复用。","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"执行任务 ──▶ ETP提取经验 ──▶ 向量索引 ──▶ RAG检索 ──▶ 新任务上下文注入\n │\n ▼\n 知识图谱关联分析","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Architecture","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌─────────────────────────────────────────────────────────────────┐\n│ EO RAG + Knowledge Layer │\n├─────────────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ Memory │ │ Expert │ │ Pattern │ │\n│ │ Manager │ │ Library │ │ Library │ │\n│ │ (会话记忆) │ │ (141专家) │ │ (模式库) │ │\n│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │\n│ │ │ │ │\n│ ▼ ▼ ▼ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ 5-Layer Knowledge Index │ │\n│ │ L1: Expert | L2: Pattern | L3: Checkpoint │ │\n│ │ L4: Decision | L5: Session │ │\n│ └─────────────────────────┬───────────────────────────┘ │\n│ │ │\n│ ▼ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ LanceDB Vector Store │ │\n│ │ (混合检索: 向量 + BM25 + RRF融合) │ │\n│ └─────────────────────────┬───────────────────────────┘ │\n│ │ │\n│ ▼ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ Retrieval Router │ │\n│ │ (expert | pattern | checkpoint | history | hybrid) │ │\n│ └─────────────────────────┬───────────────────────────┘ │\n│ │ │\n│ ▼ │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ Context Injector │ │\n│ │ (System Prompt 自动注入) │ │\n│ └─────────────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"5-Layer Knowledge Index","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Layer","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Name","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Chunk Strategy","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L1","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Expert Profiles","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"expert-library/*.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One per expert file","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L2","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pattern Instances","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"docs/PATTERNS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"patterns/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Section-based","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L3","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Checkpoint Results","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Runtime: ETP Protocol","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One per checkpoint","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L4","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cross-Expert Decisions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Runtime: ETP Protocol","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One per decision","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L5","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Session Memory","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEMORY.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"memory/*.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One per session","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Directory Structure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"skills/rag/\n├── SKILL.md # 本文档\n├── src/\n│ ├── index.ts # 主入口 + EORAGSystem\n│ ├── config.ts # 配置文件\n│ ├── types/\n│ │ └── index.ts # TypeScript 类型定义\n│ ├── vector-store/\n│ │ ├── lancedb-adapter.ts # LanceDB 适配器\n│ │ └── chromadb-adapter.ts # ChromaDB 适配器(备选)\n│ ├── indexers/\n│ │ ├── expert-indexer.ts # L1: 专家索引\n│ │ ├── pattern-indexer.ts # L2: 模式索引\n│ │ ├── session-indexer.ts # L5: 会话索引\n│ │ └── etp-indexer.ts # L3/L4: ETP索引\n│ ├── retrieval/\n│ │ ├── router.ts # 查询路由\n│ │ ├── hybrid-search.ts # 混合检索 (向量+BM25+RRF)\n│ │ └── reranker.ts # 重排序\n│ └── knowledge/\n│ ├── graph-engine.ts # 知识图谱引擎\n│ ├── etp-protocol.ts # ETP 经验传递协议\n│ └── conflict-resolver.ts # 冲突解决\n└── context-injector.ts # 上下文注入器","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Initialize the RAG System","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import { EORAGSystem, getRAGSystem } from './skills/rag/src/index.js'\n\nconst rag = new EORAGSystem({\n enabled: true,\n vectorStore: {\n type: 'lancedb',\n persistPath: './data/rag/lancedb'\n },\n retrieval: {\n defaultTopK: 5,\n enableReranking: true\n }\n})\n\nawait rag.initialize()","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Index Knowledge","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// Full reindex\nconst results = await rag.indexAll()\n\n// Index specific layers\nawait rag.indexExperts() // L1\nawait rag.indexPatterns() // L2\nawait rag.indexSessions() // L5","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Search","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// Simple search\nconst results = await rag.quickSearch('需要一个后端专家处理高并发API')\n\n// Advanced search with routing\nconst results = await rag.search({\n query: '小程序开发用什么模式好',\n topK: 5,\n strategy: 'pattern_retrieval'\n})","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Context Injection","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// Inject relevant knowledge into system prompt\nconst injection = await rag.injectContext({\n query: '我想开发一个小程序商城',\n taskType: 'miniprogram',\n topK: 3\n})\n\nconst systemPrompt = await rag.buildSystemPrompt(\n basePrompt, // Original system prompt\n { query: '我想开发一个小程序商城', taskType: 'miniprogram' }\n)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. ETP Protocol (Experience Transfer)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// Create ETP record from task completion\nconst etpRecord = rag.createETP({\n taskId: 'task-123',\n taskType: 'miniprogram',\n taskDescription: '开发小程序商城',\n participatingExperts: ['@expert-backend', '@expert-frontend'],\n durationMs: 7200000,\n outcome: 'success',\n keyDecisions: [{\n point: '选择 uni-app 框架',\n alternatives: ['原生开发', 'Taro', 'uni-app'],\n chosen: 'uni-app',\n rationale: '跨平台能力强,开发效率高'\n }],\n lessons: [\n '支付模块需要单独封装',\n '图片懒加载提升性能明显'\n ],\n checkpointsPassed: ['cp-ui-design', 'cp-api-spec', 'cp-testing'],\n checkpointsFailed: []\n})\n\n// Record checkpoint failure\nrag.recordCheckpointFailure('task-123', 'cp-security', '未进行XSS过滤')","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Retrieval Strategies","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Strategy","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Trigger Keywords","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Layers","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use Case","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"expert_retrieval","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"专家、expert、谁来做","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"匹配最佳专家","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pattern_retrieval","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"模式、pattern、怎么开发","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查找开发模式","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"checkpoint_retrieval","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"验证、测试、质量","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查找验证规则","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"decision_retrieval","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"决策、架构、选择","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查找决策记录","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"history_retrieval","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"之前、类似、历史","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L5+L4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查找项目经验","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"hybrid_retrieval","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"(default)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L1-L5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"全面检索","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Configuration","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Vector Store","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"const config = {\n vectorStore: {\n type: 'lancedb', // or 'chromadb'\n persistPath: './data/rag/lancedb',\n embeddingDimension: 1536\n },\n embedding: {\n model: 'text-embedding-3-small',\n provider: 'openai' // or 'local' for BGE-M3\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Retrieval","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"const config = {\n retrieval: {\n defaultTopK: 5,\n rerankTopK: 10,\n hybridAlpha: 0.6, // Weight for dense retrieval\n enableReranking: true\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Conflict Resolution","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"const config = {\n conflictResolution: {\n recency: 0.4, // Newer records weigh more\n successRate: 0.3, // Higher success rate weighs more\n expertAuthority: 0.2, // Authority role weighs more\n contextSimilarity: 0.1\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Integration with EO Framework","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hook: After Team Completion","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// In team-manager.ts, after team completes:\nimport { getRAGSystem } from '../skills/rag/src/index.js'\n\nasync function onTeamComplete(team) {\n const rag = getRAGSystem()\n \n // Create ETP record\n rag.createETP({\n taskId: team.id,\n taskType: team.taskType,\n taskDescription: team.description,\n participatingExperts: team.members.map(m => m.id),\n durationMs: Date.now() - team.createdAt,\n outcome: team.status === 'completed' ? 'success' : 'partial',\n // ... decisions, lessons, etc.\n })\n \n // Index the ETP record\n await rag.indexETP(rag.getETPRecord(team.id)!)\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hook: After Checkpoint Failure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// In verify-skill.ts, after checkpoint fails:\nimport { getRAGSystem } from '../skills/rag/src/index.js'\n\nasync function onCheckpointFail(checkpoint, analysis) {\n const rag = getRAGSystem()\n rag.recordCheckpointFailure(\n checkpoint.taskId,\n checkpoint.name,\n analysis\n )\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Integration with Self-Learning System","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// ETP records complement self-learning feedback:\nimport { SelfLearningOrchestrator } from '../self-learning/src/orchestrator.js'\n\nconst feedback = orchestrator.submitFeedback(taskId, { ... })\n\n// Create ETP from self-learning feedback\nrag.createETP({\n taskId: taskId,\n taskType: feedback.context.taskType,\n // ...\n keyDecisions: feedback.decisions || [],\n lessons: feedback.lessons || []\n})","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Knowledge Graph Relationships","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────────────┐\n│ Knowledge Graph │\n│ │\n│ [Expert] ──(uses)──▶ [Pattern] │\n│ │ │\n│ │ │\n│ └──(participated_in)──▶ [Task/ETP] ◀──(has)── [Decision] │\n│ │ │\n│ ▼ │\n│ [Checkpoint] │\n│ │\n│ [Session] ◀────(related_to)──── [Session] │\n│ │\n└──────────────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Conflict Resolution","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Level","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolution Strategy","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L1","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool/Parameter","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-resolve by recency + success rate","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L2","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Architecture/Design","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Escalate to Architect","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L3","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Priority/Direction","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Escalate to Planner","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Statistics","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"const stats = await rag.getStats()\n\nconsole.log(`\nEnabled: ${stats.enabled}\nVector Store: ${stats.vectorStore.type}\nTotal Chunks: ${stats.vectorStore.totalChunks}\n L1 Experts: ${stats.vectorStore.byLayer[1] || 0}\n L2 Patterns: ${stats.vectorStore.byLayer[2] || 0}\n L3 Checkpoints: ${stats.vectorStore.byLayer[3] || 0}\n L4 Decisions: ${stats.vectorStore.byLayer[4] || 0}\n L5 Sessions: ${stats.vectorStore.byLayer[5] || 0}\n\nETP Records: ${stats.knowledge.etpRecords}\nGraph Nodes: ${stats.knowledge.graphNodes}\nGraph Edges: ${stats.knowledge.graphEdges}\n\nRetrieval Queries: ${stats.retrieval.totalQueries}\nAvg Query Time: ${stats.retrieval.avgQueryTimeMs}ms\n`)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"API Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"EORAGSystem","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Method","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"initialize()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Initialize the RAG system","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"search(query)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Search the knowledge base","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"quickSearch(query, topK?)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Simple search with defaults","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"injectContext(request)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inject relevant knowledge","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"buildSystemPrompt(base, request)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Build prompt with knowledge","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"indexAll()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full reindex of all layers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"createETP(record)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create ETP record","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"getETPRecord(taskId)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get ETP by task ID","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"getStats()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Get system statistics","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"save()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Persist all data","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Indexers","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Class","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Layer","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ExpertIndexer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Index expert library profiles","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PatternIndexer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Index pattern instances","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SessionIndexer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Index session memory","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ETPIndexer","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"L3/L4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Index checkpoints & decisions","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Retrieval","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Class","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RetrievalRouter","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Route queries to strategies","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HybridSearchEngine","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vector + BM25 + RRF","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RulesBasedReranker","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rule-based reranking","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CrossEncoderReranker","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ML-based reranking","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Dependencies","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"dependencies\": {\n \"vectordb\": \"^0.4.0\", // LanceDB\n \"chroma-js\": \"^1.5.0\" // ChromaDB (optional fallback)\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Documents","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"RAG Architecture Design","type":"text","marks":[{"type":"link","attrs":{"href":"../../docs/RAG_KNOWLEDGE_SHARING_ARCHITECTURE.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"EO Multi-Expert System","type":"text","marks":[{"type":"link","attrs":{"href":"../../docs/MULTI_EXPERT.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pattern Library","type":"text","marks":[{"type":"link","attrs":{"href":"../../docs/PATTERNS.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Self-Learning System","type":"text","marks":[{"type":"link","attrs":{"href":"../self-learning/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Expert-Library","type":"text","marks":[{"type":"link","attrs":{"href":"../../expert-library/OPTIMIZATION.md","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","author":"@skillopedia","source":{"stars":4,"repo_name":"everything-openclaw","origin_url":"https://github.com/467718584/everything-openclaw/blob/HEAD/skills/rag/SKILL.md","repo_owner":"467718584","body_sha256":"df5e8cc7bb7dfe945cde39427d6a38c053ccc5d2d8b43bbafb7e96c7b009ce0c","cluster_key":"8d1f2ce5f10ef27f8b313eed01072013756927d0608e075a4894607f25f617a3","clean_bundle":{"format":"clean-skill-bundle-v1","source":"467718584/everything-openclaw/skills/rag/SKILL.md","attachments":[{"id":"54835e3e-e2a9-5854-bc73-c33b9daba6e9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/54835e3e-e2a9-5854-bc73-c33b9daba6e9/attachment.ts","path":"src/config.ts","size":5186,"sha256":"83664a3632c2923fc830159b8a821059fde26af38839390b67eed260881b5526","contentType":"text/typescript; charset=utf-8"},{"id":"064a270b-33f7-55df-8ed5-b2514e2388bc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/064a270b-33f7-55df-8ed5-b2514e2388bc/attachment.ts","path":"src/context-injector.ts","size":10764,"sha256":"d8c24007f0b4c0d342a275dd0aef0960e19dd6bd1703c423e3a215c02728633b","contentType":"text/typescript; charset=utf-8"},{"id":"33edae29-6087-5a26-9c9d-ea782d037d5d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/33edae29-6087-5a26-9c9d-ea782d037d5d/attachment.ts","path":"src/index.ts","size":14010,"sha256":"e20d2f89d5566ff2fae5ed65e5bedf16f4abad41c26273e6b59938c0018b6ff2","contentType":"text/typescript; charset=utf-8"},{"id":"195b43c2-8817-5731-84b4-a1c6c61f3ee8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/195b43c2-8817-5731-84b4-a1c6c61f3ee8/attachment.ts","path":"src/indexers/etp-indexer.ts","size":11585,"sha256":"27a1d1d2e1600d7fee24e82bef5ffb79e99f420993903f7d6d135067ac00a9a9","contentType":"text/typescript; charset=utf-8"},{"id":"8a022a8f-fdfb-560d-851c-d1cae3c71002","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8a022a8f-fdfb-560d-851c-d1cae3c71002/attachment.ts","path":"src/indexers/expert-indexer.ts","size":9766,"sha256":"aa2e1086467638cfefd2ba7398390b4f354b099b3a3d628354bf871da9b4db2e","contentType":"text/typescript; charset=utf-8"},{"id":"65c46207-55cd-578f-ab4c-613de1fbfdea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/65c46207-55cd-578f-ab4c-613de1fbfdea/attachment.ts","path":"src/indexers/pattern-indexer.ts","size":11690,"sha256":"4f9d418c62fdd39a68388668b9cf57269040a0a7503b6d94e353f7686161aa94","contentType":"text/typescript; charset=utf-8"},{"id":"0f649508-9457-5fbb-8907-77daa9d2c889","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0f649508-9457-5fbb-8907-77daa9d2c889/attachment.ts","path":"src/indexers/session-indexer.ts","size":11920,"sha256":"e32f4374ac78e9dd5442ea23cf7f1ef14e2565507bcbcc0aa40a4d54001a9151","contentType":"text/typescript; charset=utf-8"},{"id":"5465bcac-5503-5146-822a-c68e91299f4d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5465bcac-5503-5146-822a-c68e91299f4d/attachment.ts","path":"src/knowledge/conflict-resolver.ts","size":11195,"sha256":"eed700dd2faf1ebbbceb42768d3e67b41479cdf92dab904e65289c5120fa855b","contentType":"text/typescript; charset=utf-8"},{"id":"d32e689c-a3f9-5287-b822-b8f5dba4ed55","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d32e689c-a3f9-5287-b822-b8f5dba4ed55/attachment.ts","path":"src/knowledge/etp-protocol.ts","size":14978,"sha256":"6eff9b8b789b3744fb6341915ab31c488bf987f930c949d5e3d41480e64ad30d","contentType":"text/typescript; charset=utf-8"},{"id":"bb276615-6213-5191-9ad8-cb49bc9b2fc6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bb276615-6213-5191-9ad8-cb49bc9b2fc6/attachment.ts","path":"src/knowledge/graph-engine.ts","size":11722,"sha256":"b8f604586f2a914a1a33591b305fc2c0a48ee3e0c62df699450d93833b125f60","contentType":"text/typescript; charset=utf-8"},{"id":"1c31a115-a716-59ea-843e-c4b4665ab00e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c31a115-a716-59ea-843e-c4b4665ab00e/attachment.ts","path":"src/retrieval/hybrid-search.ts","size":10995,"sha256":"a6cb081df686474895b8182021c31cf1769d2f039b2b4cc9f56d6a1f3c57250a","contentType":"text/typescript; charset=utf-8"},{"id":"13b58b8d-348a-59fb-8599-1c6355739bbe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/13b58b8d-348a-59fb-8599-1c6355739bbe/attachment.ts","path":"src/retrieval/reranker.ts","size":7390,"sha256":"0269b54555431c6bd0ff9895b9dddee7b644696f4a96746c1342d51a3f6cdcce","contentType":"text/typescript; charset=utf-8"},{"id":"9aad8dbc-e4cb-533e-958d-a4e31b8293d0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9aad8dbc-e4cb-533e-958d-a4e31b8293d0/attachment.ts","path":"src/retrieval/router.ts","size":5913,"sha256":"ffb35018ce3918475cf06b9da02800d4b880398c80921f9901d264eada597aea","contentType":"text/typescript; charset=utf-8"},{"id":"3eb7ec8f-a135-55af-a795-e75e5f96835d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3eb7ec8f-a135-55af-a795-e75e5f96835d/attachment.ts","path":"src/types/index.ts","size":10854,"sha256":"0fd98a2b7805e72aaef9c22f1f85c9b922226a18417ca59747b3b2057b322e5d","contentType":"text/typescript; charset=utf-8"},{"id":"3c5d055b-628c-5d94-a73b-b3ddd6cb9d03","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3c5d055b-628c-5d94-a73b-b3ddd6cb9d03/attachment.ts","path":"src/vector-store/chromadb-adapter.ts","size":7710,"sha256":"ebdad1f915b278215f756e44e5ddcbbb5ba3bf5e398695387dfcec7ccc33da14","contentType":"text/typescript; charset=utf-8"},{"id":"b7e6cfa6-cba7-5e36-8147-828d27f7c006","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b7e6cfa6-cba7-5e36-8147-828d27f7c006/attachment.ts","path":"src/vector-store/lancedb-adapter.ts","size":15030,"sha256":"68cf1e05d4d176493043ac084ce185904e657805111556475bbd682620e69972","contentType":"text/typescript; charset=utf-8"}],"bundle_sha256":"ff5aaf895a023104d5609544be6c66c5bf75e4e59d1cbe67b68cd66908b07e3c","attachment_count":16,"text_attachments":16,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/rag/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"general","import_tag":"clean-skills-v1"}},"renderedAt":1782986696203}

EO RAG + Knowledge Sharing System 让 141 位专家形成"知识共同体" - 自动沉淀经验、智能检索、上下文注入 Overview EO RAG 系统为 Everything OpenClaw 框架添加了向量检索 + 知识共享能力,使多专家协作产生的经验能够被自动沉淀和复用。 Architecture 5-Layer Knowledge Index | Layer | Name | Source | Chunk Strategy | |-------|------|--------|----------------| | L1 | Expert Profiles | | One per expert file | | L2 | Pattern Instances | + | Section-based | | L3 | Checkpoint Results | Runtime: ETP Protocol | One per checkpoint | | L4 | Cross-Expert Decisions | Runtime: ETP Protocol | One per decision | | L5 | Session Memory | + | One per session | Directory Structure Quick Start…