@reverbia/sdk 1.0.0-next.20251212012743 → 1.0.0-next.20251215143604
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/expo/index.cjs +1661 -20
- package/dist/expo/index.d.mts +748 -1
- package/dist/expo/index.d.ts +748 -1
- package/dist/expo/index.mjs +1641 -10
- package/dist/next/index.d.mts +2 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/react/index.cjs +1377 -418
- package/dist/react/index.d.mts +702 -47
- package/dist/react/index.d.ts +702 -47
- package/dist/react/index.mjs +1348 -398
- package/package.json +7 -6
package/dist/expo/index.mjs
CHANGED
|
@@ -280,8 +280,404 @@ function useChat(options) {
|
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
// src/expo/useChatStorage.ts
|
|
284
|
+
import { useCallback as useCallback2, useState as useState2, useMemo } from "react";
|
|
285
|
+
|
|
286
|
+
// src/lib/chatStorage/types.ts
|
|
287
|
+
function convertUsageToStored(usage) {
|
|
288
|
+
if (!usage) return void 0;
|
|
289
|
+
return {
|
|
290
|
+
promptTokens: usage.prompt_tokens,
|
|
291
|
+
completionTokens: usage.completion_tokens,
|
|
292
|
+
totalTokens: usage.total_tokens,
|
|
293
|
+
costMicroUsd: usage.cost_micro_usd
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function generateConversationId() {
|
|
297
|
+
return `conv_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/lib/chatStorage/operations.ts
|
|
301
|
+
import { Q } from "@nozbe/watermelondb";
|
|
302
|
+
function messageToStored(message) {
|
|
303
|
+
return {
|
|
304
|
+
uniqueId: message.id,
|
|
305
|
+
messageId: message.messageId,
|
|
306
|
+
conversationId: message.conversationId,
|
|
307
|
+
role: message.role,
|
|
308
|
+
content: message.content,
|
|
309
|
+
model: message.model,
|
|
310
|
+
files: message.files,
|
|
311
|
+
createdAt: message.createdAt,
|
|
312
|
+
updatedAt: message.updatedAt,
|
|
313
|
+
vector: message.vector,
|
|
314
|
+
embeddingModel: message.embeddingModel,
|
|
315
|
+
usage: message.usage,
|
|
316
|
+
sources: message.sources,
|
|
317
|
+
responseDuration: message.responseDuration
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function conversationToStored(conversation) {
|
|
321
|
+
return {
|
|
322
|
+
uniqueId: conversation.id,
|
|
323
|
+
conversationId: conversation.conversationId,
|
|
324
|
+
title: conversation.title,
|
|
325
|
+
createdAt: conversation.createdAt,
|
|
326
|
+
updatedAt: conversation.updatedAt,
|
|
327
|
+
isDeleted: conversation.isDeleted
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async function createConversationOp(ctx, opts, defaultTitle = "New Conversation") {
|
|
331
|
+
const convId = opts?.conversationId || generateConversationId();
|
|
332
|
+
const title = opts?.title || defaultTitle;
|
|
333
|
+
const created = await ctx.database.write(async () => {
|
|
334
|
+
return await ctx.conversationsCollection.create((conv) => {
|
|
335
|
+
conv._setRaw("conversation_id", convId);
|
|
336
|
+
conv._setRaw("title", title);
|
|
337
|
+
conv._setRaw("is_deleted", false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
return conversationToStored(created);
|
|
341
|
+
}
|
|
342
|
+
async function getConversationOp(ctx, id) {
|
|
343
|
+
const results = await ctx.conversationsCollection.query(Q.where("conversation_id", id), Q.where("is_deleted", false)).fetch();
|
|
344
|
+
return results.length > 0 ? conversationToStored(results[0]) : null;
|
|
345
|
+
}
|
|
346
|
+
async function getConversationsOp(ctx) {
|
|
347
|
+
const results = await ctx.conversationsCollection.query(Q.where("is_deleted", false), Q.sortBy("created_at", Q.desc)).fetch();
|
|
348
|
+
return results.map(conversationToStored);
|
|
349
|
+
}
|
|
350
|
+
async function updateConversationTitleOp(ctx, id, title) {
|
|
351
|
+
const results = await ctx.conversationsCollection.query(Q.where("conversation_id", id), Q.where("is_deleted", false)).fetch();
|
|
352
|
+
if (results.length > 0) {
|
|
353
|
+
await ctx.database.write(async () => {
|
|
354
|
+
await results[0].update((conv) => {
|
|
355
|
+
conv._setRaw("title", title);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
async function deleteConversationOp(ctx, id) {
|
|
363
|
+
const results = await ctx.conversationsCollection.query(Q.where("conversation_id", id), Q.where("is_deleted", false)).fetch();
|
|
364
|
+
if (results.length > 0) {
|
|
365
|
+
await ctx.database.write(async () => {
|
|
366
|
+
await results[0].update((conv) => {
|
|
367
|
+
conv._setRaw("is_deleted", true);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
async function getMessagesOp(ctx, convId) {
|
|
375
|
+
const results = await ctx.messagesCollection.query(Q.where("conversation_id", convId), Q.sortBy("message_id", Q.asc)).fetch();
|
|
376
|
+
return results.map(messageToStored);
|
|
377
|
+
}
|
|
378
|
+
async function getMessageCountOp(ctx, convId) {
|
|
379
|
+
return await ctx.messagesCollection.query(Q.where("conversation_id", convId)).fetchCount();
|
|
380
|
+
}
|
|
381
|
+
async function clearMessagesOp(ctx, convId) {
|
|
382
|
+
const messages = await ctx.messagesCollection.query(Q.where("conversation_id", convId)).fetch();
|
|
383
|
+
await ctx.database.write(async () => {
|
|
384
|
+
for (const message of messages) {
|
|
385
|
+
await message.destroyPermanently();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function createMessageOp(ctx, opts) {
|
|
390
|
+
const existingCount = await getMessageCountOp(ctx, opts.conversationId);
|
|
391
|
+
const messageId = existingCount + 1;
|
|
392
|
+
const created = await ctx.database.write(async () => {
|
|
393
|
+
return await ctx.messagesCollection.create((msg) => {
|
|
394
|
+
msg._setRaw("message_id", messageId);
|
|
395
|
+
msg._setRaw("conversation_id", opts.conversationId);
|
|
396
|
+
msg._setRaw("role", opts.role);
|
|
397
|
+
msg._setRaw("content", opts.content);
|
|
398
|
+
if (opts.model) msg._setRaw("model", opts.model);
|
|
399
|
+
if (opts.files) msg._setRaw("files", JSON.stringify(opts.files));
|
|
400
|
+
if (opts.usage) msg._setRaw("usage", JSON.stringify(opts.usage));
|
|
401
|
+
if (opts.sources) msg._setRaw("sources", JSON.stringify(opts.sources));
|
|
402
|
+
if (opts.responseDuration !== void 0)
|
|
403
|
+
msg._setRaw("response_duration", opts.responseDuration);
|
|
404
|
+
if (opts.vector) msg._setRaw("vector", JSON.stringify(opts.vector));
|
|
405
|
+
if (opts.embeddingModel) msg._setRaw("embedding_model", opts.embeddingModel);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
return messageToStored(created);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/expo/useChatStorage.ts
|
|
412
|
+
function storedToLlmapiMessage(stored) {
|
|
413
|
+
const content = [
|
|
414
|
+
{ type: "text", text: stored.content }
|
|
415
|
+
];
|
|
416
|
+
if (stored.files?.length) {
|
|
417
|
+
for (const file of stored.files) {
|
|
418
|
+
if (file.url) {
|
|
419
|
+
content.push({
|
|
420
|
+
type: "image_url",
|
|
421
|
+
image_url: { url: file.url }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
role: stored.role,
|
|
428
|
+
content
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function useChatStorage(options) {
|
|
432
|
+
const {
|
|
433
|
+
database,
|
|
434
|
+
conversationId: initialConversationId,
|
|
435
|
+
autoCreateConversation = true,
|
|
436
|
+
defaultConversationTitle = "New Conversation",
|
|
437
|
+
getToken,
|
|
438
|
+
baseUrl,
|
|
439
|
+
onData,
|
|
440
|
+
onFinish,
|
|
441
|
+
onError
|
|
442
|
+
} = options;
|
|
443
|
+
const [currentConversationId, setCurrentConversationId] = useState2(initialConversationId || null);
|
|
444
|
+
const messagesCollection = useMemo(
|
|
445
|
+
() => database.get("history"),
|
|
446
|
+
[database]
|
|
447
|
+
);
|
|
448
|
+
const conversationsCollection = useMemo(
|
|
449
|
+
() => database.get("conversations"),
|
|
450
|
+
[database]
|
|
451
|
+
);
|
|
452
|
+
const storageCtx = useMemo(
|
|
453
|
+
() => ({
|
|
454
|
+
database,
|
|
455
|
+
messagesCollection,
|
|
456
|
+
conversationsCollection
|
|
457
|
+
}),
|
|
458
|
+
[database, messagesCollection, conversationsCollection]
|
|
459
|
+
);
|
|
460
|
+
const {
|
|
461
|
+
isLoading,
|
|
462
|
+
sendMessage: baseSendMessage,
|
|
463
|
+
stop
|
|
464
|
+
} = useChat({
|
|
465
|
+
getToken,
|
|
466
|
+
baseUrl,
|
|
467
|
+
onData,
|
|
468
|
+
onFinish,
|
|
469
|
+
onError
|
|
470
|
+
});
|
|
471
|
+
const createConversation = useCallback2(
|
|
472
|
+
async (opts) => {
|
|
473
|
+
const created = await createConversationOp(
|
|
474
|
+
storageCtx,
|
|
475
|
+
opts,
|
|
476
|
+
defaultConversationTitle
|
|
477
|
+
);
|
|
478
|
+
setCurrentConversationId(created.conversationId);
|
|
479
|
+
return created;
|
|
480
|
+
},
|
|
481
|
+
[storageCtx, defaultConversationTitle]
|
|
482
|
+
);
|
|
483
|
+
const getConversation = useCallback2(
|
|
484
|
+
async (id) => {
|
|
485
|
+
return getConversationOp(storageCtx, id);
|
|
486
|
+
},
|
|
487
|
+
[storageCtx]
|
|
488
|
+
);
|
|
489
|
+
const getConversations = useCallback2(async () => {
|
|
490
|
+
return getConversationsOp(storageCtx);
|
|
491
|
+
}, [storageCtx]);
|
|
492
|
+
const updateConversationTitle = useCallback2(
|
|
493
|
+
async (id, title) => {
|
|
494
|
+
return updateConversationTitleOp(storageCtx, id, title);
|
|
495
|
+
},
|
|
496
|
+
[storageCtx]
|
|
497
|
+
);
|
|
498
|
+
const deleteConversation = useCallback2(
|
|
499
|
+
async (id) => {
|
|
500
|
+
const deleted = await deleteConversationOp(storageCtx, id);
|
|
501
|
+
if (deleted && currentConversationId === id) {
|
|
502
|
+
setCurrentConversationId(null);
|
|
503
|
+
}
|
|
504
|
+
return deleted;
|
|
505
|
+
},
|
|
506
|
+
[storageCtx, currentConversationId]
|
|
507
|
+
);
|
|
508
|
+
const getMessages = useCallback2(
|
|
509
|
+
async (convId) => {
|
|
510
|
+
return getMessagesOp(storageCtx, convId);
|
|
511
|
+
},
|
|
512
|
+
[storageCtx]
|
|
513
|
+
);
|
|
514
|
+
const getMessageCount = useCallback2(
|
|
515
|
+
async (convId) => {
|
|
516
|
+
return getMessageCountOp(storageCtx, convId);
|
|
517
|
+
},
|
|
518
|
+
[storageCtx]
|
|
519
|
+
);
|
|
520
|
+
const clearMessages = useCallback2(
|
|
521
|
+
async (convId) => {
|
|
522
|
+
return clearMessagesOp(storageCtx, convId);
|
|
523
|
+
},
|
|
524
|
+
[storageCtx]
|
|
525
|
+
);
|
|
526
|
+
const ensureConversation = useCallback2(async () => {
|
|
527
|
+
if (currentConversationId) {
|
|
528
|
+
const existing = await getConversation(currentConversationId);
|
|
529
|
+
if (existing) {
|
|
530
|
+
return currentConversationId;
|
|
531
|
+
}
|
|
532
|
+
if (autoCreateConversation) {
|
|
533
|
+
const newConv = await createConversation({
|
|
534
|
+
conversationId: currentConversationId
|
|
535
|
+
});
|
|
536
|
+
return newConv.conversationId;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (autoCreateConversation) {
|
|
540
|
+
const newConv = await createConversation();
|
|
541
|
+
return newConv.conversationId;
|
|
542
|
+
}
|
|
543
|
+
throw new Error(
|
|
544
|
+
"No conversation ID provided and autoCreateConversation is disabled"
|
|
545
|
+
);
|
|
546
|
+
}, [
|
|
547
|
+
currentConversationId,
|
|
548
|
+
getConversation,
|
|
549
|
+
autoCreateConversation,
|
|
550
|
+
createConversation
|
|
551
|
+
]);
|
|
552
|
+
const sendMessage = useCallback2(
|
|
553
|
+
async (args) => {
|
|
554
|
+
const {
|
|
555
|
+
content,
|
|
556
|
+
model,
|
|
557
|
+
messages: providedMessages,
|
|
558
|
+
includeHistory = true,
|
|
559
|
+
maxHistoryMessages = 50,
|
|
560
|
+
files,
|
|
561
|
+
onData: perRequestOnData
|
|
562
|
+
} = args;
|
|
563
|
+
let convId;
|
|
564
|
+
try {
|
|
565
|
+
convId = await ensureConversation();
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return {
|
|
568
|
+
data: null,
|
|
569
|
+
error: err instanceof Error ? err.message : "Failed to ensure conversation"
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
let messagesToSend = [];
|
|
573
|
+
if (includeHistory && !providedMessages) {
|
|
574
|
+
const storedMessages = await getMessages(convId);
|
|
575
|
+
const limitedMessages = storedMessages.slice(-maxHistoryMessages);
|
|
576
|
+
messagesToSend = limitedMessages.map(storedToLlmapiMessage);
|
|
577
|
+
} else if (providedMessages) {
|
|
578
|
+
messagesToSend = providedMessages;
|
|
579
|
+
}
|
|
580
|
+
const userMessageContent = [
|
|
581
|
+
{ type: "text", text: content }
|
|
582
|
+
];
|
|
583
|
+
if (files?.length) {
|
|
584
|
+
for (const file of files) {
|
|
585
|
+
if (file.url) {
|
|
586
|
+
userMessageContent.push({
|
|
587
|
+
type: "image_url",
|
|
588
|
+
image_url: { url: file.url }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const userMessage = {
|
|
594
|
+
role: "user",
|
|
595
|
+
content: userMessageContent
|
|
596
|
+
};
|
|
597
|
+
messagesToSend.push(userMessage);
|
|
598
|
+
const sanitizedFiles = files?.map((file) => ({
|
|
599
|
+
id: file.id,
|
|
600
|
+
name: file.name,
|
|
601
|
+
type: file.type,
|
|
602
|
+
size: file.size,
|
|
603
|
+
// Only keep URL if it's not a data URI (e.g., external URLs)
|
|
604
|
+
url: file.url && !file.url.startsWith("data:") ? file.url : void 0
|
|
605
|
+
}));
|
|
606
|
+
let storedUserMessage;
|
|
607
|
+
try {
|
|
608
|
+
storedUserMessage = await createMessageOp(storageCtx, {
|
|
609
|
+
conversationId: convId,
|
|
610
|
+
role: "user",
|
|
611
|
+
content,
|
|
612
|
+
files: sanitizedFiles
|
|
613
|
+
});
|
|
614
|
+
} catch (err) {
|
|
615
|
+
return {
|
|
616
|
+
data: null,
|
|
617
|
+
error: err instanceof Error ? err.message : "Failed to store user message"
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const startTime = Date.now();
|
|
621
|
+
const result = await baseSendMessage({
|
|
622
|
+
messages: messagesToSend,
|
|
623
|
+
model,
|
|
624
|
+
onData: perRequestOnData
|
|
625
|
+
});
|
|
626
|
+
const responseDuration = (Date.now() - startTime) / 1e3;
|
|
627
|
+
if (result.error || !result.data) {
|
|
628
|
+
return {
|
|
629
|
+
data: null,
|
|
630
|
+
error: result.error || "No response data received",
|
|
631
|
+
userMessage: storedUserMessage
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const responseData = result.data;
|
|
635
|
+
const assistantContent = responseData.choices?.[0]?.message?.content?.map((part) => part.text || "").join("") || "";
|
|
636
|
+
let storedAssistantMessage;
|
|
637
|
+
try {
|
|
638
|
+
storedAssistantMessage = await createMessageOp(storageCtx, {
|
|
639
|
+
conversationId: convId,
|
|
640
|
+
role: "assistant",
|
|
641
|
+
content: assistantContent,
|
|
642
|
+
model: responseData.model,
|
|
643
|
+
usage: convertUsageToStored(responseData.usage),
|
|
644
|
+
responseDuration
|
|
645
|
+
});
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return {
|
|
648
|
+
data: null,
|
|
649
|
+
error: err instanceof Error ? err.message : "Failed to store assistant message",
|
|
650
|
+
userMessage: storedUserMessage
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
data: responseData,
|
|
655
|
+
error: null,
|
|
656
|
+
userMessage: storedUserMessage,
|
|
657
|
+
assistantMessage: storedAssistantMessage
|
|
658
|
+
};
|
|
659
|
+
},
|
|
660
|
+
[ensureConversation, getMessages, storageCtx, baseSendMessage]
|
|
661
|
+
);
|
|
662
|
+
return {
|
|
663
|
+
isLoading,
|
|
664
|
+
sendMessage,
|
|
665
|
+
stop,
|
|
666
|
+
conversationId: currentConversationId,
|
|
667
|
+
setConversationId: setCurrentConversationId,
|
|
668
|
+
createConversation,
|
|
669
|
+
getConversation,
|
|
670
|
+
getConversations,
|
|
671
|
+
updateConversationTitle,
|
|
672
|
+
deleteConversation,
|
|
673
|
+
getMessages,
|
|
674
|
+
getMessageCount,
|
|
675
|
+
clearMessages
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
283
679
|
// src/react/useImageGeneration.ts
|
|
284
|
-
import { useCallback as
|
|
680
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
|
|
285
681
|
|
|
286
682
|
// src/client/core/bodySerializer.gen.ts
|
|
287
683
|
var jsonBodySerializer = {
|
|
@@ -1112,7 +1508,7 @@ var getApiV1Models = (options) => {
|
|
|
1112
1508
|
// src/react/useImageGeneration.ts
|
|
1113
1509
|
function useImageGeneration(options = {}) {
|
|
1114
1510
|
const { getToken, baseUrl = BASE_URL, onFinish, onError } = options;
|
|
1115
|
-
const [isLoading, setIsLoading] =
|
|
1511
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
1116
1512
|
const abortControllerRef = useRef2(null);
|
|
1117
1513
|
useEffect2(() => {
|
|
1118
1514
|
return () => {
|
|
@@ -1122,13 +1518,13 @@ function useImageGeneration(options = {}) {
|
|
|
1122
1518
|
}
|
|
1123
1519
|
};
|
|
1124
1520
|
}, []);
|
|
1125
|
-
const stop =
|
|
1521
|
+
const stop = useCallback3(() => {
|
|
1126
1522
|
if (abortControllerRef.current) {
|
|
1127
1523
|
abortControllerRef.current.abort();
|
|
1128
1524
|
abortControllerRef.current = null;
|
|
1129
1525
|
}
|
|
1130
1526
|
}, []);
|
|
1131
|
-
const generateImage =
|
|
1527
|
+
const generateImage = useCallback3(
|
|
1132
1528
|
async (args) => {
|
|
1133
1529
|
if (abortControllerRef.current) {
|
|
1134
1530
|
abortControllerRef.current.abort();
|
|
@@ -1191,12 +1587,12 @@ function useImageGeneration(options = {}) {
|
|
|
1191
1587
|
}
|
|
1192
1588
|
|
|
1193
1589
|
// src/react/useModels.ts
|
|
1194
|
-
import { useCallback as
|
|
1590
|
+
import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
|
|
1195
1591
|
function useModels(options = {}) {
|
|
1196
1592
|
const { getToken, baseUrl = BASE_URL, provider, autoFetch = true } = options;
|
|
1197
|
-
const [models, setModels] =
|
|
1198
|
-
const [isLoading, setIsLoading] =
|
|
1199
|
-
const [error, setError] =
|
|
1593
|
+
const [models, setModels] = useState4([]);
|
|
1594
|
+
const [isLoading, setIsLoading] = useState4(false);
|
|
1595
|
+
const [error, setError] = useState4(null);
|
|
1200
1596
|
const getTokenRef = useRef3(getToken);
|
|
1201
1597
|
const baseUrlRef = useRef3(baseUrl);
|
|
1202
1598
|
const providerRef = useRef3(provider);
|
|
@@ -1214,7 +1610,7 @@ function useModels(options = {}) {
|
|
|
1214
1610
|
}
|
|
1215
1611
|
};
|
|
1216
1612
|
}, []);
|
|
1217
|
-
const fetchModels =
|
|
1613
|
+
const fetchModels = useCallback4(async () => {
|
|
1218
1614
|
if (abortControllerRef.current) {
|
|
1219
1615
|
abortControllerRef.current.abort();
|
|
1220
1616
|
}
|
|
@@ -1272,7 +1668,7 @@ function useModels(options = {}) {
|
|
|
1272
1668
|
}
|
|
1273
1669
|
}
|
|
1274
1670
|
}, []);
|
|
1275
|
-
const refetch =
|
|
1671
|
+
const refetch = useCallback4(async () => {
|
|
1276
1672
|
setModels([]);
|
|
1277
1673
|
await fetchModels();
|
|
1278
1674
|
}, [fetchModels]);
|
|
@@ -1293,8 +1689,1243 @@ function useModels(options = {}) {
|
|
|
1293
1689
|
refetch
|
|
1294
1690
|
};
|
|
1295
1691
|
}
|
|
1692
|
+
|
|
1693
|
+
// src/expo/useMemoryStorage.ts
|
|
1694
|
+
import { useCallback as useCallback5, useState as useState5, useMemo as useMemo2, useRef as useRef4 } from "react";
|
|
1695
|
+
import { postApiV1ChatCompletions } from "@reverbia/sdk";
|
|
1696
|
+
|
|
1697
|
+
// src/lib/memoryStorage/operations.ts
|
|
1698
|
+
import { Q as Q2 } from "@nozbe/watermelondb";
|
|
1699
|
+
|
|
1700
|
+
// src/lib/memoryStorage/types.ts
|
|
1701
|
+
function generateCompositeKey(namespace, key) {
|
|
1702
|
+
return `${namespace}:${key}`;
|
|
1703
|
+
}
|
|
1704
|
+
function generateUniqueKey(namespace, key, value) {
|
|
1705
|
+
return `${namespace}:${key}:${value}`;
|
|
1706
|
+
}
|
|
1707
|
+
function cosineSimilarity(a, b) {
|
|
1708
|
+
if (a.length !== b.length) {
|
|
1709
|
+
throw new Error("Vectors must have the same length");
|
|
1710
|
+
}
|
|
1711
|
+
let dotProduct = 0;
|
|
1712
|
+
let normA = 0;
|
|
1713
|
+
let normB = 0;
|
|
1714
|
+
for (let i = 0; i < a.length; i++) {
|
|
1715
|
+
dotProduct += a[i] * b[i];
|
|
1716
|
+
normA += a[i] * a[i];
|
|
1717
|
+
normB += b[i] * b[i];
|
|
1718
|
+
}
|
|
1719
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1720
|
+
if (denominator === 0) {
|
|
1721
|
+
return 0;
|
|
1722
|
+
}
|
|
1723
|
+
return dotProduct / denominator;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/lib/memoryStorage/operations.ts
|
|
1727
|
+
function memoryToStored(memory) {
|
|
1728
|
+
return {
|
|
1729
|
+
uniqueId: memory.id,
|
|
1730
|
+
type: memory.type,
|
|
1731
|
+
namespace: memory.namespace,
|
|
1732
|
+
key: memory.key,
|
|
1733
|
+
value: memory.value,
|
|
1734
|
+
rawEvidence: memory.rawEvidence,
|
|
1735
|
+
confidence: memory.confidence,
|
|
1736
|
+
pii: memory.pii,
|
|
1737
|
+
compositeKey: memory.compositeKey,
|
|
1738
|
+
uniqueKey: memory.uniqueKey,
|
|
1739
|
+
createdAt: memory.createdAt,
|
|
1740
|
+
updatedAt: memory.updatedAt,
|
|
1741
|
+
embedding: memory.embedding,
|
|
1742
|
+
embeddingModel: memory.embeddingModel,
|
|
1743
|
+
isDeleted: memory.isDeleted
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
async function getAllMemoriesOp(ctx) {
|
|
1747
|
+
const results = await ctx.memoriesCollection.query(Q2.where("is_deleted", false), Q2.sortBy("created_at", Q2.desc)).fetch();
|
|
1748
|
+
return results.map(memoryToStored);
|
|
1749
|
+
}
|
|
1750
|
+
async function getMemoryByIdOp(ctx, id) {
|
|
1751
|
+
try {
|
|
1752
|
+
const memory = await ctx.memoriesCollection.find(id);
|
|
1753
|
+
if (memory.isDeleted) return null;
|
|
1754
|
+
return memoryToStored(memory);
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
async function getMemoriesByNamespaceOp(ctx, namespace) {
|
|
1760
|
+
const results = await ctx.memoriesCollection.query(
|
|
1761
|
+
Q2.where("namespace", namespace),
|
|
1762
|
+
Q2.where("is_deleted", false),
|
|
1763
|
+
Q2.sortBy("created_at", Q2.desc)
|
|
1764
|
+
).fetch();
|
|
1765
|
+
return results.map(memoryToStored);
|
|
1766
|
+
}
|
|
1767
|
+
async function getMemoriesByKeyOp(ctx, namespace, key) {
|
|
1768
|
+
const compositeKey = generateCompositeKey(namespace, key);
|
|
1769
|
+
const results = await ctx.memoriesCollection.query(
|
|
1770
|
+
Q2.where("composite_key", compositeKey),
|
|
1771
|
+
Q2.where("is_deleted", false),
|
|
1772
|
+
Q2.sortBy("created_at", Q2.desc)
|
|
1773
|
+
).fetch();
|
|
1774
|
+
return results.map(memoryToStored);
|
|
1775
|
+
}
|
|
1776
|
+
async function saveMemoryOp(ctx, opts) {
|
|
1777
|
+
const compositeKey = generateCompositeKey(opts.namespace, opts.key);
|
|
1778
|
+
const uniqueKey = generateUniqueKey(opts.namespace, opts.key, opts.value);
|
|
1779
|
+
const result = await ctx.database.write(async () => {
|
|
1780
|
+
const existing = await ctx.memoriesCollection.query(Q2.where("unique_key", uniqueKey)).fetch();
|
|
1781
|
+
if (existing.length > 0) {
|
|
1782
|
+
const existingMemory = existing[0];
|
|
1783
|
+
const shouldPreserveEmbedding = existingMemory.value === opts.value && existingMemory.rawEvidence === opts.rawEvidence && existingMemory.type === opts.type && existingMemory.namespace === opts.namespace && existingMemory.key === opts.key && existingMemory.embedding !== void 0 && existingMemory.embedding.length > 0 && !opts.embedding;
|
|
1784
|
+
await existingMemory.update((mem) => {
|
|
1785
|
+
mem._setRaw("type", opts.type);
|
|
1786
|
+
mem._setRaw("namespace", opts.namespace);
|
|
1787
|
+
mem._setRaw("key", opts.key);
|
|
1788
|
+
mem._setRaw("value", opts.value);
|
|
1789
|
+
mem._setRaw("raw_evidence", opts.rawEvidence);
|
|
1790
|
+
mem._setRaw("confidence", opts.confidence);
|
|
1791
|
+
mem._setRaw("pii", opts.pii);
|
|
1792
|
+
mem._setRaw("composite_key", compositeKey);
|
|
1793
|
+
mem._setRaw("unique_key", uniqueKey);
|
|
1794
|
+
mem._setRaw("is_deleted", false);
|
|
1795
|
+
if (shouldPreserveEmbedding) {
|
|
1796
|
+
} else if (opts.embedding) {
|
|
1797
|
+
mem._setRaw("embedding", JSON.stringify(opts.embedding));
|
|
1798
|
+
if (opts.embeddingModel) {
|
|
1799
|
+
mem._setRaw("embedding_model", opts.embeddingModel);
|
|
1800
|
+
}
|
|
1801
|
+
} else {
|
|
1802
|
+
mem._setRaw("embedding", null);
|
|
1803
|
+
mem._setRaw("embedding_model", null);
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
return existingMemory;
|
|
1807
|
+
}
|
|
1808
|
+
return await ctx.memoriesCollection.create((mem) => {
|
|
1809
|
+
mem._setRaw("type", opts.type);
|
|
1810
|
+
mem._setRaw("namespace", opts.namespace);
|
|
1811
|
+
mem._setRaw("key", opts.key);
|
|
1812
|
+
mem._setRaw("value", opts.value);
|
|
1813
|
+
mem._setRaw("raw_evidence", opts.rawEvidence);
|
|
1814
|
+
mem._setRaw("confidence", opts.confidence);
|
|
1815
|
+
mem._setRaw("pii", opts.pii);
|
|
1816
|
+
mem._setRaw("composite_key", compositeKey);
|
|
1817
|
+
mem._setRaw("unique_key", uniqueKey);
|
|
1818
|
+
mem._setRaw("is_deleted", false);
|
|
1819
|
+
if (opts.embedding) {
|
|
1820
|
+
mem._setRaw("embedding", JSON.stringify(opts.embedding));
|
|
1821
|
+
if (opts.embeddingModel) {
|
|
1822
|
+
mem._setRaw("embedding_model", opts.embeddingModel);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
return memoryToStored(result);
|
|
1828
|
+
}
|
|
1829
|
+
async function saveMemoriesOp(ctx, memories) {
|
|
1830
|
+
const results = [];
|
|
1831
|
+
for (const memory of memories) {
|
|
1832
|
+
const saved = await saveMemoryOp(ctx, memory);
|
|
1833
|
+
results.push(saved);
|
|
1834
|
+
}
|
|
1835
|
+
return results;
|
|
1836
|
+
}
|
|
1837
|
+
async function updateMemoryOp(ctx, id, updates) {
|
|
1838
|
+
let memory;
|
|
1839
|
+
try {
|
|
1840
|
+
memory = await ctx.memoriesCollection.find(id);
|
|
1841
|
+
} catch {
|
|
1842
|
+
return { ok: false, reason: "not_found" };
|
|
1843
|
+
}
|
|
1844
|
+
if (memory.isDeleted) {
|
|
1845
|
+
return { ok: false, reason: "not_found" };
|
|
1846
|
+
}
|
|
1847
|
+
const newNamespace = updates.namespace ?? memory.namespace;
|
|
1848
|
+
const newKey = updates.key ?? memory.key;
|
|
1849
|
+
const newValue = updates.value ?? memory.value;
|
|
1850
|
+
const newCompositeKey = generateCompositeKey(newNamespace, newKey);
|
|
1851
|
+
const newUniqueKey = generateUniqueKey(newNamespace, newKey, newValue);
|
|
1852
|
+
if (newUniqueKey !== memory.uniqueKey) {
|
|
1853
|
+
const existing = await ctx.memoriesCollection.query(Q2.where("unique_key", newUniqueKey), Q2.where("is_deleted", false)).fetch();
|
|
1854
|
+
if (existing.length > 0) {
|
|
1855
|
+
return { ok: false, reason: "conflict", conflictingKey: newUniqueKey };
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
try {
|
|
1859
|
+
const updated = await ctx.database.write(async () => {
|
|
1860
|
+
await memory.update((mem) => {
|
|
1861
|
+
if (updates.type !== void 0) mem._setRaw("type", updates.type);
|
|
1862
|
+
if (updates.namespace !== void 0)
|
|
1863
|
+
mem._setRaw("namespace", updates.namespace);
|
|
1864
|
+
if (updates.key !== void 0) mem._setRaw("key", updates.key);
|
|
1865
|
+
if (updates.value !== void 0) mem._setRaw("value", updates.value);
|
|
1866
|
+
if (updates.rawEvidence !== void 0)
|
|
1867
|
+
mem._setRaw("raw_evidence", updates.rawEvidence);
|
|
1868
|
+
if (updates.confidence !== void 0)
|
|
1869
|
+
mem._setRaw("confidence", updates.confidence);
|
|
1870
|
+
if (updates.pii !== void 0) mem._setRaw("pii", updates.pii);
|
|
1871
|
+
if (updates.namespace !== void 0 || updates.key !== void 0 || updates.value !== void 0) {
|
|
1872
|
+
mem._setRaw("composite_key", newCompositeKey);
|
|
1873
|
+
mem._setRaw("unique_key", newUniqueKey);
|
|
1874
|
+
}
|
|
1875
|
+
if (updates.embedding !== void 0) {
|
|
1876
|
+
mem._setRaw(
|
|
1877
|
+
"embedding",
|
|
1878
|
+
updates.embedding ? JSON.stringify(updates.embedding) : null
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
if (updates.embeddingModel !== void 0) {
|
|
1882
|
+
mem._setRaw("embedding_model", updates.embeddingModel || null);
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
return memory;
|
|
1886
|
+
});
|
|
1887
|
+
return { ok: true, memory: memoryToStored(updated) };
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
return {
|
|
1890
|
+
ok: false,
|
|
1891
|
+
reason: "error",
|
|
1892
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
async function deleteMemoryByIdOp(ctx, id) {
|
|
1897
|
+
try {
|
|
1898
|
+
const memory = await ctx.memoriesCollection.find(id);
|
|
1899
|
+
await ctx.database.write(async () => {
|
|
1900
|
+
await memory.update((mem) => {
|
|
1901
|
+
mem._setRaw("is_deleted", true);
|
|
1902
|
+
});
|
|
1903
|
+
});
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
async function deleteMemoryOp(ctx, namespace, key, value) {
|
|
1908
|
+
const uniqueKey = generateUniqueKey(namespace, key, value);
|
|
1909
|
+
const results = await ctx.memoriesCollection.query(Q2.where("unique_key", uniqueKey)).fetch();
|
|
1910
|
+
if (results.length > 0) {
|
|
1911
|
+
await ctx.database.write(async () => {
|
|
1912
|
+
await results[0].update((mem) => {
|
|
1913
|
+
mem._setRaw("is_deleted", true);
|
|
1914
|
+
});
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
async function deleteMemoriesByKeyOp(ctx, namespace, key) {
|
|
1919
|
+
const compositeKey = generateCompositeKey(namespace, key);
|
|
1920
|
+
const results = await ctx.memoriesCollection.query(Q2.where("composite_key", compositeKey), Q2.where("is_deleted", false)).fetch();
|
|
1921
|
+
await ctx.database.write(async () => {
|
|
1922
|
+
for (const memory of results) {
|
|
1923
|
+
await memory.update((mem) => {
|
|
1924
|
+
mem._setRaw("is_deleted", true);
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
async function clearAllMemoriesOp(ctx) {
|
|
1930
|
+
const results = await ctx.memoriesCollection.query(Q2.where("is_deleted", false)).fetch();
|
|
1931
|
+
await ctx.database.write(async () => {
|
|
1932
|
+
for (const memory of results) {
|
|
1933
|
+
await memory.update((mem) => {
|
|
1934
|
+
mem._setRaw("is_deleted", true);
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
async function searchSimilarMemoriesOp(ctx, queryEmbedding, limit = 10, minSimilarity = 0.6) {
|
|
1940
|
+
const allMemories = await ctx.memoriesCollection.query(Q2.where("is_deleted", false)).fetch();
|
|
1941
|
+
const memoriesWithEmbeddings = allMemories.filter(
|
|
1942
|
+
(m) => m.embedding && m.embedding.length > 0
|
|
1943
|
+
);
|
|
1944
|
+
if (memoriesWithEmbeddings.length === 0) {
|
|
1945
|
+
return [];
|
|
1946
|
+
}
|
|
1947
|
+
const results = memoriesWithEmbeddings.map((memory) => {
|
|
1948
|
+
const similarity = cosineSimilarity(queryEmbedding, memory.embedding);
|
|
1949
|
+
return {
|
|
1950
|
+
...memoryToStored(memory),
|
|
1951
|
+
similarity
|
|
1952
|
+
};
|
|
1953
|
+
}).filter((result) => result.similarity >= minSimilarity).sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
1954
|
+
return results;
|
|
1955
|
+
}
|
|
1956
|
+
async function updateMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
|
|
1957
|
+
try {
|
|
1958
|
+
const memory = await ctx.memoriesCollection.find(id);
|
|
1959
|
+
await ctx.database.write(async () => {
|
|
1960
|
+
await memory.update((mem) => {
|
|
1961
|
+
mem._setRaw("embedding", JSON.stringify(embedding));
|
|
1962
|
+
mem._setRaw("embedding_model", embeddingModel);
|
|
1963
|
+
});
|
|
1964
|
+
});
|
|
1965
|
+
} catch {
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/lib/memory/service.ts
|
|
1970
|
+
var FACT_EXTRACTION_PROMPT = `You are a memory extraction system. Extract durable user memories from chat messages.
|
|
1971
|
+
|
|
1972
|
+
CRITICAL: You MUST respond with ONLY valid JSON. No explanations, no markdown, no code blocks, just pure JSON.
|
|
1973
|
+
|
|
1974
|
+
Only store clear, factual statements that might be relevant for future context or reference. Extract facts that will be useful in future conversations, such as identity, stable preferences, ongoing projects, skills, locations, favorites, and constraints.
|
|
1975
|
+
|
|
1976
|
+
Do not extract sensitive attributes, temporary things, or single-use instructions.
|
|
1977
|
+
|
|
1978
|
+
You must also extract stable personal preferences, including food likes/dislikes, hobbies, favorite items, favorite genres, or other enduring tastes.
|
|
1979
|
+
|
|
1980
|
+
If there are no memories to extract, return: {"items": []}
|
|
1981
|
+
|
|
1982
|
+
Response format (JSON only, no other text):
|
|
1983
|
+
|
|
1984
|
+
{
|
|
1985
|
+
"items": [
|
|
1986
|
+
{
|
|
1987
|
+
"type": "identity",
|
|
1988
|
+
"namespace": "identity",
|
|
1989
|
+
"key": "name",
|
|
1990
|
+
"value": "Charlie",
|
|
1991
|
+
"rawEvidence": "I'm Charlie",
|
|
1992
|
+
"confidence": 0.98,
|
|
1993
|
+
"pii": true
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
"type": "identity",
|
|
1997
|
+
"namespace": "work",
|
|
1998
|
+
"key": "company",
|
|
1999
|
+
"value": "ZetaChain",
|
|
2000
|
+
"rawEvidence": "called ZetaChain",
|
|
2001
|
+
"confidence": 0.99,
|
|
2002
|
+
"pii": false
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
"type": "identity",
|
|
2006
|
+
"namespace": "location",
|
|
2007
|
+
"key": "city",
|
|
2008
|
+
"value": "San Francisco",
|
|
2009
|
+
"rawEvidence": "I live in San Francisco",
|
|
2010
|
+
"confidence": 0.99,
|
|
2011
|
+
"pii": false
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"type": "preference",
|
|
2015
|
+
"namespace": "location",
|
|
2016
|
+
"key": "country",
|
|
2017
|
+
"value": "Japan",
|
|
2018
|
+
"rawEvidence": "I like to travel to the Japan",
|
|
2019
|
+
"confidence": 0.94,
|
|
2020
|
+
"pii": false
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
"type": "preference",
|
|
2024
|
+
"namespace": "answer_style",
|
|
2025
|
+
"key": "verbosity",
|
|
2026
|
+
"value": "concise_direct",
|
|
2027
|
+
"rawEvidence": "I prefer concise, direct answers",
|
|
2028
|
+
"confidence": 0.96,
|
|
2029
|
+
"pii": false
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
"type": "identity",
|
|
2033
|
+
"namespace": "timezone",
|
|
2034
|
+
"key": "tz",
|
|
2035
|
+
"value": "America/Los_Angeles",
|
|
2036
|
+
"rawEvidence": "I'm in PST",
|
|
2037
|
+
"confidence": 0.9,
|
|
2038
|
+
"pii": false
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
"type": "preference",
|
|
2042
|
+
"namespace": "food",
|
|
2043
|
+
"key": "likes_ice_cream",
|
|
2044
|
+
"value": "ice cream",
|
|
2045
|
+
"rawEvidence": "I like ice cream",
|
|
2046
|
+
"confidence": 0.95,
|
|
2047
|
+
"pii": false
|
|
2048
|
+
}
|
|
2049
|
+
]
|
|
2050
|
+
}`;
|
|
2051
|
+
var preprocessMemories = (items, minConfidence = 0.6) => {
|
|
2052
|
+
if (!items || !Array.isArray(items)) {
|
|
2053
|
+
return [];
|
|
2054
|
+
}
|
|
2055
|
+
const validItems = items.filter((item) => {
|
|
2056
|
+
if (item.namespace == null || item.key == null || item.value == null) {
|
|
2057
|
+
console.warn(
|
|
2058
|
+
"Dropping memory item with null/undefined namespace, key, or value:",
|
|
2059
|
+
item
|
|
2060
|
+
);
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
const namespace = String(item.namespace).trim();
|
|
2064
|
+
const key = String(item.key).trim();
|
|
2065
|
+
const value = String(item.value).trim();
|
|
2066
|
+
if (namespace === "" || key === "" || value === "") {
|
|
2067
|
+
console.warn(
|
|
2068
|
+
"Dropping memory item with empty namespace, key, or value after trimming:",
|
|
2069
|
+
item
|
|
2070
|
+
);
|
|
2071
|
+
return false;
|
|
2072
|
+
}
|
|
2073
|
+
if (typeof item.confidence !== "number" || item.confidence < minConfidence) {
|
|
2074
|
+
console.warn(
|
|
2075
|
+
`Dropping memory item with confidence ${item.confidence} below threshold ${minConfidence}:`,
|
|
2076
|
+
item
|
|
2077
|
+
);
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
return true;
|
|
2081
|
+
});
|
|
2082
|
+
const deduplicatedMap = /* @__PURE__ */ new Map();
|
|
2083
|
+
for (const item of validItems) {
|
|
2084
|
+
const uniqueKey = `${item.namespace}:${item.key}:${item.value}`;
|
|
2085
|
+
const existing = deduplicatedMap.get(uniqueKey);
|
|
2086
|
+
if (!existing || item.confidence > existing.confidence) {
|
|
2087
|
+
deduplicatedMap.set(uniqueKey, item);
|
|
2088
|
+
} else {
|
|
2089
|
+
console.debug(
|
|
2090
|
+
`Deduplicating memory item: keeping entry with higher confidence (${existing.confidence} > ${item.confidence})`,
|
|
2091
|
+
{ namespace: item.namespace, key: item.key, value: item.value }
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return Array.from(deduplicatedMap.values());
|
|
2096
|
+
};
|
|
2097
|
+
|
|
2098
|
+
// src/lib/memory/constants.ts
|
|
2099
|
+
var DEFAULT_API_EMBEDDING_MODEL = "openai/text-embedding-3-small";
|
|
2100
|
+
var DEFAULT_COMPLETION_MODEL = "openai/gpt-4o";
|
|
2101
|
+
|
|
2102
|
+
// src/expo/useMemoryStorage.ts
|
|
2103
|
+
import { postApiV1Embeddings } from "@reverbia/sdk";
|
|
2104
|
+
async function generateEmbeddingForTextApi(text, options) {
|
|
2105
|
+
const token = options.getToken ? await options.getToken() : null;
|
|
2106
|
+
if (!token) {
|
|
2107
|
+
throw new Error("No auth token available for embedding generation");
|
|
2108
|
+
}
|
|
2109
|
+
const response = await postApiV1Embeddings({
|
|
2110
|
+
baseUrl: options.baseUrl,
|
|
2111
|
+
body: {
|
|
2112
|
+
input: text,
|
|
2113
|
+
model: options.model
|
|
2114
|
+
},
|
|
2115
|
+
headers: {
|
|
2116
|
+
Authorization: `Bearer ${token}`
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
if (!response.data || typeof response.data === "string") {
|
|
2120
|
+
throw new Error("Failed to generate embedding");
|
|
2121
|
+
}
|
|
2122
|
+
const embedding = response.data.data?.[0]?.embedding;
|
|
2123
|
+
if (!embedding) {
|
|
2124
|
+
throw new Error("No embedding in response");
|
|
2125
|
+
}
|
|
2126
|
+
return embedding;
|
|
2127
|
+
}
|
|
2128
|
+
async function generateEmbeddingForMemoryApi(memory, options) {
|
|
2129
|
+
const text = `${memory.type}: ${memory.namespace}/${memory.key} = ${memory.value}. Evidence: ${memory.rawEvidence}`;
|
|
2130
|
+
return generateEmbeddingForTextApi(text, options);
|
|
2131
|
+
}
|
|
2132
|
+
function useMemoryStorage(options) {
|
|
2133
|
+
const {
|
|
2134
|
+
database,
|
|
2135
|
+
completionsModel = DEFAULT_COMPLETION_MODEL,
|
|
2136
|
+
embeddingModel: userEmbeddingModel,
|
|
2137
|
+
embeddingProvider = "api",
|
|
2138
|
+
// Default to API for Expo
|
|
2139
|
+
generateEmbeddings = true,
|
|
2140
|
+
onFactsExtracted,
|
|
2141
|
+
getToken,
|
|
2142
|
+
baseUrl = BASE_URL
|
|
2143
|
+
} = options;
|
|
2144
|
+
const embeddingModel = userEmbeddingModel === void 0 ? DEFAULT_API_EMBEDDING_MODEL : userEmbeddingModel;
|
|
2145
|
+
const [memories, setMemories] = useState5([]);
|
|
2146
|
+
const extractionInProgressRef = useRef4(false);
|
|
2147
|
+
const memoriesCollection = useMemo2(
|
|
2148
|
+
() => database.get("memories"),
|
|
2149
|
+
[database]
|
|
2150
|
+
);
|
|
2151
|
+
const storageCtx = useMemo2(
|
|
2152
|
+
() => ({
|
|
2153
|
+
database,
|
|
2154
|
+
memoriesCollection
|
|
2155
|
+
}),
|
|
2156
|
+
[database, memoriesCollection]
|
|
2157
|
+
);
|
|
2158
|
+
const embeddingOptions = useMemo2(
|
|
2159
|
+
() => ({
|
|
2160
|
+
model: embeddingModel ?? DEFAULT_API_EMBEDDING_MODEL,
|
|
2161
|
+
getToken: getToken || void 0,
|
|
2162
|
+
baseUrl
|
|
2163
|
+
}),
|
|
2164
|
+
[embeddingModel, getToken, baseUrl]
|
|
2165
|
+
);
|
|
2166
|
+
const refreshMemories = useCallback5(async () => {
|
|
2167
|
+
const storedMemories = await getAllMemoriesOp(storageCtx);
|
|
2168
|
+
setMemories(storedMemories);
|
|
2169
|
+
}, [storageCtx]);
|
|
2170
|
+
const extractMemoriesFromMessage = useCallback5(
|
|
2171
|
+
async (opts) => {
|
|
2172
|
+
const { messages, model } = opts;
|
|
2173
|
+
if (!getToken || extractionInProgressRef.current) {
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
extractionInProgressRef.current = true;
|
|
2177
|
+
try {
|
|
2178
|
+
const token = await getToken();
|
|
2179
|
+
if (!token) {
|
|
2180
|
+
console.error("No access token available for memory extraction");
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
const completion = await postApiV1ChatCompletions({
|
|
2184
|
+
baseUrl,
|
|
2185
|
+
body: {
|
|
2186
|
+
messages: [
|
|
2187
|
+
{
|
|
2188
|
+
role: "system",
|
|
2189
|
+
content: [{ type: "text", text: FACT_EXTRACTION_PROMPT }]
|
|
2190
|
+
},
|
|
2191
|
+
...messages.map((m) => ({
|
|
2192
|
+
role: m.role,
|
|
2193
|
+
content: [{ type: "text", text: m.content }]
|
|
2194
|
+
}))
|
|
2195
|
+
],
|
|
2196
|
+
model: model || completionsModel
|
|
2197
|
+
},
|
|
2198
|
+
headers: {
|
|
2199
|
+
Authorization: `Bearer ${token}`
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
if (!completion.data) {
|
|
2203
|
+
console.error(
|
|
2204
|
+
"Memory extraction failed:",
|
|
2205
|
+
completion.error?.error ?? "API did not return a response"
|
|
2206
|
+
);
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
if (typeof completion.data === "string") {
|
|
2210
|
+
console.error(
|
|
2211
|
+
"Memory extraction failed: API returned a string response"
|
|
2212
|
+
);
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
const messageContent = completion.data.choices?.[0]?.message?.content;
|
|
2216
|
+
let content = "";
|
|
2217
|
+
if (Array.isArray(messageContent)) {
|
|
2218
|
+
content = messageContent.map((p) => p.text || "").join("").trim();
|
|
2219
|
+
} else if (typeof messageContent === "string") {
|
|
2220
|
+
content = messageContent.trim();
|
|
2221
|
+
}
|
|
2222
|
+
if (!content) {
|
|
2223
|
+
console.error("No content in memory extraction response");
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
let jsonContent = content;
|
|
2227
|
+
jsonContent = jsonContent.replace(/^data:\s*/gm, "").trim();
|
|
2228
|
+
if (jsonContent.startsWith("{")) {
|
|
2229
|
+
let braceCount = 0;
|
|
2230
|
+
let jsonStart = -1;
|
|
2231
|
+
let jsonEnd = -1;
|
|
2232
|
+
for (let i = 0; i < jsonContent.length; i++) {
|
|
2233
|
+
if (jsonContent[i] === "{") {
|
|
2234
|
+
if (jsonStart === -1) jsonStart = i;
|
|
2235
|
+
braceCount++;
|
|
2236
|
+
} else if (jsonContent[i] === "}") {
|
|
2237
|
+
braceCount--;
|
|
2238
|
+
if (braceCount === 0 && jsonStart !== -1) {
|
|
2239
|
+
jsonEnd = i + 1;
|
|
2240
|
+
break;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (jsonStart !== -1 && jsonEnd !== -1) {
|
|
2245
|
+
jsonContent = jsonContent.substring(jsonStart, jsonEnd);
|
|
2246
|
+
}
|
|
2247
|
+
} else {
|
|
2248
|
+
const jsonMatch = jsonContent.match(
|
|
2249
|
+
/```(?:json)?\s*(\{[\s\S]*?\})\s*```/
|
|
2250
|
+
);
|
|
2251
|
+
if (jsonMatch && jsonMatch[1]) {
|
|
2252
|
+
jsonContent = jsonMatch[1].trim();
|
|
2253
|
+
} else {
|
|
2254
|
+
const jsonObjectMatch = jsonContent.match(/\{[\s\S]*\}/);
|
|
2255
|
+
if (jsonObjectMatch && jsonObjectMatch[0]) {
|
|
2256
|
+
jsonContent = jsonObjectMatch[0];
|
|
2257
|
+
} else {
|
|
2258
|
+
console.warn("Memory extraction returned non-JSON response.");
|
|
2259
|
+
return { items: [] };
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
const trimmedJson = jsonContent.trim();
|
|
2264
|
+
if (!trimmedJson.startsWith("{") || !trimmedJson.includes("items")) {
|
|
2265
|
+
console.warn("Memory extraction response doesn't appear to be valid JSON.");
|
|
2266
|
+
return { items: [] };
|
|
2267
|
+
}
|
|
2268
|
+
let result;
|
|
2269
|
+
try {
|
|
2270
|
+
result = JSON.parse(jsonContent);
|
|
2271
|
+
if (!result || typeof result !== "object") {
|
|
2272
|
+
throw new Error("Invalid JSON structure: not an object");
|
|
2273
|
+
}
|
|
2274
|
+
if (!Array.isArray(result.items)) {
|
|
2275
|
+
console.warn("Memory extraction result missing 'items' array.");
|
|
2276
|
+
return { items: [] };
|
|
2277
|
+
}
|
|
2278
|
+
} catch (parseError) {
|
|
2279
|
+
console.error(
|
|
2280
|
+
"Failed to parse memory extraction JSON:",
|
|
2281
|
+
parseError instanceof Error ? parseError.message : "Unknown error"
|
|
2282
|
+
);
|
|
2283
|
+
return { items: [] };
|
|
2284
|
+
}
|
|
2285
|
+
if (result.items && Array.isArray(result.items)) {
|
|
2286
|
+
result.items = preprocessMemories(result.items);
|
|
2287
|
+
}
|
|
2288
|
+
if (result.items && result.items.length > 0) {
|
|
2289
|
+
try {
|
|
2290
|
+
const createOptions = result.items.map(
|
|
2291
|
+
(item) => ({
|
|
2292
|
+
type: item.type,
|
|
2293
|
+
namespace: item.namespace,
|
|
2294
|
+
key: item.key,
|
|
2295
|
+
value: item.value,
|
|
2296
|
+
rawEvidence: item.rawEvidence,
|
|
2297
|
+
confidence: item.confidence,
|
|
2298
|
+
pii: item.pii
|
|
2299
|
+
})
|
|
2300
|
+
);
|
|
2301
|
+
const savedMemories = await saveMemoriesOp(storageCtx, createOptions);
|
|
2302
|
+
console.log(`Saved ${savedMemories.length} memories to WatermelonDB`);
|
|
2303
|
+
if (generateEmbeddings && embeddingModel) {
|
|
2304
|
+
try {
|
|
2305
|
+
for (const saved of savedMemories) {
|
|
2306
|
+
const memoryItem = {
|
|
2307
|
+
type: saved.type,
|
|
2308
|
+
namespace: saved.namespace,
|
|
2309
|
+
key: saved.key,
|
|
2310
|
+
value: saved.value,
|
|
2311
|
+
rawEvidence: saved.rawEvidence,
|
|
2312
|
+
confidence: saved.confidence,
|
|
2313
|
+
pii: saved.pii
|
|
2314
|
+
};
|
|
2315
|
+
const embedding = await generateEmbeddingForMemoryApi(
|
|
2316
|
+
memoryItem,
|
|
2317
|
+
embeddingOptions
|
|
2318
|
+
);
|
|
2319
|
+
await updateMemoryEmbeddingOp(
|
|
2320
|
+
storageCtx,
|
|
2321
|
+
saved.uniqueId,
|
|
2322
|
+
embedding,
|
|
2323
|
+
embeddingOptions.model
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
console.log(`Generated embeddings for ${savedMemories.length} memories`);
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
console.error("Failed to generate embeddings:", error);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
await refreshMemories();
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
console.error("Failed to save memories to WatermelonDB:", error);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
if (onFactsExtracted) {
|
|
2337
|
+
onFactsExtracted(result);
|
|
2338
|
+
}
|
|
2339
|
+
return result;
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
console.error("Failed to extract facts:", error);
|
|
2342
|
+
return null;
|
|
2343
|
+
} finally {
|
|
2344
|
+
extractionInProgressRef.current = false;
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
[
|
|
2348
|
+
completionsModel,
|
|
2349
|
+
embeddingModel,
|
|
2350
|
+
embeddingOptions,
|
|
2351
|
+
generateEmbeddings,
|
|
2352
|
+
getToken,
|
|
2353
|
+
onFactsExtracted,
|
|
2354
|
+
baseUrl,
|
|
2355
|
+
storageCtx,
|
|
2356
|
+
refreshMemories
|
|
2357
|
+
]
|
|
2358
|
+
);
|
|
2359
|
+
const searchMemories = useCallback5(
|
|
2360
|
+
async (query, limit = 10, minSimilarity = 0.6) => {
|
|
2361
|
+
if (!embeddingModel) {
|
|
2362
|
+
console.warn("Cannot search memories: embeddingModel not provided");
|
|
2363
|
+
return [];
|
|
2364
|
+
}
|
|
2365
|
+
try {
|
|
2366
|
+
const queryEmbedding = await generateEmbeddingForTextApi(
|
|
2367
|
+
query,
|
|
2368
|
+
embeddingOptions
|
|
2369
|
+
);
|
|
2370
|
+
const results = await searchSimilarMemoriesOp(
|
|
2371
|
+
storageCtx,
|
|
2372
|
+
queryEmbedding,
|
|
2373
|
+
limit,
|
|
2374
|
+
minSimilarity
|
|
2375
|
+
);
|
|
2376
|
+
if (results.length === 0) {
|
|
2377
|
+
console.warn(
|
|
2378
|
+
`[Memory Search] No memories found above similarity threshold ${minSimilarity}.`
|
|
2379
|
+
);
|
|
2380
|
+
} else {
|
|
2381
|
+
console.log(
|
|
2382
|
+
`[Memory Search] Found ${results.length} memories. Similarity scores: ${results.map((r) => r.similarity.toFixed(3)).join(", ")}`
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
return results;
|
|
2386
|
+
} catch {
|
|
2387
|
+
return [];
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
2390
|
+
[embeddingModel, embeddingOptions, storageCtx]
|
|
2391
|
+
);
|
|
2392
|
+
const fetchAllMemories = useCallback5(async () => {
|
|
2393
|
+
try {
|
|
2394
|
+
return await getAllMemoriesOp(storageCtx);
|
|
2395
|
+
} catch (error) {
|
|
2396
|
+
throw new Error(
|
|
2397
|
+
"Failed to fetch all memories: " + (error instanceof Error ? error.message : String(error))
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
}, [storageCtx]);
|
|
2401
|
+
const fetchMemoriesByNamespace = useCallback5(
|
|
2402
|
+
async (namespace) => {
|
|
2403
|
+
if (!namespace) {
|
|
2404
|
+
throw new Error("Missing required field: namespace");
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
return await getMemoriesByNamespaceOp(storageCtx, namespace);
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
throw new Error(
|
|
2410
|
+
`Failed to fetch memories for namespace "${namespace}": ` + (error instanceof Error ? error.message : String(error))
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
},
|
|
2414
|
+
[storageCtx]
|
|
2415
|
+
);
|
|
2416
|
+
const fetchMemoriesByKey = useCallback5(
|
|
2417
|
+
async (namespace, key) => {
|
|
2418
|
+
if (!namespace || !key) {
|
|
2419
|
+
throw new Error("Missing required fields: namespace, key");
|
|
2420
|
+
}
|
|
2421
|
+
try {
|
|
2422
|
+
return await getMemoriesByKeyOp(storageCtx, namespace, key);
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
`Failed to fetch memories for "${namespace}:${key}": ` + (error instanceof Error ? error.message : String(error))
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
},
|
|
2429
|
+
[storageCtx]
|
|
2430
|
+
);
|
|
2431
|
+
const getMemoryById = useCallback5(
|
|
2432
|
+
async (id) => {
|
|
2433
|
+
try {
|
|
2434
|
+
return await getMemoryByIdOp(storageCtx, id);
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
throw new Error(
|
|
2437
|
+
`Failed to get memory ${id}: ` + (error instanceof Error ? error.message : String(error))
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
},
|
|
2441
|
+
[storageCtx]
|
|
2442
|
+
);
|
|
2443
|
+
const saveMemory = useCallback5(
|
|
2444
|
+
async (memory) => {
|
|
2445
|
+
try {
|
|
2446
|
+
const saved = await saveMemoryOp(storageCtx, memory);
|
|
2447
|
+
if (generateEmbeddings && embeddingModel && !memory.embedding) {
|
|
2448
|
+
try {
|
|
2449
|
+
const memoryItem = {
|
|
2450
|
+
type: memory.type,
|
|
2451
|
+
namespace: memory.namespace,
|
|
2452
|
+
key: memory.key,
|
|
2453
|
+
value: memory.value,
|
|
2454
|
+
rawEvidence: memory.rawEvidence,
|
|
2455
|
+
confidence: memory.confidence,
|
|
2456
|
+
pii: memory.pii
|
|
2457
|
+
};
|
|
2458
|
+
const embedding = await generateEmbeddingForMemoryApi(
|
|
2459
|
+
memoryItem,
|
|
2460
|
+
embeddingOptions
|
|
2461
|
+
);
|
|
2462
|
+
await updateMemoryEmbeddingOp(
|
|
2463
|
+
storageCtx,
|
|
2464
|
+
saved.uniqueId,
|
|
2465
|
+
embedding,
|
|
2466
|
+
embeddingOptions.model
|
|
2467
|
+
);
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
console.error("Failed to generate embedding:", error);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
setMemories((prev) => {
|
|
2473
|
+
const existing = prev.find((m) => m.uniqueId === saved.uniqueId);
|
|
2474
|
+
if (existing) {
|
|
2475
|
+
return prev.map((m) => m.uniqueId === saved.uniqueId ? saved : m);
|
|
2476
|
+
}
|
|
2477
|
+
return [saved, ...prev];
|
|
2478
|
+
});
|
|
2479
|
+
return saved;
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
throw new Error(
|
|
2482
|
+
"Failed to save memory: " + (error instanceof Error ? error.message : String(error))
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
},
|
|
2486
|
+
[storageCtx, generateEmbeddings, embeddingModel, embeddingOptions]
|
|
2487
|
+
);
|
|
2488
|
+
const saveMemories = useCallback5(
|
|
2489
|
+
async (memoriesToSave) => {
|
|
2490
|
+
try {
|
|
2491
|
+
const saved = await saveMemoriesOp(storageCtx, memoriesToSave);
|
|
2492
|
+
if (generateEmbeddings && embeddingModel) {
|
|
2493
|
+
for (let i = 0; i < saved.length; i++) {
|
|
2494
|
+
const memory = memoriesToSave[i];
|
|
2495
|
+
if (!memory.embedding) {
|
|
2496
|
+
try {
|
|
2497
|
+
const memoryItem = {
|
|
2498
|
+
type: memory.type,
|
|
2499
|
+
namespace: memory.namespace,
|
|
2500
|
+
key: memory.key,
|
|
2501
|
+
value: memory.value,
|
|
2502
|
+
rawEvidence: memory.rawEvidence,
|
|
2503
|
+
confidence: memory.confidence,
|
|
2504
|
+
pii: memory.pii
|
|
2505
|
+
};
|
|
2506
|
+
const embedding = await generateEmbeddingForMemoryApi(
|
|
2507
|
+
memoryItem,
|
|
2508
|
+
embeddingOptions
|
|
2509
|
+
);
|
|
2510
|
+
await updateMemoryEmbeddingOp(
|
|
2511
|
+
storageCtx,
|
|
2512
|
+
saved[i].uniqueId,
|
|
2513
|
+
embedding,
|
|
2514
|
+
embeddingOptions.model
|
|
2515
|
+
);
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
console.error("Failed to generate embedding:", error);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
await refreshMemories();
|
|
2523
|
+
return saved;
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
throw new Error(
|
|
2526
|
+
"Failed to save memories: " + (error instanceof Error ? error.message : String(error))
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
},
|
|
2530
|
+
[storageCtx, generateEmbeddings, embeddingModel, embeddingOptions, refreshMemories]
|
|
2531
|
+
);
|
|
2532
|
+
const updateMemory = useCallback5(
|
|
2533
|
+
async (id, updates) => {
|
|
2534
|
+
const result = await updateMemoryOp(storageCtx, id, updates);
|
|
2535
|
+
if (!result.ok) {
|
|
2536
|
+
if (result.reason === "not_found") {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
if (result.reason === "conflict") {
|
|
2540
|
+
throw new Error(
|
|
2541
|
+
`Cannot update memory: a memory with key "${result.conflictingKey}" already exists`
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
throw new Error(
|
|
2545
|
+
`Failed to update memory ${id}: ${result.error.message}`
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
const updated = result.memory;
|
|
2549
|
+
const contentChanged = updates.value !== void 0 || updates.rawEvidence !== void 0 || updates.type !== void 0 || updates.namespace !== void 0 || updates.key !== void 0;
|
|
2550
|
+
if (contentChanged && generateEmbeddings && embeddingModel && !updates.embedding) {
|
|
2551
|
+
try {
|
|
2552
|
+
const memoryItem = {
|
|
2553
|
+
type: updated.type,
|
|
2554
|
+
namespace: updated.namespace,
|
|
2555
|
+
key: updated.key,
|
|
2556
|
+
value: updated.value,
|
|
2557
|
+
rawEvidence: updated.rawEvidence,
|
|
2558
|
+
confidence: updated.confidence,
|
|
2559
|
+
pii: updated.pii
|
|
2560
|
+
};
|
|
2561
|
+
const embedding = await generateEmbeddingForMemoryApi(
|
|
2562
|
+
memoryItem,
|
|
2563
|
+
embeddingOptions
|
|
2564
|
+
);
|
|
2565
|
+
await updateMemoryEmbeddingOp(storageCtx, id, embedding, embeddingOptions.model);
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
console.error("Failed to regenerate embedding:", error);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
setMemories((prev) => prev.map((m) => m.uniqueId === id ? updated : m));
|
|
2571
|
+
return updated;
|
|
2572
|
+
},
|
|
2573
|
+
[storageCtx, generateEmbeddings, embeddingModel, embeddingOptions]
|
|
2574
|
+
);
|
|
2575
|
+
const removeMemory = useCallback5(
|
|
2576
|
+
async (namespace, key, value) => {
|
|
2577
|
+
if (!namespace || !key || !value) {
|
|
2578
|
+
throw new Error("Missing required fields: namespace, key, value");
|
|
2579
|
+
}
|
|
2580
|
+
try {
|
|
2581
|
+
await deleteMemoryOp(storageCtx, namespace, key, value);
|
|
2582
|
+
setMemories(
|
|
2583
|
+
(prev) => prev.filter(
|
|
2584
|
+
(m) => !(m.namespace === namespace && m.key === key && m.value === value)
|
|
2585
|
+
)
|
|
2586
|
+
);
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
throw new Error(
|
|
2589
|
+
`Failed to delete memory "${namespace}:${key}:${value}": ` + (error instanceof Error ? error.message : String(error))
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
},
|
|
2593
|
+
[storageCtx]
|
|
2594
|
+
);
|
|
2595
|
+
const removeMemoryById = useCallback5(
|
|
2596
|
+
async (id) => {
|
|
2597
|
+
try {
|
|
2598
|
+
await deleteMemoryByIdOp(storageCtx, id);
|
|
2599
|
+
setMemories((prev) => prev.filter((m) => m.uniqueId !== id));
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
throw new Error(
|
|
2602
|
+
`Failed to delete memory with id ${id}: ` + (error instanceof Error ? error.message : String(error))
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
},
|
|
2606
|
+
[storageCtx]
|
|
2607
|
+
);
|
|
2608
|
+
const removeMemories = useCallback5(
|
|
2609
|
+
async (namespace, key) => {
|
|
2610
|
+
if (!namespace || !key) {
|
|
2611
|
+
throw new Error("Missing required fields: namespace, key");
|
|
2612
|
+
}
|
|
2613
|
+
try {
|
|
2614
|
+
await deleteMemoriesByKeyOp(storageCtx, namespace, key);
|
|
2615
|
+
setMemories(
|
|
2616
|
+
(prev) => prev.filter((m) => !(m.namespace === namespace && m.key === key))
|
|
2617
|
+
);
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
`Failed to delete memories for "${namespace}:${key}": ` + (error instanceof Error ? error.message : String(error))
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
},
|
|
2624
|
+
[storageCtx]
|
|
2625
|
+
);
|
|
2626
|
+
const clearMemories = useCallback5(async () => {
|
|
2627
|
+
try {
|
|
2628
|
+
await clearAllMemoriesOp(storageCtx);
|
|
2629
|
+
setMemories([]);
|
|
2630
|
+
} catch (error) {
|
|
2631
|
+
throw new Error(
|
|
2632
|
+
"Failed to clear all memories: " + (error instanceof Error ? error.message : String(error))
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
}, [storageCtx]);
|
|
2636
|
+
return {
|
|
2637
|
+
memories,
|
|
2638
|
+
refreshMemories,
|
|
2639
|
+
extractMemoriesFromMessage,
|
|
2640
|
+
searchMemories,
|
|
2641
|
+
fetchAllMemories,
|
|
2642
|
+
fetchMemoriesByNamespace,
|
|
2643
|
+
fetchMemoriesByKey,
|
|
2644
|
+
getMemoryById,
|
|
2645
|
+
saveMemory,
|
|
2646
|
+
saveMemories,
|
|
2647
|
+
updateMemory,
|
|
2648
|
+
removeMemory,
|
|
2649
|
+
removeMemoryById,
|
|
2650
|
+
removeMemories,
|
|
2651
|
+
clearMemories
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/lib/chatStorage/schema.ts
|
|
2656
|
+
import { appSchema, tableSchema } from "@nozbe/watermelondb";
|
|
2657
|
+
var chatStorageSchema = appSchema({
|
|
2658
|
+
version: 1,
|
|
2659
|
+
tables: [
|
|
2660
|
+
tableSchema({
|
|
2661
|
+
name: "history",
|
|
2662
|
+
columns: [
|
|
2663
|
+
{ name: "message_id", type: "number" },
|
|
2664
|
+
// Sequential ID within conversation
|
|
2665
|
+
{ name: "conversation_id", type: "string", isIndexed: true },
|
|
2666
|
+
{ name: "role", type: "string", isIndexed: true },
|
|
2667
|
+
// 'user' | 'assistant' | 'system'
|
|
2668
|
+
{ name: "content", type: "string" },
|
|
2669
|
+
{ name: "model", type: "string", isOptional: true },
|
|
2670
|
+
{ name: "files", type: "string", isOptional: true },
|
|
2671
|
+
// JSON stringified FileMetadata[]
|
|
2672
|
+
{ name: "created_at", type: "number", isIndexed: true },
|
|
2673
|
+
{ name: "updated_at", type: "number" },
|
|
2674
|
+
{ name: "vector", type: "string", isOptional: true },
|
|
2675
|
+
// JSON stringified number[]
|
|
2676
|
+
{ name: "embedding_model", type: "string", isOptional: true },
|
|
2677
|
+
{ name: "usage", type: "string", isOptional: true },
|
|
2678
|
+
// JSON stringified ChatCompletionUsage
|
|
2679
|
+
{ name: "sources", type: "string", isOptional: true },
|
|
2680
|
+
// JSON stringified SearchSource[]
|
|
2681
|
+
{ name: "response_duration", type: "number", isOptional: true }
|
|
2682
|
+
]
|
|
2683
|
+
}),
|
|
2684
|
+
tableSchema({
|
|
2685
|
+
name: "conversations",
|
|
2686
|
+
columns: [
|
|
2687
|
+
{ name: "conversation_id", type: "string", isIndexed: true },
|
|
2688
|
+
{ name: "title", type: "string" },
|
|
2689
|
+
{ name: "created_at", type: "number" },
|
|
2690
|
+
{ name: "updated_at", type: "number" },
|
|
2691
|
+
{ name: "is_deleted", type: "boolean", isIndexed: true }
|
|
2692
|
+
]
|
|
2693
|
+
})
|
|
2694
|
+
]
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
// src/lib/chatStorage/models.ts
|
|
2698
|
+
import { Model } from "@nozbe/watermelondb";
|
|
2699
|
+
var Message = class extends Model {
|
|
2700
|
+
/** Sequential message ID within conversation */
|
|
2701
|
+
get messageId() {
|
|
2702
|
+
return this._getRaw("message_id");
|
|
2703
|
+
}
|
|
2704
|
+
/** Links message to its conversation */
|
|
2705
|
+
get conversationId() {
|
|
2706
|
+
return this._getRaw("conversation_id");
|
|
2707
|
+
}
|
|
2708
|
+
/** Who sent the message: 'user' | 'assistant' | 'system' */
|
|
2709
|
+
get role() {
|
|
2710
|
+
return this._getRaw("role");
|
|
2711
|
+
}
|
|
2712
|
+
/** The message text content */
|
|
2713
|
+
get content() {
|
|
2714
|
+
return this._getRaw("content");
|
|
2715
|
+
}
|
|
2716
|
+
/** LLM model used (e.g., GPT-4, Claude) */
|
|
2717
|
+
get model() {
|
|
2718
|
+
const value = this._getRaw("model");
|
|
2719
|
+
return value ? value : void 0;
|
|
2720
|
+
}
|
|
2721
|
+
/** Optional attached files */
|
|
2722
|
+
get files() {
|
|
2723
|
+
const raw = this._getRaw("files");
|
|
2724
|
+
if (!raw) return void 0;
|
|
2725
|
+
try {
|
|
2726
|
+
return JSON.parse(raw);
|
|
2727
|
+
} catch {
|
|
2728
|
+
return void 0;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
/** Created timestamp */
|
|
2732
|
+
get createdAt() {
|
|
2733
|
+
return new Date(this._getRaw("created_at"));
|
|
2734
|
+
}
|
|
2735
|
+
/** Updated timestamp */
|
|
2736
|
+
get updatedAt() {
|
|
2737
|
+
return new Date(this._getRaw("updated_at"));
|
|
2738
|
+
}
|
|
2739
|
+
/** Embedding vector for semantic search */
|
|
2740
|
+
get vector() {
|
|
2741
|
+
const raw = this._getRaw("vector");
|
|
2742
|
+
if (!raw) return void 0;
|
|
2743
|
+
try {
|
|
2744
|
+
return JSON.parse(raw);
|
|
2745
|
+
} catch {
|
|
2746
|
+
return void 0;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
/** Model used to generate embedding */
|
|
2750
|
+
get embeddingModel() {
|
|
2751
|
+
const value = this._getRaw("embedding_model");
|
|
2752
|
+
return value ? value : void 0;
|
|
2753
|
+
}
|
|
2754
|
+
/** Token counts and cost */
|
|
2755
|
+
get usage() {
|
|
2756
|
+
const raw = this._getRaw("usage");
|
|
2757
|
+
if (!raw) return void 0;
|
|
2758
|
+
try {
|
|
2759
|
+
return JSON.parse(raw);
|
|
2760
|
+
} catch {
|
|
2761
|
+
return void 0;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
/** Web search sources */
|
|
2765
|
+
get sources() {
|
|
2766
|
+
const raw = this._getRaw("sources");
|
|
2767
|
+
if (!raw) return void 0;
|
|
2768
|
+
try {
|
|
2769
|
+
return JSON.parse(raw);
|
|
2770
|
+
} catch {
|
|
2771
|
+
return void 0;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
/** Response time in seconds */
|
|
2775
|
+
get responseDuration() {
|
|
2776
|
+
const value = this._getRaw("response_duration");
|
|
2777
|
+
return value !== null && value !== void 0 ? value : void 0;
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
Message.table = "history";
|
|
2781
|
+
Message.associations = {
|
|
2782
|
+
conversations: { type: "belongs_to", key: "conversation_id" }
|
|
2783
|
+
};
|
|
2784
|
+
var Conversation = class extends Model {
|
|
2785
|
+
/** Unique conversation identifier */
|
|
2786
|
+
get conversationId() {
|
|
2787
|
+
return this._getRaw("conversation_id");
|
|
2788
|
+
}
|
|
2789
|
+
/** Conversation title */
|
|
2790
|
+
get title() {
|
|
2791
|
+
return this._getRaw("title");
|
|
2792
|
+
}
|
|
2793
|
+
/** Created timestamp */
|
|
2794
|
+
get createdAt() {
|
|
2795
|
+
return new Date(this._getRaw("created_at"));
|
|
2796
|
+
}
|
|
2797
|
+
/** Updated timestamp */
|
|
2798
|
+
get updatedAt() {
|
|
2799
|
+
return new Date(this._getRaw("updated_at"));
|
|
2800
|
+
}
|
|
2801
|
+
/** Soft delete flag */
|
|
2802
|
+
get isDeleted() {
|
|
2803
|
+
return this._getRaw("is_deleted");
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
Conversation.table = "conversations";
|
|
2807
|
+
Conversation.associations = {
|
|
2808
|
+
history: { type: "has_many", foreignKey: "conversation_id" }
|
|
2809
|
+
};
|
|
2810
|
+
|
|
2811
|
+
// src/lib/memoryStorage/schema.ts
|
|
2812
|
+
import { appSchema as appSchema2, tableSchema as tableSchema2 } from "@nozbe/watermelondb";
|
|
2813
|
+
var memoryStorageSchema = appSchema2({
|
|
2814
|
+
version: 1,
|
|
2815
|
+
tables: [
|
|
2816
|
+
tableSchema2({
|
|
2817
|
+
name: "memories",
|
|
2818
|
+
columns: [
|
|
2819
|
+
// Memory type classification
|
|
2820
|
+
{ name: "type", type: "string", isIndexed: true },
|
|
2821
|
+
// 'identity' | 'preference' | 'project' | 'skill' | 'constraint'
|
|
2822
|
+
// Hierarchical key structure
|
|
2823
|
+
{ name: "namespace", type: "string", isIndexed: true },
|
|
2824
|
+
{ name: "key", type: "string", isIndexed: true },
|
|
2825
|
+
{ name: "value", type: "string" },
|
|
2826
|
+
// Evidence and confidence
|
|
2827
|
+
{ name: "raw_evidence", type: "string" },
|
|
2828
|
+
{ name: "confidence", type: "number" },
|
|
2829
|
+
{ name: "pii", type: "boolean", isIndexed: true },
|
|
2830
|
+
// Composite keys for efficient lookups
|
|
2831
|
+
{ name: "composite_key", type: "string", isIndexed: true },
|
|
2832
|
+
// namespace:key
|
|
2833
|
+
{ name: "unique_key", type: "string", isIndexed: true },
|
|
2834
|
+
// namespace:key:value
|
|
2835
|
+
// Timestamps
|
|
2836
|
+
{ name: "created_at", type: "number", isIndexed: true },
|
|
2837
|
+
{ name: "updated_at", type: "number" },
|
|
2838
|
+
// Vector embeddings for semantic search
|
|
2839
|
+
{ name: "embedding", type: "string", isOptional: true },
|
|
2840
|
+
// JSON stringified number[]
|
|
2841
|
+
{ name: "embedding_model", type: "string", isOptional: true },
|
|
2842
|
+
// Soft delete flag
|
|
2843
|
+
{ name: "is_deleted", type: "boolean", isIndexed: true }
|
|
2844
|
+
]
|
|
2845
|
+
})
|
|
2846
|
+
]
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
// src/lib/memoryStorage/models.ts
|
|
2850
|
+
import { Model as Model2 } from "@nozbe/watermelondb";
|
|
2851
|
+
var Memory = class extends Model2 {
|
|
2852
|
+
/** Memory type classification */
|
|
2853
|
+
get type() {
|
|
2854
|
+
return this._getRaw("type");
|
|
2855
|
+
}
|
|
2856
|
+
/** Namespace for grouping related memories */
|
|
2857
|
+
get namespace() {
|
|
2858
|
+
return this._getRaw("namespace");
|
|
2859
|
+
}
|
|
2860
|
+
/** Key within the namespace */
|
|
2861
|
+
get key() {
|
|
2862
|
+
return this._getRaw("key");
|
|
2863
|
+
}
|
|
2864
|
+
/** The memory value/content */
|
|
2865
|
+
get value() {
|
|
2866
|
+
return this._getRaw("value");
|
|
2867
|
+
}
|
|
2868
|
+
/** Raw evidence from which this memory was extracted */
|
|
2869
|
+
get rawEvidence() {
|
|
2870
|
+
return this._getRaw("raw_evidence");
|
|
2871
|
+
}
|
|
2872
|
+
/** Confidence score (0-1) */
|
|
2873
|
+
get confidence() {
|
|
2874
|
+
return this._getRaw("confidence");
|
|
2875
|
+
}
|
|
2876
|
+
/** Whether this memory contains PII */
|
|
2877
|
+
get pii() {
|
|
2878
|
+
return this._getRaw("pii");
|
|
2879
|
+
}
|
|
2880
|
+
/** Composite key (namespace:key) for efficient lookups */
|
|
2881
|
+
get compositeKey() {
|
|
2882
|
+
return this._getRaw("composite_key");
|
|
2883
|
+
}
|
|
2884
|
+
/** Unique key (namespace:key:value) for deduplication */
|
|
2885
|
+
get uniqueKey() {
|
|
2886
|
+
return this._getRaw("unique_key");
|
|
2887
|
+
}
|
|
2888
|
+
/** Created timestamp */
|
|
2889
|
+
get createdAt() {
|
|
2890
|
+
return new Date(this._getRaw("created_at"));
|
|
2891
|
+
}
|
|
2892
|
+
/** Updated timestamp */
|
|
2893
|
+
get updatedAt() {
|
|
2894
|
+
return new Date(this._getRaw("updated_at"));
|
|
2895
|
+
}
|
|
2896
|
+
/** Embedding vector for semantic search */
|
|
2897
|
+
get embedding() {
|
|
2898
|
+
const raw = this._getRaw("embedding");
|
|
2899
|
+
if (!raw) return void 0;
|
|
2900
|
+
try {
|
|
2901
|
+
return JSON.parse(raw);
|
|
2902
|
+
} catch {
|
|
2903
|
+
return void 0;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
/** Model used to generate embedding */
|
|
2907
|
+
get embeddingModel() {
|
|
2908
|
+
const value = this._getRaw("embedding_model");
|
|
2909
|
+
return value ? value : void 0;
|
|
2910
|
+
}
|
|
2911
|
+
/** Soft delete flag */
|
|
2912
|
+
get isDeleted() {
|
|
2913
|
+
return this._getRaw("is_deleted");
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
Memory.table = "memories";
|
|
1296
2917
|
export {
|
|
2918
|
+
Conversation as ChatConversation,
|
|
2919
|
+
Message as ChatMessage,
|
|
2920
|
+
Memory as StoredMemoryModel,
|
|
2921
|
+
chatStorageSchema,
|
|
2922
|
+
generateCompositeKey,
|
|
2923
|
+
generateConversationId,
|
|
2924
|
+
generateUniqueKey,
|
|
2925
|
+
memoryStorageSchema,
|
|
1297
2926
|
useChat,
|
|
2927
|
+
useChatStorage,
|
|
1298
2928
|
useImageGeneration,
|
|
2929
|
+
useMemoryStorage,
|
|
1299
2930
|
useModels
|
|
1300
2931
|
};
|