Files
Cibello-app/packages/ai-contracts/src/mock.ts
T

338 lines
10 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.
import type { AamosTaskType } from "./tasks.js";
/**
* Deterministiska mock-svar per uppgiftstyp. Realistiska nog för att bygga
* UI-flöden och tester mot inklusive osäkerhetsfall (requiresConfirmation,
* lågt confidence) så att korrigerings-UX:et alltid övas (spec §61.45).
*/
export function mockOutputFor(taskType: AamosTaskType, input: unknown): unknown {
switch (taskType) {
case "ANALYZE_FRIDGE_IMAGE":
case "ANALYZE_PANTRY_IMAGE":
return {
items: [
{
detectedName: "mjölk",
canonicalIngredientId: "milk_3",
brand: "Arla",
estimatedQuantity: 0.7,
unit: "LITER",
bestBeforeDate: null,
confidence: 0.84,
requiresConfirmation: true,
boundingBox: null,
},
{
detectedName: "crème fraîche",
canonicalIngredientId: "creme_fraiche",
brand: null,
estimatedQuantity: 1,
unit: "COUNT",
bestBeforeDate: null,
confidence: 0.61,
requiresConfirmation: true,
boundingBox: null,
},
{
detectedName: "okänd burk",
canonicalIngredientId: null,
brand: null,
estimatedQuantity: null,
unit: null,
bestBeforeDate: null,
confidence: 0.2,
requiresConfirmation: true,
boundingBox: null,
},
],
imageQualityIssues: [],
};
case "ANALYZE_MEAL_IMAGE": {
const hasContext =
typeof input === "object" &&
input !== null &&
(input as { recipeContext?: unknown }).recipeContext != null;
return {
matchesRecipeContext: hasContext ? true : null,
portionFractionEstimate: 1,
kcalRange: { min: 650, max: 750, mostLikely: 700 },
components: [
{
name: "kycklingfilé",
canonicalIngredientId: "chicken_breast",
estimatedGrams: 150,
confidence: 0.8,
},
{
name: "ris",
canonicalIngredientId: "rice_white",
estimatedGrams: 180,
confidence: 0.75,
},
],
confidence: 0.72,
};
}
case "READ_RECEIPT":
return {
storeName: "ICA Supermarket",
purchaseDate: "2026-08-01",
lines: [
{
rawText: "KYCKL FILE 925G",
normalizedName: "Kycklingfilé 925 g",
canonicalIngredientId: "chicken_breast",
quantity: 925,
unit: "GRAM",
unitPriceMinor: 11990,
totalPriceMinor: 11990,
isDiscount: false,
confidence: 0.9,
},
{
rawText: "MELLANMJ 1.5L",
normalizedName: "Mellanmjölk 1,5 l",
canonicalIngredientId: "milk_1_5",
quantity: 1.5,
unit: "LITER",
unitPriceMinor: 1890,
totalPriceMinor: 1890,
isDiscount: false,
confidence: 0.87,
},
],
totalMinor: 13880,
discountTotalMinor: 0,
confidence: 0.85,
};
case "READ_NUTRITION_LABEL":
return {
basis: "per_100_g",
values: {
kcal: 106,
proteinG: 22,
carbsG: 0,
fatG: 2,
saturatedFatG: 0.6,
fiberG: 0,
sugarG: 0,
saltG: 0.2,
},
ingredientsText: "Kycklingfilé (100 %)",
allergensDeclared: [],
gtin: null,
productName: "Kycklingfilé",
brand: null,
confidence: 0.88,
};
case "READ_EXPIRY_DATE":
return { date: "2026-08-06", dateKind: "best_before", confidence: 0.9 };
case "NORMALIZE_PRODUCTS": {
const rawNames =
typeof input === "object" && input !== null
? ((input as { rawNames?: string[] }).rawNames ?? [])
: [];
return {
matches: rawNames.map((raw) => ({
raw,
canonicalIngredientId: null,
normalizedName: raw.toLowerCase(),
confidence: 0.4,
})),
};
}
case "DEDUPLICATE_INVENTORY":
return { duplicateGroups: [] };
case "STRUCTURE_RECIPE_TEXT":
return {
titleSv: "Snabb kycklingpasta",
descriptionSv: "Krämig vardagspasta med kyckling och spenat.",
ingredients: [
{
rawText: "400 g kycklingfilé",
canonicalIngredientId: "chicken_breast",
displayNameSv: "Kycklingfilé",
quantity: 400,
unit: "GRAM",
optional: false,
confidence: 0.9,
},
{
rawText: "300 g pasta",
canonicalIngredientId: "pasta_dry",
displayNameSv: "Pasta",
quantity: 300,
unit: "GRAM",
optional: false,
confidence: 0.9,
},
],
steps: [
{ instructionSv: "Koka pastan enligt anvisning.", timerSeconds: 600, temperatureC: null },
{
instructionSv: "Stek kycklingen tills genomstekt.",
timerSeconds: null,
temperatureC: null,
},
],
prepTimeMinutes: 10,
cookTimeMinutes: 15,
portions: 4,
suggestedCuisine: "italian",
suggestedMealTypes: ["dinner"],
confidence: 0.82,
};
case "GENERATE_RECIPE_OPTIONS":
return {
suggestions: [
{
titleSv: "Krämig kycklingwok med grönsaker",
descriptionSv: "Snabb wok på det som finns hemma.",
ingredients: [
{
canonicalIngredientId: "chicken_breast",
displayNameSv: "Kycklingfilé",
quantity: 400,
unit: "GRAM",
},
{
canonicalIngredientId: "rice_white",
displayNameSv: "Ris",
quantity: 3,
unit: "DECILITER",
},
],
steps: ["Koka riset.", "Woka kycklingen.", "Blanda och servera."],
estimatedTimeMinutes: 25,
confidence: 0.7,
},
],
};
case "GENERATE_RECIPE_CANDIDATES": {
// Deterministisk mock: returnerar en enkel kandidat per target-cell
const inp = input as {
targetMatrix?: Array<{ mealType: string; mainIngredientId: string; dietVariant: string; count: number }>;
canonicalIngredientsCatalog?: Array<{ id: string; nameSv: string; category: string; defaultUnit: string }>;
};
const targets = inp.targetMatrix ?? [];
const catalog = inp.canonicalIngredientsCatalog ?? [];
const candidates = [];
for (const t of targets) {
const mainIng = catalog.find((c) => c.id === t.mainIngredientId);
for (let i = 0; i < Math.min(t.count, 2); i++) {
candidates.push({
titleSv: `Mock-${mainIng?.nameSv ?? t.mainIngredientId} ${t.dietVariant} ${i + 1}`,
descriptionSv: `Ett enkelt vardagsrecept med ${mainIng?.nameSv ?? t.mainIngredientId}.`,
cuisine: "swedish",
mealTypes: [t.mealType],
prepTimeMinutes: 10,
cookTimeMinutes: 20,
portions: 4,
spiceLevel: 1,
ingredients: [
{
canonicalIngredientId: t.mainIngredientId,
displayNameSv: mainIng?.nameSv ?? t.mainIngredientId,
quantity: 500,
unit: mainIng?.defaultUnit ?? "GRAM",
optional: false,
note: null,
},
{
canonicalIngredientId: "onion",
displayNameSv: "gul lök",
quantity: 2,
unit: "COUNT",
optional: false,
note: null,
},
{
canonicalIngredientId: "rice_white",
displayNameSv: "ris",
quantity: 3,
unit: "DECILITER",
optional: false,
note: null,
},
],
steps: [
{ instructionSv: "Förbered ingredienserna.", timerSeconds: null, temperatureC: null, tip: null },
{ instructionSv: "Stek och låt koka klart.", timerSeconds: 900, temperatureC: null, tip: null },
],
storageGuidanceSv: "Förvara i kylskåp upp till 3 dagar.",
mealPrepFriendly: false,
freezerFriendly: false,
confidence: 0.85,
});
}
}
return { candidates, rejectedPrompts: [] };
}
case "RANK_RECIPES": {
const ids =
typeof input === "object" && input !== null
? ((input as { candidateIds?: string[] }).candidateIds ?? [])
: [];
return { rankedIds: ids, rationaleSv: null };
}
case "PARSE_CRAVING":
return { tags: ["creamy"], cuisine: null, maxKcal: null, confidence: 0.6 };
case "UPDATE_USER_MEMORY":
return { memoryUpdates: [] };
case "GENERATE_WEEK_PLAN":
return { entries: [], confidence: 0.5 };
case "MODERATE_RECIPE":
return { flags: [], recommendation: "approve", confidence: 0.9 };
case "EXPORT_TRAINING_SAMPLE": {
const samples =
typeof input === "object" && input !== null
? ((input as { samples?: unknown[] }).samples ?? [])
: [];
return {
batchId: `mock-batch-${Date.now()}`,
accepted: samples.length,
rejected: 0,
};
}
case "TRANSLATE_RECIPE": {
// Deterministisk pseudo-översättning: bevarar alla tal och stegstruktur,
// markerar texten med målspråket så flödet är testbart utan riktig AI.
const inp = input as {
targetLanguageTag: string;
title: string;
description: string | null;
storageGuidance: string | null;
steps: { stepNumber: number; instruction: string; tip: string | null }[];
};
const tag = `[${inp.targetLanguageTag}]`;
return {
title: `${tag} ${inp.title}`,
description: inp.description ? `${tag} ${inp.description}` : null,
storageGuidance: inp.storageGuidance ? `${tag} ${inp.storageGuidance}` : null,
steps: inp.steps.map((s) => ({
stepNumber: s.stepNumber,
instruction: `${tag} ${s.instruction}`,
tip: s.tip ? `${tag} ${s.tip}` : null,
})),
confidence: 0.85,
};
}
}
}