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
@@ -1,54 +1,25 @@
1
1
  /**
2
- * Anchor Manager Service
3
- * 앵커 상태 관리 국소 검색 기능 제공
2
+ * Anchor Manager Service (하위 호환성 래퍼)
3
+ * 새로운 구조로 리팩토링된 서비스들을 통합하여 기존 인터페이스 유지
4
+ * Phase 1.1: anchor-manager.ts 리팩토링
4
5
  *
5
- * 클린코드 원칙:
6
- * - 단일 책임 원칙: 앵커 상태 관리 및 국소 검색만 담당
7
- * - 의존성 역전: Database와 다른 서비스에 의존
8
- * - 캐시 최적화: 메모리 캐시를 통한 빠른 읽기 접근
6
+ * @deprecated 새로운 구조에서는 src/services/anchor/ 디렉토리의 서비스들을 직접 사용하세요
9
7
  */
10
8
  import { getVectorSearchEngine } from '../algorithms/vector-search-engine.js';
11
- import { UnifiedEmbeddingService } from './unified-embedding-service.js';
9
+ import { AnchorManager as NewAnchorManager } from './anchor/anchor-manager.js';
10
+ import { AnchorCacheService } from './anchor/anchor-cache-service.js';
11
+ import { AnchorSearchService } from './anchor/anchor-search-service.js';
12
+ import { AnchorError, MemoryNotFoundError } from './anchor/anchor-interfaces.js';
13
+ import { logger } from '../utils/logger.js';
14
+ export { AnchorError, MemoryNotFoundError };
12
15
  /**
13
- * 앵커 설정 에러
14
- */
15
- export class AnchorError extends Error {
16
- constructor(message) {
17
- super(message);
18
- this.name = 'AnchorError';
19
- }
20
- }
21
- /**
22
- * 메모리를 찾을 수 없을 때 발생하는 에러
23
- */
24
- export class MemoryNotFoundError extends Error {
25
- constructor(memoryId) {
26
- super(`Memory with ID '${memoryId}' not found`);
27
- this.name = 'MemoryNotFoundError';
28
- }
29
- }
30
- /**
31
- * Anchor Manager Service
32
- * 앵커 상태 관리 및 국소 검색 기능 제공
16
+ * Anchor Manager Service (하위 호환성 래퍼)
17
+ * 기존 인터페이스를 유지하면서 새로운 구조의 서비스들을 사용
33
18
  */
