guanghulab/scripts/grid-db/extract-training-samples.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

220 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();