'use server'; import { GoogleGenerativeAI, SchemaType, HarmCategory, HarmBlockThreshold } from '@google/generative-ai'; import { createClient } from '@/lib/supabase/server'; import { trackApiUsage } from '@/services/track-api-usage'; import { deductCredits } from '@/services/credit-service'; import { getAllSystemTags } from '@/services/tags'; import { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } from '@/lib/openrouter'; // Native Schema Definition for Enrichment Data const enrichmentSchema = { description: "Sensory profile and search metadata for whisky", type: SchemaType.OBJECT as const, properties: { suggested_tags: { type: SchemaType.ARRAY, description: "Array of suggested aroma/taste tags from the available system tags", items: { type: SchemaType.STRING }, nullable: true }, suggested_custom_tags: { type: SchemaType.ARRAY, description: "Array of custom dominant notes not in the system tags", items: { type: SchemaType.STRING }, nullable: true }, search_string: { type: SchemaType.STRING, description: "Optimized search query for Whiskybase discovery", nullable: true } }, required: [], }; const ENRICHMENT_MODEL = 'google/gemma-3-27b-it'; /** * Sleep helper for retry delays */ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Enrich with OpenRouter */ async function enrichWithOpenRouter(instruction: string): Promise<{ data: any; apiTime: number }> { const client = getOpenRouterClient(); const startApi = performance.now(); const maxRetries = 3; let lastError: any = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await client.chat.completions.create({ model: ENRICHMENT_MODEL, messages: [ { role: 'user', content: instruction + `\n\nRespond ONLY with valid JSON in this format: { "suggested_tags": ["tag1", "tag2"], "suggested_custom_tags": ["custom1"], "search_string": "Distillery Name Age" }` }, ], temperature: 0.3, max_tokens: 512, // @ts-ignore - OpenRouter-specific field provider: OPENROUTER_PROVIDER_PREFERENCES, }); const endApi = performance.now(); const content = response.choices[0]?.message?.content || '{}'; // Extract JSON from response let jsonStr = content; const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); if (jsonMatch) { jsonStr = jsonMatch[1].trim(); } return { data: JSON.parse(jsonStr), apiTime: endApi - startApi, }; } catch (error: any) { lastError = error; const status = error?.status || error?.response?.status; if (status === 429 || status === 503) { const delay = Math.pow(2, attempt) * 1000; console.log(`[Enrichment] Rate limited (${status}), retry ${attempt}/${maxRetries} in ${delay}ms...`); await sleep(delay); continue; } throw error; } } throw lastError || new Error('OpenRouter enrichment failed after retries'); } /** * Enrich with Gemini */ async function enrichWithGemini(instruction: string): Promise<{ data: any; apiTime: number }> { const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash', generationConfig: { responseMimeType: "application/json", responseSchema: enrichmentSchema as any, temperature: 0.3, }, safetySettings: [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }, ] as any, }); const startApi = performance.now(); const result = await model.generateContent(instruction); const endApi = performance.now(); return { data: JSON.parse(result.response.text()), apiTime: endApi - startApi, }; } export async function enrichData(name: string, distillery: string, availableTags?: string, language: string = 'de') { const provider = getAIProvider(); // Check API key based on provider if (provider === 'gemini' && !process.env.GEMINI_API_KEY) { return { success: false, error: 'GEMINI_API_KEY is not configured.' }; } if (provider === 'openrouter' && !process.env.OPENROUTER_API_KEY) { return { success: false, error: 'OPENROUTER_API_KEY is not configured.' }; } let supabase; try { let tagsToUse = availableTags; if (!tagsToUse) { const systemTags = await getAllSystemTags(); tagsToUse = systemTags.map(t => t.name).join(', '); } supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return { success: false, error: 'Nicht autorisiert.' }; } const userId = user.id; const instruction = `Identify the sensory profile for the following whisky. Whisky: ${name} (${distillery}) Language: ${language} Available system tags (pick relevant ones): ${tagsToUse} Instructions: 1. Select the most appropriate sensory tags from the "Available system tags" list. 2. If there are dominant notes that are NOT in the system list, add them to "suggested_custom_tags". 3. Provide a clean "search_string" for Whiskybase (e.g., "Distillery Name Age").`; console.log(`[EnrichData] Using provider: ${provider}`); let result: { data: any; apiTime: number }; if (provider === 'openrouter') { result = await enrichWithOpenRouter(instruction); } else { result = await enrichWithGemini(instruction); } console.log('[EnrichData] Response:', result.data); // Track usage await trackApiUsage({ userId: userId, apiType: 'gemini_ai', endpoint: `enrichData_${provider}`, success: true }); await deductCredits(userId, 'gemini_ai', `Data enrichment (${provider})`); return { success: true, data: result.data, provider, perf: { apiDuration: result.apiTime } }; } catch (error) { console.error('Enrich Data Error:', error); return { success: false, error: error instanceof Error ? error.message : 'Fehler bei der Daten-Anreicherung.', }; } }