Sentiment Analysis
This example analyzes customer feedback in bulk with nua.list().
Problem
Section titled “Problem”You receive hundreds of customer reviews daily and need to:
- Classify each review’s sentiment
- Extract key topics mentioned
- Identify actionable feedback
- Prioritize responses
Solution
Section titled “Solution”import { Nua, z } from 'nuabase';import { findLocalAgent } from 'nuabase/local-agent';
const nua = Nua.direct({ localAgent: await findLocalAgent() });
// Define the analysis schemaconst SentimentSchema = z.object({ sentiment: z.enum(['positive', 'neutral', 'negative']), score: z.number().min(-1).max(1), topics: z.array(z.string()).max(3), isActionable: z.boolean(), actionRequired: z.string().optional(), responseUrgency: z.enum(['none', 'low', 'medium', 'high']),});
// Sample reviewsconst reviews = [ { id: 'rev-001', text: 'Absolutely love this product! The customer service was exceptional. Already recommended to friends.', source: 'app-store', date: '2024-01-15' }, { id: 'rev-002', text: 'App crashes every time I try to export. Very frustrating. Been waiting 3 days for support response.', source: 'support-ticket', date: '2024-01-15' }, { id: 'rev-003', text: 'It works fine for basic tasks. Nothing special but gets the job done.', source: 'app-store', date: '2024-01-14' }, { id: 'rev-004', text: 'The new update broke my workflow. I rely on this for my business. Please fix ASAP!', source: 'twitter', date: '2024-01-15' }, { id: 'rev-005', text: 'Great value for the price. The team feature is a game changer for our agency.', source: 'g2-review', date: '2024-01-13' }];
async function analyzeReviews() { const result = await nua.list( `Analyze each customer review for sentiment and actionability.
For each review:- Determine overall sentiment (positive/neutral/negative)- Assign a score from -1 (very negative) to 1 (very positive)- Extract up to 3 main topics mentioned- Determine if action is required (bug report, complaint needing response, etc.)- If actionable, describe what action is needed- Assess response urgency based on sentiment and business impact`, { input: reviews, primaryKey: 'id', output: { name: 'analysis', schema: SentimentSchema } } );
if (!result.success) { console.error('Analysis failed:', result.error); return; }
// Rows may come back in any order, so look each one up by its primary key const reviewsById = new Map(reviews.map(r => [r.id, r]));
// Group by urgency const urgent = result.data.filter(r => r.analysis.responseUrgency === 'high'); const actionable = result.data.filter(r => r.analysis.isActionable); const positive = result.data.filter(r => r.analysis.sentiment === 'positive'); const negative = result.data.filter(r => r.analysis.sentiment === 'negative');
// Summary report console.log('Customer Feedback Analysis'); console.log('='.repeat(50)); console.log(`Total reviews: ${reviews.length}`); console.log(`Positive: ${positive.length} | Neutral: ${result.data.length - positive.length - negative.length} | Negative: ${negative.length}`); console.log(`Urgent responses needed: ${urgent.length}`); console.log(`Actionable feedback: ${actionable.length}`);
// Urgent items if (urgent.length > 0) { console.log('\n--- URGENT: Requires Immediate Response ---\n'); for (const row of urgent) { const review = reviewsById.get(row.id)!; console.log(`[${row.id}] ${review.source}`); console.log(` "${review.text.slice(0, 80)}..."`); console.log(` Action: ${row.analysis.actionRequired}`); console.log(); } }
// Actionable items console.log('\n--- All Actionable Feedback ---\n'); for (const row of actionable) { const review = reviewsById.get(row.id)!; const a = row.analysis; console.log(`[${row.id}] ${a.sentiment} (${a.score.toFixed(2)}) - Urgency: ${a.responseUrgency}`); console.log(` Topics: ${a.topics.join(', ')}`); console.log(` Action: ${a.actionRequired}`); console.log(); }
// Stats console.log('='.repeat(50)); console.log(`Tokens: ${result.usage.totalTokens} | Latency: ${result.latencyMs}ms`);}
analyzeReviews();Output
Section titled “Output”From a run on Claude Code, with the default logger’s [INFO] lines left out. The wording of free-text fields such as the next step varies from run to run:
Customer Feedback Analysis==================================================Total reviews: 5Positive: 2 | Neutral: 1 | Negative: 2Urgent responses needed: 2Actionable feedback: 2
--- URGENT: Requires Immediate Response ---
[rev-002] support-ticket "App crashes every time I try to export. Very frustrating. Been waiting 3 days fo..." Action: Investigate and fix the crash on export; respond to the customer's support ticket right away, since they have waited 3 days.
[rev-004] twitter "The new update broke my workflow. I rely on this for my business. Please fix ASA..." Action: Reply publicly on Twitter, find out which workflow the update broke, and prioritize a fix or rollback for this business-critical user.
--- All Actionable Feedback ---
[rev-002] negative (-0.85) - Urgency: high Topics: app crash, export feature, support response time Action: Investigate and fix the crash on export; respond to the customer's support ticket right away, since they have waited 3 days.
[rev-004] negative (-0.80) - Urgency: high Topics: update regression, workflow disruption, business reliance Action: Reply publicly on Twitter, find out which workflow the update broke, and prioritize a fix or rollback for this business-critical user.
==================================================Tokens: 3104 | Latency: 6125msProcessing Daily Batches
Section titled “Processing Daily Batches”When you analyze reviews every day, keep the prompt string constant and pass each day’s reviews as input. Nuabase puts the prompt and schema at the start of the request, so the provider can serve that part from its prompt cache:
const result = await nua.list(ANALYZE_REVIEWS_PROMPT, { input: todaysReviews, primaryKey: 'id', output: { name: 'analysis', schema: SentimentSchema },});
if (result.success) { const { promptTokens, promptCacheReadTokens } = result.usage; if (promptCacheReadTokens !== undefined) { console.log(`${promptCacheReadTokens} of ${promptTokens} prompt tokens read from the provider's cache`); }}The cache fields are present only when the provider reports them. For very large batches, split the reviews into chunks; see Large arrays.
Variations
Section titled “Variations”Simple Sentiment Only
Section titled “Simple Sentiment Only”For basic sentiment without detailed analysis:
const SimpleSentimentSchema = z.object({ sentiment: z.enum(['positive', 'neutral', 'negative']), confidence: z.number().min(0).max(1),});
const result = await nua.list("Classify sentiment", { input: reviews, primaryKey: 'id', output: { name: 'sentiment', schema: SimpleSentimentSchema }});Multi-Aspect Sentiment
Section titled “Multi-Aspect Sentiment”Analyze different aspects separately:
const AspectSentimentSchema = z.object({ overall: z.enum(['positive', 'neutral', 'negative']), aspects: z.object({ product: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']), support: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']), pricing: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']), usability: z.enum(['positive', 'neutral', 'negative', 'not-mentioned']), }),});Competitor Mentions
Section titled “Competitor Mentions”Track competitor mentions in feedback:
const CompetitorSchema = z.object({ sentiment: z.enum(['positive', 'neutral', 'negative']), mentionsCompetitor: z.boolean(), competitors: z.array(z.string()).optional(), comparisonSentiment: z.enum(['favorable', 'unfavorable', 'neutral']).optional(),});