feat: Unify AI prompts for Gemini and Mistral

This commit is contained in:
2025-12-19 22:01:29 +01:00
parent 2601a8f12f
commit e0436987a1
5 changed files with 53 additions and 60 deletions

40
src/lib/ai-prompts.ts Normal file
View File

@@ -0,0 +1,40 @@
export const getSystemPrompt = (availableTags: string, language: string) => `
You are a sommelier and database clerk for a premium whisky vault. Analyze the bottle image.
PART 1: METADATA EXTRACTION
Extract precise metadata. If the image is NOT a whisky bottle or if you are very unsure, set "is_whisky" to false and provide a low "confidence" score.
Use null for any value not visible or not inferable.
Infer the 'category' (e.g., Islay Single Malt Scotch Whisky) based on the Distillery and label details if possible.
IMPORTANT: Extract technical metadata (name, distillery, category) in English.
Provide a "search_string" field for Whiskybase in this format: "site:whiskybase.com [Distillery] [Name] [Vintage/Age]".
PART 2: SENSORY ANALYSIS
Based on the identified bottle, select the most appropriate flavor tags.
CONSTRAINT: You must ONLY select tags from the following list. Do NOT invent new tags in the "suggested_tags" field.
LIST: ${availableTags}
PART 3: CUSTOM SUGGESTIONS
If you recognize highly dominant notes that are NOT in the list above, provide them in "suggested_custom_tags".
Limit this to 1-2 very unique notes (e.g. "Marshmallow" or "Balsamico"). Do not repeat tags from the system list.
Localize these custom tags in ${language === 'en' ? 'English' : 'German'}.
Output strictly raw JSON (no markdown, no other text) matching the following schema:
{
"name": "Full name of the whisky edition",
"distillery": "Name of the distillery",
"category": "Whisky category (e.g. Islay Single Malt)",
"abv": number (e.g. 43),
"age": number (years, e.g. 16),
"vintage": "Year (e.g. 1995)",
"bottleCode": "Any visible bottle codes (e.g. L-code)",
"whiskybaseId": "Whiskybase ID if clearly visible",
"distilled_at": "Distillation date if visible",
"bottled_at": "Bottling date if visible",
"batch_info": "Batch number or cask info",
"is_whisky": boolean,
"confidence": number (0-100),
"suggested_tags": string[],
"suggested_custom_tags": string[],
"search_string": string
}
`;

View File

@@ -11,42 +11,4 @@ export const geminiModel = genAI.getGenerativeModel({
},
});
export const SYSTEM_INSTRUCTION = `
You are a sommelier and database clerk. Analyze the whisky bottle image.
PART 1: METADATA EXTRACTION
Extract precise metadata. If the image is NOT a whisky bottle or if you are very unsure, set "is_whisky" to false and provide a low "confidence" score.
If a value is not visible, use null.
Infer the 'Category' (e.g., Islay Single Malt) based on the Distillery if possible.
Search specifically for a "Whiskybase ID" or "WB ID" on the label.
IMPORTANT: Extract technical metadata (name, distillery, category) in English.
The 'suggested_custom_tags' MUST be localized in {LANGUAGE}.
PART 2: SENSORY ANALYSIS (AUTO-FILL)
Based on the identified bottle, select the most appropriate flavor tags.
CONSTRAINT: You must ONLY select tags from the following provided list. Do NOT invent new tags in this field.
LIST: {AVAILABLE_TAGS}
PART 3: CUSTOM SUGGESTIONS
If you recognize highly dominant notes that are NOT in the list above, provide them in 'suggested_custom_tags'.
Limit this to 1-2 very unique notes (e.g. "Marshmallow" or "Balsamico"). Do not repeat tags from the system list.
Output strictly raw JSON matching the following schema:
{
"name": string | null,
"distillery": string | null,
"category": string | null,
"abv": number | null,
"age": number | null,
"vintage": string | null,
"bottleCode": string | null,
"whiskybaseId": string | null,
"distilled_at": string | null,
"bottled_at": string | null,
"batch_info": string | null,
"is_whisky": boolean,
"confidence": number (0-100),
"suggested_tags": string[] (from provided list),
"suggested_custom_tags": string[] (new unique notes)
}
`;
// SYSTEM_INSTRUCTION moved to src/lib/ai-prompts.ts

View File

@@ -1,6 +1,7 @@
'use server';
import { Mistral } from '@mistralai/mistralai';
import { getSystemPrompt } from '@/lib/ai-prompts';
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
import { createClient } from '@/lib/supabase/server';
import { createHash } from 'crypto';
@@ -49,21 +50,7 @@ export async function analyzeBottleMistral(base64Image: string, tags?: string[],
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
const dataUrl = `data:image/jpeg;base64,${base64Data}`;
const prompt = `Du bist ein Whisky-Experte und OCR-Spezialist.
Analysiere dieses Etikett präzise.
Sprache für Beschreibungen: ${locale === 'en' ? 'Englisch' : 'Deutsch'}.
Verfügbare Tags zur Einordnung: ${tags ? tags.join(', ') : 'Keine Tags verfügbar'}.
Antworte AUSSCHLIESSLICH mit gültigem JSON (kein Markdown, kein Text davor/danach):
{
"distillery": "Name der Brennerei (z.B. Lagavulin)",
"name": "Exakter Name/Edition (z.B. 16 Year Old)",
"vintage": "Jahrgang als Zahl oder null",
"age": "Alter als Zahl oder null (z.B. 16)",
"abv": "Alkoholgehalt als Zahl ohne % (z.B. 43)",
"category": "Kategorie (z.B. Single Malt Scotch Whisky)",
"search_string": "site:whiskybase.com [Brennerei] [Name] [Alter]"
}`;
const prompt = getSystemPrompt(tags ? tags.join(', ') : 'Keine Tags verfügbar', locale);
const chatResponse = await client.chat.complete({
model: 'mistral-large-latest',

View File

@@ -1,6 +1,7 @@
'use server';
import { geminiModel, SYSTEM_INSTRUCTION } from '@/lib/gemini';
import { geminiModel } from '@/lib/gemini';
import { getSystemPrompt } from '@/lib/ai-prompts';
import { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky';
import { createClient } from '@/lib/supabase/server';
import { createHash } from 'crypto';
@@ -50,9 +51,7 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
};
}
const instruction = SYSTEM_INSTRUCTION
.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available')
.replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German');
const instruction = getSystemPrompt(tags ? tags.join(', ') : 'No tags available', locale);
const result = await geminiModel.generateContent([
{
@@ -71,6 +70,10 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
jsonData = jsonData[0];
}
// Extract search_string if present
const searchString = jsonData.search_string;
delete jsonData.search_string;
if (!jsonData) {
throw new Error('Keine Daten in der KI-Antwort gefunden.');
}
@@ -106,7 +109,8 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
return {
success: true,
data: validatedData,
};
search_string: searchString
} as any;
} catch (error) {
console.error('Gemini Analysis Error:', error);