feat: Unify AI prompts for Gemini and Mistral
This commit is contained in:
40
src/lib/ai-prompts.ts
Normal file
40
src/lib/ai-prompts.ts
Normal 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
|
||||
}
|
||||
`;
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user