S6: i18n-paritet, provenans + eval, copy-skanning, fabricerade-minnes-eval
This commit is contained in:
@@ -97,8 +97,18 @@ export async function deriveMemoryUpdates(
|
||||
return { proposals: [], usage };
|
||||
}
|
||||
|
||||
const validEventIds = new Set(input.events.map((e) => e.id));
|
||||
const groundedProposals = parsed.data.memoryUpdates.filter((u) => {
|
||||
const grounded = u.sourceEventIds.length > 0 && u.sourceEventIds.every((id) => validEventIds.has(id));
|
||||
if (!grounded) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`UPDATE_USER_MEMORY avvisade fabricerat minne: key=${u.key}, sourceEventIds=[${u.sourceEventIds.join(", ")}]`);
|
||||
}
|
||||
return grounded;
|
||||
});
|
||||
|
||||
return {
|
||||
proposals: parsed.data.memoryUpdates.map((u) => ({
|
||||
proposals: groundedProposals.map((u) => ({
|
||||
key: u.key,
|
||||
kind: u.kind,
|
||||
summarySv: u.summarySv,
|
||||
@@ -295,6 +305,448 @@ const UI_LABELS: Record<
|
||||
},
|
||||
};
|
||||
|
||||
type MemoryValueRenderer = (value: Record<string, unknown>) => string | null;
|
||||
|
||||
const MEMORY_SUMMARY_TEMPLATES: Record<
|
||||
string,
|
||||
Record<string, MemoryValueRenderer>
|
||||
> = {
|
||||
sv: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Favoritkök: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Undviker ingrediens: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Mål: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Primärt mål: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergi: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Max styrka: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Undviker: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Laggar vanligen ${v.typicalPortions} portioner`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Styrkepreferens: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
en: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Favorite cuisine: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Avoids ingredient: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Goal: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Primary goal: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergy: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Max spice level: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Avoids: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Usually cooks ${v.typicalPortions} servings`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Spice preference: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
es: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Cocina favorita: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Evita ingrediente: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Objetivo: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Objetivo principal: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Alergia: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Nivel máximo de picante: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Evita: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Suele cocinar ${v.typicalPortions} raciones`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Preferencia de picante: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
it: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Cucina preferita: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Evita ingrediente: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Obiettivo: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Obiettivo principale: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergia: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Livello piccante max: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Evita: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Di solito cucina ${v.typicalPortions} porzioni`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Preferenza piccante: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
de: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Lieblingsküche: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Vermeidet Zutat: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Ziel: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Hauptziel: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergie: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Max. Schärfe: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Vermeidet: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Kocht meist ${v.typicalPortions} Portionen`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Schärfepräferenz: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
fr: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Cuisine préférée: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Évite l'ingrédient: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Objectif: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Objectif principal: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergie: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Niveau épicé max: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Évite: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Cuisine habituellement ${v.typicalPortions} portions`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Préférence épicée: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
da: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Yndlingskøkken: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Undgår ingrediens: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Mål: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Primært mål: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergi: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Max styrke: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Undgår: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Tilbereder som regel ${v.typicalPortions} portioner`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Styrkepræference: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
nb: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Favorittkjøkken: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Unngår ingrediens: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Mål: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Primært mål: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergi: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Maks styrke: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Unngår: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Lager vanligvis ${v.typicalPortions} porsjoner`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Styrkepreferanse: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
fi: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Suosikkikeittiö: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Vältettävä ainesosa: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Tavoite: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Päätavoite: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergia: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Max tulisuus: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Vältää: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Valmistaa yleensä ${v.typicalPortions} annosta`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Tulisuusmieltymys: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
nl: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Favoriete keuken: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Vermijdt ingrediënt: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Doel: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Primair doel: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Allergie: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Max pittigheid: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Vermijdt: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Kookt meestal ${v.typicalPortions} porties`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Pittigheidspreferentie: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
pl: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Ulubiona kuchnia: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Unika składnika: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Cel: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Cel główny: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Alergia: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Maks. ostrość: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Unika: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Zwykle gotuje ${v.typicalPortions} porcje`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Preferencja ostrości: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
pt: {
|
||||
favoriteCuisine: (v) =>
|
||||
typeof v.favoriteCuisine === "string"
|
||||
? `Cozinha favorita: ${String(v.favoriteCuisine)}`
|
||||
: null,
|
||||
avoidIngredient: (v) =>
|
||||
typeof v.avoidIngredientId === "string"
|
||||
? `Evita ingrediente: ${String(v.avoidIngredientId)}`
|
||||
: null,
|
||||
goal: (v) =>
|
||||
typeof v.goal === "string" ? `Objetivo: ${String(v.goal)}` : null,
|
||||
primaryGoal: (v) =>
|
||||
typeof v.primaryGoal === "string"
|
||||
? `Objetivo principal: ${String(v.primaryGoal)}`
|
||||
: null,
|
||||
allergen: (v) =>
|
||||
typeof v.allergen === "string"
|
||||
? `Alergia: ${String(v.allergen)}`
|
||||
: null,
|
||||
spiceLevelMax: (v) =>
|
||||
typeof v.spiceLevelMax === "number"
|
||||
? `Nível picante máx.: ${v.spiceLevelMax}`
|
||||
: null,
|
||||
dislikedIngredient: (v) =>
|
||||
typeof v.dislikedIngredient === "string"
|
||||
? `Evita: ${String(v.dislikedIngredient)}`
|
||||
: null,
|
||||
typicalPortions: (v) =>
|
||||
typeof v.typicalPortions === "number"
|
||||
? `Costuma cozinhar ${v.typicalPortions} porções`
|
||||
: null,
|
||||
spicePreference: (v) =>
|
||||
typeof v.spicePreference === "string"
|
||||
? `Preferência picante: ${String(v.spicePreference)}`
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
const SUPPORTED_MEMORY_LANGS = Object.keys(MEMORY_SUMMARY_TEMPLATES);
|
||||
|
||||
/**
|
||||
* Deterministisk rendering av summary ur strukturerad value (i18n-spec §23, M10).
|
||||
* Kända value-former renderas per språk; okända faller tillbaka på summarySv
|
||||
@@ -304,41 +756,55 @@ export function renderMemorySummary(
|
||||
item: Pick<MemoryItem, "summarySv" | "value">,
|
||||
languageTag: string,
|
||||
): string {
|
||||
const lang = languageTag.split("-")[0] ?? "sv";
|
||||
if (lang === "sv") return item.summarySv;
|
||||
const lang = (languageTag.split("-")[0] ?? "sv").toLowerCase();
|
||||
const catalog = MEMORY_SUMMARY_TEMPLATES[lang] ?? MEMORY_SUMMARY_TEMPLATES.sv!;
|
||||
const v = item.value as Record<string, unknown> | null | undefined;
|
||||
if (v && typeof v === "object") {
|
||||
if (typeof v.favoriteCuisine === "string") {
|
||||
return `Favorite cuisine: ${String(v.favoriteCuisine)}`;
|
||||
if (typeof v.favoriteCuisine === "string" && catalog.favoriteCuisine) {
|
||||
const rendered = catalog.favoriteCuisine(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.dislikedIngredient === "string") {
|
||||
return `Avoids: ${String(v.dislikedIngredient)}`;
|
||||
if (typeof v.avoidIngredientId === "string" && catalog.avoidIngredient) {
|
||||
const rendered = catalog.avoidIngredient(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.typicalPortions === "number") {
|
||||
return `Usually cooks ${v.typicalPortions} servings`;
|
||||
if (typeof v.goal === "string" && catalog.goal) {
|
||||
const rendered = catalog.goal(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.spicePreference === "string") {
|
||||
return `Spice preference: ${String(v.spicePreference)}`;
|
||||
if (typeof v.primaryGoal === "string" && catalog.primaryGoal) {
|
||||
const rendered = catalog.primaryGoal(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.goal === "string") {
|
||||
return `Goal: ${String(v.goal)}`;
|
||||
if (typeof v.allergen === "string" && catalog.allergen) {
|
||||
const rendered = catalog.allergen(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.primaryGoal === "string") {
|
||||
return `Primary goal: ${String(v.primaryGoal)}`;
|
||||
if (typeof v.spiceLevelMax === "number" && catalog.spiceLevelMax) {
|
||||
const rendered = catalog.spiceLevelMax(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.allergen === "string") {
|
||||
return `Allergy: ${String(v.allergen)}`;
|
||||
if (typeof v.dislikedIngredient === "string" && catalog.dislikedIngredient) {
|
||||
const rendered = catalog.dislikedIngredient(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.avoidIngredientId === "string") {
|
||||
return `Avoids ingredient: ${String(v.avoidIngredientId)}`;
|
||||
if (typeof v.typicalPortions === "number" && catalog.typicalPortions) {
|
||||
const rendered = catalog.typicalPortions(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
if (typeof v.spiceLevelMax === "number") {
|
||||
return `Max spice level: ${v.spiceLevelMax}`;
|
||||
if (typeof v.spicePreference === "string" && catalog.spicePreference) {
|
||||
const rendered = catalog.spicePreference(v);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
}
|
||||
return item.summarySv;
|
||||
}
|
||||
|
||||
/** Språk som stöds av renderMemorySummary. */
|
||||
export function supportedMemorySummaryLanguages(): string[] {
|
||||
return [...SUPPORTED_MEMORY_LANGS];
|
||||
}
|
||||
|
||||
export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): MemoryOverview {
|
||||
const lang = languageTag.split("-")[0] ?? "sv";
|
||||
const titles = KIND_TITLES[lang] ?? KIND_TITLES.sv!;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildMemoryOverview } from "../src/index.js";
|
||||
import { buildMemoryOverview, deriveMemoryUpdates, renderMemorySummary } from "../src/index.js";
|
||||
import type { AamosClient, AamosResult } from "@app/ai-contracts";
|
||||
import type { AamosTaskType } from "@app/ai-contracts";
|
||||
import type { MemoryItem } from "@app/shared-types";
|
||||
|
||||
function makeItem(overrides: Partial<MemoryItem> = {}): MemoryItem {
|
||||
@@ -79,4 +81,86 @@ describe("buildMemoryOverview", () => {
|
||||
expect(item.pausedLabel).toBe("Paused");
|
||||
expect(item.summary).toBe("Favorite cuisine: swedish");
|
||||
});
|
||||
|
||||
it("renderMemorySummary stödjer S5-value-former", () => {
|
||||
expect(renderMemorySummary({ summarySv: "", value: { favoriteCuisine: "thai" } }, "en-US")).toBe("Favorite cuisine: thai");
|
||||
expect(renderMemorySummary({ summarySv: "", value: { avoidIngredientId: "broccoli" } }, "en-US")).toBe("Avoids ingredient: broccoli");
|
||||
expect(renderMemorySummary({ summarySv: "", value: { goal: "less_waste" } }, "en-US")).toBe("Goal: less_waste");
|
||||
expect(renderMemorySummary({ summarySv: "", value: { primaryGoal: "less_waste" } }, "en-US")).toBe("Primary goal: less_waste");
|
||||
expect(renderMemorySummary({ summarySv: "", value: { allergen: "gluten" } }, "en-US")).toBe("Allergy: gluten");
|
||||
expect(renderMemorySummary({ summarySv: "", value: { spiceLevelMax: 2 } }, "en-US")).toBe("Max spice level: 2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveMemoryUpdates", () => {
|
||||
function fakeAamos(output: unknown): AamosClient {
|
||||
return {
|
||||
async runTask<T extends AamosTaskType>(): Promise<AamosResult<T>> {
|
||||
return {
|
||||
status: "ok",
|
||||
output: output as AamosResult<T>["output"],
|
||||
modelVersion: "fake",
|
||||
promptVersion: "fake",
|
||||
latencyMs: 10,
|
||||
costUsd: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
};
|
||||
},
|
||||
async healthCheck() {
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("avvisar fabricerade minnen utan event-stöd", async () => {
|
||||
const aamos = fakeAamos({
|
||||
memoryUpdates: [
|
||||
{
|
||||
key: "fabricated",
|
||||
kind: "structured_fact",
|
||||
summarySv: "Påhittad favoriträtt",
|
||||
value: { recipeId: "ghost" },
|
||||
origin: "ai_inferred",
|
||||
confidence: 0.6,
|
||||
expiresAt: null,
|
||||
sourceEventIds: ["event-does-not-exist"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await deriveMemoryUpdates(aamos, {
|
||||
scope: "user",
|
||||
scopeId: "u1",
|
||||
events: [{ id: "evt-real", type: "recipe_cooked", occurredAt: "2026-08-01T12:00:00Z", payload: {} }],
|
||||
existingMemoryKeys: [],
|
||||
consentFlags: { personalization: true, anonymizedImprovement: false, imageTraining: false },
|
||||
});
|
||||
expect(result.proposals).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("behåller grundade minnen med kända event-id", async () => {
|
||||
const aamos = fakeAamos({
|
||||
memoryUpdates: [
|
||||
{
|
||||
key: "grounded",
|
||||
kind: "structured_fact",
|
||||
summarySv: "Ggrundat minne",
|
||||
value: { recipeId: "r1" },
|
||||
origin: "observed",
|
||||
confidence: 0.8,
|
||||
expiresAt: null,
|
||||
sourceEventIds: ["evt-real"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await deriveMemoryUpdates(aamos, {
|
||||
scope: "user",
|
||||
scopeId: "u1",
|
||||
events: [{ id: "evt-real", type: "recipe_cooked", occurredAt: "2026-08-01T12:00:00Z", payload: {} }],
|
||||
existingMemoryKeys: [],
|
||||
consentFlags: { personalization: true, anonymizedImprovement: false, imageTraining: false },
|
||||
});
|
||||
expect(result.proposals).toHaveLength(1);
|
||||
expect(result.proposals[0]!.key).toBe("grounded");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user