@melandlabs/memory-store 0.1.2 → 0.1.5

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.
@@ -462,6 +462,34 @@ function createUnifiedSearch(deps = {}) {
462
462
  }
463
463
 
464
464
  // src/http.ts
465
+ async function embedMissingMessages(messages, deps) {
466
+ if (typeof deps.embedQuery !== "function") {
467
+ return messages;
468
+ }
469
+ const out = [];
470
+ for (const message of messages) {
471
+ if (Array.isArray(message.embedding) && message.embedding.length > 0) {
472
+ out.push(message);
473
+ continue;
474
+ }
475
+ if (typeof message.content !== "string" || message.content.length === 0) {
476
+ out.push(message);
477
+ continue;
478
+ }
479
+ const vector = await deps.embedQuery({
480
+ userId: message.userId,
481
+ query: message.content
482
+ });
483
+ out.push({
484
+ ...message,
485
+ embedding: vector,
486
+ embeddingModel: message.embeddingModel ?? "server",
487
+ embeddingDimensions: vector.length,
488
+ embeddingUpdatedAt: Date.now()
489
+ });
490
+ }
491
+ return out;
492
+ }
465
493
  async function startHttpServer(options = {}) {
466
494
  const port = options.port ?? Number.parseInt(process.env.MEMORY_HTTP_PORT ?? "7421", 10);
467
495
  const host = options.host ?? process.env.MEMORY_HTTP_HOST ?? "127.0.0.1";
@@ -497,16 +525,34 @@ async function startHttpServer(options = {}) {
497
525
  return c.json({ error: "userId and messages[] required" }, 400);
498
526
  }
499
527
  const manager2 = await rawStore.getManager();
500
- const result = await manager2.upsertRawMessages?.({
501
- userId: body.userId,
502
- messages: body.messages
503
- });
528
+ const incoming = body.messages;
529
+ const messages = body.embedOnInsert === true ? await embedMissingMessages(
530
+ incoming.map((m) => ({ ...m, userId: m.userId ?? body.userId })),
531
+ options.unified ?? {}
532
+ ) : incoming.map((m) => ({ ...m, userId: m.userId ?? body.userId }));
533
+ let result;
534
+ if (typeof manager2.upsertRawMessages === "function") {
535
+ result = await manager2.upsertRawMessages({
536
+ userId: body.userId,
537
+ messages
538
+ });
539
+ } else if (typeof manager2.storeMessages === "function") {
540
+ const ids = await manager2.storeMessages(messages);
541
+ result = { inserted: ids.length, ids };
542
+ } else {
543
+ return c.json(
544
+ {
545
+ error: "active raw-message manager exposes neither upsertRawMessages nor storeMessages"
546
+ },
547
+ 500
548
+ );
549
+ }
504
550
  try {
505
- await upsertRawMessagesToChroma(body.messages);
551
+ await upsertRawMessagesToChroma(messages);
506
552
  } catch (error) {
507
553
  console.warn("[memory-store/http] chroma upsert failed:", error);
508
554
  }
509
- return c.json({ ok: true, result });
555
+ return c.json({ ok: true, count: messages.length, result });
510
556
  });
