Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@app/memory-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "AAMOS Memory-integration: minneslager, samtyckesregler och \"Vad appen vet om mig\" (spec §32)",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@app/ai-contracts": "workspace:*",
|
||||
"@app/shared-types": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
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<SignalOrigin, "user_stated">;
|
||||
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<MemoryUpdateProposal[]> {
|
||||
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<MemoryItem & { summary: string }>;
|
||||
}>;
|
||||
totalCount: number;
|
||||
pausedCount: number;
|
||||
}
|
||||
|
||||
const KIND_TITLES: Record<string, Record<MemoryKind, string>> = {
|
||||
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<MemoryItem, "summarySv" | "value">,
|
||||
languageTag: string,
|
||||
): string {
|
||||
const lang = languageTag.split("-")[0] ?? "sv";
|
||||
if (lang === "sv") return item.summarySv;
|
||||
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.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<MemoryKind, MemoryItem[]>();
|
||||
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)}`;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user