import "server-only"; import { sql, vec, getSetting } from "../db"; import { newId } from "../ids"; import { openaiEmbed } from "../ai/openai"; import type { ToolContext } from "./index"; const SENSITIVE = /(password|mot de passe|passwd|api[_ -]?key|secret|token|sk-[a-z0-9]|ssn|nas\b|numéro d'assurance|carte de crédit|credit card|\b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}\b|iban|cvv)/i; export async function saveMemoryTool(content: string, category: string, ctx: Pick) { const settings = await getSetting<{ enabled: boolean; autoExtract: boolean; requireApproval: boolean }>("memory", { enabled: true, autoExtract: true, requireApproval: false }); if (!settings.enabled) return { saved: false, reason: "Memory is disabled by the owner." }; if (SENSITIVE.test(content)) return { saved: false, reason: "Refused: content looks like a credential or sensitive personal data." }; const dup = await sql<{ id: string }[]>`SELECT id FROM memories WHERE status <> 'disabled' AND lower(content) = lower(${content.trim()})`; if (dup.length) return { saved: false, reason: "An identical memory already exists.", id: dup[0].id }; const id = newId("mem"); let emb: number[] | null = null; try { emb = (await openaiEmbed([content])).vectors[0]; } catch { emb = null; } const status = settings.requireApproval ? "candidate" : "active"; await sql`INSERT INTO memories (id, category, content, source, source_conversation_id, source_message_id, status, embedding) VALUES (${id}, ${category}, ${content.trim()}, 'extracted', ${ctx.conversationId}, ${ctx.messageId}, ${status}, ${emb ? sql`${vec(emb)}::vector` : null})`; return { saved: true, id, status, note: status === "candidate" ? "Saved as a candidate; the owner must approve it in the Memory page." : "Saved." }; } /** Retrieve the most relevant active memories for a prompt (semantic + lexical). */ export async function retrieveMemories(query: string, opts: { projectId?: string | null; limit?: number } = {}): Promise<{ id: string; category: string; content: string }[]> { const settings = await getSetting<{ enabled: boolean }>("memory", { enabled: true }); if (!settings.enabled) return []; let qv: number[] | null = null; try { if (query.trim().length > 3) qv = (await openaiEmbed([query.slice(0, 2000)])).vectors[0]; } catch { qv = null; } const rows = await sql<{ id: string; category: string; content: string }[]>` SELECT id, category, content FROM memories WHERE status = 'active' AND (project_id IS NULL OR project_id = ${opts.projectId ?? null}) ORDER BY (${qv ? sql`CASE WHEN embedding IS NULL THEN 0 ELSE 1 - (embedding <=> ${vec(qv)}::vector) END` : sql`0`}) * 0.75 + ts_rank(tsv, plainto_tsquery('simple', ${query.slice(0, 500)})) * 0.25 DESC, updated_at DESC LIMIT ${opts.limit ?? 12}`; if (rows.length) await sql`UPDATE memories SET use_count = use_count + 1, last_used_at = now() WHERE id = ANY(${rows.map(r => r.id)})`; return rows; }