AI Agent Development Guide

Learn to build powerful AI agents for specific tasks

Build Your First AI Agent

A step-by-step tutorial to create your own task-specific AI agent

Introduction

Welcome to this hands-on tutorial where you'll build your first AI agent from scratch. We'll create a research assistant agent that can search for information, summarize content, and answer questions based on retrieved data.

Prerequisites

Before starting this tutorial, make sure you've:

  • Set up your development environment (Python 3.9+)
  • Installed necessary packages (see Environment Setup)
  • Obtained API keys for your chosen LLM provider

What You'll Build

In this tutorial, you'll create a Research Assistant Agent with the following capabilities:

  • Answer questions using step-by-step reasoning
  • Access tools to search for information
  • Summarize and extract key information from text
  • Maintain context throughout a conversation
Research Assistant Agent Workflow

Research Assistant Agent workflow diagram

Agent Architecture Overview

Before we start coding, let's understand the architecture of our research assistant agent:

📋

1. Agent Core

The core of our agent is powered by a Large Language Model (LLM) and uses the ReAct framework (Reasoning + Acting) to make decisions and process information.

🔧

2. Tool Integration

Our agent will have access to tools like web search, text processing, and mathematical calculations to help accomplish tasks.

🧠

3. Memory System

A memory component will allow the agent to maintain context throughout the conversation and refer back to previous exchanges.

💬

4. User Interface

A simple command-line interface to interact with the agent, asking questions and receiving responses.

Framework Selection

For this tutorial, we'll use LangChain as our framework because of its extensive documentation, active community, and flexibility. The concepts you learn can be applied to other frameworks like AutoGen or LlamaIndex as well.

Step 1: Project Setup

Let's start by setting up our project structure. If you've followed the Environment Setup guide, you should already have a virtual environment configured.

Create Project Files

Create the following files in your project directory:

research_agent/
├── .env                  # Environment variables
├── main.py               # Entry point
├── agent.py              # Agent definition
├── tools.py              # Custom tools
├── memory.py             # Memory management
└── prompts.py            # Prompt templates

Set Up Environment Variables

Create a .env file with your API keys:

# API Keys
OPENAI_API_KEY=sk-your-openai-key-here
# Alternatively, use Anthropic
# ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here

# Optional: Set default model
DEFAULT_MODEL=gpt-4-turbo
# For Anthropic: DEFAULT_MODEL=claude-3-opus-20240229

Security Note

Never commit your .env file to version control. Add it to your .gitignore file to prevent accidental exposure of your API keys.

Step 2: Define Custom Tools

Let's create tools for our agent to use. Open tools.py and add the following code:

from langchain.tools import tool
import requests
import json
import math

@tool
def web_search(query: str) -> str:
    """Search the web for information on a given query.
    
    Args:
        query: The search query string
        
    Returns:
        A string containing search results
    """
    # This is a mock implementation - in a real app, you would use a 
    # search API like Google Custom Search, Bing, or DuckDuckGo
    return f"Mock search results for: {query}\n1. Result 1 about {query}\n2. Result 2 about {query}"

