File size: 4,396 Bytes
9af7af9
 
 
856bafd
fad3187
9af7af9
8551660
 
9af7af9
 
 
8551660
856bafd
8551660
856bafd
 
 
 
fad3187
 
 
 
 
 
 
8551660
9af7af9
 
8551660
9af7af9
 
856bafd
 
fad3187
856bafd
fad3187
 
856bafd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9af7af9
 
856bafd
 
9af7af9
 
856bafd
 
 
 
9af7af9
 
856bafd
 
 
 
 
9af7af9
856bafd
ecd21e2
 
856bafd
9af7af9
fad3187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecd21e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9af7af9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { describe, it, expect, vi } from 'vitest';
import { runGenerator } from '../src/generator/generator_core.mjs';

describe('generator_core.mjs (thinking generator)', () => {
  it('includes question and context in the prompt and parses JSON output', async () => {
    const fakeContext = [
      { content: 'First context chunk' },
      { content: 'Second context chunk' },
    ];

    const provider = {
      generate: vi.fn(async (prompt) => {
        // Prompt should contain the question
        expect(prompt).toContain('What is love?');
        // And the context chunks (we expect {{CONTEXT}} is wired up)
        expect(prompt).toContain('First context chunk');
        expect(prompt).toContain('Second context chunk');

        // Return JSON output
        return JSON.stringify({
          reasoning: ['step A', 'step B'],
          answer: 'Love is the recognition of shared being.',
          confidence: 0.92,
          evidence: ['quote (para #1)'],
        });
      }),
    };

    const result = await runGenerator('What is love?', fakeContext, provider);

    expect(provider.generate).toHaveBeenCalledOnce();
    expect(result.question).toBe('What is love?');
    expect(result.context).toHaveLength(2);
    expect(result.raw).toContain('step A');
    expect(result.answer).toBe('Love is the recognition of shared being.');
    expect(result.thought).toContain('step A');
    expect(result.confidence).toBeCloseTo(0.92);
  });

  it('extracts thought and answer correctly when <think> block is present', async () => {
    const fakeContext = [{ content: 'ctx' }];

    const provider = {
      generate: vi.fn(async () => {
        return `<think>
Step 1: Read the context carefully.
Step 2: Identify the relevant statements.
Step 3: Synthesize an answer.
</think>
The final answer derived from the context.`;
      }),
    };

    const result = await runGenerator(
      'Test question?',
      fakeContext,
      provider,
    );

    expect(result.raw).toContain('<think>');
    expect(result.thought).toContain('Step 1:');
    expect(result.thought).toContain('Step 3:');
    expect(result.answer).toBe('The final answer derived from the context.');
  });

  it('handles output without <think> block gracefully', async () => {
    const fakeContext = [{ content: 'ctx' }];

    const provider = {
      generate: vi.fn(async () => {
        // No <think> tags at all
        return 'Just a direct answer with no visible reasoning.';
      }),
    };

    const result = await runGenerator(
      'Another question?',
      fakeContext,
      provider,
    );

    expect(result.raw).toBe('Just a direct answer with no visible reasoning.');
    // No JSON or think tags means thought falls back to raw
    expect(result.thought).toBe('Just a direct answer with no visible reasoning.');
    expect(result.answer).toBe('Just a direct answer with no visible reasoning.');
  });

  it('parses Qwen answer block and preserves thinking object', async () => {
    const fakeContext = [{ content: 'ctx' }];
    const provider = {
      generate: vi.fn(async () => ({
        response: `<|thought|>step1<|end_of_thought|>\n<|answer|>\nConfidence: High\nAnswer: Supported answer\nEvidence: ["quote1 (loc1)", "quote2 (loc2)"]\nLimitations: None\n<|end_of_answer|>`,
        thinking: { steps: ['t1', 't2'] },
      })),
    };

    const result = await runGenerator('Test?', fakeContext, provider);

    expect(result.thought).toEqual({ steps: ['t1', 't2'] });
    expect(result.answer).toBe('Supported answer');
    expect(result.confidence).toBe('High');
    expect(result.evidence).toEqual(['quote1 (loc1)', 'quote2 (loc2)']);
    expect(result.limitations).toBe('None');
  });

  it('parses legacy reasoning tags without answer block', async () => {
    const fakeContext = [{ content: 'ctx' }];
    const provider = {
      generate: vi.fn(async () =>
        `<understanding>step A</understanding>\n<reasoning_chain>step B</reasoning_chain>\nConfidence: Medium\nAnswer: Legacy answer\nEvidence: ["ev1 (loc)"]\nLimitations: None`
      ),
    };

    const result = await runGenerator('Test?', fakeContext, provider);

    expect(typeof result.thought).toBe('string');
    expect(result.answer).toBe('Legacy answer');
    expect(result.confidence).toBe('Medium');
    expect(result.evidence).toEqual(['ev1 (loc)']);
    expect(result.limitations).toBe('None');
  });
});