@convex-dev/agent 0.0.14 → 0.0.15-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -64
- package/dist/commonjs/client/index.d.ts +149 -31
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +31 -23
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/esm/client/index.d.ts +149 -31
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +31 -23
- package/dist/esm/client/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +257 -77
package/README.md
CHANGED
|
@@ -8,12 +8,13 @@ AI Agent framework built on Convex.
|
|
|
8
8
|
|
|
9
9
|
- Automatic storage of chat history, per-user or per-thread, that can span multiple agents.
|
|
10
10
|
- RAG for chat context, via hybrid text & vector search, with configuration options.
|
|
11
|
-
|
|
11
|
+
Use the API to query the history yourself and do it your way.
|
|
12
12
|
- Opt-in search for messages from other threads (for the same specified user).
|
|
13
13
|
- Support for generating / streaming objects and storing them in messages (as JSON).
|
|
14
|
-
- Tool calls via the AI SDK, along with Convex-specific
|
|
15
|
-
- Easy
|
|
16
|
-
-
|
|
14
|
+
- Tool calls via the AI SDK, along with Convex-specific tool wrappers.
|
|
15
|
+
- Easy integration with the [Workflow component](https://convex.dev/components/workflow).
|
|
16
|
+
Enables long-lived, durable workflows defined as code.
|
|
17
|
+
- Reactive & realtime updates from asynchronous functions / workflows.
|
|
17
18
|
- Support for streaming text and storing the final result.
|
|
18
19
|
- Optionally filter tool calls out of the thread history.
|
|
19
20
|
|
|
@@ -34,7 +35,9 @@ const supportAgent = new Agent(components.agent, {
|
|
|
34
35
|
export const createThread = action({
|
|
35
36
|
args: { prompt: v.string() },
|
|
36
37
|
handler: async (ctx, { prompt }) => {
|
|
38
|
+
// Start a new thread for the user.
|
|
37
39
|
const { threadId, thread } = await supportAgent.createThread(ctx);
|
|
40
|
+
// Creates a user message with the prompt, and an assistant reply message.
|
|
38
41
|
const result = await thread.generateText({ prompt });
|
|
39
42
|
return { threadId, text: result.text };
|
|
40
43
|
},
|
|
@@ -44,8 +47,9 @@ export const createThread = action({
|
|
|
44
47
|
export const continueThread = action({
|
|
45
48
|
args: { prompt: v.string(), threadId: v.string() },
|
|
46
49
|
handler: async (ctx, { prompt, threadId }) => {
|
|
47
|
-
//
|
|
50
|
+
// Continue a thread, picking up where you left off.
|
|
48
51
|
const { thread } = await anotherAgent.continueThread(ctx, { threadId });
|
|
52
|
+
// This includes previous message history from the thread automatically.
|
|
49
53
|
const result = await thread.generateText({ prompt });
|
|
50
54
|
return result.text;
|
|
51
55
|
},
|
|
@@ -118,11 +122,11 @@ import { components } from "./_generated/api";
|
|
|
118
122
|
|
|
119
123
|
// Define an agent similarly to the AI SDK
|
|
120
124
|
const supportAgent = new Agent(components.agent, {
|
|
121
|
-
//
|
|
125
|
+
// The chat completions model to use for the agent.
|
|
122
126
|
chat: openai.chat("gpt-4o-mini"),
|
|
123
|
-
//
|
|
127
|
+
// Embedding model to power vector search of message history (RAG).
|
|
124
128
|
textEmbedding: openai.embedding("text-embedding-3-small"),
|
|
125
|
-
//
|
|
129
|
+
// The default system prompt if not overriden.
|
|
126
130
|
instructions: "You are a helpful assistant.",
|
|
127
131
|
tools: {
|
|
128
132
|
// Standard AI SDK tool
|
|
@@ -136,43 +140,54 @@ const supportAgent = new Agent(components.agent, {
|
|
|
136
140
|
},
|
|
137
141
|
}),
|
|
138
142
|
},
|
|
139
|
-
// Used for fetching context messages.
|
|
143
|
+
// Used for fetching context messages. Values shown are the defaults.
|
|
140
144
|
contextOptions: {
|
|
141
145
|
// Whether to include tool messages in the context.
|
|
142
|
-
includeToolCalls:
|
|
146
|
+
includeToolCalls: false,
|
|
143
147
|
// How many recent messages to include. These are added after the search
|
|
144
148
|
// messages, and do not count against the search limit.
|
|
145
|
-
recentMessages:
|
|
146
|
-
//
|
|
147
|
-
// By default, only the current thread is searched.
|
|
148
|
-
searchOtherThreads: true,
|
|
149
|
-
// Options for searching messages.
|
|
149
|
+
recentMessages: 100,
|
|
150
|
+
// Options for searching messages via text and/or vector search.
|
|
150
151
|
searchOptions: {
|
|
151
|
-
// The maximum number of messages to fetch.
|
|
152
|
-
|
|
153
|
-
// Whether to use
|
|
154
|
-
textSearch: true,
|
|
155
|
-
// Whether to use vector search to find messages.
|
|
156
|
-
vectorSearch: true,
|
|
152
|
+
limit: 10, // The maximum number of messages to fetch.
|
|
153
|
+
textSearch: false, // Whether to use text search to find messages.
|
|
154
|
+
vectorSearch: false, // Whether to use vector search to find messages.
|
|
157
155
|
// Note, this is after the limit is applied.
|
|
158
156
|
// E.g. this will quadruple the number of messages fetched.
|
|
159
157
|
// (two before, and one after each message found in the search)
|
|
160
158
|
messageRange: { before: 2, after: 1 },
|
|
161
159
|
},
|
|
160
|
+
// Whether to search across other threads for relevant messages.
|
|
161
|
+
// By default, only the current thread is searched.
|
|
162
|
+
searchOtherThreads: false,
|
|
162
163
|
},
|
|
163
164
|
// Used for storing messages.
|
|
164
165
|
storageOptions: {
|
|
165
|
-
//
|
|
166
|
+
// When false, allows you to pass in arbitrary context that will
|
|
166
167
|
// be in addition to automatically fetched content.
|
|
167
168
|
// Pass true to have all input messages saved to the thread history.
|
|
168
|
-
saveAllInputMessages:
|
|
169
|
-
//
|
|
169
|
+
saveAllInputMessages: false,
|
|
170
|
+
// By default it saves the input message, or the last message if multiple are provided.
|
|
171
|
+
saveAnyInputMessages: true,
|
|
172
|
+
// Save the generated messages to the thread history.
|
|
170
173
|
saveOutputMessages: true,
|
|
171
174
|
},
|
|
172
175
|
// Used for limiting the number of steps when tool calls are involved.
|
|
173
|
-
maxSteps:
|
|
176
|
+
maxSteps: 1,
|
|
174
177
|
// Used for limiting the number of retries when a tool call fails.
|
|
175
178
|
maxRetries: 3,
|
|
179
|
+
// Used for tracking token usage.
|
|
180
|
+
usageHandler: async (ctx, args) => {
|
|
181
|
+
const {
|
|
182
|
+
// Who used the tokens
|
|
183
|
+
userId, threadId, agentName,
|
|
184
|
+
// What LLM was used
|
|
185
|
+
model, provider,
|
|
186
|
+
// How many tokens were used (extra info is available in providerMetadata)
|
|
187
|
+
usage, providerMetadata
|
|
188
|
+
} = args;
|
|
189
|
+
// ... log, save usage to your database, etc.
|
|
190
|
+
},
|
|
176
191
|
});
|
|
177
192
|
```
|
|
178
193
|
|
|
@@ -190,7 +205,7 @@ export const createThread = action({
|
|
|
190
205
|
args: { prompt: v.string(), userId: v.string() },
|
|
191
206
|
handler: async (ctx, { prompt, userId }): Promise<{ threadId: string; initialResponse: string }> => {
|
|
192
207
|
// Start a new thread for the user.
|
|
193
|
-
|
|
208
|
+
+ const { threadId, thread } = await supportAgent.createThread(ctx, { userId });
|
|
194
209
|
const result = await thread.generateText({ prompt });
|
|
195
210
|
return { threadId, initialResponse: result.text };
|
|
196
211
|
},
|
|
@@ -208,14 +223,59 @@ export const continueThread = action({
|
|
|
208
223
|
args: { prompt: v.string(), threadId: v.string() },
|
|
209
224
|
handler: async (ctx, { prompt, threadId }): Promise<string> => {
|
|
210
225
|
// This includes previous message history from the thread automatically.
|
|
211
|
-
|
|
226
|
+
+ const { thread } = await supportAgent.continueThread(ctx, { threadId });
|
|
212
227
|
const result = await thread.generateText({ prompt });
|
|
213
228
|
return result.text;
|
|
214
229
|
},
|
|
215
230
|
});
|
|
216
231
|
```
|
|
217
232
|
|
|
218
|
-
###
|
|
233
|
+
### Creating a tool with Convex context
|
|
234
|
+
|
|
235
|
+
There are two ways to create a tool that has access to the Convex context.
|
|
236
|
+
|
|
237
|
+
1. Use the `createTool` function, which is a wrapper around the AI SDK's `tool` function.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
export const ideaSearch = createTool({
|
|
241
|
+
description: "Search for ideas in the database",
|
|
242
|
+
args: z.object({ query: z.string() }),
|
|
243
|
+
handler: async (ctx, args): Promise<Array<Idea>> => {
|
|
244
|
+
// ctx has userId, threadId, messageId, runQuery, runMutation, and runAction
|
|
245
|
+
const ideas = await ctx.runQuery(api.ideas.searchIdeas, { query: args.query });
|
|
246
|
+
console.log("found ideas", ideas);
|
|
247
|
+
return ideas;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
2. Define tools at runtime in a context with the variables you want to use.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
async function createTool(ctx: ActionCtx, teamId: Id<"teams">) {
|
|
256
|
+
const myTool = tool({
|
|
257
|
+
description: "My tool",
|
|
258
|
+
parameters: z.object({...}),
|
|
259
|
+
execute: async (args, options) => {
|
|
260
|
+
return await ctx.runQuery(internal.foo.bar, args);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
You can provide tools at different times:
|
|
267
|
+
|
|
268
|
+
- Agent contructor: (`new Agent(components.agent, { tools: {...} })`)
|
|
269
|
+
- Creating a thread: `createThread(ctx, { tools: {...} })`
|
|
270
|
+
- Continuing a thread: `continueThread(ctx, { tools: {...} })`
|
|
271
|
+
- On thread functions: `thread.generateText({ tools: {...} })`
|
|
272
|
+
- Outside of a thread: `supportAgent.generateText(ctx, {}, { tools: {...} })`
|
|
273
|
+
|
|
274
|
+
Specifying tools at each layer will overwrite the defaults.
|
|
275
|
+
The tools will be `args.tools ?? thread.tools ?? agent.options.tools`.
|
|
276
|
+
This allows you to create tools in a context that is convenient.
|
|
277
|
+
|
|
278
|
+
### Exposing the agent as Convex actions
|
|
219
279
|
|
|
220
280
|
You can expose the agent as a Convex internal action.
|
|
221
281
|
This is generally used from a workflow, where each step is a new thread message.
|
|
@@ -226,16 +286,23 @@ export const getSupport = supportAgent.asTextAction({
|
|
|
226
286
|
});
|
|
227
287
|
```
|
|
228
288
|
|
|
229
|
-
You can also expose
|
|
289
|
+
You can also expose a standalone action that generates an object.
|
|
230
290
|
|
|
231
291
|
```ts
|
|
232
292
|
export const getStructuredSupport = supportAgent.asObjectAction({
|
|
233
293
|
schema: z.object({
|
|
234
294
|
analysis: z.string().describe("A detailed analysis of the user's request."),
|
|
235
|
-
suggestion: z.string().describe("A suggested action to take.")
|
|
295
|
+
suggestion: z.string().describe("A suggested action to take.")
|
|
296
|
+
}),
|
|
236
297
|
});
|
|
237
298
|
```
|
|
238
299
|
|
|
300
|
+
Create a thread from within a workflow, similar to agent.createThread.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
export const createThread = supportAgent.createThreadMutation();
|
|
304
|
+
```
|
|
305
|
+
|
|
239
306
|
### Using the agent actions within a workflow
|
|
240
307
|
|
|
241
308
|
You can use the [Workflow component](https://convex.dev/components/workflow)
|
|
@@ -247,8 +314,11 @@ surviving server restarts, and more. Read more about durable workflows
|
|
|
247
314
|
const workflow = new WorkflowManager(components.workflow);
|
|
248
315
|
|
|
249
316
|
export const supportAgentWorkflow = workflow.define({
|
|
250
|
-
args: { prompt: v.string(), userId: v.string()
|
|
251
|
-
handler: async (step, { prompt, userId
|
|
317
|
+
args: { prompt: v.string(), userId: v.string() },
|
|
318
|
+
handler: async (step, { prompt, userId }) => {
|
|
319
|
+
const { threadId } = await step.runMutation(internal.example.createThread, {
|
|
320
|
+
userId, title: "Support Request",
|
|
321
|
+
});
|
|
252
322
|
const suggestion = await step.runAction(internal.example.getSupport, {
|
|
253
323
|
threadId, userId, prompt,
|
|
254
324
|
});
|
|
@@ -281,75 +351,85 @@ const result = await supportAgent.generateText(ctx, { userId }, { prompt });
|
|
|
281
351
|
|
|
282
352
|
### Manually managing messages
|
|
283
353
|
|
|
354
|
+
Fetch the full messages directly. These will include things like usage, etc.
|
|
355
|
+
|
|
284
356
|
```ts
|
|
285
357
|
const messages = await ctx.runQuery(
|
|
286
358
|
components.agent.messages.getThreadMessages,
|
|
287
|
-
{ threadId,
|
|
359
|
+
{ threadId, order: "desc", paginationOpts: { cursor: null, numItems: 10 } }
|
|
288
360
|
);
|
|
289
361
|
```
|
|
290
362
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
]});
|
|
295
|
-
```
|
|
296
|
-
Note: you can also pass in message metadata if you want to save usage, etc.
|
|
297
|
-
See the docstrings in [the implementation](./src/client/index.ts#L473).
|
|
363
|
+
Fetch CoreMessages (e.g. `{ role, content }`) for a user and/or thread.
|
|
364
|
+
Accepts ContextOptions, e.g. includeToolCalls, searchOptions, etc.
|
|
365
|
+
If you provide a parentMessageId, it will only fetch messages from before that message.
|
|
298
366
|
|
|
299
367
|
```ts
|
|
300
|
-
const
|
|
368
|
+
const coreMessages = await supportAgent.fetchContextMessages(ctx, {
|
|
369
|
+
threadId, messages: [{ role, content }], contextOptions
|
|
370
|
+
});
|
|
301
371
|
```
|
|
302
372
|
|
|
303
|
-
|
|
373
|
+
Save messages to the database.
|
|
374
|
+
|
|
304
375
|
```ts
|
|
305
|
-
const
|
|
306
|
-
threadId,
|
|
307
|
-
|
|
308
|
-
|
|
376
|
+
const { lastMessageId, messageIds} = await agent.saveMessages(ctx, {
|
|
377
|
+
threadId, userId,
|
|
378
|
+
messages: [{ role, content }],
|
|
379
|
+
metadata: [{ reasoning, usage, ... }] // See MessageWithMetadata type
|
|
309
380
|
});
|
|
310
381
|
```
|
|
311
382
|
|
|
312
383
|
### Manage embeddings
|
|
313
384
|
|
|
314
|
-
|
|
315
|
-
const messages = await ctx.runQuery(
|
|
316
|
-
components.agent.vector.paginate,
|
|
317
|
-
{ vectorDimension: 1536, cursor: null, limit: 10 }
|
|
318
|
-
);
|
|
319
|
-
```
|
|
385
|
+
Generate embeddings for a set of messages.
|
|
320
386
|
|
|
321
387
|
```ts
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
388
|
+
const embeddings = await supportAgent.generateEmbeddings([
|
|
389
|
+
{ role: "user", content: "What is love?" },
|
|
390
|
+
]);
|
|
326
391
|
```
|
|
327
392
|
|
|
393
|
+
Get and update embeddings, e.g. for a migration to a new model.
|
|
394
|
+
|
|
328
395
|
```ts
|
|
329
396
|
const messages = await ctx.runQuery(
|
|
330
|
-
components.agent.vector.
|
|
331
|
-
|
|
332
|
-
vectors: [
|
|
333
|
-
{ model: "gpt-4o-mini", kind: "thread", userId: "123", threadId: "123", vector: embedding, },
|
|
334
|
-
],
|
|
335
|
-
}
|
|
397
|
+
components.agent.vector.index.paginate,
|
|
398
|
+
{ vectorDimension: 1536, cursor: null, limit: 10 }
|
|
336
399
|
);
|
|
337
400
|
```
|
|
338
401
|
|
|
402
|
+
Note: If the dimension changes, you need to delete the old and insert the new.
|
|
403
|
+
|
|
339
404
|
```ts
|
|
340
|
-
const messages = await ctx.runQuery(components.agent.vector.updateBatch, {
|
|
405
|
+
const messages = await ctx.runQuery(components.agent.vector.index.updateBatch, {
|
|
341
406
|
vectors: [
|
|
342
407
|
{ model: "gpt-4o-mini", vector: embedding, id: msg.embeddingId },
|
|
343
408
|
],
|
|
344
409
|
});
|
|
345
410
|
```
|
|
346
411
|
|
|
412
|
+
Delete embeddings
|
|
413
|
+
|
|
347
414
|
```ts
|
|
348
|
-
const messages = await ctx.runQuery(components.agent.vector.deleteBatch, {
|
|
415
|
+
const messages = await ctx.runQuery(components.agent.vector.index.deleteBatch, {
|
|
349
416
|
ids: [embeddingId1, embeddingId2],
|
|
350
417
|
});
|
|
351
418
|
```
|
|
352
419
|
|
|
420
|
+
Insert embeddings
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const messages = await ctx.runQuery(
|
|
424
|
+
components.agent.vector.index.insertBatch, {
|
|
425
|
+
vectorDimension: 1536,
|
|
426
|
+
vectors: [
|
|
427
|
+
{ model: "gpt-4o-mini", table: "messages", userId: "123", threadId: "123", vector: embedding, },
|
|
428
|
+
],
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
```
|
|
432
|
+
|
|
353
433
|
See example usage in [example.ts](./example/convex/example.ts).
|
|
354
434
|
Read more in [this Stack post](https://stack.convex.dev/ai-agents).
|
|
355
435
|
|
|
@@ -357,6 +437,41 @@ Read more in [this Stack post](https://stack.convex.dev/ai-agents).
|
|
|
357
437
|
npm i @convex-dev/agent
|
|
358
438
|
```
|
|
359
439
|
|
|
440
|
+
### Tracking token usage
|
|
441
|
+
|
|
442
|
+
You can provide a `usageHandler` to the agent to track token usage.
|
|
443
|
+
See an example in
|
|
444
|
+
[this demo](https://github.com/ianmacartney/ai-agent-chat/blob/main/convex/chat.ts)
|
|
445
|
+
that captures usage to a table, then scans it to generate per-user invoices.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const supportAgent = new Agent(components.agent, {
|
|
449
|
+
...
|
|
450
|
+
usageHandler: async (ctx, args) => {
|
|
451
|
+
const { userId, threadId, agentName } = args;
|
|
452
|
+
const { model, provider, usage, providerMetadata } = args;
|
|
453
|
+
// ... save usage to your database, etc.
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
// or when creating/continuing a thread:
|
|
457
|
+
const { thread } = await supportAgent.createThread(ctx, {
|
|
458
|
+
...
|
|
459
|
+
usageHandler: async (ctx, args) => {
|
|
460
|
+
// ...
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
// or when generating text:
|
|
464
|
+
const result = await thread.generateText({
|
|
465
|
+
...
|
|
466
|
+
usageHandler: async (ctx, args) => {
|
|
467
|
+
// ...
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Tip: Define the `usageHandler` within a function where you have more variables
|
|
473
|
+
available to attribute the usage to a different user, team, project, etc.
|
|
474
|
+
|
|
360
475
|
## Troubleshooting
|
|
361
476
|
|
|
362
477
|
### Circular dependencies
|