memento-mcp-server 1.11.0-a1 → 1.11.1-a

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 (75) hide show
  1. package/README.md +7 -0
  2. package/dist/server/context.d.ts +36 -0
  3. package/dist/server/context.d.ts.map +1 -0
  4. package/dist/server/context.js +45 -0
  5. package/dist/server/context.js.map +1 -0
  6. package/dist/server/handlers/anchor-map.handler.d.ts +60 -0
  7. package/dist/server/handlers/anchor-map.handler.d.ts.map +1 -0
  8. package/dist/server/handlers/anchor-map.handler.js +190 -0
  9. package/dist/server/handlers/anchor-map.handler.js.map +1 -0
  10. package/dist/server/http-server.d.ts.map +1 -1
  11. package/dist/server/http-server.js +41 -1131
  12. package/dist/server/http-server.js.map +1 -1
  13. package/dist/server/middleware/error-handler.middleware.d.ts +25 -0
  14. package/dist/server/middleware/error-handler.middleware.d.ts.map +1 -0
  15. package/dist/server/middleware/error-handler.middleware.js +97 -0
  16. package/dist/server/middleware/error-handler.middleware.js.map +1 -0
  17. package/dist/server/middleware/index.d.ts +8 -0
  18. package/dist/server/middleware/index.d.ts.map +1 -0
  19. package/dist/server/middleware/index.js +8 -0
  20. package/dist/server/middleware/index.js.map +1 -0
  21. package/dist/server/middleware/service-injector.middleware.d.ts +30 -0
  22. package/dist/server/middleware/service-injector.middleware.d.ts.map +1 -0
  23. package/dist/server/middleware/service-injector.middleware.js +20 -0
  24. package/dist/server/middleware/service-injector.middleware.js.map +1 -0
  25. package/dist/server/middleware/tool-context.middleware.d.ts +29 -0
  26. package/dist/server/middleware/tool-context.middleware.d.ts.map +1 -0
  27. package/dist/server/middleware/tool-context.middleware.js +34 -0
  28. package/dist/server/middleware/tool-context.middleware.js.map +1 -0
  29. package/dist/server/routes/admin.routes.d.ts +12 -0
  30. package/dist/server/routes/admin.routes.d.ts.map +1 -0
  31. package/dist/server/routes/admin.routes.js +338 -0
  32. package/dist/server/routes/admin.routes.js.map +1 -0
  33. package/dist/server/routes/api.routes.d.ts +13 -0
  34. package/dist/server/routes/api.routes.d.ts.map +1 -0
  35. package/dist/server/routes/api.routes.js +122 -0
  36. package/dist/server/routes/api.routes.js.map +1 -0
  37. package/dist/server/routes/mcp.routes.d.ts +22 -0
  38. package/dist/server/routes/mcp.routes.d.ts.map +1 -0
  39. package/dist/server/routes/mcp.routes.js +383 -0
  40. package/dist/server/routes/mcp.routes.js.map +1 -0
  41. package/dist/server/routes/tools.routes.d.ts +13 -0
  42. package/dist/server/routes/tools.routes.d.ts.map +1 -0
  43. package/dist/server/routes/tools.routes.js +99 -0
  44. package/dist/server/routes/tools.routes.js.map +1 -0
  45. package/dist/services/anchor/anchor-cache-service.d.ts +77 -0
  46. package/dist/services/anchor/anchor-cache-service.d.ts.map +1 -0
  47. package/dist/services/anchor/anchor-cache-service.js +193 -0
  48. package/dist/services/anchor/anchor-cache-service.js.map +1 -0
  49. package/dist/services/anchor/anchor-interfaces.d.ts +143 -0
  50. package/dist/services/anchor/anchor-interfaces.d.ts.map +1 -0
  51. package/dist/services/anchor/anchor-interfaces.js +24 -0
  52. package/dist/services/anchor/anchor-interfaces.js.map +1 -0
  53. package/dist/services/anchor/anchor-manager.d.ts +71 -0
  54. package/dist/services/anchor/anchor-manager.d.ts.map +1 -0
  55. package/dist/services/anchor/anchor-manager.js +205 -0
  56. package/dist/services/anchor/anchor-manager.js.map +1 -0
  57. package/dist/services/anchor/anchor-search-service.d.ts +115 -0
  58. package/dist/services/anchor/anchor-search-service.d.ts.map +1 -0
  59. package/dist/services/anchor/anchor-search-service.js +799 -0
  60. package/dist/services/anchor/anchor-search-service.js.map +1 -0
  61. package/dist/services/anchor/index.d.ts +11 -0
  62. package/dist/services/anchor/index.d.ts.map +1 -0
  63. package/dist/services/anchor/index.js +10 -0
  64. package/dist/services/anchor/index.js.map +1 -0
  65. package/dist/services/anchor-manager.d.ts +22 -208
  66. package/dist/services/anchor-manager.d.ts.map +1 -1
  67. package/dist/services/anchor-manager.js +72 -1088
  68. package/dist/services/anchor-manager.js.map +1 -1
  69. package/dist/services/error-logging-service.d.ts +1 -0
  70. package/dist/services/error-logging-service.d.ts.map +1 -1
  71. package/dist/services/error-logging-service.js +2 -0
  72. package/dist/services/error-logging-service.js.map +1 -1
  73. package/dist/tools/forget-tool.js +1 -1
  74. package/dist/tools/forget-tool.js.map +1 -1
  75. package/package.json +3 -1
