feat(memory): S3 3a minnes-yta + impact-endpoint

This commit is contained in:
Sven (AAMOS AI)
2026-08-11 02:41:39 +07:00
parent 4296a64aef
commit 335cfe0bf6
5 changed files with 873 additions and 2 deletions
+99 -2
View File
@@ -76,6 +76,18 @@ export async function deriveMemoryUpdates(
}));
}
export interface MemoryOverviewItem extends MemoryItem {
summary: string;
/** Lokaliserad etikett för ursprung (user_stated/observed/ai_inferred). */
originLabel: string;
/** Konfidens 0100 %. */
confidencePercent: number;
/** Synlig etikett om posten är pausad. */
pausedLabel?: string;
/** Tydlig gissningsmarkering för ai_inferred (R1). */
guessLabel?: string;
}
/** Gruppera minnesposter för "Vad plattformen vet om mig"-vyn. */
export interface MemoryOverview {
language: string;
@@ -84,7 +96,7 @@ export interface MemoryOverview {
/** Rubrik på begärt språk (i18n M10). titleSv behålls för bakåtkompatibilitet. */
title: string;
titleSv: string;
items: Array<MemoryItem & { summary: string }>;
items: MemoryOverviewItem[];
}>;
totalCount: number;
pausedCount: number;
@@ -177,6 +189,76 @@ const KIND_TITLES: Record<string, Record<MemoryKind, string>> = {
},
};
const UI_LABELS: Record<
string,
{
origin: Record<SignalOrigin, string>;
paused: string;
guess: string;
}
> = {
sv: {
origin: { user_stated: "Du har sagt", observed: "Vi har sett", ai_inferred: "Gissning" },
paused: "Pausad",
guess: "Detta är en gissning bekräfta eller ändra om det stämmer.",
},
en: {
origin: { user_stated: "You said", observed: "We noticed", ai_inferred: "Guess" },
paused: "Paused",
guess: "This is a guess — confirm or change it if it fits.",
},
es: {
origin: { user_stated: "Dijiste", observed: "Notamos", ai_inferred: "Suposición" },
paused: "Pausado",
guess: "Esto es una suposición: confírmala o cámbiala si encaja.",
},
it: {
origin: { user_stated: "Hai detto", observed: "Abbiamo notato", ai_inferred: "Ipotesi" },
paused: "In pausa",
guess: "Questa è un'ipotesi: confermala o modificala se è corretta.",
},
de: {
origin: { user_stated: "Du hast gesagt", observed: "Wir haben bemerkt", ai_inferred: "Vermutung" },
paused: "Pausiert",
guess: "Dies ist eine Vermutung: bestätige oder ändere sie, wenn sie stimmt.",
},
fr: {
origin: { user_stated: "Vous avez dit", observed: "Nous avons remarqué", ai_inferred: "Hypothèse" },
paused: "En pause",
guess: "Ceci est une hypothèse : confirmez ou modifiez si cela vous convient.",
},
da: {
origin: { user_stated: "Du har sagt", observed: "Vi har set", ai_inferred: "Gæt" },
paused: "Pauset",
guess: "Dette er et gæt — bekræft eller ændr det, hvis det passer.",
},
nb: {
origin: { user_stated: "Du har sagt", observed: "Vi har sett", ai_inferred: "Gjettning" },
paused: "Pauset",
guess: "Dette er en gjetning — bekreft eller endre hvis det stemmer.",
},
fi: {
origin: { user_stated: "Olet sanonut", observed: "Olemme huomanneet", ai_inferred: "Arvaus" },
paused: "Tauolla",
guess: "Tämä on arvaus: vahvista tai muuta se, jos se sopii.",
},
nl: {
origin: { user_stated: "Jij zei", observed: "We hebben gezien", ai_inferred: "Gok" },
paused: "Gepauzeerd",
guess: "Dit is een gok: bevestig of pas aan als het klopt.",
},
pl: {
origin: { user_stated: "Powiedziałeś", observed: "Zauważyliśmy", ai_inferred: "Domysł" },
paused: "Wstrzymane",
guess: "To jest przypuszczenie — potwierdź lub zmień, jeśli się zgadza.",
},
pt: {
origin: { user_stated: "Disseste", observed: "Reparámos", ai_inferred: "Suposição" },
paused: "Pausado",
guess: "Isto é uma suposição: confirma ou altera se fizer sentido.",
},
};
/**
* 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
@@ -209,6 +291,7 @@ export function renderMemorySummary(
export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): MemoryOverview {
const lang = languageTag.split("-")[0] ?? "sv";
const titles = KIND_TITLES[lang] ?? KIND_TITLES.sv!;
const labels = UI_LABELS[lang] ?? UI_LABELS.sv!;
const byKind = new Map<MemoryKind, MemoryItem[]>();
for (const item of items) {
const list = byKind.get(item.kind) ?? [];
@@ -221,7 +304,21 @@ export function buildMemoryOverview(items: MemoryItem[], languageTag = "sv"): Me
titleSv: KIND_TITLES.sv![kind],
items: list
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.map((i) => ({ ...i, summary: renderMemorySummary(i, languageTag) })),
.map((i) => {
const overviewItem: MemoryOverviewItem = {
...i,
summary: renderMemorySummary(i, languageTag),
originLabel: labels.origin[i.origin] ?? labels.origin.ai_inferred,
confidencePercent: Math.round(i.confidence * 100),
};
if (i.paused) {
overviewItem.pausedLabel = labels.paused;
}
if (i.origin === "ai_inferred") {
overviewItem.guessLabel = labels.guess;
}
return overviewItem;
}),
}));
return {
language: lang,
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { buildMemoryOverview } from "../src/index.js";
import type { MemoryItem } from "@app/shared-types";
function makeItem(overrides: Partial<MemoryItem> = {}): MemoryItem {
return {
id: "m1",
userId: "u1",
kind: "structured_fact",
key: "favorite_cuisine_swedish",
summarySv: "Gillar svensk mat",
value: { favoriteCuisine: "swedish" },
origin: "user_stated",
confidence: 1,
verifiedByUser: true,
paused: false,
createdAt: "2026-08-01T00:00:00.000Z",
updatedAt: "2026-08-01T00:00:00.000Z",
...overrides,
};
}
describe("buildMemoryOverview", () => {
it("grupperar minnen efter kind", () => {
const items: MemoryItem[] = [
makeItem({ id: "a", kind: "structured_fact", summarySv: "Fakta A" }),
makeItem({ id: "b", kind: "recipe_memory", summarySv: "Recept B" }),
makeItem({ id: "c", kind: "structured_fact", summarySv: "Fakta C" }),
];
const overview = buildMemoryOverview(items, "sv-SE");
const kinds = overview.sections.map((s) => s.kind);
expect(kinds).toContain("structured_fact");
expect(kinds).toContain("recipe_memory");
const factSection = overview.sections.find((s) => s.kind === "structured_fact")!;
expect(factSection.items).toHaveLength(2);
});
it("visar origin och confidence tydligt", () => {
const items: MemoryItem[] = [
makeItem({ origin: "observed", confidence: 0.75 }),
];
const overview = buildMemoryOverview(items, "sv-SE");
const item = overview.sections[0]!.items[0]!;
expect(item.originLabel).toBe("Vi har sett");
expect(item.confidencePercent).toBe(75);
});
it("markerar pausade poster synligt", () => {
const items: MemoryItem[] = [makeItem({ paused: true })];
const overview = buildMemoryOverview(items, "sv-SE");
const item = overview.sections[0]!.items[0]!;
expect(item.pausedLabel).toBe("Pausad");
expect(overview.pausedCount).toBe(1);
});
it("markerar ai_inferred som gissning", () => {
const items: MemoryItem[] = [makeItem({ origin: "ai_inferred", confidence: 0.5 })];
const overview = buildMemoryOverview(items, "sv-SE");
const item = overview.sections[0]!.items[0]!;
expect(item.originLabel).toBe("Gissning");
expect(item.guessLabel).toBe("Detta är en gissning bekräfta eller ändra om det stämmer.");
});
it("sorterar senast uppdaterade överst", () => {
const items: MemoryItem[] = [
makeItem({ id: "old", updatedAt: "2026-08-01T00:00:00.000Z" }),
makeItem({ id: "new", updatedAt: "2026-08-10T00:00:00.000Z" }),
];
const overview = buildMemoryOverview(items, "sv-SE");
expect(overview.sections[0]!.items[0]!.id).toBe("new");
expect(overview.sections[0]!.items[1]!.id).toBe("old");
});
it("stödjer engelska texter", () => {
const items: MemoryItem[] = [makeItem({ origin: "user_stated", paused: true })];
const overview = buildMemoryOverview(items, "en-US");
const item = overview.sections[0]!.items[0]!;
expect(item.originLabel).toBe("You said");
expect(item.pausedLabel).toBe("Paused");
expect(item.summary).toBe("Favorite cuisine: swedish");
});
});