feat(worker): S3 3b härda UPDATE_USER_MEMORY — budget, kostnad, anti-påhitt, GDPR-regression

This commit is contained in:
Sven (AAMOS AI)
2026-08-11 03:23:09 +07:00
parent 33b0a8267b
commit 05bba93b90
11 changed files with 678 additions and 62 deletions
+6
View File
@@ -22,6 +22,8 @@ export interface AamosCallOptions {
anonymizedImprovement: boolean;
imageTraining: boolean;
};
/** Systeminstruktion som skickas med till AAMOS (t.ex. anti-påhitt-regler). */
systemInstruction?: string;
}
export interface AamosResult<T extends AamosTaskType> {
@@ -118,6 +120,9 @@ export class HttpAamosClient implements AamosClient {
anonymizedImprovement: false,
imageTraining: false,
},
...(options.systemInstruction
? { systemInstruction: options.systemInstruction }
: {}),
},
};
@@ -219,6 +224,7 @@ export class MockAamosClient implements AamosClient {
async runTask<T extends AamosTaskType>(
taskType: T,
input: TaskInput<T>,
_options?: AamosCallOptions,
): Promise<AamosResult<T>> {
const contract = TASK_CONTRACTS[taskType];
const parsedInput = contract.input.safeParse(input);
+21 -2
View File
@@ -289,8 +289,27 @@ export function mockOutputFor(taskType: AamosTaskType, input: unknown): unknown
case "PARSE_CRAVING":
return { tags: ["creamy"], cuisine: null, maxKcal: null, confidence: 0.6 };
case "UPDATE_USER_MEMORY":
return { memoryUpdates: [] };
case "UPDATE_USER_MEMORY": {
const events =
typeof input === "object" && input !== null
? ((input as { events?: Array<{ id?: string; type?: string }> }).events ?? [])
: [];
const eventIds = events.map((e) => e.id).filter((id): id is string => id != null);
if (eventIds.length === 0) return { memoryUpdates: [] };
// Mock: ett grundat förslag per faktiskt event (utan att hitta på nya).
return {
memoryUpdates: eventIds.map((id) => ({
key: `event_${id}`,
kind: "event" as const,
summarySv: "Observerad från event",
value: { eventId: id },
origin: "observed" as const,
confidence: 0.75,
expiresAt: null,
sourceEventIds: [id],
})),
};
}
case "GENERATE_WEEK_PLAN":
return { entries: [], confidence: 0.5 };
+5
View File
@@ -377,6 +377,7 @@ export const updateUserMemoryInput = z.object({
scopeId: z.string(),
events: z.array(
z.object({
id: z.string(),
type: z.string(),
occurredAt: z.string(),
payload: z.unknown(),
@@ -394,6 +395,8 @@ export const updateUserMemoryOutput = z.object({
origin: z.enum(["observed", "ai_inferred"]),
confidence,
expiresAt: z.string().nullable().default(null),
/** Händelse-ID:n som stödjer förslaget obligatoriskt för grundningskoll (R1). */
sourceEventIds: z.array(z.string()).min(1),
}),
),
});
@@ -585,6 +588,8 @@ export const aamosRequestEnvelopeSchema = z.object({
anonymizedImprovement: z.boolean(),
imageTraining: z.boolean(),
}),
/** Extra systeminstruktion som AAMOS ska lägga till i prompten. */
systemInstruction: z.string().optional(),
}),
});
export type AamosRequestEnvelope = z.infer<typeof aamosRequestEnvelopeSchema>;
+52 -16
View File
@@ -1,4 +1,4 @@
import type { AamosClient } from "@app/ai-contracts";
import { TASK_CONTRACTS, type AamosClient } from "@app/ai-contracts";
import type { MemoryItem, MemoryKind, SignalOrigin } from "@app/shared-types";
/**
@@ -21,12 +21,14 @@ export interface MemoryUpdateProposal {
origin: Exclude<SignalOrigin, "user_stated">;
confidence: number;
expiresAt?: string | null;
/** Händelse-ID:n som stödjer förslaget (R1: grundat, aldrig påhittat). */
sourceEventIds: string[];
}
export interface MemorySyncInput {
scope: "user" | "household";
scopeId: string;
events: Array<{ type: string; occurredAt: string; payload: unknown }>;
events: Array<{ id: string; type: string; occurredAt: string; payload: unknown }>;
existingMemoryKeys: string[];
consentFlags: {
personalization: boolean;
@@ -38,15 +40,23 @@ export interface MemorySyncInput {
}
/**
* Kör minnesuppdatering via AAMOS. Returnerar förslag anroparen (workern)
* persisterar dem i memory_items och publicerar MEMORY_UPDATED-event.
* Kör minnesuppdatering via AAMOS. Returnerar granskade förslag + faktisk
* token/kostnadsanvändning så att workern kan bokföra i ai_usage_counters.
* Utan personaliseringssamtycke körs ingenting.
*
* R1 (grundat, aldrig påhittat): AAMOS får explicit instruktion om att aldrig
* fabricera minnen; svaret valideras mot Zod-kontraktet.
*/
export async function deriveMemoryUpdates(
aamos: AamosClient,
input: MemorySyncInput,
): Promise<MemoryUpdateProposal[]> {
if (!input.consentFlags.personalization) return [];
): Promise<{
proposals: MemoryUpdateProposal[];
usage: { costUsd: number; inputTokens: number; outputTokens: number } | null;
}> {
if (!input.consentFlags.personalization) {
return { proposals: [], usage: null };
}
const result = await aamos.runTask(
"UPDATE_USER_MEMORY",
@@ -59,21 +69,47 @@ export async function deriveMemoryUpdates(
{
subjectRef: pseudonymize(input.scopeId),
consentFlags: input.consentFlags,
systemInstruction:
"Du får ENDAST sammanfatta faktiska events. Hitta ALDRIG på minnen, preferenser eller mönster som inte har explicit stöd i events. Varje förslag måste referera till ett eller flera events via sourceEventIds. ai_inferred-förslag ska ha låg konfidens.",
...(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,
}));
const usage =
result.status === "ok" && result.output
? {
costUsd: result.costUsd ?? 0,
inputTokens: result.inputTokens ?? 0,
outputTokens: result.outputTokens ?? 0,
}
: null;
if (result.status !== "ok" || !result.output) {
return { proposals: [], usage };
}
// Extra Zod-validering: ett brutet kontrakt ska aldrig nå användardata.
const parsed = TASK_CONTRACTS["UPDATE_USER_MEMORY"].output.safeParse(result.output);
if (!parsed.success) {
// eslint-disable-next-line no-console
console.error("UPDATE_USER_MEMORY output bröt mot kontraktet:", parsed.error.message);
return { proposals: [], usage };
}
return {
proposals: parsed.data.memoryUpdates.map((u) => ({
key: u.key,
kind: u.kind,
summarySv: u.summarySv,
value: u.value,
origin: u.origin,
confidence: u.confidence,
expiresAt: u.expiresAt,
sourceEventIds: u.sourceEventIds,
})),
usage,
};
}
export interface MemoryOverviewItem extends MemoryItem {