File size: 8,380 Bytes
e706de2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/**

 * Solution 21: Product Review Analyzer

 *

 * Difficulty: β­β˜†β˜†β˜† (Beginner)

 *

 * Skills gained:

 * - Using parsers in chains

 * - Text cleaning with StringOutputParser

 * - Basic chain composition

 */

import {Runnable, PromptTemplate, StringOutputParser} from '../../../../src/index.js';
import {LlamaCppLLM} from '../../../../src/llm/llama-cpp-llm.js';
import {QwenChatWrapper} from "node-llama-cpp";

// Sample product reviews to analyze
const REVIEWS = [
    "This product is amazing! Best purchase ever. 5 stars!",
    "Terrible quality. Broke after one week. Very disappointed.",
    "It's okay. Does the job but nothing special.",
    "Love it! Exactly what I needed. Highly recommend!",
    "Not worth the price. Expected better quality."
];

async function createReviewSummarizer() {
    const prompt = new PromptTemplate({
        template: "Summarize the following review in ONE sentence:\n\n{review}",
        inputVariables: ["review"]
    });
    const llm = new LlamaCppLLM({
        modelPath: './models/Qwen3-1.7B-Q6_K.gguf',
        chatWrapper: new QwenChatWrapper({
            thoughts: 'discourage'  // Prevents the model from outputting thinking tokens
        }),
    });

    const parser = new StringOutputParser();
    const chain = prompt.pipe(llm).pipe(parser);

    return chain;
}

async function createSentimentExtractor() {
    const prompt = new PromptTemplate({
        template: `Classify the sentiment of this review:



{review}



Respond with ONLY ONE WORD: positive, negative, or neutral.`,
        inputVariables: ["review"]
    });

    const llm = new LlamaCppLLM({
        modelPath: './models/Qwen3-1.7B-Q6_K.gguf',
        chatWrapper: new QwenChatWrapper({
            thoughts: 'discourage'  // Prevents the model from outputting thinking tokens
        }),
        temperature: 0.1
    });
    ;

    const parser = new StringOutputParser();
    const chain = prompt.pipe(llm).pipe(parser);

    return chain;
}

async function analyzeReviews() {
    console.log('=== Exercise 21: Product Review Analyzer ===\n');

    const summarizerChain = await createReviewSummarizer();
    const sentimentChain = await createSentimentExtractor();

    console.log('Processing reviews...\n');

    for (let i = 0; i < REVIEWS.length; i++) {
        const review = REVIEWS[i];

        console.log(`Review ${i + 1}: "${review}"`);

        const summary = await summarizerChain.invoke({review});
        const sentiment = await sentimentChain.invoke({review});

        console.log(`Summary: ${summary}`);
        console.log(`Sentiment: ${sentiment}`);
        console.log();
    }

    console.log('βœ“ Exercise 1 Complete!');

    return {summarizerChain, sentimentChain};
}

// Run the exercise
analyzeReviews()
    .then(runTests)
    .catch(console.error);

// ============================================================================
// AUTOMATED TESTS
// ============================================================================

async function runTests(results) {
    const {summarizerChain, sentimentChain} = results;

    console.log('\n' + '='.repeat(60));
    console.log('RUNNING AUTOMATED TESTS');
    console.log('='.repeat(60) + '\n');

    const assert = (await import('assert')).default;
    let passed = 0;
    let failed = 0;

    async function test(name, fn) {
        try {
            await fn();
            passed++;
            console.log(`βœ… ${name}`);
        } catch (error) {
            failed++;
            console.error(`❌ ${name}`);
            console.error(`   ${error.message}\n`);
        }
    }

    // Test 1: Chains created
    await test('Summarizer chain created', async () => {
        assert(summarizerChain !== null && summarizerChain !== undefined, 'Create summarizerChain');
        assert(summarizerChain instanceof Runnable, 'Chain should be Runnable');
    });

    await test('Sentiment chain created', async () => {
        assert(sentimentChain !== null && sentimentChain !== undefined, 'Create sentimentChain');
        assert(sentimentChain instanceof Runnable, 'Chain should be Runnable');
    });

    // Test 2: Chains work (only run if chains exist)
    if (summarizerChain !== null && summarizerChain !== undefined) {
        await test('Summarizer chain produces output', async () => {
            const result = await summarizerChain.invoke({
                review: "Great product! Love it!"
            });
            assert(typeof result === 'string', 'Should return string');
            assert(result.length > 0, 'Should not be empty');
            assert(result.length < 200, 'Should be concise (< 200 chars)');
        });
    } else {
        failed++;
        console.error(`❌ Summarizer chain produces output`);
        console.error(`   Cannot test - summarizerChain is not created\n`);
    }

    if (sentimentChain !== null && sentimentChain !== undefined) {
        await test('Sentiment chain produces valid sentiment', async () => {
            const result = await sentimentChain.invoke({
                review: "Terrible product. Very bad."
            });
            const cleaned = result.toLowerCase().trim();
            const validSentiments = ['positive', 'negative', 'neutral'];
            assert(
                validSentiments.includes(cleaned),
                `Should be one of: ${validSentiments.join(', ')}. Got: ${cleaned}`
            );
        });
    } else {
        failed++;
        console.error(`❌ Sentiment chain produces valid sentiment`);
        console.error(`   Cannot test - sentimentChain is not created\n`);
    }

    // Test 3: Parser cleans output (only if chain exists)
    if (sentimentChain !== null && sentimentChain !== undefined) {
        await test('StringOutputParser removes extra whitespace', async () => {
            const result = await sentimentChain.invoke({
                review: "It's okay"
            });
            assert(result === result.trim(), 'Should have no leading/trailing whitespace');
            assert(!result.includes('  '), 'Should have no double spaces');
        });
    } else {
        failed++;
        console.error(`❌ StringOutputParser removes extra whitespace`);
        console.error(`   Cannot test - sentimentChain is not created\n`);
    }

    // Test 4: Consistent results (only if chain exists)
    if (sentimentChain !== null && sentimentChain !== undefined) {
        await test('Chains produce consistent sentiment', async () => {
            const positive = await sentimentChain.invoke({
                review: "Amazing! Best ever! 5 stars!"
            });
            const negative = await sentimentChain.invoke({
                review: "Horrible! Worst purchase ever! 0 stars!"
            });

            assert(
                positive.toLowerCase().includes('positive'),
                'Clearly positive review should be classified as positive'
            );
            assert(
                negative.toLowerCase().includes('negative'),
                'Clearly negative review should be classified as negative'
            );
        });
    } else {
        failed++;
        console.error(`❌ Chains produce consistent sentiment`);
        console.error(`   Cannot test - sentimentChain is not created\n`);
    }

    // Summary
    console.log('\n' + '='.repeat(60));
    console.log('TEST SUMMARY');
    console.log('='.repeat(60));
    console.log(`Total: ${passed + failed}`);
    console.log(`βœ… Passed: ${passed}`);
    console.log(`❌ Failed: ${failed}`);
    console.log('='.repeat(60));

    if (failed === 0) {
        console.log('\nπŸŽ‰ All tests passed!\n');
        console.log('πŸ“š What you learned:');
        console.log('  β€’ StringOutputParser cleans text automatically');
        console.log('  β€’ Parsers work seamlessly in chains with .pipe()');
        console.log('  β€’ Low temperature gives consistent outputs');
        console.log('  β€’ Clear prompts help parsers succeed');
        console.log('  β€’ Chains are reusable across multiple inputs\n');
    } else {
        console.log('\n⚠️  Some tests failed. Check your implementation.\n');
    }
}