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');
}
} |