feat(worker): S3 3b härda UPDATE_USER_MEMORY — budget, kostnad, anti-påhitt, GDPR-regression
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user