memento-mcp-server 1.14.0 → 1.15.0-c

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.
Files changed (77) hide show
  1. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.d.ts +18 -0
  2. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.d.ts.map +1 -0
  3. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js +346 -0
  4. package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js.map +1 -0
  5. package/dist/domains/memory/tools/remember-tool.d.ts.map +1 -1
  6. package/dist/domains/memory/tools/remember-tool.js +182 -1
  7. package/dist/domains/memory/tools/remember-tool.js.map +1 -1
  8. package/dist/infrastructure/database/database/migration/migrations/005-relation-engine-schema.sql +7 -7
  9. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.d.ts +65 -0
  10. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.d.ts.map +1 -0
  11. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.js +250 -0
  12. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.js.map +1 -0
  13. package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.sql +86 -0
  14. package/dist/infrastructure/logging/triple-extraction-logger.d.ts +92 -0
  15. package/dist/infrastructure/logging/triple-extraction-logger.d.ts.map +1 -0
  16. package/dist/infrastructure/logging/triple-extraction-logger.js +194 -0
  17. package/dist/infrastructure/logging/triple-extraction-logger.js.map +1 -0
  18. package/dist/infrastructure/scheduler/batch-scheduler.d.ts +57 -0
  19. package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
  20. package/dist/infrastructure/scheduler/batch-scheduler.js +220 -0
  21. package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
  22. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.d.ts +194 -0
  23. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.d.ts.map +1 -0
  24. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.js +639 -0
  25. package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.js.map +1 -0
  26. package/dist/services/semantic-memory/semantic-memory-statistics.d.ts +64 -0
  27. package/dist/services/semantic-memory/semantic-memory-statistics.d.ts.map +1 -0
  28. package/dist/services/semantic-memory/semantic-memory-statistics.js +113 -0
  29. package/dist/services/semantic-memory/semantic-memory-statistics.js.map +1 -0
  30. package/dist/services/semantic-memory/semantic-memory-update-service.d.ts +257 -0
  31. package/dist/services/semantic-memory/semantic-memory-update-service.d.ts.map +1 -0
  32. package/dist/services/semantic-memory/semantic-memory-update-service.js +696 -0
  33. package/dist/services/semantic-memory/semantic-memory-update-service.js.map +1 -0
  34. package/dist/services/triple-extraction/entity-linker.d.ts +55 -0
  35. package/dist/services/triple-extraction/entity-linker.d.ts.map +1 -0
  36. package/dist/services/triple-extraction/entity-linker.js +154 -0
  37. package/dist/services/triple-extraction/entity-linker.js.map +1 -0
  38. package/dist/services/triple-extraction/predicate-canonicalizer.d.ts +63 -0
  39. package/dist/services/triple-extraction/predicate-canonicalizer.d.ts.map +1 -0
  40. package/dist/services/triple-extraction/predicate-canonicalizer.js +166 -0
  41. package/dist/services/triple-extraction/predicate-canonicalizer.js.map +1 -0
  42. package/dist/services/triple-extraction/triple-extraction-service.d.ts +181 -0
  43. package/dist/services/triple-extraction/triple-extraction-service.d.ts.map +1 -0
  44. package/dist/services/triple-extraction/triple-extraction-service.js +907 -0
  45. package/dist/services/triple-extraction/triple-extraction-service.js.map +1 -0
  46. package/dist/services/triple-extraction/triple-extraction-statistics.d.ts +74 -0
  47. package/dist/services/triple-extraction/triple-extraction-statistics.d.ts.map +1 -0
  48. package/dist/services/triple-extraction/triple-extraction-statistics.js +146 -0
  49. package/dist/services/triple-extraction/triple-extraction-statistics.js.map +1 -0
  50. package/dist/shared/types/index.d.ts +1 -0
  51. package/dist/shared/types/index.d.ts.map +1 -1
  52. package/dist/shared/types/index.js.map +1 -1
  53. package/dist/shared/types/triple-extraction.d.ts +99 -0
  54. package/dist/shared/types/triple-extraction.d.ts.map +1 -0
  55. package/dist/shared/types/triple-extraction.js +6 -0
  56. package/dist/shared/types/triple-extraction.js.map +1 -0
  57. package/dist/shared/utils/pii-masker.d.ts +67 -0
  58. package/dist/shared/utils/pii-masker.d.ts.map +1 -0
  59. package/dist/shared/utils/pii-masker.js +205 -0
  60. package/dist/shared/utils/pii-masker.js.map +1 -0
  61. package/dist/shared/utils/prompt-template-loader.d.ts +42 -0
  62. package/dist/shared/utils/prompt-template-loader.d.ts.map +1 -0
  63. package/dist/shared/utils/prompt-template-loader.js +92 -0
  64. package/dist/shared/utils/prompt-template-loader.js.map +1 -0
  65. package/dist/shared/utils/triple-cache.d.ts +90 -0
  66. package/dist/shared/utils/triple-cache.d.ts.map +1 -0
  67. package/dist/shared/utils/triple-cache.js +124 -0
  68. package/dist/shared/utils/triple-cache.js.map +1 -0
  69. package/dist/tools/index.d.ts +2 -1
  70. package/dist/tools/index.d.ts.map +1 -1
  71. package/dist/tools/index.js +3 -1
  72. package/dist/tools/index.js.map +1 -1
  73. package/dist/tools/types.d.ts +1 -0
  74. package/dist/tools/types.d.ts.map +1 -1
  75. package/dist/tools/types.js +2 -0
  76. package/dist/tools/types.js.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,696 @@
1
+ /**
2
+ * Semantic Memory 갱신 서비스
3
+ *
4
+ * Triple 추출 결과를 기반으로 Semantic Memory를 생성하거나 업데이트합니다.
5
+ *
6
+ * 주요 기능:
7
+ * - Triple 기반 Semantic Memory 생성/업데이트
8
+ * - 중복 판단 및 병합
9
+ * - Confidence 계산 (구조적 검증 기반)
10
+ * - 중요도 계산
11
+ */
12
+ import Database from 'better-sqlite3';
13
+ import { DatabaseUtils } from '../../shared/utils/database.js';
14
+ import { RelationGraph } from '../../domains/relation/services/relation-graph.js';
15
+ import { UnifiedEmbeddingService } from '../../domains/embedding/services/unified-embedding-service.js';
16
+ import { PredicateCanonicalizer } from '../triple-extraction/predicate-canonicalizer.js';
17
+ import { EntityLinker } from '../triple-extraction/entity-linker.js';
18
+ import { logger } from '../../shared/utils/logger.js';
19
+ /**
20
+ * Memory ID 생성 유틸리티
21
+ */
22
+ function generateId() {
23
+ const timestamp = Date.now();
24
+ const random = Math.random().toString(36).substring(2, 11);
25
+ return `mem_${timestamp}_${random}`;
26
+ }
27
+ import { SemanticMemoryStatisticsService } from './semantic-memory-statistics.js';
28
+ /**
29
+ * Semantic Memory 갱신 서비스
30
+ */
31
+ export class SemanticMemoryUpdateService {
32
+ db;
33
+ canonicalizer;
34
+ entityLinker;
35
+ embeddingService;
36
+ relationGraph;
37
+ statistics; // PRD 8.2: Semantic Memory 생성 통계 수집
38
+ // 기본 설정
39
+ /**
40
+ * Confidence 임계값 기본값 (PRD 2.4 참고)
41
+ * 신뢰도가 이 값 이상인 경우만 Semantic Memory 생성
42
+ */
43
+ DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
44
+ /**
45
+ * Subject/Object 유사도 임계값 기본값
46
+ * 중복 판단 시 사용되는 유사도 임계값
47
+ */
48
+ DEFAULT_SIMILARITY_THRESHOLD = 0.9;
49
+ constructor(db, embeddingService, relationGraph) {
50
+ this.db = db;
51
+ this.canonicalizer = new PredicateCanonicalizer();
52
+ this.entityLinker = new EntityLinker();
53
+ this.embeddingService = embeddingService || new UnifiedEmbeddingService();
54
+ this.relationGraph = relationGraph || new RelationGraph(db);
55
+ // PRD 8.2: Semantic Memory 생성 통계 수집
56
+ this.statistics = new SemanticMemoryStatisticsService();
57
+ // 런타임 relation_type_registry 등록 (없을 경우 자동 등록)
58
+ this.ensureRelationTypes();
59
+ }
60
+ /**
61
+ * 런타임 relation_type_registry 등록
62
+ *
63
+ * extracted_from와 supported_by 관계 타입이 없을 경우 자동 등록합니다.
64
+ * 마이그레이션에서 초기 데이터 삽입은 처리되지만, 런타임에 없을 경우를 대비합니다.
65
+ */
66
+ ensureRelationTypes() {
67
+ try {
68
+ // extracted_from 관계 타입 확인 및 등록
69
+ const extractedFrom = DatabaseUtils.get(this.db, `
70
+ SELECT type_name FROM relation_type_registry WHERE type_name = ?
71
+ `, ['extracted_from']);
72
+ if (!extractedFrom) {
73
+ DatabaseUtils.run(this.db, `
74
+ INSERT INTO relation_type_registry (type_name, category, description, applicable_types, default_confidence, search_boost)
75
+ VALUES (?, ?, ?, ?, ?, ?)
76
+ `, [
77
+ 'extracted_from',
78
+ 'Structural',
79
+ '추출 관계: Semantic Memory가 Episodic Memory에서 추출됨',
80
+ JSON.stringify(['episodic', 'semantic']),
81
+ 0.7,
82
+ 1.1
83
+ ]);
84
+ logger.debug('SemanticMemoryUpdateService: extracted_from 관계 타입 등록');
85
+ }
86
+ // supported_by 관계 타입 확인 및 등록
87
+ const supportedBy = DatabaseUtils.get(this.db, `
88
+ SELECT type_name FROM relation_type_registry WHERE type_name = ?
89
+ `, ['supported_by']);
90
+ if (!supportedBy) {
91
+ DatabaseUtils.run(this.db, `
92
+ INSERT INTO relation_type_registry (type_name, category, description, applicable_types, default_confidence, search_boost)
93
+ VALUES (?, ?, ?, ?, ?, ?)
94
+ `, [
95
+ 'supported_by',
96
+ 'Structural',
97
+ '근거 관계: Semantic Memory가 Episodic Memory에 의해 근거를 가짐',
98
+ JSON.stringify(['episodic', 'semantic']),
99
+ 0.7,
100
+ 1.1
101
+ ]);
102
+ logger.debug('SemanticMemoryUpdateService: supported_by 관계 타입 등록');
103
+ }
104
+ }
105
+ catch (error) {
106
+ // relation_type_registry 테이블이 없을 수 있으므로 에러는 무시
107
+ logger.warn('SemanticMemoryUpdateService: relation_type_registry 등록 실패', {
108
+ error: error instanceof Error ? error.message : String(error)
109
+ });
110
+ }
111
+ }
112
+ /**
113
+ * Triple 배열을 기반으로 Semantic Memory를 생성하거나 업데이트합니다.
114
+ *
115
+ * @param extractionResult Triple 추출 결과
116
+ * @param options 업데이트 옵션
117
+ * @returns 업데이트 결과
118
+ */
119
+ async updateSemanticMemory(extractionResult, options) {
120
+ const processingStartTime = Date.now(); // PRD 8.2: 처리 시간 측정 시작
121
+ const result = {
122
+ created: 0,
123
+ updated: 0,
124
+ skipped: 0,
125
+ semanticMemoryIds: []
126
+ };
127
+ // Triple이 없으면 건너뛰기
128
+ if (extractionResult.triples.length === 0) {
129
+ return result;
130
+ }
131
+ const confidenceThreshold = options.confidenceThreshold ?? this.DEFAULT_CONFIDENCE_THRESHOLD;
132
+ const similarityThreshold = options.similarityThreshold ?? this.DEFAULT_SIMILARITY_THRESHOLD;
133
+ const confidences = []; // PRD 8.2: Confidence 통계 수집
134
+ let hasError = false; // PRD 8.2: 에러 발생 여부 추적
135
+ // 각 Triple에 대해 Semantic Memory 생성/업데이트
136
+ for (const triple of extractionResult.triples) {
137
+ try {
138
+ // Confidence 계산 (구조적 검증 기반, PRD 2.4 참고)
139
+ const confidence = this.calculateConfidence(triple, extractionResult.extractionInfo);
140
+ // PRD 8.2: Confidence 통계 수집
141
+ confidences.push(confidence);
142
+ // PRD 2.4: 신뢰도가 일정 수준 이상인 경우만 Semantic Memory 생성
143
+ // Confidence 임계값 필터링 (기본값: 0.7, 설정 가능)
144
+ if (confidence < confidenceThreshold) {
145
+ result.skipped++;
146
+ logger.debug('SemanticMemoryUpdateService: Confidence가 임계값 미만', {
147
+ triple,
148
+ confidence,
149
+ threshold: confidenceThreshold,
150
+ reason: 'confidence_below_threshold'
151
+ });
152
+ continue;
153
+ }
154
+ // 중복 Semantic Memory 검색 (PRD 2.2 참고)
155
+ const duplicate = await this.findDuplicateSemanticMemory(triple, similarityThreshold);
156
+ if (duplicate) {
157
+ // PRD 2.3: 유사도가 임계값 이상인 경우 새로운 항목을 생성하지 않고 기존 항목 업데이트 (병합)
158
+ // 완전히 동일한 Semantic Memory는 생성하지 않음
159
+ await this.updateExistingSemanticMemory(duplicate.id, triple, options, confidence);
160
+ result.updated++;
161
+ result.semanticMemoryIds.push(duplicate.id);
162
+ }
163
+ else {
164
+ // 새로운 Semantic Memory 생성 (중복이 없는 경우)
165
+ const semanticMemoryId = await this.createSemanticMemory(triple, options, confidence);
166
+ result.created++;
167
+ result.semanticMemoryIds.push(semanticMemoryId);
168
+ }
169
+ // Episodic-Edge 관계 생성 (각 triple마다 별도 relation)
170
+ // 관계 방향 검증은 createEpisodicEdge 내부에서 수행되지만,
171
+ // 방향 검증 실패는 상위로 전파되어야 함
172
+ try {
173
+ const semanticMemoryId = duplicate?.id || result.semanticMemoryIds[result.semanticMemoryIds.length - 1];
174
+ if (!semanticMemoryId) {
175
+ throw new Error('Semantic memory ID is required for creating episodic edge');
176
+ }
177
+ await this.createEpisodicEdge(options.episodicMemoryId, semanticMemoryId, triple, extractionResult.extractionInfo, confidence);
178
+ }
179
+ catch (edgeError) {
180
+ // 관계 방향 검증 실패는 상위로 전파
181
+ if (edgeError instanceof Error && edgeError.message.includes('관계 방향 오류')) {
182
+ throw edgeError;
183
+ }
184
+ // 기타 관계 생성 실패는 무시 (Semantic Memory 생성에는 영향 없음)
185
+ logger.warn('SemanticMemoryUpdateService: 관계 생성 실패 (무시)', {
186
+ error: edgeError instanceof Error ? edgeError.message : String(edgeError),
187
+ triple
188
+ });
189
+ }
190
+ }
191
+ catch (error) {
192
+ // PRD 8.2: 에러 통계 수집
193
+ hasError = true;
194
+ // 관계 방향 검증 실패는 상위로 전파
195
+ if (error instanceof Error && error.message.includes('관계 방향 오류')) {
196
+ throw error;
197
+ }
198
+ logger.error('SemanticMemoryUpdateService: Triple 처리 실패', {
199
+ error: error instanceof Error ? error.message : String(error),
200
+ triple
201
+ });
202
+ result.skipped++;
203
+ }
204
+ }
205
+ // PRD 8.2: Semantic Memory 생성 통계 수집
206
+ const processingTime = Date.now() - processingStartTime;
207
+ const duplicates = extractionResult.triples.length - (result.created + result.updated + result.skipped);
208
+ this.statistics.recordUpdate(result.created, result.updated, result.skipped, duplicates, confidences, processingTime, hasError);
209
+ return result;
210
+ }
211
+ /**
212
+ * Semantic Memory 생성 통계 조회
213
+ *
214
+ * PRD 8.2: Semantic Memory 생성 통계
215
+ * - 생성된 Semantic Memory 수
216
+ * - 업데이트된 Semantic Memory 수
217
+ * - 중복 제거된 항목 수
218
+ *
219
+ * @returns Semantic Memory 생성 통계
220
+ */
221
+ getStatistics() {
222
+ return this.statistics.getStatistics();
223
+ }
224
+ /**
225
+ * Triple을 자연어로 변환
226
+ *
227
+ * @param subject 정규화된 subject
228
+ * @param predicate 정규화된 predicate
229
+ * @param object 정규화된 object
230
+ * @returns 자연어 문장
231
+ */
232
+ tripleToNaturalLanguage(subject, predicate, object) {
233
+ // 간단한 자연어 변환: "subject는 object를 predicate합니다" 형식
234
+ return `${subject}는 ${object}를 ${predicate}합니다`;
235
+ }
236
+ /**
237
+ * 새로운 Semantic Memory 생성
238
+ *
239
+ * Triple의 predicate, subject, object를 정규화하여 저장합니다.
240
+ *
241
+ * @param triple Triple
242
+ * @param options 업데이트 옵션
243
+ * @param confidence Confidence
244
+ * @returns 생성된 Semantic Memory ID
245
+ */
246
+ async createSemanticMemory(triple, options, confidence) {
247
+ // Predicate 정규화 (표준 predicate 사용)
248
+ const predicateResult = this.canonicalizer.canonicalize(triple.predicate);
249
+ const normalizedPredicate = predicateResult.canonical;
250
+ // Subject/Object Entity Linking (정규화된 엔티티 사용)
251
+ const subjectResult = this.entityLinker.link(triple.subject);
252
+ const objectResult = this.entityLinker.link(triple.object);
253
+ const normalizedSubject = subjectResult.linked;
254
+ const normalizedObject = objectResult.linked;
255
+ // 정규화된 값들로 자연어 문장 생성
256
+ const content = this.tripleToNaturalLanguage(normalizedSubject, normalizedPredicate, normalizedObject);
257
+ const id = generateId();
258
+ const importance = this.calculateImportance(options.episodicImportance || 0.5, 1);
259
+ const createdAt = new Date().toISOString();
260
+ // Semantic Memory 생성 (정규화된 subject, predicate, object 컬럼에 저장)
261
+ await DatabaseUtils.run(this.db, `
262
+ INSERT INTO memory_item (
263
+ id, type, content, subject, predicate, object,
264
+ importance, privacy_scope, created_at
265
+ )
266
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
267
+ `, [
268
+ id,
269
+ 'semantic',
270
+ content,
271
+ normalizedSubject, // 정규화된 subject
272
+ normalizedPredicate, // 정규화된 predicate
273
+ normalizedObject, // 정규화된 object
274
+ importance,
275
+ 'private',
276
+ createdAt
277
+ ]);
278
+ logger.debug('SemanticMemoryUpdateService: Semantic Memory 생성', {
279
+ id,
280
+ originalTriple: triple,
281
+ normalizedTriple: {
282
+ subject: normalizedSubject,
283
+ predicate: normalizedPredicate,
284
+ object: normalizedObject
285
+ },
286
+ confidence,
287
+ importance
288
+ });
289
+ return id;
290
+ }
291
+ /**
292
+ * 기존 Semantic Memory 업데이트 (병합 전략)
293
+ *
294
+ * PRD 2.3 중복 방지 및 병합 전략에 따라:
295
+ * - 유사도가 임계값 이상인 경우 새로운 항목을 생성하지 않고 기존 항목 업데이트
296
+ * - 기존 항목의 중요도 업데이트
297
+ * - Episode Weight 누적: 동일 triple이 여러 Episodic Memory에서 추출된 경우 가중치 증가
298
+ * * recall_count를 증가시켜 Episode Weight를 추적
299
+ * * 중요도는 Episode Weight를 반영하여 재계산
300
+ * - 완전히 동일한 Semantic Memory는 생성하지 않음 (중복 판단 로직에서 처리)
301
+ *
302
+ * @param semanticMemoryId Semantic Memory ID
303
+ * @param triple Triple
304
+ * @param options 업데이트 옵션
305
+ * @param confidence Confidence
306
+ */
307
+ async updateExistingSemanticMemory(semanticMemoryId, triple, options, confidence) {
308
+ // 기존 Semantic Memory 조회
309
+ const existing = DatabaseUtils.get(this.db, `
310
+ SELECT id, importance, recall_count
311
+ FROM memory_item
312
+ WHERE id = ?
313
+ `, [semanticMemoryId]);
314
+ if (!existing) {
315
+ throw new Error(`Semantic Memory를 찾을 수 없습니다: ${semanticMemoryId}`);
316
+ }
317
+ // Episode Weight 누적: 동일 triple이 여러 Episodic Memory에서 추출된 경우 가중치 증가
318
+ // recall_count를 증가시켜 Episode Weight를 추적
319
+ const episodeWeight = (existing.recall_count || 0) + 1;
320
+ // 중요도 업데이트 (Episode Weight 반영)
321
+ // 여러 Episodic Memory에서 동일한 Semantic Memory가 추출된 경우 중요도 증가
322
+ const newImportance = this.calculateImportance(options.episodicImportance || 0.5, episodeWeight);
323
+ // 기존 항목 업데이트 (병합)
324
+ await DatabaseUtils.run(this.db, `
325
+ UPDATE memory_item
326
+ SET importance = ?,
327
+ recall_count = recall_count + 1,
328
+ last_accessed_at = CURRENT_TIMESTAMP
329
+ WHERE id = ?
330
+ `, [newImportance, semanticMemoryId]);
331
+ logger.debug('SemanticMemoryUpdateService: Semantic Memory 업데이트 (병합)', {
332
+ id: semanticMemoryId,
333
+ triple,
334
+ episodeWeight,
335
+ oldImportance: existing.importance,
336
+ newImportance,
337
+ confidence
338
+ });
339
+ }
340
+ /**
341
+ * 중복 Semantic Memory 검색
342
+ *
343
+ * Triple 요소별 비교 기준 (PRD 2.2 참고):
344
+ * - Predicate: Canonicalization 후 정확히 일치하면 동일한 것으로 처리 (정확 일치만)
345
+ * - Subject: 문자열 정규화 후 일치 여부를 기본으로, 임베딩 유사도를 보조 기준으로 사용
346
+ * - Object: Subject와 동일한 방식 (정규화 + 유사도)
347
+ *
348
+ * 중복 판단 로직:
349
+ * ```
350
+ * if (predicate == predicate' AND
351
+ * (subject.normalized() == subject'.normalized() OR subject.similarity(subject') > threshold) AND
352
+ * (object.normalized() == object'.normalized() OR object.similarity(object') > threshold))
353
+ * → 중복으로 판단
354
+ * ```
355
+ *
356
+ * @param triple Triple
357
+ * @param similarityThreshold 유사도 임계값 (기본값: 0.9)
358
+ * @returns 중복 Semantic Memory (없으면 null)
359
+ */
360
+ async findDuplicateSemanticMemory(triple, similarityThreshold) {
361
+ // Predicate 정규화 (표준 predicate 사용)
362
+ const predicateResult = this.canonicalizer.canonicalize(triple.predicate);
363
+ const normalizedPredicate = predicateResult.canonical;
364
+ // Subject/Object Entity Linking (정규화된 엔티티 사용)
365
+ const subjectResult = this.entityLinker.link(triple.subject);
366
+ const objectResult = this.entityLinker.link(triple.object);
367
+ const normalizedSubject = subjectResult.linked;
368
+ const normalizedObject = objectResult.linked;
369
+ // 1. 정확한 매칭 우선 (정규화 후 완전 일치)
370
+ // 완전히 동일한 triple (subject, predicate, object 모두 정규화 후 일치)은 즉시 중복으로 판단
371
+ const exactMatch = DatabaseUtils.get(this.db, `
372
+ SELECT id, subject, predicate, object
373
+ FROM memory_item
374
+ WHERE type = 'semantic'
375
+ AND predicate = ?
376
+ AND subject = ?
377
+ AND object = ?
378
+ LIMIT 1
379
+ `, [normalizedPredicate, normalizedSubject, normalizedObject]);
380
+ if (exactMatch) {
381
+ return exactMatch;
382
+ }
383
+ // 2. Predicate 정확 일치 + Subject/Object 유사도 검사
384
+ // Predicate는 정규화 과정을 거쳤으므로 유사도 비교가 아닌 정확 일치가 기본
385
+ const candidates = DatabaseUtils.all(this.db, `
386
+ SELECT id, subject, predicate, object, content
387
+ FROM memory_item
388
+ WHERE type = 'semantic'
389
+ AND predicate = ?
390
+ `, [normalizedPredicate]);
391
+ if (candidates.length === 0) {
392
+ return null;
393
+ }
394
+ // Subject/Object 유사도 계산
395
+ // 문자열 정규화 후 일치 여부를 기본으로, 임베딩 유사도를 보조 기준으로 사용
396
+ for (const candidate of candidates) {
397
+ const subjectSimilar = await this.checkSimilarity(normalizedSubject, candidate.subject, similarityThreshold);
398
+ const objectSimilar = await this.checkSimilarity(normalizedObject, candidate.object, similarityThreshold);
399
+ // Subject와 Object 모두 유사도 임계값 이상이면 중복으로 판단
400
+ if (subjectSimilar && objectSimilar) {
401
+ return {
402
+ id: candidate.id,
403
+ subject: candidate.subject,
404
+ predicate: candidate.predicate,
405
+ object: candidate.object
406
+ };
407
+ }
408
+ }
409
+ return null;
410
+ }
411
+ /**
412
+ * 두 엔티티의 유사도 확인
413
+ *
414
+ * PRD 2.2 중복 판단 기준표에 따라:
415
+ * - 기본 기준: 문자열 정규화 후 일치 여부 (lowercasing, 공백 제거)
416
+ * - 보조 기준: 임베딩 유사도 (임계값 이상이면 유사한 것으로 판단)
417
+ *
418
+ * 정규화: lowercasing, 공백 제거, 한글/영문 혼용 통일 (EntityLinker에서 이미 수행됨)
419
+ *
420
+ * @param entity1 첫 번째 엔티티 (이미 정규화된 값)
421
+ * @param entity2 두 번째 엔티티 (DB에서 조회한 값)
422
+ * @param threshold 유사도 임계값 (기본값: 0.9)
423
+ * @returns 유사도가 임계값 이상이면 true
424
+ */
425
+ async checkSimilarity(entity1, entity2, threshold) {
426
+ // 기본 기준: 문자열 정규화 후 일치 여부 확인
427
+ // (EntityLinker에서 이미 정규화되었지만, 안전장치로 추가 정규화 수행)
428
+ const normalized1 = entity1.toLowerCase().trim();
429
+ const normalized2 = entity2.toLowerCase().trim();
430
+ if (normalized1 === normalized2) {
431
+ return true;
432
+ }
433
+ // 보조 기준: 임베딩 유사도 계산
434
+ if (!this.embeddingService.isAvailable()) {
435
+ // 임베딩 서비스가 없으면 정규화 일치만 확인
436
+ return false;
437
+ }
438
+ try {
439
+ const embedding1 = await this.embeddingService.generateEmbedding(entity1);
440
+ const embedding2 = await this.embeddingService.generateEmbedding(entity2);
441
+ if (!embedding1 || !embedding2) {
442
+ return false;
443
+ }
444
+ const similarity = this.cosineSimilarity(embedding1.embedding, embedding2.embedding);
445
+ return similarity >= threshold;
446
+ }
447
+ catch (error) {
448
+ logger.warn('SemanticMemoryUpdateService: 유사도 계산 실패', {
449
+ error: error instanceof Error ? error.message : String(error),
450
+ entity1,
451
+ entity2
452
+ });
453
+ return false;
454
+ }
455
+ }
456
+ /**
457
+ * 코사인 유사도 계산
458
+ */
459
+ cosineSimilarity(a, b) {
460
+ if (a.length !== b.length) {
461
+ return 0;
462
+ }
463
+ let dotProduct = 0;
464
+ let normA = 0;
465
+ let normB = 0;
466
+ for (let i = 0; i < a.length; i++) {
467
+ const aVal = a[i] ?? 0;
468
+ const bVal = b[i] ?? 0;
469
+ dotProduct += aVal * bVal;
470
+ normA += aVal * aVal;
471
+ normB += bVal * bVal;
472
+ }
473
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
474
+ if (denominator === 0) {
475
+ return 0;
476
+ }
477
+ return dotProduct / denominator;
478
+ }
479
+ /**
480
+ * 구조적 검증 기반 Confidence 계산
481
+ *
482
+ * PRD 2.4 신뢰도 기반 필터링 (구조적 검증 방식)에 따라:
483
+ * - LLM 응답에서 confidence 추출은 신뢰할 수 없음 (대부분의 LLM은 신뢰할 수 있는 confidence를 제공하지 않음)
484
+ * - 구조적 검증 기반 confidence 계산 (AriGraph 방식) 사용
485
+ *
486
+ * 각 검증 단계별 점수 부여:
487
+ * 1. Triple 구조의 완전성 검증 (subject, predicate, object 모두 존재): 0.3
488
+ * 2. Predicate 정규화 성공 여부: 0.3
489
+ * 3. Entity linking 성공 여부: 0.4
490
+ * - Subject와 Object 모두 성공: 0.4
491
+ * - 하나만 성공: 0.2 (부분 성공)
492
+ *
493
+ * 최종 confidence는 0.0~1.0 범위로 정규화됩니다.
494
+ * 신뢰도가 일정 수준 이상인 경우만 Semantic Memory 생성 (기본 임계값: 0.7)
495
+ *
496
+ * @param triple Triple
497
+ * @param extractionInfo 추출 정보 (steps 정보 포함)
498
+ * @returns Confidence (0.0~1.0)
499
+ */
500
+ calculateConfidence(triple, extractionInfo) {
501
+ let confidence = 0.0;
502
+ // 1. Triple 구조 완전성 검증 (0.3)
503
+ // subject, predicate, object 모두 존재하는지 확인
504
+ if (triple.subject && triple.predicate && triple.object) {
505
+ confidence += 0.3;
506
+ }
507
+ // 2. Predicate 정규화 성공 여부 (0.3)
508
+ // 표준 predicate 사전에 있는 경우 높은 confidence 부여
509
+ const predicateResult = this.canonicalizer.canonicalize(triple.predicate);
510
+ if (predicateResult.success) {
511
+ confidence += 0.3;
512
+ }
513
+ // extractionInfo.steps는 보조 검증으로만 사용 (실제 결과가 우선)
514
+ // 3. Entity Linking 성공 여부 (0.4)
515
+ // Subject와 Object 모두 정규화 성공 시 최대 점수 부여
516
+ const subjectResult = this.entityLinker.link(triple.subject);
517
+ const objectResult = this.entityLinker.link(triple.object);
518
+ if (subjectResult.success && objectResult.success) {
519
+ // Subject와 Object 모두 성공: 0.4
520
+ confidence += 0.4;
521
+ }
522
+ else {
523
+ // 부분 성공: 하나만 성공한 경우 0.2 부여
524
+ if (subjectResult.success || objectResult.success) {
525
+ confidence += 0.2;
526
+ }
527
+ }
528
+ // extractionInfo.steps는 보조 검증으로만 사용 (실제 결과가 우선)
529
+ // 0.0~1.0 범위로 정규화
530
+ return Math.min(1.0, Math.max(0.0, confidence));
531
+ }
532
+ /**
533
+ * Semantic Memory 중요도 계산
534
+ *
535
+ * PRD 2.5 Semantic Memory 중요도 계산에 따라:
536
+ * - Triple이 추출된 Episodic Memory의 중요도 반영
537
+ * - 여러 Episodic Memory에서 동일한 Semantic Memory가 추출된 경우 중요도 증가
538
+ * * Episode Weight 누적: 동일 triple이 여러 Episodic Memory에서 추출된 경우 가중치 증가
539
+ * * 로그 스케일을 사용하여 증가폭을 제어 (과도한 증가 방지)
540
+ * - 중요도 Decay: 시간이 지나면서 중요도가 감쇠하는지 여부 결정 (초기에는 decay 없음)
541
+ * - 중요도는 0.0~1.0 범위로 정규화
542
+ *
543
+ * 계산 공식:
544
+ * - 기본 중요도 = Episodic Memory 중요도
545
+ * - Episode Weight가 1보다 큰 경우: importance += log(episodeCount + 1) / log(10) * 0.1
546
+ * - 최종 중요도 = min(1.0, max(0.0, importance))
547
+ *
548
+ * @param episodicImportance Triple이 추출된 Episodic Memory의 중요도
549
+ * @param episodeCount Episode Weight (여러 Episodic Memory에서 추출된 횟수)
550
+ * @returns 계산된 중요도 (0.0~1.0)
551
+ */
552
+ calculateImportance(episodicImportance, episodeCount) {
553
+ // 기본 중요도는 Episodic Memory 중요도 사용
554
+ // Triple이 추출된 Episodic Memory의 중요도를 반영
555
+ let importance = episodicImportance;
556
+ // 여러 Episodic Memory에서 동일한 Semantic Memory가 추출된 경우 중요도 증가
557
+ // Episode Weight 누적: episodeCount가 많을수록 중요도 증가 (로그 스케일)
558
+ // 로그 스케일을 사용하여 증가폭을 제어 (과도한 증가 방지)
559
+ if (episodeCount > 1) {
560
+ const boost = Math.log(episodeCount + 1) / Math.log(10); // 로그 스케일 (base 10)
561
+ importance = Math.min(1.0, importance + (boost * 0.1));
562
+ }
563
+ // 중요도는 0.0~1.0 범위로 정규화
564
+ // 중요도 Decay는 초기에는 없음 (시간에 따른 감쇠 없음)
565
+ return Math.min(1.0, Math.max(0.0, importance));
566
+ }
567
+ /**
568
+ * 관계 방향 검증
569
+ *
570
+ * PRD 3.2 관계 타입 정의에 따라:
571
+ * - extracted_from: source가 Episodic, target이 Semantic이어야 함
572
+ * - supported_by: source가 Semantic, target이 Episodic이어야 함
573
+ *
574
+ * @param sourceId 소스 Memory ID
575
+ * @param targetId 타겟 Memory ID
576
+ * @param relationType 관계 타입
577
+ * @throws Error 관계 방향이 올바르지 않은 경우
578
+ */
579
+ async validateRelationDirection(sourceId, targetId, relationType) {
580
+ // 소스와 타겟 Memory 타입 확인
581
+ const sourceMemory = DatabaseUtils.get(this.db, `
582
+ SELECT id, type FROM memory_item WHERE id = ?
583
+ `, [sourceId]);
584
+ const targetMemory = DatabaseUtils.get(this.db, `
585
+ SELECT id, type FROM memory_item WHERE id = ?
586
+ `, [targetId]);
587
+ if (!sourceMemory || !targetMemory) {
588
+ throw new Error(`Memory를 찾을 수 없습니다: source=${sourceId}, target=${targetId}`);
589
+ }
590
+ // 관계 방향 검증
591
+ if (relationType === 'extracted_from') {
592
+ // extracted_from: source가 Episodic, target이 Semantic이어야 함
593
+ if (sourceMemory.type !== 'episodic' || targetMemory.type !== 'semantic') {
594
+ throw new Error(`extracted_from 관계 방향 오류: source는 'episodic'이어야 하고 target은 'semantic'이어야 합니다. ` +
595
+ `현재: source=${sourceMemory.type}, target=${targetMemory.type}`);
596
+ }
597
+ }
598
+ else if (relationType === 'supported_by') {
599
+ // supported_by: source가 Semantic, target이 Episodic이어야 함
600
+ if (sourceMemory.type !== 'semantic' || targetMemory.type !== 'episodic') {
601
+ throw new Error(`supported_by 관계 방향 오류: source는 'semantic'이어야 하고 target은 'episodic'이어야 합니다. ` +
602
+ `현재: source=${sourceMemory.type}, target=${targetMemory.type}`);
603
+ }
604
+ }
605
+ }
606
+ /**
607
+ * Episodic-Edge 관계 생성
608
+ *
609
+ * PRD 2.4 Confidence 저장 위치 및 연계 방식에 따라:
610
+ * - **주 저장 위치**: `memory_relation` 테이블의 `confidence` 필드
611
+ * - Episodic Memory와 Semantic Memory 간 관계의 신뢰도로 저장
612
+ * - 검색/가중치 계산 시: `memory_relation.confidence` 참조
613
+ * - 각 relation 생성 시 계산된 confidence 값을 저장
614
+ *
615
+ * 두 가지 관계를 생성:
616
+ * 1. extracted_from (Episodic → Semantic): Episodic Memory에서 Semantic Memory로 추출된 관계
617
+ * 2. supported_by (Semantic → Episodic): Semantic Memory가 Episodic Memory에 의해 지원되는 관계 (역방향)
618
+ *
619
+ * 각 관계는 동일한 confidence 값을 가지며, 각 triple별로 독립적인 relation으로 저장됩니다.
620
+ *
621
+ * PRD 3.4 관계 중복 방지:
622
+ * - UNIQUE(source_id, target_id, relation_type) 제약 조건 활용
623
+ * - 동일한 Episodic Memory와 Semantic Memory 간의 동일한 관계 타입은 중복 생성하지 않음
624
+ *
625
+ * @param episodicMemoryId Episodic Memory ID
626
+ * @param semanticMemoryId Semantic Memory ID
627
+ * @param triple Triple
628
+ * @param extractionInfo 추출 정보
629
+ * @param confidence 계산된 Confidence (구조적 검증 기반, 0.0~1.0)
630
+ */
631
+ async createEpisodicEdge(episodicMemoryId, semanticMemoryId, triple, extractionInfo, confidence) {
632
+ // 관계 방향 검증 (PRD 3.2 참고)
633
+ // 검증 실패 시 에러를 상위로 전파
634
+ await this.validateRelationDirection(episodicMemoryId, semanticMemoryId, 'extracted_from');
635
+ await this.validateRelationDirection(semanticMemoryId, episodicMemoryId, 'supported_by');
636
+ try {
637
+ // extracted_from 관계 생성 (Episodic → Semantic)
638
+ // Note: 'extracted_from'와 'supported_by'는 relation_type_registry에 등록되어 있지만
639
+ // RelationType 타입에는 아직 포함되지 않았으므로 타입 단언 사용
640
+ // confidence는 memory_relation.confidence 필드에 저장됨
641
+ await this.relationGraph.addRelation(episodicMemoryId, semanticMemoryId, 'extracted_from', // TODO: RelationType 타입 확장 필요
642
+ {
643
+ confidence, // memory_relation.confidence 필드에 저장
644
+ metadata: {
645
+ method: 'llm',
646
+ triple: {
647
+ subject: triple.subject,
648
+ predicate: triple.predicate,
649
+ object: triple.object
650
+ },
651
+ // 각 triple별 독립적인 metadata 저장
652
+ failureReason: extractionInfo.failureReason,
653
+ steps: extractionInfo.steps
654
+ },
655
+ allowCyclic: true // Episodic-Semantic 간 양방향 관계 허용
656
+ });
657
+ // supported_by 관계 생성 (Semantic → Episodic, 역방향)
658
+ // 동일한 confidence 값을 저장
659
+ await this.relationGraph.addRelation(semanticMemoryId, episodicMemoryId, 'supported_by', // TODO: RelationType 타입 확장 필요
660
+ {
661
+ confidence, // memory_relation.confidence 필드에 저장
662
+ metadata: {
663
+ method: 'llm',
664
+ triple: {
665
+ subject: triple.subject,
666
+ predicate: triple.predicate,
667
+ object: triple.object
668
+ },
669
+ // 각 triple별 독립적인 metadata 저장
670
+ failureReason: extractionInfo.failureReason,
671
+ steps: extractionInfo.steps
672
+ },
673
+ allowCyclic: true // Episodic-Semantic 간 양방향 관계 허용
674
+ });
675
+ }
676
+ catch (error) {
677
+ // UNIQUE 제약 조건 위반은 무시 (이미 관계가 존재하는 경우)
678
+ if (error instanceof Error && error.message.includes('UNIQUE constraint')) {
679
+ logger.debug('SemanticMemoryUpdateService: 관계 중복 (무시)', {
680
+ episodicMemoryId,
681
+ semanticMemoryId,
682
+ relationType: 'extracted_from/supported_by'
683
+ });
684
+ return;
685
+ }
686
+ logger.error('SemanticMemoryUpdateService: 관계 생성 실패', {
687
+ error: error instanceof Error ? error.message : String(error),
688
+ episodicMemoryId,
689
+ semanticMemoryId,
690
+ confidence
691
+ });
692
+ // 기타 관계 생성 실패는 무시 (Semantic Memory 생성에는 영향 없음)
693
+ }
694
+ }
695
+ }
696
+ //# sourceMappingURL=semantic-memory-update-service.js.map