guanghulab/scripts/grid-db/extract-training-samples.js

220 lines
6.5 KiB
JavaScript
Raw Normal View History

/**
* scripts/grid-db/extract-training-samples.js
*
* 交互记录 训练样本提取脚本
*
* 职责
* - 扫描 grid-db/interactions/ grid-db/training-lake/raw/ 中的 JSONL 文件
* - quality_score 分级提取训练样本
* - quality_score >= 7 curated/高质量 A
* - quality_score 4-6 raw/ 保留B 需复审
* - quality_score < 4 不提取C 低质量/无关闲聊
* - 将合格交互转换为标准训练样本格式
* - session 分组生成多轮对话训练样本
*
* 训练样本格式
* {
* "sample_id": "TS-YYYYMMDD-NNN",
* "source_session": "sess-XXX",
* "source_dev": "DEV-XXX",
* "source_persona": "PER-XXXXXX",
* "sample_type": "coding-guidance",
* "quality_tier": "A|B|C",
* "turns": [...],
* "metadata": { topic_tags, emotion_arc, persona_adaptation, outcome, total_turns, duration_minutes }
* }
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
*/
const fs = require('fs');
const path = require('path');
const GRID_DB = path.join(__dirname, '../../grid-db');
const INTERACTIONS = path.join(GRID_DB, 'interactions');
const TRAINING_RAW = path.join(GRID_DB, 'training-lake/raw');
const TRAINING_CURATED = path.join(GRID_DB, 'training-lake/curated');
const CATALOG_PATH = path.join(GRID_DB, 'training-lake/metadata/catalog.json');
function getDateStr() {
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
}
function parseJsonlFile(filePath) {
if (!fs.existsSync(filePath)) return [];
const content = fs.readFileSync(filePath, 'utf8');
return content.trim().split('\n')
.filter(line => line.trim())
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(Boolean);
}
function groupBySession(records) {
const sessions = {};
for (const record of records) {
const sid = record.session_id || record.source_session || 'unknown';
if (!sessions[sid]) {
sessions[sid] = [];
}
sessions[sid].push(record);
}
return sessions;
}
function assessQuality(turns) {
// Calculate average quality score from turns that have one
const scores = turns
.map(t => (t.metadata && t.metadata.quality_score) || (t.quality_score) || null)
.filter(s => s !== null);
if (scores.length === 0) return 5; // Default to medium if no scores
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
}
function getQualityTier(score) {
if (score >= 7) return 'A';
if (score >= 4) return 'B';
return 'C';
}
function extractEmotionArc(turns) {
return turns
.map(t => (t.metadata && t.metadata.emotion) || t.emotion || null)
.filter(Boolean);
}
function extractTopicTags(turns) {
const tags = new Set();
for (const t of turns) {
if (t.tags) t.tags.forEach(tag => tags.add(tag));
if (t.metadata && t.metadata.topic) tags.add(t.metadata.topic);
}
return [...tags];
}
function generateSampleId(dateStr, counter) {
const timeStr = Date.now().toString(36);
return `TS-${dateStr}-${timeStr}-${String(counter).padStart(3, '0')}`;
}
function main() {
const dateStr = getDateStr();
console.log(`[extract-training-samples] Starting extraction: ${dateStr}`);
// Collect all JSONL files from interactions/
const devDirs = fs.readdirSync(INTERACTIONS)
.filter(d => d.startsWith('DEV-') && fs.statSync(path.join(INTERACTIONS, d)).isDirectory());
let allRecords = [];
for (const devDir of devDirs) {
const devPath = path.join(INTERACTIONS, devDir);
const jsonlFiles = fs.readdirSync(devPath).filter(f => f.endsWith('.jsonl'));
for (const file of jsonlFiles) {
const records = parseJsonlFile(path.join(devPath, file));
allRecords = allRecords.concat(records);
}
}
// Also scan training-lake/raw/ for unprocessed batches
const rawFiles = fs.readdirSync(TRAINING_RAW).filter(f => f.endsWith('.jsonl'));
for (const file of rawFiles) {
const records = parseJsonlFile(path.join(TRAINING_RAW, file));
// These may already be in sample format; check and add raw interaction records
for (const r of records) {
if (r.turns) {
// Already a sample, skip
continue;
}
allRecords.push(r);
}
}
if (allRecords.length === 0) {
console.log('[extract-training-samples] No interaction records found');
return;
}
console.log(`[extract-training-samples] Found ${allRecords.length} total records`);
// Group by session
const sessions = groupBySession(allRecords);
const sessionIds = Object.keys(sessions);
console.log(`[extract-training-samples] Found ${sessionIds.length} sessions`);
let sampleCount = 0;
let curatedCount = 0;
let rawCount = 0;
let skippedCount = 0;
for (const sid of sessionIds) {
const turns = sessions[sid];
if (turns.length < 2) continue; // Need at least 2 turns for a training sample
const qualityScore = assessQuality(turns);
const tier = getQualityTier(qualityScore);
if (tier === 'C') {
skippedCount++;
continue;
}
sampleCount++;
const sampleId = generateSampleId(dateStr, sampleCount);
const devId = turns[0].dev_id || 'unknown';
const personaId = turns[0].persona_id || 'unknown';
const sample = {
schema_version: '1.0',
sample_id: sampleId,
source_session: sid,
source_dev: devId,
source_persona: personaId,
sample_type: 'coding-guidance',
quality_tier: tier,
turns: turns.map(t => ({
role: t.role || 'system',
text: t.content || t.text || '',
timestamp: t.timestamp || t.ts || null,
strategy: t.strategy || null
})),
metadata: {
topic_tags: extractTopicTags(turns),
emotion_arc: extractEmotionArc(turns),
persona_adaptation: null,
outcome: null,
total_turns: turns.length,
duration_minutes: null
}
};
const sampleLine = JSON.stringify(sample);
if (tier === 'A') {
const curatedFile = path.join(TRAINING_CURATED, `${dateStr}-curated.jsonl`);
fs.appendFileSync(curatedFile, sampleLine + '\n');
curatedCount++;
} else {
const rawFile = path.join(TRAINING_RAW, `${dateStr}-extracted.jsonl`);
fs.appendFileSync(rawFile, sampleLine + '\n');
rawCount++;
}
}
console.log(`[extract-training-samples] Extraction complete:`);
console.log(` Total samples: ${sampleCount}`);
console.log(` Curated (A): ${curatedCount}`);
console.log(` Raw (B): ${rawCount}`);
console.log(` Skipped (C): ${skippedCount}`);
}
main();