claude-brain 0.30.2 → 0.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-Write Manager — Task 3.5
|
|
3
|
+
*
|
|
4
|
+
* Extracts the repeated FTS5 → ChromaDB → SQLite fallback pattern
|
|
5
|
+
* from MemoryManager into a single reusable class.
|
|
6
|
+
*
|
|
7
|
+
* The fallback chain for every operation:
|
|
8
|
+
* 1. FTS5 (primary, always-available SQLite-based)
|
|
9
|
+
* 2. ChromaDB (secondary, vector store)
|
|
10
|
+
* 3. Legacy SQLite MemoryStore (final fallback)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Logger } from 'pino'
|
|
14
|
+
import type { FTS5Search } from './fts5-search'
|
|
15
|
+
import type { ChromaManager } from './chroma'
|
|
16
|
+
import type { EmbeddingService } from './embeddings'
|
|
17
|
+
|
|
18
|
+
export class DualWriteManager {
|
|
19
|
+
constructor(
|
|
20
|
+
private logger: Logger,
|
|
21
|
+
private getFts5: () => FTS5Search | null,
|
|
22
|
+
private getChroma: () => { manager: ChromaManager; enabled: boolean },
|
|
23
|
+
private getEmbeddings: () => EmbeddingService
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Store to FTS5 → ChromaDB → SQLite with fallback chain.
|
|
28
|
+
* Returns the ID from whichever backend succeeded first.
|
|
29
|
+
*/
|
|
30
|
+
async store(opts: {
|
|
31
|
+
sharedId: string
|
|
32
|
+
fts5Fn: (fts5: FTS5Search, id: string) => string | undefined
|
|
33
|
+
chromaFn?: (chroma: ChromaManager, id: string) => Promise<string>
|
|
34
|
+
sqliteFn?: () => Promise<string>
|
|
35
|
+
embeddingText?: string
|
|
36
|
+
}): Promise<string> {
|
|
37
|
+
let fts5Id: string | undefined
|
|
38
|
+
|
|
39
|
+
// Step 1: FTS5
|
|
40
|
+
const fts5 = this.getFts5()
|
|
41
|
+
if (fts5) {
|
|
42
|
+
try {
|
|
43
|
+
fts5Id = opts.fts5Fn(fts5, opts.sharedId)
|
|
44
|
+
|
|
45
|
+
// Fire-and-forget embedding
|
|
46
|
+
if (fts5Id && opts.embeddingText) {
|
|
47
|
+
this.storeEmbeddingAsync(fts5Id, opts.embeddingText)
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.logger.warn({ error }, 'FTS5 store failed, continuing with other backends')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 2: ChromaDB
|
|
55
|
+
const { manager: chroma, enabled } = this.getChroma()
|
|
56
|
+
if (enabled && opts.chromaFn) {
|
|
57
|
+
try {
|
|
58
|
+
const chromaId = await opts.chromaFn(chroma, opts.sharedId)
|
|
59
|
+
return fts5Id || chromaId
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.logger.warn({ error }, 'ChromaDB store failed')
|
|
62
|
+
if (fts5Id) return fts5Id
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 3: SQLite fallback
|
|
67
|
+
if (!fts5Id && opts.sqliteFn) {
|
|
68
|
+
return await opts.sqliteFn()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return fts5Id!
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch from FTS5 → ChromaDB → SQLite with fallback chain.
|
|
76
|
+
* The fts5Fn/chromaFn/sqliteFn each return mapped results in the
|
|
77
|
+
* caller's desired shape, keeping transformation logic in MemoryManager.
|
|
78
|
+
*/
|
|
79
|
+
async fetch<T>(opts: {
|
|
80
|
+
fts5Fn?: (fts5: FTS5Search) => T[]
|
|
81
|
+
chromaFn?: (chroma: ChromaManager) => Promise<T[]>
|
|
82
|
+
sqliteFn: () => T[] | Promise<T[]>
|
|
83
|
+
}): Promise<T[]> {
|
|
84
|
+
// Try FTS5
|
|
85
|
+
const fts5 = this.getFts5()
|
|
86
|
+
if (fts5 && opts.fts5Fn) {
|
|
87
|
+
const results = opts.fts5Fn(fts5)
|
|
88
|
+
if (results.length > 0) return results
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try ChromaDB
|
|
92
|
+
const { manager: chroma, enabled } = this.getChroma()
|
|
93
|
+
if (enabled && opts.chromaFn) {
|
|
94
|
+
try {
|
|
95
|
+
return await opts.chromaFn(chroma)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
this.logger.warn({ error }, 'ChromaDB fetch failed, falling back to SQLite')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// SQLite fallback
|
|
102
|
+
return await opts.sqliteFn()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Delete from all backends: FTS5, then ChromaDB or SQLite.
|
|
107
|
+
*/
|
|
108
|
+
async delete(id: string, opts: {
|
|
109
|
+
fts5Fn?: (fts5: FTS5Search) => void
|
|
110
|
+
chromaFn?: (chroma: ChromaManager) => Promise<void>
|
|
111
|
+
sqliteFn?: () => void
|
|
112
|
+
}): Promise<void> {
|
|
113
|
+
const fts5 = this.getFts5()
|
|
114
|
+
if (fts5 && opts.fts5Fn) {
|
|
115
|
+
try {
|
|
116
|
+
opts.fts5Fn(fts5)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.logger.warn({ error, id }, 'FTS5 delete failed')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { manager: chroma, enabled } = this.getChroma()
|
|
123
|
+
if (enabled && opts.chromaFn) {
|
|
124
|
+
await opts.chromaFn(chroma)
|
|
125
|
+
} else if (opts.sqliteFn) {
|
|
126
|
+
opts.sqliteFn()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fire-and-forget embedding generation and storage.
|
|
132
|
+
*/
|
|
133
|
+
private storeEmbeddingAsync(id: string, text: string): void {
|
|
134
|
+
const embeddings = this.getEmbeddings()
|
|
135
|
+
if (!embeddings.isReady()) return
|
|
136
|
+
const fts5 = this.getFts5()
|
|
137
|
+
if (!fts5) return
|
|
138
|
+
|
|
139
|
+
embeddings.generateEmbedding(text).then(embedding => {
|
|
140
|
+
fts5.storeEmbedding(id, embedding)
|
|
141
|
+
}).catch(error => {
|
|
142
|
+
this.logger.debug({ error, id }, 'Failed to store observation embedding')
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
package/src/memory/embeddings.ts
CHANGED
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Embedding Generation Service
|
|
3
|
-
* Phase 3: Memory and Embedding System
|
|
4
|
-
*
|
|
5
|
-
* Uses transformers.js for local embedding generation
|
|
6
|
-
* Model: all-MiniLM-L6-v2 (384 dimensions)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { pipeline, env } from '@xenova/transformers'
|
|
10
|
-
import { createHash } from 'crypto'
|
|
11
|
-
import type { Logger } from 'pino'
|
|
12
|
-
import type { EmbeddingCacheStats } from './types'
|
|
13
|
-
import { cosineSimilarity } from './embedding-utils'
|
|
14
|
-
import { CircuitBreaker } from '@/utils'
|
|
15
|
-
|
|
16
|
-
// Configure transformers.js for Bun compatibility
|
|
17
|
-
env.allowLocalModels = true
|
|
18
|
-
env.allowRemoteModels = false
|
|
19
|
-
env.useBrowserCache = false
|
|
20
|
-
// Disable web workers to avoid ONNX blob URL issues in Bun
|
|
21
|
-
env.backends.onnx.wasm.numThreads = 1
|
|
22
|
-
env.backends.onnx.wasm.simd = true
|
|
23
|
-
|
|
24
|
-
type FeatureExtractionPipeline = Awaited<ReturnType<typeof pipeline>>
|
|
25
|
-
|
|
26
|
-
export class EmbeddingService {
|
|
27
|
-
private embeddingPipeline: FeatureExtractionPipeline | null = null
|
|
28
|
-
private modelName: string
|
|
29
|
-
private isInitialized: boolean = false
|
|
30
|
-
private logger: Logger
|
|
31
|
-
private cache: Map<string, number[]>
|
|
32
|
-
private maxCacheSize: number
|
|
33
|
-
private circuitBreaker: CircuitBreaker
|
|
34
|
-
|
|
35
|
-
constructor(
|
|
36
|
-
logger: Logger,
|
|
37
|
-
modelName: string = 'Xenova/all-MiniLM-L6-v2',
|
|
38
|
-
maxCacheSize: number = 1000
|
|
39
|
-
) {
|
|
40
|
-
this.modelName = modelName
|
|
41
|
-
this.logger = logger.child({ component: 'embedding-service' })
|
|
42
|
-
this.cache = new Map()
|
|
43
|
-
this.maxCacheSize = maxCacheSize
|
|
44
|
-
this.circuitBreaker = new CircuitBreaker(
|
|
45
|
-
'embedding-service',
|
|
46
|
-
logger,
|
|
47
|
-
{
|
|
48
|
-
failureThreshold: 3,
|
|
49
|
-
timeout: 30000
|
|
50
|
-
}
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Initialize the embedding pipeline
|
|
56
|
-
* Downloads model on first run, then uses cache
|
|
57
|
-
*/
|
|
58
|
-
async initialize(): Promise<void> {
|
|
59
|
-
if (this.isInitialized) {
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
this.logger.info({ model: this.modelName }, 'Loading embedding model...')
|
|
65
|
-
|
|
66
|
-
// Load the feature extraction pipeline
|
|
67
|
-
this.embeddingPipeline = await pipeline('feature-extraction', this.modelName)
|
|
68
|
-
|
|
69
|
-
this.isInitialized = true
|
|
70
|
-
this.logger.info({ model: this.modelName }, 'Embedding model loaded successfully')
|
|
71
|
-
} catch (error) {
|
|
72
|
-
this.logger.error({ error, model: this.modelName }, 'Failed to load embedding model')
|
|
73
|
-
throw error
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Generate embedding for a single text
|
|
79
|
-
*/
|
|
80
|
-
async generateEmbedding(text: string): Promise<number[]> {
|
|
81
|
-
if (!this.isInitialized) {
|
|
82
|
-
await this.initialize()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const cacheKey = this.getCacheKey(text)
|
|
86
|
-
const cached = this.cache.get(cacheKey)
|
|
87
|
-
if (cached) {
|
|
88
|
-
this.logger.debug({ cacheKey }, 'Embedding cache hit')
|
|
89
|
-
return cached
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return this.circuitBreaker.execute(async () => {
|
|
93
|
-
const startTime = Date.now()
|
|
94
|
-
|
|
95
|
-
const preprocessed = this.preprocessText(text)
|
|
96
|
-
|
|
97
|
-
const output = await (this.embeddingPipeline as any)(preprocessed, {
|
|
98
|
-
pooling: 'mean',
|
|
99
|
-
normalize: true
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const embedding = Array.from((output as any).data as Float32Array)
|
|
103
|
-
|
|
104
|
-
const duration = Date.now() - startTime
|
|
105
|
-
this.logger.debug(
|
|
106
|
-
{ textLength: text.length, duration, dimensions: embedding.length },
|
|
107
|
-
'Embedding generated'
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
this.addToCache(cacheKey, embedding)
|
|
111
|
-
|
|
112
|
-
return embedding
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Generate embeddings for multiple texts (batched)
|
|
118
|
-
*/
|
|
119
|
-
async generateEmbeddings(texts: string[]): Promise<number[][]> {
|
|
120
|
-
if (!this.isInitialized) {
|
|
121
|
-
await this.initialize()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Process in batches to avoid memory issues
|
|
125
|
-
const batchSize = 10
|
|
126
|
-
const results: number[][] = []
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < texts.length; i += batchSize) {
|
|
129
|
-
const batch = texts.slice(i, i + batchSize)
|
|
130
|
-
const batchResults = await Promise.all(batch.map((text) => this.generateEmbedding(text)))
|
|
131
|
-
results.push(...batchResults)
|
|
132
|
-
|
|
133
|
-
this.logger.debug(
|
|
134
|
-
{ progress: `${Math.min(i + batchSize, texts.length)}/${texts.length}` },
|
|
135
|
-
'Batch embedding progress'
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return results
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Preprocess text before embedding
|
|
144
|
-
*/
|
|
145
|
-
private preprocessText(text: string): string {
|
|
146
|
-
// Remove excessive whitespace
|
|
147
|
-
let processed = text.replace(/\s+/g, ' ').trim()
|
|
148
|
-
|
|
149
|
-
// Truncate to max length (256 tokens ~ 1024 chars)
|
|
150
|
-
if (processed.length > 1024) {
|
|
151
|
-
processed = processed.slice(0, 1024)
|
|
152
|
-
this.logger.debug('Text truncated to 1024 characters')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return processed
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Generate cache key from text
|
|
160
|
-
* Uses SHA-256 hash to prevent collisions
|
|
161
|
-
*/
|
|
162
|
-
private getCacheKey(text: string): string {
|
|
163
|
-
return createHash('sha256').update(text).digest('hex')
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Add embedding to cache with LRU eviction
|
|
168
|
-
*/
|
|
169
|
-
private addToCache(key: string, embedding: number[]): void {
|
|
170
|
-
// Simple LRU: if at max size, delete oldest entry
|
|
171
|
-
if (this.cache.size >= this.maxCacheSize) {
|
|
172
|
-
const firstKey = this.cache.keys().next().value
|
|
173
|
-
if (firstKey) {
|
|
174
|
-
this.cache.delete(firstKey)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
this.cache.set(key, embedding)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Clear embedding cache
|
|
182
|
-
*/
|
|
183
|
-
clearCache(): void {
|
|
184
|
-
this.cache.clear()
|
|
185
|
-
this.logger.debug('Embedding cache cleared')
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Get cache statistics
|
|
190
|
-
*/
|
|
191
|
-
getCacheStats(): EmbeddingCacheStats {
|
|
192
|
-
return {
|
|
193
|
-
size: this.cache.size,
|
|
194
|
-
keys: Array.from(this.cache.keys())
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Check if service is initialized
|
|
200
|
-
*/
|
|
201
|
-
isReady(): boolean {
|
|
202
|
-
return this.isInitialized
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Calculate cosine similarity between two embeddings
|
|
207
|
-
* Static method for external use
|
|
208
|
-
*/
|
|
209
|
-
static cosineSimilarity(a: number[], b: number[]): number {
|
|
210
|
-
return cosineSimilarity(a, b)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Get the model name
|
|
215
|
-
*/
|
|
216
|
-
getModelName(): string {
|
|
217
|
-
return this.modelName
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Get expected embedding dimensions
|
|
222
|
-
*/
|
|
223
|
-
getEmbeddingDimensions(): number {
|
|
224
|
-
return 384 // all-MiniLM-L6-v2 outputs 384 dimensions
|
|
225
|
-
}
|
|
226
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Embedding Generation Service
|
|
3
|
+
* Phase 3: Memory and Embedding System
|
|
4
|
+
*
|
|
5
|
+
* Uses transformers.js for local embedding generation
|
|
6
|
+
* Model: all-MiniLM-L6-v2 (384 dimensions)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { pipeline, env } from '@xenova/transformers'
|
|
10
|
+
import { createHash } from 'crypto'
|
|
11
|
+
import type { Logger } from 'pino'
|
|
12
|
+
import type { EmbeddingCacheStats } from './types'
|
|
13
|
+
import { cosineSimilarity } from './embedding-utils'
|
|
14
|
+
import { CircuitBreaker } from '@/utils'
|
|
15
|
+
|
|
16
|
+
// Configure transformers.js for Bun compatibility
|
|
17
|
+
env.allowLocalModels = true
|
|
18
|
+
env.allowRemoteModels = false
|
|
19
|
+
env.useBrowserCache = false
|
|
20
|
+
// Disable web workers to avoid ONNX blob URL issues in Bun
|
|
21
|
+
env.backends.onnx.wasm.numThreads = 1
|
|
22
|
+
env.backends.onnx.wasm.simd = true
|
|
23
|
+
|
|
24
|
+
type FeatureExtractionPipeline = Awaited<ReturnType<typeof pipeline>>
|
|
25
|
+
|
|
26
|
+
export class EmbeddingService {
|
|
27
|
+
private embeddingPipeline: FeatureExtractionPipeline | null = null
|
|
28
|
+
private modelName: string
|
|
29
|
+
private isInitialized: boolean = false
|
|
30
|
+
private logger: Logger
|
|
31
|
+
private cache: Map<string, number[]>
|
|
32
|
+
private maxCacheSize: number
|
|
33
|
+
private circuitBreaker: CircuitBreaker
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
logger: Logger,
|
|
37
|
+
modelName: string = 'Xenova/all-MiniLM-L6-v2',
|
|
38
|
+
maxCacheSize: number = 1000
|
|
39
|
+
) {
|
|
40
|
+
this.modelName = modelName
|
|
41
|
+
this.logger = logger.child({ component: 'embedding-service' })
|
|
42
|
+
this.cache = new Map()
|
|
43
|
+
this.maxCacheSize = maxCacheSize
|
|
44
|
+
this.circuitBreaker = new CircuitBreaker(
|
|
45
|
+
'embedding-service',
|
|
46
|
+
logger,
|
|
47
|
+
{
|
|
48
|
+
failureThreshold: 3,
|
|
49
|
+
timeout: 30000
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the embedding pipeline
|
|
56
|
+
* Downloads model on first run, then uses cache
|
|
57
|
+
*/
|
|
58
|
+
async initialize(): Promise<void> {
|
|
59
|
+
if (this.isInitialized) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
this.logger.info({ model: this.modelName }, 'Loading embedding model...')
|
|
65
|
+
|
|
66
|
+
// Load the feature extraction pipeline
|
|
67
|
+
this.embeddingPipeline = await pipeline('feature-extraction', this.modelName)
|
|
68
|
+
|
|
69
|
+
this.isInitialized = true
|
|
70
|
+
this.logger.info({ model: this.modelName }, 'Embedding model loaded successfully')
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.logger.error({ error, model: this.modelName }, 'Failed to load embedding model')
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate embedding for a single text
|
|
79
|
+
*/
|
|
80
|
+
async generateEmbedding(text: string): Promise<number[]> {
|
|
81
|
+
if (!this.isInitialized) {
|
|
82
|
+
await this.initialize()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cacheKey = this.getCacheKey(text)
|
|
86
|
+
const cached = this.cache.get(cacheKey)
|
|
87
|
+
if (cached) {
|
|
88
|
+
this.logger.debug({ cacheKey }, 'Embedding cache hit')
|
|
89
|
+
return cached
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.circuitBreaker.execute(async () => {
|
|
93
|
+
const startTime = Date.now()
|
|
94
|
+
|
|
95
|
+
const preprocessed = this.preprocessText(text)
|
|
96
|
+
|
|
97
|
+
const output = await (this.embeddingPipeline as any)(preprocessed, {
|
|
98
|
+
pooling: 'mean',
|
|
99
|
+
normalize: true
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const embedding = Array.from((output as any).data as Float32Array)
|
|
103
|
+
|
|
104
|
+
const duration = Date.now() - startTime
|
|
105
|
+
this.logger.debug(
|
|
106
|
+
{ textLength: text.length, duration, dimensions: embedding.length },
|
|
107
|
+
'Embedding generated'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
this.addToCache(cacheKey, embedding)
|
|
111
|
+
|
|
112
|
+
return embedding
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate embeddings for multiple texts (batched)
|
|
118
|
+
*/
|
|
119
|
+
async generateEmbeddings(texts: string[]): Promise<number[][]> {
|
|
120
|
+
if (!this.isInitialized) {
|
|
121
|
+
await this.initialize()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Process in batches to avoid memory issues
|
|
125
|
+
const batchSize = 10
|
|
126
|
+
const results: number[][] = []
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
129
|
+
const batch = texts.slice(i, i + batchSize)
|
|
130
|
+
const batchResults = await Promise.all(batch.map((text) => this.generateEmbedding(text)))
|
|
131
|
+
results.push(...batchResults)
|
|
132
|
+
|
|
133
|
+
this.logger.debug(
|
|
134
|
+
{ progress: `${Math.min(i + batchSize, texts.length)}/${texts.length}` },
|
|
135
|
+
'Batch embedding progress'
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Preprocess text before embedding
|
|
144
|
+
*/
|
|
145
|
+
private preprocessText(text: string): string {
|
|
146
|
+
// Remove excessive whitespace
|
|
147
|
+
let processed = text.replace(/\s+/g, ' ').trim()
|
|
148
|
+
|
|
149
|
+
// Truncate to max length (256 tokens ~ 1024 chars)
|
|
150
|
+
if (processed.length > 1024) {
|
|
151
|
+
processed = processed.slice(0, 1024)
|
|
152
|
+
this.logger.debug('Text truncated to 1024 characters')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return processed
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate cache key from text
|
|
160
|
+
* Uses SHA-256 hash to prevent collisions
|
|
161
|
+
*/
|
|
162
|
+
private getCacheKey(text: string): string {
|
|
163
|
+
return createHash('sha256').update(text).digest('hex')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add embedding to cache with LRU eviction
|
|
168
|
+
*/
|
|
169
|
+
private addToCache(key: string, embedding: number[]): void {
|
|
170
|
+
// Simple LRU: if at max size, delete oldest entry
|
|
171
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
172
|
+
const firstKey = this.cache.keys().next().value
|
|
173
|
+
if (firstKey) {
|
|
174
|
+
this.cache.delete(firstKey)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this.cache.set(key, embedding)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Clear embedding cache
|
|
182
|
+
*/
|
|
183
|
+
clearCache(): void {
|
|
184
|
+
this.cache.clear()
|
|
185
|
+
this.logger.debug('Embedding cache cleared')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get cache statistics
|
|
190
|
+
*/
|
|
191
|
+
getCacheStats(): EmbeddingCacheStats {
|
|
192
|
+
return {
|
|
193
|
+
size: this.cache.size,
|
|
194
|
+
keys: Array.from(this.cache.keys())
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if service is initialized
|
|
200
|
+
*/
|
|
201
|
+
isReady(): boolean {
|
|
202
|
+
return this.isInitialized
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculate cosine similarity between two embeddings
|
|
207
|
+
* Static method for external use
|
|
208
|
+
*/
|
|
209
|
+
static cosineSimilarity(a: number[], b: number[]): number {
|
|
210
|
+
return cosineSimilarity(a, b)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get the model name
|
|
215
|
+
*/
|
|
216
|
+
getModelName(): string {
|
|
217
|
+
return this.modelName
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get expected embedding dimensions
|
|
222
|
+
*/
|
|
223
|
+
getEmbeddingDimensions(): number {
|
|
224
|
+
return 384 // all-MiniLM-L6-v2 outputs 384 dimensions
|
|
225
|
+
}
|
|
226
|
+
}
|