memento-mcp-server 1.11.1 → 1.12.0-a1
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.en.md +21 -6
- package/README.md +38 -7
- package/dist/algorithms/hybrid-search-engine.d.ts +34 -1
- package/dist/algorithms/hybrid-search-engine.d.ts.map +1 -1
- package/dist/algorithms/hybrid-search-engine.js +186 -17
- package/dist/algorithms/hybrid-search-engine.js.map +1 -1
- package/dist/algorithms/search-ranking.d.ts +15 -1
- package/dist/algorithms/search-ranking.d.ts.map +1 -1
- package/dist/algorithms/search-ranking.js +41 -4
- package/dist/algorithms/search-ranking.js.map +1 -1
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/environment.js +4 -0
- package/dist/config/environment.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/ranking-weights-loader.d.ts +37 -0
- package/dist/config/ranking-weights-loader.d.ts.map +1 -0
- package/dist/config/ranking-weights-loader.js +109 -0
- package/dist/config/ranking-weights-loader.js.map +1 -0
- package/dist/constants/relation-constants.d.ts +95 -0
- package/dist/constants/relation-constants.d.ts.map +1 -0
- package/dist/constants/relation-constants.js +95 -0
- package/dist/constants/relation-constants.js.map +1 -0
- package/dist/database/migration/migrations/005-relation-engine-schema.d.ts +65 -0
- package/dist/database/migration/migrations/005-relation-engine-schema.d.ts.map +1 -0
- package/dist/database/migration/migrations/005-relation-engine-schema.js +295 -0
- package/dist/database/migration/migrations/005-relation-engine-schema.js.map +1 -0
- package/dist/database/migration/migrations/005-relation-engine-schema.sql +64 -0
- package/dist/server/routes/admin.routes.d.ts.map +1 -1
- package/dist/server/routes/admin.routes.js +228 -0
- package/dist/server/routes/admin.routes.js.map +1 -1
- package/dist/services/anchor/anchor-interfaces.d.ts +1 -0
- package/dist/services/anchor/anchor-interfaces.d.ts.map +1 -1
- package/dist/services/anchor/anchor-interfaces.js.map +1 -1
- package/dist/services/anchor/anchor-search-service.d.ts +16 -0
- package/dist/services/anchor/anchor-search-service.d.ts.map +1 -1
- package/dist/services/anchor/anchor-search-service.js +136 -17
- package/dist/services/anchor/anchor-search-service.js.map +1 -1
- package/dist/services/batch-scheduler.d.ts +11 -0
- package/dist/services/batch-scheduler.d.ts.map +1 -1
- package/dist/services/batch-scheduler.js +99 -0
- package/dist/services/batch-scheduler.js.map +1 -1
- package/dist/services/llm-based-relation-extractor.d.ts +156 -0
- package/dist/services/llm-based-relation-extractor.d.ts.map +1 -0
- package/dist/services/llm-based-relation-extractor.js +1350 -0
- package/dist/services/llm-based-relation-extractor.js.map +1 -0
- package/dist/services/relation-extractor.d.ts +73 -0
- package/dist/services/relation-extractor.d.ts.map +1 -0
- package/dist/services/relation-extractor.js +231 -0
- package/dist/services/relation-extractor.js.map +1 -0
- package/dist/services/relation-graph.d.ts +275 -0
- package/dist/services/relation-graph.d.ts.map +1 -0
- package/dist/services/relation-graph.js +869 -0
- package/dist/services/relation-graph.js.map +1 -0
- package/dist/services/relation-quality-validator.d.ts +211 -0
- package/dist/services/relation-quality-validator.d.ts.map +1 -0
- package/dist/services/relation-quality-validator.js +415 -0
- package/dist/services/relation-quality-validator.js.map +1 -0
- package/dist/services/rule-based-relation-extractor.d.ts +66 -0
- package/dist/services/rule-based-relation-extractor.d.ts.map +1 -0
- package/dist/services/rule-based-relation-extractor.js +258 -0
- package/dist/services/rule-based-relation-extractor.js.map +1 -0
- package/dist/tools/add-relation-tool.d.ts +34 -0
- package/dist/tools/add-relation-tool.d.ts.map +1 -0
- package/dist/tools/add-relation-tool.js +163 -0
- package/dist/tools/add-relation-tool.js.map +1 -0
- package/dist/tools/extract-relations-tool.d.ts +28 -0
- package/dist/tools/extract-relations-tool.d.ts.map +1 -0
- package/dist/tools/extract-relations-tool.js +159 -0
- package/dist/tools/extract-relations-tool.js.map +1 -0
- package/dist/tools/get-relations-tool.d.ts +34 -0
- package/dist/tools/get-relations-tool.d.ts.map +1 -0
- package/dist/tools/get-relations-tool.js +155 -0
- package/dist/tools/get-relations-tool.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/remember-tool.d.ts +17 -0
- package/dist/tools/remember-tool.d.ts.map +1 -1
- package/dist/tools/remember-tool.js +195 -26
- package/dist/tools/remember-tool.js.map +1 -1
- package/dist/tools/remove-relation-tool.d.ts +45 -0
- package/dist/tools/remove-relation-tool.d.ts.map +1 -0
- package/dist/tools/remove-relation-tool.js +142 -0
- package/dist/tools/remove-relation-tool.js.map +1 -0
- package/dist/tools/search-local-tool.d.ts.map +1 -1
- package/dist/tools/search-local-tool.js +10 -3
- package/dist/tools/search-local-tool.js.map +1 -1
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/visualize-relations-tool.d.ts +46 -0
- package/dist/tools/visualize-relations-tool.d.ts.map +1 -0
- package/dist/tools/visualize-relations-tool.js +157 -0
- package/dist/tools/visualize-relations-tool.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/relation-graph.d.ts +215 -0
- package/dist/types/relation-graph.d.ts.map +1 -0
- package/dist/types/relation-graph.js +6 -0
- package/dist/types/relation-graph.js.map +1 -0
- package/dist/types/relation.d.ts +112 -0
- package/dist/types/relation.d.ts.map +1 -0
- package/dist/types/relation.js +67 -0
- package/dist/types/relation.js.map +1 -0
- package/dist/utils/cache-key-generator.d.ts +63 -0
- package/dist/utils/cache-key-generator.d.ts.map +1 -0
- package/dist/utils/cache-key-generator.js +76 -0
- package/dist/utils/cache-key-generator.js.map +1 -0
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +37 -17
- package/dist/utils/database.js.map +1 -1
- package/dist/utils/relation-visualizer.d.ts +81 -0
- package/dist/utils/relation-visualizer.d.ts.map +1 -0
- package/dist/utils/relation-visualizer.js +239 -0
- package/dist/utils/relation-visualizer.js.map +1 -0
- package/dist/utils/type-guards.d.ts +100 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +144 -0
- package/dist/utils/type-guards.js.map +1 -0
- package/package.json +7 -2
- package/scripts/generate-relation-report.ts +481 -0
- package/scripts/weekly-relation-validation.ts +423 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 주간 관계 추출 품질 검증 스크립트
|
|
4
|
+
* 매주 자동으로 관계 추출 품질을 검증하고 리포트를 생성합니다.
|
|
5
|
+
*
|
|
6
|
+
* 사용법:
|
|
7
|
+
* npm run weekly-relation-validation
|
|
8
|
+
* npm run weekly-relation-validation -- --output weekly-report.md
|
|
9
|
+
* npm run weekly-relation-validation -- --method hybrid --allow-soft-fail
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import Database from 'better-sqlite3';
|
|
15
|
+
import { RelationExtractor } from '../src/services/relation-extractor.js';
|
|
16
|
+
import { RelationQualityValidator } from '../src/services/relation-quality-validator.js';
|
|
17
|
+
import type { ExpectedRelation, ExtractedRelation } from '../src/services/relation-quality-validator.js';
|
|
18
|
+
import { DatabaseUtils } from '../src/utils/database.js';
|
|
19
|
+
import { RelationEngineSchemaMigration } from '../src/database/migration/migrations/005-relation-engine-schema.js';
|
|
20
|
+
import type { MemoryItem } from '../src/types/index.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 명령줄 인자 파싱
|
|
24
|
+
*/
|
|
25
|
+
interface CliOptions {
|
|
26
|
+
output?: string;
|
|
27
|
+
method?: 'rule' | 'llm' | 'hybrid';
|
|
28
|
+
minConfidence?: number;
|
|
29
|
+
allowSoftFail?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(): CliOptions {
|
|
33
|
+
const args = process.argv.slice(2);
|
|
34
|
+
const options: CliOptions = {};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const arg = args[i];
|
|
38
|
+
if (arg === '--output' && args[i + 1]) {
|
|
39
|
+
options.output = args[i + 1];
|
|
40
|
+
i++;
|
|
41
|
+
} else if (arg === '--method' && args[i + 1]) {
|
|
42
|
+
const method = args[i + 1] as 'rule' | 'llm' | 'hybrid';
|
|
43
|
+
if (['rule', 'llm', 'hybrid'].includes(method)) {
|
|
44
|
+
options.method = method;
|
|
45
|
+
}
|
|
46
|
+
i++;
|
|
47
|
+
} else if (arg === '--min-confidence' && args[i + 1]) {
|
|
48
|
+
options.minConfidence = parseFloat(args[i + 1]);
|
|
49
|
+
i++;
|
|
50
|
+
} else if (arg === '--allow-soft-fail') {
|
|
51
|
+
options.allowSoftFail = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 테스트 데이터셋 로드
|
|
60
|
+
*/
|
|
61
|
+
function loadTestDataset(): ExpectedRelation[] {
|
|
62
|
+
const testsetPath = join(process.cwd(), 'tests', 'fixtures', 'relation_testset.json');
|
|
63
|
+
const testsetContent = readFileSync(testsetPath, 'utf-8');
|
|
64
|
+
const testset = JSON.parse(testsetContent) as Array<{
|
|
65
|
+
source_id: string;
|
|
66
|
+
target_id: string;
|
|
67
|
+
expected_relation_type: string;
|
|
68
|
+
expected_confidence_range: [number, number];
|
|
69
|
+
source_content: string;
|
|
70
|
+
target_content: string;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
return testset.map(item => ({
|
|
74
|
+
source_id: item.source_id,
|
|
75
|
+
target_id: item.target_id,
|
|
76
|
+
expected_relation_type: item.expected_relation_type as any,
|
|
77
|
+
expected_confidence_range: item.expected_confidence_range,
|
|
78
|
+
source_content: item.source_content,
|
|
79
|
+
target_content: item.target_content
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 테스트용 기본 스키마 생성
|
|
85
|
+
*/
|
|
86
|
+
function createBaseSchema(db: Database.Database): void {
|
|
87
|
+
db.exec(`
|
|
88
|
+
CREATE TABLE IF NOT EXISTS memory_item (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
type TEXT CHECK (type IN ('working','episodic','semantic','procedural')) NOT NULL,
|
|
91
|
+
content TEXT NOT NULL,
|
|
92
|
+
importance REAL CHECK (importance >= 0 AND importance <= 1) DEFAULT 0.5,
|
|
93
|
+
privacy_scope TEXT CHECK (privacy_scope IN ('private','team','public')) DEFAULT 'private',
|
|
94
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
95
|
+
last_accessed TIMESTAMP,
|
|
96
|
+
pinned BOOLEAN DEFAULT FALSE,
|
|
97
|
+
tags TEXT,
|
|
98
|
+
source TEXT,
|
|
99
|
+
view_count INTEGER DEFAULT 0,
|
|
100
|
+
cite_count INTEGER DEFAULT 0,
|
|
101
|
+
edit_count INTEGER DEFAULT 0
|
|
102
|
+
);
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS memento_schema_version (
|
|
107
|
+
version INTEGER PRIMARY KEY,
|
|
108
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
109
|
+
);
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 테스트용 메모리 생성
|
|
115
|
+
*/
|
|
116
|
+
function createTestMemory(
|
|
117
|
+
db: Database.Database,
|
|
118
|
+
id: string,
|
|
119
|
+
content: string,
|
|
120
|
+
type: string = 'episodic'
|
|
121
|
+
): void {
|
|
122
|
+
DatabaseUtils.run(db, `
|
|
123
|
+
INSERT INTO memory_item (id, type, content)
|
|
124
|
+
VALUES (?, ?, ?)
|
|
125
|
+
`, [id, type, content]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 관계 추출 수행
|
|
130
|
+
*/
|
|
131
|
+
async function extractRelations(
|
|
132
|
+
testDataset: ExpectedRelation[],
|
|
133
|
+
relationExtractor: RelationExtractor,
|
|
134
|
+
method: 'rule' | 'llm' | 'hybrid',
|
|
135
|
+
minConfidence: number
|
|
136
|
+
): Promise<ExtractedRelation[]> {
|
|
137
|
+
const memoryMap = new Map<string, MemoryItem>();
|
|
138
|
+
|
|
139
|
+
// 모든 메모리 생성
|
|
140
|
+
for (const testCase of testDataset) {
|
|
141
|
+
if (!memoryMap.has(testCase.source_id)) {
|
|
142
|
+
memoryMap.set(testCase.source_id, {
|
|
143
|
+
id: testCase.source_id,
|
|
144
|
+
type: 'episodic',
|
|
145
|
+
content: testCase.source_content,
|
|
146
|
+
importance: 0.5,
|
|
147
|
+
privacy_scope: 'private',
|
|
148
|
+
created_at: new Date().toISOString()
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!memoryMap.has(testCase.target_id)) {
|
|
153
|
+
memoryMap.set(testCase.target_id, {
|
|
154
|
+
id: testCase.target_id,
|
|
155
|
+
type: 'episodic',
|
|
156
|
+
content: testCase.target_content,
|
|
157
|
+
importance: 0.5,
|
|
158
|
+
privacy_scope: 'private',
|
|
159
|
+
created_at: new Date().toISOString()
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 소스 메모리별로 그룹화
|
|
165
|
+
const sourceGroups = new Map<string, ExpectedRelation[]>();
|
|
166
|
+
for (const testCase of testDataset) {
|
|
167
|
+
if (!sourceGroups.has(testCase.source_id)) {
|
|
168
|
+
sourceGroups.set(testCase.source_id, []);
|
|
169
|
+
}
|
|
170
|
+
sourceGroups.get(testCase.source_id)!.push(testCase);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 관계 추출 수행
|
|
174
|
+
const extractedRelations: ExtractedRelation[] = [];
|
|
175
|
+
|
|
176
|
+
for (const [sourceId, testCases] of sourceGroups.entries()) {
|
|
177
|
+
const sourceMemory = memoryMap.get(sourceId)!;
|
|
178
|
+
const targetMemories = testCases.map(tc => memoryMap.get(tc.target_id)!).filter(Boolean);
|
|
179
|
+
|
|
180
|
+
if (targetMemories.length > 0) {
|
|
181
|
+
try {
|
|
182
|
+
const candidates = await relationExtractor.extractRelations(
|
|
183
|
+
sourceMemory,
|
|
184
|
+
targetMemories,
|
|
185
|
+
{ method, minConfidence }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
for (const candidate of candidates) {
|
|
189
|
+
extractedRelations.push({
|
|
190
|
+
source_id: candidate.source_id,
|
|
191
|
+
target_id: candidate.target_id,
|
|
192
|
+
relation_type: candidate.relation_type,
|
|
193
|
+
confidence: candidate.confidence
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn(`관계 추출 실패: ${sourceId}`, error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return extractedRelations;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Markdown 리포트 생성
|
|
207
|
+
*/
|
|
208
|
+
function generateMarkdownReport(
|
|
209
|
+
metrics: any,
|
|
210
|
+
options: CliOptions,
|
|
211
|
+
qualityValidator: RelationQualityValidator
|
|
212
|
+
): string {
|
|
213
|
+
const timestamp = new Date().toISOString();
|
|
214
|
+
const method = options.method || 'hybrid';
|
|
215
|
+
|
|
216
|
+
let report = `# 주간 관계 추출 품질 검증 리포트\n\n`;
|
|
217
|
+
report += `**생성 일시**: ${timestamp}\n`;
|
|
218
|
+
report += `**추출 방법**: ${method}\n`;
|
|
219
|
+
report += `**최소 신뢰도**: ${options.minConfidence || 0.5}\n\n`;
|
|
220
|
+
report += `---\n\n`;
|
|
221
|
+
|
|
222
|
+
// 전체 메트릭
|
|
223
|
+
report += `## 전체 메트릭\n\n`;
|
|
224
|
+
report += `| 메트릭 | 값 |\n`;
|
|
225
|
+
report += `|--------|-----|\n`;
|
|
226
|
+
report += `| **Precision** | ${(metrics.precision * 100).toFixed(2)}% |\n`;
|
|
227
|
+
report += `| **Recall** | ${(metrics.recall * 100).toFixed(2)}% |\n`;
|
|
228
|
+
report += `| **F1-Score** | ${(metrics.f1Score * 100).toFixed(2)}% |\n`;
|
|
229
|
+
report += `| **신뢰도 준수율** | ${(metrics.confidenceComplianceRate * 100).toFixed(2)}% |\n`;
|
|
230
|
+
report += `| **예상 관계 수** | ${metrics.totalExpected} |\n`;
|
|
231
|
+
report += `| **추출된 관계 수** | ${metrics.totalExtracted} |\n`;
|
|
232
|
+
report += `| **True Positives** | ${metrics.truePositives} |\n`;
|
|
233
|
+
report += `| **False Positives** | ${metrics.falsePositives} |\n`;
|
|
234
|
+
report += `| **False Negatives** | ${metrics.falseNegatives} |\n\n`;
|
|
235
|
+
|
|
236
|
+
// 관계 유형별 메트릭
|
|
237
|
+
report += `## 관계 유형별 메트릭\n\n`;
|
|
238
|
+
report += `| 관계 유형 | Precision | Recall | F1-Score | TP | FP | FN |\n`;
|
|
239
|
+
report += `|----------|-----------|--------|----------|----|----|----|\n`;
|
|
240
|
+
|
|
241
|
+
const relationTypes = ['CAUSES', 'DEPENDS_ON', 'FOLLOWS', 'CONTRASTS_WITH', 'REFERENCES', 'BELONGS_TO'] as const;
|
|
242
|
+
for (const type of relationTypes) {
|
|
243
|
+
const typeMetrics = metrics.typeMetrics[type];
|
|
244
|
+
if (typeMetrics) {
|
|
245
|
+
report += `| **${type}** | ${(typeMetrics.precision * 100).toFixed(2)}% | ${(typeMetrics.recall * 100).toFixed(2)}% | ${(typeMetrics.f1Score * 100).toFixed(2)}% | ${typeMetrics.truePositives} | ${typeMetrics.falsePositives} | ${typeMetrics.falseNegatives} |\n`;
|
|
246
|
+
} else {
|
|
247
|
+
report += `| **${type}** | - | - | - | - | - | - |\n`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
report += `\n`;
|
|
251
|
+
|
|
252
|
+
// 임계값 검증
|
|
253
|
+
const thresholds = {
|
|
254
|
+
precision: 0.70,
|
|
255
|
+
recall: 0.65,
|
|
256
|
+
f1Score: 0.68
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
report += `## 임계값 검증\n\n`;
|
|
260
|
+
report += `| 메트릭 | 임계값 | 실제 값 | 상태 |\n`;
|
|
261
|
+
report += `|--------|--------|---------|------|\n`;
|
|
262
|
+
report += `| **Precision** | ${(thresholds.precision * 100).toFixed(2)}% | ${(metrics.precision * 100).toFixed(2)}% | ${metrics.precision >= thresholds.precision ? '✅ 통과' : '❌ 실패'} |\n`;
|
|
263
|
+
report += `| **Recall** | ${(thresholds.recall * 100).toFixed(2)}% | ${(metrics.recall * 100).toFixed(2)}% | ${metrics.recall >= thresholds.recall ? '✅ 통과' : '❌ 실패'} |\n`;
|
|
264
|
+
report += `| **F1-Score** | ${(thresholds.f1Score * 100).toFixed(2)}% | ${(metrics.f1Score * 100).toFixed(2)}% | ${metrics.f1Score >= thresholds.f1Score ? '✅ 통과' : '❌ 실패'} |\n\n`;
|
|
265
|
+
|
|
266
|
+
const failedMetrics: string[] = [];
|
|
267
|
+
if (metrics.precision < thresholds.precision) {
|
|
268
|
+
failedMetrics.push(`Precision: 예상 ${(thresholds.precision * 100).toFixed(2)}%, 실제 ${(metrics.precision * 100).toFixed(2)}%`);
|
|
269
|
+
}
|
|
270
|
+
if (metrics.recall < thresholds.recall) {
|
|
271
|
+
failedMetrics.push(`Recall: 예상 ${(thresholds.recall * 100).toFixed(2)}%, 실제 ${(metrics.recall * 100).toFixed(2)}%`);
|
|
272
|
+
}
|
|
273
|
+
if (metrics.f1Score < thresholds.f1Score) {
|
|
274
|
+
failedMetrics.push(`F1-Score: 예상 ${(thresholds.f1Score * 100).toFixed(2)}%, 실제 ${(metrics.f1Score * 100).toFixed(2)}%`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (failedMetrics.length > 0) {
|
|
278
|
+
report += `### ⚠️ 실패한 메트릭\n\n`;
|
|
279
|
+
for (const failure of failedMetrics) {
|
|
280
|
+
report += `- **${failure}**\n`;
|
|
281
|
+
}
|
|
282
|
+
report += `\n`;
|
|
283
|
+
} else {
|
|
284
|
+
report += `### ✅ 모든 임계값 통과\n\n`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
report += `---\n\n`;
|
|
288
|
+
report += `*이 리포트는 주간 자동 검증 스크립트에 의해 생성되었습니다.*\n`;
|
|
289
|
+
|
|
290
|
+
return report;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 메인 함수
|
|
295
|
+
*/
|
|
296
|
+
async function main() {
|
|
297
|
+
const options = parseArgs();
|
|
298
|
+
|
|
299
|
+
console.log('📊 주간 관계 추출 품질 검증 시작...\n');
|
|
300
|
+
console.log(`옵션:`, options);
|
|
301
|
+
|
|
302
|
+
// Given: 데이터베이스 및 서비스 초기화
|
|
303
|
+
const db = new Database(':memory:');
|
|
304
|
+
createBaseSchema(db);
|
|
305
|
+
|
|
306
|
+
const migration = new RelationEngineSchemaMigration();
|
|
307
|
+
migration.up(db);
|
|
308
|
+
|
|
309
|
+
const relationExtractor = new RelationExtractor();
|
|
310
|
+
const qualityValidator = new RelationQualityValidator();
|
|
311
|
+
|
|
312
|
+
// 테스트 데이터셋 로드
|
|
313
|
+
console.log('📂 테스트 데이터셋 로드 중...');
|
|
314
|
+
const testDataset = loadTestDataset();
|
|
315
|
+
console.log(`✅ ${testDataset.length}건의 테스트 케이스 로드 완료\n`);
|
|
316
|
+
|
|
317
|
+
// 메모리 생성
|
|
318
|
+
console.log('💾 테스트 메모리 생성 중...');
|
|
319
|
+
for (const testCase of testDataset) {
|
|
320
|
+
if (!db.prepare('SELECT id FROM memory_item WHERE id = ?').get(testCase.source_id)) {
|
|
321
|
+
createTestMemory(db, testCase.source_id, testCase.source_content);
|
|
322
|
+
}
|
|
323
|
+
if (!db.prepare('SELECT id FROM memory_item WHERE id = ?').get(testCase.target_id)) {
|
|
324
|
+
createTestMemory(db, testCase.target_id, testCase.target_content);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
console.log('✅ 메모리 생성 완료\n');
|
|
328
|
+
|
|
329
|
+
// 관계 추출
|
|
330
|
+
const method = options.method || 'hybrid';
|
|
331
|
+
const minConfidence = options.minConfidence || 0.5;
|
|
332
|
+
console.log(`🔍 관계 추출 수행 중... (방법: ${method}, 최소 신뢰도: ${minConfidence})`);
|
|
333
|
+
const extractedRelations = await extractRelations(
|
|
334
|
+
testDataset,
|
|
335
|
+
relationExtractor,
|
|
336
|
+
method,
|
|
337
|
+
minConfidence
|
|
338
|
+
);
|
|
339
|
+
console.log(`✅ ${extractedRelations.length}건의 관계 추출 완료\n`);
|
|
340
|
+
|
|
341
|
+
// 품질 메트릭 계산
|
|
342
|
+
console.log('📈 품질 메트릭 계산 중...');
|
|
343
|
+
const metrics = qualityValidator.calculateQualityMetricsWithAnalysis(
|
|
344
|
+
testDataset,
|
|
345
|
+
extractedRelations
|
|
346
|
+
);
|
|
347
|
+
console.log('✅ 메트릭 계산 완료\n');
|
|
348
|
+
|
|
349
|
+
// 리포트 생성
|
|
350
|
+
console.log('📝 리포트 생성 중...');
|
|
351
|
+
const report = generateMarkdownReport(metrics, options, qualityValidator);
|
|
352
|
+
|
|
353
|
+
// 리포트 출력 또는 파일 저장
|
|
354
|
+
const outputPath = options.output || join(process.cwd(), 'data', 'logs', `weekly-relation-validation-${new Date().toISOString().split('T')[0]}.md`);
|
|
355
|
+
|
|
356
|
+
// 디렉토리 생성
|
|
357
|
+
const outputDir = join(outputPath, '..');
|
|
358
|
+
if (!existsSync(outputDir)) {
|
|
359
|
+
mkdirSync(outputDir, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
writeFileSync(outputPath, report, 'utf-8');
|
|
363
|
+
console.log(`✅ 리포트가 ${outputPath}에 저장되었습니다.\n`);
|
|
364
|
+
|
|
365
|
+
// 데이터베이스 정리
|
|
366
|
+
db.close();
|
|
367
|
+
|
|
368
|
+
// 임계값 검증 및 결과 출력
|
|
369
|
+
const thresholds = {
|
|
370
|
+
precision: 0.70,
|
|
371
|
+
recall: 0.65,
|
|
372
|
+
f1Score: 0.68
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const failedMetrics: Array<{ metric: string; expected: number; actual: number }> = [];
|
|
376
|
+
|
|
377
|
+
if (metrics.precision < thresholds.precision) {
|
|
378
|
+
failedMetrics.push({
|
|
379
|
+
metric: 'Precision',
|
|
380
|
+
expected: thresholds.precision,
|
|
381
|
+
actual: metrics.precision
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (metrics.recall < thresholds.recall) {
|
|
385
|
+
failedMetrics.push({
|
|
386
|
+
metric: 'Recall',
|
|
387
|
+
expected: thresholds.recall,
|
|
388
|
+
actual: metrics.recall
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (metrics.f1Score < thresholds.f1Score) {
|
|
392
|
+
failedMetrics.push({
|
|
393
|
+
metric: 'F1-Score',
|
|
394
|
+
expected: thresholds.f1Score,
|
|
395
|
+
actual: metrics.f1Score
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (failedMetrics.length > 0) {
|
|
400
|
+
console.log('\n⚠️ 정확도 임계값 미달:\n');
|
|
401
|
+
for (const failure of failedMetrics) {
|
|
402
|
+
console.log(` - ${failure.metric}: 예상 ${(failure.expected * 100).toFixed(2)}%, 실제 ${(failure.actual * 100).toFixed(2)}%`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (options.allowSoftFail) {
|
|
406
|
+
console.log('\n⚠️ allow_soft_fail=true 옵션으로 인해 경고만 출력합니다.\n');
|
|
407
|
+
process.exit(0);
|
|
408
|
+
} else {
|
|
409
|
+
console.log('\n❌ 검증 실패: 정확도 임계값을 충족하지 못했습니다.\n');
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
console.log('\n✅ 모든 정확도 임계값을 충족했습니다.\n');
|
|
414
|
+
console.log('✨ 주간 검증 완료!');
|
|
415
|
+
process.exit(0);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 스크립트 실행
|
|
420
|
+
main().catch(error => {
|
|
421
|
+
console.error('❌ 오류 발생:', error);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
});
|