AI agents are everywhere right now, and for good reason. They're transforming how we interact with software, moving beyond simple question-answer systems to tools that can actually do things for us.

From customer support bots that can process refunds to coding assistants that can fix bugs across multiple files, agents are becoming the new interface between humans and complex systems.

But here's the thing: most "agents" you see aren't really agents at all. They're just chatbots following a script. Today, we're going to build a real agent—one that can think, decide, and act on its own. And don't worry, we'll keep it simple so even beginners can easily follow along!

Let's dive in! But first,

What Makes a True AI Agent?

What do we mean by a "real agent"?

The key distinction between a true agent and a simple automation lies in autonomy and dynamic decision-making. Let's illustrate with an analogy:

Workflows are great for simple, well-defined tasks, but they often can't handle the complexity of very dynamic queries, unlike agents.

What we'll build

With the aid of the OpenAI SDK, we'll build a simple stock information agent that can answer questions about stocks and companies. It will be able to:

  1. Fetch real-time stock prices using the Yahoo Finance API
  2. Find company CEOs from stock data
  3. Identify ticker symbols from company names
  4. Ask for clarification when queries are ambiguous

What makes this a true agent is that it autonomously decides:

Prerequisites

Here's what you'll need:

Create a project directory and install the required packages:

mkdir stock-info-agent
cd stock-info-agent
pip install openai yfinance python-dotenv

Create a .env file in your project directory:

OPENAI_API_KEY=your_api_key_here

{/* */}

Building the Agent: A Step-by-Step Walkthrough

Let's build this agent from the ground up, understanding each component along the way.

1. Setting Up the Agent Class

First, we create our agent class with OpenAI integration:

import json
from typing import Optional, Dict, Any, List
from openai import OpenAI
import yfinance as yf
from dotenv import load_dotenv
import os

load_dotenv()

class StockInfoAgent:
    def __init__(self):
        self.client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
        self.conversation_history = []

The conversation_history is crucial – it allows our agent to maintain context across multiple interactions, understanding, for example, that "their CEO" refers to the company discussed earlier.

2. Creating the Tools

Our agent needs tools to interact with the real world. Let's create them:

Stock Price Tool

def get_stock_price(self, ticker_symbol: str) -> Optional[str]:
    """Fetches the current stock price for the given ticker_symbol."""
    try:
        stock = yf.Ticker(ticker_symbol.upper())
        info = stock.info
        current_price = info.get('currentPrice') or info.get('regularMarketPrice')
        
        if current_price:
            return f"{current_price:.2f} USD"
        return None
    except Exception as e:
        print(f"Error fetching stock price: {e}")
        return None

This tool uses Yahoo Finance to fetch real-time stock prices. Notice the error handling—robust agents must handle failures gracefully.

CEO Finder Tool

def get_company_ceo(self, ticker_symbol: str) -> Optional[str]:
    """Fetches the name of the CEO for the company associated with the ticker_symbol."""
    try:
        stock = yf.Ticker(ticker_symbol.upper())
        info = stock.info
        
        # Look for CEO in various possible fields
        ceo = None
        for field in ['companyOfficers', 'officers']:
            if field in info:
                officers = info[field]
                if isinstance(officers, list):
                    for officer in officers:
                        if isinstance(officer, dict):
                            title = officer.get('title', '').lower()
                            if 'ceo' in title or 'chief executive' in title:
                                ceo = officer.get('name')
                                break
        return ceo
    except Exception as e:
        print(f"Error fetching CEO info: {e}")
        return None

This tool searches through different data structures to find the CEO, as Yahoo Finance doesn't have a standardized format.

Ticker Symbol Finder Tool

This tool is crucial for handling natural language queries about companies:

def find_ticker_symbol(self, company_name: str) -> Optional[str]:
    """Tries to identify the stock ticker symbol for a given company_name."""
    try:
        # Use yfinance Lookup to search for the company
        lookup = yf.Lookup(company_name)
        
        stock_results = lookup.get_stock(count=5)
        
        if not stock_results.empty:
            return stock_results.index[0]
        
        # If no stocks found, try all instruments
        all_results = lookup.get_all(count=5)
        
        if not all_results.empty:
            return all_results.index[0]
            
    except Exception as e:
        print(f"Error searching for ticker: {e}")
    
    return None

This tool allows users to refer to companies by name ("Apple", "Tesla", "that EV company") instead of needing to know ticker symbols.

The Clarification Tool

