@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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +461 -0
  3. package/boost/guidelines.md +150 -0
  4. package/dist/agent.d.ts +74 -0
  5. package/dist/agent.d.ts.map +1 -0
  6. package/dist/agent.js +1070 -0
  7. package/dist/agent.js.map +1 -0
  8. package/dist/attachment.d.ts +35 -0
  9. package/dist/attachment.d.ts.map +1 -0
  10. package/dist/attachment.js +121 -0
  11. package/dist/attachment.js.map +1 -0
  12. package/dist/audio.d.ts +33 -0
  13. package/dist/audio.d.ts.map +1 -0
  14. package/dist/audio.js +76 -0
  15. package/dist/audio.js.map +1 -0
  16. package/dist/cached-embedding.d.ts +14 -0
  17. package/dist/cached-embedding.d.ts.map +1 -0
  18. package/dist/cached-embedding.js +44 -0
  19. package/dist/cached-embedding.js.map +1 -0
  20. package/dist/conversation.d.ts +16 -0
  21. package/dist/conversation.d.ts.map +1 -0
  22. package/dist/conversation.js +53 -0
  23. package/dist/conversation.js.map +1 -0
  24. package/dist/facade.d.ts +53 -0
  25. package/dist/facade.d.ts.map +1 -0
  26. package/dist/facade.js +100 -0
  27. package/dist/facade.js.map +1 -0
  28. package/dist/fake.d.ts +55 -0
  29. package/dist/fake.d.ts.map +1 -0
  30. package/dist/fake.js +172 -0
  31. package/dist/fake.js.map +1 -0
  32. package/dist/image.d.ts +27 -0
  33. package/dist/image.d.ts.map +1 -0
  34. package/dist/image.js +90 -0
  35. package/dist/image.js.map +1 -0
  36. package/dist/index.d.ts +30 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +45 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/middleware.d.ts +18 -0
  41. package/dist/middleware.d.ts.map +1 -0
  42. package/dist/middleware.js +72 -0
  43. package/dist/middleware.js.map +1 -0
  44. package/dist/output.d.ts +22 -0
  45. package/dist/output.d.ts.map +1 -0
  46. package/dist/output.js +55 -0
  47. package/dist/output.js.map +1 -0
  48. package/dist/provider-tools.d.ts +60 -0
  49. package/dist/provider-tools.d.ts.map +1 -0
  50. package/dist/provider-tools.js +133 -0
  51. package/dist/provider-tools.js.map +1 -0
  52. package/dist/provider.d.ts +12 -0
  53. package/dist/provider.d.ts.map +1 -0
  54. package/dist/provider.js +94 -0
  55. package/dist/provider.js.map +1 -0
  56. package/dist/providers/anthropic.d.ts +12 -0
  57. package/dist/providers/anthropic.d.ts.map +1 -0
  58. package/dist/providers/anthropic.js +221 -0
  59. package/dist/providers/anthropic.js.map +1 -0
  60. package/dist/providers/azure.d.ts +13 -0
  61. package/dist/providers/azure.d.ts.map +1 -0
  62. package/dist/providers/azure.js +15 -0
  63. package/dist/providers/azure.js.map +1 -0
  64. package/dist/providers/deepseek.d.ts +12 -0
  65. package/dist/providers/deepseek.d.ts.map +1 -0
  66. package/dist/providers/deepseek.js +15 -0
  67. package/dist/providers/deepseek.js.map +1 -0
  68. package/dist/providers/google.d.ts +13 -0
  69. package/dist/providers/google.d.ts.map +1 -0
  70. package/dist/providers/google.js +293 -0
  71. package/dist/providers/google.js.map +1 -0
  72. package/dist/providers/groq.d.ts +12 -0
  73. package/dist/providers/groq.d.ts.map +1 -0
  74. package/dist/providers/groq.js +15 -0
  75. package/dist/providers/groq.js.map +1 -0
  76. package/dist/providers/mistral.d.ts +13 -0
  77. package/dist/providers/mistral.d.ts.map +1 -0
  78. package/dist/providers/mistral.js +46 -0
  79. package/dist/providers/mistral.js.map +1 -0
  80. package/dist/providers/ollama.d.ts +11 -0
  81. package/dist/providers/ollama.d.ts.map +1 -0
  82. package/dist/providers/ollama.js +15 -0
  83. package/dist/providers/ollama.js.map +1 -0
  84. package/dist/providers/openai.d.ts +26 -0
  85. package/dist/providers/openai.d.ts.map +1 -0
  86. package/dist/providers/openai.js +374 -0
  87. package/dist/providers/openai.js.map +1 -0
  88. package/dist/providers/xai.d.ts +12 -0
  89. package/dist/providers/xai.d.ts.map +1 -0
  90. package/dist/providers/xai.js +15 -0
  91. package/dist/providers/xai.js.map +1 -0
  92. package/dist/queue-job.d.ts +35 -0
  93. package/dist/queue-job.d.ts.map +1 -0
  94. package/dist/queue-job.js +82 -0
  95. package/dist/queue-job.js.map +1 -0
  96. package/dist/registry.d.ts +25 -0
  97. package/dist/registry.d.ts.map +1 -0
  98. package/dist/registry.js +54 -0
  99. package/dist/registry.js.map +1 -0
  100. package/dist/tool.d.ts +157 -0
  101. package/dist/tool.d.ts.map +1 -0
  102. package/dist/tool.js +134 -0
  103. package/dist/tool.js.map +1 -0
  104. package/dist/transcription.d.ts +28 -0
  105. package/dist/transcription.d.ts.map +1 -0
  106. package/dist/transcription.js +63 -0
  107. package/dist/transcription.js.map +1 -0
  108. package/dist/types.d.ts +439 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +2 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/vercel-protocol.d.ts +18 -0
  113. package/dist/vercel-protocol.d.ts.map +1 -0
  114. package/dist/vercel-protocol.js +75 -0
  115. package/dist/vercel-protocol.js.map +1 -0
  116. package/dist/zod-to-json-schema.d.ts +8 -0
  117. package/dist/zod-to-json-schema.d.ts.map +1 -0
  118. package/dist/zod-to-json-schema.js +86 -0
  119. package/dist/zod-to-json-schema.js.map +1 -0
  120. 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
+ ```