@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,1338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Service - Core Memory Operations
|
|
3
|
+
*
|
|
4
|
+
* Handles extraction, classification, and relationship detection for memories.
|
|
5
|
+
* This is the main service layer that orchestrates memory operations.
|
|
6
|
+
*
|
|
7
|
+
* LLM Integration: Uses LLM-based extraction when available, with automatic
|
|
8
|
+
* fallback to regex-based extraction if no LLM provider is configured.
|
|
9
|
+
*
|
|
10
|
+
* Note: All storage operations are delegated to the MemoryRepository.
|
|
11
|
+
* No in-memory caching is done here to avoid storage inconsistency.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { MemoryType, MemoryRelationship, RelationshipType, Entity } from '../types/index.js'
|
|
15
|
+
import { generateId } from '../utils/id.js'
|
|
16
|
+
import { getLogger } from '../utils/logger.js'
|
|
17
|
+
import { AppError, ValidationError, ErrorCode } from '../utils/errors.js'
|
|
18
|
+
import { validate, validateMemoryContent, containerTagSchema } from '../utils/validation.js'
|
|
19
|
+
import { isEmbeddingRelationshipsEnabled } from '../config/feature-flags.js'
|
|
20
|
+
import { getEmbeddingService, type EmbeddingService } from './embedding.service.js'
|
|
21
|
+
import {
|
|
22
|
+
Memory,
|
|
23
|
+
Relationship,
|
|
24
|
+
UpdateCheckResult,
|
|
25
|
+
ExtensionCheckResult,
|
|
26
|
+
MemoryServiceConfig,
|
|
27
|
+
DEFAULT_MEMORY_CONFIG,
|
|
28
|
+
} from './memory.types.js'
|
|
29
|
+
import { type MemoryRepository, getMemoryRepository } from './memory.repository.js'
|
|
30
|
+
import {
|
|
31
|
+
getLLMProvider,
|
|
32
|
+
isLLMAvailable,
|
|
33
|
+
type LLMProvider,
|
|
34
|
+
type LLMExtractionResult,
|
|
35
|
+
type LLMRelationshipResult,
|
|
36
|
+
LLMError,
|
|
37
|
+
getMemoryClassifier,
|
|
38
|
+
getContradictionDetector,
|
|
39
|
+
getMemoryExtensionDetector,
|
|
40
|
+
} from './llm/index.js'
|
|
41
|
+
import { classifyMemoryTypeHeuristically, countMemoryTypeMatches } from './llm/heuristics.js'
|
|
42
|
+
import { detectRelationshipsWithEmbeddings } from './relationships/detector.js'
|
|
43
|
+
|
|
44
|
+
const logger = getLogger('MemoryService')
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Relationship Detection Patterns
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Patterns indicating a memory updates or corrects previous information.
|
|
52
|
+
*
|
|
53
|
+
* @example "Actually, the deadline was moved to Friday" - update indicator
|
|
54
|
+
* @example "Correction: the API uses v2, not v1" - explicit correction
|
|
55
|
+
*/
|
|
56
|
+
const UPDATE_INDICATOR_PATTERNS: readonly RegExp[] = [
|
|
57
|
+
/** Matches update/correction verbs: update, updated, correction, corrected */
|
|
58
|
+
/\b(?:update|updated|updating|correction|corrected)\b/i,
|
|
59
|
+
/** Matches correction adverbs: now, actually, instead */
|
|
60
|
+
/\b(?:now|actually|instead)\b/i,
|
|
61
|
+
/** Matches revision verbs: changed, revised, modified */
|
|
62
|
+
/\b(?:changed|revised|modified)\b/i,
|
|
63
|
+
] as const
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Patterns indicating a memory extends or adds to previous information.
|
|
67
|
+
*
|
|
68
|
+
* @example "Additionally, the API also supports batch operations"
|
|
69
|
+
* @example "Building on the previous point..."
|
|
70
|
+
*/
|
|
71
|
+
const EXTENSION_INDICATOR_PATTERNS: readonly RegExp[] = [
|
|
72
|
+
/** Matches additive conjunctions: also, additionally, furthermore, moreover */
|
|
73
|
+
/\b(?:also|additionally|furthermore|moreover)\b/i,
|
|
74
|
+
/** Matches additive phrases: in addition, on top of, besides */
|
|
75
|
+
/\b(?:in addition|on top of|besides)\b/i,
|
|
76
|
+
/** Matches building phrases: extending, building on, adding to */
|
|
77
|
+
/\b(?:extending|building on|adding to)\b/i,
|
|
78
|
+
] as const
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Patterns indicating a memory is derived from or caused by another.
|
|
82
|
+
*
|
|
83
|
+
* @example "Therefore, we need to update the schema"
|
|
84
|
+
* @example "Based on the requirements, we chose PostgreSQL"
|
|
85
|
+
*/
|
|
86
|
+
const DERIVATION_INDICATOR_PATTERNS: readonly RegExp[] = [
|
|
87
|
+
/** Matches consequence adverbs: therefore, thus, hence, consequently */
|
|
88
|
+
/\b(?:therefore|thus|hence|consequently)\b/i,
|
|
89
|
+
/** Matches causal conjunctions: because, since, as a result */
|
|
90
|
+
/\b(?:because|since|as a result)\b/i,
|
|
91
|
+
/** Matches derivation phrases: based on, derived from, follows from */
|
|
92
|
+
/\b(?:based on|derived from|follows from)\b/i,
|
|
93
|
+
] as const
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Patterns indicating a memory contradicts previous information.
|
|
97
|
+
*
|
|
98
|
+
* @example "However, the new tests show different results"
|
|
99
|
+
* @example "That's not true; the API returns JSON, not XML"
|
|
100
|
+
*/
|
|
101
|
+
const CONTRADICTION_INDICATOR_PATTERNS: readonly RegExp[] = [
|
|
102
|
+
/** Matches contrast conjunctions: however, but, although, despite */
|
|
103
|
+
/\b(?:however|but|although|despite)\b/i,
|
|
104
|
+
/** Matches opposition words: contrary, opposite, different */
|
|
105
|
+
/\b(?:contrary|opposite|different)\b/i,
|
|
106
|
+
/** Matches negation phrases: not true, incorrect, wrong */
|
|
107
|
+
/\b(?:not true|incorrect|wrong)\b/i,
|
|
108
|
+
] as const
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Patterns indicating a memory is semantically related to another.
|
|
112
|
+
*
|
|
113
|
+
* @example "This is related to the caching discussion"
|
|
114
|
+
* @example "See also the authentication module docs"
|
|
115
|
+
*/
|
|
116
|
+
const RELATION_INDICATOR_PATTERNS: readonly RegExp[] = [
|
|
117
|
+
/** Matches relation words: related, similar, like, same */
|
|
118
|
+
/\b(?:related|similar|like|same)\b/i,
|
|
119
|
+
/** Matches connection words: connected, linked, associated */
|
|
120
|
+
/\b(?:connected|linked|associated)\b/i,
|
|
121
|
+
/** Matches reference phrases: see also, refer to, compare */
|
|
122
|
+
/\b(?:see also|refer to|compare)\b/i,
|
|
123
|
+
] as const
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Patterns indicating a memory supersedes or replaces previous information.
|
|
127
|
+
*
|
|
128
|
+
* @example "This replaces the old authentication flow"
|
|
129
|
+
* @example "The previous approach is now deprecated"
|
|
130
|
+
*/
|
|
131
|
+
const SUPERSESSION_INDICATOR_PATTERNS: readonly RegExp[] = [
|
|
132
|
+
/** Matches replacement verbs: replaces, supersedes, overrides */
|
|
133
|
+
/\b(?:replaces|supersedes|overrides)\b/i,
|
|
134
|
+
/** Matches obsolescence phrases: no longer, obsolete, deprecated */
|
|
135
|
+
/\b(?:no longer|obsolete|deprecated)\b/i,
|
|
136
|
+
/** Matches recency phrases: new version, latest, current */
|
|
137
|
+
/\b(?:new version|latest|current)\b/i,
|
|
138
|
+
] as const
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Combined relationship indicator patterns for relationship detection.
|
|
142
|
+
* Maps each RelationshipType to its corresponding regex patterns.
|
|
143
|
+
*/
|
|
144
|
+
const RELATIONSHIP_INDICATORS: Record<RelationshipType, readonly RegExp[]> = {
|
|
145
|
+
updates: UPDATE_INDICATOR_PATTERNS,
|
|
146
|
+
extends: EXTENSION_INDICATOR_PATTERNS,
|
|
147
|
+
derives: DERIVATION_INDICATOR_PATTERNS,
|
|
148
|
+
contradicts: CONTRADICTION_INDICATOR_PATTERNS,
|
|
149
|
+
related: RELATION_INDICATOR_PATTERNS,
|
|
150
|
+
supersedes: SUPERSESSION_INDICATOR_PATTERNS,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Entity Extraction Patterns
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Patterns for extracting person names from text.
|
|
159
|
+
*
|
|
160
|
+
* @example "Dr. John Smith" - matches honorific + name pattern
|
|
161
|
+
* @example "John Smith" - matches two capitalized words
|
|
162
|
+
*/
|
|
163
|
+
const PERSON_ENTITY_PATTERNS: readonly RegExp[] = [
|
|
164
|
+
/** Matches names with honorific prefixes: Mr., Mrs., Ms., Dr., Prof. */
|
|
165
|
+
/\b(?:Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.)\s*[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*/g,
|
|
166
|
+
/** Matches two consecutive capitalized words (First Last name pattern) */
|
|
167
|
+
/\b[A-Z][a-z]+\s+[A-Z][a-z]+\b/g,
|
|
168
|
+
] as const
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Patterns for extracting place/location names from text.
|
|
172
|
+
*
|
|
173
|
+
* @example "based in San Francisco" - matches preposition + place pattern
|
|
174
|
+
* @example "Tokyo" - matches known major city
|
|
175
|
+
*/
|
|
176
|
+
const PLACE_ENTITY_PATTERNS: readonly RegExp[] = [
|
|
177
|
+
/** Matches locations after prepositions: in, at, from, to */
|
|
178
|
+
/\b(?:in|at|from|to)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/gi,
|
|
179
|
+
/** Matches known major cities (extensible list) */
|
|
180
|
+
/\b(?:New York|Los Angeles|San Francisco|London|Paris|Tokyo|Berlin)\b/gi,
|
|
181
|
+
] as const
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Patterns for extracting organization names from text.
|
|
185
|
+
*
|
|
186
|
+
* @example "Acme Corp." - matches corporate suffix
|
|
187
|
+
* @example "Google" - matches known tech company
|
|
188
|
+
*/
|
|
189
|
+
const ORGANIZATION_ENTITY_PATTERNS: readonly RegExp[] = [
|
|
190
|
+
/** Matches corporate suffixes: Inc., Corp., LLC, Ltd., Company, Organization */
|
|
191
|
+
/\b(?:Inc\.|Corp\.|LLC|Ltd\.|Company|Organization)\b/gi,
|
|
192
|
+
/** Matches known major tech companies (extensible list) */
|
|
193
|
+
/\b(?:Google|Microsoft|Apple|Amazon|Meta|OpenAI)\b/gi,
|
|
194
|
+
] as const
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Patterns for extracting dates from text.
|
|
198
|
+
*
|
|
199
|
+
* @example "12/25/2024" - matches numeric date format
|
|
200
|
+
* @example "December 25, 2024" - matches month name format
|
|
201
|
+
*/
|
|
202
|
+
const DATE_ENTITY_PATTERNS: readonly RegExp[] = [
|
|
203
|
+
/** Matches numeric date formats: MM/DD/YYYY, DD-MM-YY, etc. */
|
|
204
|
+
/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g,
|
|
205
|
+
/** Matches month name formats: January 15, 2024 or January 15 */
|
|
206
|
+
/\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}(?:,?\s+\d{4})?\b/gi,
|
|
207
|
+
] as const
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Combined entity extraction patterns.
|
|
211
|
+
* Maps each entity type to its corresponding regex patterns.
|
|
212
|
+
*/
|
|
213
|
+
const ENTITY_PATTERNS: Record<string, readonly RegExp[]> = {
|
|
214
|
+
person: PERSON_ENTITY_PATTERNS,
|
|
215
|
+
place: PLACE_ENTITY_PATTERNS,
|
|
216
|
+
organization: ORGANIZATION_ENTITY_PATTERNS,
|
|
217
|
+
date: DATE_ENTITY_PATTERNS,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Memory Service
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
export class MemoryService {
|
|
225
|
+
private repository: MemoryRepository
|
|
226
|
+
private config: MemoryServiceConfig
|
|
227
|
+
private llmProvider: LLMProvider | null = null
|
|
228
|
+
private useLLM: boolean
|
|
229
|
+
private useEmbeddingRelationships: boolean
|
|
230
|
+
private embeddingService: EmbeddingService | null = null
|
|
231
|
+
// Note: Removed redundant `this.memories` Map to avoid dual storage inconsistency.
|
|
232
|
+
// All storage operations now go through the repository only.
|
|
233
|
+
|
|
234
|
+
constructor(config: Partial<MemoryServiceConfig> = {}, repository?: MemoryRepository) {
|
|
235
|
+
this.config = { ...DEFAULT_MEMORY_CONFIG, ...config }
|
|
236
|
+
this.repository = repository ?? getMemoryRepository()
|
|
237
|
+
|
|
238
|
+
// Initialize LLM provider if available
|
|
239
|
+
this.useLLM = isLLMAvailable()
|
|
240
|
+
if (this.useLLM) {
|
|
241
|
+
try {
|
|
242
|
+
this.llmProvider = getLLMProvider()
|
|
243
|
+
logger.info('LLM provider initialized for memory extraction', {
|
|
244
|
+
provider: this.llmProvider.type,
|
|
245
|
+
})
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.warn('Failed to initialize LLM provider, falling back to regex', {
|
|
248
|
+
error: error instanceof Error ? error.message : String(error),
|
|
249
|
+
})
|
|
250
|
+
this.useLLM = false
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
logger.info('No LLM provider configured, using regex-based extraction')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.useEmbeddingRelationships = isEmbeddingRelationshipsEnabled()
|
|
257
|
+
if (this.useEmbeddingRelationships) {
|
|
258
|
+
this.embeddingService = getEmbeddingService()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
logger.debug('MemoryService initialized', {
|
|
262
|
+
config: this.config,
|
|
263
|
+
useLLM: this.useLLM,
|
|
264
|
+
useEmbeddings: this.useEmbeddingRelationships,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Core API Methods (as specified in requirements)
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extract discrete memories/facts from content
|
|
274
|
+
*
|
|
275
|
+
* Uses LLM-based extraction when available, with automatic fallback
|
|
276
|
+
* to regex-based extraction if LLM fails or is not configured.
|
|
277
|
+
*
|
|
278
|
+
* @param content - The text content to extract memories from
|
|
279
|
+
* @param options - Optional extraction options
|
|
280
|
+
* @returns Promise<Memory[]> - Array of extracted memories
|
|
281
|
+
* @throws ValidationError if content is empty or invalid
|
|
282
|
+
*/
|
|
283
|
+
async extractMemories(
|
|
284
|
+
content: string,
|
|
285
|
+
options: {
|
|
286
|
+
containerTag?: string
|
|
287
|
+
minConfidence?: number
|
|
288
|
+
maxMemories?: number
|
|
289
|
+
forceLLM?: boolean
|
|
290
|
+
forceRegex?: boolean
|
|
291
|
+
} = {}
|
|
292
|
+
): Promise<Memory[]> {
|
|
293
|
+
try {
|
|
294
|
+
validateMemoryContent(content)
|
|
295
|
+
if (options.containerTag !== undefined) {
|
|
296
|
+
validate(containerTagSchema, options.containerTag)
|
|
297
|
+
}
|
|
298
|
+
logger.debug('Extracting memories from content', {
|
|
299
|
+
contentLength: content.length,
|
|
300
|
+
useLLM: this.useLLM && !options.forceRegex,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Determine extraction method
|
|
304
|
+
const shouldUseLLM = !options.forceRegex && (options.forceLLM || (this.useLLM && this.llmProvider?.isAvailable()))
|
|
305
|
+
|
|
306
|
+
if (shouldUseLLM && this.llmProvider) {
|
|
307
|
+
try {
|
|
308
|
+
return await this.extractMemoriesWithLLM(content, options)
|
|
309
|
+
} catch (error) {
|
|
310
|
+
// Log and fallback to regex
|
|
311
|
+
logger.warn('LLM extraction failed, falling back to regex', {
|
|
312
|
+
error: error instanceof Error ? error.message : String(error),
|
|
313
|
+
isRetryable: error instanceof LLMError ? error.retryable : false,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fallback to regex-based extraction
|
|
319
|
+
return this.extractMemoriesWithRegex(content, options)
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error instanceof ValidationError) {
|
|
322
|
+
throw error
|
|
323
|
+
}
|
|
324
|
+
logger.errorWithException('Failed to extract memories', error)
|
|
325
|
+
throw AppError.from(error, ErrorCode.EXTRACTION_ERROR)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Extract memories using LLM provider
|
|
331
|
+
*/
|
|
332
|
+
private async extractMemoriesWithLLM(
|
|
333
|
+
content: string,
|
|
334
|
+
options: {
|
|
335
|
+
containerTag?: string
|
|
336
|
+
minConfidence?: number
|
|
337
|
+
maxMemories?: number
|
|
338
|
+
}
|
|
339
|
+
): Promise<Memory[]> {
|
|
340
|
+
if (!this.llmProvider) {
|
|
341
|
+
throw new AppError('LLM provider not available', ErrorCode.INTERNAL_ERROR)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const startTime = Date.now()
|
|
345
|
+
const result: LLMExtractionResult = await this.llmProvider.extractMemories(content, {
|
|
346
|
+
containerTag: options.containerTag ?? this.config.defaultContainerTag,
|
|
347
|
+
minConfidence: options.minConfidence ?? this.config.minConfidenceThreshold,
|
|
348
|
+
maxMemories: options.maxMemories,
|
|
349
|
+
extractEntities: true,
|
|
350
|
+
extractKeywords: true,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// Convert LLM results to Memory objects
|
|
354
|
+
const memories: Memory[] = result.memories.map((extracted) => ({
|
|
355
|
+
id: generateId(),
|
|
356
|
+
content: extracted.content,
|
|
357
|
+
type: extracted.type,
|
|
358
|
+
relationships: [],
|
|
359
|
+
isLatest: true,
|
|
360
|
+
containerTag: options.containerTag ?? this.config.defaultContainerTag,
|
|
361
|
+
sourceContent: content.substring(0, 500),
|
|
362
|
+
confidence: extracted.confidence,
|
|
363
|
+
metadata: {
|
|
364
|
+
confidence: extracted.confidence,
|
|
365
|
+
extractedFrom: content.substring(0, 100),
|
|
366
|
+
keywords: extracted.keywords,
|
|
367
|
+
entities: extracted.entities,
|
|
368
|
+
extractionMethod: 'llm',
|
|
369
|
+
classificationMethod: 'llm',
|
|
370
|
+
llmProvider: result.provider,
|
|
371
|
+
tokensUsed: result.tokensUsed?.total,
|
|
372
|
+
},
|
|
373
|
+
createdAt: new Date(),
|
|
374
|
+
updatedAt: new Date(),
|
|
375
|
+
}))
|
|
376
|
+
|
|
377
|
+
logger.info('Memories extracted with LLM', {
|
|
378
|
+
count: memories.length,
|
|
379
|
+
provider: result.provider,
|
|
380
|
+
cached: result.cached,
|
|
381
|
+
processingTimeMs: Date.now() - startTime,
|
|
382
|
+
tokensUsed: result.tokensUsed?.total,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
return memories
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Extract memories using regex-based patterns (fallback)
|
|
390
|
+
*/
|
|
391
|
+
private extractMemoriesWithRegex(
|
|
392
|
+
content: string,
|
|
393
|
+
options: {
|
|
394
|
+
containerTag?: string
|
|
395
|
+
minConfidence?: number
|
|
396
|
+
maxMemories?: number
|
|
397
|
+
}
|
|
398
|
+
): Memory[] {
|
|
399
|
+
const sentences = this.splitIntoSentences(content)
|
|
400
|
+
const memories: Memory[] = []
|
|
401
|
+
const maxMemories = options.maxMemories ?? 50
|
|
402
|
+
const minConfidence = options.minConfidence ?? this.config.minConfidenceThreshold
|
|
403
|
+
|
|
404
|
+
for (const sentence of sentences) {
|
|
405
|
+
if (memories.length >= maxMemories) break
|
|
406
|
+
if (sentence.trim().length < 10) continue
|
|
407
|
+
|
|
408
|
+
const type = this.classifyMemoryType(sentence)
|
|
409
|
+
const entities = this.extractEntities(sentence)
|
|
410
|
+
const keywords = this.extractKeywords(sentence)
|
|
411
|
+
const confidence = this.calculateConfidence(sentence, type)
|
|
412
|
+
|
|
413
|
+
if (confidence < minConfidence) continue
|
|
414
|
+
|
|
415
|
+
const memory: Memory = {
|
|
416
|
+
id: generateId(),
|
|
417
|
+
content: sentence.trim(),
|
|
418
|
+
type,
|
|
419
|
+
relationships: [],
|
|
420
|
+
isLatest: true,
|
|
421
|
+
containerTag: options.containerTag ?? this.config.defaultContainerTag,
|
|
422
|
+
sourceContent: content.substring(0, 500),
|
|
423
|
+
confidence,
|
|
424
|
+
metadata: {
|
|
425
|
+
confidence,
|
|
426
|
+
extractedFrom: content.substring(0, 100),
|
|
427
|
+
keywords,
|
|
428
|
+
entities,
|
|
429
|
+
extractionMethod: 'regex',
|
|
430
|
+
classificationMethod: 'heuristic',
|
|
431
|
+
},
|
|
432
|
+
createdAt: new Date(),
|
|
433
|
+
updatedAt: new Date(),
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
memories.push(memory)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
logger.info('Memories extracted with regex', { count: memories.length })
|
|
440
|
+
return memories
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Detect relationships between a new memory and existing memories
|
|
445
|
+
*
|
|
446
|
+
* Uses LLM-based detection when available, with automatic fallback
|
|
447
|
+
* to pattern-based detection if LLM fails or is not configured.
|
|
448
|
+
*
|
|
449
|
+
* @param newMemory - The new memory to check
|
|
450
|
+
* @param existingMemories - Array of existing memories to compare against
|
|
451
|
+
* @param options - Optional detection options
|
|
452
|
+
* @returns Promise<Relationship[]> - Array of detected relationships
|
|
453
|
+
*/
|
|
454
|
+
async detectRelationshipsAsync(
|
|
455
|
+
newMemory: Memory,
|
|
456
|
+
existingMemories: Memory[],
|
|
457
|
+
options: {
|
|
458
|
+
minConfidence?: number
|
|
459
|
+
maxRelationships?: number
|
|
460
|
+
forceLLM?: boolean
|
|
461
|
+
forceRegex?: boolean
|
|
462
|
+
} = {}
|
|
463
|
+
): Promise<Relationship[]> {
|
|
464
|
+
// Limit comparisons for performance
|
|
465
|
+
const memoriesToCompare = existingMemories.slice(0, this.config.maxRelationshipComparisons)
|
|
466
|
+
|
|
467
|
+
if (memoriesToCompare.length === 0) {
|
|
468
|
+
return []
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const shouldUseLLM = !options.forceRegex && (options.forceLLM || (this.useLLM && this.llmProvider?.isAvailable()))
|
|
472
|
+
|
|
473
|
+
if (shouldUseLLM && this.llmProvider) {
|
|
474
|
+
try {
|
|
475
|
+
return await this.detectRelationshipsWithLLM(newMemory, memoriesToCompare, options)
|
|
476
|
+
} catch (error) {
|
|
477
|
+
logger.warn('LLM relationship detection failed, falling back to patterns', {
|
|
478
|
+
error: error instanceof Error ? error.message : String(error),
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Fallback to pattern-based detection
|
|
484
|
+
return this.detectRelationships(newMemory, memoriesToCompare)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Detect relationships using LLM provider
|
|
489
|
+
*/
|
|
490
|
+
private async detectRelationshipsWithLLM(
|
|
491
|
+
newMemory: Memory,
|
|
492
|
+
existingMemories: Memory[],
|
|
493
|
+
options: {
|
|
494
|
+
minConfidence?: number
|
|
495
|
+
maxRelationships?: number
|
|
496
|
+
}
|
|
497
|
+
): Promise<Relationship[]> {
|
|
498
|
+
if (!this.llmProvider) {
|
|
499
|
+
throw new AppError('LLM provider not available', ErrorCode.INTERNAL_ERROR)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const result: LLMRelationshipResult = await this.llmProvider.detectRelationships(
|
|
503
|
+
{ id: newMemory.id, content: newMemory.content, type: newMemory.type },
|
|
504
|
+
existingMemories.map((m) => ({ id: m.id, content: m.content, type: m.type })),
|
|
505
|
+
{
|
|
506
|
+
minConfidence: options.minConfidence ?? 0.5,
|
|
507
|
+
maxRelationships: options.maxRelationships,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
// Convert LLM results to Relationship objects
|
|
512
|
+
const relationships: Relationship[] = result.relationships.map((rel) => ({
|
|
513
|
+
id: generateId(),
|
|
514
|
+
sourceMemoryId: rel.sourceMemoryId,
|
|
515
|
+
targetMemoryId: rel.targetMemoryId,
|
|
516
|
+
type: rel.type,
|
|
517
|
+
confidence: rel.confidence,
|
|
518
|
+
description: rel.reason,
|
|
519
|
+
createdAt: new Date(),
|
|
520
|
+
metadata: {
|
|
521
|
+
detectionMethod: 'llm',
|
|
522
|
+
llmProvider: result.provider,
|
|
523
|
+
},
|
|
524
|
+
}))
|
|
525
|
+
|
|
526
|
+
logger.info('Relationships detected with LLM', {
|
|
527
|
+
count: relationships.length,
|
|
528
|
+
supersededCount: result.supersededMemoryIds.length,
|
|
529
|
+
provider: result.provider,
|
|
530
|
+
processingTimeMs: result.processingTimeMs,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
return relationships
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Detect relationships using pattern-based heuristics (synchronous, for backwards compatibility)
|
|
538
|
+
*
|
|
539
|
+
* @param newMemory - The new memory to check
|
|
540
|
+
* @param existingMemories - Array of existing memories to compare against
|
|
541
|
+
* @returns Relationship[] - Array of detected relationships
|
|
542
|
+
*/
|
|
543
|
+
detectRelationships(newMemory: Memory, existingMemories: Memory[]): Relationship[] {
|
|
544
|
+
const relationships: Relationship[] = []
|
|
545
|
+
|
|
546
|
+
// Limit comparisons for performance
|
|
547
|
+
const memoriesToCompare = existingMemories.slice(0, this.config.maxRelationshipComparisons)
|
|
548
|
+
|
|
549
|
+
for (const existing of memoriesToCompare) {
|
|
550
|
+
if (existing.id === newMemory.id) continue
|
|
551
|
+
|
|
552
|
+
// Check for updates (new memory supersedes old)
|
|
553
|
+
const updateResult = this.checkForUpdates(newMemory, existing)
|
|
554
|
+
if (updateResult.isUpdate && updateResult.confidence >= 0.7) {
|
|
555
|
+
relationships.push({
|
|
556
|
+
id: generateId(),
|
|
557
|
+
sourceMemoryId: newMemory.id,
|
|
558
|
+
targetMemoryId: existing.id,
|
|
559
|
+
type: 'updates',
|
|
560
|
+
confidence: updateResult.confidence,
|
|
561
|
+
description: updateResult.reason,
|
|
562
|
+
createdAt: new Date(),
|
|
563
|
+
metadata: { detectionMethod: 'pattern' },
|
|
564
|
+
})
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check for extensions (new memory adds to old)
|
|
569
|
+
const extensionResult = this.checkForExtensions(newMemory, existing)
|
|
570
|
+
if (extensionResult.isExtension && extensionResult.confidence >= 0.6) {
|
|
571
|
+
relationships.push({
|
|
572
|
+
id: generateId(),
|
|
573
|
+
sourceMemoryId: newMemory.id,
|
|
574
|
+
targetMemoryId: existing.id,
|
|
575
|
+
type: 'extends',
|
|
576
|
+
confidence: extensionResult.confidence,
|
|
577
|
+
description: extensionResult.reason,
|
|
578
|
+
createdAt: new Date(),
|
|
579
|
+
metadata: { detectionMethod: 'pattern' },
|
|
580
|
+
})
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Check for general semantic relationship
|
|
585
|
+
const similarity = this.calculateTextSimilarity(newMemory.content, existing.content)
|
|
586
|
+
if (similarity >= 0.5) {
|
|
587
|
+
relationships.push({
|
|
588
|
+
id: generateId(),
|
|
589
|
+
sourceMemoryId: newMemory.id,
|
|
590
|
+
targetMemoryId: existing.id,
|
|
591
|
+
type: 'related',
|
|
592
|
+
confidence: similarity,
|
|
593
|
+
description: 'Semantically related content',
|
|
594
|
+
createdAt: new Date(),
|
|
595
|
+
metadata: { detectionMethod: 'pattern' },
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return relationships
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Detect relationships using the default path selection.
|
|
605
|
+
* Embedding detection is used only when explicitly enabled.
|
|
606
|
+
*/
|
|
607
|
+
private async detectRelationshipsForMemory(
|
|
608
|
+
newMemory: Memory,
|
|
609
|
+
existingMemories: Memory[],
|
|
610
|
+
containerTag?: string
|
|
611
|
+
): Promise<Relationship[]> {
|
|
612
|
+
if (!this.useEmbeddingRelationships || !this.embeddingService) {
|
|
613
|
+
return this.detectRelationships(newMemory, existingMemories)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (existingMemories.length === 0) {
|
|
617
|
+
return []
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const result = await detectRelationshipsWithEmbeddings(newMemory, existingMemories, this.embeddingService, {
|
|
622
|
+
containerTag,
|
|
623
|
+
config: {
|
|
624
|
+
maxCandidates: this.config.maxRelationshipComparisons,
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
return result.relationships.map((rel) => rel.relationship)
|
|
628
|
+
} catch (error) {
|
|
629
|
+
logger.warn('Embedding relationship detection failed, falling back to patterns', {
|
|
630
|
+
error: error instanceof Error ? error.message : String(error),
|
|
631
|
+
})
|
|
632
|
+
return this.detectRelationships(newMemory, existingMemories)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Classify the type of memory from content
|
|
638
|
+
*
|
|
639
|
+
* @param content - The content to classify
|
|
640
|
+
* @returns MemoryType - 'fact' | 'preference' | 'episode' (mapped to full type set)
|
|
641
|
+
*/
|
|
642
|
+
classifyMemoryType(content: string): MemoryType {
|
|
643
|
+
// Use LLM-based classification service with pattern matching fallback
|
|
644
|
+
// This replaces the TODO-001 implementation
|
|
645
|
+
// Note: This is synchronous for backward compatibility.
|
|
646
|
+
// For LLM async, call: await getMemoryClassifier().classify(content)
|
|
647
|
+
|
|
648
|
+
const heuristic = classifyMemoryTypeHeuristically(content)
|
|
649
|
+
return heuristic.type
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Classify memory type asynchronously using LLM (preferred method)
|
|
654
|
+
*
|
|
655
|
+
* @param content - The content to classify
|
|
656
|
+
* @returns Promise with MemoryType
|
|
657
|
+
*/
|
|
658
|
+
async classifyMemoryTypeAsync(content: string): Promise<MemoryType> {
|
|
659
|
+
const classifier = getMemoryClassifier()
|
|
660
|
+
const result = await classifier.classify(content)
|
|
661
|
+
return result.type
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Check if a new memory updates/supersedes an existing memory (contradiction check)
|
|
666
|
+
*
|
|
667
|
+
* @param newMemory - The new memory
|
|
668
|
+
* @param existing - The existing memory to compare
|
|
669
|
+
* @returns UpdateCheckResult
|
|
670
|
+
*/
|
|
671
|
+
checkForUpdates(newMemory: Memory, existing: Memory): UpdateCheckResult {
|
|
672
|
+
// Use heuristic fallback for synchronous calls
|
|
673
|
+
// For LLM-based detection, use checkForUpdatesAsync instead
|
|
674
|
+
const newLower = newMemory.content.toLowerCase()
|
|
675
|
+
const existingLower = existing.content.toLowerCase()
|
|
676
|
+
|
|
677
|
+
const newWords = new Set(newLower.split(/\s+/).filter((w) => w.length > 3))
|
|
678
|
+
const existingWords = new Set(existingLower.split(/\s+/).filter((w) => w.length > 3))
|
|
679
|
+
|
|
680
|
+
const intersection = new Set([...newWords].filter((x) => existingWords.has(x)))
|
|
681
|
+
const overlapRatio = intersection.size / Math.min(newWords.size, existingWords.size) || 0
|
|
682
|
+
|
|
683
|
+
let hasUpdateIndicator = false
|
|
684
|
+
for (const pattern of RELATIONSHIP_INDICATORS.updates) {
|
|
685
|
+
if (pattern.test(newLower)) {
|
|
686
|
+
hasUpdateIndicator = true
|
|
687
|
+
break
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let hasContradiction = false
|
|
692
|
+
for (const pattern of RELATIONSHIP_INDICATORS.contradicts) {
|
|
693
|
+
if (pattern.test(newLower) && overlapRatio > 0.3) {
|
|
694
|
+
hasContradiction = true
|
|
695
|
+
break
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
let hasSuperseding = false
|
|
700
|
+
for (const pattern of RELATIONSHIP_INDICATORS.supersedes) {
|
|
701
|
+
if (pattern.test(newLower) && overlapRatio > 0.4) {
|
|
702
|
+
hasSuperseding = true
|
|
703
|
+
break
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const isUpdate = (hasUpdateIndicator || hasContradiction || hasSuperseding) && overlapRatio > 0.3
|
|
708
|
+
const confidence = isUpdate ? Math.min(0.9, overlapRatio + 0.3) : 0
|
|
709
|
+
|
|
710
|
+
let reason = 'No update relationship detected'
|
|
711
|
+
if (isUpdate) {
|
|
712
|
+
if (hasContradiction) {
|
|
713
|
+
reason = 'New memory contradicts existing information'
|
|
714
|
+
} else if (hasSuperseding) {
|
|
715
|
+
reason = 'New memory supersedes existing information'
|
|
716
|
+
} else {
|
|
717
|
+
reason = 'New memory updates existing information'
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
isUpdate,
|
|
723
|
+
existingMemory: isUpdate ? existing : undefined,
|
|
724
|
+
confidence,
|
|
725
|
+
reason,
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Check for updates/contradictions asynchronously using LLM (preferred method)
|
|
731
|
+
* Replaces TODO-002 with semantic analysis
|
|
732
|
+
*
|
|
733
|
+
* @param newMemory - The new memory
|
|
734
|
+
* @param existing - The existing memory to compare
|
|
735
|
+
* @returns Promise with UpdateCheckResult
|
|
736
|
+
*/
|
|
737
|
+
async checkForUpdatesAsync(newMemory: Memory, existing: Memory): Promise<UpdateCheckResult> {
|
|
738
|
+
const detector = getContradictionDetector()
|
|
739
|
+
const result = await detector.checkContradiction(newMemory, existing)
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
isUpdate: result.isContradiction,
|
|
743
|
+
existingMemory: result.isContradiction ? existing : undefined,
|
|
744
|
+
confidence: result.confidence,
|
|
745
|
+
reason: result.reason,
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Check if a new memory extends/enriches an existing memory
|
|
751
|
+
*
|
|
752
|
+
* @param newMemory - The new memory
|
|
753
|
+
* @param existing - The existing memory to compare
|
|
754
|
+
* @returns ExtensionCheckResult
|
|
755
|
+
*/
|
|
756
|
+
checkForExtensions(newMemory: Memory, existing: Memory): ExtensionCheckResult {
|
|
757
|
+
// TODO: Replace with actual LLM call for extension detection
|
|
758
|
+
// Example LLM prompt:
|
|
759
|
+
// ```
|
|
760
|
+
// Compare these two statements and determine if the NEW statement
|
|
761
|
+
// extends or adds detail to the OLD statement (without contradicting):
|
|
762
|
+
//
|
|
763
|
+
// OLD: ${existing.content}
|
|
764
|
+
// NEW: ${newMemory.content}
|
|
765
|
+
//
|
|
766
|
+
// Return JSON: { isExtension: boolean, confidence: 0-1, reason: string }
|
|
767
|
+
// ```
|
|
768
|
+
|
|
769
|
+
const newLower = newMemory.content.toLowerCase()
|
|
770
|
+
const existingLower = existing.content.toLowerCase()
|
|
771
|
+
|
|
772
|
+
// Check for common subject matter
|
|
773
|
+
const newWords = newLower.split(/\s+/).filter((w) => w.length > 3)
|
|
774
|
+
const existingWords = new Set(existingLower.split(/\s+/).filter((w) => w.length > 3))
|
|
775
|
+
|
|
776
|
+
const commonWords = newWords.filter((w) => existingWords.has(w))
|
|
777
|
+
const overlapRatio = commonWords.length / Math.min(newWords.length, existingWords.size) || 0
|
|
778
|
+
|
|
779
|
+
// New memory should be longer or contain additional information
|
|
780
|
+
const hasMoreDetail = newMemory.content.length > existing.content.length * 0.8
|
|
781
|
+
|
|
782
|
+
// Extension indicators
|
|
783
|
+
let hasExtensionIndicator = false
|
|
784
|
+
for (const pattern of RELATIONSHIP_INDICATORS.extends) {
|
|
785
|
+
if (pattern.test(newLower)) {
|
|
786
|
+
hasExtensionIndicator = true
|
|
787
|
+
break
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check if new content is contained within old (not an extension)
|
|
792
|
+
const newContentInOld = existingLower.includes(newLower.slice(0, 20))
|
|
793
|
+
|
|
794
|
+
const isExtension =
|
|
795
|
+
overlapRatio > 0.2 && overlapRatio < 0.9 && !newContentInOld && (hasMoreDetail || hasExtensionIndicator)
|
|
796
|
+
|
|
797
|
+
const confidence = isExtension ? Math.min(0.85, overlapRatio + 0.2) : 0
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
isExtension,
|
|
801
|
+
existingMemory: isExtension ? existing : undefined,
|
|
802
|
+
confidence,
|
|
803
|
+
reason: isExtension
|
|
804
|
+
? 'New memory adds additional detail to existing information'
|
|
805
|
+
: 'No extension relationship detected',
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Check for extensions asynchronously using LLM (preferred method)
|
|
811
|
+
* Replaces TODO-003 with semantic analysis
|
|
812
|
+
*
|
|
813
|
+
* @param newMemory - The new memory
|
|
814
|
+
* @param existing - The existing memory to compare
|
|
815
|
+
* @returns Promise with ExtensionCheckResult
|
|
816
|
+
*/
|
|
817
|
+
async checkForExtensionsAsync(newMemory: Memory, existing: Memory): Promise<ExtensionCheckResult> {
|
|
818
|
+
const detector = getMemoryExtensionDetector()
|
|
819
|
+
const result = await detector.checkExtension(newMemory, existing)
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
isExtension: result.isExtension,
|
|
823
|
+
existingMemory: result.isExtension ? existing : undefined,
|
|
824
|
+
confidence: result.confidence,
|
|
825
|
+
reason: result.reason,
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// Extended API Methods
|
|
831
|
+
// ============================================================================
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Process content and store memories with automatic relationship detection
|
|
835
|
+
*
|
|
836
|
+
* @throws ValidationError if content or containerTag is invalid
|
|
837
|
+
*/
|
|
838
|
+
async processAndStoreMemories(
|
|
839
|
+
content: string,
|
|
840
|
+
options: {
|
|
841
|
+
containerTag?: string
|
|
842
|
+
sourceId?: string
|
|
843
|
+
detectRelationships?: boolean
|
|
844
|
+
} = {}
|
|
845
|
+
): Promise<{
|
|
846
|
+
memories: Memory[]
|
|
847
|
+
relationships: Relationship[]
|
|
848
|
+
supersededMemoryIds: string[]
|
|
849
|
+
}> {
|
|
850
|
+
const createdMemoryIds: string[] = []
|
|
851
|
+
const relationshipIdsToRollback: string[] = []
|
|
852
|
+
const supersedeSnapshots: Array<{
|
|
853
|
+
id: string
|
|
854
|
+
isLatest: boolean
|
|
855
|
+
supersededBy?: string
|
|
856
|
+
}> = []
|
|
857
|
+
|
|
858
|
+
const rollback = async (reason: unknown) => {
|
|
859
|
+
logger.warn('Rolling back processAndStoreMemories due to failure', {
|
|
860
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
for (const snapshot of supersedeSnapshots) {
|
|
864
|
+
try {
|
|
865
|
+
const existing = await this.repository.findById(snapshot.id)
|
|
866
|
+
if (existing) {
|
|
867
|
+
await this.repository.update(snapshot.id, {
|
|
868
|
+
isLatest: snapshot.isLatest,
|
|
869
|
+
supersededBy: snapshot.supersededBy,
|
|
870
|
+
})
|
|
871
|
+
}
|
|
872
|
+
} catch (error) {
|
|
873
|
+
logger.warn('Failed to rollback superseded memory', {
|
|
874
|
+
memoryId: snapshot.id,
|
|
875
|
+
error: error instanceof Error ? error.message : String(error),
|
|
876
|
+
})
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
for (const relId of relationshipIdsToRollback) {
|
|
881
|
+
try {
|
|
882
|
+
await this.repository.deleteRelationship(relId)
|
|
883
|
+
} catch (error) {
|
|
884
|
+
logger.warn('Failed to rollback relationship', {
|
|
885
|
+
relationshipId: relId,
|
|
886
|
+
error: error instanceof Error ? error.message : String(error),
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
for (const memoryId of createdMemoryIds) {
|
|
892
|
+
try {
|
|
893
|
+
await this.repository.delete(memoryId)
|
|
894
|
+
} catch (error) {
|
|
895
|
+
logger.warn('Failed to rollback memory', {
|
|
896
|
+
memoryId,
|
|
897
|
+
error: error instanceof Error ? error.message : String(error),
|
|
898
|
+
})
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
// Only use default containerTag if not explicitly provided (including undefined)
|
|
905
|
+
const containerTag = 'containerTag' in options ? options.containerTag : this.config.defaultContainerTag
|
|
906
|
+
|
|
907
|
+
if (containerTag) {
|
|
908
|
+
validate(containerTagSchema, containerTag)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const shouldDetectRelationships = options.detectRelationships ?? this.config.autoDetectRelationships
|
|
912
|
+
|
|
913
|
+
logger.debug('Processing and storing memories', {
|
|
914
|
+
containerTag,
|
|
915
|
+
detectRelationships: shouldDetectRelationships,
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
// Extract memories from content
|
|
919
|
+
const extractedMemories = await this.extractMemories(content)
|
|
920
|
+
|
|
921
|
+
// Update container tags and source info
|
|
922
|
+
for (const memory of extractedMemories) {
|
|
923
|
+
memory.containerTag = containerTag
|
|
924
|
+
if (options.sourceId) {
|
|
925
|
+
memory.sourceId = options.sourceId
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const allRelationships: Relationship[] = []
|
|
930
|
+
const supersededMemoryIds: string[] = []
|
|
931
|
+
|
|
932
|
+
// Process each extracted memory
|
|
933
|
+
for (const memory of extractedMemories) {
|
|
934
|
+
// Store the memory in repository only (no local cache)
|
|
935
|
+
await this.repository.create(memory)
|
|
936
|
+
createdMemoryIds.push(memory.id)
|
|
937
|
+
|
|
938
|
+
// Detect relationships if enabled
|
|
939
|
+
if (shouldDetectRelationships) {
|
|
940
|
+
const existingMemories = await this.repository.findPotentialRelations(memory, {
|
|
941
|
+
containerTag,
|
|
942
|
+
limit: this.config.maxRelationshipComparisons,
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
const relationships = await this.detectRelationshipsForMemory(memory, existingMemories, containerTag)
|
|
946
|
+
|
|
947
|
+
// Set relationship detection method in memory metadata
|
|
948
|
+
const relationshipMethod = this.useEmbeddingRelationships && this.embeddingService ? 'embedding' : 'heuristic'
|
|
949
|
+
memory.metadata.relationshipMethod = relationshipMethod
|
|
950
|
+
|
|
951
|
+
const existingById = new Map(existingMemories.map((m) => [m.id, m]))
|
|
952
|
+
|
|
953
|
+
// Process update relationships - mark old memories as superseded
|
|
954
|
+
for (const rel of relationships) {
|
|
955
|
+
if (rel.type === 'updates' || rel.type === 'supersedes') {
|
|
956
|
+
const target = existingById.get(rel.targetMemoryId)
|
|
957
|
+
if (target && memory.containerTag && target.containerTag && memory.containerTag !== target.containerTag) {
|
|
958
|
+
continue
|
|
959
|
+
}
|
|
960
|
+
if (target) {
|
|
961
|
+
supersedeSnapshots.push({
|
|
962
|
+
id: target.id,
|
|
963
|
+
isLatest: target.isLatest,
|
|
964
|
+
supersededBy: target.supersededBy,
|
|
965
|
+
})
|
|
966
|
+
}
|
|
967
|
+
await this.repository.markSuperseded(rel.targetMemoryId, memory.id)
|
|
968
|
+
supersededMemoryIds.push(rel.targetMemoryId)
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Store relationships
|
|
973
|
+
if (relationships.length > 0) {
|
|
974
|
+
relationshipIdsToRollback.push(...relationships.map((rel) => rel.id))
|
|
975
|
+
await this.repository.createRelationshipBatch(relationships)
|
|
976
|
+
allRelationships.push(...relationships)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
logger.info('Memories processed and stored', {
|
|
982
|
+
memoriesCount: extractedMemories.length,
|
|
983
|
+
relationshipsCount: allRelationships.length,
|
|
984
|
+
supersededCount: supersededMemoryIds.length,
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
memories: extractedMemories,
|
|
989
|
+
relationships: allRelationships,
|
|
990
|
+
supersededMemoryIds,
|
|
991
|
+
}
|
|
992
|
+
} catch (error) {
|
|
993
|
+
await rollback(error)
|
|
994
|
+
if (error instanceof AppError) {
|
|
995
|
+
throw error
|
|
996
|
+
}
|
|
997
|
+
logger.errorWithException('Failed to process and store memories', error)
|
|
998
|
+
throw AppError.from(error, ErrorCode.INTERNAL_ERROR)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Update isLatest status when new memory supersedes existing ones
|
|
1004
|
+
*/
|
|
1005
|
+
updateIsLatest(newMemory: Memory, existingMemories: Memory[]): void {
|
|
1006
|
+
for (const existing of existingMemories) {
|
|
1007
|
+
if (newMemory.containerTag && existing.containerTag && newMemory.containerTag !== existing.containerTag) {
|
|
1008
|
+
continue
|
|
1009
|
+
}
|
|
1010
|
+
const updateResult = this.checkForUpdates(newMemory, existing)
|
|
1011
|
+
if (updateResult.isUpdate && updateResult.confidence >= 0.7) {
|
|
1012
|
+
existing.isLatest = false
|
|
1013
|
+
existing.supersededBy = newMemory.id
|
|
1014
|
+
newMemory.relationships.push({
|
|
1015
|
+
type: 'supersedes',
|
|
1016
|
+
targetId: existing.id,
|
|
1017
|
+
confidence: updateResult.confidence,
|
|
1018
|
+
})
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Extract memories from text (convenience wrapper matching original API)
|
|
1025
|
+
*
|
|
1026
|
+
* @throws ValidationError if text or containerTag is invalid
|
|
1027
|
+
*/
|
|
1028
|
+
extractMemoriesFromText(text: string, containerTag?: string): Memory[] {
|
|
1029
|
+
validateMemoryContent(text)
|
|
1030
|
+
if (containerTag) {
|
|
1031
|
+
validate(containerTagSchema, containerTag)
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const sentences = this.splitIntoSentences(text)
|
|
1035
|
+
const memories: Memory[] = []
|
|
1036
|
+
|
|
1037
|
+
for (const sentence of sentences) {
|
|
1038
|
+
if (sentence.trim().length < 10) continue
|
|
1039
|
+
|
|
1040
|
+
const type = this.classifyMemoryType(sentence)
|
|
1041
|
+
const entities = this.extractEntities(sentence)
|
|
1042
|
+
const keywords = this.extractKeywords(sentence)
|
|
1043
|
+
const confidence = this.calculateConfidence(sentence, type)
|
|
1044
|
+
|
|
1045
|
+
const memory: Memory = {
|
|
1046
|
+
id: generateId(),
|
|
1047
|
+
content: sentence.trim(),
|
|
1048
|
+
type,
|
|
1049
|
+
relationships: [],
|
|
1050
|
+
isLatest: true,
|
|
1051
|
+
containerTag: containerTag ?? this.config.defaultContainerTag,
|
|
1052
|
+
confidence,
|
|
1053
|
+
metadata: {
|
|
1054
|
+
confidence,
|
|
1055
|
+
extractedFrom: text.substring(0, 100),
|
|
1056
|
+
keywords,
|
|
1057
|
+
entities,
|
|
1058
|
+
},
|
|
1059
|
+
createdAt: new Date(),
|
|
1060
|
+
updatedAt: new Date(),
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
memories.push(memory)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Detect relationships between extracted memories
|
|
1067
|
+
this.detectRelationshipsInternal(memories)
|
|
1068
|
+
|
|
1069
|
+
return memories
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ============================================================================
|
|
1073
|
+
// Storage Methods (delegating to repository)
|
|
1074
|
+
// ============================================================================
|
|
1075
|
+
|
|
1076
|
+
async storeMemory(memory: Memory): Promise<Memory> {
|
|
1077
|
+
return this.repository.create(memory)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async getMemory(id: string): Promise<Memory | null> {
|
|
1081
|
+
return this.repository.findById(id)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async getAllMemories(): Promise<Memory[]> {
|
|
1085
|
+
return this.repository.getAllMemories()
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async getLatestMemories(): Promise<Memory[]> {
|
|
1089
|
+
const all = await this.repository.getAllMemories()
|
|
1090
|
+
return all.filter((m) => m.isLatest)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ============================================================================
|
|
1094
|
+
// Private Helper Methods
|
|
1095
|
+
// ============================================================================
|
|
1096
|
+
|
|
1097
|
+
private splitIntoSentences(text: string): string[] {
|
|
1098
|
+
return text.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 0)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private extractEntities(text: string): Entity[] {
|
|
1102
|
+
const entities: Entity[] = []
|
|
1103
|
+
const seen = new Set<string>()
|
|
1104
|
+
|
|
1105
|
+
for (const [type, patterns] of Object.entries(ENTITY_PATTERNS)) {
|
|
1106
|
+
for (const pattern of patterns) {
|
|
1107
|
+
const matches = text.matchAll(pattern)
|
|
1108
|
+
for (const match of matches) {
|
|
1109
|
+
const name = match[1] || match[0]
|
|
1110
|
+
const normalizedName = name.trim().toLowerCase()
|
|
1111
|
+
|
|
1112
|
+
if (!seen.has(normalizedName) && name.length > 1) {
|
|
1113
|
+
seen.add(normalizedName)
|
|
1114
|
+
entities.push({
|
|
1115
|
+
name: name.trim(),
|
|
1116
|
+
type: type as Entity['type'],
|
|
1117
|
+
mentions: 1,
|
|
1118
|
+
})
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return entities
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private extractKeywords(text: string): string[] {
|
|
1128
|
+
const stopWords = new Set([
|
|
1129
|
+
'the',
|
|
1130
|
+
'a',
|
|
1131
|
+
'an',
|
|
1132
|
+
'and',
|
|
1133
|
+
'or',
|
|
1134
|
+
'but',
|
|
1135
|
+
'in',
|
|
1136
|
+
'on',
|
|
1137
|
+
'at',
|
|
1138
|
+
'to',
|
|
1139
|
+
'for',
|
|
1140
|
+
'of',
|
|
1141
|
+
'with',
|
|
1142
|
+
'by',
|
|
1143
|
+
'from',
|
|
1144
|
+
'as',
|
|
1145
|
+
'is',
|
|
1146
|
+
'was',
|
|
1147
|
+
'are',
|
|
1148
|
+
'were',
|
|
1149
|
+
'been',
|
|
1150
|
+
'be',
|
|
1151
|
+
'have',
|
|
1152
|
+
'has',
|
|
1153
|
+
'had',
|
|
1154
|
+
'do',
|
|
1155
|
+
'does',
|
|
1156
|
+
'did',
|
|
1157
|
+
'will',
|
|
1158
|
+
'would',
|
|
1159
|
+
'could',
|
|
1160
|
+
'should',
|
|
1161
|
+
'may',
|
|
1162
|
+
'might',
|
|
1163
|
+
'must',
|
|
1164
|
+
'shall',
|
|
1165
|
+
'can',
|
|
1166
|
+
'need',
|
|
1167
|
+
'it',
|
|
1168
|
+
'this',
|
|
1169
|
+
'that',
|
|
1170
|
+
'these',
|
|
1171
|
+
'those',
|
|
1172
|
+
'i',
|
|
1173
|
+
'you',
|
|
1174
|
+
'he',
|
|
1175
|
+
'she',
|
|
1176
|
+
'we',
|
|
1177
|
+
'they',
|
|
1178
|
+
'my',
|
|
1179
|
+
'your',
|
|
1180
|
+
'his',
|
|
1181
|
+
'her',
|
|
1182
|
+
'our',
|
|
1183
|
+
'their',
|
|
1184
|
+
'its',
|
|
1185
|
+
])
|
|
1186
|
+
|
|
1187
|
+
const words = text.toLowerCase().match(/\b[a-z]{3,}\b/g) || []
|
|
1188
|
+
const keywords = words.filter((word) => !stopWords.has(word))
|
|
1189
|
+
|
|
1190
|
+
return [...new Set(keywords)].slice(0, 10)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
private calculateConfidence(content: string, type: MemoryType): number {
|
|
1194
|
+
let confidence = 0.5
|
|
1195
|
+
|
|
1196
|
+
// Longer content with more detail = higher confidence
|
|
1197
|
+
if (content.length > 100) confidence += 0.1
|
|
1198
|
+
if (content.length > 200) confidence += 0.1
|
|
1199
|
+
|
|
1200
|
+
// Pattern matches increase confidence
|
|
1201
|
+
const matchCount = countMemoryTypeMatches(content, type)
|
|
1202
|
+
confidence += Math.min(matchCount * 0.1, 0.2)
|
|
1203
|
+
|
|
1204
|
+
return Math.min(confidence, 1)
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private calculateTextSimilarity(text1: string, text2: string): number {
|
|
1208
|
+
const words1 = new Set(text1.toLowerCase().split(/\s+/))
|
|
1209
|
+
const words2 = new Set(text2.toLowerCase().split(/\s+/))
|
|
1210
|
+
|
|
1211
|
+
const intersection = new Set([...words1].filter((x) => words2.has(x)))
|
|
1212
|
+
const union = new Set([...words1, ...words2])
|
|
1213
|
+
|
|
1214
|
+
if (union.size === 0) return 0
|
|
1215
|
+
return intersection.size / union.size
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
private detectRelationshipsInternal(memories: Memory[]): MemoryRelationship[] {
|
|
1219
|
+
const relationships: MemoryRelationship[] = []
|
|
1220
|
+
|
|
1221
|
+
for (let i = 0; i < memories.length; i++) {
|
|
1222
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
1223
|
+
const sourceMemory = memories[i]!
|
|
1224
|
+
const targetMemory = memories[j]!
|
|
1225
|
+
|
|
1226
|
+
const relationshipType = this.detectRelationshipType(sourceMemory.content, targetMemory.content)
|
|
1227
|
+
|
|
1228
|
+
if (relationshipType) {
|
|
1229
|
+
const relationship: MemoryRelationship = {
|
|
1230
|
+
type: relationshipType,
|
|
1231
|
+
targetId: targetMemory.id,
|
|
1232
|
+
confidence: this.calculateRelationshipConfidence(sourceMemory, targetMemory, relationshipType),
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
sourceMemory.relationships.push(relationship)
|
|
1236
|
+
relationships.push(relationship)
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return relationships
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private detectRelationshipType(source: string, target: string): RelationshipType | null {
|
|
1245
|
+
const similarity = this.calculateTextSimilarity(source, target)
|
|
1246
|
+
if (similarity < 0.1) {
|
|
1247
|
+
return null
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Check explicit relationship indicators
|
|
1251
|
+
for (const [type, patterns] of Object.entries(RELATIONSHIP_INDICATORS)) {
|
|
1252
|
+
for (const pattern of patterns) {
|
|
1253
|
+
if (pattern.test(source) || pattern.test(target)) {
|
|
1254
|
+
return type as RelationshipType
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// If similar but no explicit indicator, mark as related
|
|
1260
|
+
if (similarity > 0.3) {
|
|
1261
|
+
return 'related'
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
return null
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
private calculateRelationshipConfidence(source: Memory, target: Memory, type: RelationshipType): number {
|
|
1268
|
+
let confidence = 0.5
|
|
1269
|
+
|
|
1270
|
+
// Same container increases confidence
|
|
1271
|
+
if (source.containerTag && source.containerTag === target.containerTag) {
|
|
1272
|
+
confidence += 0.1
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Text similarity affects confidence
|
|
1276
|
+
const similarity = this.calculateTextSimilarity(source.content, target.content)
|
|
1277
|
+
confidence += similarity * 0.3
|
|
1278
|
+
|
|
1279
|
+
// Explicit indicators increase confidence
|
|
1280
|
+
const patterns = RELATIONSHIP_INDICATORS[type]
|
|
1281
|
+
if (patterns) {
|
|
1282
|
+
for (const pattern of patterns) {
|
|
1283
|
+
if (pattern.test(source.content) || pattern.test(target.content)) {
|
|
1284
|
+
confidence += 0.1
|
|
1285
|
+
break
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return Math.min(confidence, 1)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ============================================================================
|
|
1295
|
+
// Factory Functions (Proxy-based Lazy Singleton)
|
|
1296
|
+
// ============================================================================
|
|
1297
|
+
|
|
1298
|
+
let _serviceInstance: MemoryService | null = null
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Get the singleton MemoryService instance (created lazily)
|
|
1302
|
+
*
|
|
1303
|
+
* Note: Config is only applied on first call. Subsequent calls
|
|
1304
|
+
* return the existing instance regardless of config parameter.
|
|
1305
|
+
* Use createMemoryService() if you need a fresh instance with specific config.
|
|
1306
|
+
*/
|
|
1307
|
+
export function getMemoryService(config?: Partial<MemoryServiceConfig>): MemoryService {
|
|
1308
|
+
if (!_serviceInstance) {
|
|
1309
|
+
_serviceInstance = new MemoryService(config)
|
|
1310
|
+
}
|
|
1311
|
+
return _serviceInstance
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Reset the singleton instance (useful for testing)
|
|
1316
|
+
*/
|
|
1317
|
+
export function resetMemoryService(): void {
|
|
1318
|
+
_serviceInstance = null
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Create a new MemoryService instance (for testing or custom configs)
|
|
1323
|
+
*/
|
|
1324
|
+
export function createMemoryService(
|
|
1325
|
+
config?: Partial<MemoryServiceConfig>,
|
|
1326
|
+
repository?: MemoryRepository
|
|
1327
|
+
): MemoryService {
|
|
1328
|
+
return new MemoryService(config, repository)
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Proxy-based lazy singleton for backwards compatibility
|
|
1333
|
+
*/
|
|
1334
|
+
export const memoryService = new Proxy({} as MemoryService, {
|
|
1335
|
+
get(_, prop) {
|
|
1336
|
+
return getMemoryService()[prop as keyof MemoryService]
|
|
1337
|
+
},
|
|
1338
|
+
})
|