rlhf-feedback-loop 0.5.0
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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/adapters/README.md +8 -0
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +20 -0
- package/adapters/chatgpt/INSTALL.md +80 -0
- package/adapters/chatgpt/openapi.yaml +292 -0
- package/adapters/claude/.mcp.json +8 -0
- package/adapters/codex/config.toml +4 -0
- package/adapters/gemini/function-declarations.json +95 -0
- package/adapters/mcp/server-stdio.js +444 -0
- package/bin/cli.js +167 -0
- package/config/mcp-allowlists.json +29 -0
- package/config/policy-bundles/constrained-v1.json +53 -0
- package/config/policy-bundles/default-v1.json +80 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/subagent-profiles.json +32 -0
- package/openapi/openapi.yaml +292 -0
- package/package.json +91 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +31 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +57 -0
- package/plugins/gemini-extension/INSTALL.md +74 -0
- package/plugins/gemini-extension/gemini_prompt.txt +10 -0
- package/plugins/gemini-extension/tool_contract.json +28 -0
- package/scripts/billing.js +471 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/code-reasoning.js +307 -0
- package/scripts/context-engine.js +547 -0
- package/scripts/contextfs.js +513 -0
- package/scripts/contract-audit.js +198 -0
- package/scripts/dpo-optimizer.js +208 -0
- package/scripts/export-dpo-pairs.js +316 -0
- package/scripts/export-training.js +448 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +838 -0
- package/scripts/feedback-schema.js +300 -0
- package/scripts/feedback-to-memory.js +165 -0
- package/scripts/feedback-to-rules.js +109 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/intent-router.js +164 -0
- package/scripts/mcp-policy.js +92 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/prove-adapters.js +364 -0
- package/scripts/prove-attribution.js +364 -0
- package/scripts/prove-automation.js +393 -0
- package/scripts/prove-data-quality.js +219 -0
- package/scripts/prove-intelligence.js +256 -0
- package/scripts/prove-lancedb.js +370 -0
- package/scripts/prove-loop-closure.js +255 -0
- package/scripts/prove-rlaif.js +404 -0
- package/scripts/prove-subway-upgrades.js +250 -0
- package/scripts/prove-training-export.js +324 -0
- package/scripts/prove-v2-milestone.js +273 -0
- package/scripts/prove-v3-milestone.js +381 -0
- package/scripts/rlaif-self-audit.js +123 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/self-heal.js +127 -0
- package/scripts/self-healing-check.js +111 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +29 -0
- package/scripts/thompson-sampling.js +331 -0
- package/scripts/train_from_feedback.py +914 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/vector-store.js +100 -0
- package/src/api/server.js +497 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Training Data Exporter
|
|
4
|
+
*
|
|
5
|
+
* Exports feedback data in multiple formats for ML training pipelines:
|
|
6
|
+
* - PyTorch JSON (XPRT-01): prompt/chosen/rejected preference pairs
|
|
7
|
+
* - CSV summary (XPRT-02): one row per feedback entry with column headers
|
|
8
|
+
* - Action analysis report (XPRT-03): tool call patterns, success rates, top failures
|
|
9
|
+
* - DPO validation gate (XPRT-04): validateMemoryStructure() prevents bad training pairs
|
|
10
|
+
*
|
|
11
|
+
* Ported and adapted from Subway_RN_Demo exportTrainingData() patterns.
|
|
12
|
+
* PATH: PROJECT_ROOT = path.join(__dirname, '..') — 1 level from scripts/
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
21
|
+
const FEEDBACK_DIR = process.env.RLHF_FEEDBACK_DIR
|
|
22
|
+
|| path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
|
|
23
|
+
const SEQUENCE_WINDOW = 10;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Utility helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function readJSONL(filePath) {
|
|
30
|
+
if (!fs.existsSync(filePath)) return [];
|
|
31
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
32
|
+
if (!raw) return [];
|
|
33
|
+
return raw
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map((line) => {
|
|
36
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
37
|
+
})
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureDir(dirPath) {
|
|
42
|
+
if (!fs.existsSync(dirPath)) {
|
|
43
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// XPRT-04: validateMemoryStructure
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Gate function: validates a DPO memory pair has the required fields
|
|
53
|
+
* before allowing it into training export.
|
|
54
|
+
*
|
|
55
|
+
* Required: title, content, category, tags (at least 1 non-generic).
|
|
56
|
+
* For DPO pairs specifically: must have a 'chosen' direction (prompt + chosen + rejected).
|
|
57
|
+
*
|
|
58
|
+
* @param {object} memory - DPO memory entry
|
|
59
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
60
|
+
*/
|
|
61
|
+
function validateMemoryStructure(memory) {
|
|
62
|
+
const issues = [];
|
|
63
|
+
|
|
64
|
+
if (!memory || typeof memory !== 'object') {
|
|
65
|
+
return { valid: false, issues: ['memory must be a non-null object'] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Required fields
|
|
69
|
+
if (!memory.title || typeof memory.title !== 'string') {
|
|
70
|
+
issues.push('title: required string');
|
|
71
|
+
}
|
|
72
|
+
if (!memory.content || typeof memory.content !== 'string') {
|
|
73
|
+
issues.push('content: required string');
|
|
74
|
+
} else if (memory.content.length < 10) {
|
|
75
|
+
issues.push(`content: too short (${memory.content.length} chars, min 10)`);
|
|
76
|
+
}
|
|
77
|
+
if (!memory.category) {
|
|
78
|
+
issues.push('category: required');
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(memory.tags) || memory.tags.length === 0) {
|
|
81
|
+
issues.push('tags: at least 1 tag required');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// DPO-specific: requires 'chosen' field for preference pair export
|
|
85
|
+
// If exporting as DPO pair, must have prompt + chosen + rejected
|
|
86
|
+
if (memory._dpoExport) {
|
|
87
|
+
if (!memory.prompt) issues.push('DPO export: prompt field required');
|
|
88
|
+
if (!memory.chosen) issues.push('DPO export: chosen field required');
|
|
89
|
+
if (!memory.rejected) issues.push('DPO export: rejected field required');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { valid: issues.length === 0, issues };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// XPRT-01: PyTorch JSON export
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build preference pairs from feedback log.
|
|
101
|
+
* Matches positive entries as 'chosen' and negative entries as 'rejected'
|
|
102
|
+
* when they share the same context domain/tags.
|
|
103
|
+
*
|
|
104
|
+
* @param {object[]} feedbackEntries - Raw feedback log entries
|
|
105
|
+
* @returns {object[]} Array of { prompt, chosen, rejected } pairs
|
|
106
|
+
*/
|
|
107
|
+
function buildPreferencePairs(feedbackEntries) {
|
|
108
|
+
const positive = feedbackEntries.filter(
|
|
109
|
+
(f) => f.signal === 'positive' || f.feedback === 'up'
|
|
110
|
+
);
|
|
111
|
+
const negative = feedbackEntries.filter(
|
|
112
|
+
(f) => f.signal === 'negative' || f.feedback === 'down'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const pairs = [];
|
|
116
|
+
const usedNeg = new Set();
|
|
117
|
+
|
|
118
|
+
for (const pos of positive) {
|
|
119
|
+
// Find a negative entry with overlapping tags (domain similarity)
|
|
120
|
+
const posTags = new Set(pos.tags || []);
|
|
121
|
+
let bestNegIdx = -1;
|
|
122
|
+
let bestOverlap = -1;
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < negative.length; i++) {
|
|
125
|
+
if (usedNeg.has(i)) continue;
|
|
126
|
+
const negTags = new Set(negative[i].tags || []);
|
|
127
|
+
let overlap = 0;
|
|
128
|
+
for (const t of posTags) {
|
|
129
|
+
if (negTags.has(t)) overlap++;
|
|
130
|
+
}
|
|
131
|
+
if (overlap > bestOverlap) {
|
|
132
|
+
bestOverlap = overlap;
|
|
133
|
+
bestNegIdx = i;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If no tag overlap, still pair with any unused negative
|
|
138
|
+
if (bestNegIdx === -1) {
|
|
139
|
+
for (let i = 0; i < negative.length; i++) {
|
|
140
|
+
if (!usedNeg.has(i)) { bestNegIdx = i; break; }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (bestNegIdx >= 0) {
|
|
145
|
+
usedNeg.add(bestNegIdx);
|
|
146
|
+
const neg = negative[bestNegIdx];
|
|
147
|
+
pairs.push({
|
|
148
|
+
prompt: pos.context || pos.richContext?.description || '',
|
|
149
|
+
chosen: (pos.richContext?.outcomeCategory || 'positive') + ': ' + (pos.context || ''),
|
|
150
|
+
rejected: (neg.richContext?.outcomeCategory || 'negative') + ': ' + (neg.context || ''),
|
|
151
|
+
metadata: {
|
|
152
|
+
posId: pos.id,
|
|
153
|
+
negId: neg.id,
|
|
154
|
+
posTags: pos.tags || [],
|
|
155
|
+
negTags: neg.tags || [],
|
|
156
|
+
tagOverlap: bestOverlap,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return pairs;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Export feedback data as PyTorch-compatible JSON.
|
|
167
|
+
*
|
|
168
|
+
* Format: { metadata: { ... }, sequences: [{ X: {...}, y, label }] }
|
|
169
|
+
* Each sequence contains rewardSequence, trend, timeGaps features.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} [feedbackDir] - Override feedback directory
|
|
172
|
+
* @param {string} [outputPath] - Override output path
|
|
173
|
+
* @returns {{ outputPath: string, pairCount: number, sequenceCount: number }}
|
|
174
|
+
*/
|
|
175
|
+
function exportPyTorchJSON(feedbackDir, outputPath) {
|
|
176
|
+
const fbDir = feedbackDir || FEEDBACK_DIR;
|
|
177
|
+
const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
|
|
178
|
+
const sequencePath = path.join(fbDir, 'feedback-sequences.jsonl');
|
|
179
|
+
const exportDir = path.join(fbDir, 'training-data');
|
|
180
|
+
|
|
181
|
+
ensureDir(exportDir);
|
|
182
|
+
|
|
183
|
+
const feedbackEntries = readJSONL(feedbackPath);
|
|
184
|
+
const sequences = readJSONL(sequencePath);
|
|
185
|
+
const pairs = buildPreferencePairs(feedbackEntries);
|
|
186
|
+
|
|
187
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
188
|
+
const outPath = outputPath || path.join(exportDir, `training-pytorch-${timestamp}.json`);
|
|
189
|
+
|
|
190
|
+
const pytorchData = {
|
|
191
|
+
metadata: {
|
|
192
|
+
exportDate: new Date().toISOString(),
|
|
193
|
+
format: 'pytorch-dpo',
|
|
194
|
+
pairCount: pairs.length,
|
|
195
|
+
sequenceCount: sequences.length,
|
|
196
|
+
windowSize: SEQUENCE_WINDOW,
|
|
197
|
+
features: ['rewardSequence', 'recentTrend', 'timeGaps'],
|
|
198
|
+
},
|
|
199
|
+
// DPO preference pairs (prompt/chosen/rejected)
|
|
200
|
+
pairs: pairs.map((p) => ({
|
|
201
|
+
prompt: p.prompt,
|
|
202
|
+
chosen: p.chosen,
|
|
203
|
+
rejected: p.rejected,
|
|
204
|
+
})),
|
|
205
|
+
// Raw sequences for LSTM/Transformer training
|
|
206
|
+
sequences: sequences.map((s) => ({
|
|
207
|
+
X: {
|
|
208
|
+
rewardSequence: (s.features && s.features.rewardSequence) || [],
|
|
209
|
+
trend: (s.features && s.features.recentTrend) || 0,
|
|
210
|
+
timeGaps: (s.features && s.features.timeGaps) || [],
|
|
211
|
+
},
|
|
212
|
+
y: s.targetReward,
|
|
213
|
+
label: s.label,
|
|
214
|
+
})),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
fs.writeFileSync(outPath, JSON.stringify(pytorchData, null, 2));
|
|
218
|
+
return { outputPath: outPath, pairCount: pairs.length, sequenceCount: sequences.length };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// XPRT-02: CSV summary export
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Escape a CSV field value (wrap in quotes if contains comma/quote/newline).
|
|
227
|
+
*
|
|
228
|
+
* @param {*} value
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
function escapeCsvField(value) {
|
|
232
|
+
const str = String(value == null ? '' : value);
|
|
233
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
234
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
235
|
+
}
|
|
236
|
+
return str;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Export feedback entries as CSV with standard headers.
|
|
241
|
+
*
|
|
242
|
+
* Columns: id, timestamp, signal, reward, context, domain, tags, outcomeCategory
|
|
243
|
+
*
|
|
244
|
+
* @param {string} [feedbackDir] - Override feedback directory
|
|
245
|
+
* @param {string} [outputPath] - Override output path
|
|
246
|
+
* @returns {{ outputPath: string, rowCount: number }}
|
|
247
|
+
*/
|
|
248
|
+
function exportCSV(feedbackDir, outputPath) {
|
|
249
|
+
const fbDir = feedbackDir || FEEDBACK_DIR;
|
|
250
|
+
const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
|
|
251
|
+
const exportDir = path.join(fbDir, 'training-data');
|
|
252
|
+
|
|
253
|
+
ensureDir(exportDir);
|
|
254
|
+
|
|
255
|
+
const entries = readJSONL(feedbackPath);
|
|
256
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
257
|
+
const outPath = outputPath || path.join(exportDir, `training-summary-${timestamp}.csv`);
|
|
258
|
+
|
|
259
|
+
const headers = ['id', 'timestamp', 'signal', 'reward', 'context', 'domain', 'tags', 'outcomeCategory'];
|
|
260
|
+
const rows = entries.map((e) => [
|
|
261
|
+
e.id || '',
|
|
262
|
+
e.timestamp || '',
|
|
263
|
+
e.signal || e.feedback || '',
|
|
264
|
+
e.reward != null ? e.reward : '',
|
|
265
|
+
e.context || '',
|
|
266
|
+
(e.richContext && e.richContext.domain) || '',
|
|
267
|
+
Array.isArray(e.tags) ? e.tags.join(';') : '',
|
|
268
|
+
(e.richContext && e.richContext.outcomeCategory) || '',
|
|
269
|
+
].map(escapeCsvField).join(','));
|
|
270
|
+
|
|
271
|
+
const csv = [headers.join(','), ...rows].join('\n');
|
|
272
|
+
fs.writeFileSync(outPath, csv);
|
|
273
|
+
|
|
274
|
+
return { outputPath: outPath, rowCount: entries.length };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// XPRT-03: Action analysis report
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Analyze action patterns from sequences and feedback entries.
|
|
283
|
+
*
|
|
284
|
+
* Produces: tool call frequency, success rates, top failure modes.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} [feedbackDir] - Override feedback directory
|
|
287
|
+
* @param {string} [outputPath] - Override output path
|
|
288
|
+
* @returns {{ outputPath: string, report: object }}
|
|
289
|
+
*/
|
|
290
|
+
function exportActionAnalysis(feedbackDir, outputPath) {
|
|
291
|
+
const fbDir = feedbackDir || FEEDBACK_DIR;
|
|
292
|
+
const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
|
|
293
|
+
const sequencePath = path.join(fbDir, 'feedback-sequences.jsonl');
|
|
294
|
+
const exportDir = path.join(fbDir, 'training-data');
|
|
295
|
+
|
|
296
|
+
ensureDir(exportDir);
|
|
297
|
+
|
|
298
|
+
const entries = readJSONL(feedbackPath);
|
|
299
|
+
const sequences = readJSONL(sequencePath);
|
|
300
|
+
|
|
301
|
+
// Aggregate action patterns from sequences
|
|
302
|
+
const allPatterns = {};
|
|
303
|
+
for (const s of sequences) {
|
|
304
|
+
const patterns = (s.features && s.features.actionPatterns) || {};
|
|
305
|
+
for (const [tag, counts] of Object.entries(patterns)) {
|
|
306
|
+
if (!allPatterns[tag]) {
|
|
307
|
+
allPatterns[tag] = { positive: 0, negative: 0, total: 0 };
|
|
308
|
+
}
|
|
309
|
+
allPatterns[tag].positive += counts.positive || 0;
|
|
310
|
+
allPatterns[tag].negative += counts.negative || 0;
|
|
311
|
+
allPatterns[tag].total += (counts.positive || 0) + (counts.negative || 0);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Compute success rates
|
|
316
|
+
for (const data of Object.values(allPatterns)) {
|
|
317
|
+
data.successRate = data.total > 0
|
|
318
|
+
? +(data.positive / data.total).toFixed(4)
|
|
319
|
+
: null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Sort by total occurrences
|
|
323
|
+
const sortedPatterns = Object.entries(allPatterns)
|
|
324
|
+
.sort((a, b) => b[1].total - a[1].total)
|
|
325
|
+
.reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {});
|
|
326
|
+
|
|
327
|
+
// Identify top failure modes (successRate < 0.4 with enough data)
|
|
328
|
+
const topFailureModes = Object.entries(allPatterns)
|
|
329
|
+
.filter(([, v]) => v.successRate !== null && v.successRate < 0.4 && v.total >= 2)
|
|
330
|
+
.sort((a, b) => a[1].successRate - b[1].successRate)
|
|
331
|
+
.slice(0, 5)
|
|
332
|
+
.map(([tag, v]) => ({
|
|
333
|
+
action: tag,
|
|
334
|
+
successRate: v.successRate,
|
|
335
|
+
total: v.total,
|
|
336
|
+
failureCount: v.negative,
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
// Summary stats from feedback log
|
|
340
|
+
const posCount = entries.filter((e) => e.signal === 'positive' || e.feedback === 'up').length;
|
|
341
|
+
const negCount = entries.filter((e) => e.signal === 'negative' || e.feedback === 'down').length;
|
|
342
|
+
|
|
343
|
+
const report = {
|
|
344
|
+
generatedAt: new Date().toISOString(),
|
|
345
|
+
summary: {
|
|
346
|
+
totalFeedbackEntries: entries.length,
|
|
347
|
+
positiveEntries: posCount,
|
|
348
|
+
negativeEntries: negCount,
|
|
349
|
+
totalSequences: sequences.length,
|
|
350
|
+
uniqueActions: Object.keys(sortedPatterns).length,
|
|
351
|
+
},
|
|
352
|
+
actionPatterns: sortedPatterns,
|
|
353
|
+
topFailureModes,
|
|
354
|
+
recommendations: generateActionRecommendations(sortedPatterns),
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
358
|
+
const outPath = outputPath || path.join(exportDir, `action-analysis-${timestamp}.json`);
|
|
359
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
360
|
+
|
|
361
|
+
return { outputPath: outPath, report };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Generate recommendations from action pattern data.
|
|
366
|
+
*
|
|
367
|
+
* @param {object} patterns
|
|
368
|
+
* @returns {string[]}
|
|
369
|
+
*/
|
|
370
|
+
function generateActionRecommendations(patterns) {
|
|
371
|
+
const recs = [];
|
|
372
|
+
for (const [tag, data] of Object.entries(patterns)) {
|
|
373
|
+
if (data.successRate !== null && data.total >= 3) {
|
|
374
|
+
if (data.successRate < 0.5) {
|
|
375
|
+
recs.push(`Avoid "${tag}" — ${(data.successRate * 100).toFixed(0)}% success rate across ${data.total} uses.`);
|
|
376
|
+
} else if (data.successRate > 0.8) {
|
|
377
|
+
recs.push(`Expand "${tag}" — ${(data.successRate * 100).toFixed(0)}% success rate across ${data.total} uses.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (recs.length === 0) recs.push('No actionable recommendations at this time.');
|
|
382
|
+
return recs;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// CLI
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
function printUsage() {
|
|
390
|
+
console.log(`
|
|
391
|
+
Training Data Exporter — Phase 10 (XPRT-01..04)
|
|
392
|
+
|
|
393
|
+
Usage:
|
|
394
|
+
node export-training.js --pytorch [--output <path>]
|
|
395
|
+
node export-training.js --csv [--output <path>]
|
|
396
|
+
node export-training.js --actions [--output <path>]
|
|
397
|
+
node export-training.js --all
|
|
398
|
+
|
|
399
|
+
Options:
|
|
400
|
+
--pytorch Export PyTorch JSON format (XPRT-01)
|
|
401
|
+
--csv Export CSV summary (XPRT-02)
|
|
402
|
+
--actions Export action analysis report (XPRT-03)
|
|
403
|
+
--all Export all formats
|
|
404
|
+
--output Override output file path
|
|
405
|
+
--feedback-dir Override feedback directory
|
|
406
|
+
`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (require.main === module) {
|
|
410
|
+
const args = process.argv.slice(2).reduce((acc, arg) => {
|
|
411
|
+
if (arg.startsWith('--')) {
|
|
412
|
+
const [k, ...v] = arg.slice(2).split('=');
|
|
413
|
+
acc[k] = v.length ? v.join('=') : true;
|
|
414
|
+
}
|
|
415
|
+
return acc;
|
|
416
|
+
}, {});
|
|
417
|
+
|
|
418
|
+
if (args.help || Object.keys(args).length === 0) {
|
|
419
|
+
printUsage();
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const fbDir = args['feedback-dir'] || undefined;
|
|
424
|
+
const outPath = args.output || undefined;
|
|
425
|
+
|
|
426
|
+
if (args.pytorch || args.all) {
|
|
427
|
+
const { outputPath, pairCount, sequenceCount } = exportPyTorchJSON(fbDir, outPath);
|
|
428
|
+
console.log(`PyTorch JSON: ${outputPath} (${pairCount} pairs, ${sequenceCount} sequences)`);
|
|
429
|
+
}
|
|
430
|
+
if (args.csv || args.all) {
|
|
431
|
+
const { outputPath, rowCount } = exportCSV(fbDir, outPath);
|
|
432
|
+
console.log(`CSV: ${outputPath} (${rowCount} rows)`);
|
|
433
|
+
}
|
|
434
|
+
if (args.actions || args.all) {
|
|
435
|
+
const { outputPath } = exportActionAnalysis(fbDir, outPath);
|
|
436
|
+
console.log(`Action Analysis: ${outputPath}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = {
|
|
441
|
+
exportPyTorchJSON,
|
|
442
|
+
exportCSV,
|
|
443
|
+
exportActionAnalysis,
|
|
444
|
+
buildPreferencePairs,
|
|
445
|
+
validateMemoryStructure,
|
|
446
|
+
escapeCsvField,
|
|
447
|
+
generateActionRecommendations,
|
|
448
|
+
};
|