@twelvehart/supermemory-runtime 1.0.0-next.0
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/.env.example +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base LLM Provider Class
|
|
3
|
+
*
|
|
4
|
+
* Provides common functionality for all LLM providers including:
|
|
5
|
+
* - Retry logic with exponential backoff
|
|
6
|
+
* - Caching
|
|
7
|
+
* - Error handling
|
|
8
|
+
* - Rate limit handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash } from 'crypto'
|
|
12
|
+
import { getLogger } from '../../utils/logger.js'
|
|
13
|
+
import { AppError, ErrorCode } from '../../utils/errors.js'
|
|
14
|
+
import type {
|
|
15
|
+
LLMProvider,
|
|
16
|
+
LLMProviderType,
|
|
17
|
+
LLMExtractionResult,
|
|
18
|
+
LLMRelationshipResult,
|
|
19
|
+
ExtractedMemory,
|
|
20
|
+
DetectedRelationship,
|
|
21
|
+
ExtractionOptions,
|
|
22
|
+
RelationshipDetectionOptions,
|
|
23
|
+
ProviderHealthStatus,
|
|
24
|
+
BaseLLMConfig,
|
|
25
|
+
CacheEntry,
|
|
26
|
+
CacheConfig,
|
|
27
|
+
LLMErrorCodeType,
|
|
28
|
+
} from './types.js'
|
|
29
|
+
import { LLMErrorCode } from './types.js'
|
|
30
|
+
import type { MemoryType } from '../../types/index.js'
|
|
31
|
+
|
|
32
|
+
const logger = getLogger('LLMProvider')
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Default Configuration
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_LLM_CONFIG: Required<BaseLLMConfig> = {
|
|
39
|
+
maxTokens: 2000,
|
|
40
|
+
temperature: 0.1,
|
|
41
|
+
timeoutMs: 30000,
|
|
42
|
+
maxRetries: 3,
|
|
43
|
+
retryDelayMs: 1000,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_CACHE_CONFIG: CacheConfig = {
|
|
47
|
+
enabled: true,
|
|
48
|
+
ttlMs: 15 * 60 * 1000, // 15 minutes
|
|
49
|
+
maxSize: 1000,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// LLM Error Class
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
export class LLMError extends AppError {
|
|
57
|
+
readonly llmCode: LLMErrorCodeType
|
|
58
|
+
readonly provider: LLMProviderType
|
|
59
|
+
readonly retryable: boolean
|
|
60
|
+
readonly retryAfterMs?: number
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
message: string,
|
|
64
|
+
llmCode: LLMErrorCodeType,
|
|
65
|
+
provider: LLMProviderType,
|
|
66
|
+
retryable: boolean = false,
|
|
67
|
+
retryAfterMs?: number
|
|
68
|
+
) {
|
|
69
|
+
super(message, ErrorCode.EXTERNAL_SERVICE_ERROR, {
|
|
70
|
+
llmCode,
|
|
71
|
+
provider,
|
|
72
|
+
retryable,
|
|
73
|
+
retryAfterMs,
|
|
74
|
+
})
|
|
75
|
+
this.name = 'LLMError'
|
|
76
|
+
this.llmCode = llmCode
|
|
77
|
+
this.provider = provider
|
|
78
|
+
this.retryable = retryable
|
|
79
|
+
this.retryAfterMs = retryAfterMs
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static rateLimited(provider: LLMProviderType, retryAfterMs?: number): LLMError {
|
|
83
|
+
return new LLMError(`Rate limited by ${provider}`, LLMErrorCode.RATE_LIMITED, provider, true, retryAfterMs)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static timeout(provider: LLMProviderType): LLMError {
|
|
87
|
+
return new LLMError(`Request to ${provider} timed out`, LLMErrorCode.TIMEOUT, provider, true)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static invalidApiKey(provider: LLMProviderType): LLMError {
|
|
91
|
+
return new LLMError(`Invalid API key for ${provider}`, LLMErrorCode.INVALID_API_KEY, provider, false)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static invalidResponse(provider: LLMProviderType, details?: string): LLMError {
|
|
95
|
+
return new LLMError(
|
|
96
|
+
`Invalid response from ${provider}${details ? `: ${details}` : ''}`,
|
|
97
|
+
LLMErrorCode.INVALID_RESPONSE,
|
|
98
|
+
provider,
|
|
99
|
+
true
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static providerUnavailable(provider: LLMProviderType): LLMError {
|
|
104
|
+
return new LLMError(`${provider} provider is unavailable`, LLMErrorCode.PROVIDER_UNAVAILABLE, provider, true)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Base LLM Provider
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export abstract class BaseLLMProvider implements LLMProvider {
|
|
113
|
+
abstract readonly type: LLMProviderType
|
|
114
|
+
|
|
115
|
+
protected config: Required<BaseLLMConfig>
|
|
116
|
+
protected cacheConfig: CacheConfig
|
|
117
|
+
protected cache: Map<string, CacheEntry<LLMExtractionResult>> = new Map()
|
|
118
|
+
protected lastSuccess?: Date
|
|
119
|
+
|
|
120
|
+
constructor(config: Partial<BaseLLMConfig> = {}, cacheConfig: Partial<CacheConfig> = {}) {
|
|
121
|
+
this.config = { ...DEFAULT_LLM_CONFIG, ...config }
|
|
122
|
+
this.cacheConfig = { ...DEFAULT_CACHE_CONFIG, ...cacheConfig }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Abstract Methods - Must be implemented by subclasses
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Perform the actual LLM API call for memory extraction
|
|
131
|
+
*/
|
|
132
|
+
protected abstract doExtractMemories(
|
|
133
|
+
text: string,
|
|
134
|
+
options: ExtractionOptions
|
|
135
|
+
): Promise<{
|
|
136
|
+
memories: ExtractedMemory[]
|
|
137
|
+
rawResponse?: string
|
|
138
|
+
tokensUsed?: { prompt: number; completion: number; total: number }
|
|
139
|
+
}>
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Perform the actual LLM API call for relationship detection
|
|
143
|
+
*/
|
|
144
|
+
protected abstract doDetectRelationships(
|
|
145
|
+
newMemory: { id: string; content: string; type: MemoryType },
|
|
146
|
+
existingMemories: Array<{ id: string; content: string; type: MemoryType }>,
|
|
147
|
+
options: RelationshipDetectionOptions
|
|
148
|
+
): Promise<{
|
|
149
|
+
relationships: DetectedRelationship[]
|
|
150
|
+
supersededMemoryIds: string[]
|
|
151
|
+
}>
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Perform a generic JSON-only prompt task.
|
|
155
|
+
*/
|
|
156
|
+
protected abstract doGenerateJson(
|
|
157
|
+
systemPrompt: string,
|
|
158
|
+
userPrompt: string
|
|
159
|
+
): Promise<{
|
|
160
|
+
rawResponse: string
|
|
161
|
+
tokensUsed?: { prompt: number; completion: number; total: number }
|
|
162
|
+
}>
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if the provider is configured and available
|
|
166
|
+
*/
|
|
167
|
+
abstract isAvailable(): boolean
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Public Interface
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
async extractMemories(text: string, options: ExtractionOptions = {}): Promise<LLMExtractionResult> {
|
|
174
|
+
const startTime = Date.now()
|
|
175
|
+
|
|
176
|
+
// Check cache first
|
|
177
|
+
if (this.cacheConfig.enabled) {
|
|
178
|
+
const cached = this.getCachedResult(text, options)
|
|
179
|
+
if (cached) {
|
|
180
|
+
logger.debug('Cache hit for memory extraction', {
|
|
181
|
+
provider: this.type,
|
|
182
|
+
textLength: text.length,
|
|
183
|
+
})
|
|
184
|
+
return {
|
|
185
|
+
...cached,
|
|
186
|
+
cached: true,
|
|
187
|
+
processingTimeMs: Date.now() - startTime,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Perform extraction with retries
|
|
193
|
+
try {
|
|
194
|
+
const result = await this.withRetry(() => this.doExtractMemories(text, options), 'extractMemories')
|
|
195
|
+
|
|
196
|
+
const llmResult: LLMExtractionResult = {
|
|
197
|
+
memories: result.memories,
|
|
198
|
+
rawResponse: result.rawResponse,
|
|
199
|
+
tokensUsed: result.tokensUsed,
|
|
200
|
+
processingTimeMs: Date.now() - startTime,
|
|
201
|
+
cached: false,
|
|
202
|
+
provider: this.type,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Cache the result
|
|
206
|
+
if (this.cacheConfig.enabled) {
|
|
207
|
+
this.cacheResult(text, options, llmResult)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.lastSuccess = new Date()
|
|
211
|
+
logger.info('Memories extracted successfully', {
|
|
212
|
+
provider: this.type,
|
|
213
|
+
count: result.memories.length,
|
|
214
|
+
processingTimeMs: llmResult.processingTimeMs,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return llmResult
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.errorWithException('Failed to extract memories', error, {
|
|
220
|
+
provider: this.type,
|
|
221
|
+
textLength: text.length,
|
|
222
|
+
})
|
|
223
|
+
throw error
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async generateJson(
|
|
228
|
+
systemPrompt: string,
|
|
229
|
+
userPrompt: string
|
|
230
|
+
): Promise<{
|
|
231
|
+
rawResponse: string
|
|
232
|
+
tokensUsed?: { prompt: number; completion: number; total: number }
|
|
233
|
+
provider: LLMProviderType
|
|
234
|
+
}> {
|
|
235
|
+
try {
|
|
236
|
+
const result = await this.withRetry(() => this.doGenerateJson(systemPrompt, userPrompt), 'generateJson')
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
rawResponse: result.rawResponse,
|
|
240
|
+
tokensUsed: result.tokensUsed,
|
|
241
|
+
provider: this.type,
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof LLMError) {
|
|
245
|
+
throw error
|
|
246
|
+
}
|
|
247
|
+
throw LLMError.invalidResponse(this.type, error instanceof Error ? error.message : String(error))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async detectRelationships(
|
|
252
|
+
newMemory: { id: string; content: string; type: MemoryType },
|
|
253
|
+
existingMemories: Array<{ id: string; content: string; type: MemoryType }>,
|
|
254
|
+
options: RelationshipDetectionOptions = {}
|
|
255
|
+
): Promise<LLMRelationshipResult> {
|
|
256
|
+
const startTime = Date.now()
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const result = await this.withRetry(
|
|
260
|
+
() => this.doDetectRelationships(newMemory, existingMemories, options),
|
|
261
|
+
'detectRelationships'
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const llmResult: LLMRelationshipResult = {
|
|
265
|
+
relationships: result.relationships,
|
|
266
|
+
supersededMemoryIds: result.supersededMemoryIds,
|
|
267
|
+
processingTimeMs: Date.now() - startTime,
|
|
268
|
+
provider: this.type,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.lastSuccess = new Date()
|
|
272
|
+
logger.info('Relationships detected successfully', {
|
|
273
|
+
provider: this.type,
|
|
274
|
+
count: result.relationships.length,
|
|
275
|
+
processingTimeMs: llmResult.processingTimeMs,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return llmResult
|
|
279
|
+
} catch (error) {
|
|
280
|
+
logger.errorWithException('Failed to detect relationships', error, {
|
|
281
|
+
provider: this.type,
|
|
282
|
+
newMemoryId: newMemory.id,
|
|
283
|
+
existingCount: existingMemories.length,
|
|
284
|
+
})
|
|
285
|
+
throw error
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async getHealthStatus(): Promise<ProviderHealthStatus> {
|
|
290
|
+
if (!this.isAvailable()) {
|
|
291
|
+
return {
|
|
292
|
+
healthy: false,
|
|
293
|
+
provider: this.type,
|
|
294
|
+
error: 'Provider not configured',
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Simple health check - extract from minimal text
|
|
300
|
+
const startTime = Date.now()
|
|
301
|
+
await this.extractMemories('Health check: The system is operational.', {
|
|
302
|
+
maxMemories: 1,
|
|
303
|
+
minConfidence: 0,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
healthy: true,
|
|
308
|
+
provider: this.type,
|
|
309
|
+
latencyMs: Date.now() - startTime,
|
|
310
|
+
lastSuccess: this.lastSuccess,
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
healthy: false,
|
|
315
|
+
provider: this.type,
|
|
316
|
+
error: error instanceof Error ? error.message : String(error),
|
|
317
|
+
lastSuccess: this.lastSuccess,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// Retry Logic
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
protected async withRetry<T>(operation: () => Promise<T>, operationName: string): Promise<T> {
|
|
327
|
+
let lastError: Error | undefined
|
|
328
|
+
|
|
329
|
+
for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {
|
|
330
|
+
try {
|
|
331
|
+
return await this.withTimeout(operation(), this.config.timeoutMs)
|
|
332
|
+
} catch (error) {
|
|
333
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
334
|
+
|
|
335
|
+
const isRetryable = error instanceof LLMError ? error.retryable : this.isRetryableError(error)
|
|
336
|
+
|
|
337
|
+
if (!isRetryable || attempt === this.config.maxRetries) {
|
|
338
|
+
throw error
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Calculate delay with exponential backoff
|
|
342
|
+
const delay =
|
|
343
|
+
error instanceof LLMError && error.retryAfterMs
|
|
344
|
+
? error.retryAfterMs
|
|
345
|
+
: this.config.retryDelayMs * Math.pow(2, attempt - 1)
|
|
346
|
+
|
|
347
|
+
logger.warn(`Retrying ${operationName} after error`, {
|
|
348
|
+
provider: this.type,
|
|
349
|
+
attempt,
|
|
350
|
+
maxRetries: this.config.maxRetries,
|
|
351
|
+
delayMs: delay,
|
|
352
|
+
error: lastError.message,
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await this.sleep(delay)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
throw lastError
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
protected async withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
363
|
+
return Promise.race([
|
|
364
|
+
promise,
|
|
365
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(LLMError.timeout(this.type)), timeoutMs)),
|
|
366
|
+
])
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
protected isRetryableError(error: unknown): boolean {
|
|
370
|
+
if (error instanceof LLMError) {
|
|
371
|
+
return error.retryable
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check for common retryable error patterns
|
|
375
|
+
const message = error instanceof Error ? error.message.toLowerCase() : ''
|
|
376
|
+
return (
|
|
377
|
+
message.includes('timeout') ||
|
|
378
|
+
message.includes('rate limit') ||
|
|
379
|
+
message.includes('429') ||
|
|
380
|
+
message.includes('503') ||
|
|
381
|
+
message.includes('network')
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
protected sleep(ms: number): Promise<void> {
|
|
386
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Caching
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
protected getCacheKey(text: string, options: ExtractionOptions): string {
|
|
394
|
+
const keyData = JSON.stringify({
|
|
395
|
+
text: text.substring(0, 5000), // Limit for hashing
|
|
396
|
+
provider: this.type,
|
|
397
|
+
options,
|
|
398
|
+
})
|
|
399
|
+
return createHash('sha256').update(keyData).digest('hex')
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
protected getCachedResult(text: string, options: ExtractionOptions): LLMExtractionResult | null {
|
|
403
|
+
const key = this.getCacheKey(text, options)
|
|
404
|
+
const entry = this.cache.get(key)
|
|
405
|
+
|
|
406
|
+
if (!entry) {
|
|
407
|
+
return null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Check if expired
|
|
411
|
+
if (entry.expiresAt < new Date()) {
|
|
412
|
+
this.cache.delete(key)
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return entry.value
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
protected cacheResult(text: string, options: ExtractionOptions, result: LLMExtractionResult): void {
|
|
420
|
+
// Enforce cache size limit
|
|
421
|
+
if (this.cache.size >= this.cacheConfig.maxSize) {
|
|
422
|
+
// Remove oldest entries
|
|
423
|
+
const entries = Array.from(this.cache.entries())
|
|
424
|
+
entries.sort((a, b) => a[1].createdAt.getTime() - b[1].createdAt.getTime())
|
|
425
|
+
const toRemove = entries.slice(0, Math.floor(this.cacheConfig.maxSize * 0.1))
|
|
426
|
+
for (const [key] of toRemove) {
|
|
427
|
+
this.cache.delete(key)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const key = this.getCacheKey(text, options)
|
|
432
|
+
const now = new Date()
|
|
433
|
+
|
|
434
|
+
this.cache.set(key, {
|
|
435
|
+
value: result,
|
|
436
|
+
createdAt: now,
|
|
437
|
+
expiresAt: new Date(now.getTime() + this.cacheConfig.ttlMs),
|
|
438
|
+
inputHash: key,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Clear all cached results
|
|
444
|
+
*/
|
|
445
|
+
clearCache(): void {
|
|
446
|
+
this.cache.clear()
|
|
447
|
+
logger.debug('Cache cleared', { provider: this.type })
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get cache statistics
|
|
452
|
+
*/
|
|
453
|
+
getCacheStats(): { size: number; maxSize: number; ttlMs: number } {
|
|
454
|
+
return {
|
|
455
|
+
size: this.cache.size,
|
|
456
|
+
maxSize: this.cacheConfig.maxSize,
|
|
457
|
+
ttlMs: this.cacheConfig.ttlMs,
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|