From 7ee778775c38348f79c31ea010bc49bc440731d7 Mon Sep 17 00:00:00 2001 From: SDK Lead Date: Thu, 2 Jul 2026 20:07:03 +0000 Subject: [PATCH] docs: top-notch README with badges, streaming, combined pattern + 4 runnable examples (DAK-7289) --- README.md | 208 ++++++++++++++++++++++++++++++-------- examples/01-middleware.ts | 44 ++++++++ examples/02-tools.ts | 44 ++++++++ examples/03-combined.ts | 54 ++++++++++ examples/04-streaming.ts | 43 ++++++++ examples/README.md | 45 +++++++++ examples/package.json | 24 +++++ 7 files changed, 419 insertions(+), 43 deletions(-) create mode 100644 examples/01-middleware.ts create mode 100644 examples/02-tools.ts create mode 100644 examples/03-combined.ts create mode 100644 examples/04-streaming.ts create mode 100644 examples/README.md create mode 100644 examples/package.json diff --git a/README.md b/README.md index 1d94d80..fa24300 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,44 @@ # @dakera-ai/ai-sdk -Vercel AI SDK integration for [Dakera](https://dakera.ai) — a self-hosted memory -server that adds **persistent, decay-weighted vector recall** across sessions. -Memories are importance-scored and decay over time, so stale context stops -competing with fresh, relevant facts. +[![npm version](https://img.shields.io/npm/v/@dakera-ai/ai-sdk.svg)](https://www.npmjs.com/package/@dakera-ai/ai-sdk) +[![CI](https://github.com/dakera-ai/dakera-ai-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/dakera-ai/dakera-ai-sdk/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/TypeScript-5%2B-blue.svg)](https://www.typescriptlang.org/) -It plugs into the AI SDK's two standard extension points — **language model -middleware** and **tools** — so you can add cross-session memory to any AI SDK -app without changing your model or provider code. +Vercel AI SDK integration for [Dakera](https://dakera.ai) — a self-hosted memory server that adds **persistent, decay-weighted vector recall** across agent sessions. -## Install +Memories are importance-scored and decay over time, so stale context stops competing with fresh, relevant facts. The integration plugs into the AI SDK's two standard extension points — **language model middleware** and **tools** — so you can add cross-session memory to any AI SDK app without changing your model or provider code. + +## Quick start ```bash npm install @dakera-ai/ai-sdk ai @dakera-ai/dakera zod ``` -Run a Dakera server first (self-hosted, no external dependencies): -see [`dakera-ai/dakera-deploy`](https://github.com/dakera-ai/dakera-deploy) for -the Docker Compose setup (server + MinIO). Point the integration at it with -`DAKERA_URL` (default `http://localhost:3000`) and `DAKERA_API_KEY`. +Run a Dakera server first (self-hosted, zero external dependencies): + +```bash +# Docker Compose — server + MinIO object storage +curl -sSL https://raw.githubusercontent.com/dakera-ai/dakera-deploy/main/docker-compose.yml | \ + docker compose -f - up -d +``` + +Then set two environment variables: + +```bash +export DAKERA_URL=http://localhost:3000 # default — can omit +export DAKERA_API_KEY=dk-your-key-here +``` ## Pattern 1 — Memory middleware (transparent) -`createDakeraMemoryMiddleware` wraps a model so that relevant memories are -recalled and injected as system context before every call, and the new exchange -is stored afterwards. No changes to your generation code. +`createDakeraMemoryMiddleware` wraps any language model. On every call it: + +1. Recalls the most relevant memories for the current prompt +2. Injects them as a system message before generation +3. Stores the new exchange back into Dakera + +Your generation code stays unchanged. ```typescript import { generateText, wrapLanguageModel } from "ai"; @@ -34,41 +48,66 @@ import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk"; const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: createDakeraMemoryMiddleware({ - apiUrl: "http://localhost:3000", - apiKey: process.env.DAKERA_API_KEY, - agentId: "user-1234", + agentId: "user-1234", // scopes memories to this agent / user + recallK: 5, // inject up to 5 relevant memories per call }), }); -// First session -await generateText({ model, prompt: "I'm building a Rust vector database." }); +// Session 1 +await generateText({ + model, + prompt: "I'm building a Rust vector database called Velox.", +}); -// A later session — the model recalls the earlier context automatically -const { text } = await generateText({ model, prompt: "What am I working on?" }); -// → "You're building a Rust vector database." +// Session 2 — days later, different process +const { text } = await generateText({ + model, + prompt: "What am I working on?", +}); +// → "You're building Velox, a Rust vector database." ``` -Under the hood the middleware uses `transformParams` to prepend recalled -memories as a system message, and `wrapGenerate` to persist the exchange. -Storage is best-effort: a memory-server error never breaks generation. +### Streaming + +The middleware works with `streamText` unchanged: + +```typescript +import { streamText, wrapLanguageModel } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk"; + +const model = wrapLanguageModel({ + model: openai("gpt-4o"), + middleware: createDakeraMemoryMiddleware({ agentId: "user-1234" }), +}); + +const { textStream } = streamText({ + model, + prompt: "Summarise what I've told you about my project.", +}); + +for await (const chunk of textStream) { + process.stdout.write(chunk); +} +``` -### Options +### Middleware options -| Option | Default | Description | -| --- | --- | --- | -| `agentId` | — | Identifier that scopes stored/recalled memories (required) | -| `apiUrl` | `$DAKERA_URL` / `http://localhost:3000` | Dakera server URL | -| `apiKey` | `$DAKERA_API_KEY` | Dakera API key (`dk-...`) | -| `client` | — | A pre-built `DakeraClient` (overrides `apiUrl`/`apiKey`) | -| `recallK` | `5` | Memories to recall per call | -| `minImportance` | `0` | Minimum importance to recall | -| `importance` | `0.7` | Importance assigned to stored memories | -| `store` | `true` | Store the exchange after generation | +| Option | Type | Default | Description | +|---|---|---|---| +| `agentId` | `string` | — | Scopes stored/recalled memories **(required)** | +| `apiUrl` | `string` | `$DAKERA_URL` / `http://localhost:3000` | Dakera server URL | +| `apiKey` | `string` | `$DAKERA_API_KEY` | API key (`dk-...`) | +| `client` | `DakeraClient` | — | Pre-built client (overrides `apiUrl`/`apiKey`) | +| `recallK` | `number` | `5` | Memories to recall and inject per call | +| `minImportance` | `number` | `0` | Minimum importance score to recall (0 – 1) | +| `importance` | `number` | `0.7` | Importance assigned to stored memories (0 – 1) | +| `store` | `boolean` | `true` | Persist the exchange after generation | +| `header` | `string` | `"Relevant memories…"` | System-message header prepended to the memory block | ## Pattern 2 — Memory tools (model-driven) -`createDakeraTools` gives the model explicit `recallMemory` and `storeMemory` -tools, so it decides when to look something up or remember it. +`createDakeraTools` returns `recallMemory` and `storeMemory` tools. The model decides when to look something up or persist a new fact — useful for agentic workflows where explicit memory control improves quality. ```typescript import { generateText } from "ai"; @@ -81,13 +120,96 @@ const { text } = await generateText({ model: openai("gpt-4o"), tools, maxSteps: 4, - prompt: "Remember that I prefer metric units, then convert 5 miles to km.", + prompt: "Remember that I prefer metric units. Then convert 5 miles to km.", }); +// Model calls storeMemory("User prefers metric units", importance 0.8), +// then answers: "5 miles = 8.047 km" ``` -The two patterns compose — use the middleware for automatic continuity and the -tools when you want the model to manage memory deliberately. +### Tool options + +| Option | Type | Default | Description | +|---|---|---|---| +| `agentId` | `string` | — | Scopes stored/recalled memories **(required)** | +| `apiUrl` | `string` | `$DAKERA_URL` / `http://localhost:3000` | Dakera server URL | +| `apiKey` | `string` | `$DAKERA_API_KEY` | API key (`dk-...`) | +| `client` | `DakeraClient` | — | Pre-built client (overrides `apiUrl`/`apiKey`) | +| `recallK` | `number` | `5` | Default `topK` for the recall tool | +| `importance` | `number` | `0.7` | Default importance for the store tool | + +## Pattern 3 — Combined (transparent + explicit) + +Use the middleware for automatic continuity and the tools when you want the model to manage memory deliberately in the same call: + +```typescript +import { generateText, wrapLanguageModel } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { createDakeraMemoryMiddleware, createDakeraTools } from "@dakera-ai/ai-sdk"; +import { DakeraClient } from "@dakera-ai/dakera"; + +// Share one client instance across middleware and tools +const client = new DakeraClient({ baseUrl: process.env.DAKERA_URL! }); +const agentId = "user-1234"; + +const model = wrapLanguageModel({ + model: openai("gpt-4o"), + // Recall automatically, but let tools handle persistence + middleware: createDakeraMemoryMiddleware({ client, agentId, store: false }), +}); + +const tools = createDakeraTools({ client, agentId }); + +const { text } = await generateText({ + model, + tools, + maxSteps: 6, + system: + "You are a helpful assistant. Use storeMemory for facts the user will want remembered across sessions.", + prompt: "My deadline for the Velox project is Friday the 11th.", +}); +``` + +## Using a pre-built client + +Share connection config across multiple agents or avoid repeating credentials: + +```typescript +import { DakeraClient } from "@dakera-ai/dakera"; +import { createDakeraMemoryMiddleware, createDakeraTools } from "@dakera-ai/ai-sdk"; + +const client = new DakeraClient({ + baseUrl: "http://my-dakera.internal:3000", + apiKey: process.env.DAKERA_API_KEY, +}); + +// All agents share the same connection +const supportMiddleware = createDakeraMemoryMiddleware({ client, agentId: "support-bot" }); +const salesTools = createDakeraTools({ client, agentId: "sales-bot" }); +``` + +## Running the examples + +```bash +git clone https://github.com/dakera-ai/dakera-ai-sdk +cd dakera-ai-sdk/examples +npm install +DAKERA_URL=http://localhost:3000 DAKERA_API_KEY=dk-dev npx tsx 01-middleware.ts +``` + +See [`examples/README.md`](examples/README.md) for all examples and prerequisites. + +## Troubleshooting + +**`Cannot find name 'process'`** — add `"types": ["node"]` to your `tsconfig.json` `compilerOptions`. + +**Memory recall is empty on first call** — expected. The first call has no history. After the first exchange the store step runs, and subsequent calls will recall. + +**Storage errors break my app** — they won't. The middleware catches all storage errors so a Dakera outage never interrupts generation. + +**Getting 401 from the server** — verify `DAKERA_API_KEY` matches the server's env. Keys look like `dk-...` when generated via the Dakera admin API, but any shared string works. + +**Using with Next.js / edge runtime** — set `DAKERA_URL` and `DAKERA_API_KEY` as Next.js environment variables and import from `@dakera-ai/ai-sdk` in server components or API routes. ## License -MIT © Dakera +MIT © [Dakera](https://dakera.ai) diff --git a/examples/01-middleware.ts b/examples/01-middleware.ts new file mode 100644 index 0000000..97c0283 --- /dev/null +++ b/examples/01-middleware.ts @@ -0,0 +1,44 @@ +/** + * Example 1 — Memory middleware (transparent) + * + * The middleware wraps a language model and automatically recalls relevant + * memories before each call, then stores the new exchange afterwards. + * Your generation code needs no changes. + * + * Run: + * OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 01-middleware.ts + */ + +import { generateText, wrapLanguageModel } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk"; + +const model = wrapLanguageModel({ + model: openai("gpt-4o-mini"), + middleware: createDakeraMemoryMiddleware({ + // Scope memories to a user or agent ID + agentId: "example-user-1", + // Recall up to 5 relevant memories per call + recallK: 5, + // Assign importance 0.8 to stored exchanges + importance: 0.8, + }), +}); + +console.log("=== Session 1: storing a fact ==="); +const { text: reply1 } = await generateText({ + model, + prompt: + "My name is Alex and I'm building a distributed key-value store called Kestrel in Go.", +}); +console.log("Assistant:", reply1); + +console.log("\n=== Session 2: recalling across a simulated restart ==="); +// In a real app this would be a separate process or later request. +// The memory persists in Dakera between sessions. +const { text: reply2 } = await generateText({ + model, + prompt: "What do you know about my current project?", +}); +console.log("Assistant:", reply2); +// → Should reference Kestrel and Alex without any explicit context in the prompt diff --git a/examples/02-tools.ts b/examples/02-tools.ts new file mode 100644 index 0000000..a0301ee --- /dev/null +++ b/examples/02-tools.ts @@ -0,0 +1,44 @@ +/** + * Example 2 — Memory tools (model-driven) + * + * `createDakeraTools` gives the model explicit `recallMemory` and `storeMemory` + * tools. The model decides when to look something up or when to persist a fact. + * Useful for agents that need deliberate memory management. + * + * Run: + * OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 02-tools.ts + */ + +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { createDakeraTools } from "@dakera-ai/ai-sdk"; + +const tools = createDakeraTools({ + agentId: "example-user-2", + recallK: 5, + importance: 0.8, +}); + +console.log("=== Step 1: storing a preference ==="); +const { text: reply1, steps: steps1 } = await generateText({ + model: openai("gpt-4o-mini"), + tools, + maxSteps: 3, + prompt: + "Please remember that I always want code examples in TypeScript, not JavaScript.", +}); +console.log("Assistant:", reply1); +console.log("Tool calls:", steps1.flatMap((s) => s.toolCalls).map((c) => c.toolName)); + +console.log("\n=== Step 2: recalling the preference ==="); +const { text: reply2, steps: steps2 } = await generateText({ + model: openai("gpt-4o-mini"), + tools, + maxSteps: 3, + system: + "You are a helpful coding assistant. Always check memory for user preferences before answering.", + prompt: "Show me a quick example of how to read a file.", +}); +console.log("Assistant:", reply2); +console.log("Tool calls:", steps2.flatMap((s) => s.toolCalls).map((c) => c.toolName)); +// → Model should call recallMemory, find the TypeScript preference, and answer in TS diff --git a/examples/03-combined.ts b/examples/03-combined.ts new file mode 100644 index 0000000..61fcbf4 --- /dev/null +++ b/examples/03-combined.ts @@ -0,0 +1,54 @@ +/** + * Example 3 — Combined middleware + tools + * + * Use the middleware for automatic recall (so the model always has relevant + * context) and the tools when you want the model to persist facts explicitly. + * A single DakeraClient is shared to avoid duplicate connections. + * + * Run: + * OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 03-combined.ts + */ + +import { generateText, wrapLanguageModel } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { DakeraClient } from "@dakera-ai/dakera"; +import { createDakeraMemoryMiddleware, createDakeraTools } from "@dakera-ai/ai-sdk"; + +// Share one DakeraClient between middleware and tools +const client = new DakeraClient({ + baseUrl: process.env["DAKERA_URL"] ?? "http://localhost:3000", + apiKey: process.env["DAKERA_API_KEY"], +}); +const agentId = "example-user-3"; + +// Middleware handles recall automatically; store: false lets the model +// decide what's worth persisting via the storeMemory tool +const model = wrapLanguageModel({ + model: openai("gpt-4o-mini"), + middleware: createDakeraMemoryMiddleware({ client, agentId, store: false }), +}); + +const tools = createDakeraTools({ client, agentId }); + +console.log("=== Turn 1: model decides what to remember ==="); +const { text: reply1, steps: steps1 } = await generateText({ + model, + tools, + maxSteps: 4, + system: + "You are a helpful assistant. Use storeMemory only for facts the user will want in future sessions (not transient conversation details).", + prompt: + "I'm Jordan. My preferred stack is Next.js + Postgres and my timezone is UTC+2.", +}); +console.log("Assistant:", reply1); +console.log("Stored:", steps1.flatMap((s) => s.toolCalls).filter((c) => c.toolName === "storeMemory").length, "memories"); + +console.log("\n=== Turn 2: middleware injects recalled memories automatically ==="); +const { text: reply2 } = await generateText({ + model, + tools, + maxSteps: 2, + prompt: "What stack should I use for my next project?", +}); +console.log("Assistant:", reply2); +// → Should reference Next.js + Postgres from memory without any explicit context diff --git a/examples/04-streaming.ts b/examples/04-streaming.ts new file mode 100644 index 0000000..2312b22 --- /dev/null +++ b/examples/04-streaming.ts @@ -0,0 +1,43 @@ +/** + * Example 4 — Streaming with memory middleware + * + * `streamText` works with `wrapLanguageModel` unchanged. Memories are recalled + * and injected before streaming begins. Note: the middleware's `wrapGenerate` + * hook only fires for non-streaming calls; for streaming, use the tools pattern + * (02-tools.ts) or a separate store step if you need persistence. + * + * Run: + * OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 04-streaming.ts + */ + +import { streamText, wrapLanguageModel } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk"; + +const model = wrapLanguageModel({ + model: openai("gpt-4o-mini"), + middleware: createDakeraMemoryMiddleware({ + agentId: "example-user-4", + store: false, // disable auto-store for streaming; handle explicitly if needed + }), +}); + +console.log("Streaming response with recalled context...\n"); + +const { textStream, finishReason } = streamText({ + model, + prompt: "Tell me about my current project in a short paragraph.", +}); + +let fullText = ""; +for await (const chunk of textStream) { + process.stdout.write(chunk); + fullText += chunk; +} + +console.log("\n\nFinish reason:", await finishReason); + +// Optionally persist after streaming completes +// import { DakeraClient } from "@dakera-ai/dakera"; +// const client = new DakeraClient({ ... }); +// await client.storeMemory("example-user-4", { content: `Assistant: ${fullText}`, importance: 0.7 }); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9a2b9fc --- /dev/null +++ b/examples/README.md @@ -0,0 +1,45 @@ +# Examples + +These are runnable examples for `@dakera-ai/ai-sdk`. Each file is standalone +and can be executed with `tsx` or compiled with `tsc`. + +## Prerequisites + +1. A running Dakera server — see [dakera-deploy](https://github.com/dakera-ai/dakera-deploy): + + ```bash + curl -sSL https://raw.githubusercontent.com/dakera-ai/dakera-deploy/main/docker-compose.yml | \ + docker compose -f - up -d + ``` + +2. An OpenAI API key (or swap for any AI SDK-compatible provider). + +3. Install dependencies: + + ```bash + npm install + ``` + +## Running + +```bash +# Set env vars +export OPENAI_API_KEY=sk-... +export DAKERA_URL=http://localhost:3000 +export DAKERA_API_KEY=dk-dev # or omit if your server runs without auth + +# Run any example +npx tsx 01-middleware.ts +npx tsx 02-tools.ts +npx tsx 03-combined.ts +npx tsx 04-streaming.ts +``` + +## Examples + +| File | Pattern | Description | +|---|---|---| +| `01-middleware.ts` | Middleware | Transparent recall + store across two "sessions" | +| `02-tools.ts` | Tools | Model-driven memory with `recallMemory` / `storeMemory` | +| `03-combined.ts` | Combined | Middleware recall + tool-driven persistence | +| `04-streaming.ts` | Streaming | `streamText` with memory middleware | diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..88d0fbb --- /dev/null +++ b/examples/package.json @@ -0,0 +1,24 @@ +{ + "name": "dakera-ai-sdk-examples", + "private": true, + "description": "Runnable examples for @dakera-ai/ai-sdk", + "type": "module", + "scripts": { + "middleware": "tsx 01-middleware.ts", + "tools": "tsx 02-tools.ts", + "combined": "tsx 03-combined.ts", + "streaming": "tsx 04-streaming.ts" + }, + "dependencies": { + "@ai-sdk/openai": "^1.0.0", + "@dakera-ai/ai-sdk": "latest", + "@dakera-ai/dakera": "latest", + "ai": "^7.0.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +}