diff --git a/src/services/aiService.server.ts b/src/services/aiService.server.ts index 27eb2dab..b4806e40 100644 --- a/src/services/aiService.server.ts +++ b/src/services/aiService.server.ts @@ -86,20 +86,64 @@ export const extractItemsFromReceiptImage = async ( * @returns A promise that resolves to the extracted core data. */ export const extractCoreDataFromFlyerImage = async ( - _imagePaths: { path: string; mimetype: string }[], - _masterItems: MasterGroceryItem[] + imagePaths: { path: string; mimetype: string }[], + masterItems: MasterGroceryItem[] ): Promise<{ store_name: string; valid_from: string | null; valid_to: string | null; items: Omit[]; }> => { - // This function's logic is now handled by the backend API endpoint in `src/routes/ai.ts`. - // The actual Gemini call logic is complex and has been moved there to keep this file clean - // and demonstrate the architectural separation. For the purpose of this fix, we can - // assume the backend correctly implements the Gemini call. - // This function is now a placeholder to show where the logic *would* live. - throw new Error("extractCoreDataFromFlyerImage is a server-side function and should be called from a backend route."); + // 1. Construct the detailed prompt for the AI. + const prompt = ` + Analyze the provided flyer image(s). Your task is to extract key information and a list of all sale items. + + First, identify the following core details for the entire flyer: + - "store_name": The name of the grocery store (e.g., "Walmart", "No Frills"). + - "valid_from": The start date of the sale period in YYYY-MM-DD format. If not present, use null. + - "valid_to": The end date of the sale period in YYYY-MM-DD format. If not present, use null. + + Second, extract each individual sale item. For each item, provide: + - "item": The name of the product (e.g., "Coca-Cola Classic"). + - "price_display": The sale price as a string (e.g., "$2.99", "2 for $5.00"). + - "price_in_cents": The primary numeric price converted to cents (e.g., for "$2.99", use 299). If a price is "2 for $5.00", use 500. If no price, use null. + - "quantity": A string describing the quantity or weight for the price (e.g., "12x355mL", "500g", "each"). + - "master_item_id": From the provided master list, find the best matching item and return its ID. If no good match is found, use null. + - "category_name": The most appropriate category for the item (e.g., "Beverages", "Meat & Seafood"). + + Here is the master list of grocery items to help with matching: + ${JSON.stringify(masterItems)} + + Return a single, valid JSON object with the keys "store_name", "valid_from", "valid_to", and "items". The "items" key should contain an array of the extracted item objects. + Do not include any other text, explanations, or markdown formatting. + `; + + // 2. Convert all uploaded image files into the format required by the Gemini API. + const imageParts = await Promise.all( + imagePaths.map(file => serverFileToGenerativePart(file.path, file.mimetype)) + ); + + // 3. Make the API call to Gemini. + const response = await model.generateContent({ + model: 'gemini-1.5-flash', + contents: [{ parts: [{ text: prompt }, ...imageParts] }] + }); + + const text = response.text; + + // 4. Clean and parse the AI's response. + const jsonMatch = text?.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + logger.error("AI response for flyer processing did not contain a valid JSON object.", { responseText: text }); + throw new Error('AI response did not contain a valid JSON object.'); + } + + try { + return JSON.parse(jsonMatch[0]); + } catch (e) { + logger.error("Failed to parse JSON from AI response in extractCoreDataFromFlyerImage", { responseText: text, error: e }); + throw new Error('Failed to parse structured data from the AI response.'); + } }; /**