memento-mcp-server 1.11.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/server/context.d.ts +36 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +45 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/handlers/anchor-map.handler.d.ts +60 -0
- package/dist/server/handlers/anchor-map.handler.d.ts.map +1 -0
- package/dist/server/handlers/anchor-map.handler.js +190 -0
- package/dist/server/handlers/anchor-map.handler.js.map +1 -0
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +41 -1131
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/middleware/error-handler.middleware.d.ts +25 -0
- package/dist/server/middleware/error-handler.middleware.d.ts.map +1 -0
- package/dist/server/middleware/error-handler.middleware.js +97 -0
- package/dist/server/middleware/error-handler.middleware.js.map +1 -0
- package/dist/server/middleware/index.d.ts +8 -0
- package/dist/server/middleware/index.d.ts.map +1 -0
- package/dist/server/middleware/index.js +8 -0
- package/dist/server/middleware/index.js.map +1 -0
- package/dist/server/middleware/service-injector.middleware.d.ts +30 -0
- package/dist/server/middleware/service-injector.middleware.d.ts.map +1 -0
- package/dist/server/middleware/service-injector.middleware.js +20 -0
- package/dist/server/middleware/service-injector.middleware.js.map +1 -0
- package/dist/server/middleware/tool-context.middleware.d.ts +29 -0
- package/dist/server/middleware/tool-context.middleware.d.ts.map +1 -0
- package/dist/server/middleware/tool-context.middleware.js +34 -0
- package/dist/server/middleware/tool-context.middleware.js.map +1 -0
- package/dist/server/routes/admin.routes.d.ts +12 -0
- package/dist/server/routes/admin.routes.d.ts.map +1 -0
- package/dist/server/routes/admin.routes.js +338 -0
- package/dist/server/routes/admin.routes.js.map +1 -0
- package/dist/server/routes/api.routes.d.ts +13 -0
- package/dist/server/routes/api.routes.d.ts.map +1 -0
- package/dist/server/routes/api.routes.js +122 -0
- package/dist/server/routes/api.routes.js.map +1 -0
- package/dist/server/routes/mcp.routes.d.ts +22 -0
- package/dist/server/routes/mcp.routes.d.ts.map +1 -0
- package/dist/server/routes/mcp.routes.js +383 -0
- package/dist/server/routes/mcp.routes.js.map +1 -0
- package/dist/server/routes/tools.routes.d.ts +13 -0
- package/dist/server/routes/tools.routes.d.ts.map +1 -0
- package/dist/server/routes/tools.routes.js +99 -0
- package/dist/server/routes/tools.routes.js.map +1 -0
- package/dist/services/anchor/anchor-cache-service.d.ts +77 -0
- package/dist/services/anchor/anchor-cache-service.d.ts.map +1 -0
- package/dist/services/anchor/anchor-cache-service.js +193 -0
- package/dist/services/anchor/anchor-cache-service.js.map +1 -0
- package/dist/services/anchor/anchor-interfaces.d.ts +143 -0
- package/dist/services/anchor/anchor-interfaces.d.ts.map +1 -0
- package/dist/services/anchor/anchor-interfaces.js +24 -0
- package/dist/services/anchor/anchor-interfaces.js.map +1 -0
- package/dist/services/anchor/anchor-manager.d.ts +71 -0
- package/dist/services/anchor/anchor-manager.d.ts.map +1 -0
- package/dist/services/anchor/anchor-manager.js +205 -0
- package/dist/services/anchor/anchor-manager.js.map +1 -0
- package/dist/services/anchor/anchor-search-service.d.ts +115 -0
- package/dist/services/anchor/anchor-search-service.d.ts.map +1 -0
- package/dist/services/anchor/anchor-search-service.js +799 -0
- package/dist/services/anchor/anchor-search-service.js.map +1 -0
- package/dist/services/anchor/index.d.ts +11 -0
- package/dist/services/anchor/index.d.ts.map +1 -0
- package/dist/services/anchor/index.js +10 -0
- package/dist/services/anchor/index.js.map +1 -0
- package/dist/services/anchor-manager.d.ts +22 -208
- package/dist/services/anchor-manager.d.ts.map +1 -1
- package/dist/services/anchor-manager.js +72 -1088
- package/dist/services/anchor-manager.js.map +1 -1
- package/dist/services/error-logging-service.d.ts +1 -0
- package/dist/services/error-logging-service.d.ts.map +1 -1
- package/dist/services/error-logging-service.js +2 -0
- package/dist/services/error-logging-service.js.map +1 -1
- package/dist/tools/forget-tool.js +1 -1
- package/dist/tools/forget-tool.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
144
|
+
logger.info('Cleared invalid anchor', { agentId, slot });
|
|
304
145
|
}
|
|
305
146
|
catch (error) {
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
342
|
-
const
|
|
343
|
-
)
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
+
logger.debug('Auto anchor move check failed (ignored)', {
|
|
200
|
+
error: error instanceof Error ? error.message : String(error)
|
|
201
|
+
});
|
|
421
202
|
}
|
|
422
203
|
}
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|