What We Are Building
In this tutorial, we will build a personal productivity agent that can:
By the end, you will have a working agent deployed on Vercel that you can extend with your own tools.
Prerequisites
Step 1: Project Setup
Create a new Next.js project:
npx create-next-app@latest ai-agent-demo --typescript --tailwind --app
cd ai-agent-demoInstall the AI SDK:
pnpm add ai @ai-sdk/openai zodStep 2: Define Your Tools
Create `lib/tools.ts`:
import { tool } from 'ai';
import { z } from 'zod';
// Web search tool
export const searchWeb = tool({
description: 'Search the web for current information. Use this when you need up-to-date data or facts you are not certain about.',
parameters: z.object({
query: z.string().describe('The search query'),
}),
execute: async ({ query }) => {
// Using a simple search API (replace with your preferred provider)
const response = await fetch(
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}`,
{
headers: {
'X-Subscription-Token': process.env.BRAVE_SEARCH_API_KEY!,
},
}
);
const data = await response.json();
// Return top 3 results
return data.web.results.slice(0, 3).map((r: any) => ({
title: r.title,
description: r.description,
url: r.url,
}));
},
});
// Todo management tool
export const manageTodo = tool({
description: 'Create, list, or complete todo items. Use this to help the user manage their tasks.',
parameters: z.object({
action: z.enum(['create', 'list', 'complete']).describe('The action to perform'),
title: z.string().optional().describe('Todo title (for create action)'),
todoId: z.string().optional().describe('Todo ID (for complete action)'),
}),
execute: async ({ action, title, todoId }) => {
// In production, use a database. For demo, use in-memory store.
const todos = globalThis.todos || [];
switch (action) {
case 'create':
const newTodo = { id: Date.now().toString(), title, completed: false };
todos.push(newTodo);
globalThis.todos = todos;
return { success: true, todo: newTodo };
case 'list':
return { todos: todos };
case 'complete':
const todo = todos.find((t: any) => t.id === todoId);
if (todo) {
todo.completed = true;
return { success: true, todo };
}
return { success: false, error: 'Todo not found' };
}
},
});
// Email notification tool
export const sendNotification = tool({
description: 'Send an email notification to the user. Use sparingly and only when explicitly requested.',
parameters: z.object({
subject: z.string().describe('Email subject'),
body: z.string().describe('Email body content'),
}),
execute: async ({ subject, body }) => {
// Using Resend (replace with your email provider)
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'agent@yourdomain.com',
to: process.env.USER_EMAIL,
subject,
text: body,
}),
});
if (response.ok) {
return { success: true, message: 'Email sent successfully' };
}
return { success: false, error: 'Failed to send email' };
},
});
// Calculator tool
export const calculate = tool({
description: 'Perform mathematical calculations. Use for any math operations.',
parameters: z.object({
expression: z.string().describe('Mathematical expression (e.g., "2 + 2", "sqrt(16)", "15% of 200")'),
}),
execute: async ({ expression }) => {
try {
// Parse and evaluate safely (use mathjs in production)
const sanitized = expression
.replace(/sqrt/g, 'Math.sqrt')
.replace(/pow/g, 'Math.pow')
.replace(/(\d+)% of (\d+)/g, '($1/100) * $2');
const result = Function('return ' + sanitized)();
return { result, expression };
} catch (error) {
return { error: 'Could not evaluate expression' };
}
},
});
// Get current date/time tool
export const getCurrentTime = tool({
description: 'Get the current date and time. Use when the user asks about today, now, or current time.',
parameters: z.object({
timezone: z.string().optional().describe('Timezone (e.g., "America/New_York")'),
}),
execute: async ({ timezone }) => {
const now = new Date();
const options: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZone: timezone || 'UTC',
};
return {
formatted: now.toLocaleString('en-US', options),
iso: now.toISOString(),
timezone: timezone || 'UTC',
};
},
});Step 3: Create the Agent API Route
Create `app/api/agent/route.ts`:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import {
searchWeb,
manageTodo,
sendNotification,
calculate,
getCurrentTime,
} from '@/lib/tools';
export const maxDuration = 30; // Allow longer execution for multi-step tasks
const systemPrompt = `You are a helpful personal productivity assistant.
Your capabilities:
- Search the web for current information
- Manage todo lists (create, list, complete tasks)
- Send email notifications when requested
- Perform calculations
- Tell the current date and time
Guidelines:
1. Always use tools when you need real-time information - never make up data
2. For multi-step tasks, think through the steps and execute them one by one
3. Be concise but helpful in your responses
4. Only send notifications when explicitly asked
5. When listing todos, format them nicely
6. If a tool fails, explain what happened and suggest alternatives
Remember: You are an agent that ACTS, not just answers. When a user asks you to do something, do it using your tools.`;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: systemPrompt,
messages,
tools: {
searchWeb,
manageTodo,
sendNotification,
calculate,
getCurrentTime,
},
maxSteps: 5, // Allow up to 5 sequential tool calls
});
return result.toDataStreamResponse();
}Step 4: Build the Chat Interface
Create `app/page.tsx`:
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function AgentChat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/agent',
});
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
<header className="mb-4">
<h1 className="text-2xl font-bold">AI Agent Demo</h1>
<p className="text-gray-600">
I can search the web, manage todos, send notifications, and more.
</p>
</header>
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((message) => (
<div
key={message.id}
className={`p-4 rounded-lg ${
message.role === 'user'
? 'bg-blue-100 ml-12'
: 'bg-gray-100 mr-12'
}`}
>
<div className="font-semibold mb-1">
{message.role === 'user' ? 'You' : 'Agent'}
</div>
<div className="whitespace-pre-wrap">{message.content}</div>
{/* Show tool calls */}
{message.toolInvocations?.map((tool, i) => (
<div key={i} className="mt-2 p-2 bg-yellow-50 rounded text-sm">
<div className="font-mono text-yellow-800">
Tool: {tool.toolName}
</div>
{tool.state === 'result' && (
<pre className="mt-1 text-xs overflow-auto">
{JSON.stringify(tool.result, null, 2)}
</pre>
)}
</div>
))}
</div>
))}
{isLoading && (
<div className="p-4 bg-gray-100 rounded-lg mr-12 animate-pulse">
Agent is thinking...
</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask me anything or give me a task..."
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
Send
</button>
</form>
</div>
);
}Step 5: Add Environment Variables
Create `.env.local`:
OPENAI_API_KEY=your_openai_key
BRAVE_SEARCH_API_KEY=your_brave_search_key # Optional
RESEND_API_KEY=your_resend_key # Optional
USER_EMAIL=your@email.com # For notificationsStep 6: Test Your Agent
Start the development server:
pnpm devTry these prompts:
1. Simple tool use: "What time is it in Tokyo?"
2. Multi-step task: "Search for the latest Next.js features and create a todo to learn each one"
3. Calculation: "What is 15% of 2500?"
4. Todo management: "Add a todo to review the quarterly report"
5. Complex request: "Find the current Bitcoin price and if it is above $60,000, send me an email notification"
Step 7: Add Conversation Memory
For a production agent, you want to persist conversation history. Update your route:
import { kv } from '@vercel/kv'; // Add Vercel KV for persistence
export async function POST(req: Request) {
const { messages, sessionId } = await req.json();
// Load previous context if exists
const previousContext = await kv.get(`agent:${sessionId}:context`);
const result = streamText({
model: openai('gpt-4o'),
system: systemPrompt + (previousContext ? `\n\nPrevious context: ${previousContext}` : ''),
messages,
tools: { /* ... */ },
maxSteps: 5,
onFinish: async ({ text }) => {
// Save important context for future conversations
await kv.set(`agent:${sessionId}:context`, text.slice(0, 500), { ex: 86400 });
},
});
return result.toDataStreamResponse();
}Step 8: Deploy to Vercel
vercel deploySet your environment variables in the Vercel dashboard.
Advanced: Adding Human-in-the-Loop
For sensitive actions, require user confirmation:
export const sendNotification = tool({
description: 'Send an email notification. ALWAYS ask for confirmation before sending.',
parameters: z.object({
subject: z.string(),
body: z.string(),
confirmed: z.boolean().describe('Whether the user has confirmed sending'),
}),
execute: async ({ subject, body, confirmed }) => {
if (!confirmed) {
return {
requiresConfirmation: true,
preview: { subject, body },
message: 'Please confirm you want to send this email.',
};
}
// Actually send the email
// ...
},
});Advanced: Error Handling and Retries
export const searchWeb = tool({
description: 'Search the web',
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(/* ... */);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
lastError = error;
await new Promise(r => setTimeout(r, 1000 * (i + 1))); // Exponential backoff
}
}
return {
error: true,
message: `Search failed after ${maxRetries} attempts: ${lastError.message}`,
suggestion: 'Try rephrasing your search query or try again later.',
};
},
});What You Built
You now have a functional AI Agent that:
Next Steps
1. Add more tools: Database queries, file operations, API integrations
2. Implement RAG: Add vector search for document retrieval
3. Build specialized agents: Create focused agents for specific domains
4. Add authentication: Protect your agent with user authentication
5. Monitor and log: Track agent performance and errors
The agent paradigm is powerful because it is composable. Each tool you add multiplies the agent's capabilities. Start simple, ship fast, and iterate based on real usage.