From 7a51a44b8671c6178812a78c0b79482e9162ad89 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Thu, 7 May 2026 20:28:58 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20batch=20Gemini=20price=20fetch=20?= =?UTF-8?q?=E2=80=94=201=20call=20for=20all=20missing=20items=20instead=20?= =?UTF-8?q?of=20N?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/index.php | 130 +++++++++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/api/index.php b/api/index.php index cfc8ee4..a51ca7d 100644 --- a/api/index.php +++ b/api/index.php @@ -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 = << [['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,36 +6737,45 @@ 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']; - $qty = $item['quantity']; - $unit = $item['unit']; - $defQty = $item['default_quantity']; - $pkgUnit = $item['package_unit']; - $key = _priceKey($name, $country); + // 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); - $priceData = _fetchPriceFromAI($name, $country, $currency, $lang); - if ($priceData && $priceData['price_per_unit'] !== null) { - $entry = [ - 'name' => $name, - 'price_per_unit' => (float)$priceData['price_per_unit'], - 'unit_label' => $priceData['unit_label'] ?? 'pz', - 'currency' => $currency, - 'source_note' => $priceData['source_note'] ?? '', - 'country' => $country, - 'cached_at' => $now, - ]; - $priceCache[$key] = $entry; - $est = _calcEstimatedTotal($entry['price_per_unit'], $entry['unit_label'], $qty, $unit, $defQty, $pkgUnit); - $prices[$name] = array_merge($entry, [ - 'estimated_total' => $est, - 'estimated_total_label' => _formatPrice($est, $currency), - 'from_cache' => false, - ]); - $total += $est ?? 0; - } else { - $prices[$name] = ['name' => $name, 'error' => 'not_found', 'estimated_total' => null]; + // 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 = $batchPrices[$name] ?? null; + if ($priceData && isset($priceData['price_per_unit'])) { + $entry = [ + 'name' => $name, + 'price_per_unit' => (float)$priceData['price_per_unit'], + 'unit_label' => $priceData['unit_label'] ?? 'pz', + 'currency' => $currency, + 'source_note' => $priceData['source_note'] ?? '', + 'country' => $country, + 'cached_at' => $now, + ]; + $priceCache[$key] = $entry; + $est = _calcEstimatedTotal($entry['price_per_unit'], $entry['unit_label'], $qty, $unit, $defQty, $pkgUnit); + $prices[$name] = array_merge($entry, [ + 'estimated_total' => $est, + 'estimated_total_label' => _formatPrice($est, $currency), + 'from_cache' => false, + ]); + $total += $est ?? 0; + } else { + $prices[$name] = ['name' => $name, 'error' => 'not_found', 'estimated_total' => null]; + } } }