Files
Cibello-app/packages/recipe-generation/scripts/pilot-batch.ts
T

158 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env tsx
/**
* PILOTBATCH Receptkatalog Fas A (docs/32).
*
* Genererar ~2030 recept över en skiva av matrisen:
* middag × mejeri/kyckling/köttfärs × allätare+veg
*
* Använder Gemini i staging/dev (AAMOS_MODE=gemini) eller mock (AAMOS_MODE=mock).
* Ingen påfyllning i prod. Resultatet skrivs till stdout som JSON + markdown.
*
* Körning:
* AAMOS_MODE=mock pnpm tsx packages/recipe-generation/scripts/pilot-batch.ts
* AAMOS_MODE=gemini GEMINI_API_KEY=... pnpm tsx packages/recipe-generation/scripts/pilot-batch.ts
*/
import { createAamosClient } from "@app/ai-contracts";
import { SEED_INGREDIENTS } from "@app/database/seed";
import { runPipeline, type PipelineTarget, type PipelineIngredient } from "@app/recipe-generation";
import type { CanonicalIngredientLookup, SimilarityLookup } from "@app/recipe-generation";
// ── 1. Bygg katalog från seed-data ─────────────────────────────────────────
const catalog: PipelineIngredient[] = SEED_INGREDIENTS.map((i) => ({
id: i.id,
nameSv: i.nameSv,
category: i.category,
defaultUnit: i.defaultUnit,
isVegan: i.isVegan,
isVegetarian: i.isVegetarian,
containsGluten: i.containsGluten,
containsLactose: i.containsLactose,
allergens: i.allergens,
}));
// ── 2. Bygg lookup för verifiering ─────────────────────────────────────────
const ingredientLookup: CanonicalIngredientLookup = {
getById(id: string) {
const ing = SEED_INGREDIENTS.find((i) => i.id === id);
if (!ing) return undefined;
return {
id: ing.id,
nutritionPer100: ing.nutritionPer100,
defaultUnit: ing.defaultUnit,
densityGPerMl: ing.densityGPerMl ?? null,
gramsPerPiece: ing.gramsPerPiece ?? null,
allergens: ing.allergens,
isVegan: ing.isVegan,
isVegetarian: ing.isVegetarian,
containsGluten: ing.containsGluten,
containsLactose: ing.containsLactose,
isPork: ing.isPork,
isBeef: ing.isBeef,
isAlcohol: ing.isAlcohol,
};
},
};
// ── 3. Dedup-lookup (enkel, baserad på titel) ──────────────────────────────
const knownTitles = new Set<string>();
const similarityLookup: SimilarityLookup = {
async hasSimilarity(title: string) {
if (knownTitles.has(title.toLowerCase())) return true;
knownTitles.add(title.toLowerCase());
return false;
},
};
// ── 4. Definiera pilot-matris ──────────────────────────────────────────────
const targets: PipelineTarget[] = [
// Mejeri × allätare
{ mealType: "dinner", mainIngredientId: "milk_3", dietVariant: "standard", count: 3 },
{ mealType: "dinner", mainIngredientId: "cream", dietVariant: "standard", count: 2 },
// Kyckling × allätare
{ mealType: "dinner", mainIngredientId: "chicken_breast", dietVariant: "standard", count: 4 },
{ mealType: "dinner", mainIngredientId: "chicken_thigh", dietVariant: "standard", count: 2 },
// Köttfärs × allätare
{ mealType: "dinner", mainIngredientId: "minced_beef", dietVariant: "standard", count: 3 },
{ mealType: "dinner", mainIngredientId: "minced_mixed", dietVariant: "standard", count: 2 },
// Veg-varianter
{ mealType: "dinner", mainIngredientId: "tofu", dietVariant: "vegan", count: 3 },
{ mealType: "dinner", mainIngredientId: "red_lentils", dietVariant: "vegetarian", count: 2 },
{ mealType: "dinner", mainIngredientId: "chickpeas_canned", dietVariant: "vegan", count: 2 },
];
// ── 5. Kör pipelinen ───────────────────────────────────────────────────────
async function main() {
const client = createAamosClient(process.env);
console.error("[pilot-batch] Startar generering...");
console.error(`[pilot-batch] Katalog: ${catalog.length} ingredienser`);
console.error(`[pilot-batch] Mål: ${targets.length} matris-celler, ~${targets.reduce((s, t) => s + t.count, 0)} recept`);
const result = await runPipeline(
client,
targets,
catalog,
ingredientLookup,
similarityLookup,
{ maxPrepTimeMinutes: 30, maxCookTimeMinutes: 45, portions: 4 },
);
// ── 6. Rapportera ────────────────────────────────────────────────────────
const report = {
batchId: `pilot-${Date.now()}`,
generatedAt: new Date().toISOString(),
targetMatrix: targets,
geminiStatus: result.geminiResult.status,
geminiCostUsd: result.geminiResult.costUsd ?? 0,
candidatesGenerated: result.candidates.length,
verifiedCount: result.verifiedCount,
unverifiedCount: result.unverifiedCount,
rejectedCount: result.rejectedCount,
candidates: result.verificationResults.map((r) => ({
title: r.candidate.titleSv,
descriptionSv: r.candidate.descriptionSv,
status: r.status,
reasons: r.reasons,
allergens: r.allergens,
nutritionPerPortion: r.nutritionPerPortion,
ingredients: r.candidate.ingredients.map((i) => ({
id: i.canonicalIngredientId,
name: i.displayNameSv,
quantity: i.quantity,
unit: i.unit,
optional: i.optional,
})),
steps: r.candidate.steps.map((s, idx) => ({
number: idx + 1,
instruction: s.instructionSv,
durationMinutes: s.durationMinutes ?? null,
})),
})),
};
console.log(JSON.stringify(report, null, 2));
// Markdown-sammanfattning till stderr
console.error("\n# Pilotbatch-sammanfattning\n");
console.error(`- Genererade kandidater: ${result.candidates.length}`);
console.error(`- Verified: ${result.verifiedCount}`);
console.error(`- Unverified: ${result.unverifiedCount}`);
console.error(`- Rejected: ${result.rejectedCount}`);
console.error(`- Gemini-kostnad: $${(result.geminiResult.costUsd ?? 0).toFixed(4)}`);
if (result.geminiResult.error) {
console.error(`\n**Fel:** ${result.geminiResult.error}`);
}
if (result.candidates.length === 0 && result.geminiResult.status === "ok") {
console.error("\n**Notering:** Gemini returnerade ok men inga kandidater. " +
"Mock-klienten stödjer inte GENERATE_RECIPE_CANDIDATES ännu — " +
"kör med AAMOS_MODE=gemini för live-generering.");
}
}
main().catch((err) => {
console.error("[pilot-batch] Fatal:", err);
process.exit(1);
});