@tool
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression.
    
    Args:
        expression: The mathematical expression to evaluate
        
    Returns:
        The result of the calculation
    """
    try:
        # Using eval can be dangerous, but this is just for demonstration
        # In production, use a safer alternative like numexpr
        return str(eval(expression))
    except Exception as e:
        return f"Error calculating: {str(e)}"

@tool
def summarize_text(text: str, max_length: int = 100) -> str:
    """Summarize a piece of text.
    
    Args:
        text: The text to summarize
        max_length: The maximum length of the summary in words
        
    Returns:
        A summarized version of the text
    """
    # This would typically be implemented with an LLM API call
    # Here we're just doing a simple truncation for demonstration
    words = text.split()
    if len(words) <= max_length:
        return text
    return " ".join(words[:max_length]) + "..."

# Export the tools list for agent use
tools_list = [web_search, calculate, summarize_text]
import requests
import json
import math
from typing import Dict, Any

# Define tool schemas for Anthropic's Tool Use API
tools_definitions = [
    {
        "name": "web_search",
        "description": "Search the web for information on a given query",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query string"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "calculate",
        "description": "Evaluate a mathematical expression",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string", 
                    "description": "The mathematical expression to evaluate"
                }
            },
            "required": ["expression"]
        }
    },
    {
        "name": "summarize_text",
        "description": "Summarize a piece of text",
        "input_schema": {
            "type": "object",
            "properties": {
                "text": {
                    "type": "string",
                    "description": "The text to summarize"
                },
                "max_length": {
                    "type": "integer",
                    "description": "The maximum length of the summary in words",
                    "default": 100
                }
            },
            "required": ["text"]
        }
    }
]

# Tool implementations
def web_search(query: str) -> str:
    """Search the web for information on a given query."""
    # This is a mock implementation - in a real app, you would use a 
    # search API like Google Custom Search, Bing, or DuckDuckGo
    return f"Mock search results for: {query}\n1. Result 1 about {query}\n2. Result 2 about {query}"

def calculate(expression: str) -> str:
    """Evaluate a mathematical expression."""
    try:
        # Using eval can be dangerous, but this is just for demonstration
        # In production, use a safer alternative like numexpr
        return str(eval(expression))
    except Exception as e:
        return f"Error calculating: {str(e)}"

def summarize_text(text: str, max_length: int = 100) -> str:
    """Summarize a piece of text."""
    # This would typically be implemented with an LLM API call
    # Here we're just doing a simple truncation for demonstration
    words = text.split()
    if len(words) <= max_length:
        return text
    return " ".join(words[:max_length]) + "..."

# Function to execute tools based on tool call from Anthropic API
def execute_tool(tool_name: str, tool_params: Dict[str, Any]) -> str:
    """Execute the specified tool with the given parameters."""
    if tool_name == "web_search":
        return web_search(tool_params["query"])
    elif tool_name == "calculate":
        return calculate(tool_params["expression"])
    elif tool_name == "summarize_text":
        max_length = tool_params.get("max_length", 100)
        return summarize_text(tool_params["text"], max_length)
    else:
        return f"Tool {tool_name} not found"

About Mock Tools

For this tutorial, we're using mock implementations of our tools. In a production application, you would integrate with real APIs:

  • For web search: Google Custom Search API, Bing Search API, or SerpAPI
  • For summarization: Use the same LLM with specific prompting

Step 3: Set Up Memory

Next, let's implement memory functionality to enable our agent to remember previous interactions. Open memory.py and add the following code:

from langchain.memory import ConversationBufferMemory
from langchain.schema import HumanMessage, AIMessage

class AgentMemory:
    def __init__(self):
        self.memory = ConversationBufferMemory(return_messages=True)
    
    def add_user_message(self, message: str):
        """Add a user message to memory."""
        self.memory.chat_memory.add_user_message(message)
    
    def add_ai_message(self, message: str):
        """Add an AI message to memory."""
        self.memory.chat_memory.add_ai_message(message)
    
    def get_chat_history(self) -> str:
        """Return chat history as a formatted string."""
        messages = self.memory.chat_memory.messages
        history = ""
        
        for message in messages:
            if isinstance(message, HumanMessage):
                history += f"Human: {message.content}\n"
            elif isinstance(message, AIMessage):
                history += f"AI: {message.content}\n"
        
        return history
    
    def clear(self):
        """Clear the memory."""
        self.memory.chat_memory.clear()
from typing import List, Dict, Any

class Message:
    def __init__(self, role: str, content: str):
        self.role = role
        self.content = content
    
    def to_dict(self) -> Dict[str, str]:
        return {
            "role": self.role, 
            "content": content
        }

class AgentMemory:
    def __init__(self):
        self.messages: List[Dict[str, str]] = []
    
    def add_user_message(self, message: str):
        """Add a user message to memory."""
        self.messages.append({"role": "user", "content": message})
    
    def add_ai_message(self, message: str):
        """Add an AI message to memory."""
        self.messages.append({"role": "assistant", "content": message})
    
    def get_messages(self) -> List[Dict[str, str]]:
        """Return all messages in the format required by Anthropic's API."""
        return self.messages
    
    def get_chat_history(self) -> str:
        """Return chat history as a formatted string."""
        history = ""
        
        for message in self.messages:
            if message["role"] == "user":
                history += f"Human: {message['content']}\n"
            elif message["role"] == "assistant":
                history += f"AI: {message['content']}\n"
        
        return history
    
    def clear(self):
        """Clear the memory."""
        self.messages = []

