perf: batch Gemini price fetch — 1 call for all missing items instead of N

This commit is contained in:
dadaloop82
2026-05-07 20:28:58 +00:00
parent c02a2fc632
commit 7a51a44b86
+58 -24
View File
@@ -6497,44 +6497,69 @@ function _priceKey(string $name, string $country): string {
* { price_per_unit, unit_label, currency, source_note } or null on failure.
*/
function _fetchPriceFromAI(string $name, string $country, string $currency, string $lang): ?array {
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) return null;
$result = _fetchPricesBatchFromAI([$name], $country, $currency, $lang);
return $result[$name] ?? null;
}
$langLabel = match($lang) { 'en' => 'English', 'de' => 'German', default => 'Italian' };
/**
* Ask Gemini to price multiple items in a SINGLE API call.
* Returns: { name => { price_per_unit, unit_label, currency, source_note } }
* Items that could not be priced are omitted from the result.
*/
function _fetchPricesBatchFromAI(array $names, string $country, string $currency, string $lang): array {
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey) || empty($names)) return [];
// Build a numbered list for the prompt
$list = '';
foreach ($names as $i => $n) {
$list .= ($i + 1) . '. ' . $n . "\n";
}
$prompt = <<<PROMPT
You are a grocery price assistant. Estimate the typical retail price for "{$name}" in {$country}, currency {$currency}.
You are a grocery price assistant. Estimate typical retail prices for the following items in {$country}, currency {$currency}.
Return the price for the MOST NATURAL RETAIL UNIT the smallest standard unit a shopper would actually buy:
- Standard packages (pasta, flour, frozen food, yogurt, canned goods, biscuits): price per typical package (e.g. "pacco 500g", "barattolo 400g", "confezione")
- Sold by piece or bunch (fresh herbs, eggs, individual fruit/vegetables, single portions): price per piece/bunch (e.g. "mazzo", "uovo", "pz")
Items:
{$list}
For each item return the price for the MOST NATURAL RETAIL UNIT the smallest standard unit a shopper buys:
- Standard packages (pasta, flour, frozen food, biscuits, canned goods): price per typical package (e.g. "pacco 500g", "barattolo 400g", "confezione")
- Sold by piece or bunch (fresh herbs, eggs, individual fruit/veg, single portions): price per piece/bunch (e.g. "mazzo", "uovo", "pz")
- Liquids in bottles or cartons: price per typical container (e.g. "bottiglia 1L", "brick 1L")
- Deli items sold loose by weight: price per kg
Rules:
1. Use mid-range supermarket prices (not premium, not discount).
1. Mid-range supermarket prices (not premium, not discount).
2. Round to 2 decimal places.
3. NEVER return per-kg for items normally sold in packages or by the piece.
4. ALWAYS return your best estimate even for generic or unusual items. Use a typical grocery item if uncertain.
5. Respond ONLY with valid JSON no markdown, no explanation:
{"price_per_unit": 1.50, "unit_label": "mazzo", "currency": "{$currency}", "source_note": "Basilico fresco ~€1.50/mazzo in {$country}"}
If you are genuinely unsure, return a rough estimate for 1 typical package with a "~" in source_note:
{"price_per_unit": 2.00, "unit_label": "confezione", "currency": "{$currency}", "source_note": "~ stima generica confezione in {$country}"}
3. NEVER use per-kg for items normally sold in packages or by piece.
4. ALWAYS return a best estimate even for branded or unusual items. Use the closest generic equivalent if needed.
5. Respond ONLY with a valid JSON object keyed by the EXACT item name from the list above. No markdown, no explanation:
{
"Item Name 1": {"price_per_unit": 1.50, "unit_label": "mazzo", "currency": "{$currency}", "source_note": "..."},
"Item Name 2": {"price_per_unit": 2.80, "unit_label": "kg", "currency": "{$currency}", "source_note": "..."}
}
PROMPT;
$payload = ['contents' => [['parts' => [['text' => $prompt]]]]];
$result = callGeminiWithFallback($apiKey, $payload, 20);
// Allow more time for batch (max 45s)
$result = callGeminiWithFallback($apiKey, $payload, 45);
if ($result['http_code'] !== 200) return null;
if ($result['http_code'] !== 200) return [];
$text = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? '');
$text = preg_replace('/^```json\s*/i', '', $text);
$text = preg_replace('/\s*```$/i', '', $text);
$data = json_decode(trim($text), true);
if (!$data || !isset($data['price_per_unit'])) return null;
return $data;
if (!is_array($data)) return [];
// Validate and return only items with valid price
$out = [];
foreach ($data as $name => $entry) {
if (isset($entry['price_per_unit']) && is_numeric($entry['price_per_unit'])) {
$out[$name] = $entry;
}
}
return $out;
}
/**
@@ -6712,17 +6737,25 @@ function getAllShoppingPrices(PDO $db): void {
$missing[] = $item;
}
// Second pass: fetch missing from AI (sequential to avoid rate limits)
foreach ($missing as $item) {
$name = $item['name'];
// Second pass: fetch ALL missing items in ONE batch Gemini call
if (!empty($missing)) {
$missingNames = array_column($missing, 'name');
$batchPrices = _fetchPricesBatchFromAI($missingNames, $country, $currency, $lang);
// Build a lookup from item name → item params
$missingByName = [];
foreach ($missing as $item) $missingByName[$item['name']] = $item;
foreach ($missingNames as $name) {
$item = $missingByName[$name];
$qty = $item['quantity'];
$unit = $item['unit'];
$defQty = $item['default_quantity'];
$pkgUnit = $item['package_unit'];
$key = _priceKey($name, $country);
$priceData = _fetchPriceFromAI($name, $country, $currency, $lang);
if ($priceData && $priceData['price_per_unit'] !== null) {
$priceData = $batchPrices[$name] ?? null;
if ($priceData && isset($priceData['price_per_unit'])) {
$entry = [
'name' => $name,
'price_per_unit' => (float)$priceData['price_per_unit'],
@@ -6744,6 +6777,7 @@ function getAllShoppingPrices(PDO $db): void {
$prices[$name] = ['name' => $name, 'error' => 'not_found', 'estimated_total' => null];
}
}
}
_savePriceCache($priceCache);