- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
516 lines
13 KiB
Markdown
516 lines
13 KiB
Markdown
---
|
|
name: Convex Agents
|
|
description: Building AI agents with the Convex Agent component including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration
|
|
version: 1.0.0
|
|
author: Convex
|
|
tags: [convex, agents, ai, llm, tools, rag, workflows]
|
|
---
|
|
|
|
# Convex Agents
|
|
|
|
Build persistent, stateful AI agents with Convex including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration.
|
|
|
|
## Documentation Sources
|
|
|
|
Before implementing, do not assume; fetch the latest documentation:
|
|
|
|
- Primary: https://docs.convex.dev/ai
|
|
- Convex Agent Component: https://www.npmjs.com/package/@convex-dev/agent
|
|
- For broader context: https://docs.convex.dev/llms.txt
|
|
|
|
## Instructions
|
|
|
|
### Why Convex for AI Agents
|
|
|
|
- **Persistent State** - Conversation history survives restarts
|
|
- **Real-time Updates** - Stream responses to clients automatically
|
|
- **Tool Execution** - Run Convex functions as agent tools
|
|
- **Durable Workflows** - Long-running agent tasks with reliability
|
|
- **Built-in RAG** - Vector search for knowledge retrieval
|
|
|
|
### Setting Up Convex Agent
|
|
|
|
```bash
|
|
npm install @convex-dev/agent ai openai
|
|
```
|
|
|
|
```typescript
|
|
// convex/agent.ts
|
|
import { Agent } from "@convex-dev/agent";
|
|
import { components } from "./_generated/api";
|
|
import { OpenAI } from "openai";
|
|
|
|
const openai = new OpenAI();
|
|
|
|
export const agent = new Agent(components.agent, {
|
|
chat: openai.chat,
|
|
textEmbedding: openai.embeddings,
|
|
});
|
|
```
|
|
|
|
### Thread Management
|
|
|
|
```typescript
|
|
// convex/threads.ts
|
|
import { mutation, query } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { agent } from "./agent";
|
|
|
|
// Create a new conversation thread
|
|
export const createThread = mutation({
|
|
args: {
|
|
userId: v.id("users"),
|
|
title: v.optional(v.string()),
|
|
},
|
|
returns: v.id("threads"),
|
|
handler: async (ctx, args) => {
|
|
const threadId = await agent.createThread(ctx, {
|
|
userId: args.userId,
|
|
metadata: {
|
|
title: args.title ?? "New Conversation",
|
|
createdAt: Date.now(),
|
|
},
|
|
});
|
|
return threadId;
|
|
},
|
|
});
|
|
|
|
// List user's threads
|
|
export const listThreads = query({
|
|
args: { userId: v.id("users") },
|
|
returns: v.array(v.object({
|
|
_id: v.id("threads"),
|
|
title: v.string(),
|
|
lastMessageAt: v.optional(v.number()),
|
|
})),
|
|
handler: async (ctx, args) => {
|
|
return await agent.listThreads(ctx, {
|
|
userId: args.userId,
|
|
});
|
|
},
|
|
});
|
|
|
|
// Get thread messages
|
|
export const getMessages = query({
|
|
args: { threadId: v.id("threads") },
|
|
returns: v.array(v.object({
|
|
role: v.string(),
|
|
content: v.string(),
|
|
createdAt: v.number(),
|
|
})),
|
|
handler: async (ctx, args) => {
|
|
return await agent.getMessages(ctx, {
|
|
threadId: args.threadId,
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
### Sending Messages and Streaming Responses
|
|
|
|
```typescript
|
|
// convex/chat.ts
|
|
import { action } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { agent } from "./agent";
|
|
import { internal } from "./_generated/api";
|
|
|
|
export const sendMessage = action({
|
|
args: {
|
|
threadId: v.id("threads"),
|
|
message: v.string(),
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
// Add user message to thread
|
|
await ctx.runMutation(internal.chat.addUserMessage, {
|
|
threadId: args.threadId,
|
|
content: args.message,
|
|
});
|
|
|
|
// Generate AI response with streaming
|
|
const response = await agent.chat(ctx, {
|
|
threadId: args.threadId,
|
|
messages: [{ role: "user", content: args.message }],
|
|
stream: true,
|
|
onToken: async (token) => {
|
|
// Stream tokens to client via mutation
|
|
await ctx.runMutation(internal.chat.appendToken, {
|
|
threadId: args.threadId,
|
|
token,
|
|
});
|
|
},
|
|
});
|
|
|
|
// Save complete response
|
|
await ctx.runMutation(internal.chat.saveResponse, {
|
|
threadId: args.threadId,
|
|
content: response.content,
|
|
});
|
|
|
|
return null;
|
|
},
|
|
});
|
|
```
|
|
|
|
### Tool Integration
|
|
|
|
Define tools that agents can use:
|
|
|
|
```typescript
|
|
// convex/tools.ts
|
|
import { tool } from "@convex-dev/agent";
|
|
import { v } from "convex/values";
|
|
import { api } from "./_generated/api";
|
|
|
|
// Tool to search knowledge base
|
|
export const searchKnowledge = tool({
|
|
name: "search_knowledge",
|
|
description: "Search the knowledge base for relevant information",
|
|
parameters: v.object({
|
|
query: v.string(),
|
|
limit: v.optional(v.number()),
|
|
}),
|
|
handler: async (ctx, args) => {
|
|
const results = await ctx.runQuery(api.knowledge.search, {
|
|
query: args.query,
|
|
limit: args.limit ?? 5,
|
|
});
|
|
return results;
|
|
},
|
|
});
|
|
|
|
// Tool to create a task
|
|
export const createTask = tool({
|
|
name: "create_task",
|
|
description: "Create a new task for the user",
|
|
parameters: v.object({
|
|
title: v.string(),
|
|
description: v.optional(v.string()),
|
|
dueDate: v.optional(v.string()),
|
|
}),
|
|
handler: async (ctx, args) => {
|
|
const taskId = await ctx.runMutation(api.tasks.create, {
|
|
title: args.title,
|
|
description: args.description,
|
|
dueDate: args.dueDate ? new Date(args.dueDate).getTime() : undefined,
|
|
});
|
|
return { success: true, taskId };
|
|
},
|
|
});
|
|
|
|
// Tool to get weather
|
|
export const getWeather = tool({
|
|
name: "get_weather",
|
|
description: "Get current weather for a location",
|
|
parameters: v.object({
|
|
location: v.string(),
|
|
}),
|
|
handler: async (ctx, args) => {
|
|
const response = await fetch(
|
|
`https://api.weather.com/current?location=${encodeURIComponent(args.location)}`
|
|
);
|
|
return await response.json();
|
|
},
|
|
});
|
|
```
|
|
|
|
### Agent with Tools
|
|
|
|
```typescript
|
|
// convex/assistant.ts
|
|
import { action } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { agent } from "./agent";
|
|
import { searchKnowledge, createTask, getWeather } from "./tools";
|
|
|
|
export const chat = action({
|
|
args: {
|
|
threadId: v.id("threads"),
|
|
message: v.string(),
|
|
},
|
|
returns: v.string(),
|
|
handler: async (ctx, args) => {
|
|
const response = await agent.chat(ctx, {
|
|
threadId: args.threadId,
|
|
messages: [{ role: "user", content: args.message }],
|
|
tools: [searchKnowledge, createTask, getWeather],
|
|
systemPrompt: `You are a helpful assistant. You have access to tools to:
|
|
- Search the knowledge base for information
|
|
- Create tasks for the user
|
|
- Get weather information
|
|
Use these tools when appropriate to help the user.`,
|
|
});
|
|
|
|
return response.content;
|
|
},
|
|
});
|
|
```
|
|
|
|
### RAG (Retrieval Augmented Generation)
|
|
|
|
```typescript
|
|
// convex/knowledge.ts
|
|
import { mutation, query } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { agent } from "./agent";
|
|
|
|
// Add document to knowledge base
|
|
export const addDocument = mutation({
|
|
args: {
|
|
title: v.string(),
|
|
content: v.string(),
|
|
metadata: v.optional(v.object({
|
|
source: v.optional(v.string()),
|
|
category: v.optional(v.string()),
|
|
})),
|
|
},
|
|
returns: v.id("documents"),
|
|
handler: async (ctx, args) => {
|
|
// Generate embedding
|
|
const embedding = await agent.embed(ctx, args.content);
|
|
|
|
return await ctx.db.insert("documents", {
|
|
title: args.title,
|
|
content: args.content,
|
|
embedding,
|
|
metadata: args.metadata ?? {},
|
|
createdAt: Date.now(),
|
|
});
|
|
},
|
|
});
|
|
|
|
// Search knowledge base
|
|
export const search = query({
|
|
args: {
|
|
query: v.string(),
|
|
limit: v.optional(v.number()),
|
|
},
|
|
returns: v.array(v.object({
|
|
_id: v.id("documents"),
|
|
title: v.string(),
|
|
content: v.string(),
|
|
score: v.number(),
|
|
})),
|
|
handler: async (ctx, args) => {
|
|
const results = await agent.search(ctx, {
|
|
query: args.query,
|
|
table: "documents",
|
|
limit: args.limit ?? 5,
|
|
});
|
|
|
|
return results.map((r) => ({
|
|
_id: r._id,
|
|
title: r.title,
|
|
content: r.content,
|
|
score: r._score,
|
|
}));
|
|
},
|
|
});
|
|
```
|
|
|
|
### Workflow Orchestration
|
|
|
|
```typescript
|
|
// convex/workflows.ts
|
|
import { action, internalMutation } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { agent } from "./agent";
|
|
import { internal } from "./_generated/api";
|
|
|
|
// Multi-step research workflow
|
|
export const researchTopic = action({
|
|
args: {
|
|
topic: v.string(),
|
|
userId: v.id("users"),
|
|
},
|
|
returns: v.id("research"),
|
|
handler: async (ctx, args) => {
|
|
// Create research record
|
|
const researchId = await ctx.runMutation(internal.workflows.createResearch, {
|
|
topic: args.topic,
|
|
userId: args.userId,
|
|
status: "searching",
|
|
});
|
|
|
|
// Step 1: Search for relevant documents
|
|
const searchResults = await agent.search(ctx, {
|
|
query: args.topic,
|
|
table: "documents",
|
|
limit: 10,
|
|
});
|
|
|
|
await ctx.runMutation(internal.workflows.updateStatus, {
|
|
researchId,
|
|
status: "analyzing",
|
|
});
|
|
|
|
// Step 2: Analyze and synthesize
|
|
const analysis = await agent.chat(ctx, {
|
|
messages: [{
|
|
role: "user",
|
|
content: `Analyze these sources about "${args.topic}" and provide a comprehensive summary:\n\n${
|
|
searchResults.map((r) => r.content).join("\n\n---\n\n")
|
|
}`,
|
|
}],
|
|
systemPrompt: "You are a research assistant. Provide thorough, well-cited analysis.",
|
|
});
|
|
|
|
// Step 3: Generate key insights
|
|
await ctx.runMutation(internal.workflows.updateStatus, {
|
|
researchId,
|
|
status: "summarizing",
|
|
});
|
|
|
|
const insights = await agent.chat(ctx, {
|
|
messages: [{
|
|
role: "user",
|
|
content: `Based on this analysis, list 5 key insights:\n\n${analysis.content}`,
|
|
}],
|
|
});
|
|
|
|
// Save final results
|
|
await ctx.runMutation(internal.workflows.completeResearch, {
|
|
researchId,
|
|
analysis: analysis.content,
|
|
insights: insights.content,
|
|
sources: searchResults.map((r) => r._id),
|
|
});
|
|
|
|
return researchId;
|
|
},
|
|
});
|
|
```
|
|
|
|
## Examples
|
|
|
|
### Complete Chat Application Schema
|
|
|
|
```typescript
|
|
// convex/schema.ts
|
|
import { defineSchema, defineTable } from "convex/server";
|
|
import { v } from "convex/values";
|
|
|
|
export default defineSchema({
|
|
threads: defineTable({
|
|
userId: v.id("users"),
|
|
title: v.string(),
|
|
lastMessageAt: v.optional(v.number()),
|
|
metadata: v.optional(v.any()),
|
|
}).index("by_user", ["userId"]),
|
|
|
|
messages: defineTable({
|
|
threadId: v.id("threads"),
|
|
role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")),
|
|
content: v.string(),
|
|
toolCalls: v.optional(v.array(v.object({
|
|
name: v.string(),
|
|
arguments: v.any(),
|
|
result: v.optional(v.any()),
|
|
}))),
|
|
createdAt: v.number(),
|
|
}).index("by_thread", ["threadId"]),
|
|
|
|
documents: defineTable({
|
|
title: v.string(),
|
|
content: v.string(),
|
|
embedding: v.array(v.float64()),
|
|
metadata: v.object({
|
|
source: v.optional(v.string()),
|
|
category: v.optional(v.string()),
|
|
}),
|
|
createdAt: v.number(),
|
|
}).vectorIndex("by_embedding", {
|
|
vectorField: "embedding",
|
|
dimensions: 1536,
|
|
}),
|
|
});
|
|
```
|
|
|
|
### React Chat Component
|
|
|
|
```typescript
|
|
import { useQuery, useMutation, useAction } from "convex/react";
|
|
import { api } from "../convex/_generated/api";
|
|
import { useState, useRef, useEffect } from "react";
|
|
|
|
function ChatInterface({ threadId }: { threadId: Id<"threads"> }) {
|
|
const messages = useQuery(api.threads.getMessages, { threadId });
|
|
const sendMessage = useAction(api.chat.sendMessage);
|
|
const [input, setInput] = useState("");
|
|
const [sending, setSending] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [messages]);
|
|
|
|
const handleSend = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!input.trim() || sending) return;
|
|
|
|
const message = input.trim();
|
|
setInput("");
|
|
setSending(true);
|
|
|
|
try {
|
|
await sendMessage({ threadId, message });
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="chat-container">
|
|
<div className="messages">
|
|
{messages?.map((msg, i) => (
|
|
<div key={i} className={`message ${msg.role}`}>
|
|
<strong>{msg.role === "user" ? "You" : "Assistant"}:</strong>
|
|
<p>{msg.content}</p>
|
|
</div>
|
|
))}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
<form onSubmit={handleSend} className="input-form">
|
|
<input
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
placeholder="Type your message..."
|
|
disabled={sending}
|
|
/>
|
|
<button type="submit" disabled={sending || !input.trim()}>
|
|
{sending ? "Sending..." : "Send"}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
- Never run `npx convex deploy` unless explicitly instructed
|
|
- Never run any git commands unless explicitly instructed
|
|
- Store conversation history in Convex for persistence
|
|
- Use streaming for better user experience with long responses
|
|
- Implement proper error handling for tool failures
|
|
- Use vector indexes for efficient RAG retrieval
|
|
- Rate limit agent interactions to control costs
|
|
- Log tool usage for debugging and analytics
|
|
|
|
## Common Pitfalls
|
|
|
|
1. **Not persisting threads** - Conversations lost on refresh
|
|
2. **Blocking on long responses** - Use streaming instead
|
|
3. **Tool errors crashing agents** - Add proper error handling
|
|
4. **Large context windows** - Summarize old messages
|
|
5. **Missing embeddings for RAG** - Generate embeddings on insert
|
|
|
|
## References
|
|
|
|
- Convex Documentation: https://docs.convex.dev/
|
|
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
|
|
- Convex AI: https://docs.convex.dev/ai
|
|
- Agent Component: https://www.npmjs.com/package/@convex-dev/agent
|