perf: batch price fetch
This commit is contained in:
+82
-48
@@ -6497,44 +6497,69 @@ function _priceKey(string $name, string $country): string {
|
|||||||
* { price_per_unit, unit_label, currency, source_note } or null on failure.
|
* { price_per_unit, unit_label, currency, source_note } or null on failure.
|
||||||
*/
|
*/
|
||||||
function _fetchPriceFromAI(string $name, string $country, string $currency, string $lang): ?array {
|
function _fetchPriceFromAI(string $name, string $country, string $currency, string $lang): ?array {
|
||||||
$apiKey = env('GEMINI_API_KEY');
|
$result = _fetchPricesBatchFromAI([$name], $country, $currency, $lang);
|
||||||
if (empty($apiKey)) return null;
|
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
|
$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:
|
Items:
|
||||||
- Standard packages (pasta, flour, frozen food, yogurt, canned goods, biscuits): price per typical package (e.g. "pacco 500g", "barattolo 400g", "confezione")
|
{$list}
|
||||||
- Sold by piece or bunch (fresh herbs, eggs, individual fruit/vegetables, single portions): price per piece/bunch (e.g. "mazzo", "uovo", "pz")
|
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")
|
- Liquids in bottles or cartons: price per typical container (e.g. "bottiglia 1L", "brick 1L")
|
||||||
- Deli items sold loose by weight: price per kg
|
- Deli items sold loose by weight: price per kg
|
||||||
|
|
||||||
Rules:
|
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.
|
2. Round to 2 decimal places.
|
||||||
3. NEVER return per-kg for items normally sold in packages or by the piece.
|
3. NEVER use per-kg for items normally sold in packages or by piece.
|
||||||
4. ALWAYS return your best estimate — even for generic or unusual items. Use a typical grocery item if uncertain.
|
4. ALWAYS return a best estimate — even for branded or unusual items. Use the closest generic equivalent if needed.
|
||||||
5. Respond ONLY with valid JSON — no markdown, no explanation:
|
5. Respond ONLY with a valid JSON object keyed by the EXACT item name from the list above. No markdown, no explanation:
|
||||||
{"price_per_unit": 1.50, "unit_label": "mazzo", "currency": "{$currency}", "source_note": "Basilico fresco ~€1.50/mazzo in {$country}"}
|
{
|
||||||
|
"Item Name 1": {"price_per_unit": 1.50, "unit_label": "mazzo", "currency": "{$currency}", "source_note": "..."},
|
||||||
If you are genuinely unsure, return a rough estimate for 1 typical package with a "~" in source_note:
|
"Item Name 2": {"price_per_unit": 2.80, "unit_label": "kg", "currency": "{$currency}", "source_note": "..."}
|
||||||
{"price_per_unit": 2.00, "unit_label": "confezione", "currency": "{$currency}", "source_note": "~ stima generica confezione in {$country}"}
|
}
|
||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
$payload = ['contents' => [['parts' => [['text' => $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 = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? '');
|
||||||
$text = preg_replace('/^```json\s*/i', '', $text);
|
$text = preg_replace('/^```json\s*/i', '', $text);
|
||||||
$text = preg_replace('/\s*```$/i', '', $text);
|
$text = preg_replace('/\s*```$/i', '', $text);
|
||||||
$data = json_decode(trim($text), true);
|
$data = json_decode(trim($text), true);
|
||||||
|
|
||||||
if (!$data || !isset($data['price_per_unit'])) return null;
|
if (!is_array($data)) return [];
|
||||||
return $data;
|
|
||||||
|
// 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;
|
$missing[] = $item;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: fetch missing from AI (sequential to avoid rate limits)
|
// Second pass: fetch ALL missing items in ONE batch Gemini call
|
||||||
foreach ($missing as $item) {
|
if (!empty($missing)) {
|
||||||
$name = $item['name'];
|
$missingNames = array_column($missing, 'name');
|
||||||
$qty = $item['quantity'];
|
$batchPrices = _fetchPricesBatchFromAI($missingNames, $country, $currency, $lang);
|
||||||
$unit = $item['unit'];
|
|
||||||
$defQty = $item['default_quantity'];
|
|
||||||
$pkgUnit = $item['package_unit'];
|
|
||||||
$key = _priceKey($name, $country);
|
|
||||||
|
|
||||||
$priceData = _fetchPriceFromAI($name, $country, $currency, $lang);
|
// Build a lookup from item name → item params
|
||||||
if ($priceData && $priceData['price_per_unit'] !== null) {
|
$missingByName = [];
|
||||||
$entry = [
|
foreach ($missing as $item) $missingByName[$item['name']] = $item;
|
||||||
'name' => $name,
|
|
||||||
'price_per_unit' => (float)$priceData['price_per_unit'],
|
foreach ($missingNames as $name) {
|
||||||
'unit_label' => $priceData['unit_label'] ?? 'pz',
|
$item = $missingByName[$name];
|
||||||
'currency' => $currency,
|
$qty = $item['quantity'];
|
||||||
'source_note' => $priceData['source_note'] ?? '',
|
$unit = $item['unit'];
|
||||||
'country' => $country,
|
$defQty = $item['default_quantity'];
|
||||||
'cached_at' => $now,
|
$pkgUnit = $item['package_unit'];
|
||||||
];
|
$key = _priceKey($name, $country);
|
||||||
$priceCache[$key] = $entry;
|
|
||||||
$est = _calcEstimatedTotal($entry['price_per_unit'], $entry['unit_label'], $qty, $unit, $defQty, $pkgUnit);
|
$priceData = $batchPrices[$name] ?? null;
|
||||||
$prices[$name] = array_merge($entry, [
|
if ($priceData && isset($priceData['price_per_unit'])) {
|
||||||
'estimated_total' => $est,
|
$entry = [
|
||||||
'estimated_total_label' => _formatPrice($est, $currency),
|
'name' => $name,
|
||||||
'from_cache' => false,
|
'price_per_unit' => (float)$priceData['price_per_unit'],
|
||||||
]);
|
'unit_label' => $priceData['unit_label'] ?? 'pz',
|
||||||
$total += $est ?? 0;
|
'currency' => $currency,
|
||||||
} else {
|
'source_note' => $priceData['source_note'] ?? '',
|
||||||
$prices[$name] = ['name' => $name, 'error' => 'not_found', 'estimated_total' => null];
|
'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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user