updating generator
Browse files- prompts/generator_prompt.txt +29 -4
- src/generator/generator_core.mjs +48 -31
- src/pipeline/step.mjs +43 -31
- src/reward/reward_core.mjs +23 -48
- src/verifier/verifier_core.mjs +20 -52
- tests/generator_core.test.mjs +19 -22
- tests/pipeline.mock.test.mjs +17 -10
- tests/reward_core.test.mjs +62 -25
- tests/verifier_core.test.mjs +47 -26
prompts/generator_prompt.txt
CHANGED
|
@@ -1,6 +1,31 @@
|
|
| 1 |
-
You are
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are part of a knowledge distillation pipeline.
|
| 2 |
|
| 3 |
+
You will be given CONTEXT (excerpts from a corpus) and a QUESTION about that context.
|
| 4 |
+
|
| 5 |
+
Your task:
|
| 6 |
+
|
| 7 |
+
1. First, think step by step INSIDE <think> and </think> tags,
|
| 8 |
+
using ONLY information that can be supported by the CONTEXT.
|
| 9 |
+
2. Then, AFTER </think>, write the final answer plainly, without any tags.
|
| 10 |
+
|
| 11 |
+
Rules:
|
| 12 |
+
|
| 13 |
+
- Use ONLY the CONTEXT for facts and claims.
|
| 14 |
+
- Do NOT use outside knowledge or guess.
|
| 15 |
+
- If the CONTEXT is not sufficient to answer, the final answer must be:
|
| 16 |
+
"I cannot answer this from the provided context."
|
| 17 |
+
- You may quote short phrases from the CONTEXT as evidence, but avoid long copy-paste.
|
| 18 |
+
- Be concise and clear.
|
| 19 |
|
| 20 |
+
Format (follow exactly):
|
| 21 |
+
|
| 22 |
+
<think>
|
| 23 |
+
[your detailed reasoning here, step by step, grounded in the CONTEXT]
|
| 24 |
+
</think>
|
| 25 |
+
[your final answer here]
|
| 26 |
+
|
| 27 |
+
CONTEXT:
|
| 28 |
+
{{CONTEXT}}
|
| 29 |
+
|
| 30 |
+
QUESTION:
|
| 31 |
+
{{QUESTION}}
|
src/generator/generator_core.mjs
CHANGED
|
@@ -15,50 +15,67 @@ async function loadPromptTemplate() {
|
|
| 15 |
}
|
| 16 |
|
| 17 |
/**
|
| 18 |
-
*
|
| 19 |
-
*
|
| 20 |
-
* @param {string} query - the natural language question
|
| 21 |
-
* @param {Array} contextChunks - retrieved context objects from RAG
|
| 22 |
-
* @param {object} provider - { generate(prompt) → string }
|
| 23 |
-
* @returns {object} parsed JSON sample
|
| 24 |
*/
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
throw new Error("runGenerator: provider.generate() not found");
|
| 28 |
-
}
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
// Call LLM
|
| 43 |
const raw = await provider.generate(prompt);
|
| 44 |
|
| 45 |
-
|
| 46 |
-
let
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
-
// Return normalized object
|
| 54 |
return {
|
| 55 |
-
query,
|
| 56 |
-
context: contextChunks,
|
| 57 |
raw,
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
| 59 |
};
|
| 60 |
}
|
| 61 |
|
| 62 |
export default {
|
| 63 |
-
runGenerator
|
| 64 |
};
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
/**
|
| 18 |
+
* Build the full prompt from template, question, and context chunks.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
*/
|
| 20 |
+
async function makeGeneratorPrompt(question, contextChunks) {
|
| 21 |
+
const tmpl = await loadPromptTemplate();
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
let prompt = tmpl.replace(/{{QUESTION}}/g, question);
|
| 24 |
|
| 25 |
+
if (tmpl.includes('{{CONTEXT}}')) {
|
| 26 |
+
const contextText =
|
| 27 |
+
contextChunks?.map((c) => c.content || c.text || '').join('\n\n---\n\n') ??
|
| 28 |
+
'';
|
| 29 |
|
| 30 |
+
prompt = prompt.replace(/{{CONTEXT}}/g, contextText);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return prompt;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* runGenerator
|
| 38 |
+
*
|
| 39 |
+
* @param {string} question - the natural language question
|
| 40 |
+
* @param {Array} contextChunks - retrieved context objects from RAG
|
| 41 |
+
* @param {object} provider - { generate(prompt) → string }
|
| 42 |
+
* @returns {object} generator result:
|
| 43 |
+
* {
|
| 44 |
+
* raw: string, // full model output (think + answer)
|
| 45 |
+
* thought: string|null, // content inside <think>...</think> if present
|
| 46 |
+
* answer: string, // final answer after </think> (or whole raw if no tags)
|
| 47 |
+
* query: string, // original question
|
| 48 |
+
* context: Array // original context chunks
|
| 49 |
+
* }
|
| 50 |
+
*/
|
| 51 |
+
export async function runGenerator(question, contextChunks, provider) {
|
| 52 |
+
const prompt = await makeGeneratorPrompt(question, contextChunks);
|
| 53 |
|
|
|
|
| 54 |
const raw = await provider.generate(prompt);
|
| 55 |
|
| 56 |
+
let thought = null;
|
| 57 |
+
let answer = raw?.trim() ?? '';
|
| 58 |
+
|
| 59 |
+
// Extract <think> ... </think> if the thinking model emits it
|
| 60 |
+
const startTag = '<think>';
|
| 61 |
+
const endTag = '</think>';
|
| 62 |
+
const startIdx = raw.indexOf(startTag);
|
| 63 |
+
const endIdx = raw.indexOf(endTag);
|
| 64 |
+
|
| 65 |
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
| 66 |
+
thought = raw.slice(startIdx + startTag.length, endIdx).trim();
|
| 67 |
+
answer = raw.slice(endIdx + endTag.length).trim();
|
| 68 |
}
|
| 69 |
|
|
|
|
| 70 |
return {
|
|
|
|
|
|
|
| 71 |
raw,
|
| 72 |
+
thought,
|
| 73 |
+
answer,
|
| 74 |
+
query: question,
|
| 75 |
+
context: contextChunks || [],
|
| 76 |
};
|
| 77 |
}
|
| 78 |
|
| 79 |
export default {
|
| 80 |
+
runGenerator,
|
| 81 |
};
|
src/pipeline/step.mjs
CHANGED
|
@@ -9,21 +9,10 @@ import { preview } from './util.mjs';
|
|
| 9 |
/**
|
| 10 |
* Run a single pipeline step for one question.
|
| 11 |
*
|
| 12 |
-
*
|
| 13 |
* retrieval → generator → verifier → reward
|
| 14 |
*
|
| 15 |
-
*
|
| 16 |
-
* {
|
| 17 |
-
* status: 'accepted' | 'invalid_question' | 'retrieval_failed'
|
| 18 |
-
* | 'generator_failed' | 'verifier_rejected'
|
| 19 |
-
* | 'reward_rejected' | 'verifier_error' | 'reward_error',
|
| 20 |
-
* question,
|
| 21 |
-
* context,
|
| 22 |
-
* gen,
|
| 23 |
-
* ver,
|
| 24 |
-
* rew,
|
| 25 |
-
* error? // optional message
|
| 26 |
-
* }
|
| 27 |
*/
|
| 28 |
export async function runPipelineStep({
|
| 29 |
question,
|
|
@@ -38,6 +27,9 @@ export async function runPipelineStep({
|
|
| 38 |
const log = logger?.log?.bind(logger) || console.log;
|
| 39 |
const errLog = logger?.error?.bind(logger) || console.error;
|
| 40 |
|
|
|
|
|
|
|
|
|
|
| 41 |
if (!question || !question.trim()) {
|
| 42 |
if (verbose) log(' [pipeline] empty / invalid question, skipping');
|
| 43 |
return { status: 'invalid_question', question };
|
|
@@ -47,11 +39,14 @@ export async function runPipelineStep({
|
|
| 47 |
const verProv = verifierProvider || loadProviderFor('verifier');
|
| 48 |
const rewProv = rewardProvider || loadProviderFor('reward');
|
| 49 |
|
| 50 |
-
// ---
|
|
|
|
|
|
|
| 51 |
let context = [];
|
| 52 |
try {
|
| 53 |
if (verbose) log(` [retrieval] mode=${retrievalMode} k=${k}`);
|
| 54 |
context = await hybridSearch(question, k);
|
|
|
|
| 55 |
if (verbose) {
|
| 56 |
log(` [retrieval] got ${context.length} chunks`);
|
| 57 |
if (context.length > 0) {
|
|
@@ -70,17 +65,17 @@ export async function runPipelineStep({
|
|
| 70 |
};
|
| 71 |
}
|
| 72 |
|
| 73 |
-
// ---
|
|
|
|
|
|
|
| 74 |
let gen;
|
| 75 |
try {
|
| 76 |
if (verbose) log(' [generator] calling model…');
|
| 77 |
-
// NOTE: runGenerator(query, contextChunks, provider)
|
| 78 |
gen = await runGenerator(question, context, genProv);
|
|
|
|
| 79 |
if (verbose) {
|
| 80 |
-
log(' [generator]
|
| 81 |
-
log(' ' + preview(gen.
|
| 82 |
-
log(' [generator] parsed:');
|
| 83 |
-
log(' ' + preview(gen.parsed, 400).replace(/\n/g, '\n '));
|
| 84 |
}
|
| 85 |
} catch (e) {
|
| 86 |
const msg = e?.message || String(e);
|
|
@@ -93,16 +88,29 @@ export async function runPipelineStep({
|
|
| 93 |
};
|
| 94 |
}
|
| 95 |
|
| 96 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
let ver;
|
| 98 |
try {
|
| 99 |
if (verbose) log(' [verifier] calling model…');
|
| 100 |
-
|
| 101 |
-
|
| 102 |
if (verbose) {
|
| 103 |
-
log(' [verifier]
|
| 104 |
-
log(' ' + preview(ver.
|
| 105 |
-
log(` [verifier] ok=${ver.ok === true}`);
|
| 106 |
}
|
| 107 |
} catch (e) {
|
| 108 |
const msg = e?.message || String(e);
|
|
@@ -127,16 +135,17 @@ export async function runPipelineStep({
|
|
| 127 |
};
|
| 128 |
}
|
| 129 |
|
| 130 |
-
// ---
|
|
|
|
|
|
|
| 131 |
let rew;
|
| 132 |
try {
|
| 133 |
if (verbose) log(' [reward] calling model…');
|
| 134 |
-
|
| 135 |
-
|
| 136 |
if (verbose) {
|
| 137 |
-
log(' [reward] parsed:');
|
| 138 |
-
log(' ' + preview(rew.parsed, 400).replace(/\n/g, '\n '));
|
| 139 |
log(` [reward] score=${rew.score} ok=${rew.ok}`);
|
|
|
|
| 140 |
}
|
| 141 |
} catch (e) {
|
| 142 |
const msg = e?.message || String(e);
|
|
@@ -163,6 +172,9 @@ export async function runPipelineStep({
|
|
| 163 |
};
|
| 164 |
}
|
| 165 |
|
|
|
|
|
|
|
|
|
|
| 166 |
if (verbose) log(' [pipeline] accepted ✅');
|
| 167 |
|
| 168 |
return {
|
|
|
|
| 9 |
/**
|
| 10 |
* Run a single pipeline step for one question.
|
| 11 |
*
|
| 12 |
+
* Flow:
|
| 13 |
* retrieval → generator → verifier → reward
|
| 14 |
*
|
| 15 |
+
* No JSON formats required — all models return free-text strings.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
*/
|
| 17 |
export async function runPipelineStep({
|
| 18 |
question,
|
|
|
|
| 27 |
const log = logger?.log?.bind(logger) || console.log;
|
| 28 |
const errLog = logger?.error?.bind(logger) || console.error;
|
| 29 |
|
| 30 |
+
// ----------------------------------------
|
| 31 |
+
// Validate input question
|
| 32 |
+
// ----------------------------------------
|
| 33 |
if (!question || !question.trim()) {
|
| 34 |
if (verbose) log(' [pipeline] empty / invalid question, skipping');
|
| 35 |
return { status: 'invalid_question', question };
|
|
|
|
| 39 |
const verProv = verifierProvider || loadProviderFor('verifier');
|
| 40 |
const rewProv = rewardProvider || loadProviderFor('reward');
|
| 41 |
|
| 42 |
+
// ----------------------------------------
|
| 43 |
+
// Retrieval
|
| 44 |
+
// ----------------------------------------
|
| 45 |
let context = [];
|
| 46 |
try {
|
| 47 |
if (verbose) log(` [retrieval] mode=${retrievalMode} k=${k}`);
|
| 48 |
context = await hybridSearch(question, k);
|
| 49 |
+
|
| 50 |
if (verbose) {
|
| 51 |
log(` [retrieval] got ${context.length} chunks`);
|
| 52 |
if (context.length > 0) {
|
|
|
|
| 65 |
};
|
| 66 |
}
|
| 67 |
|
| 68 |
+
// ----------------------------------------
|
| 69 |
+
// Generator
|
| 70 |
+
// ----------------------------------------
|
| 71 |
let gen;
|
| 72 |
try {
|
| 73 |
if (verbose) log(' [generator] calling model…');
|
|
|
|
| 74 |
gen = await runGenerator(question, context, genProv);
|
| 75 |
+
|
| 76 |
if (verbose) {
|
| 77 |
+
log(' [generator] answer:');
|
| 78 |
+
log(' ' + preview(gen.answer ?? '', 400).replace(/\n/g, '\n '));
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
} catch (e) {
|
| 81 |
const msg = e?.message || String(e);
|
|
|
|
| 88 |
};
|
| 89 |
}
|
| 90 |
|
| 91 |
+
// Empty answer means generator failed
|
| 92 |
+
if (!gen || !gen.answer || !gen.answer.trim()) {
|
| 93 |
+
if (verbose) log(' [generator] empty answer — generator_failed');
|
| 94 |
+
return {
|
| 95 |
+
status: 'generator_failed',
|
| 96 |
+
question,
|
| 97 |
+
context,
|
| 98 |
+
gen,
|
| 99 |
+
error: 'empty_answer',
|
| 100 |
+
};
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// ----------------------------------------
|
| 104 |
+
// Verifier
|
| 105 |
+
// ----------------------------------------
|
| 106 |
let ver;
|
| 107 |
try {
|
| 108 |
if (verbose) log(' [verifier] calling model…');
|
| 109 |
+
ver = await runVerifier({ question, context, gen }, verProv);
|
| 110 |
+
|
| 111 |
if (verbose) {
|
| 112 |
+
log(' [verifier] ok=' + ver.ok);
|
| 113 |
+
log(' ' + preview(ver.raw ?? '', 200).replace(/\n/g, '\n '));
|
|
|
|
| 114 |
}
|
| 115 |
} catch (e) {
|
| 116 |
const msg = e?.message || String(e);
|
|
|
|
| 135 |
};
|
| 136 |
}
|
| 137 |
|
| 138 |
+
// ----------------------------------------
|
| 139 |
+
// Reward
|
| 140 |
+
// ----------------------------------------
|
| 141 |
let rew;
|
| 142 |
try {
|
| 143 |
if (verbose) log(' [reward] calling model…');
|
| 144 |
+
rew = await runReward({ question, context, gen }, rewProv);
|
| 145 |
+
|
| 146 |
if (verbose) {
|
|
|
|
|
|
|
| 147 |
log(` [reward] score=${rew.score} ok=${rew.ok}`);
|
| 148 |
+
log(' ' + preview(rew.raw ?? '', 200).replace(/\n/g, '\n '));
|
| 149 |
}
|
| 150 |
} catch (e) {
|
| 151 |
const msg = e?.message || String(e);
|
|
|
|
| 172 |
};
|
| 173 |
}
|
| 174 |
|
| 175 |
+
// ----------------------------------------
|
| 176 |
+
// Accepted sample
|
| 177 |
+
// ----------------------------------------
|
| 178 |
if (verbose) log(' [pipeline] accepted ✅');
|
| 179 |
|
| 180 |
return {
|
src/reward/reward_core.mjs
CHANGED
|
@@ -1,66 +1,41 @@
|
|
| 1 |
// src/reward/reward_core.mjs
|
| 2 |
-
import fs from
|
| 3 |
-
import path from
|
| 4 |
|
| 5 |
-
async function
|
| 6 |
const filePath = path.resolve(
|
| 7 |
path.dirname(new URL(import.meta.url).pathname),
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
);
|
| 13 |
-
return await fs.readFile(filePath,
|
| 14 |
}
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
*
|
| 19 |
-
* @param {object} sample - Verified sample (or generator output)
|
| 20 |
-
* @param {object} provider - { generate(prompt) }
|
| 21 |
-
* @returns {object} reward result:
|
| 22 |
-
* { score: number, parsed: {...}, raw, ok: boolean }
|
| 23 |
-
*/
|
| 24 |
-
export async function runReward(sample, provider) {
|
| 25 |
-
if (!sample || typeof sample !== 'object') {
|
| 26 |
-
throw new Error("runReward: sample must be an object");
|
| 27 |
-
}
|
| 28 |
-
if (!provider || typeof provider.generate !== 'function') {
|
| 29 |
-
throw new Error("runReward: provider.generate() missing");
|
| 30 |
-
}
|
| 31 |
|
| 32 |
-
const
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
const raw = await provider.generate(prompt);
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
parsed = JSON.parse(raw);
|
| 45 |
-
} catch {
|
| 46 |
-
parsed = { error: 'invalid_json', raw };
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// Score extraction
|
| 50 |
-
const score = typeof parsed.score === 'number' ? parsed.score : -1;
|
| 51 |
-
|
| 52 |
-
// "ok" means usable in gold dataset
|
| 53 |
-
const ok = score >= 0;
|
| 54 |
|
| 55 |
return {
|
| 56 |
-
score,
|
| 57 |
-
ok,
|
| 58 |
raw,
|
| 59 |
-
|
| 60 |
-
|
| 61 |
};
|
| 62 |
}
|
| 63 |
|
| 64 |
-
export default {
|
| 65 |
-
runReward
|
| 66 |
-
};
|
|
|
|
| 1 |
// src/reward/reward_core.mjs
|
| 2 |
+
import fs from "fs/promises";
|
| 3 |
+
import path from "path";
|
| 4 |
|
| 5 |
+
async function loadTemplate() {
|
| 6 |
const filePath = path.resolve(
|
| 7 |
path.dirname(new URL(import.meta.url).pathname),
|
| 8 |
+
"..",
|
| 9 |
+
"..",
|
| 10 |
+
"prompts",
|
| 11 |
+
"reward_prompt.txt"
|
| 12 |
);
|
| 13 |
+
return await fs.readFile(filePath, "utf8");
|
| 14 |
}
|
| 15 |
|
| 16 |
+
export async function runReward({ question, context, gen }, provider) {
|
| 17 |
+
const tmpl = await loadTemplate();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
const ctxText = context
|
| 20 |
+
.map((c) => c.content || c.text || "")
|
| 21 |
+
.join("\n\n---\n\n");
|
| 22 |
|
| 23 |
+
const prompt = tmpl
|
| 24 |
+
.replace(/{{QUESTION}}/g, question)
|
| 25 |
+
.replace(/{{ANSWER}}/g, gen.answer || gen.raw)
|
| 26 |
+
.replace(/{{CONTEXT}}/g, ctxText);
|
| 27 |
|
| 28 |
const raw = await provider.generate(prompt);
|
| 29 |
|
| 30 |
+
const match = raw.match(/\b(?:0(?:\.\d+)?|1(?:\.0+)?)\b/);
|
| 31 |
+
const parsed = match ? parseFloat(match[0]) : 0;
|
| 32 |
+
const score = Math.max(0, Math.min(parsed, 1));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
return {
|
|
|
|
|
|
|
| 35 |
raw,
|
| 36 |
+
score,
|
| 37 |
+
ok: score >= 0.5,
|
| 38 |
};
|
| 39 |
}
|
| 40 |
|
| 41 |
+
export default { runReward };
|
|
|
|
|
|
src/verifier/verifier_core.mjs
CHANGED
|
@@ -1,68 +1,36 @@
|
|
| 1 |
// src/verifier/verifier_core.mjs
|
| 2 |
-
import fs from
|
| 3 |
-
import path from
|
| 4 |
|
| 5 |
-
// Load verifier prompt template
|
| 6 |
async function loadTemplate() {
|
| 7 |
const filePath = path.resolve(
|
| 8 |
path.dirname(new URL(import.meta.url).pathname),
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
);
|
| 14 |
-
return await fs.readFile(filePath,
|
| 15 |
}
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
*
|
| 20 |
-
* @param {object} sample - generator output object
|
| 21 |
-
* @param {object} provider - { generate(prompt) }
|
| 22 |
-
* @returns {object} verification result:
|
| 23 |
-
* { ok: boolean, raw: string, parsed: object }
|
| 24 |
-
*/
|
| 25 |
-
export async function runVerifier(sample, provider) {
|
| 26 |
-
if (!sample || typeof sample !== 'object') {
|
| 27 |
-
throw new Error("runVerifier: sample must be an object");
|
| 28 |
-
}
|
| 29 |
-
if (!provider || typeof provider.generate !== 'function') {
|
| 30 |
-
throw new Error("runVerifier: provider.generate() missing");
|
| 31 |
-
}
|
| 32 |
|
| 33 |
-
const
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
|
| 38 |
const raw = await provider.generate(prompt);
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
try {
|
| 43 |
-
parsed = JSON.parse(raw);
|
| 44 |
-
} catch {
|
| 45 |
-
parsed = { error: 'invalid_json', raw };
|
| 46 |
-
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
// Allow 3 styles:
|
| 50 |
-
// - { ok: true }
|
| 51 |
-
// - { verdict: "pass" }
|
| 52 |
-
// - { score: 0-10 }
|
| 53 |
-
let ok = false;
|
| 54 |
-
if (parsed.ok === true) ok = true;
|
| 55 |
-
if (parsed.verdict === "pass") ok = true;
|
| 56 |
-
if (typeof parsed.score === "number" && parsed.score > 0) ok = true;
|
| 57 |
-
|
| 58 |
-
return {
|
| 59 |
-
ok,
|
| 60 |
-
raw,
|
| 61 |
-
parsed,
|
| 62 |
-
sample
|
| 63 |
-
};
|
| 64 |
}
|
| 65 |
|
| 66 |
-
export default {
|
| 67 |
-
runVerifier
|
| 68 |
-
};
|
|
|
|
| 1 |
// src/verifier/verifier_core.mjs
|
| 2 |
+
import fs from "fs/promises";
|
| 3 |
+
import path from "path";
|
| 4 |
|
|
|
|
| 5 |
async function loadTemplate() {
|
| 6 |
const filePath = path.resolve(
|
| 7 |
path.dirname(new URL(import.meta.url).pathname),
|
| 8 |
+
"..",
|
| 9 |
+
"..",
|
| 10 |
+
"prompts",
|
| 11 |
+
"verifier_prompt.txt"
|
| 12 |
);
|
| 13 |
+
return await fs.readFile(filePath, "utf8");
|
| 14 |
}
|
| 15 |
|
| 16 |
+
export async function runVerifier({ question, context, gen }, provider) {
|
| 17 |
+
const tmpl = await loadTemplate();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
const ctxText = context
|
| 20 |
+
.map((c) => c.content || c.text || "")
|
| 21 |
+
.join("\n\n---\n\n");
|
| 22 |
|
| 23 |
+
const prompt = tmpl
|
| 24 |
+
.replace(/{{QUESTION}}/g, question)
|
| 25 |
+
.replace(/{{ANSWER}}/g, gen.answer || gen.raw)
|
| 26 |
+
.replace(/{{CONTEXT}}/g, ctxText);
|
| 27 |
|
| 28 |
const raw = await provider.generate(prompt);
|
| 29 |
|
| 30 |
+
const first = raw.split("\n")[0].trim();
|
| 31 |
+
const ok = /^yes\b/i.test(first);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
return { raw, ok };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
+
export default { runVerifier };
|
|
|
|
|
|
tests/generator_core.test.mjs
CHANGED
|
@@ -1,43 +1,40 @@
|
|
| 1 |
import { describe, it, expect, vi } from 'vitest';
|
| 2 |
import { runGenerator } from '../src/generator/generator_core.mjs';
|
| 3 |
-
import fs from 'fs/promises';
|
| 4 |
-
import path from 'path';
|
| 5 |
|
| 6 |
describe('generator_core.mjs', () => {
|
| 7 |
-
it('builds prompt and
|
| 8 |
-
// Fake context from retrieval
|
| 9 |
const fakeContext = [
|
| 10 |
-
{ content:
|
| 11 |
-
{ content:
|
| 12 |
];
|
| 13 |
|
| 14 |
-
// Fake provider
|
| 15 |
const provider = {
|
| 16 |
-
generate: vi.fn(async () =>
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 20 |
};
|
| 21 |
|
| 22 |
-
const result = await runGenerator(
|
| 23 |
|
| 24 |
expect(provider.generate).toHaveBeenCalledOnce();
|
| 25 |
-
expect(result.
|
| 26 |
-
expect(result.
|
| 27 |
-
expect(result.query).toBe("What is love?");
|
| 28 |
-
expect(result.context.length).toBe(2);
|
| 29 |
});
|
| 30 |
|
| 31 |
-
it('
|
| 32 |
-
const fakeContext = [{ content:
|
| 33 |
|
| 34 |
const provider = {
|
| 35 |
-
generate: vi.fn(async () =>
|
| 36 |
};
|
| 37 |
|
| 38 |
-
const result = await runGenerator(
|
| 39 |
|
| 40 |
-
expect(result.
|
| 41 |
-
expect(result.
|
| 42 |
});
|
| 43 |
});
|
|
|
|
| 1 |
import { describe, it, expect, vi } from 'vitest';
|
| 2 |
import { runGenerator } from '../src/generator/generator_core.mjs';
|
|
|
|
|
|
|
| 3 |
|
| 4 |
describe('generator_core.mjs', () => {
|
| 5 |
+
it('builds prompt and returns a plain-text answer', async () => {
|
|
|
|
| 6 |
const fakeContext = [
|
| 7 |
+
{ content: 'First context chunk' },
|
| 8 |
+
{ content: 'Second context chunk' },
|
| 9 |
];
|
| 10 |
|
|
|
|
| 11 |
const provider = {
|
| 12 |
+
generate: vi.fn(async (prompt) => {
|
| 13 |
+
// At minimum, prompt should contain the question
|
| 14 |
+
expect(prompt).toContain('What is love?');
|
| 15 |
+
// We *don’t* assume the template includes the raw context text,
|
| 16 |
+
// since the current generator_prompt.txt may only use {{QUESTION}}.
|
| 17 |
+
return 'Because love is all that there is';
|
| 18 |
+
}),
|
| 19 |
};
|
| 20 |
|
| 21 |
+
const result = await runGenerator('What is love?', fakeContext, provider);
|
| 22 |
|
| 23 |
expect(provider.generate).toHaveBeenCalledOnce();
|
| 24 |
+
expect(result.raw).toBe('Because love is all that there is');
|
| 25 |
+
expect(result.answer).toBe('Because love is all that there is');
|
|
|
|
|
|
|
| 26 |
});
|
| 27 |
|
| 28 |
+
it('handles arbitrary non-JSON output without error', async () => {
|
| 29 |
+
const fakeContext = [{ content: 'chunk' }];
|
| 30 |
|
| 31 |
const provider = {
|
| 32 |
+
generate: vi.fn(async () => 'this is not JSON and that is fine'),
|
| 33 |
};
|
| 34 |
|
| 35 |
+
const result = await runGenerator('broken?', fakeContext, provider);
|
| 36 |
|
| 37 |
+
expect(result.raw).toBe('this is not JSON and that is fine');
|
| 38 |
+
expect(result.answer).toBe('this is not JSON and that is fine');
|
| 39 |
});
|
| 40 |
});
|
tests/pipeline.mock.test.mjs
CHANGED
|
@@ -10,19 +10,18 @@ vi.mock('../src/providers/provider.mjs', () => {
|
|
| 10 |
stage,
|
| 11 |
async generate(prompt) {
|
| 12 |
if (stage === 'generator') {
|
| 13 |
-
//
|
| 14 |
-
return
|
| 15 |
}
|
| 16 |
if (stage === 'verifier') {
|
| 17 |
-
// verifier
|
| 18 |
-
return
|
| 19 |
}
|
| 20 |
if (stage === 'reward') {
|
| 21 |
-
// reward
|
| 22 |
-
return
|
| 23 |
}
|
| 24 |
-
|
| 25 |
-
return JSON.stringify({ ok: true });
|
| 26 |
},
|
| 27 |
};
|
| 28 |
},
|
|
@@ -47,15 +46,23 @@ describe('runPipelineStep (mocked providers)', () => {
|
|
| 47 |
});
|
| 48 |
|
| 49 |
it('runs a full pipeline step successfully', async () => {
|
| 50 |
-
const result = await runPipelineStep({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
expect(result.status).toBe('accepted');
|
| 53 |
|
| 54 |
// generator output made it through
|
| 55 |
-
expect(result.gen.
|
| 56 |
|
| 57 |
// verifier + reward both say OK
|
| 58 |
expect(result.ver.ok).toBe(true);
|
| 59 |
expect(result.rew.ok).toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
});
|
| 61 |
});
|
|
|
|
| 10 |
stage,
|
| 11 |
async generate(prompt) {
|
| 12 |
if (stage === 'generator') {
|
| 13 |
+
// generator returns a plain-text answer
|
| 14 |
+
return 'mocked answer';
|
| 15 |
}
|
| 16 |
if (stage === 'verifier') {
|
| 17 |
+
// verifier first line YES ⇒ ok = true
|
| 18 |
+
return 'YES\nLooks good';
|
| 19 |
}
|
| 20 |
if (stage === 'reward') {
|
| 21 |
+
// reward outputs a numeric score between 0 and 1
|
| 22 |
+
return '0.99\nExcellent sample';
|
| 23 |
}
|
| 24 |
+
return 'YES';
|
|
|
|
| 25 |
},
|
| 26 |
};
|
| 27 |
},
|
|
|
|
| 46 |
});
|
| 47 |
|
| 48 |
it('runs a full pipeline step successfully', async () => {
|
| 49 |
+
const result = await runPipelineStep({
|
| 50 |
+
question: 'What is mock testing?',
|
| 51 |
+
verbose: false,
|
| 52 |
+
logger: console,
|
| 53 |
+
});
|
| 54 |
|
| 55 |
expect(result.status).toBe('accepted');
|
| 56 |
|
| 57 |
// generator output made it through
|
| 58 |
+
expect(result.gen.answer).toBe('mocked answer');
|
| 59 |
|
| 60 |
// verifier + reward both say OK
|
| 61 |
expect(result.ver.ok).toBe(true);
|
| 62 |
expect(result.rew.ok).toBe(true);
|
| 63 |
+
expect(result.rew.score).toBeCloseTo(0.99, 5);
|
| 64 |
+
|
| 65 |
+
// context came from mocked retrieval
|
| 66 |
+
expect(result.context.length).toBe(2);
|
| 67 |
});
|
| 68 |
});
|
tests/reward_core.test.mjs
CHANGED
|
@@ -3,51 +3,88 @@ import { describe, it, expect, vi } from 'vitest';
|
|
| 3 |
import { runReward } from '../src/reward/reward_core.mjs';
|
| 4 |
|
| 5 |
describe('reward_core.mjs', () => {
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
| 10 |
};
|
| 11 |
|
| 12 |
const provider = {
|
| 13 |
-
generate: vi.fn(async () =>
|
| 14 |
-
score: 8.7,
|
| 15 |
-
notes: "High quality explanation"
|
| 16 |
-
}))
|
| 17 |
};
|
| 18 |
|
| 19 |
-
const result = await runReward(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
expect(result.score).
|
| 22 |
expect(result.ok).toBe(true);
|
| 23 |
});
|
| 24 |
|
| 25 |
-
it('handles invalid
|
| 26 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
const provider = {
|
| 29 |
-
|
|
|
|
| 30 |
};
|
| 31 |
|
| 32 |
-
const result = await runReward(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
expect(result.
|
| 35 |
expect(result.ok).toBe(false);
|
| 36 |
-
expect(result.score).toBe(-1);
|
| 37 |
});
|
| 38 |
|
| 39 |
-
it('treats
|
| 40 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
const
|
| 43 |
-
generate: vi.fn(async () =>
|
| 44 |
-
verdict: "pass-but-no-score"
|
| 45 |
-
}))
|
| 46 |
};
|
| 47 |
|
| 48 |
-
const
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
});
|
| 53 |
});
|
|
|
|
| 3 |
import { runReward } from '../src/reward/reward_core.mjs';
|
| 4 |
|
| 5 |
describe('reward_core.mjs', () => {
|
| 6 |
+
it('parses reward score from valid numeric output', async () => {
|
| 7 |
+
const sample = {
|
| 8 |
+
question: 'Q',
|
| 9 |
+
context: [{ content: 'ctx' }],
|
| 10 |
+
gen: { answer: 'A', raw: 'A' },
|
| 11 |
};
|
| 12 |
|
| 13 |
const provider = {
|
| 14 |
+
generate: vi.fn(async () => '0.87\nHigh quality explanation'),
|
|
|
|
|
|
|
|
|
|
| 15 |
};
|
| 16 |
|
| 17 |
+
const result = await runReward(
|
| 18 |
+
{
|
| 19 |
+
question: sample.question,
|
| 20 |
+
context: sample.context,
|
| 21 |
+
gen: sample.gen,
|
| 22 |
+
},
|
| 23 |
+
provider,
|
| 24 |
+
);
|
| 25 |
|
| 26 |
+
expect(result.score).toBeCloseTo(0.87, 5);
|
| 27 |
expect(result.ok).toBe(true);
|
| 28 |
});
|
| 29 |
|
| 30 |
+
it('handles invalid or missing score by falling back to 0 and ok=false', async () => {
|
| 31 |
+
const sample = {
|
| 32 |
+
question: 'Q',
|
| 33 |
+
context: [{ content: 'ctx' }],
|
| 34 |
+
gen: { answer: 'A', raw: 'A' },
|
| 35 |
+
};
|
| 36 |
|
| 37 |
const provider = {
|
| 38 |
+
// no valid 0–1 number present
|
| 39 |
+
generate: vi.fn(async () => 'no numeric score here\ntext'),
|
| 40 |
};
|
| 41 |
|
| 42 |
+
const result = await runReward(
|
| 43 |
+
{
|
| 44 |
+
question: sample.question,
|
| 45 |
+
context: sample.context,
|
| 46 |
+
gen: sample.gen,
|
| 47 |
+
},
|
| 48 |
+
provider,
|
| 49 |
+
);
|
| 50 |
|
| 51 |
+
expect(result.score).toBe(0);
|
| 52 |
expect(result.ok).toBe(false);
|
|
|
|
| 53 |
});
|
| 54 |
|
| 55 |
+
it('treats score >= 0.5 as success and < 0.5 as failure', async () => {
|
| 56 |
+
const sample = {
|
| 57 |
+
question: 'Q',
|
| 58 |
+
context: [{ content: 'ctx' }],
|
| 59 |
+
gen: { answer: 'A', raw: 'A' },
|
| 60 |
+
};
|
| 61 |
|
| 62 |
+
const highProvider = {
|
| 63 |
+
generate: vi.fn(async () => '0.9\nGreat answer'),
|
|
|
|
|
|
|
| 64 |
};
|
| 65 |
|
| 66 |
+
const lowProvider = {
|
| 67 |
+
generate: vi.fn(async () => '0.2\nWeak answer'),
|
| 68 |
+
};
|
| 69 |
|
| 70 |
+
const high = await runReward(
|
| 71 |
+
{
|
| 72 |
+
question: sample.question,
|
| 73 |
+
context: sample.context,
|
| 74 |
+
gen: sample.gen,
|
| 75 |
+
},
|
| 76 |
+
highProvider,
|
| 77 |
+
);
|
| 78 |
+
const low = await runReward(
|
| 79 |
+
{
|
| 80 |
+
question: sample.question,
|
| 81 |
+
context: sample.context,
|
| 82 |
+
gen: sample.gen,
|
| 83 |
+
},
|
| 84 |
+
lowProvider,
|
| 85 |
+
);
|
| 86 |
+
|
| 87 |
+
expect(high.ok).toBe(true);
|
| 88 |
+
expect(low.ok).toBe(false);
|
| 89 |
});
|
| 90 |
});
|
tests/verifier_core.test.mjs
CHANGED
|
@@ -3,54 +3,75 @@ import { describe, it, expect, vi } from 'vitest';
|
|
| 3 |
import { runVerifier } from '../src/verifier/verifier_core.mjs';
|
| 4 |
|
| 5 |
describe('verifier_core.mjs', () => {
|
| 6 |
-
it('
|
| 7 |
-
const
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
reasoning: "We are all one."
|
| 12 |
-
}
|
| 13 |
};
|
| 14 |
|
| 15 |
const provider = {
|
| 16 |
-
generate: vi.fn(async () =>
|
| 17 |
-
verdict: "pass",
|
| 18 |
-
notes: "Tone correct."
|
| 19 |
-
}))
|
| 20 |
};
|
| 21 |
|
| 22 |
-
const result = await runVerifier(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
|
|
|
| 24 |
expect(result.ok).toBe(true);
|
| 25 |
-
expect(result.
|
| 26 |
});
|
| 27 |
|
| 28 |
-
it('
|
| 29 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
const provider = {
|
| 32 |
-
generate: vi.fn(async () =>
|
| 33 |
};
|
| 34 |
|
| 35 |
-
const result = await runVerifier(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
expect(result.parsed.error).toBe("invalid_json");
|
| 38 |
expect(result.ok).toBe(false);
|
|
|
|
| 39 |
});
|
| 40 |
|
| 41 |
-
it('accepts
|
| 42 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
const provider = {
|
| 45 |
-
generate: vi.fn(async () =>
|
| 46 |
-
score: 8.5,
|
| 47 |
-
details: "Solid answer."
|
| 48 |
-
}))
|
| 49 |
};
|
| 50 |
|
| 51 |
-
const result = await runVerifier(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
expect(result.ok).toBe(true);
|
| 54 |
-
expect(result.parsed.score).toBe(8.5);
|
| 55 |
});
|
| 56 |
});
|
|
|
|
| 3 |
import { runVerifier } from '../src/verifier/verifier_core.mjs';
|
| 4 |
|
| 5 |
describe('verifier_core.mjs', () => {
|
| 6 |
+
it('returns ok=true when first line is YES', async () => {
|
| 7 |
+
const sample = {
|
| 8 |
+
question: 'What is love?',
|
| 9 |
+
context: [{ content: 'ctx' }],
|
| 10 |
+
gen: { answer: 'Love is unity.', raw: 'Love is unity.' },
|
|
|
|
|
|
|
| 11 |
};
|
| 12 |
|
| 13 |
const provider = {
|
| 14 |
+
generate: vi.fn(async () => 'YES\nLooks good.'),
|
|
|
|
|
|
|
|
|
|
| 15 |
};
|
| 16 |
|
| 17 |
+
const result = await runVerifier(
|
| 18 |
+
{
|
| 19 |
+
question: sample.question,
|
| 20 |
+
context: sample.context,
|
| 21 |
+
gen: sample.gen,
|
| 22 |
+
},
|
| 23 |
+
provider,
|
| 24 |
+
);
|
| 25 |
|
| 26 |
+
expect(provider.generate).toHaveBeenCalledOnce();
|
| 27 |
expect(result.ok).toBe(true);
|
| 28 |
+
expect(result.raw.startsWith('YES')).toBe(true);
|
| 29 |
});
|
| 30 |
|
| 31 |
+
it('returns ok=false when first line is NO', async () => {
|
| 32 |
+
const sample = {
|
| 33 |
+
question: 'What is love?',
|
| 34 |
+
context: [{ content: 'ctx' }],
|
| 35 |
+
gen: { answer: 'Bad answer', raw: 'Bad answer' },
|
| 36 |
+
};
|
| 37 |
|
| 38 |
const provider = {
|
| 39 |
+
generate: vi.fn(async () => 'NO\nIncorrect interpretation.'),
|
| 40 |
};
|
| 41 |
|
| 42 |
+
const result = await runVerifier(
|
| 43 |
+
{
|
| 44 |
+
question: sample.question,
|
| 45 |
+
context: sample.context,
|
| 46 |
+
gen: sample.gen,
|
| 47 |
+
},
|
| 48 |
+
provider,
|
| 49 |
+
);
|
| 50 |
|
|
|
|
| 51 |
expect(result.ok).toBe(false);
|
| 52 |
+
expect(result.raw.startsWith('NO')).toBe(true);
|
| 53 |
});
|
| 54 |
|
| 55 |
+
it('accepts lowercase/whitespace yes on first line', async () => {
|
| 56 |
+
const sample = {
|
| 57 |
+
question: 'What is love?',
|
| 58 |
+
context: [{ content: 'ctx' }],
|
| 59 |
+
gen: { answer: 'Reasonable answer', raw: 'Reasonable answer' },
|
| 60 |
+
};
|
| 61 |
|
| 62 |
const provider = {
|
| 63 |
+
generate: vi.fn(async () => 'yes, this is fine\nMore details…'),
|
|
|
|
|
|
|
|
|
|
| 64 |
};
|
| 65 |
|
| 66 |
+
const result = await runVerifier(
|
| 67 |
+
{
|
| 68 |
+
question: sample.question,
|
| 69 |
+
context: sample.context,
|
| 70 |
+
gen: sample.gen,
|
| 71 |
+
},
|
| 72 |
+
provider,
|
| 73 |
+
);
|
| 74 |
|
| 75 |
expect(result.ok).toBe(true);
|
|
|
|
| 76 |
});
|
| 77 |
});
|