Building the Agent Loop
The Complete Loop
Here’s the full agent loop in src/agent.ts. This is where everything comes together:
import OpenAI from 'openai';
import * as dotenv from 'dotenv';
import { addTodo, listTodos, completeTodo } from './tools';
dotenv.config();
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const SYSTEM_PROMPT = `You are a to-do list agent. You help users add, list, and complete tasks using the available tools.
When a user asks you to:
- Add a task: Use the addTodo tool
- List tasks: Use the listTodos tool
- Complete a task: Use the completeTodo tool
Always be helpful and confirm what you've done.`;
const tools = [
{
type: 'function' as const,
function: {
name: 'addTodo',
description: 'Add a new todo item to the list',
parameters: {
type: 'object',
properties: {
text: { type: 'string', description: 'The text of the todo item' },
dueDate: { type: 'string', description: 'Optional due date in YYYY-MM-DD format' },
},
required: ['text'],
},
},
},
{
type: 'function' as const,
function: {
name: 'listTodos',
description: 'List all todo items',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function' as const,
function: {
name: 'completeTodo',
description: 'Mark a todo item as completed',
parameters: {
type: 'object',
properties: {
id: { type: 'number', description: 'The ID of the todo to complete' },
},
required: ['id'],
},
},
},
];
async function runAgent(userMessage: string, maxSteps = 5) {
const messages: any[] = [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userMessage },
];
let steps = 0;
while (steps < maxSteps) {
steps++;
const response = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages,
tools,
tool_choice: 'auto',
});
const message = response.choices[0].message;
messages.push(message);
// If no tool calls, we're done
if (!message.tool_calls || message.tool_calls.length === 0) {
return message.content;
}
// Execute tool calls
for (const toolCall of message.tool_calls) {
const functionName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
console.log(`Calling tool: ${functionName} with args:`, args);
let result;
if (functionName === 'addTodo') {
result = addTodo(args.text, args.dueDate);
} else if (functionName === 'listTodos') {
result = listTodos();
} else if (functionName === 'completeTodo') {
result = completeTodo(args.id);
} else {
result = { error: `Unknown function: ${functionName}` };
}
// Add tool result to messages
messages.push({
role: 'tool',
content: JSON.stringify(result),
tool_call_id: toolCall.id,
});
}
}
return 'Agent reached max steps limit.';
}
// Test it
async function main() {
const response = await runAgent('I have a meeting with Ali tomorrow at 10, add it');
console.log('Agent response:', response);
}
main().catch(console.error);
Understanding the Loop
- Start with system prompt and user message
- Call the model with tools
- Check for tool calls - if none, return the response
- Execute tools and add results to messages
- Loop back to step 2 (with a max steps limit)
Try It
Run the agent:
npx ts-node src/agent.ts
You should see:
- Tool calls being logged
- The agent’s final response
Try different messages:
- “Add a task to buy groceries”
- “List all my tasks”
- “Complete task 1”
Adding a Simple Web UI
Create public/index.html:
<!DOCTYPE html>
<html>
<head>
<title>To-Do Agent</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.chat-panel {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
.tools-panel {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
#messages {
height: 400px;
overflow-y: auto;
margin-bottom: 1rem;
padding: 1rem;
background: #f5f5f5;
border-radius: 4px;
}
.message {
margin-bottom: 0.5rem;
padding: 0.5rem;
background: white;
border-radius: 4px;
}
.user-message {
background: #e3f2fd;
}
.agent-message {
background: #f1f8e9;
}
#input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#toolsLog {
height: 400px;
overflow-y: auto;
padding: 1rem;
background: #f5f5f5;
border-radius: 4px;
font-family: monospace;
font-size: 0.875rem;
}
</style>
</head>
<body>
<div class="chat-panel">
<h2>Chat</h2>
<div id="messages"></div>
<input type="text" id="input" placeholder="Type your message...">
<button onclick="sendMessage()">Send</button>
</div>
<div class="tools-panel">
<h2>Tools Log</h2>
<div id="toolsLog"></div>
</div>
<script>
async function sendMessage() {
const input = document.getElementById('input');
const message = input.value;
if (!message) return;
// Add user message to chat
const messagesDiv = document.getElementById('messages');
const userMsg = document.createElement('div');
userMsg.className = 'message user-message';
userMsg.textContent = `You: ${message}`;
messagesDiv.appendChild(userMsg);
input.value = '';
try {
const response = await fetch('/api/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
const data = await response.json();
// Add agent response
const agentMsg = document.createElement('div');
agentMsg.className = 'message agent-message';
agentMsg.textContent = `Agent: ${data.response}`;
messagesDiv.appendChild(agentMsg);
// Update tools log
if (data.toolsLog) {
const toolsLog = document.getElementById('toolsLog');
const logEntry = document.createElement('div');
logEntry.textContent = JSON.stringify(data.toolsLog, null, 2);
toolsLog.appendChild(logEntry);
}
} catch (error) {
console.error('Error:', error);
}
}
document.getElementById('input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
You’ll need a simple Express server to serve this and handle the API. But the core agent is done!
Progress 88%
Page 7 of 8
← Previous
→ Next