import { FoodLogRepository } from '@/add-log/repository'; import { ALL_NUTRITION_KEYS, USA_GOV_DGA_KEYS } from '@/common/constants/nutrition'; import { getDatabase } from '@/remote/database'; import { strings } from '@/common/strings'; import type { AIProviderName, NutritionResult } from '@/types/foodlog'; import type { Nutrition } from '@/types/nutrition'; import { z } from 'zod'; import { ClaudeProvider, GeminiProvider, OpenAIProvider } from './providers'; const NutritionResultSchema = z.object({ novaClass: z.number().min(1).max(4).optional(), nutrition: z.record(z.string(), z.number()).optional(), }); const PROVIDERS = { gemini: new GeminiProvider(), claude: new ClaudeProvider(), openai: new OpenAIProvider(), } as const; const ALL_NUTRIENTS = ALL_NUTRITION_KEYS.join(', '); const DGA_NUTRIENTS = USA_GOV_DGA_KEYS.join(', '); function extractJSON(text: string): string { const match = text.match(/```json\s*([\s\S]*?)\s*```/) || text.match(/\{[\s\S]*\}/) || text.match(/\[[\s\S]*\]/); return match ? match[1] || match[0] : text; } type NutritionResultWithProvider = NutritionResult & { provider: AIProviderName; error?: string; }; export async function analyzeNutritionWithComparison(input: { foodLogId: string; profileId: string; foodName: string; ingredients: string[]; }): Promise<{ novaClass?: number; nutrition?: Nutrition }> { const { foodLogId, profileId, foodName, ingredients } = input; const prompt = strings.ai.estimateNutrition(foodName, ingredients, ALL_NUTRIENTS, DGA_NUTRIENTS); const providerNames: AIProviderName[] = ['gemini', 'claude', 'openai']; const settled = await Promise.allSettled( providerNames.map(async (providerName): Promise => { const responseText = await PROVIDERS[providerName].query(prompt); const parsed = JSON.parse(extractJSON(responseText)); const validated = NutritionResultSchema.parse(parsed); return { ...validated, provider: providerName }; }) ); const results = settled.map((result, index) => { if (result.status === 'fulfilled') return result.value; return { provider: providerNames[index], error: String(result.reason) }; }); const geminiResult = results.find(r => r.provider === 'gemini' && !r.error); const validPredictions = results.filter(r => !r.error); if (validPredictions.length > 0) { const db = getDatabase(); const repo = new FoodLogRepository(db); await repo.savePredictions(foodLogId, profileId, foodName, validPredictions); } return { novaClass: geminiResult?.novaClass, nutrition: geminiResult?.nutrition }; }