Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ Debugging no longer means probing an opaque database — it becomes a determinis
| [`CHANGELOG.md`](./CHANGELOG.md) | Release notes and version history |
| [`openclaw.plugin.json`](./openclaw.plugin.json) | OpenClaw plugin manifest and configuration schema |
| [`src/adapters/gateway-client/README.md`](./src/adapters/gateway-client/README.md) | Gateway-based adapter guide for Codex, Claude Code, Dify, LangGraph, and custom agents |
| [`docs/DIFY-ADAPTER.md`](./docs/DIFY-ADAPTER.md) | Dify workflow adapter guide built on the Gateway adapter kit |

---

Expand Down
1 change: 1 addition & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ export MEMORY_TENCENTDB_GATEWAY_API_KEY="<与 Gateway 同一份密钥>"
| [`CHANGELOG.md`](./CHANGELOG.md) | 版本变更记录 |
| [`openclaw.plugin.json`](./openclaw.plugin.json) | OpenClaw 插件声明与配置 Schema |
| [`src/adapters/gateway-client/README.md`](./src/adapters/gateway-client/README.md) | 面向 Codex、Claude Code、Dify、LangGraph 和自定义 Agent 的 Gateway 适配指南 |
| [`docs/DIFY-ADAPTER.md`](./docs/DIFY-ADAPTER.md) | 基于 Gateway 适配工具包的 Dify workflow 接入说明 |

---
## 社区与贡献
Expand Down
118 changes: 118 additions & 0 deletions docs/DIFY-ADAPTER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Dify Adapter Guide

Related issue: https://github.com/TencentCloud/TencentDB-Agent-Memory/issues/235

This follow-up builds on the Gateway client adapter kit from #316. It does not
add a second Gateway SDK or a parallel cross-platform abstraction. The Dify
adapter only maps Dify workflow variables to the #316 `GatewayMemoryClient`
boundary and the existing Gateway `/recall` and `/capture` routes.

## Scope

Included:

- Dify workflow input mapping for recall before the LLM node.
- Dify answer mapping for capture after the LLM node.
- Stable Dify `session_key` construction from platform, user, conversation, and
optional session IDs.
- HTTP-only Dify node examples for users who do not run TypeScript.

Not included:

- A general-purpose Gateway SDK beyond #316.
- MCP server, Python SDK, Codex or Claude hooks.
- Core memory runtime changes.
- CI or packaging changes.

## TypeScript Adapter

Use the TypeScript adapter when your Dify workflow can call a small Node.js
bridge service. The adapter accepts either an injected Dify memory port or the
same `GatewayMemoryClientOptions` shape introduced by #316.

```ts
import { createDifyWorkflowMemoryAdapter } from "@tencentdb-agent-memory/memory-tencentdb";

const memory = createDifyWorkflowMemoryAdapter({
gateway: {
baseUrl: "http://127.0.0.1:8420",
apiKey: process.env.TDAI_GATEWAY_API_KEY,
},
});

const recalled = await memory.recall({
query: inputs.query,
conversation_id: conversationId,
user: userId,
});

// Inject recalled.memory_context into the Dify prompt template.

await memory.capture({
query: inputs.query,
answer: llmAnswer,
conversation_id: conversationId,
user: userId,
});
```

## HTTP-Only Dify Nodes

If you do not run TypeScript, use two HTTP request nodes.

Before the LLM node:

```http
POST http://127.0.0.1:8420/recall
Content-Type: application/json
Authorization: Bearer ${TDAI_GATEWAY_API_KEY}

{
"query": "{{query}}",
"session_key": "dify:{{user}}:{{conversation_id}}",
"user_id": "{{user}}"
}
```

Use `context` from the response as `memory_context`.

After the LLM node:

```http
POST http://127.0.0.1:8420/capture
Content-Type: application/json
Authorization: Bearer ${TDAI_GATEWAY_API_KEY}

{
"user_content": "{{query}}",
"assistant_content": "{{answer}}",
"session_key": "dify:{{user}}:{{conversation_id}}",
"user_id": "{{user}}"
}
```

## Field Mapping

| Dify field | Gateway field | Notes |
| --- | --- | --- |
| `query`, `inputs.query`, `user_content`, `prompt`, `message` | `query` / `user_content` | Recall and capture accept common Dify variable names. |
| `answer`, `assistant_content`, `response`, `output` | `assistant_content` | Required for capture. |
| `user`, `user_id` | `user_id` and `session_key` part | Falls back to `default_user`. |
| `conversation_id`, `session_id` | `session_key` part | Falls back to `default_conversation`. |
| `messages` | `messages` | Forwarded to `/capture` when present. |

## Validation

Run adapter-focused tests:

```bash
npm test -- src/adapters/gateway-client/gateway-client.test.ts src/adapters/dify/index.test.ts
```

Run the full suite before opening or updating a PR:

