Email / tutorial /02-composition /02-parsers /solutions /21-review-analyzer-solution.js
lenzcom's picture
Upload folder using huggingface_hub
e706de2 verified
/**
* 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');
}
}