htaf commited on
Commit
8551660
·
1 Parent(s): 90760e2

updating generator

Browse files
prompts/generator_prompt.txt CHANGED
@@ -1,6 +1,31 @@
1
- You are generating clean, spiritually grounded Q’uo-style answers.
2
 
3
- Question:
4
- {{QUESTION}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- Respond in the tone of the Confederation, clear, humble, and service-oriented.
 
 
 
 
 
 
 
 
 
 
 
 
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
- * runGenerator
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
- export async function runGenerator(query, contextChunks, provider) {
26
- if (!provider || typeof provider.generate !== 'function') {
27
- throw new Error("runGenerator: provider.generate() not found");
28
- }
29
 
30
- const template = await loadPromptTemplate();
31
 
32
- // Build context string
33
- const contextText = contextChunks
34
- .map((c, i) => `Context ${i + 1}:\n${c.content}\n`)
35
- .join('\n');
36
 
37
- // Fill prompt template
38
- const prompt = template
39
- .replace('{{QUERY}}', query)
40
- .replace('{{CONTEXT}}', contextText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- // Call LLM
43
  const raw = await provider.generate(prompt);
44
 
45
- // Try parsing JSON (verifier handles correctness later)
46
- let parsed;
47
- try {
48
- parsed = JSON.parse(raw);
49
- } catch (err) {
50
- parsed = { error: 'invalid_json', raw };
 
 
 
 
 
 
51
  }
52
 
53
- // Return normalized object
54
  return {
55
- query,
56
- context: contextChunks,
57
  raw,
58
- parsed
 
 
 
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
- * Orchestrates:
13
  * retrieval → generator → verifier → reward
14
  *
15
- * Returns a structured result:
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
- // --- Retrieval ---
 
 
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
- // --- Generator ---
 
 
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] raw:');
81
- log(' ' + preview(gen.raw ?? '', 400).replace(/\n/g, '\n '));
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
- // --- Verifier ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  let ver;
98
  try {
99
  if (verbose) log(' [verifier] calling model…');
100
- // NOTE: runVerifier(sample, provider)
101
- ver = await runVerifier(gen, verProv);
102
  if (verbose) {
103
- log(' [verifier] parsed:');
104
- log(' ' + preview(ver.parsed, 400).replace(/\n/g, '\n '));
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
- // --- Reward ---
 
 
131
  let rew;
132
  try {
133
  if (verbose) log(' [reward] calling model…');
134
- // NOTE: runReward(sample, provider)
135
- rew = await runReward(gen, rewProv);
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 'fs/promises';
3
- import path from 'path';
4
 
5
- async function loadRewardTemplate() {
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
- /**
17
- * runReward
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 template = await loadRewardTemplate();
 
 
33
 
34
- // Provide JSON form of the answer to the reward prompt
35
- const content = JSON.stringify(sample.parsed ?? sample.raw, null, 2);
36
-
37
- const prompt = template.replace('{{SAMPLE}}', content);
38
 
39
  const raw = await provider.generate(prompt);
40
 
41
- // Try to parse JSON produced by reward model
42
- let parsed;
43
- try {
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
- parsed,
60
- sample
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 'fs/promises';
3
- import path from 'path';
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
- 'prompts',
12
- 'verifier_prompt.txt'
13
  );
14
- return await fs.readFile(filePath, 'utf8');
15
  }
16
 
17
- /**
18
- * runVerifier
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 template = await loadTemplate();
 
 
34
 
35
- // Fill template: give model the generator's JSON
36
- const prompt = template.replace('{{SAMPLE}}', JSON.stringify(sample.parsed ?? sample.raw, null, 2));
 
 
37
 
38
  const raw = await provider.generate(prompt);
39
 
40
- // Try parsing JSON produced by the verifier
41
- let parsed;
42
- try {
43
- parsed = JSON.parse(raw);
44
- } catch {
45
- parsed = { error: 'invalid_json', raw };
46
- }
47
 
48
- // Determine “ok” status:
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 parses JSON output correctly', async () => {
8
- // Fake context from retrieval
9
  const fakeContext = [
10
- { content: "First context chunk" },
11
- { content: "Second context chunk" }
12
  ];
13
 
14
- // Fake provider
15
  const provider = {
16
- generate: vi.fn(async () => JSON.stringify({
17
- answer: "Because love is all that there is",
18
- reasoning: "Unity and service arise from one source."
19
- }))
 
 
 
20
  };
21
 
22
- const result = await runGenerator("What is love?", fakeContext, provider);
23
 
24
  expect(provider.generate).toHaveBeenCalledOnce();
25
- expect(result.parsed.answer).toBeDefined();
26
- expect(result.parsed.reasoning).toBeDefined();
27
- expect(result.query).toBe("What is love?");
28
- expect(result.context.length).toBe(2);
29
  });
30
 
31
- it('captures invalid JSON when model output is malformed', async () => {
32
- const fakeContext = [{ content: "chunk" }];
33
 
34
  const provider = {
35
- generate: vi.fn(async () => "{ this is not json ")
36
  };
37
 
38
- const result = await runGenerator("broken?", fakeContext, provider);
39
 
40
- expect(result.parsed.error).toBe("invalid_json");
41
- expect(result.raw).toContain("not json");
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
- // pretend generator returns a JSON string
14
- return JSON.stringify({ answer: 'mocked', stage: 'generator' });
15
  }
16
  if (stage === 'verifier') {
17
- // verifier returns { ok: true }
18
- return JSON.stringify({ ok: true });
19
  }
20
  if (stage === 'reward') {
21
- // reward returns a positive score
22
- return JSON.stringify({ score: 1 });
23
  }
24
- // fallback
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({ question: 'What is mock testing?' });
 
 
 
 
51
 
52
  expect(result.status).toBe('accepted');
53
 
54
  // generator output made it through
55
- expect(result.gen.parsed.answer).toBe('mocked');
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
- it('parses reward score from valid JSON', async () => {
8
- const fakeSample = {
9
- parsed: { answer: "X", reasoning: "Y" }
 
10
  };
11
 
12
  const provider = {
13
- generate: vi.fn(async () => JSON.stringify({
14
- score: 8.7,
15
- notes: "High quality explanation"
16
- }))
17
  };
18
 
19
- const result = await runReward(fakeSample, provider);
 
 
 
 
 
 
 
20
 
21
- expect(result.score).toBe(8.7);
22
  expect(result.ok).toBe(true);
23
  });
24
 
25
- it('handles invalid JSON safely', async () => {
26
- const fakeSample = { parsed: { answer: "A" } };
 
 
 
 
27
 
28
  const provider = {
29
- generate: vi.fn(async () => "{ broken json")
 
30
  };
31
 
32
- const result = await runReward(fakeSample, provider);
 
 
 
 
 
 
 
33
 
34
- expect(result.parsed.error).toBe("invalid_json");
35
  expect(result.ok).toBe(false);
36
- expect(result.score).toBe(-1);
37
  });
38
 
39
- it('treats missing score as failure', async () => {
40
- const fakeSample = { parsed: {} };
 
 
 
 
41
 
42
- const provider = {
43
- generate: vi.fn(async () => JSON.stringify({
44
- verdict: "pass-but-no-score"
45
- }))
46
  };
47
 
48
- const result = await runReward(fakeSample, provider);
 
 
49
 
50
- expect(result.ok).toBe(false);
51
- expect(result.score).toBe(-1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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('parses verifier JSON and returns ok=true on pass', async () => {
7
- const fakeSample = {
8
- query: "What is love?",
9
- parsed: {
10
- answer: "Love is unity.",
11
- reasoning: "We are all one."
12
- }
13
  };
14
 
15
  const provider = {
16
- generate: vi.fn(async () => JSON.stringify({
17
- verdict: "pass",
18
- notes: "Tone correct."
19
- }))
20
  };
21
 
22
- const result = await runVerifier(fakeSample, provider);
 
 
 
 
 
 
 
23
 
 
24
  expect(result.ok).toBe(true);
25
- expect(result.parsed.verdict).toBe("pass");
26
  });
27
 
28
- it('captures invalid JSON from verifier', async () => {
29
- const fakeSample = { parsed: { answer: "x" } };
 
 
 
 
30
 
31
  const provider = {
32
- generate: vi.fn(async () => "{ not JSON")
33
  };
34
 
35
- const result = await runVerifier(fakeSample, provider);
 
 
 
 
 
 
 
36
 
37
- expect(result.parsed.error).toBe("invalid_json");
38
  expect(result.ok).toBe(false);
 
39
  });
40
 
41
- it('accepts scoring-based decisions', async () => {
42
- const fakeSample = { parsed: { answer: "ok" } };
 
 
 
 
43
 
44
  const provider = {
45
- generate: vi.fn(async () => JSON.stringify({
46
- score: 8.5,
47
- details: "Solid answer."
48
- }))
49
  };
50
 
51
- const result = await runVerifier(fakeSample, provider);
 
 
 
 
 
 
 
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
  });