Memory Types

We're using a simple buffer memory that stores all conversation turns. For more advanced applications, consider:

  • Summary Memory: Summarizes past conversations to save context window space
  • Vector Memory: Stores embeddings of messages for semantic retrieval
  • Entity Memory: Tracks specific entities mentioned in the conversation

Step 4: Create Prompt Templates

Now, let's define the prompt templates for our agent. Open prompts.py and add:

from langchain.prompts import ChatPromptTemplate

RESEARCH_AGENT_PROMPT = ChatPromptTemplate.from_template("""
You are an advanced AI research assistant designed to help users find and analyze information.
You have access to several tools that you can use to assist users:

1. web_search: Search the web for up-to-date information
2. calculate: Perform mathematical calculations
3. summarize_text: Summarize long pieces of text

When responding to user questions:
1. Think step-by-step about what information is needed
2. Use the appropriate tools to gather necessary information
3. Provide comprehensive, accurate answers with appropriate detail
4. Cite your sources when you provide information from searches

Previous conversation history:
{chat_history}

Current User: {input}

Remember to use tools when necessary to answer the user's question accurately.
""")

SYSTEM_PROMPT = """You are an advanced AI research assistant designed to help users find and analyze information.
You have access to several tools that you can use to assist users.
Always think step-by-step and use the appropriate tools when needed to provide accurate information."""
# Define the system prompt for the research assistant agent
SYSTEM_PROMPT = """You are an advanced AI research assistant designed to help users find and analyze information.
You have access to several tools that you can use to assist users:

1. web_search: Search the web for up-to-date information
2. calculate: Perform mathematical calculations
3. summarize_text: Summarize long pieces of text

When responding to user questions:
1. Think step-by-step about what information is needed
2. Use the appropriate tools to gather necessary information
3. Provide comprehensive, accurate answers with appropriate detail
4. Cite your sources when you provide information from searches
"""

def format_chat_history(chat_history: str) -> str:
    """Format chat history to be included in the prompt."""
    if not chat_history:
        return "No previous conversation."
    
    return f"""Previous conversation history:
{chat_history}"""

Prompt Engineering Tip

When designing prompts for agents with tools:

  • Clearly define the agent's role and capabilities
  • List available tools with clear descriptions
  • Provide guidelines for when to use each tool
  • Include examples of appropriate tool usage if possible

Step 5: Implement the Agent

Now, let's create the main agent implementation. Open agent.py and add the following code:

import os
from langchain.chains import create_structured_output_runnable
from langchain.chat_models import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain.schema import HumanMessage

from tools import tools_list
from memory import AgentMemory
from prompts import RESEARCH_AGENT_PROMPT, SYSTEM_PROMPT

class ResearchAgent:
    def __init__(self, model_name=None):
        # Load API key from environment
        api_key = os.getenv("OPENAI_API_KEY")
        if not api_key:
            raise ValueError("OPENAI_API_KEY not found in environment variables")
        
        # Set model name
        self.model_name = model_name or os.getenv("DEFAULT_MODEL", "gpt-4-turbo")
        
        # Initialize LLM
        self.llm = ChatOpenAI(
            model=self.model_name,
            temperature=0.2,
            api_key=api_key
        )
        
        # Initialize memory
        self.memory = AgentMemory()
        
        # Initialize agent with tools
        self.agent_executor = create_tool_calling_agent(
            llm=self.llm,
            tools=tools_list,
            prompt=RESEARCH_AGENT_PROMPT,
            system_message=SYSTEM_PROMPT
        )
        
        self.executor = AgentExecutor(
            agent=self.agent_executor,
            tools=tools_list,
            verbose=True
        )
    
    async def process_query(self, query: str) -> str:
        """Process a user query and return the agent's response."""
        # Add user query to memory
        self.memory.add_user_message(query)
        
        # Get chat history for context
        chat_history = self.memory.get_chat_history()
        
        # Execute agent with the query and chat history
        response = await self.executor.invoke({
            "input": query,
            "chat_history": chat_history
        })
        
        # Extract response
        output = response["output"]
        
        # Add agent response to memory
        self.memory.add_ai_message(output)
        
        return output
