SPB Git
2.9 KB · 38 lines typescript
1import "server-only";2import { sql, vec, getSetting } from "../db";3import { newId } from "../ids";4import { openaiEmbed } from "../ai/openai";5import type { ToolContext } from "./index";67const 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;89export async function saveMemoryTool(content: string, category: string, ctx: Pick<ToolContext, "conversationId" | "messageId" | "projectId">) {10  const settings = await getSetting<{ enabled: boolean; autoExtract: boolean; requireApproval: boolean }>("memory", { enabled: true, autoExtract: true, requireApproval: false });11  if (!settings.enabled) return { saved: false, reason: "Memory is disabled by the owner." };12  if (SENSITIVE.test(content)) return { saved: false, reason: "Refused: content looks like a credential or sensitive personal data." };13  const dup = await sql<{ id: string }[]>`SELECT id FROM memories WHERE status <> 'disabled' AND lower(content) = lower(${content.trim()})`;14  if (dup.length) return { saved: false, reason: "An identical memory already exists.", id: dup[0].id };15  const id = newId("mem");16  let emb: number[] | null = null;17  try { emb = (await openaiEmbed([content])).vectors[0]; } catch { emb = null; }18  const status = settings.requireApproval ? "candidate" : "active";19  await sql`INSERT INTO memories (id, category, content, source, source_conversation_id, source_message_id, status, embedding)20            VALUES (${id}, ${category}, ${content.trim()}, 'extracted', ${ctx.conversationId}, ${ctx.messageId}, ${status}, ${emb ? sql`${vec(emb)}::vector` : null})`;21  return { saved: true, id, status, note: status === "candidate" ? "Saved as a candidate; the owner must approve it in the Memory page." : "Saved." };22}2324/** Retrieve the most relevant active memories for a prompt (semantic + lexical). */25export async function retrieveMemories(query: string, opts: { projectId?: string | null; limit?: number } = {}): Promise<{ id: string; category: string; content: string }[]> {26  const settings = await getSetting<{ enabled: boolean }>("memory", { enabled: true });27  if (!settings.enabled) return [];28  let qv: number[] | null = null;29  try { if (query.trim().length > 3) qv = (await openaiEmbed([query.slice(0, 2000)])).vectors[0]; } catch { qv = null; }30  const rows = await sql<{ id: string; category: string; content: string }[]>`31    SELECT id, category, content FROM memories32    WHERE status = 'active' AND (project_id IS NULL OR project_id = ${opts.projectId ?? null})33    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 DESC34    LIMIT ${opts.limit ?? 12}`;35  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)})`;36  return rows;37}38
Files · laptop-build-20261101
Menu

Theme