memento-mcp-server 1.12.0 → 1.13.0-b
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/dist/algorithms/forgetting-algorithm.d.ts +21 -13
- package/dist/algorithms/forgetting-algorithm.d.ts.map +1 -1
- package/dist/algorithms/forgetting-algorithm.js +32 -24
- package/dist/algorithms/forgetting-algorithm.js.map +1 -1
- package/dist/algorithms/hybrid-search-engine.d.ts +11 -9
- package/dist/algorithms/hybrid-search-engine.d.ts.map +1 -1
- package/dist/algorithms/hybrid-search-engine.js +77 -75
- package/dist/algorithms/hybrid-search-engine.js.map +1 -1
- package/dist/algorithms/search-engine.d.ts +33 -11
- package/dist/algorithms/search-engine.d.ts.map +1 -1
- package/dist/algorithms/search-engine.js +157 -62
- package/dist/algorithms/search-engine.js.map +1 -1
- package/dist/algorithms/search-ranking.d.ts +57 -50
- package/dist/algorithms/search-ranking.d.ts.map +1 -1
- package/dist/algorithms/search-ranking.js +91 -84
- package/dist/algorithms/search-ranking.js.map +1 -1
- package/dist/algorithms/spaced-repetition.d.ts +18 -13
- package/dist/algorithms/spaced-repetition.d.ts.map +1 -1
- package/dist/algorithms/spaced-repetition.js +28 -23
- package/dist/algorithms/spaced-repetition.js.map +1 -1
- package/dist/algorithms/vector-search-engine-migration.d.ts +8 -6
- package/dist/algorithms/vector-search-engine-migration.d.ts.map +1 -1
- package/dist/algorithms/vector-search-engine-migration.js +13 -11
- package/dist/algorithms/vector-search-engine-migration.js.map +1 -1
- package/dist/algorithms/vector-search-engine-refactored.d.ts +7 -7
- package/dist/algorithms/vector-search-engine-refactored.d.ts.map +1 -1
- package/dist/algorithms/vector-search-engine-refactored.js +7 -7
- package/dist/algorithms/vector-search-engine-refactored.js.map +1 -1
- package/dist/algorithms/vector-search-engine.d.ts +25 -20
- package/dist/algorithms/vector-search-engine.d.ts.map +1 -1
- package/dist/algorithms/vector-search-engine.js +47 -43
- package/dist/algorithms/vector-search-engine.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +4 -1
- package/dist/config/index.js.map +1 -1
- package/dist/database/init.d.ts.map +1 -1
- package/dist/database/init.js +24 -4
- package/dist/database/init.js.map +1 -1
- package/dist/database/migration/migrations/006-fts5-reflection-notes-migration-status.sql +30 -0
- package/dist/database/migration/migrations/006-fts5-reflection-notes.d.ts +113 -0
- package/dist/database/migration/migrations/006-fts5-reflection-notes.d.ts.map +1 -0
- package/dist/database/migration/migrations/006-fts5-reflection-notes.js +441 -0
- package/dist/database/migration/migrations/006-fts5-reflection-notes.js.map +1 -0
- package/dist/database/migration/migrations/006-fts5-reflection-notes.sql +26 -0
- package/dist/database/schema.sql +11 -9
- package/dist/server/bootstrap.d.ts +4 -0
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +11 -1
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +3 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +2 -1
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/services/async-optimizer.d.ts +2 -1
- package/dist/services/async-optimizer.d.ts.map +1 -1
- package/dist/services/async-optimizer.js +28 -1
- package/dist/services/async-optimizer.js.map +1 -1
- package/dist/services/batch-scheduler.d.ts +5 -1
- package/dist/services/batch-scheduler.d.ts.map +1 -1
- package/dist/services/batch-scheduler.js +13 -1
- package/dist/services/batch-scheduler.js.map +1 -1
- package/dist/services/cache-service.js +1 -1
- package/dist/services/cache-service.js.map +1 -1
- package/dist/services/failure-detector.d.ts +120 -0
- package/dist/services/failure-detector.d.ts.map +1 -0
- package/dist/services/failure-detector.js +370 -0
- package/dist/services/failure-detector.js.map +1 -0
- package/dist/services/llm-based-relation-extractor.js +1 -1
- package/dist/services/llm-based-relation-extractor.js.map +1 -1
- package/dist/services/reflexion-worker.d.ts +170 -0
- package/dist/services/reflexion-worker.d.ts.map +1 -0
- package/dist/services/reflexion-worker.js +636 -0
- package/dist/services/reflexion-worker.js.map +1 -0
- package/dist/services/relation-graph.d.ts +2 -2
- package/dist/services/relation-graph.js +3 -3
- package/dist/services/relation-graph.js.map +1 -1
- package/dist/tools/base-tool.d.ts +5 -0
- package/dist/tools/base-tool.d.ts.map +1 -1
- package/dist/tools/base-tool.js +39 -0
- package/dist/tools/base-tool.js.map +1 -1
- package/dist/tools/recall-tool.d.ts.map +1 -1
- package/dist/tools/recall-tool.js +36 -2
- package/dist/tools/recall-tool.js.map +1 -1
- package/dist/tools/remember-tool.d.ts +24 -0
- package/dist/tools/remember-tool.d.ts.map +1 -1
- package/dist/tools/remember-tool.js +445 -273
- package/dist/tools/remember-tool.js.map +1 -1
- package/dist/tools/types.d.ts +5 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +34 -10
- package/dist/utils/database.js.map +1 -1
- package/dist/utils/fts5-migration-status.d.ts +72 -0
- package/dist/utils/fts5-migration-status.d.ts.map +1 -0
- package/dist/utils/fts5-migration-status.js +304 -0
- package/dist/utils/fts5-migration-status.js.map +1 -0
- package/dist/utils/reflection-notes-merge.d.ts +58 -0
- package/dist/utils/reflection-notes-merge.d.ts.map +1 -0
- package/dist/utils/reflection-notes-merge.js +227 -0
- package/dist/utils/reflection-notes-merge.js.map +1 -0
- package/dist/utils/reflection-notes-normalize.d.ts +43 -0
- package/dist/utils/reflection-notes-normalize.d.ts.map +1 -0
- package/dist/utils/reflection-notes-normalize.js +164 -0
- package/dist/utils/reflection-notes-normalize.js.map +1 -0
- package/dist/utils/reflection-notes-schema.d.ts +84 -0
- package/dist/utils/reflection-notes-schema.d.ts.map +1 -0
- package/dist/utils/reflection-notes-schema.js +215 -0
- package/dist/utils/reflection-notes-schema.js.map +1 -0
- package/package.json +3 -1
- package/src/database/schema.sql +11 -9
|
@@ -17,6 +17,8 @@ import { KnowledgeVaultService } from '../services/knowledge-vault-service.js';
|
|
|
17
17
|
import { validateTypeParam } from '../utils/type-param-validator.js';
|
|
18
18
|
import { mementoConfig } from '../config/index.js';
|
|
19
19
|
import { RelationExtractor } from '../services/relation-extractor.js';
|
|
20
|
+
import { validateReflectionNotes, formatValidationErrors } from '../utils/reflection-notes-schema.js';
|
|
21
|
+
import { mergeReflectionNotes, serializeReflectionNotes } from '../utils/reflection-notes-merge.js';
|
|
20
22
|
const RememberSchema = z.object({
|
|
21
23
|
content: CommonSchemas.Content,
|
|
22
24
|
type: CommonSchemas.MemoryType.optional(), // optional - validateTypeParam에서 기본값 처리
|
|
@@ -123,126 +125,286 @@ export class RememberTool extends BaseTool {
|
|
|
123
125
|
required: [] // 조건부 필수는 Zod 스키마에서 검증
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
/**
|
|
129
|
+
* reflection_notes 파라미터의 JSON 형식 및 스키마 검증
|
|
130
|
+
* 단일 객체 또는 배열 형식 모두 허용
|
|
131
|
+
*
|
|
132
|
+
* @param reflectionNotes - 검증할 reflection_notes 문자열
|
|
133
|
+
* @throws Error - JSON 형식이 유효하지 않거나 스키마 검증 실패 시
|
|
134
|
+
*/
|
|
135
|
+
validateReflectionNotesJson(reflectionNotes) {
|
|
136
|
+
const validationResult = validateReflectionNotes(reflectionNotes);
|
|
137
|
+
if (!validationResult.isValid) {
|
|
138
|
+
const errorMessage = formatValidationErrors(validationResult);
|
|
139
|
+
throw new Error(`reflection_notes 스키마 검증 실패:\n${errorMessage}`);
|
|
134
140
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 같은 task_goal을 가진 기존 procedural memory 레코드의 reflection_notes 조회
|
|
144
|
+
*
|
|
145
|
+
* @param db - 데이터베이스 인스턴스
|
|
146
|
+
* @param taskGoal - 작업 목표
|
|
147
|
+
* @returns 기존 reflection_notes 조회 결과
|
|
148
|
+
*/
|
|
149
|
+
async getExistingReflectionNotes(db, taskGoal) {
|
|
150
|
+
// task_goal이 제공되지 않은 경우 조회 불가
|
|
151
|
+
if (!taskGoal) {
|
|
152
|
+
return {
|
|
153
|
+
exists: false,
|
|
154
|
+
type: 'null',
|
|
155
|
+
value: null,
|
|
156
|
+
rawValue: null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
// 같은 task_goal을 가진 가장 최근 procedural memory 레코드 조회
|
|
161
|
+
const existingRecord = DatabaseUtils.get(db, `SELECT reflection_notes FROM memory_item
|
|
162
|
+
WHERE type = 'procedural' AND task_goal = ?
|
|
163
|
+
ORDER BY created_at DESC LIMIT 1`, [taskGoal]);
|
|
164
|
+
if (!existingRecord || !existingRecord.reflection_notes) {
|
|
165
|
+
return {
|
|
166
|
+
exists: false,
|
|
167
|
+
type: 'null',
|
|
168
|
+
value: null,
|
|
169
|
+
rawValue: null
|
|
170
|
+
};
|
|
142
171
|
}
|
|
172
|
+
// reflection_notes 파싱 및 타입 확인
|
|
173
|
+
return this.parseReflectionNotes(existingRecord.reflection_notes);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
// 조회 실패 시 빈 결과 반환
|
|
177
|
+
this.logWarning(`기존 reflection_notes 조회 실패: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
return {
|
|
179
|
+
exists: false,
|
|
180
|
+
type: 'null',
|
|
181
|
+
value: null,
|
|
182
|
+
rawValue: null
|
|
183
|
+
};
|
|
143
184
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* reflection_notes 문자열을 파싱하고 타입 확인
|
|
188
|
+
* NULL, 단일 객체, 배열 케이스 처리
|
|
189
|
+
*
|
|
190
|
+
* @param reflectionNotes - 파싱할 reflection_notes 문자열
|
|
191
|
+
* @returns 파싱 결과
|
|
192
|
+
*/
|
|
193
|
+
parseReflectionNotes(reflectionNotes) {
|
|
194
|
+
if (!reflectionNotes || reflectionNotes.trim() === '') {
|
|
195
|
+
return {
|
|
196
|
+
exists: true,
|
|
197
|
+
type: 'null',
|
|
198
|
+
value: null,
|
|
199
|
+
rawValue: null
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(reflectionNotes);
|
|
204
|
+
if (Array.isArray(parsed)) {
|
|
205
|
+
return {
|
|
206
|
+
exists: true,
|
|
207
|
+
type: 'array',
|
|
208
|
+
value: parsed,
|
|
209
|
+
rawValue: reflectionNotes
|
|
210
|
+
};
|
|
160
211
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
213
|
+
return {
|
|
214
|
+
exists: true,
|
|
215
|
+
type: 'object',
|
|
216
|
+
value: parsed,
|
|
217
|
+
rawValue: reflectionNotes
|
|
218
|
+
};
|
|
167
219
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
always_load: record.always_load,
|
|
186
|
-
message: `Core Memory가 저장되었습니다: ${record.core_id}`
|
|
187
|
-
});
|
|
220
|
+
// 객체나 배열이 아닌 경우
|
|
221
|
+
return {
|
|
222
|
+
exists: true,
|
|
223
|
+
type: 'null',
|
|
224
|
+
value: null,
|
|
225
|
+
rawValue: reflectionNotes
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// 파싱 실패 시 원본 문자열 반환
|
|
230
|
+
this.logWarning(`reflection_notes 파싱 실패: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
return {
|
|
232
|
+
exists: true,
|
|
233
|
+
type: 'null',
|
|
234
|
+
value: null,
|
|
235
|
+
rawValue: reflectionNotes
|
|
236
|
+
};
|
|
188
237
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
}
|
|
239
|
+
async handle(params, context) {
|
|
240
|
+
const startTime = Date.now();
|
|
241
|
+
try {
|
|
242
|
+
const { content, type: rawType, key, value, always_load, immutable, task_goal, steps, reflection_notes, tags, importance, source, privacy_scope } = RememberSchema.parse(params);
|
|
243
|
+
// type 파라미터 롤아웃 모드 검증
|
|
244
|
+
const typeParamMode = mementoConfig.typeParamMode;
|
|
245
|
+
const typeValidation = validateTypeParam(rawType, typeParamMode, 'remember');
|
|
246
|
+
// 에러 모드인 경우 에러 발생
|
|
247
|
+
if (!typeValidation.isValid) {
|
|
248
|
+
throw new Error(typeValidation.message || "type 파라미터는 필수입니다.");
|
|
193
249
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
250
|
+
// 경고/Deprecation 메시지 출력
|
|
251
|
+
if (typeValidation.message) {
|
|
252
|
+
if (typeParamMode === 'warn') {
|
|
253
|
+
this.logWarning(typeValidation.message);
|
|
254
|
+
}
|
|
255
|
+
else if (typeParamMode === 'deprecate') {
|
|
256
|
+
this.logWarning(typeValidation.message);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// type 파라미터 결정 (제공된 값 또는 기본값)
|
|
260
|
+
const type = (rawType || typeValidation.defaultType || 'episodic');
|
|
261
|
+
// reflection_notes JSON 검증 (type='procedural'이고 reflection_notes가 제공된 경우에만)
|
|
262
|
+
if (type === 'procedural' && reflection_notes !== undefined && reflection_notes !== null) {
|
|
263
|
+
this.validateReflectionNotesJson(reflection_notes);
|
|
264
|
+
}
|
|
265
|
+
// 데이터베이스 연결 확인
|
|
266
|
+
this.validateDatabase(context);
|
|
267
|
+
// origin_source 생성 (JSON 형식)
|
|
268
|
+
const origin_source = JSON.stringify({
|
|
269
|
+
tool: 'remember',
|
|
270
|
+
caller: 'user',
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
context: {
|
|
273
|
+
type,
|
|
274
|
+
has_content: !!content,
|
|
275
|
+
has_key: !!key,
|
|
276
|
+
has_value: !!value,
|
|
277
|
+
type_param_mode: typeParamMode,
|
|
278
|
+
type_was_defaulted: !rawType
|
|
279
|
+
}
|
|
211
280
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
281
|
+
// type에 따른 분기 처리
|
|
282
|
+
if (type === 'core') {
|
|
283
|
+
// Core Memory 저장
|
|
284
|
+
if (!key || !value) {
|
|
285
|
+
throw new Error("type='core'일 때는 key와 value가 필수입니다");
|
|
286
|
+
}
|
|
287
|
+
const coreMemoryRepository = new CoreMemoryRepository(context.db);
|
|
288
|
+
const { getCoreMemoryCache } = await import('../services/core-memory-cache-service.js');
|
|
289
|
+
const coreMemoryCache = getCoreMemoryCache();
|
|
290
|
+
const coreMemoryService = new CoreMemoryService(coreMemoryRepository, coreMemoryCache);
|
|
291
|
+
const agent_id = 'default'; // TODO: 향후 context에서 가져오기
|
|
292
|
+
const record = await coreMemoryService.create({
|
|
293
|
+
agent_id,
|
|
294
|
+
key,
|
|
295
|
+
value,
|
|
296
|
+
always_load: always_load || false,
|
|
297
|
+
origin_source
|
|
298
|
+
});
|
|
299
|
+
return this.createSuccessResult({
|
|
300
|
+
memory_id: record.core_id,
|
|
301
|
+
type: 'core',
|
|
302
|
+
key: record.key,
|
|
303
|
+
value: record.value,
|
|
304
|
+
always_load: record.always_load,
|
|
305
|
+
message: `Core Memory가 저장되었습니다: ${record.core_id}`
|
|
306
|
+
});
|
|
217
307
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
308
|
+
else if (type === 'vault') {
|
|
309
|
+
// Knowledge Vault 저장
|
|
310
|
+
if (!key || !value) {
|
|
311
|
+
throw new Error("type='vault'일 때는 key와 value가 필수입니다");
|
|
312
|
+
}
|
|
313
|
+
const knowledgeVaultRepository = new KnowledgeVaultRepository(context.db);
|
|
314
|
+
const knowledgeVaultService = new KnowledgeVaultService(knowledgeVaultRepository);
|
|
315
|
+
const agent_id = 'default'; // TODO: 향후 context에서 가져오기
|
|
316
|
+
const record = await knowledgeVaultService.create({
|
|
317
|
+
agent_id,
|
|
318
|
+
key,
|
|
319
|
+
value,
|
|
320
|
+
immutable: immutable !== false, // 기본값 true
|
|
321
|
+
origin_source
|
|
322
|
+
});
|
|
323
|
+
return this.createSuccessResult({
|
|
324
|
+
memory_id: record.vault_id,
|
|
325
|
+
type: 'vault',
|
|
326
|
+
key: record.key,
|
|
327
|
+
value: record.value,
|
|
328
|
+
immutable: record.immutable,
|
|
329
|
+
message: `Knowledge Vault가 저장되었습니다: ${record.vault_id}`
|
|
330
|
+
});
|
|
221
331
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
332
|
+
else {
|
|
333
|
+
// 기존 memory_item 저장 (episodic, semantic, procedural, working)
|
|
334
|
+
if (!content) {
|
|
335
|
+
throw new Error("type이 'core' 또는 'vault'가 아닐 때는 content가 필수입니다");
|
|
336
|
+
}
|
|
337
|
+
// 타입 가드로 검증
|
|
338
|
+
if (!isMemoryItemType(type)) {
|
|
339
|
+
throw new Error(`Invalid memory type: ${type}`);
|
|
340
|
+
}
|
|
341
|
+
// reflection_notes 처리
|
|
342
|
+
// type='procedural'이고 reflection_notes가 제공된 경우 기존 reflection_notes 조회 및 병합
|
|
343
|
+
// non-procedural 타입에서는 reflection_notes를 무시 (PRD: "Procedural Memory에서만 사용 가능")
|
|
344
|
+
let finalReflectionNotes = null;
|
|
345
|
+
if (type === 'procedural' && reflection_notes !== undefined && reflection_notes !== null) {
|
|
346
|
+
// procedural 타입: 기존 reflection_notes 조회 및 병합
|
|
347
|
+
finalReflectionNotes = reflection_notes;
|
|
348
|
+
const existingReflectionNotes = await this.getExistingReflectionNotes(context.db, task_goal);
|
|
349
|
+
// 기존 reflection_notes가 있는 경우 병합
|
|
350
|
+
if (existingReflectionNotes.exists) {
|
|
351
|
+
try {
|
|
352
|
+
// 병합 유틸리티 함수 사용
|
|
353
|
+
const existing = existingReflectionNotes.type === 'null' ? { type: 'null', value: null } :
|
|
354
|
+
existingReflectionNotes.type === 'object' ? { type: 'object', value: existingReflectionNotes.value } :
|
|
355
|
+
{ type: 'array', value: existingReflectionNotes.value };
|
|
356
|
+
const mergeResult = mergeReflectionNotes(existing, reflection_notes);
|
|
357
|
+
// 병합 결과를 JSON 문자열로 변환
|
|
358
|
+
finalReflectionNotes = serializeReflectionNotes(mergeResult.merged);
|
|
359
|
+
// 경고 메시지 처리
|
|
360
|
+
if (mergeResult.warnings.length > 0) {
|
|
361
|
+
mergeResult.warnings.forEach(warning => {
|
|
362
|
+
this.logWarning(`reflection_notes 병합 경고: ${warning}`);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (mergeResult.removedCount > 0) {
|
|
366
|
+
this.logWarning(`reflection_notes 크기 제한으로 인해 ${mergeResult.removedCount}개 항목이 제거되었습니다`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
// 병합 실패 시 에러 처리
|
|
371
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
372
|
+
// 단일 객체 크기 초과 같은 경우는 에러를 던짐 (검증 단계에서 이미 처리되어야 하지만 안전장치)
|
|
373
|
+
if (errorMessage.includes('최대') && errorMessage.includes('바이트')) {
|
|
374
|
+
throw new Error(`reflection_notes 크기 제한 초과: ${errorMessage}. ` +
|
|
375
|
+
`단일 객체는 최대 10KB, 전체 필드는 최대 1MB를 초과할 수 없습니다.`);
|
|
376
|
+
}
|
|
377
|
+
// 기타 병합 실패 시 원본 reflection_notes 사용 (경고 로그)
|
|
378
|
+
this.logWarning(`reflection_notes 병합 실패, 원본 값 사용: ${errorMessage}. ` +
|
|
379
|
+
`기존 reflection_notes는 유지되고 새 reflection_notes만 저장됩니다.`);
|
|
380
|
+
}
|
|
244
381
|
}
|
|
245
|
-
|
|
382
|
+
}
|
|
383
|
+
// non-procedural 타입에서는 reflection_notes를 무시 (null로 설정)
|
|
384
|
+
// UUID 생성 (임시로 간단한 ID 사용)
|
|
385
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
386
|
+
try {
|
|
387
|
+
// 메모리 저장 (트랜잭션 사용)
|
|
388
|
+
await DatabaseUtils.runTransaction(context.db, async () => {
|
|
389
|
+
// Consolidation Score System 초기화 값 설정
|
|
390
|
+
const createdAt = new Date().toISOString();
|
|
391
|
+
const recallCount = mementoConfig.consolidationScoreEnabled ? 1 : 0; // 기능 활성화 시 1, 비활성화 시 0
|
|
392
|
+
const gValue = mementoConfig.consolidationScoreEnabled ? 1.0 : null; // 기능 활성화 시 1.0, 비활성화 시 NULL
|
|
393
|
+
const lastAccessedAt = mementoConfig.consolidationScoreEnabled ? createdAt : null; // 기능 활성화 시 created_at과 동일, 비활성화 시 NULL
|
|
394
|
+
// consolidation_score 계산 (기능 활성화 시)
|
|
395
|
+
let consolidationScore = null;
|
|
396
|
+
if (mementoConfig.consolidationScoreEnabled && context.services.consolidationScoreService) {
|
|
397
|
+
const scoreResult = context.services.consolidationScoreService.calculateScore({
|
|
398
|
+
recallCount: 1,
|
|
399
|
+
lastAccessedAt: new Date(createdAt),
|
|
400
|
+
createdAt: new Date(createdAt),
|
|
401
|
+
gValue: 1.0,
|
|
402
|
+
type: type,
|
|
403
|
+
pinned: false
|
|
404
|
+
});
|
|
405
|
+
consolidationScore = scoreResult.score;
|
|
406
|
+
}
|
|
407
|
+
await DatabaseUtils.run(context.db, `
|
|
246
408
|
INSERT INTO memory_item (
|
|
247
409
|
id, type, content, importance, privacy_scope, tags, source, origin_source,
|
|
248
410
|
task_goal, steps, reflection_notes, created_at,
|
|
@@ -250,72 +412,116 @@ export class RememberTool extends BaseTool {
|
|
|
250
412
|
)
|
|
251
413
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
252
414
|
`, [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
// 트랜잭션이 완전히 커밋되도록 짧은 지연
|
|
283
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
284
|
-
// 데이터베이스 연결이 여전히 유효한지 확인 (간단한 쿼리로 테스트)
|
|
285
|
-
// DatabaseUtils.get은 동기 함수이지만, 비동기 컨텍스트에서 안전하게 실행하기 위해 Promise로 감싸서 await
|
|
415
|
+
id,
|
|
416
|
+
type,
|
|
417
|
+
content,
|
|
418
|
+
importance,
|
|
419
|
+
privacy_scope,
|
|
420
|
+
tags ? JSON.stringify(tags) : null,
|
|
421
|
+
source || null,
|
|
422
|
+
origin_source,
|
|
423
|
+
task_goal || null,
|
|
424
|
+
steps || null,
|
|
425
|
+
finalReflectionNotes,
|
|
426
|
+
createdAt,
|
|
427
|
+
recallCount,
|
|
428
|
+
lastAccessedAt,
|
|
429
|
+
gValue,
|
|
430
|
+
consolidationScore
|
|
431
|
+
]);
|
|
432
|
+
});
|
|
433
|
+
// 메모리 저장 완료 후 임베딩 생성, 인접 기억 갱신, 관계 추출 (비동기, 실패해도 메모리 저장은 성공)
|
|
434
|
+
// 데이터베이스 참조를 미리 저장하여 비동기 콜백에서 안전하게 사용
|
|
435
|
+
const dbRef = context.db;
|
|
436
|
+
const embeddingServiceRef = context.services.embeddingService;
|
|
437
|
+
const savedMemoryId = id; // 클로저에서 사용할 수 있도록 저장
|
|
438
|
+
const savedMemoryType = type; // 클로저에서 사용할 수 있도록 저장
|
|
439
|
+
if (dbRef) {
|
|
440
|
+
// 비동기 작업을 별도로 실행 (fire-and-forget 패턴)
|
|
441
|
+
// 메모리 저장 응답은 즉시 반환하고, 임베딩/인접 기억 갱신/관계 추출은 백그라운드에서 처리
|
|
442
|
+
(async () => {
|
|
286
443
|
try {
|
|
287
|
-
|
|
444
|
+
// 트랜잭션이 완전히 커밋되도록 짧은 지연
|
|
445
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
446
|
+
// 데이터베이스 연결이 여전히 유효한지 확인 (간단한 쿼리로 테스트)
|
|
447
|
+
// DatabaseUtils.get은 동기 함수이지만, 비동기 컨텍스트에서 안전하게 실행하기 위해 Promise로 감싸서 await
|
|
448
|
+
try {
|
|
449
|
+
await new Promise((resolve, reject) => {
|
|
450
|
+
try {
|
|
451
|
+
DatabaseUtils.get(dbRef, 'SELECT 1');
|
|
452
|
+
resolve();
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
reject(error);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (dbError) {
|
|
460
|
+
this.logWarning('데이터베이스 연결이 유효하지 않아 백그라운드 작업을 건너뜁니다', {
|
|
461
|
+
memory_id: savedMemoryId,
|
|
462
|
+
error: dbError instanceof Error ? dbError.message : String(dbError)
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// 임베딩 생성 (embeddingService가 사용 가능한 경우에만)
|
|
467
|
+
let embeddingResult = null;
|
|
468
|
+
if (embeddingServiceRef?.isAvailable()) {
|
|
288
469
|
try {
|
|
289
|
-
|
|
290
|
-
resolve();
|
|
470
|
+
embeddingResult = await embeddingServiceRef.createAndStoreEmbedding(dbRef, savedMemoryId, content, savedMemoryType);
|
|
291
471
|
}
|
|
292
472
|
catch (error) {
|
|
293
|
-
|
|
473
|
+
// 임베딩 생성 실패해도 메모리 저장은 성공했으므로 경고만 출력
|
|
474
|
+
this.logWarning(`임베딩 생성 실패 (${savedMemoryId})`, {
|
|
475
|
+
error: error instanceof Error ? error.message : String(error)
|
|
476
|
+
});
|
|
294
477
|
}
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
catch (dbError) {
|
|
298
|
-
this.logWarning('데이터베이스 연결이 유효하지 않아 백그라운드 작업을 건너뜁니다', {
|
|
299
|
-
memory_id: savedMemoryId,
|
|
300
|
-
error: dbError instanceof Error ? dbError.message : String(dbError)
|
|
301
|
-
});
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
// 임베딩 생성 (embeddingService가 사용 가능한 경우에만)
|
|
305
|
-
let embeddingResult = null;
|
|
306
|
-
if (embeddingServiceRef?.isAvailable()) {
|
|
307
|
-
try {
|
|
308
|
-
embeddingResult = await embeddingServiceRef.createAndStoreEmbedding(dbRef, savedMemoryId, content, savedMemoryType);
|
|
309
478
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
479
|
+
// PRD 3.1-3.3: 인접 기억 갱신 (임베딩이 생성된 경우에만)
|
|
480
|
+
if (embeddingResult && embeddingServiceRef) {
|
|
481
|
+
try {
|
|
482
|
+
// 데이터베이스 연결 재확인
|
|
483
|
+
let dbValid = false;
|
|
484
|
+
try {
|
|
485
|
+
await new Promise((resolve, reject) => {
|
|
486
|
+
try {
|
|
487
|
+
DatabaseUtils.get(dbRef, 'SELECT 1');
|
|
488
|
+
resolve();
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
reject(error);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
dbValid = true;
|
|
495
|
+
}
|
|
496
|
+
catch (dbError) {
|
|
497
|
+
this.logWarning('데이터베이스 연결이 유효하지 않아 인접 기억 갱신을 건너뜁니다', {
|
|
498
|
+
memory_id: savedMemoryId,
|
|
499
|
+
error: dbError instanceof Error ? dbError.message : String(dbError)
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (dbValid) {
|
|
503
|
+
const vectorSearchEngine = getVectorSearchEngine();
|
|
504
|
+
const neighborService = new MemoryNeighborService(vectorSearchEngine, embeddingServiceRef);
|
|
505
|
+
neighborService.setDatabase(dbRef);
|
|
506
|
+
// 인접 기억 갱신 (기본 유사도 임계값: 0.8)
|
|
507
|
+
const neighborIds = await neighborService.updateNeighborsForNewMemory(savedMemoryId, 0.8);
|
|
508
|
+
if (neighborIds.length > 0) {
|
|
509
|
+
this.logInfo('인접 기억 갱신 완료', {
|
|
510
|
+
memory_id: savedMemoryId,
|
|
511
|
+
neighbor_count: neighborIds.length
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
// 인접 기억 갱신 실패해도 메모리 저장은 성공했으므로 경고만 출력
|
|
518
|
+
this.logWarning(`인접 기억 갱신 실패 (${savedMemoryId})`, {
|
|
519
|
+
error: error instanceof Error ? error.message : String(error)
|
|
520
|
+
});
|
|
521
|
+
}
|
|
315
522
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (embeddingResult && embeddingServiceRef) {
|
|
523
|
+
// PRD 2.12: 관계 추출 트리거 (비동기 배치 처리)
|
|
524
|
+
// 임베딩 생성 여부와 관계없이 관계 추출 수행 (규칙 기반 추출은 임베딩 불필요)
|
|
319
525
|
try {
|
|
320
526
|
// 데이터베이스 연결 재확인
|
|
321
527
|
let dbValid = false;
|
|
@@ -332,138 +538,104 @@ export class RememberTool extends BaseTool {
|
|
|
332
538
|
dbValid = true;
|
|
333
539
|
}
|
|
334
540
|
catch (dbError) {
|
|
335
|
-
this.logWarning('데이터베이스 연결이 유효하지 않아
|
|
541
|
+
this.logWarning('데이터베이스 연결이 유효하지 않아 관계 추출을 건너뜁니다', {
|
|
336
542
|
memory_id: savedMemoryId,
|
|
337
543
|
error: dbError instanceof Error ? dbError.message : String(dbError)
|
|
338
544
|
});
|
|
339
545
|
}
|
|
340
546
|
if (dbValid) {
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
547
|
+
// 기존 기억들 조회 (최근 100개로 제한하여 성능 최적화)
|
|
548
|
+
const existingMemories = await this.getExistingMemoriesForRelationExtraction(dbRef, savedMemoryId, 100);
|
|
549
|
+
if (existingMemories.length > 0) {
|
|
550
|
+
// 새로 저장된 기억 정보 조회
|
|
551
|
+
const newMemory = await this.getMemoryById(dbRef, savedMemoryId);
|
|
552
|
+
if (newMemory) {
|
|
553
|
+
// RelationExtractor를 사용하여 관계 추출
|
|
554
|
+
const relationExtractor = new RelationExtractor();
|
|
555
|
+
// 비동기 배치 처리로 관계 추출
|
|
556
|
+
// immediate: true로 설정하여 캐싱 활성화
|
|
557
|
+
const candidates = await relationExtractor.extractRelations(newMemory, existingMemories, {
|
|
558
|
+
method: 'hybrid',
|
|
559
|
+
minConfidence: 0.5,
|
|
560
|
+
candidateLimit: 30, // MiniLM 필터링을 위한 제한
|
|
561
|
+
immediate: true // 캐싱 활성화
|
|
562
|
+
});
|
|
563
|
+
if (candidates.length > 0) {
|
|
564
|
+
this.logInfo('관계 추출 완료', {
|
|
565
|
+
memory_id: savedMemoryId,
|
|
566
|
+
relation_count: candidates.length,
|
|
567
|
+
relations: candidates.map(c => ({
|
|
568
|
+
target_id: c.target_id,
|
|
569
|
+
relation_type: c.relation_type,
|
|
570
|
+
confidence: c.confidence,
|
|
571
|
+
method: c.method
|
|
572
|
+
}))
|
|
573
|
+
});
|
|
574
|
+
// TODO: PRD 3.0에서 RelationGraph가 구현되면 여기서 관계를 저장
|
|
575
|
+
// const relationGraph = new RelationGraph(dbRef);
|
|
576
|
+
// for (const candidate of candidates) {
|
|
577
|
+
// await relationGraph.addRelation(candidate);
|
|
578
|
+
// }
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
this.logInfo('관계 추출 완료 (관계 없음)', {
|
|
582
|
+
memory_id: savedMemoryId
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
351
586
|
}
|
|
352
587
|
}
|
|
353
588
|
}
|
|
354
589
|
catch (error) {
|
|
355
|
-
//
|
|
356
|
-
this.logWarning(
|
|
590
|
+
// 관계 추출 실패해도 메모리 저장은 성공했으므로 경고만 출력
|
|
591
|
+
this.logWarning(`관계 추출 실패 (${savedMemoryId})`, {
|
|
357
592
|
error: error instanceof Error ? error.message : String(error)
|
|
358
593
|
});
|
|
359
594
|
}
|
|
360
595
|
}
|
|
361
|
-
// PRD 2.12: 관계 추출 트리거 (비동기 배치 처리)
|
|
362
|
-
// 임베딩 생성 여부와 관계없이 관계 추출 수행 (규칙 기반 추출은 임베딩 불필요)
|
|
363
|
-
try {
|
|
364
|
-
// 데이터베이스 연결 재확인
|
|
365
|
-
let dbValid = false;
|
|
366
|
-
try {
|
|
367
|
-
await new Promise((resolve, reject) => {
|
|
368
|
-
try {
|
|
369
|
-
DatabaseUtils.get(dbRef, 'SELECT 1');
|
|
370
|
-
resolve();
|
|
371
|
-
}
|
|
372
|
-
catch (error) {
|
|
373
|
-
reject(error);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
dbValid = true;
|
|
377
|
-
}
|
|
378
|
-
catch (dbError) {
|
|
379
|
-
this.logWarning('데이터베이스 연결이 유효하지 않아 관계 추출을 건너뜁니다', {
|
|
380
|
-
memory_id: savedMemoryId,
|
|
381
|
-
error: dbError instanceof Error ? dbError.message : String(dbError)
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
if (dbValid) {
|
|
385
|
-
// 기존 기억들 조회 (최근 100개로 제한하여 성능 최적화)
|
|
386
|
-
const existingMemories = await this.getExistingMemoriesForRelationExtraction(dbRef, savedMemoryId, 100);
|
|
387
|
-
if (existingMemories.length > 0) {
|
|
388
|
-
// 새로 저장된 기억 정보 조회
|
|
389
|
-
const newMemory = await this.getMemoryById(dbRef, savedMemoryId);
|
|
390
|
-
if (newMemory) {
|
|
391
|
-
// RelationExtractor를 사용하여 관계 추출
|
|
392
|
-
const relationExtractor = new RelationExtractor();
|
|
393
|
-
// 비동기 배치 처리로 관계 추출
|
|
394
|
-
// immediate: true로 설정하여 캐싱 활성화
|
|
395
|
-
const candidates = await relationExtractor.extractRelations(newMemory, existingMemories, {
|
|
396
|
-
method: 'hybrid',
|
|
397
|
-
minConfidence: 0.5,
|
|
398
|
-
candidateLimit: 30, // MiniLM 필터링을 위한 제한
|
|
399
|
-
immediate: true // 캐싱 활성화
|
|
400
|
-
});
|
|
401
|
-
if (candidates.length > 0) {
|
|
402
|
-
this.logInfo('관계 추출 완료', {
|
|
403
|
-
memory_id: savedMemoryId,
|
|
404
|
-
relation_count: candidates.length,
|
|
405
|
-
relations: candidates.map(c => ({
|
|
406
|
-
target_id: c.target_id,
|
|
407
|
-
relation_type: c.relation_type,
|
|
408
|
-
confidence: c.confidence,
|
|
409
|
-
method: c.method
|
|
410
|
-
}))
|
|
411
|
-
});
|
|
412
|
-
// TODO: PRD 3.0에서 RelationGraph가 구현되면 여기서 관계를 저장
|
|
413
|
-
// const relationGraph = new RelationGraph(dbRef);
|
|
414
|
-
// for (const candidate of candidates) {
|
|
415
|
-
// await relationGraph.addRelation(candidate);
|
|
416
|
-
// }
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
this.logInfo('관계 추출 완료 (관계 없음)', {
|
|
420
|
-
memory_id: savedMemoryId
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
596
|
catch (error) {
|
|
428
|
-
//
|
|
429
|
-
this.logWarning(
|
|
597
|
+
// 백그라운드 작업 실패해도 메모리 저장은 성공했으므로 경고만 출력
|
|
598
|
+
this.logWarning(`백그라운드 작업 실패 (${savedMemoryId})`, {
|
|
430
599
|
error: error instanceof Error ? error.message : String(error)
|
|
431
600
|
});
|
|
432
601
|
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// 백그라운드 작업 실패해도 메모리 저장은 성공했으므로 경고만 출력
|
|
602
|
+
})().catch((error) => {
|
|
603
|
+
// 예상치 못한 에러 처리
|
|
436
604
|
this.logWarning(`백그라운드 작업 실패 (${savedMemoryId})`, {
|
|
437
605
|
error: error instanceof Error ? error.message : String(error)
|
|
438
606
|
});
|
|
439
|
-
}
|
|
440
|
-
})().catch((error) => {
|
|
441
|
-
// 예상치 못한 에러 처리
|
|
442
|
-
this.logWarning(`백그라운드 작업 실패 (${savedMemoryId})`, {
|
|
443
|
-
error: error instanceof Error ? error.message : String(error)
|
|
444
607
|
});
|
|
608
|
+
}
|
|
609
|
+
return this.createSuccessResult({
|
|
610
|
+
memory_id: id,
|
|
611
|
+
type: type,
|
|
612
|
+
message: `기억이 저장되었습니다: ${id}`,
|
|
613
|
+
embedding_created: context.services.embeddingService?.isAvailable() || false
|
|
445
614
|
});
|
|
446
615
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (error.code === 'SQLITE_BUSY') {
|
|
457
|
-
try {
|
|
458
|
-
await DatabaseUtils.checkpointWAL(context.db);
|
|
459
|
-
}
|
|
460
|
-
catch (checkpointError) {
|
|
461
|
-
// WAL 체크포인트 실패
|
|
616
|
+
catch (error) {
|
|
617
|
+
// 데이터베이스 락 문제인 경우 WAL 체크포인트 시도
|
|
618
|
+
if (error.code === 'SQLITE_BUSY') {
|
|
619
|
+
try {
|
|
620
|
+
await DatabaseUtils.checkpointWAL(context.db);
|
|
621
|
+
}
|
|
622
|
+
catch (checkpointError) {
|
|
623
|
+
// WAL 체크포인트 실패
|
|
624
|
+
}
|
|
462
625
|
}
|
|
626
|
+
// 실패 감지 훅 호출
|
|
627
|
+
const executionTime = Date.now() - startTime;
|
|
628
|
+
await this.handleFailure(error instanceof Error ? error : new Error(String(error)), params, context, executionTime);
|
|
629
|
+
throw error;
|
|
463
630
|
}
|
|
464
|
-
throw error;
|
|
465
631
|
}
|
|
466
632
|
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
// 최상위 에러 처리 (내부 catch에서 처리되지 않은 에러)
|
|
635
|
+
const executionTime = Date.now() - startTime;
|
|
636
|
+
await this.handleFailure(error instanceof Error ? error : new Error(String(error)), params, context, executionTime);
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
467
639
|
}
|
|
468
640
|
/**
|
|
469
641
|
* 관계 추출을 위한 기존 기억들 조회
|