This is what makes our agent truly interactive, allowing it to ask the user for clarification when needed.

def ask_user_for_clarification(self, question_to_user: str) -> str:
    """Poses the question_to_user to the actual user and returns their typed response."""
    print(f"\nAgent needs clarification: {question_to_user}")
    response = input("Your response: ")
    return response

3. Defining Tools for OpenAI

The agent needs to know what tools are available and how to use them. We define them in OpenAI's function calling format:

def create_tool_definitions(self) -> List[Dict[str, Any]]:
    """Creates OpenAI function calling definitions for the tools."""
    return [
        {
            "type": "function",
            "function": {
                "name": "get_stock_price",
                "description": "Fetches the current stock price for the given ticker symbol",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "ticker_symbol": {
                            "type": "string",
                            "description": "The stock ticker symbol (e.g., 'AAPL', 'MSFT')"
                        }
                    },
                    "required": ["ticker_symbol"]
                }
            }
        },
        # ... (other tool definitions)
    ]

These definitions are like instruction manuals for the AI—they tell it what each tool does and what parameters it needs.

4. The Brain: Processing User Queries

Here's where the magic happens—the agent's decision-making loop:

def process_user_query(self, user_query: str) -> str:
    """Processes a user query using the OpenAI API with function calling."""
    self.conversation_history.append({"role": "user", "content": user_query})
    
    system_prompt = """You are a helpful stock information assistant. You have access to tools that can:
                    1. Get current stock prices
                    2. Find company CEOs
                    3. Find ticker symbols for company names
                    4. Ask users for clarification when needed

                    Use these tools to help answer user questions about stocks and companies. 
                    If information is ambiguous, ask for clarification."""
    
    while True:
        messages = [
            {"role": "system", "content": system_prompt},
            *self.conversation_history
        ]
        
        # Call OpenAI API with function calling
        response = self.client.chat.completions.create(
            model="gpt-4-turbo-preview",
            messages=messages,
            tools=self.create_tool_definitions(),
            tool_choice="auto"  # Let the model decide which tool to use
        )
        
        response_message = response.choices[0].message
        
        # If no tool calls, we're done
        if not response_message.tool_calls:
            self.conversation_history.append({"role": "assistant", "content": response_message.content})
            return response_message.content
        
        # Execute the tool the agent chose
        tool_call = response_message.tool_calls[0]
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)
        
        print(f"\nExecuting tool: {function_name} with args: {function_args}")
        
        # Execute the tool
        result = self.execute_tool(function_name, function_args)
        
        # Add everything to conversation history
        self.conversation_history.append({
            "role": "assistant",
            "content": None,
            "tool_calls": [{
                "id": tool_call.id,
                "type": "function",
                "function": {
                    "name": function_name,
                    "arguments": json.dumps(function_args)
                }
            }]
        })
        
        self.conversation_history.append({
            "tool_call_id": tool_call.id,
            "role": "tool",
            "name": function_name,
            "content": str(result) if result is not None else "No result found"
        })

The key insight here is the while True loop—the agent keeps thinking and acting until it has a complete answer. It might use one tool, or five, or ask for clarification multiple times. This is true autonomy.

See Our Agent In Action

Here's a real conversation that demonstrates our agent's capabilities very well:

You: Who is the CEO of the EV company from China and what is its stock price?

Executing tool: ask_user_for_clarification with args: {'question_to_user': 'Are you referring to NIO, XPeng, or another Chinese EV company?'}
Agent needs clarification: Are you referring to NIO, XPeng, or another Chinese EV company?

Your response: BYD

Executing tool: find_ticker_symbol with args: {'company_name': 'BYD'}
Executing tool: get_company_ceo with args: {'ticker_symbol': 'BYDDF'}
Executing tool: get_stock_price with args: {'ticker_symbol': 'BYDDF'}

Agent: The CEO of BYD, the Chinese EV company, is Mr. Chuan-Fu Wang, and its current stock price is $59.50 USD.

The agent autonomously:

  1. Recognized "EV company from China" was ambiguous
  2. Asked which specific company
  3. Found the ticker symbol for BYD
  4. Retrieved the CEO information
  5. Fetched the current stock price
  6. Composed a complete answer

How to Use the Agent

Running the agent is simple, navigate to the project directory and run the following command:

python stock_agent.py

Try these example queries to see different behaviors:

Simple query:

You: What's Apple's stock price?
Agent: Apple's current stock price is $182.63 USD.