34
19
  export class AnchorManager {
35
- /**
36
- * 메모리 캐시: agent_id별 슬롯 상태 관리
37
- * Map<agent_id, {A: memory_id | null, B: memory_id | null, C: memory_id | null}>
38
- */
39
- cache = new Map();
40
- /**
41
- * 쿼리 임베딩 서비스
42
- */
43
- queryEmbeddingService = new UnifiedEmbeddingService();
44
- /**
45
- * 슬롯별 설정
46
- */
47
- slotConfig = {
48
- A: { hop_limit: 1, vector_threshold: 0.8 },
49
- B: { hop_limit: 2, vector_threshold: 0.6 },
50
- C: { hop_limit: 3, vector_threshold: 0.4 }
51
- };
20
+ newAnchorManager;
21
+ cacheService;
22
+ searchService;
52
23
  db = null;
53
24
  embeddingService = null;
54
25
  hybridSearchEngine = null;
@@ -57,17 +28,23 @@ export class AnchorManager {
57
28
  * 생성자
58
29
  */
59
30
  constructor() {
60
- console.log('✅ AnchorManager 서비스 초기화 완료');
31
+ // 새로운 구조의 서비스들 초기화
32
+ this.cacheService = new AnchorCacheService();
33
+ this.searchService = new AnchorSearchService(this.cacheService);
34
+ this.newAnchorManager = new NewAnchorManager(this.cacheService, this.searchService);
35
+ logger.info('AnchorManager 서비스 초기화 완료 (하위 호환성 래퍼)');
61
36
  }
62
37
  /**
63
38
  * 데이터베이스 설정
64
- * @param db - 데이터베이스 인스턴스
65
39
  */
66
40
  setDatabase(db) {
67
41
  if (!db) {
68
42
  throw new Error('Database instance is required');
69
43
  }
70
44
  this.db = db;
45
+ this.newAnchorManager.setDatabase(db);
46
+ this.cacheService.setDatabase(db);
47
+ this.searchService.setDatabase(db);
71
48
  // VectorSearchEngine이 설정되어 있으면 초기화
72
49
  if (this.vectorSearchEngine) {
73
50
  this.vectorSearchEngine.initialize(db);
@@ -75,33 +52,33 @@ export class AnchorManager {
75
52
  }
76
53
  /**
77
54
  * 임베딩 서비스 설정
78
- * @param embeddingService - 메모리 임베딩 서비스 인스턴스
79
55
  */
80
56
  setEmbeddingService(embeddingService) {
81
57
  if (!embeddingService) {
82
58
  throw new Error('MemoryEmbeddingService is required');
83
59
  }
84
60
  this.embeddingService = embeddingService;
61
+ this.cacheService.setEmbeddingService(embeddingService);
85
62
  }
86
63
  /**
87
64
  * 하이브리드 검색 엔진 설정
88
- * @param hybridSearchEngine - 하이브리드 검색 엔진 인스턴스
89
65
  */
90
66
  setHybridSearchEngine(hybridSearchEngine) {
91
67
  if (!hybridSearchEngine) {
92
68
  throw new Error('HybridSearchEngine is required');
93
69
  }
94
70
  this.hybridSearchEngine = hybridSearchEngine;
71
+ this.searchService.setHybridSearchEngine(hybridSearchEngine);
95
72
  }
96
73
  /**
97
74
  * 벡터 검색 엔진 설정
98
- * @param vectorSearchEngine - 벡터 검색 엔진 인스턴스
99
75
  */
100
76
  setVectorSearchEngine(vectorSearchEngine) {
101
77
  if (!vectorSearchEngine) {
102
78
  throw new Error('VectorSearchEngine is required');
103
79
  }
104
80
  this.vectorSearchEngine = vectorSearchEngine;
81
+ this.searchService.setVectorSearchEngine(vectorSearchEngine);
105
82
  // 데이터베이스가 이미 설정되어 있으면 초기화
106
83
  if (this.db) {
107
84
  this.vectorSearchEngine.initialize(this.db);
@@ -109,227 +86,92 @@ export class AnchorManager {
109
86
  }
110
87
  /**
111
88
  * 앵커 설정
112
- * @param agentId - 에이전트 ID
113
- * @param memoryId - 메모리 ID
114
- * @param slot - 슬롯 (A, B, C)
115
- * @throws {MemoryNotFoundError} 메모리가 존재하지 않는 경우
116
- * @throws {AnchorError} 동일한 memory_id를 다른 슬롯에 이미 설정한 경우
117
89
  */
118
90
  async setAnchor(agentId, memoryId, slot) {
119
- if (!this.db) {
120
- throw new Error('Database is not set. Call setDatabase() first.');
121
- }
122
- // 메모리 존재 확인
123
- const memory = this.db.prepare(`
124
- SELECT id FROM memory_item WHERE id = ?
125
- `).get(memoryId);
126
- if (!memory) {
127
- throw new MemoryNotFoundError(memoryId);
128
- }
129
- // 동일한 agent_id가 동일한 memory_id를 다른 슬롯에 이미 설정했는지 확인
130
- const existingAnchor = this.db.prepare(`
131
- SELECT slot FROM anchor
132
- WHERE agent_id = ? AND memory_id = ? AND slot != ?
133
- `).get(agentId, memoryId, slot);
134
- if (existingAnchor) {
135
- throw new AnchorError(`Memory '${memoryId}' is already set as anchor in slot '${existingAnchor.slot}'. ` +
136
- `An agent cannot set the same memory in multiple slots.`);
137
- }
138
- // 기존 앵커가 있으면 업데이트, 없으면 삽입
139
- const existing = this.db.prepare(`
140
- SELECT id FROM anchor WHERE agent_id = ? AND slot = ?
141
- `).get(agentId, slot);
142
- if (existing) {
143
- // 업데이트
144
- this.db.prepare(`
145
- UPDATE anchor
146
- SET memory_id = ?, updated_at = CURRENT_TIMESTAMP
147
- WHERE agent_id = ? AND slot = ?
148
- `).run(memoryId, agentId, slot);
149
- }
150
- else {
151
- // 삽입
152
- this.db.prepare(`
153
- INSERT INTO anchor (agent_id, slot, memory_id)
154
- VALUES (?, ?, ?)
155
- `).run(agentId, slot, memoryId);
156
- }
157
- // 캐시 업데이트
158
- this.updateCache(agentId, slot, memoryId);
91
+ return this.newAnchorManager.setAnchor(agentId, memoryId, slot);
159
92
  }
160
93
  /**
161
94
  * 앵커 조회
162
- * @param agentId - 에이전트 ID
163
- * @param slot - 슬롯 (A, B, C), 선택적. 없으면 모든 슬롯 반환
164
- * @returns 앵커 정보 또는 null
165
95
  */
166
96
  async getAnchor(agentId, slot) {
167
- if (!this.db) {
168
- throw new Error('Database is not set. Call setDatabase() first.');
169
- }
170
- // 캐시에서 먼저 확인
171
- const cached = this.cache.get(agentId);
172
- if (slot) {
173
- // 특정 슬롯 조회
174
- const cachedMemoryId = cached?.[slot];
175
- if (cachedMemoryId !== undefined) {
176
- // 캐시에 있으면 DB에서 상세 정보 조회
177
- const anchor = this.db.prepare(`
178
- SELECT agent_id, slot, memory_id, created_at, updated_at
179
- FROM anchor
180
- WHERE agent_id = ? AND slot = ?
181
- `).get(agentId, slot);
182
- return anchor || null;
183
- }
184
- }
185
- else {
186
- // 모든 슬롯 조회
187
- if (cached) {
188
- // 캐시에 있으면 DB에서 상세 정보 조회
189
- const anchors = this.db.prepare(`
190
- SELECT agent_id, slot, memory_id, created_at, updated_at
191
- FROM anchor
192
- WHERE agent_id = ?
193
- ORDER BY slot
194
- `).all(agentId);
195
- return anchors;
196
- }
197
- }
198
- // 캐시에 없으면 DB에서 조회 후 캐시 업데이트
199
- if (slot) {
200
- const anchor = this.db.prepare(`
201
- SELECT agent_id, slot, memory_id, created_at, updated_at
202
- FROM anchor
203
- WHERE agent_id = ? AND slot = ?
204
- `).get(agentId, slot);
205
- if (anchor) {
206
- this.updateCache(agentId, slot, anchor.memory_id);
207
- }
208
- return anchor || null;
209
- }
210
- else {
211
- const anchors = this.db.prepare(`
212
- SELECT agent_id, slot, memory_id, created_at, updated_at
213
- FROM anchor
214
- WHERE agent_id = ?
215
- ORDER BY slot
216
- `).all(agentId);
217
- // 캐시 업데이트
218
- for (const anchor of anchors) {
219
- this.updateCache(anchor.agent_id, anchor.slot, anchor.memory_id);
220
- }
221
- return anchors.length > 0 ? anchors : null;
222
- }
97
+ return this.newAnchorManager.getAnchor(agentId, slot);
223
98
  }
224
99
  /**
225
100
  * 앵커 제거
226
- * @param agentId - 에이전트 ID
227
- * @param slot - 슬롯 (A, B, C), 선택적. 없으면 모든 슬롯 제거
228
101
  */
229
102
  async clearAnchor(agentId, slot) {
230
- if (!this.db) {
231
- throw new Error('Database is not set. Call setDatabase() first.');
232
- }
233
- if (slot) {
234
- // 특정 슬롯 제거
235
- this.db.prepare(`
236
- DELETE FROM anchor WHERE agent_id = ? AND slot = ?
237
- `).run(agentId, slot);
238
- // 캐시 업데이트
239
- this.updateCache(agentId, slot, null);
240
- }
241
- else {
242
- // 모든 슬롯 제거
243
- this.db.prepare(`
244
- DELETE FROM anchor WHERE agent_id = ?
245
- `).run(agentId);
246
- // 캐시에서 제거
247
- this.cache.delete(agentId);
248
- }
103
+ return this.newAnchorManager.clearAnchor(agentId, slot);
249
104
  }
250
105
  /**
251
106
  * 국소 검색
252
- * 앵커 메모리를 기준으로 N-hop 제한 검색 수행
253
- * @param agentId - 에이전트 ID
254
- * @param slot - 슬롯 (A, B, C)
255
- * @param query - 검색 쿼리 (선택적)
256
- * @param hopLimit - Hop 제한 (선택적, 기본값: 슬롯별 설정값)
257
- * @param options - 검색 옵션
258
- * @returns 검색 결과
259
107
  */
260
108
  async searchLocal(agentId, slot, query, hopLimit, options) {
261
- const startTime = Date.now();
262
109
  if (!this.db) {
263
110
  throw new Error('Database is not set. Call setDatabase() first.');
264
111
  }
265
112
  if (!this.embeddingService) {
266
113
  throw new Error('MemoryEmbeddingService is not set. Call setEmbeddingService() first.');
267
114
  }
115
+ const startTime = Date.now();
268
116
  // 앵커 조회
269
117
  const anchor = await this.getAnchor(agentId, slot);
270
118
  if (!anchor || Array.isArray(anchor) || !anchor.memory_id) {
271
- // 앵커가 없거나 memory_id가 NULL인 경우 (Edge Case: 앵커 없음 또는 메모리 삭제)
272
- console.warn(`⚠️ No anchor set for agent '${agentId}' in slot '${slot}' (memory_id is NULL)`);
119
+ logger.warn('No anchor set for agent', { agentId, slot });
273
120
  if (!query) {
274
- // query가 없으면 에러 반환 (앵커 기반 리콜은 앵커가 필수)
275
121
  throw new AnchorError(`No anchor set for agent '${agentId}' in slot '${slot}'. ` +
276
122
  `Anchor is required for anchor-based recall. ` +
277
123
  `If the anchor memory was deleted, please set a new anchor.`);
278
124
  }
279
125
  // query가 있으면 전역 검색으로 fallback
280
- console.log(`🔄 Anchor missing, falling back to global search for query: "${query}"`);
281
- return await this.fallbackToGlobalSearch(query, options, startTime);
126
+ logger.info('Anchor missing, falling back to global search', { query });
127
+ return await this.searchService.fallbackToGlobalSearch(query, options, startTime);
282
128
  }
283
129
  // 슬롯별 설정 가져오기
284
- const slotConfig = this.getSlotConfig(slot);
130
+ const slotConfig = this.newAnchorManager.getSlotConfig(slot);
285
131
  const finalHopLimit = hopLimit ?? slotConfig.hop_limit;
286
- const vectorThreshold = slotConfig.vector_threshold;
287
- // 검색 옵션 기본값
288
- const limit = options?.limit ?? 10;
289
- const minResults = options?.min_results ?? 3;
290
- // 앵커 메모리 임베딩 조회 (Edge Case: 임베딩 없음, 메모리 삭제)
291
- let anchorEmbedding = await this.getAnchorEmbedding(anchor.memory_id);
132
+ // 앵커 메모리 임베딩 조회
133
+ let anchorEmbedding = await this.cacheService.getAnchorEmbedding(anchor.memory_id);
292
134
  if (!anchorEmbedding) {
293
135
  // 임베딩이 없으면 메모리 존재 확인 후 생성 시도
294
136
  const memory = this.db.prepare(`
295
137
  SELECT id, content, type FROM memory_item WHERE id = ?
296
138
  `).get(anchor.memory_id);
297
139
  if (!memory) {
298
- // Edge Case: 메모리가 삭제된 경우
299
- console.warn(`⚠️ Anchor memory '${anchor.memory_id}' not found (may have been deleted)`);
300
- // 앵커를 자동으로 정리 (선택적)
140
+ logger.warn('Anchor memory not found (may have been deleted)', { memoryId: anchor.memory_id });
141
+ // 앵커를 자동으로 정리
301
142
  try {
302
143
  await this.clearAnchor(agentId, slot);
303
- console.log(`🧹 Cleared invalid anchor for agent '${agentId}' in slot '${slot}'`);
144
+ logger.info('Cleared invalid anchor', { agentId, slot });
304
145
  }
305
146
  catch (error) {
306
- console.error(`❌ Failed to clear invalid anchor:`, error);
147
+ logger.error('Failed to clear invalid anchor', {
148
+ error: error instanceof Error ? error.message : String(error)
149
+ });
307
150
  }
308
151
  // query가 있으면 전역 검색으로 fallback
309
152
  if (query) {
310
- console.log(`🔄 Anchor memory deleted, falling back to global search for query: "${query}"`);
311
- return await this.fallbackToGlobalSearch(query, options, startTime);
153
+ logger.info('Anchor memory deleted, falling back to global search', { query });
154
+ return await this.searchService.fallbackToGlobalSearch(query, options, startTime);
312
155
  }
313
156
  // query가 없으면 에러 반환
314
157
  throw new MemoryNotFoundError(anchor.memory_id +
315
158
  ` (Memory may have been deleted. Please set a new anchor.)`);
316
159
  }
317
160
  // Edge Case: 임베딩 없음 - 생성 시도
318
- console.log(`🔄 Generating embedding for anchor memory '${anchor.memory_id}'`);
161
+ logger.info('Generating embedding for anchor memory', { memoryId: anchor.memory_id });
319
162
  const embeddingResult = await this.embeddingService.createAndStoreEmbedding(this.db, memory.id, memory.content, memory.type);
320
163
  if (!embeddingResult) {
321
164
  throw new Error(`Failed to generate embedding for anchor memory '${anchor.memory_id}'. ` +
322
165
  `Please check if the embedding service is available.`);
323
166
  }
324
167
  // 생성된 임베딩 다시 조회
325
- const newEmbedding = await this.getAnchorEmbedding(anchor.memory_id);
168
+ const newEmbedding = await this.cacheService.getAnchorEmbedding(anchor.memory_id);
326
169
  if (!newEmbedding) {
327
170
  throw new Error(`Failed to retrieve newly created embedding for '${anchor.memory_id}'. ` +
328
171
  `Please try again or check the database.`);
329
172
  }
330
- // 새로 생성된 임베딩 사용
331
173
  anchorEmbedding = newEmbedding;
332
- console.log(`✅ Embedding generated and retrieved for anchor memory '${anchor.memory_id}'`);
174
+ logger.info('Embedding generated and retrieved for anchor memory', { memoryId: anchor.memory_id });
333
175
  }
334
176
  // VectorSearchEngine이 없으면 생성
335
177
  if (!this.vectorSearchEngine) {
@@ -337,771 +179,56 @@ export class AnchorManager {
337
179
  if (this.db) {
338
180
  this.vectorSearchEngine.initialize(this.db);
339
181
  }
182
+ this.searchService.setVectorSearchEngine(this.vectorSearchEngine);
340
183
  }
341
- // N-hop 검색 구현
342
- const allHopResults = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchor.memory_id, vectorThreshold, finalHopLimit, limit * 2 // 더 많이 가져와서 필터링 후 최종 limit 적용
343
- );
344
- // 쿼리가 있는 경우 쿼리 기반 필터링 (작업 3.7)
345
- let filteredResults = allHopResults;
346
- let queryEmbeddingForReanchor;
347
- if (query && query.trim().length > 0) {
348
- filteredResults = await this.filterByQuery(query, allHopResults, anchorEmbedding.provider);
349
- // 자동 앵커 이동을 위한 쿼리 임베딩 생성 (선택적, 비동기)
184
+ // 검색 실행
185
+ const searchResult = await this.searchService.searchLocal(agentId, slot, query, finalHopLimit, options, anchor.memory_id, anchorEmbedding, startTime);
186
+ // 자동 앵커 이동 체크 (query가 있고 queryEmbeddingForReanchor가 생성된 경우)
187
+ if (query && options?.autoMoveEnabled !== false) {
350
188
  try {
351
- const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
189
+ // 쿼리 임베딩 생성
190
+ const { UnifiedEmbeddingService } = await import('./unified-embedding-service.js');
191
+ const queryEmbeddingService = new UnifiedEmbeddingService();
192
+ const queryEmbeddingResult = await queryEmbeddingService.generateEmbedding(query);
352
193
  if (queryEmbeddingResult && queryEmbeddingResult.embedding) {
353
- queryEmbeddingForReanchor = queryEmbeddingResult.embedding;
194
+ await this.searchService.checkAndAutoReanchor(agentId, slot, this.newAnchorManager, queryEmbeddingResult.embedding, true);
354
195
  }
355
196
  }
356
- catch (error) {
357
- // 쿼리 임베딩 생성 실패는 무시 (자동 이동은 선택적)
358
- console.debug('쿼리 임베딩 생성 실패 (자동 앵커 이동용):', error);
359
- }
360
- }
361
- // 결과 포맷팅
362
- const formattedResults = filteredResults.map(result => ({
363
- id: result.memory_id,
364
- content: result.content,
365
- type: result.type,
366
- similarity: result.similarity,
367
- hop_distance: result.hop_distance,
368
- importance: result.importance,
369
- created_at: result.created_at,
370
- tags: result.tags
371
- }));
372
- // 최종 limit 적용
373
- const localResults = formattedResults.slice(0, limit);
374
- const localCount = localResults.length;
375
- // Fallback 체크 (query가 있을 때만, min_results 미만 시)
376
- let fallbackUsed = false;
377
- let finalResults = localResults;
378
- let totalCount = localCount;
379
- if (query && query.trim().length > 0 && localCount < minResults) {
380
- try {
381
- console.log(`🔄 Fallback to global search: local results (${localCount}) < min_results (${minResults})`);
382
- // Fallback 수행
383
- const fallbackResult = await this.fallbackToGlobalSearch(query, { ...options, limit: limit - localCount }, // 부족한 만큼만 가져오기
384
- startTime);
385
- fallbackUsed = true;
386
- // Local 결과와 Fallback 결과 병합
387
- // Local 결과를 우선하고, 중복 제거 (memory_id 기준)
388
- const localMemoryIds = new Set(localResults.map(r => r.id));
389
- const fallbackItems = fallbackResult.items
390
- .filter(item => !localMemoryIds.has(item.id))
391
- .map(item => ({
392
- id: item.id,
393
- content: item.content,
394
- type: item.type,
395
- similarity: item.similarity ?? 0,
396
- hop_distance: item.hop_distance ?? 999, // fallback 결과는 hop_distance가 없으므로 큰 값으로 설정
397
- importance: item.importance ?? 0.5,
398
- created_at: item.created_at ?? new Date().toISOString(),
399
- tags: item.tags ?? undefined
400
- }));
401
- // Local 결과 + Fallback 결과 (중복 제거된 것만)
402
- finalResults = [...localResults, ...fallbackItems].slice(0, limit);
403
- totalCount = finalResults.length;
404
- console.log(`✅ Fallback 완료: local ${localCount} + fallback ${fallbackItems.length} = total ${totalCount}`);
405
- }
406
- catch (error) {
407
- console.error('❌ Fallback 실패:', error);
408
- // Fallback 실패 시 local 결과만 반환
409
- fallbackUsed = false;
410
- }
411
- }
412
- // 자동 앵커 이동 체크 (query가 있고 queryEmbeddingForReanchor가 생성된 경우)
413
- // 기본적으로 활성화되어 있으며, options에 autoMoveEnabled: false를 전달하여 비활성화 가능
414
- if (queryEmbeddingForReanchor && options?.autoMoveEnabled !== false) {
415
- try {
416
- await this.checkAndAutoReanchor(agentId, slot, queryEmbeddingForReanchor, true);
417
- }
418
197
  catch (error) {
419
198
  // 자동 앵커 이동 실패는 검색 결과에 영향을 주지 않음
420
- console.debug('⚠️ 자동 앵커 이동 체크 실패 (무시됨):', error);
199
+ logger.debug('Auto anchor move check failed (ignored)', {
200
+ error: error instanceof Error ? error.message : String(error)
201
+ });
421
202
  }
422
203
  }
423
- const queryTime = Date.now() - startTime;
424
- return {
425
- items: finalResults,
426
- total_count: totalCount,
427
- local_results_count: localCount,
428
- fallback_used: fallbackUsed,
429
- query_time: queryTime,
430
- anchor_info: {
431
- agent_id: agentId,
432
- slot: slot,
433
- memory_id: anchor.memory_id
434
- }
435
- };
204
+ return searchResult;
436
205
  }
437
206
  /**
438
207
  * 앵커 메모리의 임베딩 조회
439
- * @param memoryId - 메모리 ID
440
- * @returns 임베딩 벡터 및 제공자 정보, 없으면 null
441
- * @throws {MemoryNotFoundError} 메모리가 삭제된 경우
442
208
  */
443
209
  async getAnchorEmbedding(memoryId) {
444
- if (!this.db) {
445
- throw new Error('Database is not set.');
446
- }
447
- try {
448
- // Edge Case: 메모리 존재 확인 (메모리 삭제 체크)
449
- const memoryExists = this.db.prepare(`
450
- SELECT id FROM memory_item WHERE id = ?
451
- `).get(memoryId);
452
- if (!memoryExists) {
453
- // 메모리가 삭제된 경우
454
- console.warn(`⚠️ Memory '${memoryId}' not found (may have been deleted)`);
455
- return null;
456
- }
457
- const embeddingRecord = this.db.prepare(`
458
- SELECT
459
- embedding,
460
- embedding_provider,
461
- dimensions,
462
- dim
463
- FROM memory_embedding
464
- WHERE memory_id = ?
465
- ORDER BY created_at DESC
466
- LIMIT 1
467
- `).get(memoryId);
468
- if (!embeddingRecord || !embeddingRecord.embedding) {
469
- // Edge Case: 임베딩 없음 (메모리는 존재하지만 임베딩이 없음)
470
- return null;
471
- }
472
- // JSON 문자열로 저장된 임베딩을 배열로 파싱
473
- let embeddingVector;
474
- try {
475
- embeddingVector = typeof embeddingRecord.embedding === 'string'
476
- ? JSON.parse(embeddingRecord.embedding)
477
- : embeddingRecord.embedding;
478
- if (!Array.isArray(embeddingVector) || embeddingVector.length === 0) {
479
- // Edge Case: 유효하지 않은 임베딩
480
- console.warn(`⚠️ Invalid embedding for memory '${memoryId}' (empty or not an array)`);
481
- return null;
482
- }
483
- }
484
- catch (error) {
485
- // Edge Case: 임베딩 파싱 실패
486
- console.error(`❌ 임베딩 파싱 실패 (${memoryId}):`, error);
487
- return null;
488
- }
489
- const provider = embeddingRecord.embedding_provider || 'tfidf';
490
- return {
491
- embedding: embeddingVector,
492
- provider: provider
493
- };
494
- }
495
- catch (error) {
496
- // Edge Case: 데이터베이스 오류
497
- console.error(`❌ 임베딩 조회 실패 (${memoryId}):`, error);
498
- return null;
499
- }
500
- }
501
- /**
502
- * 1-hop 검색: 앵커와 직접적으로 유사한 메모리 검색
503
- * @param anchorEmbedding - 앵커 메모리의 임베딩 벡터
504
- * @param provider - 임베딩 제공자
505
- * @param anchorMemoryId - 앵커 메모리 ID (제외할 메모리)
506
- * @param threshold - 유사도 임계값
507
- * @param limit - 최대 결과 수
508
- * @returns 검색 결과
509
- */
510
- async searchOneHop(anchorEmbedding, provider, anchorMemoryId, threshold, limit) {
511
- if (!this.vectorSearchEngine || !this.db) {
512
- throw new Error('VectorSearchEngine or Database is not set.');
513
- }
514
- try {
515
- // VectorSearchEngine 초기화 확인
516
- if (typeof this.vectorSearchEngine.initialize === 'function') {
517
- this.vectorSearchEngine.initialize(this.db);
518
- }
519
- // 벡터 검색 실행 (임계값은 낮게 설정하고 나중에 필터링)
520
- const searchResults = await this.vectorSearchEngine.search(anchorEmbedding, {
521
- limit: limit + 1, // 자기 자신 제외를 위해 +1
522
- threshold: 0.0, // 임계값은 나중에 필터링에서 적용
523
- includeContent: true,
524
- includeMetadata: true
525
- }, provider);
526
- // 결과 필터링: 앵커 메모리 제외, 유사도 임계값 이상만 반환
527
- const filteredResults = searchResults
528
- .filter(result => {
529
- // 앵커 메모리 제외
530
- if (result.memory_id === anchorMemoryId) {
531
- return false;
532
- }
533
- // 유사도 임계값 이상만 반환
534
- return result.similarity >= threshold;
535
- })
536
- .slice(0, limit); // 최종 limit 적용
537
- return filteredResults;
538
- }
539
- catch (error) {
540
- console.error(`❌ 1-hop 검색 실패:`, error);
541
- throw new Error(`Failed to perform 1-hop search: ${error instanceof Error ? error.message : 'Unknown error'}`);
542
- }
543
- }
544
- /**
545
- * N-hop 검색: 앵커를 기준으로 최대 N-hop까지 확장 검색
546
- * @param anchorEmbedding - 앵커 메모리의 임베딩 벡터
547
- * @param provider - 임베딩 제공자
548
- * @param anchorMemoryId - 앵커 메모리 ID (제외할 메모리)
549
- * @param threshold - 유사도 임계값
550
- * @param maxHops - 최대 hop 수
551
- * @param limit - 최대 결과 수 (전체 hop 합계)
552
- * @returns 검색 결과 (hop_distance 포함)
553
- */
554
- async searchNHop(anchorEmbedding, provider, anchorMemoryId, threshold, maxHops, limit) {
555
- if (!this.vectorSearchEngine || !this.db) {
556
- throw new Error('VectorSearchEngine or Database is not set.');
557
- }
558
- // VectorSearchEngine 초기화 확인
559
- if (typeof this.vectorSearchEngine.initialize === 'function') {
560
- this.vectorSearchEngine.initialize(this.db);
561
- }
562
- // 이미 발견된 메모리 ID 추적 (중복 방지)
563
- const discoveredMemoryIds = new Set([anchorMemoryId]);
564
- // 각 hop 레벨의 결과를 저장
565
- const allResults = [];
566
- // 현재 hop 레벨의 메모리들 (임베딩 포함)
567
- // 1-hop: 앵커 임베딩을 사용
568
- let currentHopMemories = [
569
- { memory_id: anchorMemoryId, embedding: anchorEmbedding }
570
- ];
571
- // 각 hop 레벨별로 검색 수행
572
- for (let hop = 1; hop <= maxHops; hop++) {
573
- const nextHopMemories = [];
574
- const hopResults = [];
575
- // 현재 hop의 각 메모리에 대해 검색 수행
576
- for (const currentMemory of currentHopMemories) {
577
- try {
578
- // memory_link를 활용한 직접 연결된 메모리 조회 (최적화)
579
- const linkedMemories = await this.getLinkedMemories(currentMemory.memory_id);
580
- // 벡터 검색 실행
581
- const vectorSearchResults = await this.vectorSearchEngine.search(currentMemory.embedding, {
582
- limit: Math.ceil(limit / maxHops) + 10, // 각 hop당 충분한 결과 가져오기
583
- threshold: 0.0, // 임계값은 나중에 필터링에서 적용
584
- includeContent: true,
585
- includeMetadata: true
586
- }, provider);
587
- // 디버깅: 벡터 검색 결과 로깅
588
- if (hop === 1 && currentMemory.memory_id === anchorMemoryId) {
589
- console.log(`🔍 [Debug] 벡터 검색 결과 (${hop}-hop, 앵커: ${anchorMemoryId}):`, {
590
- totalResults: vectorSearchResults.length,
591
- top5Similarities: vectorSearchResults.slice(0, 5).map(r => ({
592
- memory_id: r.memory_id,
593
- similarity: r.similarity.toFixed(4)
594
- })),
595
- threshold,
596
- provider
597
- });
598
- }
599
- // memory_link 결과와 벡터 검색 결과를 병합
600
- // memory_link 결과는 우선순위가 높음 (직접 연결된 관계)
601
- const allCandidates = new Map();
602
- // memory_link 결과 추가 (우선순위 높음)
603
- for (const linked of linkedMemories) {
604
- if (!discoveredMemoryIds.has(linked.memory_id)) {
605
- allCandidates.set(linked.memory_id, {
606
- ...linked,
607
- isLinked: true
608
- });
609
- }
610
- }
611
- // 벡터 검색 결과 추가 (memory_link에 없는 경우만)
612
- // 임계값을 낮춰서 더 많은 결과를 포함 (나중에 effectiveThreshold로 재필터링)
613
- const relaxedThreshold = threshold * 0.5; // 임계값을 50%로 완화하여 후보 확보
614
- for (const result of vectorSearchResults) {
615
- if (!allCandidates.has(result.memory_id) && !discoveredMemoryIds.has(result.memory_id)) {
616
- // 완화된 임계값으로 후보 추가 (나중에 effectiveThreshold로 재필터링)
617
- if (result.similarity >= relaxedThreshold) {
618
- allCandidates.set(result.memory_id, {
619
- ...result,
620
- isLinked: false
621
- });
622
- }
623
- }
624
- else if (allCandidates.has(result.memory_id)) {
625
- // memory_link로 이미 추가된 경우, 유사도 정보 업데이트
626
- const existing = allCandidates.get(result.memory_id);
627
- existing.similarity = Math.max(existing.similarity, result.similarity);
628
- }
629
- }
630
- // 결과 필터링 및 추가
631
- for (const [memoryId, candidate] of allCandidates.entries()) {
632
- // 이미 발견된 메모리 제외
633
- if (discoveredMemoryIds.has(memoryId)) {
634
- continue;
635
- }
636
- // 유사도 임계값 이상만 반환 (memory_link는 임계값 완화 가능)
637
- const effectiveThreshold = candidate.isLinked
638
- ? threshold * 0.8 // memory_link 연결은 임계값 20% 완화
639
- : threshold;
640
- if (candidate.similarity < effectiveThreshold) {
641
- continue;
642
- }
643
- // 새로 발견된 메모리
644
- discoveredMemoryIds.add(memoryId);
645
- hopResults.push({
646
- memory_id: candidate.memory_id,
647
- content: candidate.content,
648
- type: candidate.type,
649
- similarity: candidate.similarity,
650
- importance: candidate.importance,
651
- created_at: candidate.created_at,
652
- tags: candidate.tags
653
- });
654
- // 다음 hop을 위한 임베딩 조회 (다음 hop이 있을 경우)
655
- // Edge Case: 중간 hop의 메모리가 삭제되거나 임베딩이 없는 경우 무시
656
- if (hop < maxHops) {
657
- try {
658
- const nextEmbedding = await this.getAnchorEmbedding(candidate.memory_id);
659
- if (nextEmbedding && nextEmbedding.embedding) {
660
- nextHopMemories.push({
661
- memory_id: candidate.memory_id,
662
- embedding: nextEmbedding.embedding
663
- });
664
- }
665
- else {
666
- // 임베딩이 없으면 다음 hop에서 제외 (경고 없이)
667
- console.debug(`⚠️ Skipping memory '${candidate.memory_id}' for next hop: no embedding`);
668
- }
669
- }
670
- catch (error) {
671
- // 메모리 삭제 또는 임베딩 조회 실패 시 다음 hop에서 제외
672
- console.debug(`⚠️ Skipping memory '${candidate.memory_id}' for next hop: ${error instanceof Error ? error.message : 'Unknown error'}`);
673
- }
674
- }
675
- }
676
- }
677
- catch (error) {
678
- console.error(`❌ ${hop}-hop 검색 실패 (${currentMemory.memory_id}):`, error);
679
- // 개별 메모리 검색 실패는 무시하고 계속 진행
680
- continue;
681
- }
682
- }
683
- // 현재 hop의 결과를 전체 결과에 추가 (hop_distance 설정)
684
- for (const result of hopResults) {
685
- allResults.push({
686
- ...result,
687
- hop_distance: hop
688
- });
689
- }
690
- // limit에 도달했으면 중단
691
- if (allResults.length >= limit) {
692
- break;
693
- }
694
- // 다음 hop을 위한 메모리가 없으면 중단
695
- if (nextHopMemories.length === 0) {
696
- break;
697
- }
698
- // 다음 hop을 위한 메모리로 업데이트
699
- currentHopMemories = nextHopMemories;
700
- }
701
- // 랭킹 점수 계산 및 적용 (hop 거리 기반 점수 + 앵커 근처 부스트)
702
- const rankedResults = allResults.map(result => {
703
- const rankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
704
- return {
705
- ...result,
706
- similarity: rankingScore // 랭킹 점수로 업데이트
707
- };
708
- });
709
- // 랭킹 점수 기준으로 정렬 (높은 점수 우선)
710
- rankedResults.sort((a, b) => {
711
- // 점수가 같으면 hop 거리가 가까운 것 우선
712
- if (Math.abs(a.similarity - b.similarity) < 0.001) {
713
- return a.hop_distance - b.hop_distance;
714
- }
715
- // 점수가 높은 것 우선
716
- return b.similarity - a.similarity;
717
- });
718
- // 최종 limit 적용
719
- return rankedResults.slice(0, limit);
720
- }
721
- /**
722
- * 쿼리 기반 필터링: 앵커 주변 검색 결과 중 쿼리와 관련된 메모리만 필터링
723
- * @param query - 검색 쿼리
724
- * @param results - 앵커 주변 검색 결과
725
- * @param provider - 임베딩 제공자
726
- * @returns 필터링된 결과 (쿼리 관련성 점수 포함)
727
- */
728
- async filterByQuery(query, results, provider) {
729
- if (results.length === 0) {
730
- return results;
731
- }
732
- try {
733
- // 1. 쿼리 임베딩 생성
734
- const queryEmbeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
735
- if (!queryEmbeddingResult || !queryEmbeddingResult.embedding) {
736
- console.warn('⚠️ 쿼리 임베딩 생성 실패, 필터링 건너뜀');
737
- return results;
738
- }
739
- const queryEmbedding = queryEmbeddingResult.embedding;
740
- // 2. 각 결과 메모리의 임베딩 조회 및 쿼리 유사도 계산
741
- const resultsWithQuerySimilarity = await Promise.all(results.map(async (result) => {
742
- try {
743
- // 메모리 임베딩 조회
744
- const memoryEmbedding = await this.getAnchorEmbedding(result.memory_id);
745
- if (!memoryEmbedding || !memoryEmbedding.embedding) {
746
- // 임베딩이 없으면 쿼리 유사도 0으로 설정
747
- return {
748
- ...result,
749
- query_similarity: 0,
750
- combined_similarity: result.similarity * 0.5 // 앵커 유사도만 반영
751
- };
752
- }
753
- // 쿼리 임베딩과 메모리 임베딩 간 유사도 계산
754
- // 차원이 다를 수 있으므로 호환성 확인
755
- let querySim = 0;
756
- if (queryEmbedding.length === memoryEmbedding.embedding.length) {
757
- querySim = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
758
- }
759
- else {
760
- // 차원이 다르면 텍스트 기반 간단한 매칭 (fallback)
761
- const queryLower = query.toLowerCase();
762
- const contentLower = result.content.toLowerCase();
763
- const queryWords = queryLower.split(/\s+/);
764
- const matchCount = queryWords.filter(word => contentLower.includes(word)).length;
765
- querySim = matchCount / Math.max(queryWords.length, 1);
766
- }
767
- // hop 거리 기반 랭킹 점수 계산
768
- const baseRankingScore = this.calculateRankingScore(result.similarity, result.hop_distance, result.importance);
769
- // 결합 유사도: 랭킹 점수(60%) + 쿼리 유사도(40%)
770
- const combinedSimilarity = baseRankingScore * 0.6 + querySim * 0.4;
771
- return {
772
- ...result,
773
- query_similarity: querySim,
774
- combined_similarity: combinedSimilarity
775
- };
776
- }
777
- catch (error) {
778
- console.error(`❌ 쿼리 필터링 실패 (${result.memory_id}):`, error);
779
- // 에러 발생 시 원본 similarity 사용
780
- return {
781
- ...result,
782
- query_similarity: 0,
783
- combined_similarity: result.similarity * 0.5
784
- };
785
- }
786
- }));
787
- // 3. 쿼리 유사도 임계값 적용 (0.3 이상만 유지)
788
- const queryThreshold = 0.3;
789
- const filtered = resultsWithQuerySimilarity.filter(r => r.query_similarity >= queryThreshold || r.combined_similarity >= 0.5);
790
- // 4. 결합 유사도 기준으로 재정렬
791
- filtered.sort((a, b) => {
792
- // 결합 유사도가 같으면 hop 거리가 가까운 것 우선
793
- if (Math.abs(a.combined_similarity - b.combined_similarity) < 0.001) {
794
- return a.hop_distance - b.hop_distance;
795
- }
796
- // 결합 유사도가 높은 것 우선
797
- return b.combined_similarity - a.combined_similarity;
798
- });
799
- // 5. 원본 similarity를 combined_similarity로 업데이트하여 반환
800
- return filtered.map(r => ({
801
- ...r,
802
- similarity: r.combined_similarity
803
- }));
804
- }
805
- catch (error) {
806
- console.error('❌ 쿼리 기반 필터링 실패:', error);
807
- // 에러 발생 시 원본 결과 반환
808
- return results;
809
- }
810
- }
811
- /**
812
- * 검색 결과 랭킹 점수 계산 (hop 거리 기반 점수 + 앵커 근처 부스트)
813
- * @param similarity - 벡터 유사도 (0-1)
814
- * @param hopDistance - hop 거리 (1부터 시작)
815
- * @param importance - 메모리 중요도 (0-1)
816
- * @returns 랭킹 점수 (0-1)
817
- */
818
- calculateRankingScore(similarity, hopDistance, importance = 0.5) {
819
- // 1. Hop 거리 기반 점수 감쇠 (거리가 멀수록 점수 감소)
820
- // hop_distance=1: 1.0, hop_distance=2: 0.7, hop_distance=3: 0.5
821
- const hopDecayFactor = 1.0 / (1.0 + (hopDistance - 1) * 0.3);
822
- // 2. 앵커 근처 부스트 (1-hop은 추가 부스트)
823
- const anchorProximityBoost = hopDistance === 1 ? 1.2 : 1.0; // 1-hop은 20% 부스트
824
- // 3. 중요도 가중치 (0.1 가중치)
825
- const importanceWeight = 0.1;
826
- const importanceBoost = 1.0 + (importance - 0.5) * importanceWeight;
827
- // 4. 최종 랭킹 점수 계산
828
- // 기본 공식: similarity * hop_decay * proximity_boost * importance_boost
829
- // 점수가 1.0을 초과하지 않도록 클램프
830
- const rankingScore = Math.min(1.0, similarity * hopDecayFactor * anchorProximityBoost * importanceBoost);
831
- return rankingScore;
832
- }
833
- /**
834
- * 코사인 유사도 계산
835
- * @param a - 벡터 A
836
- * @param b - 벡터 B
837
- * @returns 코사인 유사도 (0-1)
838
- */
839
- cosineSimilarity(a, b) {
840
- if (a.length !== b.length) {
841
- throw new Error('벡터 차원이 일치하지 않습니다');
842
- }
843
- let dotProduct = 0;
844
- let normA = 0;
845
- let normB = 0;
846
- for (let i = 0; i < a.length; i++) {
847
- const aVal = a[i] ?? 0;
848
- const bVal = b[i] ?? 0;
849
- dotProduct += aVal * bVal;
850
- normA += aVal * aVal;
851
- normB += bVal * bVal;
852
- }
853
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
854
- return magnitude === 0 ? 0 : dotProduct / magnitude;
855
- }
856
- /**
857
- * memory_link 테이블을 활용한 직접 연결된 메모리 조회
858
- * @param memoryId - 메모리 ID
859
- * @returns 연결된 메모리 목록 (임베딩 정보 포함)
860
- */
861
- async getLinkedMemories(memoryId) {
862
- if (!this.db) {
863
- return [];
864
- }
865
- try {
866
- // memory_link를 통해 연결된 메모리 조회
867
- const linkedRecords = this.db.prepare(`
868
- SELECT
869
- ml.target_id as memory_id,
870
- mi.content,
871
- mi.type,
872
- mi.importance,
873
- mi.created_at,
874
- mi.tags,
875
- ml.relation_type
876
- FROM memory_link ml
877
- JOIN memory_item mi ON mi.id = ml.target_id
878
- WHERE ml.source_id = ?
879
- ORDER BY ml.created_at DESC
880
- `).all(memoryId);
881
- // 결과 포맷팅
882
- return linkedRecords.map(record => ({
883
- memory_id: record.memory_id,
884
- content: record.content,
885
- type: record.type,
886
- similarity: 0.9, // memory_link 연결은 높은 유사도로 간주
887
- importance: record.importance,
888
- created_at: record.created_at,
889
- tags: record.tags ? (typeof record.tags === 'string' ? JSON.parse(record.tags) : record.tags) : undefined
890
- }));
891
- }
892
- catch (error) {
893
- console.error(`❌ memory_link 조회 실패 (${memoryId}):`, error);
894
- return [];
895
- }
896
- }
897
- /**
898
- * 전역 검색으로 Fallback
899
- * @param query - 검색 쿼리
900
- * @param options - 검색 옵션
901
- * @param startTime - 시작 시간
902
- * @returns 검색 결과
903
- */
904
- async fallbackToGlobalSearch(query, options, startTime) {
905
- if (!this.hybridSearchEngine) {
906
- throw new Error('HybridSearchEngine is not set. Call setHybridSearchEngine() first.');
907
- }
908
- if (!this.db) {
909
- throw new Error('Database is not set.');
910
- }
911
- const limit = options?.limit ?? 10;
912
- const fallbackStartTime = Date.now();
913
- try {
914
- // HybridSearchEngine을 사용한 전역 검색
915
- const globalSearchResult = await this.hybridSearchEngine.search(this.db, {
916
- query: query,
917
- limit: limit,
918
- vectorWeight: options?.vector_weight,
919
- textWeight: options?.text_weight
920
- });
921
- // HybridSearchResult를 SearchResult 형식으로 변환
922
- const convertedItems = globalSearchResult.items.map(item => ({
923
- id: item.id,
924
- content: item.content,
925
- type: item.type,
926
- similarity: item.finalScore, // finalScore를 similarity로 사용
927
- importance: item.importance,
928
- created_at: item.created_at,
929
- tags: item.tags,
930
- // fallback 결과는 hop_distance가 없음 (전역 검색이므로)
931
- hop_distance: undefined
932
- }));
933
- const queryTime = startTime ? Date.now() - startTime : Date.now() - fallbackStartTime;
934
- return {
935
- items: convertedItems,
936
- total_count: convertedItems.length,
937
- local_results_count: 0, // 전역 검색이므로 local 결과는 0
938
- fallback_used: true,
939
- query_time: queryTime
940
- };
941
- }
942
- catch (error) {
943
- console.error('❌ 전역 검색 Fallback 실패:', error);
944
- const queryTime = startTime ? Date.now() - startTime : 0;
945
- // 에러 발생 시 빈 결과 반환
946
- return {
947
- items: [],
948
- total_count: 0,
949
- local_results_count: 0,
950
- fallback_used: true,
951
- query_time: queryTime
952
- };
953
- }
210
+ return this.cacheService.getAnchorEmbedding(memoryId);
954
211
  }
955
212
  /**
956
213
  * 서버 재시작 시 DB에서 캐시 복원
957
- * @param db - 데이터베이스 인스턴스
958
214
  */
959
215
  async restoreCacheFromDB(db) {
960
- if (!db) {
961
- throw new Error('Database instance is required');
962
- }
963
- try {
964
- // anchor 테이블 존재 여부 확인
965
- const tableExists = db.prepare(`
966
- SELECT name FROM sqlite_master
967
- WHERE type='table' AND name='anchor'
968
- `).get();
969
- if (!tableExists) {
970
- // 테이블이 없으면 빈 캐시로 시작 (마이그레이션이 아직 실행되지 않았을 수 있음)
971
- this.cache.clear();
972
- console.log('⚠️ Anchor table does not exist yet, starting with empty cache');
973
- return;
974
- }
975
- const anchors = db.prepare(`
976
- SELECT agent_id, slot, memory_id
977
- FROM anchor
978
- ORDER BY agent_id, slot
979
- `).all();
980
- // 캐시 초기화
981
- this.cache.clear();
982
- // DB 데이터로 캐시 복원
983
- for (const anchor of anchors) {
984
- const agentId = anchor.agent_id;
985
- const slot = anchor.slot;
986
- const memoryId = anchor.memory_id;
987
- if (!this.cache.has(agentId)) {
988
- this.cache.set(agentId, { A: null, B: null, C: null });
989
- }
990
- const agentCache = this.cache.get(agentId);
991
- agentCache[slot] = memoryId;
992
- }
993
- console.log(`✅ Anchor cache restored: ${this.cache.size} agents`);
994
- }
995
- catch (error) {
996
- // 에러 발생 시 빈 캐시로 시작 (테이블이 없거나 다른 문제)
997
- this.cache.clear();
998
- console.warn('⚠️ Failed to restore anchor cache from DB, starting with empty cache:', error instanceof Error ? error.message : String(error));
999
- }
1000
- }
1001
- /**
1002
- * 캐시 업데이트 헬퍼 메서드
1003
- * @param agentId - 에이전트 ID
1004
- * @param slot - 슬롯
1005
- * @param memoryId - 메모리 ID (null이면 제거)
1006
- */
1007
- updateCache(agentId, slot, memoryId) {
1008
- if (!this.cache.has(agentId)) {
1009
- this.cache.set(agentId, { A: null, B: null, C: null });
1010
- }
1011
- const agentCache = this.cache.get(agentId);
1012
- agentCache[slot] = memoryId;
216
+ return this.cacheService.restoreCacheFromDB(db);
1013
217
  }
1014
218
  /**
1015
219
  * 슬롯별 설정 조회
1016
- * @param slot - 슬롯
1017
- * @returns 슬롯 설정 (hop_limit, vector_threshold)
1018
220
  */
1019
221
  getSlotConfig(slot) {
1020
- return this.slotConfig[slot];
222
+ return this.newAnchorManager.getSlotConfig(slot);
1021
223
  }
1022
224
  /**
1023
- * 자동 앵커 이동 점수 계산
1024
- * 사용 빈도와 의미적 거리를 종합한 점수
1025
- * @param memoryId - 메모리 ID
1026
- * @param queryEmbedding - 검색 쿼리 임베딩 (선택적)
1027
- * @param anchorEmbedding - 현재 앵커 임베딩 (선택적)
1028
- * @returns 앵커 이동 점수 (0-1)
225
+ * 자동 앵커 이동 실행
1029
226
  */
1030
- async calculateReanchorScore(memoryId, queryEmbedding, anchorEmbedding) {
1031
- if (!this.db) {
1032
- return 0;
1033
- }
1034
- try {
1035
- // 1. 사용 빈도 점수 계산 (view_count, cite_count, last_accessed 기반)
1036
- const memory = this.db.prepare(`
1037
- SELECT
1038
- view_count,
1039
- cite_count,
1040
- edit_count,
1041
- last_accessed,
1042
- created_at,
1043
- importance
1044
- FROM memory_item
1045
- WHERE id = ?
1046
- `).get(memoryId);
1047
- if (!memory) {
1048
- return 0;
1049
- }
1050
- // 사용 빈도 점수 (로그 스케일)
1051
- const usageScore = Math.min(1.0, (Math.log(1 + memory.view_count) +
1052
- 2 * Math.log(1 + memory.cite_count) +
1053
- 0.5 * Math.log(1 + memory.edit_count)) / 10 // 정규화
1054
- );
1055
- // 최근성 점수 (last_accessed 기반, 최근일수록 높음)
1056
- let recencyScore = 0.5; // 기본값
1057
- if (memory.last_accessed) {
1058
- const lastAccessed = new Date(memory.last_accessed);
1059
- const now = new Date();
1060
- const daysSinceAccess = (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
1061
- // 7일 이내면 높은 점수, 그 이후로 감소
1062
- recencyScore = Math.max(0, 1.0 - daysSinceAccess / 30);
1063
- }
1064
- // 중요도 점수
1065
- const importanceScore = memory.importance || 0.5;
1066
- // 2. 의미적 거리 점수 계산 (query와의 유사도)
1067
- let semanticScore = 0.5; // 기본값
1068
- if (queryEmbedding) {
1069
- const memoryEmbedding = await this.getAnchorEmbedding(memoryId);
1070
- if (memoryEmbedding && memoryEmbedding.embedding) {
1071
- const similarity = this.cosineSimilarity(queryEmbedding, memoryEmbedding.embedding);
1072
- semanticScore = similarity;
1073
- }
1074
- }
1075
- // 3. 현재 앵커와의 비교 (현재 앵커보다 더 나은지)
1076
- let anchorComparisonScore = 0.5; // 기본값
1077
- if (anchorEmbedding) {
1078
- const memoryEmbedding = await this.getAnchorEmbedding(memoryId);
1079
- if (memoryEmbedding && memoryEmbedding.embedding) {
1080
- const similarity = this.cosineSimilarity(anchorEmbedding, memoryEmbedding.embedding);
1081
- // 현재 앵커와 유사하면 낮은 점수, 다르면 높은 점수 (다양성)
1082
- anchorComparisonScore = 1.0 - similarity;
1083
- }
1084
- }
1085
- // 4. 종합 점수 계산 (가중 평균)
1086
- // 사용 빈도(30%) + 최근성(20%) + 중요도(20%) + 의미적 거리(20%) + 앵커 비교(10%)
1087
- const finalScore = usageScore * 0.3 +
1088
- recencyScore * 0.2 +
1089
- importanceScore * 0.2 +
1090
- semanticScore * 0.2 +
1091
- anchorComparisonScore * 0.1;
1092
- return Math.min(1.0, Math.max(0.0, finalScore));
1093
- }
1094
- catch (error) {
1095
- console.error(`❌ 앵커 이동 점수 계산 실패 (${memoryId}):`, error);
1096
- return 0;
1097
- }
227
+ async autoReanchor(agentId, slot, queryEmbedding, threshold = 0.7, strategy = 'gradual') {
228
+ return this.searchService.autoReanchor(agentId, slot, this.newAnchorManager, queryEmbedding, threshold, strategy);
1098
229
  }
1099
230
  /**
1100
231
  * 앵커 주변 메모리 사용 패턴 분석
1101
- * @param agentId - 에이전트 ID
1102
- * @param slot - 슬롯
1103
- * @param queryEmbedding - 검색 쿼리 임베딩 (선택적)
1104
- * @returns 더 적합한 앵커 후보 목록 (점수 내림차순)
1105
232
  */
1106
233
  async analyzeAnchorUsage(agentId, slot, queryEmbedding) {
1107
234
  if (!this.db) {
@@ -1111,160 +238,17 @@ export class AnchorManager {
1111
238
  if (!anchor || Array.isArray(anchor) || !anchor.memory_id) {
1112
239
  return [];
1113
240
  }
1114
- try {
1115
- // 현재 앵커 임베딩 조회
1116
- const anchorEmbedding = await this.getAnchorEmbedding(anchor.memory_id);
1117
- if (!anchorEmbedding) {
1118
- return [];
1119
- }
1120
- // 앵커 주변 메모리 검색 (1-hop)
1121
- const slotConfig = this.getSlotConfig(slot);
1122
- const nearbyMemories = await this.searchNHop(anchorEmbedding.embedding, anchorEmbedding.provider, anchor.memory_id, slotConfig.vector_threshold * 0.8, // 더 넓은 범위
1123
- slotConfig.hop_limit, 20 // 더 많은 후보
1124
- );
1125
- // 각 메모리에 대해 앵커 이동 점수 계산
1126
- const candidates = [];
1127
- for (const memory of nearbyMemories) {
1128
- const score = await this.calculateReanchorScore(memory.memory_id, queryEmbedding, anchorEmbedding.embedding);
1129
- if (score > 0.5) { // 최소 점수 이상만 후보로
1130
- const reason = this.generateReanchorReason(memory, score);
1131
- candidates.push({
1132
- memory_id: memory.memory_id,
1133
- score,
1134
- reason
1135
- });
1136
- }
1137
- }
1138
- // 점수 내림차순 정렬
1139
- candidates.sort((a, b) => b.score - a.score);
1140
- return candidates;
1141
- }
1142
- catch (error) {
1143
- console.error(`❌ 앵커 사용 패턴 분석 실패 (${agentId}/${slot}):`, error);
241
+ const anchorEmbedding = await this.cacheService.getAnchorEmbedding(anchor.memory_id);
242
+ if (!anchorEmbedding) {
1144
243
  return [];
1145
244
  }
245
+ return this.searchService.analyzeAnchorUsage(agentId, slot, anchor.memory_id, anchorEmbedding, queryEmbedding);
1146
246
  }
1147
247
  /**
1148
- * 앵커 이동 이유 생성
1149
- */
1150
- generateReanchorReason(memory, score) {
1151
- const reasons = [];
1152
- if (score > 0.7) {
1153
- reasons.push('높은 사용 빈도');
1154
- }
1155
- if (memory.similarity && memory.similarity > 0.8) {
1156
- reasons.push('쿼리와 높은 유사도');
1157
- }
1158
- if (memory.hop_distance === 1) {
1159
- reasons.push('앵커와 직접 연결');
1160
- }
1161
- return reasons.length > 0 ? reasons.join(', ') : '종합 점수 우수';
1162
- }
1163
- /**
1164
- * 자동 앵커 이동 실행
1165
- * @param agentId - 에이전트 ID
1166
- * @param slot - 슬롯
1167
- * @param queryEmbedding - 검색 쿼리 임베딩 (선택적)
1168
- * @param threshold - 이동 임계값 (기본값: 0.7)
1169
- * @param strategy - 이동 전략 ('gradual' | 'immediate', 기본값: 'gradual')
1170
- * @returns 이동 결과 (이동 여부, 새 앵커 정보)
1171
- */
1172
- async autoReanchor(agentId, slot, queryEmbedding, threshold = 0.7, strategy = 'gradual') {
1173
- if (!this.db) {
1174
- throw new Error('Database is not set.');
1175
- }
1176
- try {
1177
- // 현재 앵커 조회
1178
- const currentAnchor = await this.getAnchor(agentId, slot);
1179
- if (!currentAnchor || Array.isArray(currentAnchor) || !currentAnchor.memory_id) {
1180
- return {
1181
- moved: false,
1182
- old_anchor: null,
1183
- new_anchor: null,
1184
- score: 0,
1185
- reason: '앵커가 설정되지 않았습니다'
1186
- };
1187
- }
1188
- // 더 적합한 앵커 후보 찾기
1189
- const candidates = await this.analyzeAnchorUsage(agentId, slot, queryEmbedding);
1190
- if (candidates.length === 0 || !candidates[0] || candidates[0].score < threshold) {
1191
- return {
1192
- moved: false,
1193
- old_anchor: currentAnchor.memory_id,
1194
- new_anchor: null,
1195
- score: candidates[0]?.score || 0,
1196
- reason: `임계값(${threshold}) 미만 또는 후보 없음`
1197
- };
1198
- }
1199
- const bestCandidate = candidates[0];
1200
- if (!bestCandidate) {
1201
- return {
1202
- moved: false,
1203
- old_anchor: currentAnchor.memory_id,
1204
- new_anchor: null,
1205
- score: 0,
1206
- reason: '후보 없음'
1207
- };
1208
- }
1209
- // 이동 전략에 따라 처리
1210
- if (strategy === 'gradual') {
1211
- // 점진적 이동: 기존 앵커를 B나 C로 이동하고 새로운 메모리를 A에 설정
1212
- if (slot === 'A') {
1213
- // A -> B로 이동
1214
- const bAnchor = await this.getAnchor(agentId, 'B');
1215
- if (bAnchor && !Array.isArray(bAnchor) && bAnchor.memory_id) {
1216
- // B -> C로 이동 (인자 순서: agentId, memoryId, slot)
1217
- await this.setAnchor(agentId, bAnchor.memory_id, 'C');
1218
- }
1219
- // 현재 A -> B로 이동
1220
- await this.setAnchor(agentId, currentAnchor.memory_id, 'B');
1221
- }
1222
- else if (slot === 'B') {
1223
- // B -> C로 이동
1224
- await this.setAnchor(agentId, currentAnchor.memory_id, 'C');
1225
- }
1226
- // 새로운 메모리를 현재 슬롯에 설정
1227
- await this.setAnchor(agentId, bestCandidate.memory_id, slot);
1228
- }
1229
- else {
1230
- // 급격한 이동: 현재 앵커를 완전히 교체
1231
- await this.setAnchor(agentId, bestCandidate.memory_id, slot);
1232
- }
1233
- console.log(`🔄 자동 앵커 이동 완료: ${agentId}/${slot} ` +
1234
- `${currentAnchor.memory_id} -> ${bestCandidate.memory_id} ` +
1235
- `(점수: ${bestCandidate.score.toFixed(3)}, 이유: ${bestCandidate.reason})`);
1236
- return {
1237
- moved: true,
1238
- old_anchor: currentAnchor.memory_id,
1239
- new_anchor: bestCandidate.memory_id,
1240
- score: bestCandidate.score,
1241
- reason: bestCandidate.reason
1242
- };
1243
- }
1244
- catch (error) {
1245
- console.error(`❌ 자동 앵커 이동 실패 (${agentId}/${slot}):`, error);
1246
- throw error;
1247
- }
1248
- }
1249
- /**
1250
- * 검색 후 자동 앵커 이동 체크 (선택적)
1251
- * @param agentId - 에이전트 ID
1252
- * @param slot - 슬롯
1253
- * @param queryEmbedding - 검색 쿼리 임베딩
1254
- * @param autoMoveEnabled - 자동 이동 활성화 여부 (기본값: false)
1255
- * @returns 이동 결과
248
+ * 검색 후 자동 앵커 이동 체크
1256
249
  */
1257
250
  async checkAndAutoReanchor(agentId, slot, queryEmbedding, autoMoveEnabled = false) {
1258
- if (!autoMoveEnabled) {
1259
- return null;
1260
- }
1261
- try {
1262
- return await this.autoReanchor(agentId, slot, queryEmbedding, 0.7, 'gradual');
1263
- }
1264
- catch (error) {
1265
- console.error(`❌ 자동 앵커 이동 체크 실패:`, error);
1266
- return null;
1267
- }
251
+ return this.searchService.checkAndAutoReanchor(agentId, slot, this.newAnchorManager, queryEmbedding, autoMoveEnabled);
1268
252
  }
1269
253
  }
1270
254
  //# sourceMappingURL=anchor-manager.js.map