import os
import json
import requests
from typing import Dict, Any, List

from tools import tools_definitions, execute_tool
from memory import AgentMemory
from prompts import SYSTEM_PROMPT, format_chat_history

class ResearchAgent:
    def __init__(self, model_name=None):
        # Load API key from environment
        self.api_key = os.getenv("ANTHROPIC_API_KEY")
        if not self.api_key:
            raise ValueError("ANTHROPIC_API_KEY not found in environment variables")
        
        # Set model name
        self.model_name = model_name or os.getenv("DEFAULT_MODEL", "claude-3-opus-20240229")
        
        # Initialize memory
        self.memory = AgentMemory()
        
        # API constants
        self.api_url = "https://api.anthropic.com/v1/messages"
        self.headers = {
            "Content-Type": "application/json",
            "x-api-key": self.api_key,
            "anthropic-version": "2023-06-01"
        }
    
    async def process_query(self, query: str) -> str:
        """Process a user query and return the agent's response."""
        # Add user query to memory
        self.memory.add_user_message(query)
        
        # Get chat history
        chat_history = self.memory.get_chat_history()
        formatted_history = format_chat_history(chat_history)
        
        # Create message payload
        messages = self.memory.get_messages()
        
        # Create API request data
        request_data = {
            "model": self.model_name,
            "max_tokens": 1024,
            "temperature": 0.2,
            "system": SYSTEM_PROMPT + "\n\n" + formatted_history,
            "messages": messages,
            "tools": tools_definitions
        }
        
        # Make initial API call
        response = requests.post(
            self.api_url, 
            headers=self.headers,
            json=request_data
        )
        
        if response.status_code != 200:
            return f"Error: {response.status_code} - {response.text}"
        
        response_data = response.json()
        
        # Check if the model wants to use tools
        content = response_data["content"]
        final_response = ""
        
        # Process any tool calls
        if response_data.get("tool_calls"):
            # Handle tool calls and create a follow-up message
            tool_calls = response_data["tool_calls"]
            tool_results = []
            
            for tool_call in tool_calls:
                tool_name = tool_call["name"]
                tool_params = json.loads(tool_call["input"])
                tool_result = execute_tool(tool_name, tool_params)
                tool_results.append({
                    "role": "tool",
                    "tool_call_id": tool_call["id"],
                    "name": tool_name,
                    "content": tool_result
                })
            
            # Add tool responses to messages
            messages.extend(tool_results)
            
            # Make a follow-up API call with tool results
            follow_up_request = {
                "model": self.model_name,
                "max_tokens": 1024,
                "temperature": 0.2,
                "system": SYSTEM_PROMPT,
                "messages": messages
            }
            
            follow_up_response = requests.post(
                self.api_url, 
                headers=self.headers,
                json=follow_up_request
            )
            
            if follow_up_response.status_code != 200:
                return f"Error in follow-up: {follow_up_response.status_code} - {follow_up_response.text}"
            
            follow_up_data = follow_up_response.json()
            final_response = follow_up_data["content"][0]["text"]
        else:
            # If no tool calls, use the direct response
            final_response = content[0]["text"]
        
        # Add agent response to memory
        self.memory.add_ai_message(final_response)
        
        return final_response

Model Selection

For this tutorial, we're using powerful language models:

  • GPT-4-turbo: Excellent reasoning and tool use capabilities (OpenAI)
  • Claude 3 Opus: Strong reasoning and follows instructions well (Anthropic)

Depending on your requirements, you might want to use more cost-effective models like GPT-3.5-turbo or Claude 3 Sonnet for less complex tasks.

Step 6: Create the Main Application

Finally, let's create our entry point script. Open main.py and add:

import os
import asyncio
from dotenv import load_dotenv
from agent import ResearchAgent

# Load environment variables
load_dotenv()

