memento-mcp-server 1.14.0-b → 1.15.0-c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.d.ts +18 -0
- package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.d.ts.map +1 -0
- package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js +346 -0
- package/dist/domains/memory/tools/convert-episodic-to-semantic-tool.js.map +1 -0
- package/dist/domains/memory/tools/recall-tool.d.ts +177 -0
- package/dist/domains/memory/tools/recall-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/recall-tool.js +329 -3
- package/dist/domains/memory/tools/recall-tool.js.map +1 -1
- package/dist/domains/memory/tools/remember-tool.d.ts.map +1 -1
- package/dist/domains/memory/tools/remember-tool.js +182 -1
- package/dist/domains/memory/tools/remember-tool.js.map +1 -1
- package/dist/infrastructure/database/database/migration/migrations/005-relation-engine-schema.sql +7 -7
- package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.d.ts +65 -0
- package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.d.ts.map +1 -0
- package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.js +250 -0
- package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.js.map +1 -0
- package/dist/infrastructure/database/database/migration/migrations/008-arigraph-schema-expansion.sql +86 -0
- package/dist/infrastructure/logging/triple-extraction-logger.d.ts +92 -0
- package/dist/infrastructure/logging/triple-extraction-logger.d.ts.map +1 -0
- package/dist/infrastructure/logging/triple-extraction-logger.js +194 -0
- package/dist/infrastructure/logging/triple-extraction-logger.js.map +1 -0
- package/dist/infrastructure/scheduler/batch-scheduler.d.ts +57 -0
- package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
- package/dist/infrastructure/scheduler/batch-scheduler.js +220 -0
- package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
- package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.d.ts +194 -0
- package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.d.ts.map +1 -0
- package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.js +639 -0
- package/dist/infrastructure/scheduler/jobs/triple-extraction-batch-job.js.map +1 -0
- package/dist/services/semantic-memory/semantic-memory-statistics.d.ts +64 -0
- package/dist/services/semantic-memory/semantic-memory-statistics.d.ts.map +1 -0
- package/dist/services/semantic-memory/semantic-memory-statistics.js +113 -0
- package/dist/services/semantic-memory/semantic-memory-statistics.js.map +1 -0
- package/dist/services/semantic-memory/semantic-memory-update-service.d.ts +257 -0
- package/dist/services/semantic-memory/semantic-memory-update-service.d.ts.map +1 -0
- package/dist/services/semantic-memory/semantic-memory-update-service.js +696 -0
- package/dist/services/semantic-memory/semantic-memory-update-service.js.map +1 -0
- package/dist/services/triple-extraction/entity-linker.d.ts +55 -0
- package/dist/services/triple-extraction/entity-linker.d.ts.map +1 -0
- package/dist/services/triple-extraction/entity-linker.js +154 -0
- package/dist/services/triple-extraction/entity-linker.js.map +1 -0
- package/dist/services/triple-extraction/predicate-canonicalizer.d.ts +63 -0
- package/dist/services/triple-extraction/predicate-canonicalizer.d.ts.map +1 -0
- package/dist/services/triple-extraction/predicate-canonicalizer.js +166 -0
- package/dist/services/triple-extraction/predicate-canonicalizer.js.map +1 -0
- package/dist/services/triple-extraction/triple-extraction-service.d.ts +181 -0
- package/dist/services/triple-extraction/triple-extraction-service.d.ts.map +1 -0
- package/dist/services/triple-extraction/triple-extraction-service.js +907 -0
- package/dist/services/triple-extraction/triple-extraction-service.js.map +1 -0
- package/dist/services/triple-extraction/triple-extraction-statistics.d.ts +74 -0
- package/dist/services/triple-extraction/triple-extraction-statistics.d.ts.map +1 -0
- package/dist/services/triple-extraction/triple-extraction-statistics.js +146 -0
- package/dist/services/triple-extraction/triple-extraction-statistics.js.map +1 -0
- package/dist/shared/types/index.d.ts +1 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/shared/types/index.js.map +1 -1
- package/dist/shared/types/triple-extraction.d.ts +99 -0
- package/dist/shared/types/triple-extraction.d.ts.map +1 -0
- package/dist/shared/types/triple-extraction.js +6 -0
- package/dist/shared/types/triple-extraction.js.map +1 -0
- package/dist/shared/utils/pii-masker.d.ts +67 -0
- package/dist/shared/utils/pii-masker.d.ts.map +1 -0
- package/dist/shared/utils/pii-masker.js +205 -0
- package/dist/shared/utils/pii-masker.js.map +1 -0
- package/dist/shared/utils/prompt-template-loader.d.ts +42 -0
- package/dist/shared/utils/prompt-template-loader.d.ts.map +1 -0
- package/dist/shared/utils/prompt-template-loader.js +92 -0
- package/dist/shared/utils/prompt-template-loader.js.map +1 -0
- package/dist/shared/utils/triple-cache.d.ts +90 -0
- package/dist/shared/utils/triple-cache.d.ts.map +1 -0
- package/dist/shared/utils/triple-cache.js +124 -0
- package/dist/shared/utils/triple-cache.js.map +1 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triple 추출 배치 작업
|
|
3
|
+
*
|
|
4
|
+
* 미처리 또는 실패한 Episodic Memory에 대해 Triple 추출을 수행하는 배치 작업입니다.
|
|
5
|
+
*
|
|
6
|
+
* 주요 기능:
|
|
7
|
+
* - 미처리 Episodic Memory 조회
|
|
8
|
+
* - Triple 추출 및 Semantic Memory 생성
|
|
9
|
+
* - 재시도 정책 적용 (최대 3회, 지수 백오프: 1일, 2일, 4일)
|
|
10
|
+
* - 상태 업데이트 (성공/실패/abandoned)
|
|
11
|
+
* - 로깅 및 통계 수집
|
|
12
|
+
*
|
|
13
|
+
* 재시도 정책:
|
|
14
|
+
* - 최대 시도 횟수: 3회 (설정 가능)
|
|
15
|
+
* - 지수 백오프: 1일, 2일, 4일 후 재시도
|
|
16
|
+
* - 즉각 재시도 금지: LLM 호출 실패 시 바로 재시도하지 않음 (비용 절감)
|
|
17
|
+
* - 지연 재시도: 배치 작업에서 실패한 항목을 다음 배치에서 재시도
|
|
18
|
+
* - 최대 시도 횟수 초과 시: abandoned 상태로 설정하여 재시도 중단
|
|
19
|
+
*/
|
|
20
|
+
import Database from 'better-sqlite3';
|
|
21
|
+
import { DatabaseUtils } from '../../../shared/utils/database.js';
|
|
22
|
+
import { TripleExtractionService } from '../../../services/triple-extraction/triple-extraction-service.js';
|
|
23
|
+
import { SemanticMemoryUpdateService } from '../../../services/semantic-memory/semantic-memory-update-service.js';
|
|
24
|
+
import { logger } from '../../../shared/utils/logger.js';
|
|
25
|
+
/**
|
|
26
|
+
* Triple 추출 배치 작업 클래스
|
|
27
|
+
*/
|
|
28
|
+
export class TripleExtractionBatchJob {
|
|
29
|
+
config;
|
|
30
|
+
tripleExtractionService;
|
|
31
|
+
semanticMemoryUpdateService = null;
|
|
32
|
+
constructor(config, dependencies) {
|
|
33
|
+
this.config = {
|
|
34
|
+
batchSize: config?.batchSize ?? 10, // PRD 6.2: 배치 크기 10개
|
|
35
|
+
timeout: config?.timeout ?? 30000, // PRD 6.2: 타임아웃 30초
|
|
36
|
+
maxRetries: config?.maxRetries ?? 3,
|
|
37
|
+
retryBackoffDays: config?.retryBackoffDays ?? [1, 2, 4],
|
|
38
|
+
chunkSize: config?.chunkSize ?? 5, // SQLite WAL 환경 고려: 작은 단위로 처리
|
|
39
|
+
chunkDelayMs: config?.chunkDelayMs ?? 100, // 청크 사이 짧은 지연
|
|
40
|
+
parallelism: config?.parallelism ?? 1, // PRD 6.2: 병렬성 제어, 기본값 1 (싱글톤 배치 작업)
|
|
41
|
+
...config
|
|
42
|
+
};
|
|
43
|
+
this.tripleExtractionService = dependencies?.tripleExtractionService ?? new TripleExtractionService();
|
|
44
|
+
this.semanticMemoryUpdateService = dependencies?.semanticMemoryUpdateService ?? null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 배치 작업 실행
|
|
48
|
+
*
|
|
49
|
+
* PRD 6.2 배치 처리 최적화:
|
|
50
|
+
* - 배치 크기: 10개씩 처리 (설정 가능)
|
|
51
|
+
* - 타임아웃: 배치당 최대 30초 (설정 가능)
|
|
52
|
+
* - 병렬성 제어: Parallelism = 1 (동시에 하나의 배치만 실행)
|
|
53
|
+
*
|
|
54
|
+
* @param db 데이터베이스 연결
|
|
55
|
+
* @returns 배치 작업 결과
|
|
56
|
+
*/
|
|
57
|
+
async execute(db) {
|
|
58
|
+
const startTime = new Date();
|
|
59
|
+
const timeoutDeadline = startTime.getTime() + this.config.timeout;
|
|
60
|
+
const result = {
|
|
61
|
+
jobType: 'triple_extraction_batch',
|
|
62
|
+
startTime,
|
|
63
|
+
endTime: new Date(),
|
|
64
|
+
duration: 0,
|
|
65
|
+
success: false,
|
|
66
|
+
processed: 0,
|
|
67
|
+
errors: [],
|
|
68
|
+
warnings: [],
|
|
69
|
+
details: {
|
|
70
|
+
processed: 0,
|
|
71
|
+
success: 0,
|
|
72
|
+
failed: 0,
|
|
73
|
+
skipped: 0,
|
|
74
|
+
semanticMemoriesCreated: 0,
|
|
75
|
+
semanticMemoriesUpdated: 0,
|
|
76
|
+
retryCounts: new Map()
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
// PRD 6.3: 배치 작업 로깅 - 시작 로깅
|
|
81
|
+
logger.info('Starting triple extraction batch job', {
|
|
82
|
+
batchSize: this.config.batchSize,
|
|
83
|
+
timeout: `${this.config.timeout}ms`,
|
|
84
|
+
parallelism: 1, // PRD 6.2: 병렬성 제어, 기본값 1 (싱글톤 배치 작업)
|
|
85
|
+
chunkSize: this.config.chunkSize,
|
|
86
|
+
chunkDelayMs: this.config.chunkDelayMs,
|
|
87
|
+
maxRetries: this.config.maxRetries,
|
|
88
|
+
retryBackoffDays: this.config.retryBackoffDays
|
|
89
|
+
});
|
|
90
|
+
// SemanticMemoryUpdateService 초기화 (아직 초기화되지 않은 경우)
|
|
91
|
+
if (!this.semanticMemoryUpdateService) {
|
|
92
|
+
this.semanticMemoryUpdateService = new SemanticMemoryUpdateService(db);
|
|
93
|
+
}
|
|
94
|
+
// 배치 작업 대상 조회
|
|
95
|
+
const targetMemories = await this.getTargetMemories(db, this.config.batchSize);
|
|
96
|
+
// PRD 6.3: 배치 작업 로깅 - 대상 조회 결과
|
|
97
|
+
logger.info('Found target memories for triple extraction', {
|
|
98
|
+
count: targetMemories.length,
|
|
99
|
+
batchSize: this.config.batchSize,
|
|
100
|
+
chunkSize: this.config.chunkSize,
|
|
101
|
+
estimatedChunks: Math.ceil(targetMemories.length / this.config.chunkSize),
|
|
102
|
+
estimatedDuration: targetMemories.length > 0
|
|
103
|
+
? `${Math.ceil((targetMemories.length * 3) / 60)} minutes (estimated)`
|
|
104
|
+
: '0 minutes'
|
|
105
|
+
});
|
|
106
|
+
// SQLite WAL 환경 고려: 작은 청크 단위로 나누어 처리하여 Lock 충돌 방지
|
|
107
|
+
// PRD 6.1: 배치 작업은 단일 트랜잭션으로 처리하지 않고, 작은 단위로 나누어 처리
|
|
108
|
+
const chunks = this.splitIntoChunks(targetMemories, this.config.chunkSize);
|
|
109
|
+
logger.debug('Split memories into chunks for WAL-safe processing', {
|
|
110
|
+
totalMemories: targetMemories.length,
|
|
111
|
+
chunkCount: chunks.length,
|
|
112
|
+
chunkSize: this.config.chunkSize
|
|
113
|
+
});
|
|
114
|
+
// 각 청크를 순차적으로 처리 (PRD 6.2: 병렬성 제어, parallelism=1)
|
|
115
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
116
|
+
// 타임아웃 체크 (PRD 6.2: 배치당 최대 30초)
|
|
117
|
+
const currentTime = Date.now();
|
|
118
|
+
if (currentTime >= timeoutDeadline) {
|
|
119
|
+
const elapsed = currentTime - startTime.getTime();
|
|
120
|
+
result.warnings.push(`Batch job timeout after ${elapsed}ms (limit: ${this.config.timeout}ms)`);
|
|
121
|
+
result.timeoutOccurred = true; // 타임아웃 플래그 설정
|
|
122
|
+
logger.warn('Triple extraction batch job timeout', {
|
|
123
|
+
elapsed,
|
|
124
|
+
timeout: this.config.timeout,
|
|
125
|
+
processed: result.details.processed,
|
|
126
|
+
remainingChunks: chunks.length - chunkIndex
|
|
127
|
+
});
|
|
128
|
+
break; // 타임아웃 발생 시 처리 중단
|
|
129
|
+
}
|
|
130
|
+
const chunk = chunks[chunkIndex];
|
|
131
|
+
if (!chunk) {
|
|
132
|
+
// 청크가 없으면 건너뛰기
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
// 청크 단위로 처리 (작은 트랜잭션으로 Lock 충돌 방지)
|
|
137
|
+
const chunkResult = await this.processChunk(db, chunk, result, timeoutDeadline);
|
|
138
|
+
// 청크 처리 결과를 전체 결과에 반영
|
|
139
|
+
result.details.processed += chunkResult.processed;
|
|
140
|
+
result.details.success += chunkResult.success;
|
|
141
|
+
result.details.failed += chunkResult.failed;
|
|
142
|
+
result.details.skipped += chunkResult.skipped;
|
|
143
|
+
result.details.semanticMemoriesCreated += chunkResult.semanticMemoriesCreated;
|
|
144
|
+
result.details.semanticMemoriesUpdated += chunkResult.semanticMemoriesUpdated;
|
|
145
|
+
// 청크별 재시도 횟수 반영
|
|
146
|
+
for (const [memoryId, retryCount] of chunkResult.retryCounts) {
|
|
147
|
+
result.details.retryCounts.set(memoryId, retryCount);
|
|
148
|
+
}
|
|
149
|
+
// 에러 수집
|
|
150
|
+
result.errors.push(...chunkResult.errors);
|
|
151
|
+
// PRD 6.3: 배치 작업 로깅 - 청크 처리 결과
|
|
152
|
+
logger.debug('Chunk processed', {
|
|
153
|
+
chunkIndex: chunkIndex + 1,
|
|
154
|
+
totalChunks: chunks.length,
|
|
155
|
+
chunkSize: chunk?.length ?? 0,
|
|
156
|
+
processed: chunkResult.processed,
|
|
157
|
+
success: chunkResult.success,
|
|
158
|
+
failed: chunkResult.failed,
|
|
159
|
+
skipped: chunkResult.skipped,
|
|
160
|
+
semanticMemoriesCreated: chunkResult.semanticMemoriesCreated,
|
|
161
|
+
semanticMemoriesUpdated: chunkResult.semanticMemoriesUpdated,
|
|
162
|
+
progress: `${((chunkIndex + 1) / chunks.length * 100).toFixed(1)}%`
|
|
163
|
+
});
|
|
164
|
+
// 마지막 청크가 아니면 짧은 지연 (SQLite WAL Lock 충돌 방지)
|
|
165
|
+
if (chunkIndex < chunks.length - 1 && this.config.chunkDelayMs > 0) {
|
|
166
|
+
await new Promise(resolve => setTimeout(resolve, this.config.chunkDelayMs));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
// 청크 처리 실패 시 에러 기록하고 다음 청크 계속 처리
|
|
171
|
+
const errorMessage = `Failed to process chunk ${chunkIndex + 1}/${chunks.length}: ${error instanceof Error ? error.message : String(error)}`;
|
|
172
|
+
result.errors.push(errorMessage);
|
|
173
|
+
logger.error('Error processing chunk in batch job', {
|
|
174
|
+
chunkIndex: chunkIndex + 1,
|
|
175
|
+
totalChunks: chunks.length,
|
|
176
|
+
error: error instanceof Error ? error.message : String(error)
|
|
177
|
+
});
|
|
178
|
+
// 청크 내 모든 메모리를 실패로 처리
|
|
179
|
+
result.details.failed += chunk?.length ?? 0;
|
|
180
|
+
result.details.processed += chunk?.length ?? 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 배치 작업 성공 여부: 처리된 항목이 있고, 치명적 에러가 없는 경우 성공으로 간주
|
|
184
|
+
// 부분 실패(failed > 0)는 허용 (재시도 정책에 따라 처리됨)
|
|
185
|
+
result.success = result.details.processed > 0;
|
|
186
|
+
result.processed = result.details.processed;
|
|
187
|
+
// PRD 6.3: 배치 작업 로깅
|
|
188
|
+
// - 처리된 Episodic Memory 수
|
|
189
|
+
// - 생성된 Semantic Memory 수
|
|
190
|
+
// - 실패한 항목 수 및 에러 로그
|
|
191
|
+
// - 배치 실행 시간 및 성능 메트릭
|
|
192
|
+
const duration = result.endTime.getTime() - result.startTime.getTime();
|
|
193
|
+
const durationSeconds = (duration / 1000).toFixed(2);
|
|
194
|
+
const avgProcessingTime = result.details.processed > 0
|
|
195
|
+
? (duration / result.details.processed).toFixed(0)
|
|
196
|
+
: '0';
|
|
197
|
+
const successRate = result.details.processed > 0
|
|
198
|
+
? ((result.details.success / result.details.processed) * 100).toFixed(1)
|
|
199
|
+
: '0.0';
|
|
200
|
+
logger.info('Triple extraction batch job completed', {
|
|
201
|
+
// 처리된 Episodic Memory 수
|
|
202
|
+
processed: result.details.processed,
|
|
203
|
+
success: result.details.success,
|
|
204
|
+
failed: result.details.failed,
|
|
205
|
+
skipped: result.details.skipped,
|
|
206
|
+
// 생성된 Semantic Memory 수
|
|
207
|
+
semanticMemoriesCreated: result.details.semanticMemoriesCreated,
|
|
208
|
+
semanticMemoriesUpdated: result.details.semanticMemoriesUpdated,
|
|
209
|
+
totalSemanticMemories: result.details.semanticMemoriesCreated + result.details.semanticMemoriesUpdated,
|
|
210
|
+
// 실패한 항목 수 및 에러 로그
|
|
211
|
+
errorCount: result.errors.length,
|
|
212
|
+
warningCount: result.warnings.length,
|
|
213
|
+
errors: result.errors.length > 0 ? result.errors.slice(0, 5) : [], // 최대 5개만 로깅
|
|
214
|
+
warnings: result.warnings.length > 0 ? result.warnings.slice(0, 5) : [], // 최대 5개만 로깅
|
|
215
|
+
// 배치 실행 시간 및 성능 메트릭
|
|
216
|
+
duration: `${durationSeconds}s`,
|
|
217
|
+
durationMs: duration,
|
|
218
|
+
avgProcessingTimeMs: avgProcessingTime,
|
|
219
|
+
successRate: `${successRate}%`,
|
|
220
|
+
// 재시도 통계
|
|
221
|
+
retryCounts: result.details.retryCounts.size > 0
|
|
222
|
+
? Array.from(result.details.retryCounts.values()).reduce((sum, count) => sum + count, 0)
|
|
223
|
+
: 0,
|
|
224
|
+
uniqueRetriedMemories: result.details.retryCounts.size,
|
|
225
|
+
// 청크 통계
|
|
226
|
+
totalChunks: chunks.length,
|
|
227
|
+
chunkSize: this.config.chunkSize,
|
|
228
|
+
// 작업 상태
|
|
229
|
+
jobStatus: result.success ? 'success' : 'partial_failure',
|
|
230
|
+
timeoutOccurred: result.warnings.some(w => w.includes('timeout'))
|
|
231
|
+
});
|
|
232
|
+
// 성능 메트릭 상세 로깅 (디버그 레벨)
|
|
233
|
+
logger.debug('Triple extraction batch job performance metrics', {
|
|
234
|
+
throughput: result.details.processed > 0
|
|
235
|
+
? `${(result.details.processed / (duration / 1000)).toFixed(2)} memories/sec`
|
|
236
|
+
: '0 memories/sec',
|
|
237
|
+
semanticMemoryCreationRate: result.details.semanticMemoriesCreated > 0
|
|
238
|
+
? `${(result.details.semanticMemoriesCreated / (duration / 1000)).toFixed(2)} semantic memories/sec`
|
|
239
|
+
: '0 semantic memories/sec',
|
|
240
|
+
chunkProcessingStats: {
|
|
241
|
+
totalChunks: chunks.length,
|
|
242
|
+
avgChunkSize: chunks.length > 0
|
|
243
|
+
? (targetMemories.length / chunks.length).toFixed(1)
|
|
244
|
+
: '0',
|
|
245
|
+
chunkDelayMs: this.config.chunkDelayMs
|
|
246
|
+
},
|
|
247
|
+
retryDistribution: result.details.retryCounts.size > 0
|
|
248
|
+
? Array.from(result.details.retryCounts.values()).reduce((acc, count) => {
|
|
249
|
+
acc[count] = (acc[count] || 0) + 1;
|
|
250
|
+
return acc;
|
|
251
|
+
}, {})
|
|
252
|
+
: {}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
// PRD 6.3: 배치 작업 로깅 - 에러 로그
|
|
257
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
258
|
+
result.errors.push(errorMessage);
|
|
259
|
+
const duration = result.endTime.getTime() - result.startTime.getTime();
|
|
260
|
+
logger.error('Triple extraction batch job failed', {
|
|
261
|
+
error: errorMessage,
|
|
262
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
263
|
+
processed: result.details.processed,
|
|
264
|
+
success: result.details.success,
|
|
265
|
+
failed: result.details.failed,
|
|
266
|
+
duration: `${(duration / 1000).toFixed(2)}s`,
|
|
267
|
+
errors: result.errors,
|
|
268
|
+
warnings: result.warnings
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
result.endTime = new Date();
|
|
273
|
+
result.duration = result.endTime.getTime() - result.startTime.getTime();
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 메모리 배열을 청크로 분할
|
|
279
|
+
*
|
|
280
|
+
* SQLite WAL 환경 고려: 작은 단위로 나누어 처리하여 Lock 충돌 방지
|
|
281
|
+
*
|
|
282
|
+
* @param memories 메모리 배열
|
|
283
|
+
* @param chunkSize 청크 크기
|
|
284
|
+
* @returns 청크 배열
|
|
285
|
+
*/
|
|
286
|
+
splitIntoChunks(memories, chunkSize) {
|
|
287
|
+
const chunks = [];
|
|
288
|
+
for (let i = 0; i < memories.length; i += chunkSize) {
|
|
289
|
+
chunks.push(memories.slice(i, i + chunkSize));
|
|
290
|
+
}
|
|
291
|
+
return chunks;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 청크 단위 처리
|
|
295
|
+
*
|
|
296
|
+
* SQLite WAL 환경 고려: 작은 트랜잭션으로 처리하여 Lock 충돌 방지
|
|
297
|
+
* 각 메모리 처리는 개별적으로 처리되며, SQLITE_BUSY 오류 발생 시 재시도
|
|
298
|
+
*
|
|
299
|
+
* PRD 6.2 배치 처리 최적화:
|
|
300
|
+
* - 타임아웃 체크: 배치당 최대 30초
|
|
301
|
+
* - 병렬성 제어: 순차 처리 (parallelism=1)
|
|
302
|
+
*
|
|
303
|
+
* @param db 데이터베이스 연결
|
|
304
|
+
* @param chunk 청크 (메모리 배열)
|
|
305
|
+
* @param overallResult 전체 결과 객체 (로깅용)
|
|
306
|
+
* @param timeoutDeadline 타임아웃 데드라인 (타임스탬프)
|
|
307
|
+
* @returns 청크 처리 결과
|
|
308
|
+
*/
|
|
309
|
+
async processChunk(db, chunk, overallResult, timeoutDeadline) {
|
|
310
|
+
const chunkResult = {
|
|
311
|
+
processed: 0,
|
|
312
|
+
success: 0,
|
|
313
|
+
failed: 0,
|
|
314
|
+
skipped: 0,
|
|
315
|
+
semanticMemoriesCreated: 0,
|
|
316
|
+
semanticMemoriesUpdated: 0,
|
|
317
|
+
retryCounts: new Map(),
|
|
318
|
+
errors: []
|
|
319
|
+
};
|
|
320
|
+
// 각 메모리를 개별적으로 처리 (작은 트랜잭션으로 Lock 충돌 방지)
|
|
321
|
+
// PRD 6.2: 병렬성 제어, 순차 처리 (parallelism=1)
|
|
322
|
+
for (const memory of chunk) {
|
|
323
|
+
// 타임아웃 체크 (PRD 6.2: 배치당 최대 30초)
|
|
324
|
+
if (Date.now() >= timeoutDeadline) {
|
|
325
|
+
overallResult.timeoutOccurred = true; // 타임아웃 플래그 설정
|
|
326
|
+
logger.warn('Chunk processing timeout, stopping chunk processing', {
|
|
327
|
+
chunkSize: chunk.length,
|
|
328
|
+
processedInChunk: chunkResult.processed
|
|
329
|
+
});
|
|
330
|
+
break; // 타임아웃 발생 시 청크 처리 중단
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
// 재시도 정책 확인 (getTargetMemories에서 이미 필터링되었지만, 이중 확인)
|
|
334
|
+
const shouldRetry = this.shouldRetry(memory);
|
|
335
|
+
if (!shouldRetry) {
|
|
336
|
+
chunkResult.skipped++;
|
|
337
|
+
logger.debug('Skipping memory due to retry policy', {
|
|
338
|
+
memory_id: memory.id,
|
|
339
|
+
retry_count: this.getRetryCount(memory)
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// Triple 추출 수행 (LLM 호출, 트랜잭션 외부에서 처리)
|
|
344
|
+
const extractionResult = await this.tripleExtractionService.extractTriples(memory.content);
|
|
345
|
+
if (extractionResult.triples.length > 0) {
|
|
346
|
+
// Semantic Memory 생성/업데이트 (트랜잭션 내부에서 처리)
|
|
347
|
+
// DatabaseUtils.runTransaction은 SQLITE_BUSY 오류 발생 시 자동 재시도
|
|
348
|
+
await DatabaseUtils.runTransaction(db, async () => {
|
|
349
|
+
const updateResult = await this.semanticMemoryUpdateService.updateSemanticMemory(extractionResult, {
|
|
350
|
+
episodicMemoryId: memory.id,
|
|
351
|
+
episodicImportance: memory.importance ?? 0.5
|
|
352
|
+
});
|
|
353
|
+
// 성공 상태 업데이트 (트랜잭션 내부)
|
|
354
|
+
const confidenceAvg = await this.calculateAverageConfidence(db, memory.id);
|
|
355
|
+
const successMetadata = {
|
|
356
|
+
triple_count: extractionResult.triples.length,
|
|
357
|
+
extracted_at: new Date().toISOString()
|
|
358
|
+
};
|
|
359
|
+
if (confidenceAvg !== null) {
|
|
360
|
+
successMetadata.confidence_avg = confidenceAvg;
|
|
361
|
+
}
|
|
362
|
+
await this.updateMemoryStatus(db, memory.id, 'success', successMetadata);
|
|
363
|
+
chunkResult.success++;
|
|
364
|
+
chunkResult.semanticMemoriesCreated += updateResult.created;
|
|
365
|
+
chunkResult.semanticMemoriesUpdated += updateResult.updated;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Triple 추출 실패 - 상태 업데이트 (트랜잭션 내부)
|
|
370
|
+
const failureReason = extractionResult.extractionInfo.failureReason || 'no_triple';
|
|
371
|
+
const currentRetryCount = this.getRetryCount(memory);
|
|
372
|
+
const newRetryCount = currentRetryCount + 1;
|
|
373
|
+
await DatabaseUtils.runTransaction(db, async () => {
|
|
374
|
+
// 재시도 정책 적용: 최대 시도 횟수 확인
|
|
375
|
+
if (newRetryCount >= this.config.maxRetries) {
|
|
376
|
+
// 최대 시도 횟수 초과 - abandoned 상태로 설정
|
|
377
|
+
await this.updateMemoryStatus(db, memory.id, 'abandoned', {
|
|
378
|
+
failureReason,
|
|
379
|
+
retry_count: newRetryCount,
|
|
380
|
+
last_attempt: new Date().toISOString(),
|
|
381
|
+
abandoned_at: new Date().toISOString()
|
|
382
|
+
});
|
|
383
|
+
logger.info('Triple extraction abandoned after max retries', {
|
|
384
|
+
memory_id: memory.id,
|
|
385
|
+
retry_count: newRetryCount,
|
|
386
|
+
max_retries: this.config.maxRetries,
|
|
387
|
+
failure_reason: failureReason
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// 재시도 가능 - failed 상태로 업데이트
|
|
392
|
+
const backoffDays = this.config.retryBackoffDays[newRetryCount - 1] ||
|
|
393
|
+
this.config.retryBackoffDays[this.config.retryBackoffDays.length - 1];
|
|
394
|
+
await this.updateMemoryStatus(db, memory.id, 'failed', {
|
|
395
|
+
failureReason,
|
|
396
|
+
retry_count: newRetryCount,
|
|
397
|
+
last_attempt: new Date().toISOString(),
|
|
398
|
+
next_retry_after_days: backoffDays
|
|
399
|
+
});
|
|
400
|
+
logger.debug('Triple extraction failed, will retry after backoff', {
|
|
401
|
+
memory_id: memory.id,
|
|
402
|
+
retry_count: newRetryCount,
|
|
403
|
+
backoff_days: backoffDays,
|
|
404
|
+
failure_reason: failureReason
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
chunkResult.failed++;
|
|
409
|
+
chunkResult.retryCounts.set(memory.id, newRetryCount);
|
|
410
|
+
}
|
|
411
|
+
chunkResult.processed++;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
// SQLITE_BUSY 오류는 DatabaseUtils.runTransaction에서 이미 재시도됨
|
|
415
|
+
// 여기서는 예상치 못한 오류만 처리
|
|
416
|
+
const errorMessage = `Failed to process memory ${memory.id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
417
|
+
chunkResult.errors.push(errorMessage);
|
|
418
|
+
chunkResult.failed++;
|
|
419
|
+
chunkResult.processed++;
|
|
420
|
+
logger.error('Error processing episodic memory in batch job', {
|
|
421
|
+
memory_id: memory.id,
|
|
422
|
+
error: error instanceof Error ? error.message : String(error),
|
|
423
|
+
error_code: error?.code
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return chunkResult;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* 배치 작업 대상 조회
|
|
431
|
+
*
|
|
432
|
+
* @param db 데이터베이스 연결
|
|
433
|
+
* @param limit 최대 조회 개수
|
|
434
|
+
* @returns 대상 Episodic Memory 목록 (재시도 정책 적용)
|
|
435
|
+
*/
|
|
436
|
+
async getTargetMemories(db, limit) {
|
|
437
|
+
// 미처리 또는 실패한 Episodic Memory 조회
|
|
438
|
+
// - triple_extracted=false 또는 null (미처리)
|
|
439
|
+
// - triple_extracted_status='failed' (재시도 가능, 백오프 간격 확인 필요)
|
|
440
|
+
// - triple_extracted_status IS NULL (미처리)
|
|
441
|
+
// - abandoned 상태는 제외 (수동 재시도 필요)
|
|
442
|
+
const memories = DatabaseUtils.all(db, `
|
|
443
|
+
SELECT
|
|
444
|
+
id,
|
|
445
|
+
content,
|
|
446
|
+
importance,
|
|
447
|
+
triple_extracted,
|
|
448
|
+
triple_extracted_status,
|
|
449
|
+
triple_extraction_metadata
|
|
450
|
+
FROM memory_item
|
|
451
|
+
WHERE type = 'episodic'
|
|
452
|
+
AND (
|
|
453
|
+
triple_extracted IS NULL
|
|
454
|
+
OR triple_extracted = 0
|
|
455
|
+
OR triple_extracted_status = 'failed'
|
|
456
|
+
)
|
|
457
|
+
AND (triple_extracted_status IS NULL OR triple_extracted_status != 'abandoned')
|
|
458
|
+
ORDER BY created_at ASC
|
|
459
|
+
LIMIT ?
|
|
460
|
+
`, [limit]);
|
|
461
|
+
// 재시도 정책 적용: 백오프 간격이 지나지 않은 항목은 제외
|
|
462
|
+
const now = new Date();
|
|
463
|
+
const filteredMemories = memories.filter(memory => {
|
|
464
|
+
// 미처리 항목은 항상 포함
|
|
465
|
+
if (memory.triple_extracted_status === null || memory.triple_extracted_status === '') {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
// failed 상태인 경우 재시도 정책 확인
|
|
469
|
+
if (memory.triple_extracted_status === 'failed') {
|
|
470
|
+
return this.shouldRetry(memory, now);
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
});
|
|
474
|
+
return filteredMemories;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* 재시도 정책 확인
|
|
478
|
+
*
|
|
479
|
+
* @param memory Episodic Memory (triple_extraction_metadata 포함)
|
|
480
|
+
* @param now 현재 시간
|
|
481
|
+
* @returns 재시도 가능 여부
|
|
482
|
+
*/
|
|
483
|
+
shouldRetry(memory, now = new Date()) {
|
|
484
|
+
// 최대 재시도 횟수 확인
|
|
485
|
+
const retryCount = this.getRetryCount(memory);
|
|
486
|
+
if (retryCount >= this.config.maxRetries) {
|
|
487
|
+
return false; // 최대 시도 횟수 초과
|
|
488
|
+
}
|
|
489
|
+
// 미처리 항목은 항상 재시도 가능
|
|
490
|
+
if (!memory.triple_extraction_metadata) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const metadata = JSON.parse(memory.triple_extraction_metadata);
|
|
495
|
+
const lastAttempt = metadata.last_attempt;
|
|
496
|
+
if (!lastAttempt) {
|
|
497
|
+
return true; // last_attempt가 없으면 재시도 가능
|
|
498
|
+
}
|
|
499
|
+
// 백오프 간격 확인 (지수 백오프: 1일, 2일, 4일)
|
|
500
|
+
const lastAttemptDate = new Date(lastAttempt);
|
|
501
|
+
const daysSinceLastAttempt = Math.floor((now.getTime() - lastAttemptDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
502
|
+
// 현재 retry_count에 해당하는 백오프 간격 확인
|
|
503
|
+
// retry_count가 0이면 첫 번째 재시도 (1일), 1이면 두 번째 재시도 (2일), 2이면 세 번째 재시도 (4일)
|
|
504
|
+
const backoffDays = this.config.retryBackoffDays[retryCount] ??
|
|
505
|
+
this.config.retryBackoffDays[this.config.retryBackoffDays.length - 1] ?? 1;
|
|
506
|
+
// 백오프 간격이 지났으면 재시도 가능
|
|
507
|
+
return daysSinceLastAttempt >= backoffDays;
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
// 메타데이터 파싱 실패 시 재시도 가능으로 간주
|
|
511
|
+
logger.warn('Failed to parse triple_extraction_metadata for retry check', {
|
|
512
|
+
memory_id: memory.id,
|
|
513
|
+
error: error instanceof Error ? error.message : String(error)
|
|
514
|
+
});
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* 재시도 횟수 조회
|
|
520
|
+
*
|
|
521
|
+
* @param memory Episodic Memory (triple_extraction_metadata 포함)
|
|
522
|
+
* @returns 재시도 횟수
|
|
523
|
+
*/
|
|
524
|
+
getRetryCount(memory) {
|
|
525
|
+
if (!memory.triple_extraction_metadata) {
|
|
526
|
+
return 0; // 메타데이터가 없으면 재시도 횟수 0
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const metadata = JSON.parse(memory.triple_extraction_metadata);
|
|
530
|
+
return metadata.retry_count || 0;
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
logger.warn('Failed to parse triple_extraction_metadata for retry count', {
|
|
534
|
+
memory_id: memory.id,
|
|
535
|
+
error: error instanceof Error ? error.message : String(error)
|
|
536
|
+
});
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 메모리 상태 업데이트
|
|
542
|
+
*
|
|
543
|
+
* PRD 1.4 재시도 종료 조건 및 필드 조합 규칙에 따라 상태를 업데이트합니다.
|
|
544
|
+
*
|
|
545
|
+
* 상태 전이 규칙:
|
|
546
|
+
* - 성공 시: triple_extracted=true, triple_extracted_status='success', 이전 실패 기록 초기화
|
|
547
|
+
* - 최대 시도 횟수 초과 시: triple_extracted=false, triple_extracted_status='abandoned', 재시도 중단
|
|
548
|
+
* - 재시도 가능 시: triple_extracted=false, triple_extracted_status='failed', 재시도 정보 저장
|
|
549
|
+
*
|
|
550
|
+
* 필드 조합 규칙 (PRD 1.4):
|
|
551
|
+
* | triple_extracted | triple_extracted_status | 의미 |
|
|
552
|
+
* | ---------------- | ----------------------- | ---- |
|
|
553
|
+
* | NULL | NULL | 미처리 |
|
|
554
|
+
* | true | 'success' | 성공 |
|
|
555
|
+
* | false | 'failed' | 실패 (재시도 가능) |
|
|
556
|
+
* | false | 'abandoned' | 포기 (수동 재시도 필요) |
|
|
557
|
+
* | false | NULL | 미처리 또는 초기 상태 |
|
|
558
|
+
*
|
|
559
|
+
* 이 메서드는 필드 조합 규칙을 준수하여 triple_extracted와 triple_extracted_status를 동기화합니다.
|
|
560
|
+
*
|
|
561
|
+
* @param db 데이터베이스 연결
|
|
562
|
+
* @param memoryId 메모리 ID
|
|
563
|
+
* @param status 상태 ('success' | 'failed' | 'abandoned')
|
|
564
|
+
* @param metadata 메타데이터
|
|
565
|
+
*/
|
|
566
|
+
async updateMemoryStatus(db, memoryId, status, metadata) {
|
|
567
|
+
// 필드 조합 규칙에 따른 triple_extracted boolean 동기화
|
|
568
|
+
// PRD 1.4 필드 조합 규칙 준수:
|
|
569
|
+
// - 성공: triple_extracted=true, triple_extracted_status='success'
|
|
570
|
+
// - 실패: triple_extracted=false, triple_extracted_status='failed' (재시도 가능)
|
|
571
|
+
// - 포기: triple_extracted=false, triple_extracted_status='abandoned' (수동 재시도 필요)
|
|
572
|
+
//
|
|
573
|
+
// 상태 전이 시 항상 두 필드를 함께 업데이트하여 일관성 유지
|
|
574
|
+
// 이는 필드 조합 규칙을 보장하기 위한 필수 동기화 로직입니다.
|
|
575
|
+
const tripleExtracted = status === 'success' ? true : false;
|
|
576
|
+
// 필드 조합 규칙 준수: triple_extracted와 triple_extracted_status를 항상 함께 업데이트
|
|
577
|
+
// 이는 데이터 일관성을 보장하기 위한 필수 동기화 로직입니다.
|
|
578
|
+
// SQLite에서는 boolean을 INTEGER로 변환 (true=1, false=0)
|
|
579
|
+
await DatabaseUtils.run(db, `
|
|
580
|
+
UPDATE memory_item SET
|
|
581
|
+
triple_extracted = ?,
|
|
582
|
+
triple_extracted_status = ?,
|
|
583
|
+
triple_extraction_metadata = ?
|
|
584
|
+
WHERE id = ?
|
|
585
|
+
`, [
|
|
586
|
+
tripleExtracted ? 1 : 0, // SQLite에서는 boolean을 INTEGER로 변환
|
|
587
|
+
status,
|
|
588
|
+
JSON.stringify(metadata),
|
|
589
|
+
memoryId
|
|
590
|
+
]);
|
|
591
|
+
// 상태 전이 로깅 (필드 조합 규칙 준수 확인)
|
|
592
|
+
logger.debug('Memory status updated with field combination rule', {
|
|
593
|
+
memory_id: memoryId,
|
|
594
|
+
triple_extracted: tripleExtracted,
|
|
595
|
+
triple_extracted_status: status,
|
|
596
|
+
field_combination_valid: ((status === 'success' && tripleExtracted === true) ||
|
|
597
|
+
((status === 'failed' || status === 'abandoned') && tripleExtracted === false))
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* 평균 Confidence 계산
|
|
602
|
+
*
|
|
603
|
+
* memory_relation 테이블에서 해당 Episodic Memory에서 생성된 모든 관계의 confidence 값을 수집하여 평균 계산
|
|
604
|
+
*
|
|
605
|
+
* @param db 데이터베이스 연결
|
|
606
|
+
* @param episodicMemoryId Episodic Memory ID
|
|
607
|
+
* @returns 평균 Confidence (0.0~1.0), 관계가 없으면 null
|
|
608
|
+
*/
|
|
609
|
+
async calculateAverageConfidence(db, episodicMemoryId) {
|
|
610
|
+
try {
|
|
611
|
+
// memory_relation에서 confidence 값 수집 (각 triple별로 저장됨)
|
|
612
|
+
const relations = DatabaseUtils.all(db, `
|
|
613
|
+
SELECT confidence FROM memory_relation
|
|
614
|
+
WHERE source_id = ? AND relation_type = 'extracted_from'
|
|
615
|
+
`, [episodicMemoryId]);
|
|
616
|
+
if (relations.length === 0) {
|
|
617
|
+
return null; // 관계가 없으면 null 반환
|
|
618
|
+
}
|
|
619
|
+
// confidence 값이 있는 관계만 필터링
|
|
620
|
+
const confidenceValues = relations
|
|
621
|
+
.map(rel => rel.confidence)
|
|
622
|
+
.filter((c) => c !== null && c !== undefined);
|
|
623
|
+
if (confidenceValues.length === 0) {
|
|
624
|
+
return null; // confidence 값이 없으면 null 반환
|
|
625
|
+
}
|
|
626
|
+
// 평균 계산
|
|
627
|
+
const average = confidenceValues.reduce((sum, c) => sum + c, 0) / confidenceValues.length;
|
|
628
|
+
return Math.min(1.0, Math.max(0.0, average)); // 0.0~1.0 범위로 정규화
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
logger.warn('Failed to calculate average confidence', {
|
|
632
|
+
episodic_memory_id: episodicMemoryId,
|
|
633
|
+
error: error instanceof Error ? error.message : String(error)
|
|
634
|
+
});
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
//# sourceMappingURL=triple-extraction-batch-job.js.map
|