511
557
  app.get("/v1/raw-messages/:id", async (c) => {
512
558
  const id = c.req.param("id");
@@ -528,33 +574,352 @@ async function startHttpServer(options = {}) {
528
574
  };
529
575
  }
530
576
 
577
+ // src/storage/sqlite-vector-index.ts
578
+ function getMemoryVectorStoreBackend() {
579
+ const backend = (process.env.INSIGHT_VECTOR_STORE_BACKEND || process.env.MEMORY_VECTOR_STORE_BACKEND || process.env.VECTOR_STORE_BACKEND || "sqlite-vec").toLowerCase();
580
+ return backend === "chroma" ? "chroma" : "sqlite-vec";
581
+ }
582
+ function isInsightSQLiteVecEnabled(_env) {
583
+ return getMemoryVectorStoreBackend() === "sqlite-vec";
584
+ }
585
+ async function getInsightSQLiteVecStore(_env) {
586
+ const dbPath = resolveSQLiteRawMessageDbPath();
587
+ const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
588
+ return await getSQLiteVecStore(dbPath, void 0, {
589
+ collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
590
+ });
591
+ }
592
+ async function searchInsightsWithSQLiteVec(input, env) {
593
+ const store = await getInsightSQLiteVecStore();
594
+ const overfetchLimit = Math.max(input.limit * 8, input.limit);
595
+ const results = await store.similaritySearchWithOptions(input.queryEmbedding, {
596
+ limit: overfetchLimit,
597
+ filter: { userId: input.userId }
598
+ });
599
+ return results.filter((result) => result.score >= input.threshold).filter((result) => {
600
+ const metadata = result.metadata ?? {};
601
+ if (input.botIds?.length && !input.botIds.includes(String(metadata.botId ?? ""))) {
602
+ return false;
603
+ }
604
+ return input.includeArchived || metadata.archived !== true;
605
+ }).slice(0, input.limit);
606
+ }
607
+
608
+ // src/server/cli-shared.ts
609
+ async function loadAiRag() {
610
+ try {
611
+ const [local, chroma] = await Promise.all([
612
+ import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
613
+ import('@melandlabs/ai-rag/chroma-store')
614
+ ]);
615
+ return {
616
+ LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
617
+ ChromaVectorStore: chroma.ChromaVectorStore
618
+ };
619
+ } catch (error) {
620
+ throw new Error(
621
+ `--embedding-provider local / --*-backend=chroma require @melandlabs/ai-rag to be installed. Run \`pnpm add @melandlabs/ai-rag\` and try again. (${error.message})`
622
+ );
623
+ }
624
+ }
625
+ function parseUnifiedArgs(argv) {
626
+ const env = process.env;
627
+ const args = {
628
+ embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
629
+ embeddingModel: env.EMBEDDING_MODEL,
630
+ chromaUrl: env.CHROMA_URL,
631
+ memoryBackend: env.MEMORY_BACKEND ?? "none",
632
+ insightsBackend: env.INSIGHTS_BACKEND ?? "none",
633
+ insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
634
+ knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
635
+ knowledgeCollection: env.KNOWLEDGE_COLLECTION ?? "opencontext_knowledge"
636
+ };
637
+ for (let i = 0; i < argv.length; i += 1) {
638
+ const arg = argv[i];
639
+ const next = argv[i + 1];
640
+ const takeValue = () => {
641
+ if (next === void 0) throw new Error(`[memory-store/http] ${arg} requires a value`);
642
+ i += 1;
643
+ return next;
644
+ };
645
+ switch (arg) {
646
+ case "--embedding-provider":
647
+ args.embeddingProvider = takeValue();
648
+ break;
649
+ case "--embedding-model":
650
+ args.embeddingModel = takeValue();
651
+ break;
652
+ case "--chroma-url":
653
+ args.chromaUrl = takeValue();
654
+ break;
655
+ case "--memory-backend":
656
+ args.memoryBackend = takeValue();
657
+ break;
658
+ case "--insights-backend":
659
+ args.insightsBackend = takeValue();
660
+ break;
661
+ case "--insights-collection":
662
+ args.insightsCollection = takeValue();
663
+ break;
664
+ case "--knowledge-backend":
665
+ args.knowledgeBackend = takeValue();
666
+ break;
667
+ case "--knowledge-collection":
668
+ args.knowledgeCollection = takeValue();
669
+ break;
670
+ }
671
+ }
672
+ for (const [name, value] of [
673
+ ["--embedding-provider", args.embeddingProvider],
674
+ ["--memory-backend", args.memoryBackend],
675
+ ["--insights-backend", args.insightsBackend],
676
+ ["--knowledge-backend", args.knowledgeBackend]
677
+ ]) {
678
+ const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
679
+ if (!allowed.includes(value)) {
680
+ throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
681
+ }
682
+ }
683
+ return args;
684
+ }
685
+ async function buildUnified(args) {
686
+ const unified = {};
687
+ const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
688
+ const wantsAiRag = args.embeddingProvider === "local" || args.memoryBackend === "chroma" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
689
+ const aiRag = wantsAiRag ? await loadAiRag() : null;
690
+ if (args.embeddingProvider === "local") {
691
+ const provider = new aiRag.LocalTransformersEmbeddingProvider({ modelName: args.embeddingModel });
692
+ unified.embedQuery = async ({ query }) => provider.embedQuery(query);
693
+ log(
694
+ `embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
695
+ );
696
+ } else if (args.embeddingProvider === "openrouter") {
697
+ const apiKey = process.env.OPENROUTER_API_KEY;
698
+ if (!apiKey) {
699
+ throw new Error("--embedding-provider openrouter requires OPENROUTER_API_KEY in the environment");
700
+ }
701
+ const model = args.embeddingModel ?? "text-embedding-3-small";
702
+ const baseURL = "https://openrouter.ai/api/v1";
703
+ unified.embedQuery = async ({ query }) => {
704
+ const res = await fetch(`${baseURL}/embeddings`, {
705
+ method: "POST",
706
+ headers: {
707
+ "content-type": "application/json",
708
+ authorization: `Bearer ${apiKey}`,
709
+ "HTTP-Referer": process.env.NEXT_PUBLIC_APP_URL ?? "https://opencontext.ai",
710
+ "X-Title": "opencontext AI"
711
+ },
712
+ body: JSON.stringify({ model, input: query })
713
+ });
714
+ if (!res.ok) {
715
+ throw new Error(`openrouter embeddings ${res.status}: ${await res.text()}`);
716
+ }
717
+ const body = await res.json();
718
+ const embedding = body.data?.[0]?.embedding;
719
+ if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
720
+ return embedding;
721
+ };
722
+ log(`embedQuery wired via openrouter (model=${model})`);
723
+ }
724
+ if (args.memoryBackend === "sqlite-vec") {
725
+ const store = createRawMessageStore({ env: void 0 });
726
+ const manager2 = await store.getManager();
727
+ if (typeof manager2.searchMessagesSemantically !== "function") {
728
+ throw new Error(
729
+ "the active raw-message manager does not implement searchMessagesSemantically; sqlite-vec ANN for the memory source is unavailable in this build"
730
+ );
731
+ }
732
+ unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
733
+ const rows = await manager2.searchMessagesSemantically({
734
+ userId,
735
+ queryEmbedding,
736
+ limit,
737
+ threshold,
738
+ botId
739
+ });
740
+ return rows.map((r) => ({
741
+ id: r.id,
742
+ content: r.content,
743
+ similarity: r.similarity,
744
+ metadata: r.metadata ?? {}
745
+ }));
746
+ };
747
+ log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
748
+ } else if (args.memoryBackend === "chroma") {
749
+ if (!args.chromaUrl)
750
+ throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
751
+ const store = new aiRag.ChromaVectorStore({
752
+ url: args.chromaUrl,
753
+ collectionName: "opencontext_raw_messages"
754
+ });
755
+ unified.searchRawMessagesAnn = async ({ queryEmbedding, limit, threshold }) => {
756
+ const results = await store.similaritySearchWithOptions(queryEmbedding, { limit: limit + 1 });
757
+ return results.filter((r) => r.score >= threshold).map((r) => ({ id: r.id, content: r.content, similarity: r.score, metadata: r.metadata ?? {} }));
758
+ };
759
+ log("memory backend wired via chroma (collection=opencontext_raw_messages)");
760
+ }
761
+ if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
762
+ if (!unified.embedQuery) {
763
+ throw new Error("--insights-backend=sqlite-vec requires --embedding-provider local|openrouter");
764
+ }
765
+ const embedQuery = unified.embedQuery;
766
+ unified.searchInsights = async ({ userId, query, limit, threshold, botIds, includeArchived }) => {
767
+ const emb = await embedQuery({ userId, query });
768
+ const rows = await searchInsightsWithSQLiteVec({
769
+ userId,
770
+ queryEmbedding: emb,
771
+ limit,
772
+ threshold,
773
+ botIds,
774
+ includeArchived
775
+ });
776
+ return rows.map((r) => ({
777
+ id: r.id,
778
+ content: r.content,
779
+ similarity: r.score,
780
+ metadata: r.metadata ?? {}
781
+ }));
782
+ };
783
+ log("insights backend wired via sqlite-vec");
784
+ } else if (args.insightsBackend === "chroma") {
785
+ if (!args.chromaUrl)
786
+ throw new Error("--insights-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
787
+ if (!unified.embedQuery)
788
+ throw new Error("--insights-backend=chroma requires --embedding-provider local|openrouter");
789
+ const store = new aiRag.ChromaVectorStore({
790
+ url: args.chromaUrl,
791
+ collectionName: args.insightsCollection
792
+ });
793
+ const embedQuery = unified.embedQuery;
794
+ unified.searchInsights = async ({ query, limit, threshold }) => {
795
+ const emb = await embedQuery({ userId: "", query });
796
+ const results = await store.similaritySearchWithOptions(emb, { limit: limit + 1 });
797
+ return results.filter((r) => r.score >= threshold).map((r) => ({ id: r.id, content: r.content, similarity: r.score, metadata: r.metadata ?? {} }));
798
+ };
799
+ log(`insights backend wired via chroma (collection=${args.insightsCollection})`);
800
+ }
801
+ if (args.knowledgeBackend === "chroma") {
802
+ if (!args.chromaUrl)
803
+ throw new Error("--knowledge-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
804
+ if (!unified.embedQuery)
805
+ throw new Error("--knowledge-backend=chroma requires --embedding-provider local|openrouter");
806
+ const store = new aiRag.ChromaVectorStore({
807
+ url: args.chromaUrl,
808
+ collectionName: args.knowledgeCollection
809
+ });
810
+ const embedQuery = unified.embedQuery;
811
+ unified.searchKnowledge = async ({ query, options }) => {
812
+ const emb = await embedQuery({ userId: "", query });
813
+ const results = await store.similaritySearchWithOptions(emb, { limit: options.limit + 1 });
814
+ return results.filter((r) => r.score >= options.threshold).map((r, i) => ({
815
+ chunkId: r.id,
816
+ documentId: r.documentId || String(r.metadata?.documentId ?? r.id),
817
+ documentName: String(r.metadata?.documentName ?? ""),
818
+ content: r.content,
819
+ similarity: r.score,
820
+ chunkIndex: i
821
+ }));
822
+ };
823
+ log(`knowledge backend wired via chroma (collection=${args.knowledgeCollection})`);
824
+ }
825
+ return unified;
826
+ }
827
+ function printUnifiedHelp() {
828
+ console.log(`Embedding (wires unified.embedQuery):
829
+ --embedding-provider <name> local | openrouter | none
830
+ (env: EMBEDDING_PROVIDER, default: none)
831
+ --embedding-model <name> Model name
832
+ (env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
833
+ openrouter \u2192 text-embedding-3-small)
834
+ Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
835
+
836
+ Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
837
+ --chroma-url <url> Chroma server URL
838
+ (env: CHROMA_URL; required when any *-backend=chroma)
839
+ --memory-backend <name> sqlite-vec | chroma | none
840
+ (env: MEMORY_BACKEND, default: none)
841
+ --insights-backend <name> sqlite-vec | chroma | none
842
+ (env: INSIGHTS_BACKEND, default: none)
843
+ --insights-collection <name> Chroma collection (default: opencontext_insights)
844
+ --knowledge-backend <name> chroma | none
845
+ (env: KNOWLEDGE_BACKEND, default: none)
846
+ --knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
847
+ Note: the chroma backends dynamically import @melandlabs/ai-rag.`);
848
+ }
849
+
531
850
  // src/server/cli-http.ts
532
851
  function parseArgs(argv) {
852
+ const env = process.env;
853
+ const unified = parseUnifiedArgs(argv);
533
854
  const args = {
534
- port: Number.parseInt(process.env.MEMORY_HTTP_PORT ?? "7421", 10),
535
- host: process.env.MEMORY_HTTP_HOST ?? "127.0.0.1"
855
+ ...unified,
856
+ port: Number.parseInt(env.MEMORY_HTTP_PORT ?? "7421", 10),
857
+ host: env.MEMORY_HTTP_HOST ?? "127.0.0.1"
536
858
  };
537
859
  for (let i = 0; i < argv.length; i += 1) {
538
860
  const arg = argv[i];
539
861
  const next = argv[i + 1];
540
- if (arg === "--port" && next) {
541
- args.port = Number.parseInt(next, 10);
862
+ const takeValue = () => {
863
+ if (next === void 0) throw new Error(`[memory-store/http] ${arg} requires a value`);
542
864
  i += 1;
543
- } else if (arg === "--host" && next) {
544
- args.host = next;
545
- i += 1;
546
- } else if (arg === "--help" || arg === "-h") {
547
- console.log("Usage: opencontext-memory-http [--port <port>] [--host <host>]");
548
- process.exit(0);
865
+ return next;
866
+ };
867
+ switch (arg) {
868
+ case "--port":
869
+ args.port = Number.parseInt(takeValue(), 10);
870
+ break;
871
+ case "--host":
872
+ args.host = takeValue();
873
+ break;
874
+ case "--help":
875
+ case "-h":
876
+ printHelp();
877
+ process.exit(0);
549
878
  }
550
879
  }
880
+ if (!Number.isFinite(args.port) || args.port <= 0) {
881
+ throw new Error(`[memory-store/http] invalid --port: ${args.port}`);
882
+ }
551
883
  return args;
552
884
  }
885
+ function printHelp() {
886
+ console.log(`opencontext-memory-http \u2014 standalone memory-store HTTP daemon.
887
+
888
+ Usage:
889
+ opencontext-memory-http [options]
890
+
891
+ Server:
892
+ --port <port> Port to listen on (default: 7421, env: MEMORY_HTTP_PORT)
893
+ --host <host> Host to bind (default: 127.0.0.1, env: MEMORY_HTTP_HOST)
894
+ `);
895
+ printUnifiedHelp();
896
+ console.log(`
897
+ Examples:
898
+ # Minimal \u2014 same as before, all three *not_configured warnings remain
899
+ opencontext-memory-http
900
+
901
+ # Local ONNX embedder + sqlite-vec ANN for the memory source (no API key,
902
+ # no extra services). Removes embedQuery + memory warnings; the
903
+ # insights/knowledge sources still need a backend of their own.
904
+ opencontext-memory-http \\
905
+ --embedding-provider local \\
906
+ --memory-backend sqlite-vec
907
+
908
+ # Wire everything via a running Chroma server
909
+ opencontext-memory-http \\
910
+ --embedding-provider openrouter \\
911
+ --chroma-url http://127.0.0.1:8000 \\
912
+ --memory-backend chroma \\
913
+ --insights-backend chroma \\
914
+ --knowledge-backend chroma`);
915
+ }
553
916
  async function main() {
554
917
  const args = parseArgs(process.argv.slice(2));
555
- const { url, port, stop } = await startHttpServer({
918
+ const unified = await buildUnified(args);
919
+ const { url, stop } = await startHttpServer({
556
920
  port: args.port,
557
- host: args.host
921
+ host: args.host,
922
+ unified
558
923
  });
559
924
  console.log(`[memory-store/http] listening at ${url}`);
560
925
  const shutdown = async (signal) => {