mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-26 06:04:08 +00:00
Merge 6f160141c9 into f355dc8039
This commit is contained in:
commit
dd66ad8c2d
11 changed files with 798 additions and 7 deletions
41
bun.lock
41
bun.lock
|
|
@ -227,7 +227,7 @@
|
|||
},
|
||||
"packages/tools": {
|
||||
"name": "@supermemory/tools",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.2",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.2.6",
|
||||
"openai": "^4.104.0",
|
||||
|
|
@ -239,6 +239,8 @@
|
|||
"@ai-sdk/openai": "^2.0.23",
|
||||
"@ai-sdk/provider": "^3.0.0",
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@langchain/core": "^1.2.14",
|
||||
"@langchain/langgraph": "^1.4.18",
|
||||
"@mastra/core": "^1.0.0",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/bun": "^1.2.21",
|
||||
|
|
@ -251,10 +253,15 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"@langchain/core": "^1.2.14",
|
||||
"@langchain/langgraph": "^1.4.18",
|
||||
"@voltagent/core": "^2.6.12",
|
||||
"ai": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@ai-sdk/provider",
|
||||
"@langchain/core",
|
||||
"@langchain/langgraph",
|
||||
"@voltagent/core",
|
||||
"ai",
|
||||
],
|
||||
|
|
@ -887,6 +894,16 @@
|
|||
|
||||
"@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="],
|
||||
|
||||
"@langchain/core": ["@langchain/core@1.2.17", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "zod": "^3.25.76 || ^4" } }, "sha512-aLQLSed6oLCMLtJHrINhnFmsu79G2UiDlRslbOx+TSN9NmU+WuDv1OtVxrLpYglhXNy53elSduMVk3XBPG9ulw=="],
|
||||
|
||||
"@langchain/langgraph": ["@langchain/langgraph@1.5.0", "", { "dependencies": { "@langchain/langgraph-checkpoint": "^1.2.0", "@langchain/langgraph-sdk": "~1.12.4", "@langchain/protocol": "^0.0.19", "@standard-schema/spec": "1.1.0" }, "peerDependencies": { "@langchain/core": "^1.1.48", "zod": "^3.25.32 || ^4.2.0" } }, "sha512-nYBV1vkWtAdU9GFDzdnPKiEMZVXerqCadRcyRtjTnd6Z4nIPmyB4SGSKioQFjSWBZK6nd/RIoyQVhjnLpw9oxQ=="],
|
||||
|
||||
"@langchain/langgraph-checkpoint": ["@langchain/langgraph-checkpoint@1.2.0", "", { "peerDependencies": { "@langchain/core": "^1.1.48" } }, "sha512-a9F1tO/bKGS4AlLR0FVa859oImD0iQiQLAAPkP66Nnp7a5dXPgys2IpzdJQ9slKiVznsSAFjxxNCmM25dBOvWA=="],
|
||||
|
||||
"@langchain/langgraph-sdk": ["@langchain/langgraph-sdk@1.12.4", "", { "dependencies": { "@langchain/protocol": "^0.0.19", "@types/json-schema": "^7.0.15", "p-queue": "^9.0.1", "p-retry": "^7.1.1" }, "peerDependencies": { "@langchain/core": "^1.1.48", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["react", "react-dom"] }, "sha512-DuYk7HbajphOzBBtZYm+4LpCZuHj1/Csqepej5Ro1PJvchotA+bjg9XLh9LRsbz5j49Tl3owNoUp9ezj0lMCXA=="],
|
||||
|
||||
"@langchain/protocol": ["@langchain/protocol@0.0.19", "", {}, "sha512-9hKcRrH7cBX6gfutdfXPoft1OCchHe4FEpALoDJMl5Qu+n/YG5ynZmyu8+8cxORlPwHBoKTxggvXz+76M1yX1Q=="],
|
||||
|
||||
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
|
||||
|
||||
"@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="],
|
||||
|
|
@ -2689,6 +2706,8 @@
|
|||
|
||||
"kysely": ["kysely@0.29.6", "", {}, "sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg=="],
|
||||
|
||||
"langsmith": ["langsmith@0.10.13", "", { "dependencies": { "p-queue": "6.6.2" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*", "ws": ">=7" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai", "ws"] }, "sha512-v5500fr7rVzQSfQphTBJV2/OrMRDzb2QfLfoO+gvPBY/kER99QF2+wbG6QQUydm0N7ueyJfBOYhq6tqYacXVAA=="],
|
||||
|
||||
"lcm": ["lcm@0.0.3", "", { "dependencies": { "gcd": "^0.0.1" } }, "sha512-TB+ZjoillV6B26Vspf9l2L/vKaRY/4ep3hahcyVkCGFgsTNRUQdc24bQeNFiZeoxH0vr5+7SfNRMQuPHv/1IrQ=="],
|
||||
|
||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||
|
|
@ -2943,6 +2962,8 @@
|
|||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
|
||||
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
|
@ -3051,13 +3072,17 @@
|
|||
|
||||
"p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="],
|
||||
|
||||
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
|
||||
|
||||
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
|
||||
|
||||
"p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
|
||||
|
||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
"p-some": ["p-some@6.0.0", "", { "dependencies": { "aggregate-error": "^4.0.0", "p-cancelable": "^3.0.0" } }, "sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg=="],
|
||||
|
||||
"p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="],
|
||||
"p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
|
|
@ -4019,6 +4044,12 @@
|
|||
|
||||
"@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@langchain/core/zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="],
|
||||
|
||||
"@langchain/langgraph-sdk/p-queue": ["p-queue@9.3.5", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ=="],
|
||||
|
||||
"@langchain/langgraph-sdk/p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="],
|
||||
|
||||
"@mastra/core/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||
|
||||
"@mastra/core/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
|
|
@ -4451,6 +4482,8 @@
|
|||
|
||||
"is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="],
|
||||
|
||||
"is-online/p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="],
|
||||
|
||||
"is-regex/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
|
@ -4485,6 +4518,8 @@
|
|||
|
||||
"openid-client/jose": ["jose@6.2.0", "", {}, "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ=="],
|
||||
|
||||
"p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
|
@ -4813,6 +4848,8 @@
|
|||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@langchain/langgraph-sdk/p-queue/p-timeout": ["p-timeout@7.0.2", "", {}, "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw=="],
|
||||
|
||||
"@mastra/core/@modelcontextprotocol/sdk/jose": ["jose@6.2.0", "", {}, "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ=="],
|
||||
|
||||
"@mastra/core/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# @supermemory/tools
|
||||
|
||||
Memory tools and middleware for Vercel AI SDK, OpenAI, Mastra, VoltAgent and Claude, backed by the [Supermemory](https://supermemory.ai) v5 API.
|
||||
Memory tools and middleware for Vercel AI SDK, OpenAI, LangChain, LangGraph, Mastra, VoltAgent and Claude, backed by the [Supermemory](https://supermemory.ai) v5 API.
|
||||
|
||||
Pick the entry point for your stack:
|
||||
|
||||
|
|
@ -8,6 +8,7 @@ Pick the entry point for your stack:
|
|||
| --- | --- |
|
||||
| `@supermemory/tools/ai-sdk` | Vercel AI SDK: 7 memory tools for `generateText`/`streamText`, plus `withSupermemory` model middleware |
|
||||
| `@supermemory/tools/openai` | OpenAI SDK: function-calling tool definitions and executor, plus `withSupermemory` client middleware |
|
||||
| `@supermemory/tools/langchain` | LangGraph: persistent `BaseStore`; LangChain: hybrid-search `BaseRetriever` |
|
||||
| `@supermemory/tools/mastra` | Mastra agents: input/output processors or the `withSupermemory` config wrapper |
|
||||
| `@supermemory/tools/voltagent` | VoltAgent agents: `withSupermemory` config wrapper built on hooks |
|
||||
| `@supermemory/tools/claude-memory` | Claude's native memory tool (`memory_20250818`), stored as Supermemory documents |
|
||||
|
|
@ -40,7 +41,32 @@ The `ai-sdk` entry point works with AI SDK 5, 6 and 7. It uses the `ai` package
|
|||
|
||||
## Usage
|
||||
|
||||
Each section below covers one entry point: AI SDK, OpenAI, Mastra, VoltAgent, then the Claude memory tool. The `withSupermemory` middleware takes the same options everywhere; see [withSupermemory Middleware Options](#withsupermemory-middleware-options).
|
||||
Each section below covers one entry point: AI SDK, OpenAI, LangChain/LangGraph, Mastra, VoltAgent, then the Claude memory tool. The `withSupermemory` middleware takes the same options everywhere; see [withSupermemory Middleware Options](#withsupermemory-middleware-options).
|
||||
|
||||
### LangChain and LangGraph
|
||||
|
||||
Install the optional peer dependencies, then use Supermemory as a persistent LangGraph store or a LangChain retriever:
|
||||
|
||||
```bash
|
||||
npm install @supermemory/tools @langchain/core @langchain/langgraph
|
||||
```
|
||||
|
||||
```typescript
|
||||
import {
|
||||
SupermemoryRetriever,
|
||||
SupermemoryStore,
|
||||
} from "@supermemory/tools/langchain"
|
||||
|
||||
const store = new SupermemoryStore(process.env.SUPERMEMORY_API_KEY!)
|
||||
await store.put(["users", "ada"], "preferences", { theme: "dark" })
|
||||
const preferences = await store.get(["users", "ada"], "preferences")
|
||||
|
||||
const retriever = new SupermemoryRetriever(
|
||||
process.env.SUPERMEMORY_API_KEY!,
|
||||
{ namespace: "user_ada", limit: 5 },
|
||||
)
|
||||
const context = await retriever.invoke("What editor does Ada use?")
|
||||
```
|
||||
|
||||
### AI SDK Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "3.1.2",
|
||||
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
|
||||
"description": "Memory tools for AI SDK, OpenAI, LangChain, LangGraph, VoltAgent and Mastra with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch --ignore-watch .turbo",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest --testTimeout 100000",
|
||||
"test:unit": "vitest run --testTimeout 100000 src/tools-shared.test.ts src/tool-operations.test.ts src/claude-memory.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/openai-middleware.unit.test.ts test/mastra/unit.test.ts test/voltagent.unit.test.ts",
|
||||
"test:unit": "vitest run --testTimeout 100000 src/tools-shared.test.ts src/tool-operations.test.ts src/claude-memory.test.ts src/langchain/*.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/openai-middleware.unit.test.ts test/mastra/unit.test.ts test/voltagent.unit.test.ts",
|
||||
"test:watch": "vitest --watch --testTimeout 100000",
|
||||
"test:compat": "bun test/ai-sdk-compat/run.ts"
|
||||
},
|
||||
|
|
@ -23,10 +23,12 @@
|
|||
"@ai-sdk/openai": "^2.0.23",
|
||||
"@ai-sdk/provider": "^3.0.0",
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@voltagent/core": "^2.6.12",
|
||||
"@langchain/core": "^1.2.14",
|
||||
"@langchain/langgraph": "^1.4.18",
|
||||
"@mastra/core": "^1.0.0",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/bun": "^1.2.21",
|
||||
"@voltagent/core": "^2.6.12",
|
||||
"ai": "^5.0.29",
|
||||
"dotenv": "^16.6.1",
|
||||
"tsdown": "^0.14.2",
|
||||
|
|
@ -35,6 +37,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"@langchain/core": "^1.2.14",
|
||||
"@langchain/langgraph": "^1.4.18",
|
||||
"@voltagent/core": "^2.6.12",
|
||||
"ai": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
},
|
||||
|
|
@ -42,6 +46,12 @@
|
|||
"@ai-sdk/provider": {
|
||||
"optional": true
|
||||
},
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@langchain/langgraph": {
|
||||
"optional": true
|
||||
},
|
||||
"@voltagent/core": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -59,6 +69,7 @@
|
|||
".": "./dist/index.js",
|
||||
"./ai-sdk": "./dist/ai-sdk.js",
|
||||
"./claude-memory": "./dist/claude-memory.js",
|
||||
"./langchain": "./dist/langchain/index.js",
|
||||
"./mastra": "./dist/mastra.js",
|
||||
"./openai": "./dist/openai/index.js",
|
||||
"./voltagent": "./dist/voltagent/index.js",
|
||||
|
|
|
|||
9
packages/tools/src/langchain/index.ts
Normal file
9
packages/tools/src/langchain/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/** LangChain and LangGraph integrations backed by Supermemory v5. */
|
||||
|
||||
export { SupermemoryRetriever } from "./retriever"
|
||||
export type { SupermemoryRetrieverOptions } from "./retriever"
|
||||
export { SupermemoryStore } from "./store"
|
||||
export {
|
||||
fromSupermemoryNamespace,
|
||||
toSupermemoryNamespace,
|
||||
} from "./namespace"
|
||||
22
packages/tools/src/langchain/namespace.test.ts
Normal file
22
packages/tools/src/langchain/namespace.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from "vitest"
|
||||
import { fromSupermemoryNamespace, toSupermemoryNamespace } from "./namespace"
|
||||
|
||||
describe("LangGraph namespace mapping", () => {
|
||||
it("round-trips segments without collisions", () => {
|
||||
const namespaces = [
|
||||
["users", "ada"],
|
||||
["a:b"],
|
||||
["a", "b"],
|
||||
["spaces and/slashes", "under_score", "日本語"],
|
||||
]
|
||||
|
||||
const encoded = namespaces.map(toSupermemoryNamespace)
|
||||
expect(new Set(encoded).size).toBe(namespaces.length)
|
||||
expect(encoded.map(fromSupermemoryNamespace)).toEqual(namespaces)
|
||||
})
|
||||
|
||||
it("ignores namespaces not owned by the store", () => {
|
||||
expect(fromSupermemoryNamespace("user_alex")).toBeNull()
|
||||
expect(fromSupermemoryNamespace("langgraph:bad_encoding_")).toBeNull()
|
||||
})
|
||||
})
|
||||
28
packages/tools/src/langchain/namespace.ts
Normal file
28
packages/tools/src/langchain/namespace.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const PREFIX = "langgraph:"
|
||||
const SEPARATOR = ":"
|
||||
|
||||
function encodeSegment(segment: string): string {
|
||||
return encodeURIComponent(segment)
|
||||
.replace(/[^A-Za-z0-9%-]/g, (char) => `%${char.charCodeAt(0).toString(16)}`)
|
||||
.replaceAll("%", "_")
|
||||
}
|
||||
|
||||
function decodeSegment(segment: string): string {
|
||||
return decodeURIComponent(segment.replaceAll("_", "%"))
|
||||
}
|
||||
|
||||
/** Maps a hierarchical LangGraph namespace to one isolated v5 namespace. */
|
||||
export function toSupermemoryNamespace(namespace: string[]): string {
|
||||
return `${PREFIX}${namespace.map(encodeSegment).join(SEPARATOR)}`
|
||||
}
|
||||
|
||||
/** Returns null for namespaces not owned by this LangGraph store. */
|
||||
export function fromSupermemoryNamespace(namespace: string): string[] | null {
|
||||
if (!namespace.startsWith(PREFIX)) return null
|
||||
const encoded = namespace.slice(PREFIX.length)
|
||||
try {
|
||||
return encoded ? encoded.split(SEPARATOR).map(decodeSegment) : []
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
70
packages/tools/src/langchain/retriever.test.ts
Normal file
70
packages/tools/src/langchain/retriever.test.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const search = vi.fn()
|
||||
|
||||
vi.mock("supermemory", () => ({
|
||||
NotFoundError: class NotFoundError extends Error {},
|
||||
Supermemory: class MockSupermemory {
|
||||
search = search
|
||||
},
|
||||
}))
|
||||
|
||||
import { SupermemoryRetriever } from "./retriever"
|
||||
|
||||
describe("SupermemoryRetriever", () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it("searches its v5 namespace in hybrid mode", async () => {
|
||||
search.mockResolvedValue({ results: [] })
|
||||
const retriever = new SupermemoryRetriever("sm_test", {
|
||||
namespace: "user_1",
|
||||
limit: 5,
|
||||
})
|
||||
|
||||
await retriever.invoke("favorite editor")
|
||||
|
||||
expect(search).toHaveBeenCalledWith("user_1", {
|
||||
query: "favorite editor",
|
||||
limit: 5,
|
||||
threshold: 0.6,
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
})
|
||||
|
||||
it("maps memory and chunk results to LangChain documents", async () => {
|
||||
search.mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
id: "mem_1",
|
||||
memory: "Uses Neovim",
|
||||
metadata: { source: "profile" },
|
||||
similarity: 0.9,
|
||||
system: { createdAt: "c", updatedAt: "u" },
|
||||
},
|
||||
{
|
||||
id: "chunk_1",
|
||||
chunk: "Editor notes",
|
||||
metadata: {},
|
||||
similarity: 0.8,
|
||||
system: { updatedAt: "u" },
|
||||
},
|
||||
],
|
||||
})
|
||||
const retriever = new SupermemoryRetriever("sm_test", {
|
||||
namespace: "user_1",
|
||||
})
|
||||
|
||||
const documents = await retriever.invoke("editor")
|
||||
|
||||
expect(documents.map((document) => document.pageContent)).toEqual([
|
||||
"Uses Neovim",
|
||||
"Editor notes",
|
||||
])
|
||||
expect(documents[0]?.metadata).toMatchObject({
|
||||
id: "mem_1",
|
||||
namespace: "user_1",
|
||||
similarity: 0.9,
|
||||
source: "profile",
|
||||
})
|
||||
})
|
||||
})
|
||||
62
packages/tools/src/langchain/retriever.ts
Normal file
62
packages/tools/src/langchain/retriever.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Document } from "@langchain/core/documents"
|
||||
import {
|
||||
BaseRetriever,
|
||||
type BaseRetrieverInput,
|
||||
} from "@langchain/core/retrievers"
|
||||
import {
|
||||
DEFAULT_VALUES,
|
||||
clampSearchLimit,
|
||||
createToolsClient,
|
||||
} from "../tools-shared"
|
||||
import type { SupermemoryToolsConfig } from "../types"
|
||||
|
||||
export interface SupermemoryRetrieverOptions extends BaseRetrieverInput {
|
||||
/** Supermemory v5 namespace to search. */
|
||||
namespace: string
|
||||
/** Maximum number of documents to return. */
|
||||
limit?: number
|
||||
config?: SupermemoryToolsConfig
|
||||
}
|
||||
|
||||
/** LangChain retriever backed by Supermemory's hybrid memory search. */
|
||||
export class SupermemoryRetriever extends BaseRetriever {
|
||||
lc_namespace = ["supermemory", "retrievers"]
|
||||
|
||||
private client
|
||||
private namespace: string
|
||||
private limit: number
|
||||
|
||||
constructor(apiKey: string, options: SupermemoryRetrieverOptions) {
|
||||
super(options)
|
||||
this.client = createToolsClient(apiKey, options.config)
|
||||
this.namespace = options.namespace
|
||||
this.limit = clampSearchLimit(options.limit ?? DEFAULT_VALUES.limit)
|
||||
}
|
||||
|
||||
override async _getRelevantDocuments(query: string): Promise<Document[]> {
|
||||
const response = await this.client.search(this.namespace, {
|
||||
query,
|
||||
limit: this.limit,
|
||||
threshold: DEFAULT_VALUES.searchThreshold,
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
|
||||
return response.results.flatMap((result) => {
|
||||
const pageContent = result.memory ?? result.chunk
|
||||
if (!pageContent) return []
|
||||
return [
|
||||
new Document({
|
||||
pageContent,
|
||||
metadata: {
|
||||
...result.metadata,
|
||||
id: result.id,
|
||||
namespace: this.namespace,
|
||||
similarity: result.similarity,
|
||||
createdAt: result.system.createdAt,
|
||||
updatedAt: result.system.updatedAt,
|
||||
},
|
||||
}),
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
202
packages/tools/src/langchain/store.test.ts
Normal file
202
packages/tools/src/langchain/store.test.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const {
|
||||
add,
|
||||
documentsDelete,
|
||||
documentsGet,
|
||||
documentsUpdate,
|
||||
list,
|
||||
namespacesList,
|
||||
search,
|
||||
MockNotFoundError,
|
||||
} = vi.hoisted(() => ({
|
||||
add: vi.fn(),
|
||||
documentsDelete: vi.fn(),
|
||||
documentsGet: vi.fn(),
|
||||
documentsUpdate: vi.fn(),
|
||||
list: vi.fn(),
|
||||
namespacesList: vi.fn(),
|
||||
search: vi.fn(),
|
||||
MockNotFoundError: class MockNotFoundError extends Error {},
|
||||
}))
|
||||
|
||||
vi.mock("supermemory", () => ({
|
||||
NotFoundError: MockNotFoundError,
|
||||
Supermemory: class MockSupermemory {
|
||||
add = add
|
||||
documents = {
|
||||
delete: documentsDelete,
|
||||
get: documentsGet,
|
||||
update: documentsUpdate,
|
||||
}
|
||||
list = list
|
||||
namespaces = { list: namespacesList }
|
||||
search = search
|
||||
},
|
||||
}))
|
||||
|
||||
import { SupermemoryStore } from "./store"
|
||||
|
||||
const timestamps = {
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-02T00:00:00Z",
|
||||
}
|
||||
|
||||
function document(id: string, key: string, value: Record<string, unknown>) {
|
||||
return {
|
||||
id,
|
||||
content: JSON.stringify(value),
|
||||
metadata: { langgraphKey: key },
|
||||
system: { status: "done", ...timestamps },
|
||||
}
|
||||
}
|
||||
|
||||
describe("SupermemoryStore", () => {
|
||||
let store: SupermemoryStore
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
documentsGet.mockRejectedValue(new MockNotFoundError())
|
||||
namespacesList.mockResolvedValue({
|
||||
namespaces: [],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalItems: 0 },
|
||||
})
|
||||
store = new SupermemoryStore("sm_test")
|
||||
})
|
||||
|
||||
it("creates an item with a scoped v5 namespace and caller ID", async () => {
|
||||
await store.put(["users", "ada"], "profile", { theme: "dark" })
|
||||
|
||||
expect(add).toHaveBeenCalledWith("langgraph:users:ada", {
|
||||
content: JSON.stringify({ theme: "dark" }),
|
||||
id: "profile",
|
||||
metadata: { langgraphKey: "profile" },
|
||||
taskType: "superrag",
|
||||
})
|
||||
})
|
||||
|
||||
it("reads and replaces an existing item directly by namespace and key", async () => {
|
||||
documentsGet.mockResolvedValue(
|
||||
document("doc_1", "profile", { theme: "light" }),
|
||||
)
|
||||
|
||||
const item = await store.get(["users", "ada"], "profile")
|
||||
await store.put(["users", "ada"], "profile", { theme: "dark" })
|
||||
|
||||
expect(item).toMatchObject({
|
||||
key: "profile",
|
||||
namespace: ["users", "ada"],
|
||||
value: { theme: "light" },
|
||||
})
|
||||
expect(documentsUpdate).toHaveBeenCalledWith(
|
||||
"langgraph:users:ada",
|
||||
"doc_1",
|
||||
{ content: JSON.stringify({ theme: "dark" }) },
|
||||
)
|
||||
})
|
||||
|
||||
it("deletes an existing item", async () => {
|
||||
documentsGet.mockResolvedValue(document("doc_1", "profile", {}))
|
||||
documentsDelete.mockResolvedValue({ count: 1, errors: [] })
|
||||
|
||||
await store.delete(["users", "ada"], "profile")
|
||||
|
||||
expect(documentsDelete).toHaveBeenCalledWith("langgraph:users:ada", {
|
||||
ids: ["doc_1"],
|
||||
})
|
||||
})
|
||||
|
||||
it("searches every managed namespace under the requested prefix", async () => {
|
||||
namespacesList.mockResolvedValue({
|
||||
namespaces: [
|
||||
{ namespace: "langgraph:users:ada" },
|
||||
{ namespace: "langgraph:users:grace" },
|
||||
{ namespace: "unrelated" },
|
||||
],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalItems: 3 },
|
||||
})
|
||||
search
|
||||
.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
metadata: { langgraphKey: "profile" },
|
||||
similarity: 0.7,
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
metadata: { langgraphKey: "profile" },
|
||||
similarity: 0.9,
|
||||
},
|
||||
],
|
||||
})
|
||||
documentsGet
|
||||
.mockResolvedValueOnce(document("ada", "profile", { active: true }))
|
||||
.mockResolvedValueOnce(document("grace", "profile", { active: true }))
|
||||
|
||||
const results = await store.search(["users"], {
|
||||
query: "active profile",
|
||||
filter: { active: true },
|
||||
})
|
||||
|
||||
expect(search).toHaveBeenCalledTimes(2)
|
||||
expect(search).toHaveBeenCalledWith("langgraph:users:ada", {
|
||||
query: "active profile",
|
||||
searchMode: "chunks",
|
||||
limit: 50,
|
||||
threshold: 0,
|
||||
})
|
||||
expect(results.map((item) => item.namespace)).toEqual([
|
||||
["users", "grace"],
|
||||
["users", "ada"],
|
||||
])
|
||||
})
|
||||
|
||||
it("lists and filters items when search has no semantic query", async () => {
|
||||
namespacesList.mockResolvedValue({
|
||||
namespaces: [{ namespace: "langgraph:users:ada" }],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalItems: 1 },
|
||||
})
|
||||
list.mockResolvedValue({
|
||||
documents: [{ id: "profile" }],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalItems: 1 },
|
||||
})
|
||||
documentsGet.mockResolvedValue(
|
||||
document("doc_1", "profile", { theme: "dark" }),
|
||||
)
|
||||
|
||||
const results = await store.search(["users"], {
|
||||
filter: { theme: "dark" },
|
||||
})
|
||||
|
||||
expect(list).toHaveBeenCalledWith("langgraph:users:ada", "documents", {
|
||||
page: 1,
|
||||
limit: 100,
|
||||
})
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]?.value).toEqual({ theme: "dark" })
|
||||
})
|
||||
|
||||
it("lists only store-owned namespaces with LangGraph matching", async () => {
|
||||
namespacesList.mockResolvedValue({
|
||||
namespaces: [
|
||||
{ namespace: "langgraph:users:ada" },
|
||||
{ namespace: "langgraph:users:grace" },
|
||||
{ namespace: "langgraph:projects:demo" },
|
||||
{ namespace: "user_alex" },
|
||||
],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalItems: 4 },
|
||||
})
|
||||
|
||||
const namespaces = await store.listNamespaces({
|
||||
prefix: ["users"],
|
||||
})
|
||||
|
||||
expect(namespaces).toEqual([
|
||||
["users", "ada"],
|
||||
["users", "grace"],
|
||||
])
|
||||
})
|
||||
})
|
||||
323
packages/tools/src/langchain/store.ts
Normal file
323
packages/tools/src/langchain/store.ts
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import { BaseStore } from "@langchain/langgraph"
|
||||
import type {
|
||||
GetOperation,
|
||||
Item,
|
||||
ListNamespacesOperation,
|
||||
MatchCondition,
|
||||
Operation,
|
||||
OperationResults,
|
||||
PutOperation,
|
||||
SearchOperation,
|
||||
} from "@langchain/langgraph"
|
||||
import type { Supermemory } from "supermemory"
|
||||
import {
|
||||
DEFAULT_VALUES,
|
||||
SEARCH_LIMIT_BOUNDS,
|
||||
createToolsClient,
|
||||
isNotFoundError,
|
||||
} from "../tools-shared"
|
||||
import type { SupermemoryToolsConfig } from "../types"
|
||||
import { fromSupermemoryNamespace, toSupermemoryNamespace } from "./namespace"
|
||||
|
||||
const KEY_FIELD = "langgraphKey"
|
||||
const PAGE_SIZE = 100
|
||||
|
||||
type StoredDocument = Awaited<ReturnType<Supermemory["documents"]["get"]>>
|
||||
|
||||
/** LangGraph's persistent store interface backed by Supermemory v5. */
|
||||
export class SupermemoryStore extends BaseStore {
|
||||
private client
|
||||
|
||||
constructor(apiKey: string, config?: SupermemoryToolsConfig) {
|
||||
super()
|
||||
this.client = createToolsClient(apiKey, config)
|
||||
}
|
||||
|
||||
async batch<Op extends Operation[]>(
|
||||
operations: Op,
|
||||
): Promise<OperationResults<Op>> {
|
||||
const results = []
|
||||
for (const operation of operations) {
|
||||
if ("namespacePrefix" in operation) {
|
||||
results.push(await this.runSearch(operation))
|
||||
} else if ("value" in operation) {
|
||||
results.push(await this.runPut(operation))
|
||||
} else if ("key" in operation) {
|
||||
results.push(await this.runGet(operation))
|
||||
} else {
|
||||
results.push(await this.runListNamespaces(operation))
|
||||
}
|
||||
}
|
||||
return results as OperationResults<Op>
|
||||
}
|
||||
|
||||
private async runPut(operation: PutOperation): Promise<void> {
|
||||
const namespace = toSupermemoryNamespace(operation.namespace)
|
||||
const existing = await this.getDocument(namespace, operation.key)
|
||||
|
||||
if (operation.value === null) {
|
||||
if (!existing) return
|
||||
const document = await this.settle(namespace, existing)
|
||||
const response = await this.client.documents.delete(namespace, {
|
||||
ids: [document.id],
|
||||
})
|
||||
if (response.count !== 1) {
|
||||
throw new Error(
|
||||
response.errors[0]?.error ??
|
||||
`Failed to delete LangGraph item ${operation.key}`,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const content = JSON.stringify(operation.value)
|
||||
if (existing) {
|
||||
const document = await this.settle(namespace, existing)
|
||||
await this.client.documents.update(namespace, document.id, { content })
|
||||
return
|
||||
}
|
||||
|
||||
await this.client.add(namespace, {
|
||||
content,
|
||||
id: operation.key,
|
||||
metadata: { [KEY_FIELD]: operation.key },
|
||||
taskType: "superrag",
|
||||
})
|
||||
}
|
||||
|
||||
private async runGet(operation: GetOperation): Promise<Item | null> {
|
||||
const namespace = toSupermemoryNamespace(operation.namespace)
|
||||
const found = await this.getDocument(namespace, operation.key)
|
||||
if (!found) return null
|
||||
return this.toItem(
|
||||
await this.settle(namespace, found),
|
||||
operation.namespace,
|
||||
operation.key,
|
||||
)
|
||||
}
|
||||
|
||||
private async runSearch(operation: SearchOperation) {
|
||||
const limit = operation.limit ?? DEFAULT_VALUES.limit
|
||||
const offset = operation.offset ?? 0
|
||||
const namespaces = await this.namespacesUnder(operation.namespacePrefix)
|
||||
const pages = await Promise.all(
|
||||
namespaces.map((namespace) =>
|
||||
operation.query
|
||||
? this.searchNamespace(namespace, operation.query)
|
||||
: this.listNamespace(namespace),
|
||||
),
|
||||
)
|
||||
const items = pages.flat()
|
||||
|
||||
if (operation.query) {
|
||||
items.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
||||
}
|
||||
const filter = operation.filter
|
||||
const matching = filter
|
||||
? items.filter((item) => matchesFilter(item.value, filter))
|
||||
: items
|
||||
return matching.slice(offset, offset + limit)
|
||||
}
|
||||
|
||||
private async searchNamespace(namespace: string[], query: string) {
|
||||
const encoded = toSupermemoryNamespace(namespace)
|
||||
// ponytail: one maximum API page per namespace; add server-side paging if
|
||||
// search gains it and stores need more than the top 50 candidates.
|
||||
const response = await this.client.search(encoded, {
|
||||
query,
|
||||
searchMode: "chunks",
|
||||
limit: SEARCH_LIMIT_BOUNDS.max,
|
||||
threshold: 0,
|
||||
})
|
||||
|
||||
const items = await Promise.all(
|
||||
response.results.map(async (result) => {
|
||||
const key = result.metadata[KEY_FIELD]
|
||||
if (typeof key !== "string") return null
|
||||
const document = await this.getDocument(encoded, key)
|
||||
if (!document) return null
|
||||
return {
|
||||
...this.toItem(document, namespace, key),
|
||||
score: result.similarity,
|
||||
}
|
||||
}),
|
||||
)
|
||||
return items.filter((item) => item !== null)
|
||||
}
|
||||
|
||||
private async listNamespace(namespace: string[]) {
|
||||
const encoded = toSupermemoryNamespace(namespace)
|
||||
const items: Array<Item & { score?: number }> = []
|
||||
let page = 1
|
||||
while (true) {
|
||||
const response = await this.client.list(encoded, "documents", {
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
const documents = await Promise.all(
|
||||
response.documents.map(async (listed) => {
|
||||
const document = await this.getDocument(encoded, listed.id)
|
||||
return document ? this.settle(encoded, document) : null
|
||||
}),
|
||||
)
|
||||
for (const document of documents) {
|
||||
if (!document) continue
|
||||
const key = document.metadata[KEY_FIELD]
|
||||
if (typeof key !== "string") continue
|
||||
items.push(this.toItem(document, namespace, key))
|
||||
}
|
||||
if (page >= response.pagination.totalPages) return items
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
|
||||
private async runListNamespaces(operation: ListNamespacesOperation) {
|
||||
const seen = new Map<string, string[]>()
|
||||
const conditions = operation.matchConditions ?? []
|
||||
for (const namespace of await this.listManagedNamespaces()) {
|
||||
let candidate = namespace
|
||||
if (operation.maxDepth !== undefined) {
|
||||
candidate = candidate.slice(0, operation.maxDepth)
|
||||
}
|
||||
if (!conditions.every((condition) => matches(candidate, condition))) {
|
||||
continue
|
||||
}
|
||||
seen.set(candidate.join("\u0000"), candidate)
|
||||
}
|
||||
|
||||
return [...seen.values()]
|
||||
.sort((a, b) => a.join("\u0000").localeCompare(b.join("\u0000")))
|
||||
.slice(operation.offset, operation.offset + operation.limit)
|
||||
}
|
||||
|
||||
private async namespacesUnder(prefix: string[]): Promise<string[][]> {
|
||||
const namespaces = await this.listManagedNamespaces()
|
||||
return namespaces.filter(
|
||||
(namespace) =>
|
||||
namespace.length >= prefix.length &&
|
||||
prefix.every((segment, index) => namespace[index] === segment),
|
||||
)
|
||||
}
|
||||
|
||||
private async listManagedNamespaces(): Promise<string[][]> {
|
||||
const namespaces: string[][] = []
|
||||
let page = 1
|
||||
while (true) {
|
||||
const response = await this.client.namespaces.list({
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
for (const entry of response.namespaces) {
|
||||
const decoded = fromSupermemoryNamespace(entry.namespace)
|
||||
if (decoded) namespaces.push(decoded)
|
||||
}
|
||||
if (page >= response.pagination.totalPages) return namespaces
|
||||
page += 1
|
||||
}
|
||||
}
|
||||
|
||||
private async getDocument(
|
||||
namespace: string,
|
||||
id: string,
|
||||
): Promise<StoredDocument | null> {
|
||||
try {
|
||||
return await this.client.documents.get(namespace, id)
|
||||
} catch (error) {
|
||||
if (isNotFoundError(error)) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async settle(
|
||||
namespace: string,
|
||||
document: StoredDocument,
|
||||
): Promise<StoredDocument> {
|
||||
let current = document
|
||||
for (let attempt = 0; !isSettled(current.system.status); attempt++) {
|
||||
if (attempt === 60) {
|
||||
throw new Error(`Supermemory document ${document.id} still processing`)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
current = await this.client.documents.get(namespace, document.id)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private toItem(
|
||||
document: StoredDocument,
|
||||
namespace: string[],
|
||||
key: string,
|
||||
): Item {
|
||||
return {
|
||||
value: parseValue(document.content),
|
||||
key,
|
||||
namespace,
|
||||
createdAt: new Date(document.system.createdAt),
|
||||
updatedAt: new Date(document.system.updatedAt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSettled(status: string): boolean {
|
||||
return status === "done" || status === "failed"
|
||||
}
|
||||
|
||||
function parseValue(content: string | null): Record<string, unknown> {
|
||||
if (!content) return {}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(content)
|
||||
return parsed !== null && typeof parsed === "object"
|
||||
? (parsed as Record<string, unknown>)
|
||||
: { value: parsed }
|
||||
} catch {
|
||||
return { content }
|
||||
}
|
||||
}
|
||||
|
||||
function matchesFilter(
|
||||
value: Record<string, unknown>,
|
||||
filter: Record<string, unknown>,
|
||||
): boolean {
|
||||
return Object.entries(filter).every(([field, expected]) =>
|
||||
compareValues(value[field], expected),
|
||||
)
|
||||
}
|
||||
|
||||
function compareValues(actual: unknown, expected: unknown): boolean {
|
||||
if (expected === null || typeof expected !== "object") {
|
||||
return actual === expected
|
||||
}
|
||||
return Object.entries(expected).every(([operator, value]) => {
|
||||
switch (operator) {
|
||||
case "$eq":
|
||||
return actual === value
|
||||
case "$ne":
|
||||
return actual !== value
|
||||
case "$gt":
|
||||
return Number(actual) > Number(value)
|
||||
case "$gte":
|
||||
return Number(actual) >= Number(value)
|
||||
case "$lt":
|
||||
return Number(actual) < Number(value)
|
||||
case "$lte":
|
||||
return Number(actual) <= Number(value)
|
||||
case "$in":
|
||||
return Array.isArray(value) && value.includes(actual)
|
||||
case "$nin":
|
||||
return !Array.isArray(value) || !value.includes(actual)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function matches(namespace: string[], condition: MatchCondition): boolean {
|
||||
if (condition.path.length > namespace.length) return false
|
||||
const start =
|
||||
condition.matchType === "suffix"
|
||||
? namespace.length - condition.path.length
|
||||
: 0
|
||||
return condition.path.every(
|
||||
(pattern, index) => pattern === "*" || namespace[start + index] === pattern,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ export default defineConfig({
|
|||
"src/index.ts",
|
||||
"src/ai-sdk.ts",
|
||||
"src/claude-memory.ts",
|
||||
"src/langchain/index.ts",
|
||||
"src/openai/index.ts",
|
||||
"src/mastra.ts",
|
||||
"src/voltagent/index.ts",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue