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 = `
|
// SYSTEM_INSTRUCTION moved to src/lib/ai-prompts.ts
|
||||||
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)
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { Mistral } from '@mistralai/mistralai';
|
import { Mistral } from '@mistralai/mistralai';
|
||||||
|
import { getSystemPrompt } from '@/lib/ai-prompts';
|
||||||
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { createHash } from 'crypto';
|
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 client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
|
||||||
const dataUrl = `data:image/jpeg;base64,${base64Data}`;
|
const dataUrl = `data:image/jpeg;base64,${base64Data}`;
|
||||||
|
|
||||||
const prompt = `Du bist ein Whisky-Experte und OCR-Spezialist.
|
const prompt = getSystemPrompt(tags ? tags.join(', ') : 'Keine Tags verfügbar', locale);
|
||||||
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 chatResponse = await client.chat.complete({
|
const chatResponse = await client.chat.complete({
|
||||||
model: 'mistral-large-latest',
|
model: 'mistral-large-latest',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use server';
|
'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 { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky';
|
||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
@@ -50,9 +51,7 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const instruction = SYSTEM_INSTRUCTION
|
const instruction = getSystemPrompt(tags ? tags.join(', ') : 'No tags available', locale);
|
||||||
.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available')
|
|
||||||
.replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German');
|
|
||||||
|
|
||||||
const result = await geminiModel.generateContent([
|
const result = await geminiModel.generateContent([
|
||||||
{
|
{
|
||||||
@@ -71,6 +70,10 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
|
|||||||
jsonData = jsonData[0];
|
jsonData = jsonData[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract search_string if present
|
||||||
|
const searchString = jsonData.search_string;
|
||||||
|
delete jsonData.search_string;
|
||||||
|
|
||||||
if (!jsonData) {
|
if (!jsonData) {
|
||||||
throw new Error('Keine Daten in der KI-Antwort gefunden.');
|
throw new Error('Keine Daten in der KI-Antwort gefunden.');
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,8 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: validatedData,
|
data: validatedData,
|
||||||
};
|
search_string: searchString
|
||||||
|
} as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gemini Analysis Error:', 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