@memoweft/adapter-ai-sdk 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/README.md +84 -0
- package/README.zh-CN.md +84 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/persistOnEnd.d.ts +68 -0
- package/dist/persistOnEnd.js +56 -0
- package/dist/recallMiddleware.d.ts +55 -0
- package/dist/recallMiddleware.js +94 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @memoweft/adapter-ai-sdk
|
|
2
|
+
|
|
3
|
+
> 中文版 · [README.zh-CN.md](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
**Vercel AI SDK adapter for [MemoWeft](https://github.com/memoweft/memoweft).** Give your AI SDK app long-term memory: **read** = recall relevant memory and inject it into the prompt; **write** = persist the user's own words after each turn.
|
|
6
|
+
|
|
7
|
+
This is an **external integration package**. It wraps MemoWeft's public Core facade (`createMemoWeftCore`) — it does not touch Core internals. `ai` is a peer dependency (bring your own).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i ai memoweft @memoweft/adapter-ai-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`ai` `^7` and `memoweft` `^0.5.0` are peer dependencies. You also need an `ai` provider (e.g. `@ai-sdk/openai`) for a real model.
|
|
16
|
+
|
|
17
|
+
## Two paths: read and write
|
|
18
|
+
|
|
19
|
+
### Read — recall via middleware
|
|
20
|
+
|
|
21
|
+
`createMemoWeftMiddleware(core)` returns a Vercel AI SDK `LanguageModelMiddleware`. In `transformParams` it takes the **last user message text**, calls `core.recall({ query })`, and injects the recalled cognitions into that user message before the model sees it.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { wrapLanguageModel, generateText } from 'ai';
|
|
25
|
+
import { createMemoWeftCore } from 'memoweft';
|
|
26
|
+
import { createMemoWeftMiddleware } from '@memoweft/adapter-ai-sdk';
|
|
27
|
+
|
|
28
|
+
const core = createMemoWeftCore({ dbPath: './memory.db' });
|
|
29
|
+
|
|
30
|
+
const model = wrapLanguageModel({
|
|
31
|
+
model: baseModel, // your @ai-sdk/* model
|
|
32
|
+
middleware: createMemoWeftMiddleware(core),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { text } = await generateText({ model, prompt: 'Explain recursion.' });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The injected block uses MemoWeft's own neutral wording (ported verbatim from Core's `knowledgeBlock`). Low-confidence items are explicitly marked *"only guesses — do not treat as established facts."* The adapter **adds no persona / character prompt** of its own — tone and role stay the host's job.
|
|
39
|
+
|
|
40
|
+
If recall returns nothing, the query has no user text, or recall throws, the params pass through unchanged (recall failure never blocks the reply).
|
|
41
|
+
|
|
42
|
+
Options: `{ subjectId?, lang?: 'en' | 'zh', onRecall? }`.
|
|
43
|
+
|
|
44
|
+
### Write — persist the user's turn on finish
|
|
45
|
+
|
|
46
|
+
`createPersistOnEnd(core, { userMessage, originId })` returns a callback for `generateText`/`streamText`'s **`onEnd`** (the SDK's `onFinish` is a deprecated alias for `onEnd`). After the turn ends it calls `core.ingestUserMessage` to store **the user's own words** as one `spoken` evidence.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createPersistOnEnd } from '@memoweft/adapter-ai-sdk';
|
|
50
|
+
|
|
51
|
+
const userMessage = 'I strongly prefer short, direct answers.';
|
|
52
|
+
await generateText({
|
|
53
|
+
model,
|
|
54
|
+
prompt: userMessage,
|
|
55
|
+
onEnd: createPersistOnEnd(core, { userMessage, originId: 'turn-1' }),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Why you pass `userMessage` explicitly:** the SDK's `onEnd` event carries only *result-side* fields (text, steps, usage, response…). It does not carry the original user input, and the request body sent to the provider has already been rewritten by the read middleware (recalled memory injected). So the only clean source of the user's real words is the value you already hold when you call `generateText`. The `onEnd` event object is **not used** — it is just the trigger.
|
|
60
|
+
|
|
61
|
+
- **User words only, never the assistant reply** (Core discipline: the assistant reply is not recorded as evidence).
|
|
62
|
+
- Give a stable `originId` (your turn/message id) for **idempotency** — the same turn stores at most one evidence even if `onEnd` fires more than once.
|
|
63
|
+
- No cloud-authorization bits are passed: `ingestUserMessage` stores `spoken` evidence, which does not involve the observed cloud-consent flags.
|
|
64
|
+
- Empty/whitespace-only input is skipped. Ingest errors go to `onError` (or are swallowed) — persisting memory never crashes your turn.
|
|
65
|
+
|
|
66
|
+
There is also a plain `persistUserTurn(core, { userMessage, originId? })` if you want to call it outside an `onEnd` hook.
|
|
67
|
+
|
|
68
|
+
## Full example
|
|
69
|
+
|
|
70
|
+
See [`examples/basic.ts`](./examples/basic.ts) — a two-turn chat where turn 1's words are stored and recalled into turn 2's prompt.
|
|
71
|
+
|
|
72
|
+
## Relationship to the MemoWeft Host
|
|
73
|
+
|
|
74
|
+
The Host (`apps/memoweft-host`) is MemoWeft's *reference application* — chat UI, multi-session, backups. This adapter is the *opposite direction*: it lets **your** app (built on the Vercel AI SDK) reuse MemoWeft as a memory backend, without the Host. Both talk to the same public Core facade; they don't depend on each other. Pick the Host if you want a ready UI; pick this adapter if you already have an AI SDK app and just want the memory layer.
|
|
75
|
+
|
|
76
|
+
## What it does not do
|
|
77
|
+
|
|
78
|
+
- No persona / character prompt (Core is headless — tone/role is the host's job).
|
|
79
|
+
- Does not store the assistant reply, only the user's words.
|
|
80
|
+
- Does not widen `observed` cloud sharing, and does not pass authorization bits on the write path.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @memoweft/adapter-ai-sdk
|
|
2
|
+
|
|
3
|
+
> English · [README.md](./README.md)
|
|
4
|
+
|
|
5
|
+
**[MemoWeft](https://github.com/memoweft/memoweft) 的 Vercel AI SDK 适配器。** 给你的 AI SDK 应用接上长期记忆:**读** = 召回相关记忆、注入进 prompt;**写** = 每轮对话结束后,沉淀【用户原话】。
|
|
6
|
+
|
|
7
|
+
这是个**外部集成包**,只消费 MemoWeft 的公开 Core 门面(`createMemoWeftCore`),不碰 Core 内部。`ai` 是 peer 依赖(你自备)。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i ai memoweft @memoweft/adapter-ai-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`ai`(`^7`)和 `memoweft`(`^0.5.0`)是 peer 依赖;跑真模型还需一个 `ai` provider(如 `@ai-sdk/openai`)。
|
|
16
|
+
|
|
17
|
+
## 两条路:读和写
|
|
18
|
+
|
|
19
|
+
### 读 —— 用 middleware 召回
|
|
20
|
+
|
|
21
|
+
`createMemoWeftMiddleware(core)` 返回一个 Vercel AI SDK 的 `LanguageModelMiddleware`。它在 `transformParams` 里取**最后一条 user 消息的文本**,调 `core.recall({ query })`,把召回到的认知注入回那条 user 消息,再交给模型。
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { wrapLanguageModel, generateText } from 'ai';
|
|
25
|
+
import { createMemoWeftCore } from 'memoweft';
|
|
26
|
+
import { createMemoWeftMiddleware } from '@memoweft/adapter-ai-sdk';
|
|
27
|
+
|
|
28
|
+
const core = createMemoWeftCore({ dbPath: './memory.db' });
|
|
29
|
+
|
|
30
|
+
const model = wrapLanguageModel({
|
|
31
|
+
model: baseModel, // 你的 @ai-sdk/* 模型
|
|
32
|
+
middleware: createMemoWeftMiddleware(core),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { text } = await generateText({ model, prompt: '讲讲递归。' });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
注入的这段说明**照搬 MemoWeft Core 现成的中性措辞**(逐字对齐 Core 的 `knowledgeBlock`):低置信条目明确标"只是假设,别当定论"。适配器**不自造任何人格/人设 prompt**——语气和角色仍归宿主。
|
|
39
|
+
|
|
40
|
+
召回为空、没有 user 文本、或召回抛错时,params 原样透传(召回失败绝不挡回话)。
|
|
41
|
+
|
|
42
|
+
选项:`{ subjectId?, lang?: 'en' | 'zh', onRecall? }`。
|
|
43
|
+
|
|
44
|
+
### 写 —— 对话结束沉淀用户原话
|
|
45
|
+
|
|
46
|
+
`createPersistOnEnd(core, { userMessage, originId })` 返回一个塞给 `generateText`/`streamText` 的 **`onEnd`** 回调(SDK 的 `onFinish` 是 `onEnd` 的 @deprecated 别名)。这一轮结束后,它调 `core.ingestUserMessage`,把【用户原话】存成一条 `spoken` 证据。
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createPersistOnEnd } from '@memoweft/adapter-ai-sdk';
|
|
50
|
+
|
|
51
|
+
const userMessage = '我很偏好简短、直接的回答。';
|
|
52
|
+
await generateText({
|
|
53
|
+
model,
|
|
54
|
+
prompt: userMessage,
|
|
55
|
+
onEnd: createPersistOnEnd(core, { userMessage, originId: 'turn-1' }),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**为什么要你显式传 `userMessage`:** SDK 的 `onEnd` 事件只带【结果侧】字段(text、steps、usage、response…),**不带**原始用户输入;而发给 provider 的请求体已经被读 middleware 改过(注入了召回记忆)。所以"用户真正说的那句原话"唯一干净的来源,就是你调 `generateText` 时本来就持有的那份。`onEnd` 事件对象**不被使用**——它只当触发时机。
|
|
60
|
+
|
|
61
|
+
- **只存用户原话、绝不存助手回话**(Core 纪律:助手回话不落证据)。
|
|
62
|
+
- 给稳定的 `originId`(你的 turnId/messageId)保证**幂等**——同一轮即便 `onEnd` 触发多次也只落一条。
|
|
63
|
+
- **不传任何上云授权位**:`ingestUserMessage` 存 `spoken` 证据,本就不涉 observed 的上云授权位。
|
|
64
|
+
- 空串/纯空白跳过不落库;落库出错走 `onError`(或静默吞)——存记忆失败绝不崩你这轮对话。
|
|
65
|
+
|
|
66
|
+
也有一个直白的 `persistUserTurn(core, { userMessage, originId? })`,想在 `onEnd` 之外自己调时用。
|
|
67
|
+
|
|
68
|
+
## 完整示例
|
|
69
|
+
|
|
70
|
+
见 [`examples/basic.ts`](./examples/basic.ts)——两轮对话:第 1 轮的话被存进去、召回进第 2 轮的 prompt。
|
|
71
|
+
|
|
72
|
+
## 与 MemoWeft Host 的关系
|
|
73
|
+
|
|
74
|
+
Host(`apps/memoweft-host`)是 MemoWeft 的*参考应用*——聊天界面、多会话、备份。本适配器是*反方向*:让**你自己**的应用(基于 Vercel AI SDK 搭的)把 MemoWeft 当记忆后端复用,不需要 Host。两者都对着同一个公开 Core 门面,彼此不依赖。想要现成 UI 就用 Host;已经有 AI SDK 应用、只想要记忆层,就用本适配器。
|
|
75
|
+
|
|
76
|
+
## 不做什么
|
|
77
|
+
|
|
78
|
+
- 不做人格/人设 prompt(Core 无头——语气/角色归宿主)。
|
|
79
|
+
- 不存助手回话,只存用户原话。
|
|
80
|
+
- 不放宽 `observed` 上云,写路径不传授权位。
|
|
81
|
+
|
|
82
|
+
## 许可
|
|
83
|
+
|
|
84
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memoweft/adapter-ai-sdk · 公开面。
|
|
3
|
+
*
|
|
4
|
+
* 把 MemoWeft 的长期记忆接进 Vercel AI SDK(`ai`):
|
|
5
|
+
* - 读:createMemoWeftMiddleware(core) → 塞进 wrapLanguageModel,召回记忆注入进 prompt。
|
|
6
|
+
* - 写:createPersistOnEnd(core, { userMessage, originId }) → 塞进 generateText 的 onEnd,
|
|
7
|
+
* 对话结束后把【用户原话】沉淀成 spoken 证据(不存助手回话)。
|
|
8
|
+
*/
|
|
9
|
+
export { createMemoWeftMiddleware, buildKnowledgeBlock, getLastUserMessageText, addToLastUserMessage, type MemoWeftMiddlewareOptions, } from './recallMiddleware.ts';
|
|
10
|
+
export { createPersistOnEnd, persistUserTurn, type PersistOnEndOptions, type PersistUserTurnInput, } from './persistOnEnd.ts';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memoweft/adapter-ai-sdk · 公开面。
|
|
3
|
+
*
|
|
4
|
+
* 把 MemoWeft 的长期记忆接进 Vercel AI SDK(`ai`):
|
|
5
|
+
* - 读:createMemoWeftMiddleware(core) → 塞进 wrapLanguageModel,召回记忆注入进 prompt。
|
|
6
|
+
* - 写:createPersistOnEnd(core, { userMessage, originId }) → 塞进 generateText 的 onEnd,
|
|
7
|
+
* 对话结束后把【用户原话】沉淀成 spoken 证据(不存助手回话)。
|
|
8
|
+
*/
|
|
9
|
+
export { createMemoWeftMiddleware, buildKnowledgeBlock, getLastUserMessageText, addToLastUserMessage, } from "./recallMiddleware.js";
|
|
10
|
+
export { createPersistOnEnd, persistUserTurn, } from "./persistOnEnd.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 写适配器:一轮对话结束后,把【用户这轮说的原话】沉淀成 MemoWeft 的 spoken 证据。
|
|
3
|
+
*
|
|
4
|
+
* 为什么由调用方显式传用户原话,而不是从 onEnd 事件里解析:
|
|
5
|
+
* Vercel AI SDK 的 onEnd(generateText/streamText)事件字段全是【结果侧】
|
|
6
|
+
* (text/content/steps/usage/responseMessages/response…),SDK 不保证有原始用户输入。
|
|
7
|
+
* 而发给 provider 的 request.body 已经被读适配器 middleware 注入过召回文本——
|
|
8
|
+
* 若从那里反解析用户话,会把注入的记忆一起当成"用户原话"存回去(脏数据)。
|
|
9
|
+
* 所以用户原话必须由调用方在发起 generateText 时就知道、显式交给本 helper(闭包捕获)。
|
|
10
|
+
* onEnd 只用来当"这一轮成功结束了、可以落库"的触发时机。
|
|
11
|
+
*
|
|
12
|
+
* Core 纪律(本适配器严格照做):
|
|
13
|
+
* - 只存【用户原话】,不存助手回话(conversation.ts 纪律:助手回话不落证据)。
|
|
14
|
+
* - 走 core.ingestUserMessage(只落一条 spoken 证据、不改画像)。
|
|
15
|
+
* - 稳定 originId 保证幂等(同一轮即便 onEnd 被触发多次 / 重放,也只落一条)。
|
|
16
|
+
*/
|
|
17
|
+
import type { MemoWeftCore } from 'memoweft';
|
|
18
|
+
/** 只依赖 ingestUserMessage 一个方法——测试可传最小 stub。 */
|
|
19
|
+
type IngestOnly = Pick<MemoWeftCore, 'ingestUserMessage'>;
|
|
20
|
+
export interface PersistUserTurnInput {
|
|
21
|
+
/** 用户这轮说的原话(由调用方在发起 generateText 时就持有的那份,别从模型响应里回捞)。 */
|
|
22
|
+
userMessage: string;
|
|
23
|
+
/**
|
|
24
|
+
* 稳定幂等键:同一轮对话给同一个 originId,重复触发 onEnd 也只落一条。
|
|
25
|
+
* 建议宿主用自己的 turnId / messageId;不传则不去重(每次都落,见 Core originId 语义)。
|
|
26
|
+
*/
|
|
27
|
+
originId?: string | null;
|
|
28
|
+
subjectId?: string;
|
|
29
|
+
hostId?: string;
|
|
30
|
+
/** ISO 时间戳;缺省交给 Core(perceive 里取"现在")。 */
|
|
31
|
+
occurredAt?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 直接落一轮用户原话(薄封装 core.ingestUserMessage,字段名对齐 Core `UserMessageInput`)。
|
|
35
|
+
* 不做任何"从响应里提取"的事——传进来什么原话就存什么。
|
|
36
|
+
*/
|
|
37
|
+
export declare function persistUserTurn(core: IngestOnly, input: PersistUserTurnInput): Promise<void>;
|
|
38
|
+
/** 造 onEnd 回调时的选项(用户原话由 factory 参数闭包捕获)。 */
|
|
39
|
+
export interface PersistOnEndOptions {
|
|
40
|
+
/** 用户这轮说的原话(发起 generateText 时就持有的那份)。 */
|
|
41
|
+
userMessage: string;
|
|
42
|
+
/** 稳定幂等键(强烈建议给:同一轮的 turnId/messageId)。 */
|
|
43
|
+
originId?: string | null;
|
|
44
|
+
subjectId?: string;
|
|
45
|
+
hostId?: string;
|
|
46
|
+
occurredAt?: string;
|
|
47
|
+
/** 落库出错时的回调(可选;不给则静默吞——落记忆失败不该崩主流程)。 */
|
|
48
|
+
onError?: (err: unknown) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 造一个可直接塞进 `generateText({ onEnd })` / `streamText({ onEnd })` 的回调。
|
|
52
|
+
*
|
|
53
|
+
* onEnd(`onFinish` 是它的 @deprecated 别名)事件对象【不被本回调使用】——
|
|
54
|
+
* 用户原话来自闭包捕获的 opts.userMessage,事件仅作触发时机。
|
|
55
|
+
* 回调返回 Promise:generateText 会 await onEnd(Callback 类型允许返回 PromiseLike),
|
|
56
|
+
* 所以落库完成后才 resolve;出错走 onError、不向外抛(不崩宿主主流程)。
|
|
57
|
+
*
|
|
58
|
+
* 用法:
|
|
59
|
+
* const userMessage = '……用户这轮的话……';
|
|
60
|
+
* await generateText({
|
|
61
|
+
* model: wrapLanguageModel({ model, middleware: createMemoWeftMiddleware(core) }),
|
|
62
|
+
* prompt: userMessage,
|
|
63
|
+
* onEnd: createPersistOnEnd(core, { userMessage, originId: turnId }),
|
|
64
|
+
* });
|
|
65
|
+
*/
|
|
66
|
+
export declare function createPersistOnEnd(core: IngestOnly, opts: PersistOnEndOptions): (event?: unknown) => Promise<void>;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=persistOnEnd.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 直接落一轮用户原话(薄封装 core.ingestUserMessage,字段名对齐 Core `UserMessageInput`)。
|
|
3
|
+
* 不做任何"从响应里提取"的事——传进来什么原话就存什么。
|
|
4
|
+
*/
|
|
5
|
+
export async function persistUserTurn(core, input) {
|
|
6
|
+
const text = input.userMessage;
|
|
7
|
+
// 空串 / 全空白不落库(没有"用户原话"可存)。
|
|
8
|
+
if (typeof text !== 'string' || text.trim() === '')
|
|
9
|
+
return;
|
|
10
|
+
await core.ingestUserMessage({
|
|
11
|
+
content: text,
|
|
12
|
+
originId: input.originId ?? null,
|
|
13
|
+
subjectId: input.subjectId,
|
|
14
|
+
hostId: input.hostId,
|
|
15
|
+
occurredAt: input.occurredAt,
|
|
16
|
+
// sourceKind 不传:Core 缺省 'spoken'(用户亲口)。
|
|
17
|
+
// 注意:不传任何授权位——ingestUserMessage 存 spoken,本就不涉上云授权位(红线)。
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 造一个可直接塞进 `generateText({ onEnd })` / `streamText({ onEnd })` 的回调。
|
|
22
|
+
*
|
|
23
|
+
* onEnd(`onFinish` 是它的 @deprecated 别名)事件对象【不被本回调使用】——
|
|
24
|
+
* 用户原话来自闭包捕获的 opts.userMessage,事件仅作触发时机。
|
|
25
|
+
* 回调返回 Promise:generateText 会 await onEnd(Callback 类型允许返回 PromiseLike),
|
|
26
|
+
* 所以落库完成后才 resolve;出错走 onError、不向外抛(不崩宿主主流程)。
|
|
27
|
+
*
|
|
28
|
+
* 用法:
|
|
29
|
+
* const userMessage = '……用户这轮的话……';
|
|
30
|
+
* await generateText({
|
|
31
|
+
* model: wrapLanguageModel({ model, middleware: createMemoWeftMiddleware(core) }),
|
|
32
|
+
* prompt: userMessage,
|
|
33
|
+
* onEnd: createPersistOnEnd(core, { userMessage, originId: turnId }),
|
|
34
|
+
* });
|
|
35
|
+
*/
|
|
36
|
+
export function createPersistOnEnd(core, opts) {
|
|
37
|
+
// 形参 event 被【故意忽略】——用户原话来自闭包 opts.userMessage,事件仅作触发时机。
|
|
38
|
+
// 声明它只为契合 SDK Callback = (event) => PromiseLike<void>,能直接塞进 onEnd。
|
|
39
|
+
return async (_event) => {
|
|
40
|
+
try {
|
|
41
|
+
await persistUserTurn(core, {
|
|
42
|
+
userMessage: opts.userMessage,
|
|
43
|
+
originId: opts.originId,
|
|
44
|
+
subjectId: opts.subjectId,
|
|
45
|
+
hostId: opts.hostId,
|
|
46
|
+
occurredAt: opts.occurredAt,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (opts.onError)
|
|
51
|
+
opts.onError(err);
|
|
52
|
+
// 无 onError 时静默吞:落记忆失败不该让宿主这轮对话失败。
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=persistOnEnd.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读适配器(RAG-as-middleware):把 MemoWeft 的长期记忆召回,注入进发给模型的 prompt。
|
|
3
|
+
*
|
|
4
|
+
* 范式:`wrapLanguageModel({ model, middleware: createMemoWeftMiddleware(core) })`。
|
|
5
|
+
* 在 `transformParams` 里取本轮最后一条 user 文本 → `await core.recall({ query })` →
|
|
6
|
+
* 按 Core 现成的 knowledgeBlock 中性口径拼成一段说明 → 注入回最后一条 user 消息。
|
|
7
|
+
*
|
|
8
|
+
* 边界(照 MemoWeft「Core 无头」纪律):注入文案只搬 Core `action.ts` 的中性措辞,
|
|
9
|
+
* 低置信条目明确标 "only guesses—do not treat as established facts"。适配器里不自造人格/人设 prompt。
|
|
10
|
+
*
|
|
11
|
+
* 类型:用 `ai` re-export 的宽松 `LanguageModelMiddleware`(specificationVersion 可选,抗大版本漂移),
|
|
12
|
+
* 不直绑 `@ai-sdk/provider` 的强版类型。
|
|
13
|
+
*/
|
|
14
|
+
import type { LanguageModelMiddleware } from 'ai';
|
|
15
|
+
import type { MemoWeftCore } from 'memoweft';
|
|
16
|
+
/** 最小召回项形状(只用注入需要的三个字段)。故意不引 Core 的完整类型,保持松耦合。 */
|
|
17
|
+
interface RecalledLike {
|
|
18
|
+
content: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
credStatus: string;
|
|
21
|
+
}
|
|
22
|
+
/** 只依赖 recall 一个方法——测试可传最小 stub。 */
|
|
23
|
+
type RecallOnly = Pick<MemoWeftCore, 'recall'>;
|
|
24
|
+
export interface MemoWeftMiddlewareOptions {
|
|
25
|
+
/** 召回归属的 subject;缺省交给 Core(config.identity.subjectId)。 */
|
|
26
|
+
subjectId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* 注入块的语言(措辞照 Core action.ts 的 knowledgeBlock 双语口径)。缺省 'en'。
|
|
29
|
+
* 只影响适配器拼的这段说明文字,不改 Core 行为。
|
|
30
|
+
*/
|
|
31
|
+
lang?: 'en' | 'zh';
|
|
32
|
+
/** 召回为空时的回调(可选,便于宿主观测/日志)。 */
|
|
33
|
+
onRecall?: (items: RecalledLike[]) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 拼注入块:照搬 Core `src/pipeline/action.ts` 的 knowledgeBlock 中性措辞(逐字对齐,别自造人设)。
|
|
37
|
+
* 空召回返回空串(调用方据此决定不注入)。
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildKnowledgeBlock(relevant: RecalledLike[], lang?: 'en' | 'zh'): string;
|
|
40
|
+
/** 从 SDK prompt 数组里取最后一条 user 消息里的纯文本(多个 text part 用换行拼)。找不到返回 null。 */
|
|
41
|
+
export declare function getLastUserMessageText(prompt: unknown): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* 把一段说明追加进最后一条 user 消息(作为额外的 text part 塞在原文本之前——
|
|
44
|
+
* 让"你已了解的情况"排在用户这轮问题之前,符合 knowledgeBlock 进 system 的语序意图)。
|
|
45
|
+
* 返回一个【新的】prompt 数组(不原地改,避免污染调用方持有的对象)。找不到 user 消息则原样返回。
|
|
46
|
+
*/
|
|
47
|
+
export declare function addToLastUserMessage(prompt: unknown, block: string): unknown;
|
|
48
|
+
/**
|
|
49
|
+
* 造一个 MemoWeft 读适配器 middleware。
|
|
50
|
+
* @param core 只需持有 `recall` 方法的 Core(或任意实现了 recall 的对象)。
|
|
51
|
+
* @param opts subjectId / lang / onRecall。
|
|
52
|
+
*/
|
|
53
|
+
export declare function createMemoWeftMiddleware(core: RecallOnly, opts?: MemoWeftMiddlewareOptions): LanguageModelMiddleware;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=recallMiddleware.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 拼注入块:照搬 Core `src/pipeline/action.ts` 的 knowledgeBlock 中性措辞(逐字对齐,别自造人设)。
|
|
3
|
+
* 空召回返回空串(调用方据此决定不注入)。
|
|
4
|
+
*/
|
|
5
|
+
export function buildKnowledgeBlock(relevant, lang = 'en') {
|
|
6
|
+
if (relevant.length === 0)
|
|
7
|
+
return '';
|
|
8
|
+
const lines = relevant
|
|
9
|
+
.map((c) => lang === 'zh'
|
|
10
|
+
? `- ${c.content}(把握度 ${c.confidence}/1000,${c.credStatus})`
|
|
11
|
+
: `- ${c.content} (confidence ${c.confidence}/1000, ${c.credStatus})`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
const head = lang === 'zh'
|
|
14
|
+
? '\n\n你已了解关于这个用户的一些情况(带把握度;低置信的只是假设,别当定论、别生硬复述):\n'
|
|
15
|
+
: '\n\nHere is some of what you already understand about this user (with confidence; low-confidence items are only guesses—do not treat them as established facts, and do not recite them stiffly):\n';
|
|
16
|
+
return head + lines;
|
|
17
|
+
}
|
|
18
|
+
// ── last-user-message helper(非 SDK 自带,按 params.prompt 真实结构自研)──
|
|
19
|
+
//
|
|
20
|
+
// `ai` 的 LanguageModelV4Prompt = Array<LanguageModelV4Message>;
|
|
21
|
+
// user 消息 = { role:'user', content: Array<TextPart|FilePart> },TextPart = { type:'text', text:string }。
|
|
22
|
+
// 这里只碰 text part、不动 file/image part(多模态原样保留)。
|
|
23
|
+
/** 从 SDK prompt 数组里取最后一条 user 消息里的纯文本(多个 text part 用换行拼)。找不到返回 null。 */
|
|
24
|
+
export function getLastUserMessageText(prompt) {
|
|
25
|
+
if (!Array.isArray(prompt))
|
|
26
|
+
return null;
|
|
27
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
28
|
+
const msg = prompt[i];
|
|
29
|
+
if (!msg || msg.role !== 'user')
|
|
30
|
+
continue;
|
|
31
|
+
if (!Array.isArray(msg.content))
|
|
32
|
+
return null;
|
|
33
|
+
const texts = msg.content
|
|
34
|
+
.filter((p) => {
|
|
35
|
+
const part = p;
|
|
36
|
+
return part?.type === 'text' && typeof part.text === 'string';
|
|
37
|
+
})
|
|
38
|
+
.map((p) => p.text);
|
|
39
|
+
return texts.length > 0 ? texts.join('\n') : null;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 把一段说明追加进最后一条 user 消息(作为额外的 text part 塞在原文本之前——
|
|
45
|
+
* 让"你已了解的情况"排在用户这轮问题之前,符合 knowledgeBlock 进 system 的语序意图)。
|
|
46
|
+
* 返回一个【新的】prompt 数组(不原地改,避免污染调用方持有的对象)。找不到 user 消息则原样返回。
|
|
47
|
+
*/
|
|
48
|
+
export function addToLastUserMessage(prompt, block) {
|
|
49
|
+
if (!Array.isArray(prompt) || block === '')
|
|
50
|
+
return prompt;
|
|
51
|
+
const next = prompt.slice();
|
|
52
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
53
|
+
const msg = next[i];
|
|
54
|
+
if (!msg || msg.role !== 'user' || !Array.isArray(msg.content))
|
|
55
|
+
continue;
|
|
56
|
+
const injected = { type: 'text', text: block.replace(/^\n+/, '') + '\n\n' };
|
|
57
|
+
next[i] = { ...msg, content: [injected, ...msg.content] };
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
return prompt;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 造一个 MemoWeft 读适配器 middleware。
|
|
64
|
+
* @param core 只需持有 `recall` 方法的 Core(或任意实现了 recall 的对象)。
|
|
65
|
+
* @param opts subjectId / lang / onRecall。
|
|
66
|
+
*/
|
|
67
|
+
export function createMemoWeftMiddleware(core, opts = {}) {
|
|
68
|
+
const { subjectId, lang = 'en', onRecall } = opts;
|
|
69
|
+
return {
|
|
70
|
+
async transformParams({ params }) {
|
|
71
|
+
const query = getLastUserMessageText(params.prompt);
|
|
72
|
+
// 没有可召回的 query(无 user 文本 / 纯多模态)→ 原样透传,不注入。
|
|
73
|
+
if (!query)
|
|
74
|
+
return params;
|
|
75
|
+
let recalled;
|
|
76
|
+
try {
|
|
77
|
+
recalled = await core.recall(subjectId ? { query, subjectId } : { query });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// 召回失败不挡回话(呼应 Core "召回失败不阻塞对话"纪律):静默降级为不注入。
|
|
81
|
+
return params;
|
|
82
|
+
}
|
|
83
|
+
onRecall?.(recalled);
|
|
84
|
+
const block = buildKnowledgeBlock(recalled, lang);
|
|
85
|
+
if (block === '')
|
|
86
|
+
return params;
|
|
87
|
+
// addToLastUserMessage 收/返宽松 unknown(对外也当独立工具用);这里回填 params 时
|
|
88
|
+
// 收窄回 params.prompt 的真实类型——注入只改了 text part、结构与原 prompt 一致,cast 安全。
|
|
89
|
+
const prompt = addToLastUserMessage(params.prompt, block);
|
|
90
|
+
return { ...params, prompt };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=recallMiddleware.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memoweft/adapter-ai-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vercel AI SDK adapter for MemoWeft — inject long-term memory via middleware (recall) and persist the user's turn on finish (write).",
|
|
5
|
+
"keywords": ["vercel-ai-sdk", "ai-sdk", "memoweft", "memory", "llm", "middleware"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
|
|
12
|
+
},
|
|
13
|
+
"files": ["dist/**/*.js", "dist/**/*.d.ts", "README.md", "README.zh-CN.md"],
|
|
14
|
+
"engines": { "node": ">=22" },
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/memoweft/memoweft.git",
|
|
18
|
+
"directory": "packages/adapter-ai-sdk"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": { "access": "public" },
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
23
|
+
"test": "node --test \"tests/**/*.test.ts\"",
|
|
24
|
+
"build": "tsc -p tsconfig.build.json",
|
|
25
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"ai": "^7",
|
|
29
|
+
"memoweft": "^0.5.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"ai": "^7.0.15",
|
|
33
|
+
"memoweft": "*"
|
|
34
|
+
}
|
|
35
|
+
}
|