Ambiguous query requiring clarification:

You: Who runs that big EV company?
Agent: Are you asking about the CEO of Tesla?
You: Yes
Agent: The CEO of Tesla is Mr. Elon R. Musk.

Complex multi-tool query:

You: Compare the stock prices of Microsoft and Apple
Agent: Microsoft (MSFT) is currently trading at $415.26 USD, while Apple (AAPL) is trading at $182.63 USD.

Extending Your Agent

The modular design makes it easy to add new capabilities:

Add Market Analysis

def get_price_change(self, ticker_symbol: str, period: str = "1d") -> Dict[str, Any]:
    """Get price change over a period"""
    stock = yf.Ticker(ticker_symbol)
    hist = stock.history(period=period)
    if len(hist) >= 2:
        start_price = hist['Close'].iloc[0]
        end_price = hist['Close'].iloc[-1]
        change = end_price - start_price
        percent_change = (change / start_price) * 100
        return {
            "change": f"${change:.2f}",
            "percent": f"{percent_change:.2f}%"
        }

Add News Integration

def get_company_news(self, ticker_symbol: str) -> List[Dict[str, str]]:
    """Get recent news about the company"""
    stock = yf.Ticker(ticker_symbol)
    news = stock.news
    return [{"title": item['title'], "link": item['link']} for item in news[:5]]

Best Practices for Building Agents

  1. Clear Tool Descriptions: Write descriptions as if explaining to a colleague
  2. Graceful Error Handling: Always handle API failures and missing data
  3. Conversation Context: Maintain history for natural interactions
  4. User Transparency: Show which tools are being executed
  5. Start Simple: Add complexity only when needed

For more helpful tips, check out Anthropic's guide.

The Complete Code

<details> <summary>Click to expand the full implementation</summary>

import json
from typing import Optional, Dict, Any, List
from openai import OpenAI
import yfinance as yf
from dotenv import load_dotenv
import os

# Load environment variables
load_dotenv()

