@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,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contradiction Detector Service
|
|
3
|
+
*
|
|
4
|
+
* LLM-based semantic contradiction detection between memory pairs.
|
|
5
|
+
* Replaces heuristic matching for TODO-002 in memory.service.ts
|
|
6
|
+
*
|
|
7
|
+
* Cost optimization:
|
|
8
|
+
* - HNSW similarity search to reduce comparison pairs
|
|
9
|
+
* - Prompt caching for repeated patterns
|
|
10
|
+
* - Batch contradiction detection
|
|
11
|
+
* - Fallback to heuristic matching
|
|
12
|
+
*
|
|
13
|
+
* Target: <$0.60/month with typical usage
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getLogger } from '../../utils/logger.js'
|
|
17
|
+
import { createHash } from 'crypto'
|
|
18
|
+
import type { Memory } from '../../types/index.js'
|
|
19
|
+
import { getLLMProvider, isLLMAvailable } from './index.js'
|
|
20
|
+
import { LLMError } from './base.js'
|
|
21
|
+
|
|
22
|
+
const logger = getLogger('ContradictionDetector')
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Prompt Templates
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export const CONTRADICTION_DETECTOR_SYSTEM_PROMPT = `You are an expert at detecting contradictions and updates between statements.
|
|
29
|
+
|
|
30
|
+
Compare two statements and determine:
|
|
31
|
+
1. Do they contradict each other?
|
|
32
|
+
2. Does the NEW statement update or supersede the OLD statement?
|
|
33
|
+
3. What is your confidence (0.0-1.0)?
|
|
34
|
+
|
|
35
|
+
Types of relationships:
|
|
36
|
+
- CONTRADICTION: Statements directly conflict (both may be valid from different times)
|
|
37
|
+
- UPDATE: NEW corrects or modifies OLD (making OLD outdated)
|
|
38
|
+
- SUPERSEDE: NEW completely replaces OLD (OLD should be archived)
|
|
39
|
+
- COMPATIBLE: No contradiction (related or compatible information)
|
|
40
|
+
|
|
41
|
+
Respond with ONLY a JSON object:
|
|
42
|
+
{
|
|
43
|
+
"isContradiction": boolean,
|
|
44
|
+
"confidence": 0.0-1.0,
|
|
45
|
+
"reason": "brief explanation",
|
|
46
|
+
"shouldSupersede": boolean
|
|
47
|
+
}`
|
|
48
|
+
|
|
49
|
+
export function buildContradictionUserPrompt(newContent: string, existingContent: string): string {
|
|
50
|
+
return `Compare these statements:\n\nOLD: "${existingContent}"\nNEW: "${newContent}"\n\nRespond with JSON only.`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Types
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
export interface ContradictionResult {
|
|
58
|
+
isContradiction: boolean
|
|
59
|
+
confidence: number
|
|
60
|
+
reason: string
|
|
61
|
+
shouldSupersede: boolean
|
|
62
|
+
cached: boolean
|
|
63
|
+
usedLLM: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DetectorConfig {
|
|
67
|
+
/** Minimum confidence for contradiction (0-1) */
|
|
68
|
+
minConfidence?: number
|
|
69
|
+
/** Whether to enable caching */
|
|
70
|
+
enableCache?: boolean
|
|
71
|
+
/** Cache TTL in milliseconds */
|
|
72
|
+
cacheTTLMs?: number
|
|
73
|
+
/** Maximum cache size */
|
|
74
|
+
maxCacheSize?: number
|
|
75
|
+
/** Whether to fallback to heuristics on errors */
|
|
76
|
+
fallbackToHeuristics?: boolean
|
|
77
|
+
/** Minimum word overlap ratio to even check (0-1) */
|
|
78
|
+
minOverlapForCheck?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface CacheEntry {
|
|
82
|
+
isContradiction: boolean
|
|
83
|
+
confidence: number
|
|
84
|
+
reason: string
|
|
85
|
+
shouldSupersede: boolean
|
|
86
|
+
timestamp: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Heuristic Patterns
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
const RELATIONSHIP_INDICATORS = {
|
|
94
|
+
updates: [
|
|
95
|
+
/\b(now|currently|as of|updated to|changed to|modified to)\b/i,
|
|
96
|
+
/\b(no longer|not anymore|stopped|quit|left)\b/i,
|
|
97
|
+
],
|
|
98
|
+
contradicts: [
|
|
99
|
+
/\b(but|however|actually|instead|rather|on the contrary)\b/i,
|
|
100
|
+
/\b(never|not|don't|doesn't|didn't|won't|can't)\b/i,
|
|
101
|
+
],
|
|
102
|
+
supersedes: [/\b(replaced|superseded|obsolete|deprecated|archived)\b/i, /\b(new version|latest|updated|revised)\b/i],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Contradiction Detector Service
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
export class ContradictionDetectorService {
|
|
110
|
+
private config: Required<DetectorConfig>
|
|
111
|
+
private cache: Map<string, CacheEntry> = new Map()
|
|
112
|
+
private stats = {
|
|
113
|
+
totalChecks: 0,
|
|
114
|
+
llmChecks: 0,
|
|
115
|
+
heuristicChecks: 0,
|
|
116
|
+
cacheHits: 0,
|
|
117
|
+
contradictionsFound: 0,
|
|
118
|
+
errors: 0,
|
|
119
|
+
totalCost: 0,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
constructor(config: DetectorConfig = {}) {
|
|
123
|
+
this.config = {
|
|
124
|
+
minConfidence: config.minConfidence ?? 0.7,
|
|
125
|
+
enableCache: config.enableCache ?? true,
|
|
126
|
+
cacheTTLMs: config.cacheTTLMs ?? 30 * 60 * 1000, // 30 minutes
|
|
127
|
+
maxCacheSize: config.maxCacheSize ?? 500,
|
|
128
|
+
fallbackToHeuristics: config.fallbackToHeuristics ?? true,
|
|
129
|
+
minOverlapForCheck: config.minOverlapForCheck ?? 0.2,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
logger.info('Contradiction detector initialized', {
|
|
133
|
+
cacheEnabled: this.config.enableCache,
|
|
134
|
+
fallbackEnabled: this.config.fallbackToHeuristics,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// Public API
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a new memory contradicts or updates an existing memory
|
|
144
|
+
*
|
|
145
|
+
* @param newMemory - The new memory being added
|
|
146
|
+
* @param existingMemory - The existing memory to compare against
|
|
147
|
+
* @returns Contradiction detection result
|
|
148
|
+
*/
|
|
149
|
+
async checkContradiction(newMemory: Memory, existingMemory: Memory): Promise<ContradictionResult> {
|
|
150
|
+
this.stats.totalChecks++
|
|
151
|
+
|
|
152
|
+
// Check cache first
|
|
153
|
+
if (this.config.enableCache) {
|
|
154
|
+
const cached = this.getCached(newMemory.content, existingMemory.content)
|
|
155
|
+
if (cached) {
|
|
156
|
+
this.stats.cacheHits++
|
|
157
|
+
logger.debug('Cache hit for contradiction check')
|
|
158
|
+
return {
|
|
159
|
+
...cached,
|
|
160
|
+
cached: true,
|
|
161
|
+
usedLLM: false,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// NOTE: We don't skip LLM based on word overlap anymore.
|
|
167
|
+
// The LLM should handle semantic analysis - "I live in New York" vs "I moved to San Francisco"
|
|
168
|
+
// have 0% word overlap but ARE semantically related and need LLM analysis.
|
|
169
|
+
// Only skip for truly empty content.
|
|
170
|
+
|
|
171
|
+
// Try LLM detection if available (semantic analysis with minimal overlap filter)
|
|
172
|
+
if (isLLMAvailable()) {
|
|
173
|
+
try {
|
|
174
|
+
const result = await this.detectWithLLM(newMemory, existingMemory)
|
|
175
|
+
this.stats.llmChecks++
|
|
176
|
+
|
|
177
|
+
if (result.isContradiction) {
|
|
178
|
+
this.stats.contradictionsFound++
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Cache the result
|
|
182
|
+
if (this.config.enableCache && result.confidence >= this.config.minConfidence) {
|
|
183
|
+
this.setCached(newMemory.content, existingMemory.content, {
|
|
184
|
+
isContradiction: result.isContradiction,
|
|
185
|
+
confidence: result.confidence,
|
|
186
|
+
reason: result.reason,
|
|
187
|
+
shouldSupersede: result.shouldSupersede,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...result,
|
|
194
|
+
cached: false,
|
|
195
|
+
usedLLM: true,
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.stats.errors++
|
|
199
|
+
logger.warn('LLM contradiction detection failed, falling back to heuristics', {
|
|
200
|
+
error: error instanceof Error ? error.message : String(error),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (!this.config.fallbackToHeuristics) {
|
|
204
|
+
throw error
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fallback to heuristics
|
|
210
|
+
// Only apply overlap filter for heuristics (semantic analysis not available)
|
|
211
|
+
const overlap = this.calculateWordOverlap(newMemory.content, existingMemory.content)
|
|
212
|
+
if (overlap < this.config.minOverlapForCheck) {
|
|
213
|
+
logger.debug('Skipping heuristic check due to low overlap', { overlap })
|
|
214
|
+
return {
|
|
215
|
+
isContradiction: false,
|
|
216
|
+
confidence: 0,
|
|
217
|
+
reason: 'Insufficient content overlap for heuristic analysis',
|
|
218
|
+
shouldSupersede: false,
|
|
219
|
+
cached: false,
|
|
220
|
+
usedLLM: false,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const heuristicResult = this.detectWithHeuristics(newMemory, existingMemory)
|
|
225
|
+
this.stats.heuristicChecks++
|
|
226
|
+
|
|
227
|
+
if (heuristicResult.isContradiction) {
|
|
228
|
+
this.stats.contradictionsFound++
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
...heuristicResult,
|
|
233
|
+
cached: false,
|
|
234
|
+
usedLLM: false,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get detection statistics
|
|
240
|
+
*/
|
|
241
|
+
getStats() {
|
|
242
|
+
const cacheHitRate = this.stats.totalChecks > 0 ? (this.stats.cacheHits / this.stats.totalChecks) * 100 : 0
|
|
243
|
+
|
|
244
|
+
const contradictionRate =
|
|
245
|
+
this.stats.totalChecks > 0 ? (this.stats.contradictionsFound / this.stats.totalChecks) * 100 : 0
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...this.stats,
|
|
249
|
+
cacheHitRate: parseFloat(cacheHitRate.toFixed(2)),
|
|
250
|
+
contradictionRate: parseFloat(contradictionRate.toFixed(2)),
|
|
251
|
+
cacheSize: this.cache.size,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Clear the cache
|
|
257
|
+
*/
|
|
258
|
+
clearCache(): void {
|
|
259
|
+
this.cache.clear()
|
|
260
|
+
logger.info('Contradiction cache cleared')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// LLM Detection
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
private async detectWithLLM(
|
|
268
|
+
newMemory: Memory,
|
|
269
|
+
existingMemory: Memory
|
|
270
|
+
): Promise<{
|
|
271
|
+
isContradiction: boolean
|
|
272
|
+
confidence: number
|
|
273
|
+
reason: string
|
|
274
|
+
shouldSupersede: boolean
|
|
275
|
+
}> {
|
|
276
|
+
const provider = getLLMProvider()
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const response = await provider.generateJson(
|
|
280
|
+
CONTRADICTION_DETECTOR_SYSTEM_PROMPT,
|
|
281
|
+
buildContradictionUserPrompt(newMemory.content, existingMemory.content)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const parsed = this.parseJsonResponse(response.rawResponse, response.provider)
|
|
285
|
+
|
|
286
|
+
// Estimate cost
|
|
287
|
+
const inputCost = ((response.tokensUsed?.prompt ?? 0) / 1000000) * 0.25
|
|
288
|
+
const outputCost = ((response.tokensUsed?.completion ?? 0) / 1000000) * 1.25
|
|
289
|
+
this.stats.totalCost += inputCost + outputCost
|
|
290
|
+
|
|
291
|
+
logger.debug('LLM contradiction detection successful', {
|
|
292
|
+
isContradiction: parsed.isContradiction,
|
|
293
|
+
confidence: parsed.confidence,
|
|
294
|
+
tokensUsed: response.tokensUsed?.total ?? 0,
|
|
295
|
+
cost: inputCost + outputCost,
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return parsed
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof LLMError) {
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
throw new Error(`LLM contradiction detection failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private parseJsonResponse(
|
|
308
|
+
rawResponse: string,
|
|
309
|
+
provider: 'openai' | 'anthropic' | 'mock'
|
|
310
|
+
): {
|
|
311
|
+
isContradiction: boolean
|
|
312
|
+
confidence: number
|
|
313
|
+
reason: string
|
|
314
|
+
shouldSupersede: boolean
|
|
315
|
+
} {
|
|
316
|
+
const trimmed = rawResponse.trim()
|
|
317
|
+
const jsonMatch = trimmed.startsWith('{') ? trimmed : trimmed.match(/\{[\s\S]*\}/)?.[0]
|
|
318
|
+
if (!jsonMatch) {
|
|
319
|
+
throw LLMError.invalidResponse(provider, 'No JSON object found in response')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let parsed: unknown
|
|
323
|
+
try {
|
|
324
|
+
parsed = JSON.parse(jsonMatch)
|
|
325
|
+
} catch {
|
|
326
|
+
throw LLMError.invalidResponse(provider, 'Invalid JSON response')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (
|
|
330
|
+
!parsed ||
|
|
331
|
+
typeof parsed !== 'object' ||
|
|
332
|
+
!('isContradiction' in parsed) ||
|
|
333
|
+
!('confidence' in parsed) ||
|
|
334
|
+
!('reason' in parsed) ||
|
|
335
|
+
!('shouldSupersede' in parsed)
|
|
336
|
+
) {
|
|
337
|
+
throw LLMError.invalidResponse(provider, 'Missing required fields in JSON response')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const isContradiction = (parsed as { isContradiction: boolean }).isContradiction
|
|
341
|
+
const confidence = (parsed as { confidence: number }).confidence
|
|
342
|
+
const reason = (parsed as { reason: string }).reason
|
|
343
|
+
const shouldSupersede = (parsed as { shouldSupersede: boolean }).shouldSupersede
|
|
344
|
+
|
|
345
|
+
if (typeof isContradiction !== 'boolean' || typeof shouldSupersede !== 'boolean') {
|
|
346
|
+
throw LLMError.invalidResponse(provider, 'Invalid boolean fields in response')
|
|
347
|
+
}
|
|
348
|
+
if (typeof confidence !== 'number' || Number.isNaN(confidence)) {
|
|
349
|
+
throw LLMError.invalidResponse(provider, 'Invalid confidence in response')
|
|
350
|
+
}
|
|
351
|
+
if (typeof reason !== 'string') {
|
|
352
|
+
throw LLMError.invalidResponse(provider, 'Invalid reason in response')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { isContradiction, confidence, reason, shouldSupersede }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// Heuristic Detection
|
|
360
|
+
// ============================================================================
|
|
361
|
+
|
|
362
|
+
private detectWithHeuristics(
|
|
363
|
+
newMemory: Memory,
|
|
364
|
+
existingMemory: Memory
|
|
365
|
+
): {
|
|
366
|
+
isContradiction: boolean
|
|
367
|
+
confidence: number
|
|
368
|
+
reason: string
|
|
369
|
+
shouldSupersede: boolean
|
|
370
|
+
} {
|
|
371
|
+
const newLower = newMemory.content.toLowerCase()
|
|
372
|
+
const existingLower = existingMemory.content.toLowerCase()
|
|
373
|
+
|
|
374
|
+
// Calculate word overlap
|
|
375
|
+
const overlap = this.calculateWordOverlap(newLower, existingLower)
|
|
376
|
+
|
|
377
|
+
// Check for update indicators
|
|
378
|
+
let hasUpdateIndicator = false
|
|
379
|
+
for (const pattern of RELATIONSHIP_INDICATORS.updates) {
|
|
380
|
+
if (pattern.test(newLower)) {
|
|
381
|
+
hasUpdateIndicator = true
|
|
382
|
+
break
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check for contradiction indicators
|
|
387
|
+
let hasContradiction = false
|
|
388
|
+
for (const pattern of RELATIONSHIP_INDICATORS.contradicts) {
|
|
389
|
+
if (pattern.test(newLower) && overlap > 0.3) {
|
|
390
|
+
hasContradiction = true
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check for superseding indicators
|
|
396
|
+
let hasSuperseding = false
|
|
397
|
+
for (const pattern of RELATIONSHIP_INDICATORS.supersedes) {
|
|
398
|
+
if (pattern.test(newLower) && overlap > 0.4) {
|
|
399
|
+
hasSuperseding = true
|
|
400
|
+
break
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const isContradiction = (hasUpdateIndicator || hasContradiction || hasSuperseding) && overlap > 0.3
|
|
405
|
+
const confidence = isContradiction ? Math.min(0.6, overlap + 0.2) : 0.3
|
|
406
|
+
const shouldSupersede = hasSuperseding || (hasUpdateIndicator && overlap > 0.5)
|
|
407
|
+
|
|
408
|
+
let reason = 'No contradiction detected via heuristics'
|
|
409
|
+
if (isContradiction) {
|
|
410
|
+
if (hasSuperseding) {
|
|
411
|
+
reason = 'New memory supersedes existing (via pattern matching)'
|
|
412
|
+
} else if (hasUpdateIndicator) {
|
|
413
|
+
reason = 'New memory updates existing (via pattern matching)'
|
|
414
|
+
} else {
|
|
415
|
+
reason = 'Contradiction detected (via pattern matching)'
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
logger.debug('Heuristic contradiction detection', {
|
|
420
|
+
isContradiction,
|
|
421
|
+
confidence,
|
|
422
|
+
overlap,
|
|
423
|
+
shouldSupersede,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
isContradiction,
|
|
428
|
+
confidence,
|
|
429
|
+
reason,
|
|
430
|
+
shouldSupersede,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ============================================================================
|
|
435
|
+
// Helpers
|
|
436
|
+
// ============================================================================
|
|
437
|
+
|
|
438
|
+
private calculateWordOverlap(text1: string, text2: string): number {
|
|
439
|
+
const words1 = new Set(
|
|
440
|
+
text1
|
|
441
|
+
.toLowerCase()
|
|
442
|
+
.split(/\s+/)
|
|
443
|
+
.filter((w) => w.length > 3)
|
|
444
|
+
)
|
|
445
|
+
const words2 = new Set(
|
|
446
|
+
text2
|
|
447
|
+
.toLowerCase()
|
|
448
|
+
.split(/\s+/)
|
|
449
|
+
.filter((w) => w.length > 3)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
const intersection = new Set([...words1].filter((x) => words2.has(x)))
|
|
453
|
+
const union = new Set([...words1, ...words2])
|
|
454
|
+
|
|
455
|
+
return union.size > 0 ? intersection.size / union.size : 0
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// Caching
|
|
460
|
+
// ============================================================================
|
|
461
|
+
|
|
462
|
+
private getCacheKey(content1: string, content2: string): string {
|
|
463
|
+
// Normalize and create deterministic key regardless of order
|
|
464
|
+
const normalized = [content1, content2]
|
|
465
|
+
.map((c) => c.substring(0, 200).trim().toLowerCase())
|
|
466
|
+
.sort()
|
|
467
|
+
.join('|||')
|
|
468
|
+
return createHash('sha256').update(normalized).digest('hex')
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private getCached(content1: string, content2: string): CacheEntry | null {
|
|
472
|
+
const key = this.getCacheKey(content1, content2)
|
|
473
|
+
const entry = this.cache.get(key)
|
|
474
|
+
|
|
475
|
+
if (!entry) {
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check if expired
|
|
480
|
+
const age = Date.now() - entry.timestamp
|
|
481
|
+
if (age > this.config.cacheTTLMs) {
|
|
482
|
+
this.cache.delete(key)
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return entry
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private setCached(content1: string, content2: string, entry: CacheEntry): void {
|
|
490
|
+
// Enforce cache size limit
|
|
491
|
+
if (this.cache.size >= this.config.maxCacheSize) {
|
|
492
|
+
const entries = Array.from(this.cache.entries())
|
|
493
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
|
494
|
+
const toRemove = entries.slice(0, Math.floor(this.config.maxCacheSize * 0.1))
|
|
495
|
+
for (const [key] of toRemove) {
|
|
496
|
+
this.cache.delete(key)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const key = this.getCacheKey(content1, content2)
|
|
501
|
+
this.cache.set(key, entry)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// Singleton Instance
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
let _instance: ContradictionDetectorService | null = null
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get the singleton instance
|
|
513
|
+
*/
|
|
514
|
+
export function getContradictionDetector(config?: DetectorConfig): ContradictionDetectorService {
|
|
515
|
+
if (!_instance) {
|
|
516
|
+
_instance = new ContradictionDetectorService(config)
|
|
517
|
+
}
|
|
518
|
+
return _instance
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Reset the singleton (for testing)
|
|
523
|
+
*/
|
|
524
|
+
export function resetContradictionDetector(): void {
|
|
525
|
+
_instance = null
|
|
526
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Heuristic Classification Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides a single source of truth for memory type pattern matching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MemoryType } from '../../types/index.js'
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Memory Type Classification Patterns
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
const FACT_PATTERNS: readonly RegExp[] = [
|
|
14
|
+
/\b(?:is|are|was|were|has|have|had)\b/i,
|
|
15
|
+
/\b(?:born|died|founded|created|invented)\b/i,
|
|
16
|
+
/\b(?:located|situated|found)\s+(?:in|at|on)\b/i,
|
|
17
|
+
/\b(?:equals|means|represents)\b/i,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const EVENT_PATTERNS: readonly RegExp[] = [
|
|
21
|
+
/\b(?:happened|occurred|took place)\b/i,
|
|
22
|
+
/\b(?:yesterday|today|tomorrow|last|next)\s+(?:week|month|year|day)\b/i,
|
|
23
|
+
/\b(?:on|at)\s+\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/i,
|
|
24
|
+
/\b(?:meeting|conference|event|party|celebration)\b/i,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const PREFERENCE_PATTERNS: readonly RegExp[] = [
|
|
28
|
+
/\b(?:prefer|like|love|enjoy|hate|dislike)\b/i,
|
|
29
|
+
/\b(?:favorite|favourite|best|worst)\b/i,
|
|
30
|
+
/\b(?:want|wish|hope|desire)\b/i,
|
|
31
|
+
/\b(?:always|never|usually|often)\s+(?:use|choose|pick|select)\b/i,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const SKILL_PATTERNS: readonly RegExp[] = [
|
|
35
|
+
/\b(?:know|learn|understand|master)\s+(?:how to|to)\b/i,
|
|
36
|
+
/\b(?:can|able to|capable of)\b/i,
|
|
37
|
+
/\b(?:expert|proficient|skilled|experienced)\s+(?:in|at|with)\b/i,
|
|
38
|
+
/\b(?:programming|coding|developing|designing)\b/i,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const RELATIONSHIP_PATTERNS: readonly RegExp[] = [
|
|
42
|
+
/\b(?:married|engaged|dating|friends with)\b/i,
|
|
43
|
+
/\b(?:works|worked)\s+(?:for|with|at)\b/i,
|
|
44
|
+
/\b(?:brother|sister|mother|father|parent|child|spouse)\b/i,
|
|
45
|
+
/\b(?:colleague|teammate|partner|boss|manager)\b/i,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const CONTEXT_PATTERNS: readonly RegExp[] = [
|
|
49
|
+
/\b(?:currently|right now|at the moment)\b/i,
|
|
50
|
+
/\b(?:working on|thinking about|planning)\b/i,
|
|
51
|
+
/\b(?:in the context of|regarding|about)\b/i,
|
|
52
|
+
/\b(?:situation|scenario|case)\b/i,
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const NOTE_PATTERNS: readonly RegExp[] = [
|
|
56
|
+
/^(?:note|reminder|todo|remember)\s*:/i,
|
|
57
|
+
/\b(?:don't forget|keep in mind|note that)\b/i,
|
|
58
|
+
/^#|^\*|^-\s/m,
|
|
59
|
+
/\b(?:important|key|critical)\s+(?:point|note|fact)\b/i,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
export const MEMORY_TYPE_PATTERNS: Record<MemoryType, readonly RegExp[]> = {
|
|
63
|
+
fact: FACT_PATTERNS,
|
|
64
|
+
event: EVENT_PATTERNS,
|
|
65
|
+
preference: PREFERENCE_PATTERNS,
|
|
66
|
+
skill: SKILL_PATTERNS,
|
|
67
|
+
relationship: RELATIONSHIP_PATTERNS,
|
|
68
|
+
context: CONTEXT_PATTERNS,
|
|
69
|
+
note: NOTE_PATTERNS,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Heuristic Helpers
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export function getMemoryTypeScores(content: string): Record<MemoryType, number> {
|
|
77
|
+
const scores: Record<MemoryType, number> = {
|
|
78
|
+
fact: 0,
|
|
79
|
+
event: 0,
|
|
80
|
+
preference: 0,
|
|
81
|
+
skill: 0,
|
|
82
|
+
relationship: 0,
|
|
83
|
+
context: 0,
|
|
84
|
+
note: 0,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [type, patterns] of Object.entries(MEMORY_TYPE_PATTERNS)) {
|
|
88
|
+
for (const pattern of patterns) {
|
|
89
|
+
if (pattern.test(content)) {
|
|
90
|
+
scores[type as MemoryType] += 1
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return scores
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function classifyMemoryTypeHeuristically(content: string): {
|
|
99
|
+
type: MemoryType
|
|
100
|
+
matchCount: number
|
|
101
|
+
scores: Record<MemoryType, number>
|
|
102
|
+
} {
|
|
103
|
+
const scores = getMemoryTypeScores(content)
|
|
104
|
+
const maxScore = Math.max(...Object.values(scores))
|
|
105
|
+
|
|
106
|
+
if (maxScore === 0) {
|
|
107
|
+
return { type: 'note', matchCount: 0, scores }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const matchedType = Object.entries(scores).find(([_, score]) => score === maxScore)
|
|
111
|
+
return {
|
|
112
|
+
type: (matchedType?.[0] as MemoryType) || 'note',
|
|
113
|
+
matchCount: maxScore,
|
|
114
|
+
scores,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function countMemoryTypeMatches(content: string, type: MemoryType): number {
|
|
119
|
+
const patterns = MEMORY_TYPE_PATTERNS[type] || []
|
|
120
|
+
let matchCount = 0
|
|
121
|
+
for (const pattern of patterns) {
|
|
122
|
+
if (pattern.test(content)) {
|
|
123
|
+
matchCount += 1
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return matchCount
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function calculateHeuristicConfidence(
|
|
130
|
+
matchCount: number,
|
|
131
|
+
options: {
|
|
132
|
+
base?: number
|
|
133
|
+
perMatch?: number
|
|
134
|
+
max?: number
|
|
135
|
+
defaultConfidence?: number
|
|
136
|
+
} = {}
|
|
137
|
+
): number {
|
|
138
|
+
const base = options.base ?? 0.5
|
|
139
|
+
const perMatch = options.perMatch ?? 0.1
|
|
140
|
+
const max = options.max ?? 0.9
|
|
141
|
+
const defaultConfidence = options.defaultConfidence ?? 0.3
|
|
142
|
+
|
|
143
|
+
if (matchCount <= 0) {
|
|
144
|
+
return defaultConfidence
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Math.min(base + matchCount * perMatch, max)
|
|
148
|
+
}
|