handlers.ts•13.7 kB
import { OpenFoodFactsClient } from './client.js';
import { Product } from './types.js';
export class ToolHandlers {
constructor(private client: OpenFoodFactsClient) {}
async handleGetProduct(barcode: string) {
const response = await this.client.getProduct(barcode);
if (response.status === 0 || !response.product) {
return {
content: [
{
type: "text" as const,
text: `Product not found for barcode: ${barcode}`,
},
],
};
}
const product = response.product;
const formattedProduct = this.formatProduct(product);
return {
content: [
{
type: "text" as const,
text: formattedProduct,
},
],
};
}
async handleSearchProducts(params: any) {
const response = await this.client.searchProducts(params);
if (response.products.length === 0) {
return {
content: [
{
type: "text" as const,
text: "No products found matching your search criteria.",
},
],
};
}
const summary = `Found ${response.count} products (showing page ${response.page} of ${response.page_count}):\n\n`;
const productList = response.products
.map((product, index) => `${index + 1}. ${this.formatProductSummary(product)}`)
.join('\n\n');
return {
content: [
{
type: "text" as const,
text: summary + productList,
},
],
};
}
async handleAnalyzeProduct(barcode: string) {
const response = await this.client.getProduct(barcode);
if (response.status === 0 || !response.product) {
return {
content: [
{
type: "text" as const,
text: `Product not found for barcode: ${barcode}`,
},
],
};
}
const analysis = this.analyzeNutrition(response.product);
return {
content: [
{
type: "text" as const,
text: analysis,
},
],
};
}
async handleCompareProducts(barcodes: string[], focus: string = 'nutrition') {
const responses = await this.client.getProductsByBarcodes(barcodes);
const validProducts = responses.filter(r => r.status !== 0 && r.product);
if (validProducts.length < 2) {
return {
content: [
{
type: "text" as const,
text: "Need at least 2 valid products to compare.",
},
],
};
}
const comparison = this.compareProductsByFocus(validProducts.map(r => r.product!), focus);
return {
content: [
{
type: "text" as const,
text: comparison,
},
],
};
}
async handleGetProductSuggestions(params: any) {
const { category, dietary_preferences = [], max_results = 10, min_nutriscore } = params;
const searchParams: any = {
categories: category,
page_size: Math.min(max_results * 2, 100), // Get extra to filter
sort_by: 'popularity',
};
if (min_nutriscore) {
const validGrades = ['a', 'b', 'c', 'd', 'e'];
const minIndex = validGrades.indexOf(min_nutriscore);
const allowedGrades = validGrades.slice(0, minIndex + 1);
searchParams.nutrition_grades = allowedGrades.join(',');
}
const response = await this.client.searchProducts(searchParams);
let filteredProducts = response.products;
// Filter by dietary preferences
if (dietary_preferences.length > 0) {
filteredProducts = this.filterByDietaryPreferences(filteredProducts, dietary_preferences);
}
const suggestions = filteredProducts
.slice(0, max_results)
.map((product, index) => `${index + 1}. ${this.formatProductSuggestion(product)}`)
.join('\n\n');
return {
content: [
{
type: "text" as const,
text: `Product suggestions in ${category}:\n\n${suggestions}`,
},
],
};
}
private formatProduct(product: Product): string {
const sections = [];
// Basic info
sections.push(`**${product.product_name || 'Unknown Product'}**`);
if (product.brands) sections.push(`Brand: ${product.brands}`);
if (product.quantity) sections.push(`Quantity: ${product.quantity}`);
if (product.categories) sections.push(`Categories: ${product.categories}`);
// Nutritional scores
const scores = [];
if (product.nutriscore_grade) scores.push(`Nutri-Score: ${product.nutriscore_grade.toUpperCase()}`);
if (product.nova_group) scores.push(`NOVA Group: ${product.nova_group}`);
if (product.ecoscore_grade) scores.push(`Eco-Score: ${product.ecoscore_grade.toUpperCase()}`);
if (scores.length > 0) sections.push(`\n**Scores:**\n${scores.join(' | ')}`);
// Ingredients
if (product.ingredients_text) {
sections.push(`\n**Ingredients:**\n${product.ingredients_text}`);
}
// Key nutrients
if (product.nutriments) {
const keyNutrients = this.extractKeyNutrients(product.nutriments);
if (keyNutrients) {
sections.push(`\n**Nutrition (per 100g):**\n${keyNutrients}`);
}
}
// Additional info
const additional = [];
if (product.packaging) additional.push(`Packaging: ${product.packaging}`);
if (product.labels) additional.push(`Labels: ${product.labels}`);
if (product.countries) additional.push(`Countries: ${product.countries}`);
if (additional.length > 0) sections.push(`\n**Additional Info:**\n${additional.join('\n')}`);
return sections.join('\n');
}
private formatProductSummary(product: Product): string {
const name = product.product_name || 'Unknown Product';
const brand = product.brands ? ` (${product.brands})` : '';
const nutriscore = product.nutriscore_grade ? ` [Nutri-Score: ${product.nutriscore_grade.toUpperCase()}]` : '';
return `**${name}**${brand}${nutriscore}\nBarcode: ${product.code}`;
}
private formatProductSuggestion(product: Product): string {
const summary = this.formatProductSummary(product);
const scores = [];
if (product.nutriscore_grade) scores.push(`Nutri-Score: ${product.nutriscore_grade.toUpperCase()}`);
if (product.nova_group) scores.push(`Processing: NOVA ${product.nova_group}`);
if (product.ecoscore_grade) scores.push(`Eco: ${product.ecoscore_grade.toUpperCase()}`);
return summary + (scores.length > 0 ? `\nScores: ${scores.join(' | ')}` : '');
}
private analyzeNutrition(product: Product): string {
const sections = [`**Nutritional Analysis: ${product.product_name || 'Unknown Product'}**\n`];
// Scores interpretation
const scoreAnalysis = [];
if (product.nutriscore_grade) {
const grade = product.nutriscore_grade.toUpperCase();
const gradeDesc = this.getNutriscoreDescription(grade);
scoreAnalysis.push(`• Nutri-Score ${grade}: ${gradeDesc}`);
}
if (product.nova_group) {
const group = String(product.nova_group);
const groupDesc = this.getNovaDescription(group);
scoreAnalysis.push(`• NOVA Group ${group}: ${groupDesc}`);
}
if (product.ecoscore_grade) {
const grade = product.ecoscore_grade.toUpperCase();
const gradeDesc = this.getEcoscoreDescription(grade);
scoreAnalysis.push(`• Eco-Score ${grade}: ${gradeDesc}`);
}
if (scoreAnalysis.length > 0) {
sections.push(`**Scores:**\n${scoreAnalysis.join('\n')}\n`);
}
// Detailed nutrition
if (product.nutriments) {
const nutrition = this.analyzeNutriments(product.nutriments);
if (nutrition) {
sections.push(`**Nutritional Breakdown:**\n${nutrition}\n`);
}
}
return sections.join('\n');
}
private compareProductsByFocus(products: Product[], focus: string): string {
const comparison = [`**Product Comparison (${focus})**\n`];
products.forEach((product, index) => {
comparison.push(`**${index + 1}. ${product.product_name || 'Unknown'}**`);
switch (focus) {
case 'nutrition':
if (product.nutriscore_grade) {
comparison.push(`Nutri-Score: ${product.nutriscore_grade.toUpperCase()}`);
}
if (product.nutriments) {
const keyNutrients = this.extractKeyNutrients(product.nutriments);
if (keyNutrients) comparison.push(`Nutrients:\n${keyNutrients}`);
}
break;
case 'environmental':
if (product.ecoscore_grade) {
comparison.push(`Eco-Score: ${product.ecoscore_grade.toUpperCase()}`);
}
if (product.packaging) {
comparison.push(`Packaging: ${product.packaging}`);
}
break;
case 'processing':
if (product.nova_group) {
comparison.push(`NOVA Group: ${product.nova_group} (${this.getNovaDescription(String(product.nova_group))})`);
}
break;
case 'ingredients':
if (product.ingredients_text) {
comparison.push(`Ingredients: ${product.ingredients_text.substring(0, 200)}${product.ingredients_text.length > 200 ? '...' : ''}`);
}
break;
}
comparison.push('');
});
return comparison.join('\n');
}
private filterByDietaryPreferences(products: Product[], preferences: string[]): Product[] {
return products.filter(product => {
const labels = (product.labels || '').toLowerCase();
const categories = (product.categories || '').toLowerCase();
const ingredients = (product.ingredients_text || '').toLowerCase();
return preferences.every(pref => {
switch (pref) {
case 'vegan':
return labels.includes('vegan') || categories.includes('vegan');
case 'vegetarian':
return labels.includes('vegetarian') || categories.includes('vegetarian');
case 'gluten-free':
return labels.includes('gluten') && labels.includes('free');
case 'organic':
return labels.includes('organic') || labels.includes('bio');
case 'low-fat':
return labels.includes('low-fat') || labels.includes('light');
case 'low-sugar':
return labels.includes('sugar-free') || labels.includes('no-sugar');
case 'high-protein':
return labels.includes('high-protein') || labels.includes('protein');
default:
return true;
}
});
});
}
private extractKeyNutrients(nutriments: Record<string, string | number>): string {
const nutrients = [];
if (nutriments.energy_100g || nutriments['energy-kcal_100g']) {
const energy = nutriments['energy-kcal_100g'] || nutriments.energy_100g;
nutrients.push(`Energy: ${energy} kcal`);
}
if (nutriments.fat_100g) nutrients.push(`Fat: ${nutriments.fat_100g}g`);
if (nutriments.carbohydrates_100g) nutrients.push(`Carbs: ${nutriments.carbohydrates_100g}g`);
if (nutriments.sugars_100g) nutrients.push(`Sugars: ${nutriments.sugars_100g}g`);
if (nutriments.proteins_100g) nutrients.push(`Protein: ${nutriments.proteins_100g}g`);
if (nutriments.salt_100g) nutrients.push(`Salt: ${nutriments.salt_100g}g`);
if (nutriments.fiber_100g) nutrients.push(`Fiber: ${nutriments.fiber_100g}g`);
return nutrients.join(' | ');
}
private analyzeNutriments(nutriments: Record<string, string | number>): string {
const analysis = [];
const energy = Number(nutriments['energy-kcal_100g'] || nutriments.energy_100g || 0);
if (energy > 0) {
let energyLevel = 'moderate';
if (energy < 150) energyLevel = 'low';
else if (energy > 400) energyLevel = 'high';
analysis.push(`• Energy: ${energy} kcal (${energyLevel})`);
}
const fat = Number(nutriments.fat_100g || 0);
if (fat > 0) {
let fatLevel = fat > 20 ? 'high' : fat < 3 ? 'low' : 'moderate';
analysis.push(`• Fat: ${fat}g (${fatLevel})`);
}
const sugar = Number(nutriments.sugars_100g || 0);
if (sugar > 0) {
let sugarLevel = sugar > 22.5 ? 'high' : sugar < 5 ? 'low' : 'moderate';
analysis.push(`• Sugars: ${sugar}g (${sugarLevel})`);
}
const salt = Number(nutriments.salt_100g || 0);
if (salt > 0) {
let saltLevel = salt > 1.5 ? 'high' : salt < 0.3 ? 'low' : 'moderate';
analysis.push(`• Salt: ${salt}g (${saltLevel})`);
}
return analysis.join('\n');
}
private getNutriscoreDescription(grade: string): string {
const descriptions: Record<string, string> = {
A: 'Excellent nutritional quality',
B: 'Good nutritional quality',
C: 'Average nutritional quality',
D: 'Poor nutritional quality',
E: 'Very poor nutritional quality',
};
return descriptions[grade] || 'Unknown quality';
}
private getNovaDescription(group: string): string {
const descriptions: Record<string, string> = {
'1': 'Unprocessed or minimally processed foods',
'2': 'Processed culinary ingredients',
'3': 'Processed foods',
'4': 'Ultra-processed foods',
};
return descriptions[group] || 'Unknown processing level';
}
private getEcoscoreDescription(grade: string): string {
const descriptions: Record<string, string> = {
A: 'Very low environmental impact',
B: 'Low environmental impact',
C: 'Moderate environmental impact',
D: 'High environmental impact',
E: 'Very high environmental impact',
};
return descriptions[grade] || 'Unknown environmental impact';
}
}