class StockInfoAgent:
    def __init__(self):
        self.client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
        self.conversation_history = []
        
    def get_stock_price(self, ticker_symbol: str) -> Optional[str]:
        """Fetches the current stock price for the given ticker_symbol."""
        try:
            stock = yf.Ticker(ticker_symbol.upper())
            info = stock.info
            current_price = info.get('currentPrice') or info.get('regularMarketPrice')
            
            if current_price:
                return f"{current_price:.2f} USD"
            return None
        except Exception as e:
            print(f"Error fetching stock price: {e}")
            return None
    
    def get_company_ceo(self, ticker_symbol: str) -> Optional[str]:
        """Fetches the name of the CEO for the company associated with the ticker_symbol."""
        try:
            stock = yf.Ticker(ticker_symbol.upper())
            info = stock.info
            
            # Look for CEO in various possible fields
            ceo = None
            for field in ['companyOfficers', 'officers']:
                if field in info:
                    officers = info[field]
                    if isinstance(officers, list):
                        for officer in officers:
                            if isinstance(officer, dict):
                                title = officer.get('title', '').lower()
                                if 'ceo' in title or 'chief executive' in title:
                                    ceo = officer.get('name')
                                    break
            
            # Fallback to general company info
            if not ceo and 'longBusinessSummary' in info:
                ceo = None  
                
            return ceo
        except Exception as e:
            print(f"Error fetching CEO info: {e}")
            return None
    
    def find_ticker_symbol(self, company_name: str) -> Optional[str]:
        """Tries to identify the stock ticker symbol for a given company_name."""
        try:
            # Use yfinance Lookup to search for the company
            lookup = yf.Lookup(company_name)
            
            stock_results = lookup.get_stock(count=5)
            
            if not stock_results.empty:
                return stock_results.index[0]
            
            # If no stocks found, try all instruments
            all_results = lookup.get_all(count=5)
            
            if not all_results.empty:
                return all_results.index[0]
                
        except Exception as e:
            print(f"Error searching for ticker: {e}")
        
        return None
    
    def ask_user_for_clarification(self, question_to_user: str) -> str:
        """Poses the question_to_user to the actual user and returns their typed response."""
        print(f"\nAgent needs clarification: {question_to_user}")
        response = input("Your response: ")
        return response
    
    def create_tool_definitions(self) -> List[Dict[str, Any]]:
        """Creates OpenAI function calling definitions for the tools."""
        return [
            {
                "type": "function",
                "function": {
                    "name": "get_stock_price",
                    "description": "Fetches the current stock price for the given ticker symbol",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "ticker_symbol": {
                                "type": "string",
                                "description": "The stock ticker symbol (e.g., 'AAPL', 'MSFT')"
                            }
                        },
                        "required": ["ticker_symbol"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "get_company_ceo",
                    "description": "Fetches the name of the CEO for the company associated with the ticker symbol",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "ticker_symbol": {
                                "type": "string",
                                "description": "The stock ticker symbol"
                            }
                        },
                        "required": ["ticker_symbol"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "find_ticker_symbol",
                    "description": "Tries to identify the stock ticker symbol for a given company name",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "company_name": {
                                "type": "string",
                                "description": "The name of the company"
                            }
                        },
                        "required": ["company_name"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "ask_user_for_clarification",
                    "description": "Poses a question to the user and returns their response",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "question_to_user": {
                                "type": "string",
                                "description": "The question to ask the user"
                            }
                        },
                        "required": ["question_to_user"]
                    }
                }
            }
        ]
    
    def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
        """Executes the specified tool with given arguments."""
        if tool_name == "get_stock_price":
            return self.get_stock_price(arguments["ticker_symbol"])
        elif tool_name == "get_company_ceo":
            return self.get_company_ceo(arguments["ticker_symbol"])
        elif tool_name == "find_ticker_symbol":
            return self.find_ticker_symbol(arguments["company_name"])
        elif tool_name == "ask_user_for_clarification":
            return self.ask_user_for_clarification(arguments["question_to_user"])
        else:
            return None
    
    def process_user_query(self, user_query: str) -> str:
        """Processes a user query using the OpenAI API with function calling."""
        # Add user message to conversation history
        self.conversation_history.append({"role": "user", "content": user_query})
        
        system_prompt = """You are a helpful stock information assistant. You have access to tools that can:
                        1. Get current stock prices
                        2. Find company CEOs
                        3. Find ticker symbols for company names
                        4. Ask users for clarification when needed

                        Use these tools to help answer user questions about stocks and companies. If information is ambiguous, ask for clarification."""
        
        while True:
            messages = [
                {"role": "system", "content": system_prompt},
                *self.conversation_history
            ]
            
            # Call OpenAI API with function calling
            response = self.client.chat.completions.create(
                model="gpt-4-turbo-preview",
                messages=messages,
                tools=self.create_tool_definitions(),
                tool_choice="auto"
            )
            
            response_message = response.choices[0].message
            
            # If no tool calls, we're done
            if not response_message.tool_calls:
                self.conversation_history.append({"role": "assistant", "content": response_message.content})
                return response_message.content
            
            # Execute the first tool call
            tool_call = response_message.tool_calls[0]
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)
            
            print(f"\nExecuting tool: {function_name} with args: {function_args}")
            
            # Execute the tool
            result = self.execute_tool(function_name, function_args)
            
            # Add the assistant's message with tool calls to history
            self.conversation_history.append({
                "role": "assistant",
                "content": None,
                "tool_calls": [{
                    "id": tool_call.id,
                    "type": "function",
                    "function": {
                        "name": function_name,
                        "arguments": json.dumps(function_args)
                    }
                }]
            })
            
            # Add tool result to history
            self.conversation_history.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": str(result) if result is not None else "No result found"
            })
    
    def chat(self):
        """Interactive chat loop."""
        print("Stock Information Agent")
        print("Ask me about stock prices, company CEOs, or any stock-related questions!")
        print("Type 'quit' to exit.\n")
        
        while True:
            user_input = input("You: ")
            
            if user_input.lower() in ['quit', 'exit', 'bye']:
                print("Goodbye!")
                break
            
            try:
                response = self.process_user_query(user_input)
                print(f"\nAgent: {response}\n")
            except Exception as e:
                print(f"\nError: {e}\n")

if __name__ == "__main__":
    agent = StockInfoAgent()
    agent.chat()

</details>

Conclusion

Congratulations! You've built a true AI agent that can think, decide, and act autonomously. This isn't just a chatbot following a script—it's an intelligent system that can handle ambiguous queries, ask for clarification, and chain multiple tools together to solve complex problems.

The key takeaways:

With this foundation, you can build agents for any domain, from financial analysis to customer support to personal assistants. The possibilities are endless when you give AI the ability to think and act autonomously.

Best of luck, and we can't wait to see what you build!