fix: reduce finished-banner to only products with unexpected zero balance
Of the 75 qty=0 rows restored in previous commit, delete the 66 where total_in - total_out <= unit threshold (legitimately finished by user). Keep only 9 products where transaction math says there should still be stock but inventory hit 0 (likely system/scale error): - Latte di Montagna (0.41 conf) - Passata di pomodoro (692g) - Carote (80g), mele (6pz), Uova biologiche (1pz) - Cipolla dorata (496pz), Panna da cucina (0.6 conf) - Gran bauletto integrale e noci (448 conf), il tuo muesli mountain (331g) These 9 will appear in the banner asking user to verify.
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CLI Test for generateRecipeStream
|
||||||
|
* Tests: prompt token reduction (B), SSE output format, model fallback (C)
|
||||||
|
* Run: php test_recipe_stream.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
define('CRON_MODE', true); // skip HTTP routing
|
||||||
|
require_once __DIR__ . '/api/database.php';
|
||||||
|
require_once __DIR__ . '/api/index.php';
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
function pass(string $msg): void { echo "\033[32m✓\033[0m $msg\n"; }
|
||||||
|
function fail(string $msg): void { echo "\033[31m✗\033[0m $msg\n"; }
|
||||||
|
function info(string $msg): void { echo "\033[33m→\033[0m $msg\n"; }
|
||||||
|
|
||||||
|
// ── TEST 1: API key present ───────────────────────────────────────────────────
|
||||||
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
|
if (!empty($apiKey)) {
|
||||||
|
pass("API key set (" . substr($apiKey, 0, 8) . "...)");
|
||||||
|
} else {
|
||||||
|
fail("API key missing in .env");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TEST 2: DB reachable + has inventory ─────────────────────────────────────
|
||||||
|
$db = getDB();
|
||||||
|
$itemCount = $db->query("SELECT count(*) FROM inventory WHERE quantity > 0")->fetchColumn();
|
||||||
|
if ($itemCount > 0) {
|
||||||
|
pass("Inventory: $itemCount items");
|
||||||
|
} else {
|
||||||
|
fail("Inventory is empty — cannot generate recipe");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TEST 3: Prompt token estimation (B) ─────────────────────────────────────
|
||||||
|
// Simulate building the ingredient list with new limits
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, i.opened_at,
|
||||||
|
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
|
||||||
|
FROM inventory i
|
||||||
|
JOIN products p ON p.id = i.product_id
|
||||||
|
WHERE i.quantity > 0 ORDER BY days_left ASC
|
||||||
|
");
|
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$getItemPriority = function($item): int {
|
||||||
|
$daysLeft = floatval($item['days_left']);
|
||||||
|
$isOpen = !empty($item['opened_at']) || (floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf');
|
||||||
|
if (!empty($item['expiry_date']) && $daysLeft < 0) return 1;
|
||||||
|
if (!empty($item['expiry_date']) && $daysLeft <= 3) return 2;
|
||||||
|
if (!empty($item['expiry_date']) && $daysLeft <= 7) return 3;
|
||||||
|
if (!empty($item['expiry_date'])) return 4;
|
||||||
|
if ($isOpen) return 5;
|
||||||
|
return 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
$staplePatterns = '/\b(sale|pepe|olio d.oliva|olio di semi|olio extra|acqua|aceto balsamico|aceto di|sel marin)\b/i';
|
||||||
|
$priorityGroups = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$group = $getItemPriority($item);
|
||||||
|
if ($group >= 5 && preg_match($staplePatterns, $item['name'])) continue;
|
||||||
|
$line = "- {$item['name']}: {$item['quantity']} {$item['unit']}";
|
||||||
|
$priorityGroups[$group][] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OLD limits
|
||||||
|
$oldSections = [];
|
||||||
|
foreach ([1=>null,2=>null,3=>null,4=>40,5=>null,6=>20] as $g => $limit) {
|
||||||
|
if (empty($priorityGroups[$g])) continue;
|
||||||
|
$gi = $limit ? array_slice($priorityGroups[$g], 0, $limit) : $priorityGroups[$g];
|
||||||
|
$oldSections[] = implode("\n", $gi);
|
||||||
|
}
|
||||||
|
$oldText = implode("\n", $oldSections);
|
||||||
|
$oldTokens = (int)(str_word_count($oldText) * 1.3); // rough estimate: words * 1.3
|
||||||
|
|
||||||
|
// NEW limits
|
||||||
|
$newSections = [];
|
||||||
|
foreach ([1=>null,2=>null,3=>null,4=>15,5=>null,6=>8] as $g => $limit) {
|
||||||
|
if (empty($priorityGroups[$g])) continue;
|
||||||
|
$gi = $limit ? array_slice($priorityGroups[$g], 0, $limit) : $priorityGroups[$g];
|
||||||
|
$newSections[] = implode("\n", $gi);
|
||||||
|
}
|
||||||
|
$newText = implode("\n", $newSections);
|
||||||
|
$newTokens = (int)(str_word_count($newText) * 1.3);
|
||||||
|
$savings = $oldTokens > 0 ? round(($oldTokens - $newTokens) / $oldTokens * 100) : 0;
|
||||||
|
|
||||||
|
info("Prompt ingredient tokens: OLD ~$oldTokens → NEW ~$newTokens (saved ~$savings%)");
|
||||||
|
if ($savings >= 20) {
|
||||||
|
pass("Token reduction >= 20% (got $savings%)");
|
||||||
|
} else {
|
||||||
|
fail("Token reduction too low ($savings%) — check group limits");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TEST 4: Real SSE call via HTTP ───────────────────────────────────────────
|
||||||
|
info("Calling generate_recipe_stream via HTTP (cena, pesce, 2 persone)...");
|
||||||
|
|
||||||
|
$postData = json_encode([
|
||||||
|
'meal' => 'cena',
|
||||||
|
'persons' => 2,
|
||||||
|
'sub_type' => '',
|
||||||
|
'options' => [],
|
||||||
|
'appliances' => [],
|
||||||
|
'dietary_restrictions' => '',
|
||||||
|
'today_recipes' => [],
|
||||||
|
'meal_plan_type' => 'pesce',
|
||||||
|
'variation' => 0,
|
||||||
|
'rejected_ingredients' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$ch = curl_init('https://localhost/dispensa/api/index.php?action=generate_recipe_stream');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $postData,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 120,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => false,
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlErr = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
$elapsed = round(microtime(true) - $startTime, 1);
|
||||||
|
|
||||||
|
if ($curlErr) {
|
||||||
|
fail("curl error: $curlErr");
|
||||||
|
// Try via PHP CLI directly instead
|
||||||
|
info("Trying direct PHP execution instead...");
|
||||||
|
// Simulate SSE output capture
|
||||||
|
ob_start();
|
||||||
|
$_GET['action'] = 'generate_recipe_stream';
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||||
|
// Override php://input
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'recipe_test_');
|
||||||
|
file_put_contents($tmpFile, $postData);
|
||||||
|
// Can't easily override php://input in CLI, skip HTTP test
|
||||||
|
ob_end_clean();
|
||||||
|
info("HTTP test skipped (no web server on localhost) — checking SSE parsing only");
|
||||||
|
$response = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response !== null) {
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
fail("HTTP status $httpCode (expected 200)");
|
||||||
|
} else {
|
||||||
|
pass("HTTP 200 in {$elapsed}s");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse SSE events
|
||||||
|
$events = [];
|
||||||
|
foreach (explode("\n", $response) as $line) {
|
||||||
|
if (strpos($line, 'data: ') === 0) {
|
||||||
|
$evt = json_decode(substr($line, 6), true);
|
||||||
|
if ($evt) $events[] = $evt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info("SSE events received: " . count($events));
|
||||||
|
foreach ($events as $evt) {
|
||||||
|
$type = $evt['type'] ?? '?';
|
||||||
|
$msg = $evt['message'] ?? $evt['error'] ?? json_encode($evt);
|
||||||
|
info(" [$type] $msg");
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusEvents = array_filter($events, fn($e) => ($e['type'] ?? '') === 'status');
|
||||||
|
$recipeEvents = array_filter($events, fn($e) => ($e['type'] ?? '') === 'recipe');
|
||||||
|
$errorEvents = array_filter($events, fn($e) => ($e['type'] ?? '') === 'error');
|
||||||
|
|
||||||
|
if (!empty($errorEvents)) {
|
||||||
|
$err = reset($errorEvents);
|
||||||
|
$errMsg = $err['error'] ?? 'unknown';
|
||||||
|
$errDetail = $err['detail'] ?? '';
|
||||||
|
$errCode = $err['http_code'] ?? '';
|
||||||
|
fail("Got error event: $errMsg | code=$errCode | $errDetail");
|
||||||
|
} elseif (!empty($recipeEvents)) {
|
||||||
|
$recipe = reset($recipeEvents)['recipe'] ?? [];
|
||||||
|
pass("Got recipe: \"" . ($recipe['title'] ?? '?') . "\"");
|
||||||
|
|
||||||
|
// Verify steps exist
|
||||||
|
if (!empty($recipe['steps']) && count($recipe['steps']) >= 2) {
|
||||||
|
pass("Recipe has " . count($recipe['steps']) . " steps");
|
||||||
|
} else {
|
||||||
|
fail("Recipe missing steps");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify meal type
|
||||||
|
if (($recipe['meal'] ?? '') === 'cena') {
|
||||||
|
pass("Meal type correct (cena)");
|
||||||
|
} else {
|
||||||
|
fail("Meal type wrong: " . ($recipe['meal'] ?? 'missing'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check steps count
|
||||||
|
if (count($statusEvents) >= 3) {
|
||||||
|
pass("Got " . count($statusEvents) . " status events (agent steps working)");
|
||||||
|
} else {
|
||||||
|
fail("Too few status events: " . count($statusEvents));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fail("No recipe and no error event in SSE response");
|
||||||
|
echo "Raw response (first 500 chars):\n" . substr($response, 0, 500) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n\033[1mDone.\033[0m\n";
|
||||||
Reference in New Issue
Block a user