getmnemo-vercel-ai 0.1.0
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +92 -0
- package/dist/react.js.map +1 -0
- package/dist/tools.d.ts +37 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +90 -0
- package/dist/tools.js.map +1 -0
- package/package.json +71 -0
- package/src/index.ts +6 -0
- package/src/react.ts +129 -0
- package/src/tools.test.ts +63 -0
- package/src/tools.ts +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mnemo, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @ledgermem/vercel-ai
|
|
2
|
+
|
|
3
|
+
LedgerMem adapter for the [Vercel AI SDK](https://sdk.vercel.dev). Drop-in
|
|
4
|
+
`tool()` definitions that let any model search and write persistent memory,
|
|
5
|
+
plus a small React hook for client-side memory views.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ledgermem/vercel-ai @ledgermem/memory ai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Set `LEDGERMEM_API_KEY` and `LEDGERMEM_WORKSPACE_ID` in your environment, or
|
|
14
|
+
pass them explicitly.
|
|
15
|
+
|
|
16
|
+
## Quickstart (30 seconds)
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { streamText } from "ai";
|
|
20
|
+
import { openai } from "@ai-sdk/openai";
|
|
21
|
+
import { ledgermemTools } from "@ledgermem/vercel-ai";
|
|
22
|
+
|
|
23
|
+
const result = await streamText({
|
|
24
|
+
model: openai("gpt-4o"),
|
|
25
|
+
tools: ledgermemTools, // memorySearch + memoryAdd
|
|
26
|
+
maxSteps: 5,
|
|
27
|
+
messages: [{ role: "user", content: "What did I tell you about my coffee?" }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
for await (const chunk of result.textStream) process.stdout.write(chunk);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Per-user memory (route handler)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createLedgerMemTools } from "@ledgermem/vercel-ai";
|
|
37
|
+
|
|
38
|
+
export async function POST(req: Request) {
|
|
39
|
+
const { messages, userId } = await req.json();
|
|
40
|
+
const tools = createLedgerMemTools({ metadata: { userId } });
|
|
41
|
+
return streamText({ model: openai("gpt-4o"), tools, messages }).toDataStreamResponse();
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## React hook
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
"use client";
|
|
49
|
+
import { useLedgerMem } from "@ledgermem/vercel-ai/react";
|
|
50
|
+
|
|
51
|
+
export function MemorySidebar() {
|
|
52
|
+
const { results, search, loading } = useLedgerMem({ initialQuery: "preferences" });
|
|
53
|
+
return (
|
|
54
|
+
<ul>
|
|
55
|
+
{results.map((m: any) => <li key={m.id}>{m.content}</li>)}
|
|
56
|
+
</ul>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,GAGd,MAAM,YAAY,CAAC"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Mnemo } from "getmnemo";
|
|
2
|
+
export interface UseMnemoOptions {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
workspaceId?: string;
|
|
5
|
+
client?: Mnemo;
|
|
6
|
+
/** Initial query to run on mount; pass `undefined` to skip. */
|
|
7
|
+
initialQuery?: string;
|
|
8
|
+
initialLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface UseMnemoResult<T = unknown> {
|
|
11
|
+
results: T[];
|
|
12
|
+
loading: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
search: (query: string, limit?: number) => Promise<T[]>;
|
|
15
|
+
add: (content: string, metadata?: Record<string, unknown>) => Promise<T>;
|
|
16
|
+
remove: (id: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tiny React hook for direct Mnemo access from client components.
|
|
20
|
+
*
|
|
21
|
+
* For tool-use inside `useChat`, prefer wiring `getmnemoTools` into the
|
|
22
|
+
* server route instead — this hook is for sidebars / memory inspectors.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useMnemo<T = unknown>(options?: UseMnemoOptions): UseMnemoResult<T>;
|
|
25
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACxD,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAClC,OAAO,GAAE,eAAoB,GAC5B,cAAc,CAAC,CAAC,CAAC,CAsFnB"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Mnemo } from "getmnemo";
|
|
3
|
+
/**
|
|
4
|
+
* Tiny React hook for direct Mnemo access from client components.
|
|
5
|
+
*
|
|
6
|
+
* For tool-use inside `useChat`, prefer wiring `getmnemoTools` into the
|
|
7
|
+
* server route instead — this hook is for sidebars / memory inspectors.
|
|
8
|
+
*/
|
|
9
|
+
export function useMnemo(options = {}) {
|
|
10
|
+
const [client] = useState(() => resolveClient(options));
|
|
11
|
+
const [results, setResults] = useState([]);
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
// Last-wins guard: every search() bumps the counter and only the most
|
|
15
|
+
// recent call is allowed to write to state. Without this, two rapid
|
|
16
|
+
// searches let the slower one overwrite the fresher one (the classic
|
|
17
|
+
// "stale request wins" race), and unmounting mid-flight wrote state on
|
|
18
|
+
// a torn-down component.
|
|
19
|
+
const requestIdRef = useRef(0);
|
|
20
|
+
const mountedRef = useRef(true);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
mountedRef.current = true;
|
|
23
|
+
return () => {
|
|
24
|
+
mountedRef.current = false;
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
const search = useCallback(async (query, limit = 5) => {
|
|
28
|
+
const myId = ++requestIdRef.current;
|
|
29
|
+
setLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
const r = ((await client.search({ query, limit })).hits);
|
|
33
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
34
|
+
setResults(r);
|
|
35
|
+
}
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
40
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
41
|
+
setError(err);
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [client]);
|
|
51
|
+
const add = useCallback(async (content, metadata = {}) => {
|
|
52
|
+
// Bump the request id BEFORE awaiting so any in-flight ``search``
|
|
53
|
+
// started earlier loses the last-wins race and cannot wipe the
|
|
54
|
+
// memory we are about to prepend. Without this, a fast
|
|
55
|
+
// ``add()`` immediately after a slow ``search()`` would see the
|
|
56
|
+
// search resolve last and clobber the freshly-added entry.
|
|
57
|
+
const myId = ++requestIdRef.current;
|
|
58
|
+
const memory = (await client.add({ content, metadata }));
|
|
59
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
60
|
+
setResults((prev) => [memory, ...prev]);
|
|
61
|
+
}
|
|
62
|
+
return memory;
|
|
63
|
+
}, [client]);
|
|
64
|
+
const remove = useCallback(async (id) => {
|
|
65
|
+
const myId = ++requestIdRef.current;
|
|
66
|
+
await client.delete(id);
|
|
67
|
+
// Same last-wins guard as ``add`` — a slow concurrent ``search``
|
|
68
|
+
// could otherwise resolve after the delete and re-introduce the
|
|
69
|
+
// just-removed row into the rendered list.
|
|
70
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
71
|
+
setResults((prev) => prev.filter((r) => r?.id !== id));
|
|
72
|
+
}
|
|
73
|
+
}, [client]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (options.initialQuery) {
|
|
76
|
+
void search(options.initialQuery, options.initialLimit);
|
|
77
|
+
}
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
}, []);
|
|
80
|
+
return { results, loading, error, search, add, remove };
|
|
81
|
+
}
|
|
82
|
+
function resolveClient(opts) {
|
|
83
|
+
if (opts.client)
|
|
84
|
+
return opts.client;
|
|
85
|
+
const apiKey = opts.apiKey ?? process.env.NEXT_PUBLIC_GETMNEMO_API_KEY;
|
|
86
|
+
const workspaceId = opts.workspaceId ?? process.env.NEXT_PUBLIC_GETMNEMO_WORKSPACE_ID;
|
|
87
|
+
if (!apiKey || !workspaceId) {
|
|
88
|
+
throw new Error("useMnemo: missing apiKey/workspaceId. Note: client-side keys are public — prefer a server route in production.");
|
|
89
|
+
}
|
|
90
|
+
return new Mnemo({ apiKey, workspaceId });
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Mnemo } from "getmnemo";
|
|
2
|
+
import { tool } from "ai";
|
|
3
|
+
/**
|
|
4
|
+
* Options for constructing a Mnemo toolset.
|
|
5
|
+
*
|
|
6
|
+
* Either pass `client` (a pre-built `Mnemo` instance) or `apiKey` +
|
|
7
|
+
* `workspaceId` and one will be created for you.
|
|
8
|
+
*/
|
|
9
|
+
export interface MnemoToolsOptions {
|
|
10
|
+
client?: Mnemo;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
workspaceId?: string;
|
|
13
|
+
/** Default `limit` passed to `search` when the model omits it. */
|
|
14
|
+
defaultLimit?: number;
|
|
15
|
+
/** Static metadata merged into every `add` call (e.g. `{ userId }`). */
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface MnemoToolset {
|
|
19
|
+
memorySearch: ReturnType<typeof tool>;
|
|
20
|
+
memoryAdd: ReturnType<typeof tool>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a pair of Vercel AI SDK tools backed by Mnemo.
|
|
24
|
+
*
|
|
25
|
+
* Drop the returned object into `streamText({ tools })` or
|
|
26
|
+
* `generateText({ tools })` and the model can search and write
|
|
27
|
+
* persistent memory.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createMnemoTools(options?: MnemoToolsOptions): MnemoToolset;
|
|
30
|
+
/**
|
|
31
|
+
* Pre-built default toolset using `GETMNEMO_API_KEY` and
|
|
32
|
+
* `GETMNEMO_WORKSPACE_ID` from `process.env`.
|
|
33
|
+
*
|
|
34
|
+
* Lazy — the client isn't constructed until a tool actually runs.
|
|
35
|
+
*/
|
|
36
|
+
export declare const getmnemoTools: MnemoToolset;
|
|
37
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAG1B;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;IACtC,SAAS,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,iBAAsB,GAC9B,YAAY,CA8Dd;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,YAMxB,CAAC"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Mnemo } from "getmnemo";
|
|
2
|
+
import { tool } from "ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* Build a pair of Vercel AI SDK tools backed by Mnemo.
|
|
6
|
+
*
|
|
7
|
+
* Drop the returned object into `streamText({ tools })` or
|
|
8
|
+
* `generateText({ tools })` and the model can search and write
|
|
9
|
+
* persistent memory.
|
|
10
|
+
*/
|
|
11
|
+
export function createMnemoTools(options = {}) {
|
|
12
|
+
const client = resolveClient(options);
|
|
13
|
+
const defaultLimit = options.defaultLimit ?? 5;
|
|
14
|
+
const baseMetadata = options.metadata ?? {};
|
|
15
|
+
const memorySearch = tool({
|
|
16
|
+
description: "Search the user's long-term memory for facts, preferences, or past conversations relevant to the current query. Returns the most relevant snippets.",
|
|
17
|
+
// .strict() emits additionalProperties:false so providers that honour
|
|
18
|
+
// strict JSON schema (OpenAI, Anthropic) reject hallucinated keys
|
|
19
|
+
// instead of silently dropping them at parse time.
|
|
20
|
+
parameters: z
|
|
21
|
+
.object({
|
|
22
|
+
query: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.describe("Natural-language query describing what to recall."),
|
|
26
|
+
limit: z
|
|
27
|
+
.number()
|
|
28
|
+
.int()
|
|
29
|
+
.positive()
|
|
30
|
+
.max(50)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Max number of memories to return."),
|
|
33
|
+
}),
|
|
34
|
+
execute: async ({ query, limit }) => {
|
|
35
|
+
// Clamp the limit defensively — the schema constrains the model,
|
|
36
|
+
// but a non-conforming provider response could still smuggle a
|
|
37
|
+
// huge value through and blow the context window.
|
|
38
|
+
const requested = limit ?? defaultLimit;
|
|
39
|
+
const safeLimit = Math.min(50, Math.max(1, Math.floor(requested)));
|
|
40
|
+
const results = await client.search({ query, limit: safeLimit });
|
|
41
|
+
return { results };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const memoryAdd = tool({
|
|
45
|
+
description: "Save a new fact, preference, or noteworthy detail about the user to long-term memory. Use sparingly — only for information worth remembering across sessions.",
|
|
46
|
+
parameters: z
|
|
47
|
+
.object({
|
|
48
|
+
content: z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.describe("The fact or note to remember, written in plain text."),
|
|
52
|
+
metadata: z
|
|
53
|
+
.record(z.unknown())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Optional structured tags (e.g. { topic, source })."),
|
|
56
|
+
}),
|
|
57
|
+
execute: async ({ content, metadata }) => {
|
|
58
|
+
// Model-supplied metadata is merged FIRST so trusted baseMetadata
|
|
59
|
+
// (e.g. userId, workspaceId) cannot be overwritten by prompt injection.
|
|
60
|
+
const merged = { ...(metadata ?? {}), ...baseMetadata };
|
|
61
|
+
const memory = await client.add({ content, metadata: merged });
|
|
62
|
+
return { memory };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return { memorySearch, memoryAdd };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Pre-built default toolset using `GETMNEMO_API_KEY` and
|
|
69
|
+
* `GETMNEMO_WORKSPACE_ID` from `process.env`.
|
|
70
|
+
*
|
|
71
|
+
* Lazy — the client isn't constructed until a tool actually runs.
|
|
72
|
+
*/
|
|
73
|
+
export const getmnemoTools = (() => {
|
|
74
|
+
let cached = null;
|
|
75
|
+
const get = () => (cached ??= createMnemoTools());
|
|
76
|
+
return new Proxy({}, {
|
|
77
|
+
get: (_target, prop) => get()[prop],
|
|
78
|
+
});
|
|
79
|
+
})();
|
|
80
|
+
function resolveClient(opts) {
|
|
81
|
+
if (opts.client)
|
|
82
|
+
return opts.client;
|
|
83
|
+
const apiKey = opts.apiKey ?? process.env.GETMNEMO_API_KEY;
|
|
84
|
+
const workspaceId = opts.workspaceId ?? process.env.GETMNEMO_WORKSPACE_ID;
|
|
85
|
+
if (!apiKey || !workspaceId) {
|
|
86
|
+
throw new Error("createMnemoTools: missing apiKey/workspaceId. Pass them explicitly or set GETMNEMO_API_KEY and GETMNEMO_WORKSPACE_ID.");
|
|
87
|
+
}
|
|
88
|
+
return new Mnemo({ apiKey, workspaceId });
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "getmnemo-vercel-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mnemo adapter for the Vercel AI SDK — drop-in tools for streamText / generateText / useChat.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./dist/react.d.ts",
|
|
15
|
+
"import": "./dist/react.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"lint": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"getmnemo",
|
|
31
|
+
"vercel-ai",
|
|
32
|
+
"ai-sdk",
|
|
33
|
+
"memory",
|
|
34
|
+
"rag",
|
|
35
|
+
"tools"
|
|
36
|
+
],
|
|
37
|
+
"author": "Mnemo <founders@getmnemo.xyz>",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/ledgermem/getmnemo-vercel-ai.git"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"getmnemo": "^0.1.0",
|
|
45
|
+
"zod": "^3.23.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@ai-sdk/react": "^1.0.0",
|
|
49
|
+
"ai": "^4.0.0",
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@ai-sdk/react": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"react": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"@types/react": "^19.2.14",
|
|
63
|
+
"ai": "^4.0.0",
|
|
64
|
+
"typescript": "^5.6.0",
|
|
65
|
+
"vitest": "^2.1.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18.17.0"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://getmnemo.xyz"
|
|
71
|
+
}
|
package/src/index.ts
ADDED
package/src/react.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Mnemo } from "getmnemo";
|
|
3
|
+
|
|
4
|
+
export interface UseMnemoOptions {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
workspaceId?: string;
|
|
7
|
+
client?: Mnemo;
|
|
8
|
+
/** Initial query to run on mount; pass `undefined` to skip. */
|
|
9
|
+
initialQuery?: string;
|
|
10
|
+
initialLimit?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseMnemoResult<T = unknown> {
|
|
14
|
+
results: T[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
search: (query: string, limit?: number) => Promise<T[]>;
|
|
18
|
+
add: (content: string, metadata?: Record<string, unknown>) => Promise<T>;
|
|
19
|
+
remove: (id: string) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tiny React hook for direct Mnemo access from client components.
|
|
24
|
+
*
|
|
25
|
+
* For tool-use inside `useChat`, prefer wiring `getmnemoTools` into the
|
|
26
|
+
* server route instead — this hook is for sidebars / memory inspectors.
|
|
27
|
+
*/
|
|
28
|
+
export function useMnemo<T = unknown>(
|
|
29
|
+
options: UseMnemoOptions = {},
|
|
30
|
+
): UseMnemoResult<T> {
|
|
31
|
+
const [client] = useState(() => resolveClient(options));
|
|
32
|
+
const [results, setResults] = useState<T[]>([]);
|
|
33
|
+
const [loading, setLoading] = useState(false);
|
|
34
|
+
const [error, setError] = useState<Error | null>(null);
|
|
35
|
+
// Last-wins guard: every search() bumps the counter and only the most
|
|
36
|
+
// recent call is allowed to write to state. Without this, two rapid
|
|
37
|
+
// searches let the slower one overwrite the fresher one (the classic
|
|
38
|
+
// "stale request wins" race), and unmounting mid-flight wrote state on
|
|
39
|
+
// a torn-down component.
|
|
40
|
+
const requestIdRef = useRef(0);
|
|
41
|
+
const mountedRef = useRef(true);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
mountedRef.current = true;
|
|
44
|
+
return () => {
|
|
45
|
+
mountedRef.current = false;
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const search = useCallback(
|
|
50
|
+
async (query: string, limit = 5): Promise<T[]> => {
|
|
51
|
+
const myId = ++requestIdRef.current;
|
|
52
|
+
setLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const r = ((await client.search({ query, limit })).hits) as unknown as T[];
|
|
56
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
57
|
+
setResults(r);
|
|
58
|
+
}
|
|
59
|
+
return r;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
62
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
63
|
+
setError(err);
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
} finally {
|
|
67
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[client],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const add = useCallback(
|
|
76
|
+
async (content: string, metadata: Record<string, unknown> = {}) => {
|
|
77
|
+
// Bump the request id BEFORE awaiting so any in-flight ``search``
|
|
78
|
+
// started earlier loses the last-wins race and cannot wipe the
|
|
79
|
+
// memory we are about to prepend. Without this, a fast
|
|
80
|
+
// ``add()`` immediately after a slow ``search()`` would see the
|
|
81
|
+
// search resolve last and clobber the freshly-added entry.
|
|
82
|
+
const myId = ++requestIdRef.current;
|
|
83
|
+
const memory = (await client.add({ content, metadata })) as T;
|
|
84
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
85
|
+
setResults((prev) => [memory, ...prev]);
|
|
86
|
+
}
|
|
87
|
+
return memory;
|
|
88
|
+
},
|
|
89
|
+
[client],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const remove = useCallback(
|
|
93
|
+
async (id: string) => {
|
|
94
|
+
const myId = ++requestIdRef.current;
|
|
95
|
+
await client.delete(id);
|
|
96
|
+
// Same last-wins guard as ``add`` — a slow concurrent ``search``
|
|
97
|
+
// could otherwise resolve after the delete and re-introduce the
|
|
98
|
+
// just-removed row into the rendered list.
|
|
99
|
+
if (mountedRef.current && myId === requestIdRef.current) {
|
|
100
|
+
setResults((prev) =>
|
|
101
|
+
prev.filter((r) => (r as { id?: string })?.id !== id),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[client],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (options.initialQuery) {
|
|
110
|
+
void search(options.initialQuery, options.initialLimit);
|
|
111
|
+
}
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
return { results, loading, error, search, add, remove };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveClient(opts: UseMnemoOptions): Mnemo {
|
|
119
|
+
if (opts.client) return opts.client;
|
|
120
|
+
const apiKey = opts.apiKey ?? process.env.NEXT_PUBLIC_GETMNEMO_API_KEY;
|
|
121
|
+
const workspaceId =
|
|
122
|
+
opts.workspaceId ?? process.env.NEXT_PUBLIC_GETMNEMO_WORKSPACE_ID;
|
|
123
|
+
if (!apiKey || !workspaceId) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"useMnemo: missing apiKey/workspaceId. Note: client-side keys are public — prefer a server route in production.",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return new Mnemo({ apiKey, workspaceId });
|
|
129
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createMnemoTools } from "./tools.js";
|
|
3
|
+
|
|
4
|
+
vi.mock("getmnemo", () => {
|
|
5
|
+
return {
|
|
6
|
+
Mnemo: vi.fn().mockImplementation(() => ({
|
|
7
|
+
search: vi
|
|
8
|
+
.fn()
|
|
9
|
+
.mockResolvedValue([{ id: "m1", content: "user likes oat milk" }]),
|
|
10
|
+
add: vi.fn().mockResolvedValue({ id: "m2", content: "stored" }),
|
|
11
|
+
update: vi.fn(),
|
|
12
|
+
delete: vi.fn(),
|
|
13
|
+
list: vi.fn(),
|
|
14
|
+
})),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("createMnemoTools", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("builds memorySearch and memoryAdd tools", () => {
|
|
24
|
+
const tools = createMnemoTools({ apiKey: "k", workspaceId: "w" });
|
|
25
|
+
expect(tools.memorySearch).toBeDefined();
|
|
26
|
+
expect(tools.memoryAdd).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("memorySearch.execute calls client.search with default limit", async () => {
|
|
30
|
+
const tools = createMnemoTools({
|
|
31
|
+
apiKey: "k",
|
|
32
|
+
workspaceId: "w",
|
|
33
|
+
defaultLimit: 7,
|
|
34
|
+
});
|
|
35
|
+
const out = await tools.memorySearch.execute!(
|
|
36
|
+
{ query: "oat milk" },
|
|
37
|
+
{ messages: [], toolCallId: "t1" },
|
|
38
|
+
);
|
|
39
|
+
expect(out).toEqual({
|
|
40
|
+
results: [{ id: "m1", content: "user likes oat milk" }],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("memoryAdd.execute merges base metadata with per-call metadata", async () => {
|
|
45
|
+
const tools = createMnemoTools({
|
|
46
|
+
apiKey: "k",
|
|
47
|
+
workspaceId: "w",
|
|
48
|
+
metadata: { userId: "u1" },
|
|
49
|
+
});
|
|
50
|
+
const out = await tools.memoryAdd.execute!(
|
|
51
|
+
{ content: "likes oat milk", metadata: { topic: "drinks" } },
|
|
52
|
+
{ messages: [], toolCallId: "t2" },
|
|
53
|
+
);
|
|
54
|
+
expect(out).toEqual({ memory: { id: "m2", content: "stored" } });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("throws if apiKey missing and env unset", () => {
|
|
58
|
+
const orig = process.env.GETMNEMO_API_KEY;
|
|
59
|
+
delete process.env.GETMNEMO_API_KEY;
|
|
60
|
+
expect(() => createMnemoTools({})).toThrow(/missing apiKey/);
|
|
61
|
+
if (orig) process.env.GETMNEMO_API_KEY = orig;
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Mnemo } from "getmnemo";
|
|
2
|
+
import { tool } from "ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for constructing a Mnemo toolset.
|
|
7
|
+
*
|
|
8
|
+
* Either pass `client` (a pre-built `Mnemo` instance) or `apiKey` +
|
|
9
|
+
* `workspaceId` and one will be created for you.
|
|
10
|
+
*/
|
|
11
|
+
export interface MnemoToolsOptions {
|
|
12
|
+
client?: Mnemo;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
workspaceId?: string;
|
|
15
|
+
/** Default `limit` passed to `search` when the model omits it. */
|
|
16
|
+
defaultLimit?: number;
|
|
17
|
+
/** Static metadata merged into every `add` call (e.g. `{ userId }`). */
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MnemoToolset {
|
|
22
|
+
memorySearch: ReturnType<typeof tool>;
|
|
23
|
+
memoryAdd: ReturnType<typeof tool>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a pair of Vercel AI SDK tools backed by Mnemo.
|
|
28
|
+
*
|
|
29
|
+
* Drop the returned object into `streamText({ tools })` or
|
|
30
|
+
* `generateText({ tools })` and the model can search and write
|
|
31
|
+
* persistent memory.
|
|
32
|
+
*/
|
|
33
|
+
export function createMnemoTools(
|
|
34
|
+
options: MnemoToolsOptions = {},
|
|
35
|
+
): MnemoToolset {
|
|
36
|
+
const client = resolveClient(options);
|
|
37
|
+
const defaultLimit = options.defaultLimit ?? 5;
|
|
38
|
+
const baseMetadata = options.metadata ?? {};
|
|
39
|
+
|
|
40
|
+
const memorySearch: any = tool({
|
|
41
|
+
description:
|
|
42
|
+
"Search the user's long-term memory for facts, preferences, or past conversations relevant to the current query. Returns the most relevant snippets.",
|
|
43
|
+
// .strict() emits additionalProperties:false so providers that honour
|
|
44
|
+
// strict JSON schema (OpenAI, Anthropic) reject hallucinated keys
|
|
45
|
+
// instead of silently dropping them at parse time.
|
|
46
|
+
parameters: z
|
|
47
|
+
.object({
|
|
48
|
+
query: z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.describe("Natural-language query describing what to recall."),
|
|
52
|
+
limit: z
|
|
53
|
+
.number()
|
|
54
|
+
.int()
|
|
55
|
+
.positive()
|
|
56
|
+
.max(50)
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Max number of memories to return."),
|
|
59
|
+
})
|
|
60
|
+
,
|
|
61
|
+
execute: async ({ query, limit }) => {
|
|
62
|
+
// Clamp the limit defensively — the schema constrains the model,
|
|
63
|
+
// but a non-conforming provider response could still smuggle a
|
|
64
|
+
// huge value through and blow the context window.
|
|
65
|
+
const requested = limit ?? defaultLimit;
|
|
66
|
+
const safeLimit = Math.min(50, Math.max(1, Math.floor(requested)));
|
|
67
|
+
const results = await client.search({ query, limit: safeLimit });
|
|
68
|
+
return { results };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const memoryAdd: any = tool({
|
|
73
|
+
description:
|
|
74
|
+
"Save a new fact, preference, or noteworthy detail about the user to long-term memory. Use sparingly — only for information worth remembering across sessions.",
|
|
75
|
+
parameters: z
|
|
76
|
+
.object({
|
|
77
|
+
content: z
|
|
78
|
+
.string()
|
|
79
|
+
.min(1)
|
|
80
|
+
.describe("The fact or note to remember, written in plain text."),
|
|
81
|
+
metadata: z
|
|
82
|
+
.record(z.unknown())
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Optional structured tags (e.g. { topic, source })."),
|
|
85
|
+
})
|
|
86
|
+
,
|
|
87
|
+
execute: async ({ content, metadata }) => {
|
|
88
|
+
// Model-supplied metadata is merged FIRST so trusted baseMetadata
|
|
89
|
+
// (e.g. userId, workspaceId) cannot be overwritten by prompt injection.
|
|
90
|
+
const merged = { ...(metadata ?? {}), ...baseMetadata };
|
|
91
|
+
const memory = await client.add({ content, metadata: merged });
|
|
92
|
+
return { memory };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { memorySearch, memoryAdd };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pre-built default toolset using `GETMNEMO_API_KEY` and
|
|
101
|
+
* `GETMNEMO_WORKSPACE_ID` from `process.env`.
|
|
102
|
+
*
|
|
103
|
+
* Lazy — the client isn't constructed until a tool actually runs.
|
|
104
|
+
*/
|
|
105
|
+
export const getmnemoTools: MnemoToolset = (() => {
|
|
106
|
+
let cached: MnemoToolset | null = null;
|
|
107
|
+
const get = () => (cached ??= createMnemoTools());
|
|
108
|
+
return new Proxy({} as MnemoToolset, {
|
|
109
|
+
get: (_target, prop: string) => get()[prop as keyof MnemoToolset],
|
|
110
|
+
});
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
function resolveClient(opts: MnemoToolsOptions): Mnemo {
|
|
114
|
+
if (opts.client) return opts.client;
|
|
115
|
+
const apiKey = opts.apiKey ?? process.env.GETMNEMO_API_KEY;
|
|
116
|
+
const workspaceId = opts.workspaceId ?? process.env.GETMNEMO_WORKSPACE_ID;
|
|
117
|
+
if (!apiKey || !workspaceId) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"createMnemoTools: missing apiKey/workspaceId. Pass them explicitly or set GETMNEMO_API_KEY and GETMNEMO_WORKSPACE_ID.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return new Mnemo({ apiKey, workspaceId });
|
|
123
|
+
}
|