/** * Brain-API-Client fuer die App. * * Die App hat keinen direkten HTTP-Zugriff aufs Brain (nur via RVS). Wir * tunneln alle Memory-Operationen ueber den generischen brain_request / * brain_response RVS-Channel den die Bridge implementiert. * * Pattern: pro Call eine eindeutige requestId, Listener wartet auf passende * brain_response, Promise loest auf / wird abgelehnt bei status>=400. */ import rvs from './rvs'; type AnyJson = any; interface PendingRequest { resolve: (data: AnyJson) => void; reject: (err: Error) => void; timer: ReturnType; expectBinary?: boolean; } const pending = new Map(); let installed = false; function _ensureListener() { if (installed) return; installed = true; rvs.onMessage((msg: any) => { if (!msg || msg.type !== 'brain_response') return; const p = msg.payload || {}; const reqId: string = p.requestId || ''; const handler = pending.get(reqId); if (!handler) return; pending.delete(reqId); clearTimeout(handler.timer); const status: number = Number(p.status || 0); if (status >= 200 && status < 300) { if (handler.expectBinary) { handler.resolve({ base64: p.base64 || '', contentType: p.contentType || '' }); } else { handler.resolve(p.json !== undefined ? p.json : (p.text !== undefined ? p.text : null)); } } else { const detail = (p.json && p.json.detail) || p.text || `HTTP ${status}`; handler.reject(new Error(`Brain ${status}: ${detail}`)); } }); } let _nextId = 0; function _newRequestId(): string { _nextId += 1; return `brain_${Date.now().toString(36)}_${_nextId}`; } interface SendOpts { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; body?: AnyJson; bodyBase64?: string; contentType?: string; expectBinary?: boolean; timeoutMs?: number; } function _send(path: string, opts: SendOpts = {}): Promise { _ensureListener(); return new Promise((resolve, reject) => { const requestId = _newRequestId(); const timer = setTimeout(() => { if (pending.delete(requestId)) { reject(new Error(`Brain-Timeout fuer ${path}`)); } }, opts.timeoutMs || 30000); pending.set(requestId, { resolve, reject, timer, expectBinary: opts.expectBinary }); rvs.send('brain_request' as any, { requestId, method: opts.method || 'GET', path, ...(opts.body !== undefined ? { body: opts.body } : {}), ...(opts.bodyBase64 ? { bodyBase64: opts.bodyBase64 } : {}), ...(opts.contentType ? { contentType: opts.contentType } : {}), }); }); } // ── Typen ──────────────────────────────────────────────────────────── export interface MemoryAttachment { name: string; mime: string; size: number; path: string; } export interface Memory { id: string; type: string; title: string; content: string; pinned: boolean; category: string; source: string; tags: string[]; created_at: string; updated_at: string; conversation_id?: string | null; score?: number | null; attachments?: MemoryAttachment[]; } // ── Memory CRUD ────────────────────────────────────────────────────── export const brainApi = { /** Einzelne Memory holen (mit allen Feldern inkl. Anhaenge) */ getMemory(id: string): Promise { return _send(`/memory/get/${encodeURIComponent(id)}`); }, /** Liste aller Memories, optional nach Type gefiltert. */ listMemories(opts: { type?: string; limit?: number } = {}): Promise { const qs = new URLSearchParams(); if (opts.type) qs.set('type', opts.type); qs.set('limit', String(opts.limit || 500)); return _send(`/memory/list?${qs.toString()}`); }, /** Volltext-Substring-Suche. */ searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise { const qs = new URLSearchParams({ q }); if (opts.type) qs.set('type', opts.type); qs.set('include_pinned', String(opts.includePinned !== false)); qs.set('k', String(opts.k || 50)); return _send(`/memory/search-text?${qs.toString()}`); }, /** Semantische Suche (Embedder). */ searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise { const qs = new URLSearchParams({ q }); if (opts.type) qs.set('type', opts.type); qs.set('include_pinned', String(opts.includePinned !== false)); qs.set('k', String(opts.k || 10)); qs.set('score_threshold', String(opts.threshold ?? 0.30)); return _send(`/memory/search?${qs.toString()}`); }, /** Memory anlegen. */ saveMemory(body: { type: string; title: string; content: string; pinned?: boolean; category?: string; tags?: string[]; }): Promise { return _send('/memory/save', { method: 'POST', body: { source: 'app', ...body }, }); }, /** Memory aktualisieren (Patch — nur uebergebene Felder werden geaendert). */ updateMemory(id: string, body: Partial>): Promise { return _send(`/memory/update/${encodeURIComponent(id)}`, { method: 'PATCH', body, }); }, /** Memory loeschen. */ deleteMemory(id: string): Promise<{ deleted: string }> { return _send(`/memory/delete/${encodeURIComponent(id)}`, { method: 'DELETE', timeoutMs: 15000, }); }, // ── Anhaenge ──────────────────────────────────────────────────────── /** Datei als Anhang an die Memory haengen (Base64-Upload). */ uploadAttachment(memoryId: string, name: string, base64: string): Promise { return _send(`/memory/${encodeURIComponent(memoryId)}/attachments`, { method: 'POST', body: { name, data_base64: base64 }, timeoutMs: 120000, }); }, /** Anhang loeschen. */ deleteAttachment(memoryId: string, filename: string): Promise { return _send( `/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`, { method: 'DELETE' }, ); }, /** Anhang-Bytes holen (fuer Vorschau / Download). Liefert Base64. */ getAttachmentBytes(memoryId: string, filename: string): Promise<{ base64: string; contentType: string }> { return _send( `/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`, { expectBinary: true, timeoutMs: 60000 }, ); }, }; export default brainApi;