import type { AamosClient } from "@app/ai-contracts"; import type { MemoryItem, MemoryKind, SignalOrigin } from "@app/shared-types"; /** * Minnesarkitektur (spec §32): * * - plattformens databas (memory_items) är den ANVÄNDARSYNLIGA sanningen: * allt som visas i "Vad plattformen vet om mig" och allt som kan korrigeras, * pausas, raderas och exporteras finns där. * - AAMOS får härleda nya minnesposter (UPDATE_USER_MEMORY-jobbet), men * förslagen skrivs alltid in i plattformens tabell där användaren äger dem. * - Personligt minne är ALDRIG automatiskt träningsdata (spec §32–33); * consentSnapshot följer med varje härledning. */ export interface MemoryUpdateProposal { key: string; kind: MemoryKind; summarySv: string; value: unknown; origin: Exclude; confidence: number; expiresAt?: string | null; } export interface MemorySyncInput { scope: "user" | "household"; scopeId: string; events: Array<{ type: string; occurredAt: string; payload: unknown }>; existingMemoryKeys: string[]; consentFlags: { personalization: boolean; anonymizedImprovement: boolean; imageTraining: boolean; }; correlationId?: string; localeContext?: import("@app/shared-types").LocaleContext; } /** * Kör minnesuppdatering via AAMOS. Returnerar förslag – anroparen (workern) * persisterar dem i memory_items och publicerar MEMORY_UPDATED-event. * Utan personaliseringssamtycke körs ingenting. */ export async function deriveMemoryUpdates( aamos: AamosClient, input: MemorySyncInput, ): Promise { if (!input.consentFlags.personalization) return []; const result = await aamos.runTask( "UPDATE_USER_MEMORY", { scope: input.scope, scopeId: input.scopeId, events: input.events, existingMemoryKeys: input.existingMemoryKeys, }, { subjectRef: pseudonymize(input.scopeId), consentFlags: input.consentFlags, ...(input.localeContext ? { localeContext: input.localeContext } : {}), ...(input.correlationId ? { correlationId: input.correlationId } : {}), }, ); if (result.status !== "ok" || !result.output) return []; return result.output.memoryUpdates.map((u) => ({ key: u.key, kind: u.kind, summarySv: u.summarySv, value: u.value, origin: u.origin, confidence: u.confidence, expiresAt: u.expiresAt, })); } /** Gruppera minnesposter för "Vad plattformen vet om mig"-vyn. */ export interface MemoryOverview { language: string; sections: Array<{ kind: MemoryKind; /** Rubrik på begärt språk (i18n M10). titleSv behålls för bakåtkompatibilitet. */ title: string; titleSv: string; items: Array; }>; totalCount: number; pausedCount: number; } const KIND_TITLES: Record> = { sv: { structured_fact: "Fakta om dig och hushållet", event: "Händelser vi kommer ihåg", semantic: "Mönster vi har observerat", profile_summary: "Din profil i korthet", recipe_memory: "Recept och måltider", }, en: { structured_fact: "Facts about you and your household", event: "Events we remember", semantic: "Patterns we've observed", profile_summary: "Your profile at a glance", recipe_memory: "Recipes and meals", }, es: { structured_fact: "Datos sobre ti y tu hogar", event: "Eventos que recordamos", semantic: "Patrones observados", profile_summary: "Tu perfil de un vistazo", recipe_memory: "Recetas y comidas", }, it: { structured_fact: "Informazioni su di te e la tua famiglia", event: "Eventi che ricordiamo", semantic: "Schemi osservati", profile_summary: "Il tuo profilo in breve", recipe_memory: "Ricette e pasti", }, de: { structured_fact: "Fakten über dich und deinen Haushalt", event: "Ereignisse, die wir uns merken", semantic: "Beobachtete Muster", profile_summary: "Dein Profil auf einen Blick", recipe_memory: "Rezepte und Mahlzeiten", }, fr: { structured_fact: "Infos sur vous et votre foyer", event: "Événements mémorisés", semantic: "Habitudes observées", profile_summary: "Votre profil en un coup d'œil", recipe_memory: "Recettes et repas", }, da: { structured_fact: "Fakta om dig og husstanden", event: "Hændelser vi husker", semantic: "Mønstre vi har observeret", profile_summary: "Din profil i korte træk", recipe_memory: "Opskrifter og måltider", }, nb: { structured_fact: "Fakta om deg og husstanden", event: "Hendelser vi husker", semantic: "Mønstre vi har observert", profile_summary: "Profilen din i korte trekk", recipe_memory: "Oppskrifter og måltider", }, fi: { structured_fact: "Tietoa sinusta ja kotitaloudestasi", event: "Muistamamme tapahtumat", semantic: "Havaitut tavat", profile_summary: "Profiilisi lyhyesti", recipe_memory: "Reseptit ja ateriat", }, nl: { structured_fact: "Feiten over jou en je huishouden", event: "Gebeurtenissen die we onthouden", semantic: "Waargenomen patronen", profile_summary: "Je profiel in het kort", recipe_memory: "Recepten en maaltijden", }, pl: { structured_fact: "Fakty o Tobie i Twoim gospodarstwie", event: "Zdarzenia, które pamiętamy", semantic: "Zaobserwowane wzorce", profile_summary: "Twój profil w skrócie", recipe_memory: "Przepisy i posiłki", }, pt: { structured_fact: "Factos sobre si e o seu agregado", event: "Eventos que recordamos", semantic: "Padrões observados", profile_summary: "O seu perfil em resumo", recipe_memory: "Receitas e refeições", }, }; /** * 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 * (nya minnen får lokaliserad summary direkt från AAMOS via localeContext). */ export function renderMemorySummary( item: Pick, languageTag: string, ): string { const lang = languageTag.split("-")[0] ?? "sv"; if (lang === "sv") return item.summarySv; const v = item.value as Record | null | undefined; if (v && typeof v === "object") { if (typeof v.favoriteCuisine === "string") { return `Favorite cuisine: ${String(v.favoriteCuisine)}`; } if (typeof v.dislikedIngredient === "string") { return `Avoids: ${String(v.dislikedIngredient)}`; } if (typeof v.typicalPortions === "number") { return `Usually cooks ${v.typicalPortions} servings`; } if (typeof v.spicePreference === "string") { return `Spice preference: ${String(v.spicePreference)}`; } } return item.summarySv; } export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): MemoryOverview { const lang = languageTag.split("-")[0] ?? "sv"; const titles = KIND_TITLES[lang] ?? KIND_TITLES.sv!; const byKind = new Map(); for (const item of items) { const list = byKind.get(item.kind) ?? []; list.push(item); byKind.set(item.kind, list); } const sections = [...byKind.entries()].map(([kind, list]) => ({ kind, title: titles[kind], titleSv: KIND_TITLES.sv![kind], items: list .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) .map((i) => ({ ...i, summary: renderMemorySummary(i, languageTag) })), })); return { language: lang, sections, totalCount: items.length, pausedCount: items.filter((i) => i.paused).length, }; } /** Pseudonymisera id innan det skickas till AAMOS (spec §56: minimization). */ export function pseudonymize(id: string): string { // Enkel stabil pseudonym – riktiga miljöer använder HMAC med rotationsbar nyckel. return `subj_${Buffer.from(id).toString("base64url").slice(0, 16)}`; }