async def main():
    print("Starting Research Assistant Agent (LangChain version)...")
    
    # Initialize the agent
    agent = ResearchAgent()
    
    print("\nResearch Assistant is ready! Type 'exit' to quit.\n")
    
    while True:
        # Get user input
        user_input = input("You: ")
        
        # Exit condition
        if user_input.lower() in ["exit", "quit", "bye"]:
            print("Goodbye!")
            break
        
        try:
            # Process the query with LangChain agent
            response = await agent.process_query(user_input)
            print(f"\nAssistant: {response}\n")
        except Exception as e:
            print(f"Error processing with LangChain agent: {str(e)}")

if __name__ == "__main__":
    # Run the main function
    asyncio.run(main())
import os
import asyncio
from dotenv import load_dotenv
from agent import ResearchAgent

# Load environment variables
load_dotenv()

async def main():
    print("Starting Research Assistant Agent (Anthropic Claude version)...")
    
    try:
        # Initialize the Anthropic-based agent
        agent = ResearchAgent()
        
        print("\nResearch Assistant is ready! Type 'exit' to quit.\n")
        
        while True:
            # Get user input
            user_input = input("You: ")
            
            # Exit condition
            if user_input.lower() in ["exit", "quit", "bye"]:
                print("Goodbye!")
                break
            
            try:
                # Process the query with Anthropic-based agent
                response = await agent.process_query(user_input)
                print(f"\nAssistant: {response}\n")
            except Exception as e:
                print(f"Error in Claude API request: {str(e)}")
    except ValueError as e:
        # Handle missing API key error
        print(f"Configuration error: {str(e)}")
        print("Please make sure ANTHROPIC_API_KEY is set in your .env file")

if __name__ == "__main__":
    # Run the main function
    asyncio.run(main())

Running the Application

To run your research assistant, navigate to your project directory and execute:

python main.py

Testing Your Agent

Let's test our research assistant with a few example queries to see it in action:

Example 1: Basic Information Query

User Query

This tests the agent's ability to search for information:

What are the key features of LangChain?

Example 2: Calculation Query

User Query

This tests the agent's ability to perform calculations:

If I invest $1000 with an annual interest rate of 7% compounded monthly, how much will I have after 10 years?

Example 3: Multi-step Query

User Query

This tests the agent's ability to combine multiple tools and reasoning:

Find information about the latest developments in quantum computing and summarize it in 3 key points.

Mock Tools Reminder

Remember that we're using mock implementations of our tools in this tutorial. In a real application, you would replace these with actual API calls to search engines, LLM summarization endpoints, etc.

Next Steps and Enhancements

Now that you've built a basic research assistant agent, here are some ways to enhance it:

Improve Tool Integration

  • Replace mock tools with real API integrations (Google Search, DuckDuckGo, etc.)
  • Add more specialized tools like PDF processing, data analysis, or image generation
  • Implement error handling and retry mechanisms for tool calls

Enhance Memory and Context

  • Implement vector-based memory for semantic search of past conversations
  • Add knowledge graph capabilities to track entities and relationships
  • Implement token-aware context management to handle long conversations

User Experience Improvements

  • Build a web interface using Flask or Streamlit
  • Add progress indicators for long-running operations
  • Implement voice input/output for a conversational interface

Performance Optimization

  • Implement caching for frequently requested information
  • Add rate limiting and concurrency control for API calls
  • Monitor and optimize token usage to reduce costs

Try It Yourself

Challenge yourself to implement one of these enhancements to your research assistant agent:

  • Add a real search API integration
  • Implement a simple web UI using Flask or Streamlit
  • Add a new tool like weather information or news retrieval
  • Enhance the prompt to improve the agent's reasoning capabilities

Conclusion

🎯
🏆

Congratulations!

You've successfully built your first AI agent - a research assistant that can search for information, perform calculations, and summarize text.

What You've Learned

🏗️
How to structure an AI agent project
🔧
How to integrate tools with an LLM
🧠
How to implement memory for context persistence
💬
How to craft effective prompts for agent behavior
👥
How to handle user input and agent responses

Key Focus Areas for Future Development

Clear task definition

Define exactly what your agent should and shouldn't do

Thoughtful prompt engineering

Guide the agent's behavior with well-crafted instructions

Robust tool integration

Give your agent the capabilities it needs to succeed

User-centric design

Create agents that solve real problems for users

Level Up Your Agent

Take your agent beyond the basics. Integrate tools like memory, web search, and decision trees to supercharge its performance.

Add Features