@rudderjs/ai 0.0.1
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 +461 -0
- package/boost/guidelines.md +150 -0
- package/dist/agent.d.ts +74 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +1070 -0
- package/dist/agent.js.map +1 -0
- package/dist/attachment.d.ts +35 -0
- package/dist/attachment.d.ts.map +1 -0
- package/dist/attachment.js +121 -0
- package/dist/attachment.js.map +1 -0
- package/dist/audio.d.ts +33 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +76 -0
- package/dist/audio.js.map +1 -0
- package/dist/cached-embedding.d.ts +14 -0
- package/dist/cached-embedding.d.ts.map +1 -0
- package/dist/cached-embedding.js +44 -0
- package/dist/cached-embedding.js.map +1 -0
- package/dist/conversation.d.ts +16 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +53 -0
- package/dist/conversation.js.map +1 -0
- package/dist/facade.d.ts +53 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +100 -0
- package/dist/facade.js.map +1 -0
- package/dist/fake.d.ts +55 -0
- package/dist/fake.d.ts.map +1 -0
- package/dist/fake.js +172 -0
- package/dist/fake.js.map +1 -0
- package/dist/image.d.ts +27 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +90 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +18 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +72 -0
- package/dist/middleware.js.map +1 -0
- package/dist/output.d.ts +22 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +55 -0
- package/dist/output.js.map +1 -0
- package/dist/provider-tools.d.ts +60 -0
- package/dist/provider-tools.d.ts.map +1 -0
- package/dist/provider-tools.js +133 -0
- package/dist/provider-tools.js.map +1 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +94 -0
- package/dist/provider.js.map +1 -0
- package/dist/providers/anthropic.d.ts +12 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +221 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/azure.d.ts +13 -0
- package/dist/providers/azure.d.ts.map +1 -0
- package/dist/providers/azure.js +15 -0
- package/dist/providers/azure.js.map +1 -0
- package/dist/providers/deepseek.d.ts +12 -0
- package/dist/providers/deepseek.d.ts.map +1 -0
- package/dist/providers/deepseek.js +15 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/google.d.ts +13 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +293 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/groq.d.ts +12 -0
- package/dist/providers/groq.d.ts.map +1 -0
- package/dist/providers/groq.js +15 -0
- package/dist/providers/groq.js.map +1 -0
- package/dist/providers/mistral.d.ts +13 -0
- package/dist/providers/mistral.d.ts.map +1 -0
- package/dist/providers/mistral.js +46 -0
- package/dist/providers/mistral.js.map +1 -0
- package/dist/providers/ollama.d.ts +11 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +15 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +26 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +374 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/xai.d.ts +12 -0
- package/dist/providers/xai.d.ts.map +1 -0
- package/dist/providers/xai.js +15 -0
- package/dist/providers/xai.js.map +1 -0
- package/dist/queue-job.d.ts +35 -0
- package/dist/queue-job.d.ts.map +1 -0
- package/dist/queue-job.js +82 -0
- package/dist/queue-job.js.map +1 -0
- package/dist/registry.d.ts +25 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +54 -0
- package/dist/registry.js.map +1 -0
- package/dist/tool.d.ts +157 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +134 -0
- package/dist/tool.js.map +1 -0
- package/dist/transcription.d.ts +28 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +63 -0
- package/dist/transcription.js.map +1 -0
- package/dist/types.d.ts +439 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vercel-protocol.d.ts +18 -0
- package/dist/vercel-protocol.d.ts.map +1 -0
- package/dist/vercel-protocol.js +75 -0
- package/dist/vercel-protocol.js.map +1 -0
- package/dist/zod-to-json-schema.d.ts +8 -0
- package/dist/zod-to-json-schema.d.ts.map +1 -0
- package/dist/zod-to-json-schema.js +86 -0
- package/dist/zod-to-json-schema.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suleiman Shahbari
|
|
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,461 @@
|
|
|
1
|
+
# @rudderjs/ai
|
|
2
|
+
|
|
3
|
+
AI engine for RudderJS — providers, agents, tools, streaming, middleware, structured output, conversation memory, and testing fakes.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @rudderjs/ai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Install the provider SDK(s) you need:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @anthropic-ai/sdk # Anthropic (Claude)
|
|
15
|
+
pnpm add openai # OpenAI (GPT)
|
|
16
|
+
pnpm add @google/genai # Google (Gemini)
|
|
17
|
+
# Ollama — no extra package needed (OpenAI-compatible)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// config/ai.ts
|
|
24
|
+
export default {
|
|
25
|
+
default: 'anthropic/claude-sonnet-4-5',
|
|
26
|
+
providers: {
|
|
27
|
+
anthropic: { driver: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY! },
|
|
28
|
+
openai: { driver: 'openai', apiKey: process.env.OPENAI_API_KEY! },
|
|
29
|
+
google: { driver: 'google', apiKey: process.env.GOOGLE_API_KEY! },
|
|
30
|
+
ollama: { driver: 'ollama', baseUrl: 'http://localhost:11434' },
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// bootstrap/providers.ts
|
|
35
|
+
import { ai } from '@rudderjs/ai'
|
|
36
|
+
export default [ai(configs.ai), ...]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Agent Class
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Agent, toolDefinition, stepCountIs } from '@rudderjs/ai'
|
|
45
|
+
import type { HasTools } from '@rudderjs/ai'
|
|
46
|
+
import { z } from 'zod'
|
|
47
|
+
|
|
48
|
+
const searchTool = toolDefinition({
|
|
49
|
+
name: 'search_users',
|
|
50
|
+
description: 'Search users by name',
|
|
51
|
+
inputSchema: z.object({ query: z.string() }),
|
|
52
|
+
}).server(async ({ query }) => {
|
|
53
|
+
return db.users.findMany({ where: { name: { contains: query } } })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
class SearchAgent extends Agent implements HasTools {
|
|
57
|
+
instructions() { return 'You help find users in the system.' }
|
|
58
|
+
model() { return 'anthropic/claude-sonnet-4-5' }
|
|
59
|
+
tools() { return [searchTool] }
|
|
60
|
+
stopWhen() { return stepCountIs(5) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await new SearchAgent().prompt('Find all admins')
|
|
64
|
+
console.log(response.text)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Anonymous Agent
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { agent, AI } from '@rudderjs/ai'
|
|
71
|
+
|
|
72
|
+
const response = await agent('You summarize text.').prompt('Summarize this...')
|
|
73
|
+
|
|
74
|
+
// Or via facade
|
|
75
|
+
const response = await AI.prompt('Hello world')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Tools (Server + Client)
|
|
79
|
+
|
|
80
|
+
A `Tool` is just `{ definition, execute? }`. The presence or absence of
|
|
81
|
+
`execute` is the only discriminator: with it, the tool runs server-side;
|
|
82
|
+
without it, it's a client tool that the browser executes via
|
|
83
|
+
`@rudderjs/panels`'s `clientTools` registry.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { toolDefinition, dynamicTool } from '@rudderjs/ai'
|
|
87
|
+
import { z } from 'zod'
|
|
88
|
+
|
|
89
|
+
// Server tool — executes on backend
|
|
90
|
+
const weatherTool = toolDefinition({
|
|
91
|
+
name: 'get_weather',
|
|
92
|
+
description: 'Get weather for a location',
|
|
93
|
+
inputSchema: z.object({ location: z.string() }),
|
|
94
|
+
needsApproval: true, // pauses the agent loop until the user approves
|
|
95
|
+
lazy: true, // not sent to LLM upfront
|
|
96
|
+
}).server(async ({ location }) => ({ temp: 72, unit: 'F' }))
|
|
97
|
+
|
|
98
|
+
// Client tool — no `.server()`, so the browser executes it
|
|
99
|
+
const readFormState = toolDefinition({
|
|
100
|
+
name: 'read_form_state',
|
|
101
|
+
description: 'Read the user\'s current local form values',
|
|
102
|
+
inputSchema: z.object({ fields: z.array(z.string()).optional() }),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Dynamic tool — schemas built at runtime from user data
|
|
106
|
+
const customTool = dynamicTool({
|
|
107
|
+
name: 'custom_op',
|
|
108
|
+
description: 'Built at runtime',
|
|
109
|
+
inputSchema: z.object({ q: z.string() }),
|
|
110
|
+
}).server(async (input) => JSON.stringify(input))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Client tool round-trip and approval gates
|
|
114
|
+
|
|
115
|
+
When the model calls a client tool (no `execute`) or a tool with
|
|
116
|
+
`needsApproval: true`, the agent loop **stops** instead of failing — and
|
|
117
|
+
exposes the pending state on `AgentResponse`:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const result = await agent({ tools: [readFormState, weatherTool] })
|
|
121
|
+
.prompt('what is in the form?', {
|
|
122
|
+
toolCallStreamingMode: 'stop-on-client-tool',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if (result.finishReason === 'client_tool_calls') {
|
|
126
|
+
// result.pendingClientToolCalls — execute these in the browser, then
|
|
127
|
+
// re-POST with `messages: [...history, assistantMsg, ...toolResultMsgs]`
|
|
128
|
+
}
|
|
129
|
+
if (result.finishReason === 'tool_approval_required') {
|
|
130
|
+
// result.pendingApprovalToolCall — show approval UI, then re-POST with
|
|
131
|
+
// `approvedToolCallIds: [id]` or `rejectedToolCallIds: [id]`
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The **continuation** uses `options.messages` instead of `history` + `input`:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
await agent({ tools: [...] }).prompt('', {
|
|
139
|
+
messages: [...priorConversation, assistantWithToolCalls, toolResult],
|
|
140
|
+
approvedToolCallIds: ['tc_id'], // or rejectedToolCallIds
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When continuing after an approval round-trip, the loop transparently
|
|
145
|
+
**resumes the pending tool call server-side** before re-entering the model
|
|
146
|
+
loop — the resulting `tool` messages are exposed via
|
|
147
|
+
`result.resumedToolMessages` so callers can persist them. This guarantees
|
|
148
|
+
the conversation store never holds an unfulfilled `tool_use` block.
|
|
149
|
+
|
|
150
|
+
`@rudderjs/panels` does all the wiring (validating message prefixes against
|
|
151
|
+
the persisted store, executing client tools via the `clientTools` registry,
|
|
152
|
+
showing the inline approval card) — see its README for the end-to-end flow.
|
|
153
|
+
|
|
154
|
+
### Tool execution context
|
|
155
|
+
|
|
156
|
+
Server-tool executes can optionally accept a second `ctx: ToolCallContext`
|
|
157
|
+
argument carrying loop-level metadata — currently `{ toolCallId }`. The
|
|
158
|
+
parameter is optional, so existing one-arg tools keep working unchanged.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { toolDefinition, type ToolCallContext } from '@rudderjs/ai'
|
|
162
|
+
|
|
163
|
+
const myTool = toolDefinition({
|
|
164
|
+
name: 'my_tool',
|
|
165
|
+
description: '...',
|
|
166
|
+
inputSchema: z.object({ q: z.string() }),
|
|
167
|
+
}).server(async (input, ctx?: ToolCallContext) => {
|
|
168
|
+
console.log('this call id:', ctx?.toolCallId)
|
|
169
|
+
return { ok: true }
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The primary consumer is `@rudderjs/panels`'s `runAgentTool`, which uses
|
|
174
|
+
`ctx.toolCallId` to correlate sub-agent suspensions with the parent's
|
|
175
|
+
`run_agent` call (see "Pausing the loop from a server tool" below).
|
|
176
|
+
|
|
177
|
+
### Pausing the loop from a server tool
|
|
178
|
+
|
|
179
|
+
A server tool's async-generator execute can `yield` a `pauseForClientTools`
|
|
180
|
+
control chunk to halt the enclosing agent loop and surface a set of
|
|
181
|
+
**client** tool calls to the caller — as if the model itself had emitted
|
|
182
|
+
them. The yielding tool's own call stays orphaned in the message history
|
|
183
|
+
until the caller resolves it on continuation.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { toolDefinition, pauseForClientTools } from '@rudderjs/ai'
|
|
187
|
+
|
|
188
|
+
const runNestedTool = toolDefinition({
|
|
189
|
+
name: 'run_nested',
|
|
190
|
+
description: 'Runs a nested workflow that may need browser interaction',
|
|
191
|
+
inputSchema: z.object({ task: z.string() }),
|
|
192
|
+
}).server(async function* (input, ctx) {
|
|
193
|
+
// ...do some server-side work, maybe yield progress chunks...
|
|
194
|
+
|
|
195
|
+
if (needsBrowserAction) {
|
|
196
|
+
// Persist whatever state you need to resume later, keyed by an
|
|
197
|
+
// opaque `resumeHandle` your continuation logic understands.
|
|
198
|
+
const handle = await persistMyResumeState({
|
|
199
|
+
parentToolCallId: ctx?.toolCallId,
|
|
200
|
+
task: input.task,
|
|
201
|
+
// ...
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Yielding the control chunk halts iteration. The agent loop
|
|
205
|
+
// appends the toolCalls to its own pendingClientToolCalls,
|
|
206
|
+
// sets stop-for-client-tools, and emits 'pending-client-tools'
|
|
207
|
+
// upward. The browser executes the calls and POSTs back, your
|
|
208
|
+
// continuation handler picks up `handle` and resumes.
|
|
209
|
+
yield pauseForClientTools(
|
|
210
|
+
[{ id: 'call_xyz', name: 'update_form_state', arguments: { ... } }],
|
|
211
|
+
handle,
|
|
212
|
+
)
|
|
213
|
+
// Unreachable — the loop halts iteration after the pause chunk.
|
|
214
|
+
return null as never
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { result: 'done' }
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Why a yield instead of a throw:**
|
|
222
|
+
|
|
223
|
+
- Symmetry with the existing `tool-update` yield protocol (no parallel
|
|
224
|
+
catch-based control path)
|
|
225
|
+
- Middleware can observe pauses through `runOnChunk`; throws would route
|
|
226
|
+
through `onError` and muddle telemetry
|
|
227
|
+
- Exceptions signal "something went wrong"; this is not an error
|
|
228
|
+
- Any server tool can yield this — not just nested agent runners. E.g., a
|
|
229
|
+
tool that wants the browser's geolocation, clipboard, or a user file
|
|
230
|
+
upload.
|
|
231
|
+
|
|
232
|
+
**Recognizing the chunk:** the loop uses `isPauseForClientToolsChunk(value)`
|
|
233
|
+
internally. Tool authors should construct chunks via the
|
|
234
|
+
`pauseForClientTools()` factory rather than by hand so future shape
|
|
235
|
+
changes stay source-compatible.
|
|
236
|
+
|
|
237
|
+
**Resuming:** that's caller territory — `@rudderjs/ai` knows nothing about
|
|
238
|
+
the resume protocol. The canonical implementation is in
|
|
239
|
+
`@rudderjs/panels`'s `subAgentResume.ts`, which uses a runStore to persist
|
|
240
|
+
sub-agent state and re-invokes the tool's enclosing agent on the
|
|
241
|
+
continuation request.
|
|
242
|
+
|
|
243
|
+
### Structured Output
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { agent, Output } from '@rudderjs/ai'
|
|
247
|
+
import { z } from 'zod'
|
|
248
|
+
|
|
249
|
+
const output = Output.object({
|
|
250
|
+
schema: z.object({
|
|
251
|
+
people: z.array(z.string()),
|
|
252
|
+
companies: z.array(z.string()),
|
|
253
|
+
}),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Use with agent (append output instructions to system prompt)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Failover
|
|
260
|
+
|
|
261
|
+
Try multiple providers in order — if the primary fails, fall through to the next:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
class ResilientAgent extends Agent {
|
|
265
|
+
instructions() { return 'You are helpful.' }
|
|
266
|
+
model() { return 'anthropic/claude-sonnet-4-5' }
|
|
267
|
+
failover() { return ['openai/gpt-4o', 'google/gemini-2.5-pro'] }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If Anthropic is down, tries OpenAI, then Google
|
|
271
|
+
const response = await new ResilientAgent().prompt('Hello')
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Works with both `prompt()` and `stream()`.
|
|
275
|
+
|
|
276
|
+
### Image Generation
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { AI } from '@rudderjs/ai'
|
|
280
|
+
|
|
281
|
+
const result = await AI.image('A mountain at sunset')
|
|
282
|
+
.model('openai/dall-e-3')
|
|
283
|
+
.size('landscape')
|
|
284
|
+
.quality('hd')
|
|
285
|
+
.generate()
|
|
286
|
+
|
|
287
|
+
// result.images[0].base64 or result.images[0].url
|
|
288
|
+
await AI.image('Logo design').model('openai/dall-e-3').store('images/logo.png')
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Text-to-Speech
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import { AI } from '@rudderjs/ai'
|
|
295
|
+
|
|
296
|
+
const result = await AI.audio('Hello world')
|
|
297
|
+
.model('openai/tts-1')
|
|
298
|
+
.voice('nova')
|
|
299
|
+
.format('mp3')
|
|
300
|
+
.generate()
|
|
301
|
+
|
|
302
|
+
// result.audio → Buffer
|
|
303
|
+
await AI.audio('Welcome').model('openai/tts-1').store('audio/welcome.mp3')
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Speech-to-Text
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { AI } from '@rudderjs/ai'
|
|
310
|
+
|
|
311
|
+
const result = await AI.transcribe('./meeting.mp3')
|
|
312
|
+
.model('openai/whisper-1')
|
|
313
|
+
.language('en')
|
|
314
|
+
.generate()
|
|
315
|
+
|
|
316
|
+
// result.text → transcribed text
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Provider Tools (WebSearch, WebFetch)
|
|
320
|
+
|
|
321
|
+
Built-in tools that leverage provider capabilities:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { AI, WebSearch, WebFetch } from '@rudderjs/ai'
|
|
325
|
+
|
|
326
|
+
const agent = AI.agent({
|
|
327
|
+
instructions: 'Research assistant',
|
|
328
|
+
tools: [
|
|
329
|
+
WebSearch.make().domains(['docs.rudderjs.dev']).toTool(),
|
|
330
|
+
WebFetch.make().maxLength(5000).toTool(),
|
|
331
|
+
],
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Embeddings
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { AI } from '@rudderjs/ai'
|
|
339
|
+
|
|
340
|
+
// Single text
|
|
341
|
+
const result = await AI.embed('Hello world')
|
|
342
|
+
|
|
343
|
+
// Batch (auto-chunks arrays > 100 items)
|
|
344
|
+
const result = await AI.embed(['text one', 'text two'])
|
|
345
|
+
|
|
346
|
+
// With caching
|
|
347
|
+
const result = await AI.embed('text', { cache: true })
|
|
348
|
+
|
|
349
|
+
// Specific model
|
|
350
|
+
const result = await AI.embed('text', { model: 'openai/text-embedding-3-small' })
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Vercel AI Protocol
|
|
354
|
+
|
|
355
|
+
Stream to frontend frameworks (Next.js, Nuxt, SvelteKit):
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { toVercelResponse } from '@rudderjs/ai'
|
|
359
|
+
|
|
360
|
+
// In a route handler
|
|
361
|
+
const { stream } = agent('You are helpful.').stream(input)
|
|
362
|
+
return toVercelResponse(stream)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Streaming
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
const { stream, response } = agent('You are helpful.').stream('Tell me a story')
|
|
369
|
+
|
|
370
|
+
for await (const chunk of stream) {
|
|
371
|
+
if (chunk.type === 'text-delta') process.stdout.write(chunk.text!)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const final = await response // full AgentResponse when stream completes
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Conversation History
|
|
378
|
+
|
|
379
|
+
Pass message history to maintain context across turns:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const response = await agent('You are helpful.').prompt('Follow up question', {
|
|
383
|
+
history: [
|
|
384
|
+
{ role: 'user', content: 'What is TypeScript?' },
|
|
385
|
+
{ role: 'assistant', content: 'TypeScript is a typed superset of JavaScript...' },
|
|
386
|
+
],
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Works with both `.prompt()` and `.stream()`. History messages are prepended after the system prompt, before the current user message.
|
|
391
|
+
|
|
392
|
+
### Model Selection
|
|
393
|
+
|
|
394
|
+
Configure available models for user selection (used by `@rudderjs/panels` chat UI):
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
// config/ai.ts
|
|
398
|
+
export default {
|
|
399
|
+
default: 'anthropic/claude-sonnet-4-5',
|
|
400
|
+
providers: { ... },
|
|
401
|
+
models: [
|
|
402
|
+
{ id: 'anthropic/claude-sonnet-4-5', label: 'Claude Sonnet 4.5', default: true },
|
|
403
|
+
{ id: 'anthropic/claude-opus-4-5', label: 'Claude Opus 4.5' },
|
|
404
|
+
{ id: 'openai/gpt-4o', label: 'GPT-4o' },
|
|
405
|
+
{ id: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The model registry is available via `AiRegistry.getModels()` / `AiRegistry.getDefault()`.
|
|
411
|
+
|
|
412
|
+
### Middleware
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import type { AiMiddleware } from '@rudderjs/ai'
|
|
416
|
+
|
|
417
|
+
const loggingMiddleware: AiMiddleware = {
|
|
418
|
+
name: 'logger',
|
|
419
|
+
onStart(ctx) { console.log(`[AI] Request ${ctx.requestId} started`) },
|
|
420
|
+
onFinish(ctx) { console.log(`[AI] Request ${ctx.requestId} finished`) },
|
|
421
|
+
onBeforeToolCall(ctx, toolName, args) {
|
|
422
|
+
console.log(`[AI] Calling tool: ${toolName}`, args)
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Testing
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import { AiFake, AI } from '@rudderjs/ai'
|
|
431
|
+
|
|
432
|
+
const fake = AiFake.fake()
|
|
433
|
+
fake.respondWith('Mocked response')
|
|
434
|
+
|
|
435
|
+
const response = await AI.prompt('Hello')
|
|
436
|
+
assert.strictEqual(response.text, 'Mocked response')
|
|
437
|
+
|
|
438
|
+
fake.assertPrompted(input => input.includes('Hello'))
|
|
439
|
+
fake.restore()
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Providers
|
|
443
|
+
|
|
444
|
+
| Provider | SDK | Model String | Embeddings | Images | TTS/STT |
|
|
445
|
+
|---|---|---|---|---|---|
|
|
446
|
+
| Anthropic | `@anthropic-ai/sdk` | `anthropic/claude-sonnet-4-5` | | | |
|
|
447
|
+
| OpenAI | `openai` | `openai/gpt-4o` | ✓ | ✓ | ✓ |
|
|
448
|
+
| Google | `@google/genai` | `google/gemini-2.5-pro` | ✓ | ✓ | |
|
|
449
|
+
| Ollama | *(none)* | `ollama/llama3` | | | |
|
|
450
|
+
| Groq | *(none)* | `groq/llama-3.3-70b` | | | |
|
|
451
|
+
| DeepSeek | *(none)* | `deepseek/deepseek-chat` | | | |
|
|
452
|
+
| xAI | *(none)* | `xai/grok-3` | | | |
|
|
453
|
+
| Mistral | *(none)* | `mistral/mistral-large` | ✓ | | |
|
|
454
|
+
| Azure OpenAI | `openai` | `azure/gpt-4o` | | | |
|
|
455
|
+
|
|
456
|
+
## Notes
|
|
457
|
+
|
|
458
|
+
- Provider SDKs are optional dependencies — install only what you use
|
|
459
|
+
- `exactOptionalPropertyTypes` compatible
|
|
460
|
+
- All adapters lazy-load their SDK on first use
|
|
461
|
+
- Ollama, Groq, DeepSeek, xAI, Mistral reuse the OpenAI adapter (OpenAI-compatible API)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @rudderjs/ai
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
AI engine for RudderJS providing a provider-agnostic agent framework with tool calling, streaming, middleware, attachments, conversation persistence, structured output, and queued execution. Supports Anthropic, OpenAI, Google, Ollama, DeepSeek, xAI, Groq, Mistral, and Azure OpenAI out of the box. Models are addressed via `provider/model` strings (e.g. `anthropic/claude-sonnet-4-5`), and the `AiRegistry` handles provider resolution and failover.
|
|
6
|
+
|
|
7
|
+
## Key Patterns
|
|
8
|
+
|
|
9
|
+
### Creating Agents
|
|
10
|
+
|
|
11
|
+
Extend the `Agent` class for reusable agents, or use `agent()` for inline one-offs:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
class SearchAgent extends Agent implements HasTools, HasMiddleware {
|
|
15
|
+
instructions() { return 'You are a search assistant.' }
|
|
16
|
+
model() { return 'anthropic/claude-sonnet-4-5' }
|
|
17
|
+
tools() { return [searchTool] }
|
|
18
|
+
middleware() { return [loggingMiddleware] }
|
|
19
|
+
}
|
|
20
|
+
const response = await new SearchAgent().prompt('Find users named John')
|
|
21
|
+
|
|
22
|
+
// Inline agents
|
|
23
|
+
await agent({ instructions: 'You are helpful.', tools: [weatherTool] }).prompt('Hello')
|
|
24
|
+
await agent('You are helpful.').prompt('Hello') // simplest form
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Using Providers (Anthropic, OpenAI, Google, etc.)
|
|
28
|
+
|
|
29
|
+
Configure providers in `config/ai.ts` and register with `ai()`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// config/ai.ts — providers: anthropic, openai, google, ollama, deepseek, xai, groq, mistral, azure
|
|
33
|
+
export default {
|
|
34
|
+
default: 'anthropic/claude-sonnet-4-5',
|
|
35
|
+
providers: {
|
|
36
|
+
anthropic: { apiKey: process.env.ANTHROPIC_API_KEY },
|
|
37
|
+
openai: { apiKey: process.env.OPENAI_API_KEY },
|
|
38
|
+
ollama: { driver: 'ollama', baseUrl: 'http://localhost:11434' },
|
|
39
|
+
},
|
|
40
|
+
} satisfies AiConfig
|
|
41
|
+
|
|
42
|
+
// bootstrap/providers.ts
|
|
43
|
+
export default [ai(configs.ai), ...]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Agents support failover: `failover() { return ['openai/gpt-4o'] }`
|
|
47
|
+
|
|
48
|
+
### Tools
|
|
49
|
+
|
|
50
|
+
Define tools with Zod schemas. Tools are either `server` (executed on backend) or `client` (forwarded to frontend):
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const weatherTool = toolDefinition({
|
|
54
|
+
name: 'get_weather',
|
|
55
|
+
description: 'Get weather for a location',
|
|
56
|
+
inputSchema: z.object({ location: z.string() }),
|
|
57
|
+
}).server(async ({ location }) => ({ temp: 72, unit: 'F', location }))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Middleware
|
|
61
|
+
|
|
62
|
+
Middleware hooks into the agent loop lifecycle. Hooks: `onConfig`, `onStart`, `onIteration`, `onChunk`, `onBeforeToolCall`, `onAfterToolCall`, `onToolPhaseComplete`, `onUsage`, `onAbort`, `onError`, `onFinish`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const loggingMiddleware: AiMiddleware = {
|
|
66
|
+
onStart(ctx) { console.log(`[AI] Request ${ctx.requestId} started`) },
|
|
67
|
+
onUsage(ctx, usage) { console.log(`[AI] Tokens: ${usage.totalTokens}`) },
|
|
68
|
+
onBeforeToolCall(ctx, toolName, args) {
|
|
69
|
+
if (toolName === 'dangerous_tool') return { type: 'skip', result: 'Tool disabled' }
|
|
70
|
+
return undefined // continue normally
|
|
71
|
+
},
|
|
72
|
+
onChunk(ctx, chunk) { return chunk }, // transform or return null to drop
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Attachments
|
|
77
|
+
|
|
78
|
+
Send images and documents alongside prompts:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { Image, Document } from '@rudderjs/ai'
|
|
82
|
+
|
|
83
|
+
const img = await Image.fromPath('./screenshot.png')
|
|
84
|
+
const doc = await Document.fromUrl('https://example.com/report.pdf')
|
|
85
|
+
|
|
86
|
+
await myAgent.prompt('Describe this image and summarize the doc', {
|
|
87
|
+
attachments: [img.toAttachment(), doc.toAttachment()],
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Conversations
|
|
92
|
+
|
|
93
|
+
Persist multi-turn conversations with `ConversationStore`. Register via `setConversationStore()` or pass `conversations` in AI config:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
setConversationStore(new MemoryConversationStore())
|
|
97
|
+
const response = await myAgent.forUser('user-123').prompt('Hello') // creates conversation
|
|
98
|
+
const follow = await myAgent.continue(response.conversationId).prompt('Follow up')
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Streaming
|
|
102
|
+
|
|
103
|
+
Use `.stream()` for real-time token delivery:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const { stream, response } = myAgent.stream('Write a story')
|
|
107
|
+
|
|
108
|
+
for await (const chunk of stream) {
|
|
109
|
+
if (chunk.type === 'text-delta') process.stdout.write(chunk.text ?? '')
|
|
110
|
+
if (chunk.type === 'tool-call') console.log('Tool called:', chunk.toolCall)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const final = await response // full AgentResponse after stream ends
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Structured Output
|
|
117
|
+
|
|
118
|
+
Use `Output` to constrain responses to typed schemas:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Output } from '@rudderjs/ai'
|
|
122
|
+
|
|
123
|
+
const sentiment = Output.choice({ options: ['positive', 'negative', 'neutral'] as const })
|
|
124
|
+
const extraction = Output.object({ schema: z.object({ name: z.string(), age: z.number() }) })
|
|
125
|
+
const items = Output.array({ element: z.object({ title: z.string() }) })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Common Pitfalls
|
|
129
|
+
|
|
130
|
+
- **Model string format**: Always use `provider/model` (e.g. `anthropic/claude-sonnet-4-5`). A bare model name throws.
|
|
131
|
+
- **Optional SDK deps**: Provider SDKs (`@anthropic-ai/sdk`, `openai`, `@google/genai`) are optional dependencies. Install the ones you need.
|
|
132
|
+
- **ConversationStore required for `.forUser()`/`.continue()`**: Call `setConversationStore()` or pass `conversations` in the AI config. Without it, conversation methods throw.
|
|
133
|
+
- **Tool loop limits**: `maxSteps()` defaults to 20. If the agent hits the limit it stops silently. Increase it for complex multi-tool workflows.
|
|
134
|
+
- **Streaming response access**: `await response` only resolves after the stream is fully consumed. Always iterate the stream first.
|
|
135
|
+
- **Embeddings**: Only providers that implement `createEmbedding()` support `AI.embed()`. Currently OpenAI-compatible providers.
|
|
136
|
+
|
|
137
|
+
## Key Imports
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { ai } from '@rudderjs/ai' // provider factory
|
|
141
|
+
import { Agent, agent, ConversableAgent } from '@rudderjs/ai' // agents
|
|
142
|
+
import { AI } from '@rudderjs/ai' // facade (AI.prompt, AI.agent, AI.embed)
|
|
143
|
+
import { toolDefinition } from '@rudderjs/ai' // tool builder
|
|
144
|
+
import { Image, Document } from '@rudderjs/ai' // attachments
|
|
145
|
+
import { MemoryConversationStore, setConversationStore } from '@rudderjs/ai'
|
|
146
|
+
import { Output } from '@rudderjs/ai' // structured output
|
|
147
|
+
import { AiRegistry } from '@rudderjs/ai' // provider registry
|
|
148
|
+
import { stepCountIs, hasToolCall } from '@rudderjs/ai' // stop conditions
|
|
149
|
+
import type { AgentResponse, AiConfig, AiMiddleware, AnyTool, HasTools, HasMiddleware } from '@rudderjs/ai'
|
|
150
|
+
```
|