@@ -0,0 +1,799 @@
1
+ /**
2
+ * Anchor Search Service
3
+ * 검색 관련 로직 담당
4
+ * Phase 1.1: anchor-manager.ts 리팩토링
5
+ */
6
+ import { UnifiedEmbeddingService } from '../unified-embedding-service.js';
7
+ import { logger } from '../../utils/logger.js';
8
+ /**
9
+ * Anchor Search Service 구현
10
+ */
11
+ export class AnchorSearchService {
12
+ db = null;
13
+ cacheService;
14
+ hybridSearchEngine = null;
15
+ vectorSearchEngine = null;
16
+ queryEmbeddingService = new UnifiedEmbeddingService();
17
+ /**
18
+ * 생성자
19
+ */
20
+ constructor(cacheService) {
21
+ this.cacheService = cacheService;
22
+ logger.info('AnchorSearchService 초기화 완료');
23
+ }
24
+ /**
25
+ * 데이터베이스 설정
26
+ */
27
+ setDatabase(db) {
28
+ if (!db) {
29
+ throw new Error('Database instance is required');
30
+ }
31
+ this.db = db;
32
+ }
33
+ /**
34
+ * 하이브리드 검색 엔진 설정
35
+ */
36
+ setHybridSearchEngine(hybridSearchEngine) {
37
+ if (!hybridSearchEngine) {
38
+ throw new Error('HybridSearchEngine is required');
39
+ }
40
+ this.hybridSearchEngine = hybridSearchEngine;
41
+ }
42
+ /**
43
+ * 벡터 검색 엔진 설정
44
+ */
45
+ setVectorSearchEngine(vectorSearchEngine) {
46
+ if (!vectorSearchEngine) {
47
+ throw new Error('VectorSearchEngine is required');
48
+ }
49
+ this.vectorSearchEngine = vectorSearchEngine;
50
+ // 데이터베이스가 이미 설정되어 있으면 초기화
51
+ if (this.db) {
52
+ this.vectorSearchEngine.initialize(this.db);
53
+ }
54
+ }
55
+ /**
56
+ * 국소 검색
57
+ * 앵커 메모리를 기준으로 N-hop 제한 검색 수행
58
+ */
59
+ async searchLocal(agentId, slot, query, hopLimit, options, anchorMemoryId, anchorEmbedding, startTime) {
60
+ if (!this.db) {
61
+ throw new Error('Database is not set. Call setDatabase() first.');
62
+ }
63
+ // 슬롯별 설정 가져오기
64
+ const slotConfig = this.getSlotConfig(slot);
65
+ const finalHopLimit = hopLimit ?? slotConfig.hop_limit;
66
+ const vectorThreshold = slotConfig.vector_threshold;
67
+ // 검색 옵션 기본값
68
+ const limit = options?.limit ?? 10;
69
+ const minResults = options?.min_results ?? 3;
70
+ // VectorSearchEngine이 없으면 에러
71
+ if (!this.vectorSearchEngine) {
72
+ throw new Error('VectorSearchEngine is not set. Call setVectorSearchEngine() first.');
73
+ }
74
+ // N-hop 검색 구현
75
+ const allHopResults = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchorMemoryId, vectorThreshold, finalHopLimit, limit * 2 // 더 많이 가져와서 필터링 후 최종 limit 적용
76
+ );
77
+ // 쿼리가 있는 경우 쿼리 기반 필터링
78
+ let filteredResults = allHopResults;
79
+ let queryEmbeddingForReanchor;
80
+ if (query && query.trim().length > 0) {
81
+ filteredResults = await this.filterByQuery(query, allHopResults, anchorEmbedding.provider);
82
+ // 자동 앵커 이동을 위한 쿼리 임베딩 생성 (선택적, 비동기)
83
+ try {
84
+ const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
85
+ if (queryEmbeddingResult && queryEmbeddingResult.embedding) {
86
+ queryEmbeddingForReanchor = queryEmbeddingResult.embedding;
87
+ }
88
+ }
89
+ catch (error) {
90
+ // 쿼리 임베딩 생성 실패는 무시 (자동 이동은 선택적)
91
+ logger.debug('Query embedding generation failed (for auto anchor move)', {
92
+ error: error instanceof Error ? error.message : String(error)
93
+ });
94
+ }
95
+ }
96
+ // 결과 포맷팅
97
+ const formattedResults = filteredResults.map(result => ({
98
+ id: result.memory_id,
99
+ content: result.content,
100
+ type: result.type,
101
+ similarity: result.similarity,
102
+ hop_distance: result.hop_distance,
103
+ importance: result.importance,
104
+ created_at: result.created_at,
105
+ tags: result.tags
106
+ }));
107
+ // 최종 limit 적용
108
+ const localResults = formattedResults.slice(0, limit);
109
+ const localCount = localResults.length;
110
+ // Fallback 체크 (query가 있을 때만, min_results 미만 시)
111
+ let fallbackUsed = false;
112
+ let finalResults = localResults;
113
+ let totalCount = localCount;
114
+ if (query && query.trim().length > 0 && localCount < minResults) {
115
+ try {
116
+ logger.info('Fallback to global search', {
117
+ localCount,
118
+ minResults
119
+ });
120
+ // Fallback 수행
121
+ const fallbackResult = await this.fallbackToGlobalSearch(query, { ...options, limit: limit - localCount }, // 부족한 만큼만 가져오기
122
+ startTime);
123
+ fallbackUsed = true;
124
+ // Local 결과와 Fallback 결과 병합
125
+ // Local 결과를 우선하고, 중복 제거 (memory_id 기준)
126
+ const localMemoryIds = new Set(localResults.map(r => r.id));
127
+ const fallbackItems = fallbackResult.items
128
+ .filter(item => !localMemoryIds.has(item.id))
129
+ .map(item => ({
130
+ id: item.id,
131
+ content: item.content,
132
+ type: item.type,
133
+ similarity: item.similarity ?? 0,
134
+ hop_distance: item.hop_distance ?? 999, // fallback 결과는 hop_distance가 없으므로 큰 값으로 설정
135
+ importance: item.importance ?? 0.5,
136
+ created_at: item.created_at ?? new Date().toISOString(),
137
+ tags: item.tags ?? undefined
138
+ }));
139
+ // Local 결과 + Fallback 결과 (중복 제거된 것만)
140
+ finalResults = [...localResults, ...fallbackItems].slice(0, limit);
141
+ totalCount = finalResults.length;
142
+ logger.info('Fallback completed', {
143
+ localCount,
144
+ fallbackCount: fallbackItems.length,
145
+ totalCount
146
+ });
147
+ }
148
+ catch (error) {
149
+ logger.error('Fallback failed', {
150
+ error: error instanceof Error ? error.message : String(error)
151
+ });
152
+ // Fallback 실패 시 local 결과만 반환
153
+ fallbackUsed = false;
154
+ }
155
+ }
156
+ const queryTime = Date.now() - startTime;
157
+ return {
158
+ items: finalResults,
159
+ total_count: totalCount,
160
+ local_results_count: localCount,
161
+ fallback_used: fallbackUsed,
162
+ query_time: queryTime,
163
+ anchor_info: {
164
+ agent_id: agentId,
165
+ slot: slot,
166
+ memory_id: anchorMemoryId
167
+ }
168
+ };
169
+ }
170
+ /**
171
+ * 1-hop 검색: 앵커와 직접적으로 유사한 메모리 검색
172
+ */
173
+ async searchOneHop(anchorEmbedding, provider, anchorMemoryId, threshold, limit) {
174
+ if (!this.vectorSearchEngine || !this.db) {
175
+ throw new Error('VectorSearchEngine or Database is not set.');
176
+ }
177
+ try {
178
+ // VectorSearchEngine 초기화 확인
179
+ if (typeof this.vectorSearchEngine.initialize === 'function') {
180
+ this.vectorSearchEngine.initialize(this.db);
181
+ }
182
+ // 벡터 검색 실행 (임계값은 낮게 설정하고 나중에 필터링)
183
+ const searchResults = await this.vectorSearchEngine.search(anchorEmbedding, {
184
+ limit: limit + 1, // 자기 자신 제외를 위해 +1
185
+ threshold: 0.0, // 임계값은 나중에 필터링에서 적용
186
+ includeContent: true,
187
+ includeMetadata: true
188
+ }, provider);
189
+ // 결과 필터링: 앵커 메모리 제외, 유사도 임계값 이상만 반환
190
+ const filteredResults = searchResults
191
+ .filter(result => {
192
+ // 앵커 메모리 제외
193
+ if (result.memory_id === anchorMemoryId) {
194
+ return false;
195
+ }
196
+ // 유사도 임계값 이상만 반환
197
+ return result.similarity >= threshold;
198
+ })
199
+ .slice(0, limit); // 최종 limit 적용
200
+ return filteredResults;
201
+ }
202
+ catch (error) {
203
+ logger.error('1-hop search failed', {
204
+ error: error instanceof Error ? error.message : String(error)
205
+ });
206
+ throw new Error(`Failed to perform 1-hop search: ${error instanceof Error ? error.message : 'Unknown error'}`);
207
+ }
208
+ }
209
+ /**
210
+ * N-hop 검색: 앵커를 기준으로 최대 N-hop까지 확장 검색
211
+ */
212
+ async searchNHop(anchorEmbedding, provider, anchorMemoryId, threshold, maxHops, limit) {
213
+ if (!this.vectorSearchEngine || !this.db) {
214
+ throw new Error('VectorSearchEngine or Database is not set.');
215
+ }
216
+ // VectorSearchEngine 초기화 확인
217
+ if (typeof this.vectorSearchEngine.initialize === 'function') {
218
+ this.vectorSearchEngine.initialize(this.db);
219
+ }
220
+ // 이미 발견된 메모리 ID 추적 (중복 방지)
221
+ const discoveredMemoryIds = new Set([anchorMemoryId]);
222
+ // 각 hop 레벨의 결과를 저장
223
+ const allResults = [];
224
+ // 현재 hop 레벨의 메모리들 (임베딩 포함)
225
+ // 1-hop: 앵커 임베딩을 사용
226
+ let currentHopMemories = [
227
+ { memory_id: anchorMemoryId, embedding: anchorEmbedding }
228
+ ];
229
+ // 각 hop 레벨별로 검색 수행
230
+ for (let hop = 1; hop <= maxHops; hop++) {
231
+ const nextHopMemories = [];
232
+ const hopResults = [];
233
+ // 현재 hop의 각 메모리에 대해 검색 수행
234
+ for (const currentMemory of currentHopMemories) {
235
+ try {
236
+ // memory_link를 활용한 직접 연결된 메모리 조회 (최적화)
237
+ const linkedMemories = await this.getLinkedMemories(currentMemory.memory_id);
238
+ // 벡터 검색 실행
239
+ const vectorSearchResults = await this.vectorSearchEngine.search(currentMemory.embedding, {
240
+ limit: Math.ceil(limit / maxHops) + 10, // 각 hop당 충분한 결과 가져오기
241
+ threshold: 0.0, // 임계값은 나중에 필터링에서 적용
242
+ includeContent: true,
243
+ includeMetadata: true
244
+ }, provider);
245
+ // memory_link 결과와 벡터 검색 결과를 병합
246
+ const allCandidates = new Map();
247
+ // memory_link 결과 추가 (우선순위 높음)
248
+ for (const linked of linkedMemories) {
249
+ if (!discoveredMemoryIds.has(linked.memory_id)) {
250
+ allCandidates.set(linked.memory_id, {
251
+ ...linked,
252
+ isLinked: true
253
+ });
254
+ }
255
+ }
256
+ // 벡터 검색 결과 추가
257
+ const relaxedThreshold = threshold * 0.5;
258
+ for (const result of vectorSearchResults) {
259
+ if (!allCandidates.has(result.memory_id) && !discoveredMemoryIds.has(result.memory_id)) {
260
+ if (result.similarity >= relaxedThreshold) {
261
+ allCandidates.set(result.memory_id, {
262
+ ...result,
263
+ isLinked: false
264
+ });
265
+ }
266
+ }
267
+ else if (allCandidates.has(result.memory_id)) {
268
+ // memory_link로 이미 추가된 경우, 유사도 정보 업데이트
269
+ const existing = allCandidates.get(result.memory_id);
270
+ existing.similarity = Math.max(existing.similarity, result.similarity);
271
+ }
272
+ }
273
+ // 결과 필터링 및 추가
274
+ for (const [memoryId, candidate] of allCandidates.entries()) {
275
+ if (discoveredMemoryIds.has(memoryId)) {
276
+ continue;
277
+ }
278
+ const effectiveThreshold = candidate.isLinked
279
+ ? threshold * 0.8
280
+ : threshold;
281
+ if (candidate.similarity < effectiveThreshold) {
282
+ continue;
283
+ }
284
+ discoveredMemoryIds.add(memoryId);
285
+ hopResults.push({
286
+ memory_id: candidate.memory_id,
287
+ content: candidate.content,
288
+ type: candidate.type,
289
+ similarity: candidate.similarity,
290
+ importance: candidate.importance,
291
+ created_at: candidate.created_at,
292
+ tags: candidate.tags
293
+ });
294
+ // 다음 hop을 위한 임베딩 조회
295
+ if (hop < maxHops) {
296
+ try {
297
+ const nextEmbedding = await this.cacheService.getAnchorEmbedding(candidate.memory_id);
298
+ if (nextEmbedding && nextEmbedding.embedding) {
299
+ nextHopMemories.push({
300
+ memory_id: candidate.memory_id,
301
+ embedding: nextEmbedding.embedding
302
+ });
303
+ }
304
+ }
305
+ catch (error) {
306
+ // 임베딩 조회 실패 시 다음 hop에서 제외
307
+ logger.debug('Skipping memory for next hop (no embedding)', {
308
+ memoryId: candidate.memory_id,
309
+ error: error instanceof Error ? error.message : String(error)
310
+ });
311
+ }
312
+ }
313
+ }
314
+ }
315
+ catch (error) {
316
+ logger.error('Hop search failed', {
317
+ hop,
318
+ memoryId: currentMemory.memory_id,
319
+ error: error instanceof Error ? error.message : String(error)
320
+ });
321
+ continue;
322
+ }
323
+ }
324
+ // 현재 hop의 결과를 전체 결과에 추가
325
+ for (const result of hopResults) {
326
+ allResults.push({
327
+ ...result,
328
+ hop_distance: hop
329
+ });
330
+ }
331
+ // limit에 도달했으면 중단
332
+ if (allResults.length >= limit) {
333
+ break;
334
+ }
335
+ // 다음 hop을 위한 메모리가 없으면 중단
336
+ if (nextHopMemories.length === 0) {
337
+ break;
338
+ }
339
+ // 다음 hop을 위한 메모리로 업데이트
340
+ currentHopMemories = nextHopMemories;
341
+ }
342
+ // 랭킹 점수 계산 및 적용
343
+ const rankedResults = allResults.map(result => {
344
+ const rankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
345
+ return {
346
+ ...result,
347
+ similarity: rankingScore
348
+ };
349
+ });
350
+ // 랭킹 점수 기준으로 정렬
351
+ rankedResults.sort((a, b) => {
352
+ if (Math.abs(a.similarity - b.similarity) < 0.001) {
353
+ return a.hop_distance - b.hop_distance;
354
+ }
355
+ return b.similarity - a.similarity;
356
+ });
357
+ // 최종 limit 적용
358
+ return rankedResults.slice(0, limit);
359
+ }
360
+ /**
361
+ * 쿼리 기반 필터링
362
+ */
363
+ async filterByQuery(query, results, provider) {
364
+ if (results.length === 0) {
365
+ return results;
366
+ }
367
+ try {
368
+ // 쿼리 임베딩 생성
369
+ const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
370
+ if (!queryEmbeddingResult || !queryEmbeddingResult.embedding) {
371
+ logger.warn('Query embedding generation failed, skipping filter');
372
+ return results;
373
+ }
374
+ const queryEmbedding = queryEmbeddingResult.embedding;
375
+ // 각 결과 메모리의 임베딩 조회 및 쿼리 유사도 계산
376
+ const resultsWithQuerySimilarity = await Promise.all(results.map(async (result) => {
377
+ try {
378
+ const memoryEmbedding = await this.cacheService.getAnchorEmbedding(result.memory_id);
379
+ if (!memoryEmbedding || !memoryEmbedding.embedding) {
380
+ return {
381
+ ...result,
382
+ query_similarity: 0,
383
+ combined_similarity: result.similarity * 0.5
384
+ };
385
+ }
386
+ // 쿼리 임베딩과 메모리 임베딩 간 유사도 계산
387
+ let querySim = 0;
388
+ if (queryEmbedding.length === memoryEmbedding.embedding.length) {
389
+ querySim = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
390
+ }
391
+ else {
392
+ // 차원이 다르면 텍스트 기반 간단한 매칭
393
+ const queryLower = query.toLowerCase();
394
+ const contentLower = result.content.toLowerCase();
395
+ const queryWords = queryLower.split(/\s+/);
396
+ const matchCount = queryWords.filter(word => contentLower.includes(word)).length;
397
+ querySim = matchCount / Math.max(queryWords.length, 1);
398
+ }
399
+ const baseRankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
400
+ const combinedSimilarity = baseRankingScore * 0.6 + querySim * 0.4;
401
+ return {
402
+ ...result,
403
+ query_similarity: querySim,
404
+ combined_similarity: combinedSimilarity
405
+ };
406
+ }
407
+ catch (error) {
408
+ logger.error('Query filtering failed for memory', {
409
+ memoryId: result.memory_id,
410
+ error: error instanceof Error ? error.message : String(error)
411
+ });
412
+ return {
413
+ ...result,
414
+ query_similarity: 0,
415
+ combined_similarity: result.similarity * 0.5
416
+ };
417
+ }
418
+ }));
419
+ // 쿼리 유사도 임계값 적용
420
+ const queryThreshold = 0.3;
421
+ const filtered = resultsWithQuerySimilarity.filter(r => r.query_similarity >= queryThreshold || r.combined_similarity >= 0.5);
422
+ // 결합 유사도 기준으로 재정렬
423
+ filtered.sort((a, b) => {
424
+ if (Math.abs(a.combined_similarity - b.combined_similarity) < 0.001) {
425
+ return a.hop_distance - b.hop_distance;
426
+ }
427
+ return b.combined_similarity - a.combined_similarity;
428
+ });
429
+ // 원본 similarity를 combined_similarity로 업데이트하여 반환
430
+ return filtered.map(r => ({
431
+ ...r,
432
+ similarity: r.combined_similarity
433
+ }));
434
+ }
435
+ catch (error) {
436
+ logger.error('Query-based filtering failed', {
437
+ error: error instanceof Error ? error.message : String(error)
438
+ });
439
+ return results;
440
+ }
441
+ }
442
+ /**
443
+ * 전역 검색으로 Fallback
444
+ */
445
+ async fallbackToGlobalSearch(query, options, startTime) {
446
+ if (!this.hybridSearchEngine) {
447
+ throw new Error('HybridSearchEngine is not set. Call setHybridSearchEngine() first.');
448
+ }
449
+ if (!this.db) {
450
+ throw new Error('Database is not set.');
451
+ }
452
+ const limit = options?.limit ?? 10;
453
+ const fallbackStartTime = Date.now();
454
+ try {
455
+ // HybridSearchEngine을 사용한 전역 검색
456
+ const globalSearchResult = await this.hybridSearchEngine.search(this.db, {
457
+ query: query,
458
+ limit: limit,
459
+ vectorWeight: options?.vector_weight,
460
+ textWeight: options?.text_weight
461
+ });
462
+ // HybridSearchResult를 SearchResult 형식으로 변환
463
+ const convertedItems = globalSearchResult.items.map(item => ({
464
+ id: item.id,
465
+ content: item.content,
466
+ type: item.type,
467
+ similarity: item.finalScore,
468
+ importance: item.importance,
469
+ created_at: item.created_at,
470
+ tags: item.tags,
471
+ hop_distance: undefined
472
+ }));
473
+ const queryTime = startTime ? Date.now() - startTime : Date.now() - fallbackStartTime;
474
+ return {
475
+ items: convertedItems,
476
+ total_count: convertedItems.length,
477
+ local_results_count: 0,
478
+ fallback_used: true,
479
+ query_time: queryTime
480
+ };
481
+ }
482
+ catch (error) {
483
+ logger.error('Global search fallback failed', {
484
+ error: error instanceof Error ? error.message : String(error)
485
+ });
486
+ const queryTime = startTime ? Date.now() - startTime : 0;
487
+ return {
488
+ items: [],
489
+ total_count: 0,
490
+ local_results_count: 0,
491
+ fallback_used: true,
492
+ query_time: queryTime
493
+ };
494
+ }
495
+ }
496
+ /**
497
+ * memory_link 테이블을 활용한 직접 연결된 메모리 조회
498
+ */
499
+ async getLinkedMemories(memoryId) {
500
+ if (!this.db) {
501
+ return [];
502
+ }
503
+ try {
504
+ const linkedRecords = this.db.prepare(`
505
+ SELECT
506
+ ml.target_id as memory_id,
507
+ mi.content,
508
+ mi.type,
509
+ mi.importance,
510
+ mi.created_at,
511
+ mi.tags,
512
+ ml.relation_type
513
+ FROM memory_link ml
514
+ JOIN memory_item mi ON mi.id = ml.target_id
515
+ WHERE ml.source_id = ?
516
+ ORDER BY ml.created_at DESC
517
+ `).all(memoryId);
518
+ return linkedRecords.map(record => ({
519
+ memory_id: record.memory_id,
520
+ content: record.content,
521
+ type: record.type,
522
+ similarity: 0.9,
523
+ importance: record.importance,
524
+ created_at: record.created_at,
525
+ tags: record.tags ? (typeof record.tags === 'string' ? JSON.parse(record.tags) : record.tags) : undefined
526
+ }));
527
+ }
528
+ catch (error) {
529
+ logger.error('memory_link retrieval failed', {
530
+ memoryId,
531
+ error: error instanceof Error ? error.message : String(error)
532
+ });
533
+ return [];
534
+ }
535
+ }
536
+ /**
537
+ * 검색 결과 랭킹 점수 계산
538
+ */
539
+ calculateRankingScore(similarity, hopDistance, importance = 0.5) {
540
+ const hopDecayFactor = 1.0 / (1.0 + (hopDistance - 1) * 0.3);
541
+ const anchorProximityBoost = hopDistance === 1 ? 1.2 : 1.0;
542
+ const importanceWeight = 0.1;
543
+ const importanceBoost = 1.0 + (importance - 0.5) * importanceWeight;
544
+ const rankingScore = Math.min(1.0, similarity * hopDecayFactor * anchorProximityBoost * importanceBoost);
545
+ return rankingScore;
546
+ }
547
+ /**
548
+ * 코사인 유사도 계산
549
+ */
550
+ cosineSimilarity(a, b) {
551
+ if (a.length !== b.length) {
552
+ throw new Error('벡터 차원이 일치하지 않습니다');
553
+ }
554
+ let dotProduct = 0;
555
+ let normA = 0;
556
+ let normB = 0;
557
+ for (let i = 0; i < a.length; i++) {
558
+ const aVal = a[i] ?? 0;
559
+ const bVal = b[i] ?? 0;
560
+ dotProduct += aVal * bVal;
561
+ normA += aVal * aVal;
562
+ normB += bVal * bVal;
563
+ }
564
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
565
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
566
+ }
567
+ /**
568
+ * 슬롯별 설정 조회
569
+ */
570
+ getSlotConfig(slot) {
571
+ const slotConfig = {
572
+ A: { hop_limit: 1, vector_threshold: 0.8 },
573
+ B: { hop_limit: 2, vector_threshold: 0.6 },
574
+ C: { hop_limit: 3, vector_threshold: 0.4 }
575
+ };
576
+ return slotConfig[slot];
577
+ }
578
+ /**
579
+ * 자동 앵커 이동 점수 계산
580
+ */
581
+ async calculateReanchorScore(memoryId, queryEmbedding, anchorEmbedding) {
582
+ if (!this.db) {
583
+ return 0;
584
+ }
585
+ try {
586
+ const memory = this.db.prepare(`
587
+ SELECT
588
+ view_count,
589
+ cite_count,
590
+ edit_count,
591
+ last_accessed,
592
+ created_at,
593
+ importance
594
+ FROM memory_item
595
+ WHERE id = ?
596
+ `).get(memoryId);
597
+ if (!memory) {
598
+ return 0;
599
+ }
600
+ const usageScore = Math.min(1.0, (Math.log(1 + memory.view_count) +
601
+ 2 * Math.log(1 + memory.cite_count) +
602
+ 0.5 * Math.log(1 + memory.edit_count)) / 10);
603
+ let recencyScore = 0.5;
604
+ if (memory.last_accessed) {
605
+ const lastAccessed = new Date(memory.last_accessed);
606
+ const now = new Date();
607
+ const daysSinceAccess = (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
608
+ recencyScore = Math.max(0, 1.0 - daysSinceAccess / 30);
609
+ }
610
+ const importanceScore = memory.importance || 0.5;
611
+ let semanticScore = 0.5;
612
+ if (queryEmbedding) {
613
+ const memoryEmbedding = await this.cacheService.getAnchorEmbedding(memoryId);
614
+ if (memoryEmbedding && memoryEmbedding.embedding) {
615
+ const similarity = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
616
+ semanticScore = similarity;
617
+ }
618
+ }
619
+ let anchorComparisonScore = 0.5;
620
+ if (anchorEmbedding) {
621
+ const memoryEmbedding = await this.cacheService.getAnchorEmbedding(memoryId);
622
+ if (memoryEmbedding && memoryEmbedding.embedding) {
623
+ const similarity = this.cosineSimilarity(anchorEmbedding, memoryEmbedding.embedding);
624
+ anchorComparisonScore = 1.0 - similarity;
625
+ }
626
+ }
627
+ const finalScore = usageScore * 0.3 +
628
+ recencyScore * 0.2 +
629
+ importanceScore * 0.2 +
630
+ semanticScore * 0.2 +
631
+ anchorComparisonScore * 0.1;
632
+ return Math.min(1.0, Math.max(0.0, finalScore));
633
+ }
634
+ catch (error) {
635
+ logger.error('Reanchor score calculation failed', {
636
+ memoryId,
637
+ error: error instanceof Error ? error.message : String(error)
638
+ });
639
+ return 0;
640
+ }
641
+ }
642
+ /**
643
+ * 앵커 주변 메모리 사용 패턴 분석
644
+ */
645
+ async analyzeAnchorUsage(agentId, slot, anchorMemoryId, anchorEmbedding, queryEmbedding) {
646
+ if (!this.db) {
647
+ throw new Error('Database is not set.');
648
+ }
649
+ try {
650
+ const slotConfig = this.getSlotConfig(slot);
651
+ const nearbyMemories = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchorMemoryId, slotConfig.vector_threshold * 0.8, slotConfig.hop_limit, 20);
652
+ const candidates = [];
653
+ for (const memory of nearbyMemories) {
654
+ const score = await this.calculateReanchorScore(memory.memory_id, queryEmbedding, anchorEmbedding.embedding);
655
+ if (score > 0.5) {
656
+ const reason = this.generateReanchorReason(memory, score);
657
+ candidates.push({
658
+ memory_id: memory.memory_id,
659
+ score,
660
+ reason
661
+ });
662
+ }
663
+ }
664
+ candidates.sort((a, b) => b.score - a.score);
665
+ return candidates;
666
+ }
667
+ catch (error) {
668
+ logger.error('Anchor usage analysis failed', {
669
+ agentId,
670
+ slot,
671
+ error: error instanceof Error ? error.message : String(error)
672
+ });
673
+ return [];
674
+ }
675
+ }
676
+ /**
677
+ * 앵커 이동 이유 생성
678
+ */
679
+ generateReanchorReason(memory, score) {
680
+ const reasons = [];
681
+ if (score > 0.7) {
682
+ reasons.push('높은 사용 빈도');
683
+ }
684
+ if (memory.similarity && memory.similarity > 0.8) {
685
+ reasons.push('쿼리와 높은 유사도');
686
+ }
687
+ if (memory.hop_distance === 1) {
688
+ reasons.push('앵커와 직접 연결');
689
+ }
690
+ return reasons.length > 0 ? reasons.join(', ') : '종합 점수 우수';
691
+ }
692
+ /**
693
+ * 자동 앵커 이동 실행
694
+ */
695
+ async autoReanchor(agentId, slot, anchorManager, queryEmbedding, threshold = 0.7, strategy = 'gradual') {
696
+ if (!this.db) {
697
+ throw new Error('Database is not set.');
698
+ }
699
+ try {
700
+ const currentAnchor = await anchorManager.getAnchor(agentId, slot);
701
+ if (!currentAnchor || Array.isArray(currentAnchor) || !currentAnchor.memory_id) {
702
+ return {
703
+ moved: false,
704
+ old_anchor: null,
705
+ new_anchor: null,
706
+ score: 0,
707
+ reason: '앵커가 설정되지 않았습니다'
708
+ };
709
+ }
710
+ const anchorEmbedding = await this.cacheService.getAnchorEmbedding(currentAnchor.memory_id);
711
+ if (!anchorEmbedding) {
712
+ return {
713
+ moved: false,
714
+ old_anchor: currentAnchor.memory_id,
715
+ new_anchor: null,
716
+ score: 0,
717
+ reason: '앵커 임베딩을 찾을 수 없습니다'
718
+ };
719
+ }
720
+ const candidates = await this.analyzeAnchorUsage(agentId, slot, currentAnchor.memory_id, anchorEmbedding, queryEmbedding);
721
+ if (candidates.length === 0 || !candidates[0] || candidates[0].score < threshold) {
722
+ return {
723
+ moved: false,
724
+ old_anchor: currentAnchor.memory_id,
725
+ new_anchor: null,
726
+ score: candidates[0]?.score || 0,
727
+ reason: `임계값(${threshold}) 미만 또는 후보 없음`
728
+ };
729
+ }
730
+ const bestCandidate = candidates[0];
731
+ if (!bestCandidate) {
732
+ return {
733
+ moved: false,
734
+ old_anchor: currentAnchor.memory_id,
735
+ new_anchor: null,
736
+ score: 0,
737
+ reason: '후보 없음'
738
+ };
739
+ }
740
+ if (strategy === 'gradual') {
741
+ if (slot === 'A') {
742
+ const bAnchor = await anchorManager.getAnchor(agentId, 'B');
743
+ if (bAnchor && !Array.isArray(bAnchor) && bAnchor.memory_id) {
744
+ await anchorManager.setAnchor(agentId, bAnchor.memory_id, 'C');
745
+ }
746
+ await anchorManager.setAnchor(agentId, currentAnchor.memory_id, 'B');
747
+ }
748
+ else if (slot === 'B') {
749
+ await anchorManager.setAnchor(agentId, currentAnchor.memory_id, 'C');
750
+ }
751
+ await anchorManager.setAnchor(agentId, bestCandidate.memory_id, slot);
752
+ }
753
+ else {
754
+ await anchorManager.setAnchor(agentId, bestCandidate.memory_id, slot);
755
+ }
756
+ logger.info('Auto reanchor completed', {
757
+ agentId,
758
+ slot,
759
+ oldAnchor: currentAnchor.memory_id,
760
+ newAnchor: bestCandidate.memory_id,
761
+ score: bestCandidate.score,
762
+ reason: bestCandidate.reason
763
+ });
764
+ return {
765
+ moved: true,
766
+ old_anchor: currentAnchor.memory_id,
767
+ new_anchor: bestCandidate.memory_id,
768
+ score: bestCandidate.score,
769
+ reason: bestCandidate.reason
770
+ };
771
+ }
772
+ catch (error) {
773
+ logger.error('Auto reanchor failed', {
774
+ agentId,
775
+ slot,
776
+ error: error instanceof Error ? error.message : String(error)
777
+ });
778
+ throw error;
779
+ }
780
+ }
781
+ /**
782
+ * 검색 후 자동 앵커 이동 체크
783
+ */
784
+ async checkAndAutoReanchor(agentId, slot, anchorManager, queryEmbedding, autoMoveEnabled = false) {
785
+ if (!autoMoveEnabled) {
786
+ return null;
787
+ }
788
+ try {
789
+ return await this.autoReanchor(agentId, slot, anchorManager, queryEmbedding, 0.7, 'gradual');
790
+ }
791
+ catch (error) {
792
+ logger.debug('Auto reanchor check failed (ignored)', {
793
+ error: error instanceof Error ? error.message : String(error)
794
+ });
795
+ return null;
796
+ }
797
+ }
798
+ }
799
+ //# sourceMappingURL=anchor-search-service.js.map