```bash
npm test
npm run build
git diff --check
```
11 changes: 9 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,23 @@ import {
import { resolveOpenClawStateDir } from "./src/utils/openclaw-state-dir.js";

export {
DifyWorkflowMemoryAdapter,
GatewayMemoryClient,
GatewayMemoryClientError,
createDifyWorkflowMemoryAdapter,
createGatewayPlatformAdapter,
} from "./src/adapters/gateway-client/index.js";
} from "./src/adapters/index.js";
export type {
DifyCaptureResult,
DifyGatewayMemoryPort,
DifyRecallResult,
DifyWorkflowInput,
DifyWorkflowMemoryAdapterOptions,
GatewayMemoryClientOptions,
GatewayPlatformAdapter,
GatewayPlatformAdapterOptions,
GatewayPlatformContext,
} from "./src/adapters/gateway-client/index.js";
} from "./src/adapters/index.js";

const TAG = "[memory-tdai]";

Expand Down
172 changes: 172 additions & 0 deletions src/adapters/dify/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { describe, expect, it } from "vitest";

import {
DifyWorkflowMemoryAdapter,
type DifyGatewayMemoryPort,
} from "./index.js";

function makeClient(calls: unknown[]): DifyGatewayMemoryPort {
return {
async recall(body) {
calls.push({ method: "recall", body });
return { context: "memory context", memory_count: 2, strategy: "hybrid" };
},
async capture(body) {
calls.push({ method: "capture", body });
return { l0_recorded: 2, scheduler_notified: true };
},
};
}

describe("DifyWorkflowMemoryAdapter", () => {
it("maps Dify workflow input to Gateway recall", async () => {
const calls: unknown[] = [];
const adapter = new DifyWorkflowMemoryAdapter({ client: makeClient(calls) });

const result = await adapter.recall({
inputs: { query: "project preference" },
conversation_id: "conv 1",
user: "user/42",
});

expect(result).toEqual({
session_key: "dify:user_42:conv_1",
memory_context: "memory context",
memory_count: 2,
strategy: "hybrid",
});
expect(calls).toEqual([
{
method: "recall",
body: {
query: "project preference",
session_key: "dify:user_42:conv_1",
user_id: "user/42",
},
},
]);
});

it("maps Dify answer output to Gateway capture", async () => {
const calls: unknown[] = [];
const adapter = new DifyWorkflowMemoryAdapter({
client: makeClient(calls),
platform: "dify-cloud",
});

const result = await adapter.capture({
query: "remember this",
answer: "stored",
conversation_id: "conv",
session_id: "run-1",
user_id: "u",
messages: [{ role: "user", content: "remember this" }],
});

expect(result).toEqual({
session_key: "dify-cloud:u:conv:run-1",
l0_recorded: 2,
scheduler_notified: true,
});
expect(calls).toEqual([
{
method: "capture",
body: {
user_content: "remember this",
assistant_content: "stored",
session_key: "dify-cloud:u:conv:run-1",
session_id: "run-1",
user_id: "u",
messages: [{ role: "user", content: "remember this" }],
},
},
]);
});

it("uses the #316 GatewayMemoryClient boundary for HTTP integration", async () => {
const calls: Array<{ url: string; init: RequestInit }> = [];
const adapter = new DifyWorkflowMemoryAdapter({
gateway: {
baseUrl: "http://127.0.0.1:8420/",
apiKey: "token",
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init: init ?? {} });
return new Response(JSON.stringify({ context: "from gateway", memory_count: 1 }), {
headers: { "Content-Type": "application/json" },
});
},
},
});

await expect(adapter.recall({
query: "latest project memory",
conversation_id: "conv",
user_id: "u",
})).resolves.toMatchObject({
memory_context: "from gateway",
session_key: "dify:u:conv",
});
expect(calls).toHaveLength(1);
expect(calls[0].url).toBe("http://127.0.0.1:8420/recall");
expect(calls[0].init.headers).toMatchObject({
"Content-Type": "application/json",
Authorization: "Bearer token",
});
expect(JSON.parse(String(calls[0].init.body))).toEqual({
query: "latest project memory",
session_key: "dify:u:conv",
user_id: "u",
});
});

it("keeps the platform adapter scoped to recall and capture routes", async () => {
const calls: Array<{ url: string; body: unknown }> = [];
const adapter = new DifyWorkflowMemoryAdapter({
gateway: {
baseUrl: "http://gateway.local",
fetchImpl: async (url, init) => {
calls.push({
url: String(url),
body: init?.body ? JSON.parse(String(init.body)) : undefined,
});
return new Response(JSON.stringify({ l0_recorded: 1, scheduler_notified: true }), {
headers: { "Content-Type": "application/json" },
});
},
},
});

await adapter.capture({
query: "remember",
answer: "done",
conversation_id: "conv",
user: "user",
});

expect(calls).toEqual([
{
url: "http://gateway.local/capture",
body: {
user_content: "remember",
assistant_content: "done",
session_key: "dify:user:conv",
user_id: "user",
},
},
]);
});

it("rejects incomplete capture payloads", async () => {
const adapter = new DifyWorkflowMemoryAdapter({ client: makeClient([]) });

await expect(adapter.capture({ query: "only user text" })).rejects.toThrow(
"Dify capture requires `assistant_content` or `answer`",
);
});

it("requires either an injected Dify port or GatewayMemoryClient options", () => {
expect(() => new DifyWorkflowMemoryAdapter({})).toThrow(
"DifyWorkflowMemoryAdapter requires either `client` or `gateway` options",
);
});
});
Loading