3  Building Financial Tools

3.1 Learning Objectives

  • Understand how to integrate with external APIs in AI agents
  • Build and test tools for retrieving financial data
  • Implement logging for better debugging and visibility
  • Create a reusable tool structure for different agent implementations

3.2 The Role of Tools in AI Agents

Tools are the “Action” component in our ABC Framework. They allow our agent to interact with the external world, retrieve information, and perform tasks that the language model alone cannot do. For our financial assistant, we want to have tools that can:

  1. Retrieve current stock prices
  2. Get historical price data
  3. Fetch company information
  4. Access financial metrics

These tools will connect to Yahoo Finance’s API to get real-time financial data. Let’s start by setting up the necessary dependencies.

3.3 Setting Up Yahoo Finance Integration

First, we need to install the yfinance package, which provides a convenient Python interface to Yahoo Finance data. We should have already installed it when we set up our project, if not, we can do:

uv add yfinance

Now, let’s create our first tool file in the common directory:

touch src/common/tools_yf.py

Below is a sample implementation of our financial tools. It is perfectly fine to have AI coding assistant generate these functions for you as long as you verify them. Refer to the yfinance documentation for more details.

# src/common/tools_yf.py
import logging
import sys
import yfinance as yf
from datetime import datetime, timedelta

# Configure logger for this module
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

def get_stock_price(ticker: str) -> str:
    """
    Get the current price of a stock.

    Args:
        ticker: The stock ticker symbol (e.g., 'AAPL')

    Returns:
        A string with the current price of the stock
    """
    logger.info(f"Getting stock price for {ticker}")
    ticker = ticker.upper()

    try: 
        stock = yf.Ticker(ticker)
        info = stock.info

        hist = stock.history(period="1d")

        if hist.empty:
            return f"Could not retrieve stock price for {ticker}. Please check the ticker symbol."

        current_price = hist['Close'].iloc[-1]

        try:
            prev_close = info.get('previousClose', hist['Open'].iloc[-1])
            change = current_price - prev_close
            percent_change = (change / prev_close) * 100
        except (KeyError, IndexError):
            change = 0
            percent_change = 0
        
        # Get company name
        company_name = info.get('shortName', ticker)

        # Format with direction indicators
        direction = "up" if change > 0 else "down" if change < 0 else "unchanged"

        return f"{company_name} ({ticker}) is currently trading at ${current_price:.2f}, {direction} {abs(percent_change):.2f}% today."
    except Exception as e:
        logger.error(f"Error fetching stock price for {ticker}: {str(e)}", exc_info=True)
        return f"An error occurred while fetching the stock price for {ticker}: {str(e)}"

This first tool retrieves the current stock price for a given ticker symbol. Let’s continue by implementing the remaining tools:

def get_stock_history(ticker: str, days: int) -> str:
    """
    Get historical price data for a stock using Yahoo Finance.
    
    Args:
        ticker: The stock ticker symbol (e.g., 'AAPL')
        days: Number of days of history to retrieve (default: 7)
        
    Returns:
        A string with the historical price information
    """
    logger.info(f"Getting {days} days of stock history for {ticker}")
    ticker = ticker.upper()
    
    try:
        # Get ticker info from Yahoo Finance
        stock = yf.Ticker(ticker)
        
        # Get historical data for the specified period
        # Add a buffer to ensure we get enough business days
        buffer_days = max(5, int(days * 1.5))
        end_date = datetime.now()
        start_date = end_date - timedelta(days=buffer_days)
        
        hist = stock.history(start=start_date, end=end_date)
        
        if hist.empty:
            return f"Could not retrieve historical data for {ticker}."
        
        # Filter to the exact number of days requested
        hist = hist.tail(days)
        
        # Format the historical data
        history_lines = []
        for date, row in hist.iterrows():
            date_str = date.strftime("%Y-%m-%d")
            close_price = row['Close']
            history_lines.append(f"{date_str}: ${close_price:.2f}")
        
        # Get company name
        company_name = stock.info.get('shortName', ticker)
        
        result = f"Historical prices for {company_name} ({ticker}) - last {len(history_lines)} days:\n\n"
        result += "\n".join(history_lines)
        
        return result
        
    except Exception as e:
        logger.error(f"Error fetching stock history for {ticker}: {str(e)}", exc_info=True)
        return f"Error retrieving historical data for {ticker}: {str(e)}"


def get_company_info(ticker: str) -> str:
    """
    Get basic information about a company using Yahoo Finance.
    
    Args:
        ticker: The stock ticker symbol (e.g., 'AAPL')
        
    Returns:
        A string with the company information
    """
    logger.info(f"Getting company info for {ticker}")
    ticker = ticker.upper()
    
    try:
        # Get ticker info from Yahoo Finance
        stock = yf.Ticker(ticker)
        info = stock.info
        
        if not info:
            return f"Company information for {ticker} is not available."
        
        # Extract relevant company information
        company_name = info.get('shortName', ticker)
        sector = info.get('sector', 'Not available')
        industry = info.get('industry', 'Not available')
        description = info.get('longBusinessSummary', 'No description available')
        
        # Additional information if available
        website = info.get('website', 'Not available')
        employees = info.get('fullTimeEmployees', 'Not available')
        country = info.get('country', 'Not available')
        city = info.get('city', 'Not available')
        
        # Format the company profile
        result = f"Company Profile: {company_name} ({ticker})\n\n"
        result += f"Sector: {sector}\n"
        result += f"Industry: {industry}\n"
        result += f"Country: {country}\n"
        result += f"City: {city}\n"
        result += f"Website: {website}\n"
        result += f"Employees: {employees}\n\n"
        result += f"Description: {description}"
        
        return result
        
    except Exception as e:
        logger.error(f"Error fetching company info for {ticker}: {str(e)}", exc_info=True)
        return f"Error retrieving company information for {ticker}: {str(e)}"


def get_financial_metrics(ticker: str) -> str:
    """
    Get key financial metrics for a company using Yahoo Finance.
    
    Args:
        ticker: The stock ticker symbol (e.g., 'AAPL')
        
    Returns:
        A string with key financial metrics
    """
    logger.info(f"Getting financial metrics for {ticker}")
    ticker = ticker.upper()
    
    try:
        # Get ticker info from Yahoo Finance
        stock = yf.Ticker(ticker)
        info = stock.info
        
        if not info:
            return f"Financial metrics for {ticker} are not available."
        
        # Extract key financial metrics
        company_name = info.get('shortName', ticker)
        
        # Market data
        market_cap = info.get('marketCap', 'N/A')
        if isinstance(market_cap, (int, float)):
            market_cap = f"${market_cap / 1_000_000_000:.2f} billion"
            
        pe_ratio = info.get('trailingPE', 'N/A')
        if isinstance(pe_ratio, (int, float)):
            pe_ratio = f"{pe_ratio:.2f}"
            
        dividend_yield = info.get('dividendYield', 'N/A')
        if isinstance(dividend_yield, (int, float)):
            dividend_yield = f"{dividend_yield * 100:.2f}%"
            
        fifty_two_week_high = info.get('fiftyTwoWeekHigh', 'N/A')
        fifty_two_week_low = info.get('fiftyTwoWeekLow', 'N/A')
        
        # Financial metrics
        revenue = info.get('totalRevenue', 'N/A')
        if isinstance(revenue, (int, float)):
            revenue = f"${revenue / 1_000_000_000:.2f} billion"
            
        profit_margin = info.get('profitMargins', 'N/A')
        if isinstance(profit_margin, (int, float)):
            profit_margin = f"{profit_margin * 100:.2f}%"
            
        return_on_equity = info.get('returnOnEquity', 'N/A')
        if isinstance(return_on_equity, (int, float)):
            return_on_equity = f"{return_on_equity * 100:.2f}%"
        
        # Format the metrics
        result = f"Financial Metrics: {company_name} ({ticker})\n\n"
        result += f"Market Cap: {market_cap}\n"
        result += f"P/E Ratio: {pe_ratio}\n"
        result += f"Dividend Yield: {dividend_yield}\n"
        result += f"52-Week Range: ${fifty_two_week_low} - ${fifty_two_week_high}\n\n"
        result += f"Revenue: {revenue}\n"
        result += f"Profit Margin: {profit_margin}\n"
        result += f"Return on Equity: {return_on_equity}"
        
        return result
        
    except Exception as e:
        logger.error(f"Error fetching financial metrics for {ticker}: {str(e)}", exc_info=True)
        return f"Error retrieving financial metrics for {ticker}: {str(e)}" 

3.4 Enhancing Tools with Colorful Logging

To make our tools more user-friendly, let’s add colorful console output for logging. This will help users understand what the agent is doing when it calls these tools:

# Add this at the top of tools_yf.py, replacing the simple logger setup
# Set up a colorful console handler for nice log presentation
class ColoredFormatter(logging.Formatter):
    def format(self, record):
        if record.levelno == logging.INFO:
            # Colorize function calls
            if record.msg.startswith("Getting stock price"):
                return f"  🔍 {record.getMessage()}"
            elif record.msg.startswith("Getting stock history"):
                return f"  📈 {record.getMessage()}"
            elif record.msg.startswith("Getting company info"):
                return f"  🏢 {record.getMessage()}"
            elif record.msg.startswith("Getting financial metrics"):
                return f"  💰 {record.getMessage()}"
        return super().format(record)

# Configure logger for this module to output INFO level with color
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Add a special handler for colorful console output
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(ColoredFormatter('%(message)s'))
logger.addHandler(console_handler)

3.5 Testing Our Financial Tools

Before integrating these tools with our agent, let’s create a simple test script to make sure they work correctly. Create a file called test_tools.py in the tests directory:

# tests/test_tools.py
from src.common.tools_yf import (
    get_stock_price,
    get_stock_history,
    get_company_info,
    get_financial_metrics
)

# Test the stock price function
ticker = "TSLA"
print("\n--- Testing get_stock_price ---")
result = get_stock_price(ticker)
print(f"Result: {result}")

# Test the stock history function
print("\n--- Testing get_stock_history ---")
result = get_stock_history(ticker, 7)
print(f"Result: {result}")

# Test the company info function
print("\n--- Testing get_company_info ---")
result = get_company_info(ticker)
print(f"Result: {result}")

# Test the financial metrics function
print("\n--- Testing get_financial_metrics ---")
result = get_financial_metrics(ticker)
print(f"Result: {result}")

Run the test script to make sure our tools are working:

python test_tools.py

You should see output similar to this:

--- Testing get_stock_price ---
  🔍 Getting stock price for TSLA
Result: Tesla, Inc. (TSLA) is currently trading at $237.97, up 4.60% today.

--- Testing get_stock_history ---
  📈 Getting 7 days of stock history for TSLA
Result: Historical prices for Tesla, Inc. (TSLA) - last 7 days:

2025-04-15: $225.36
2025-04-16: $232.41
2025-04-17: $235.89
2025-04-18: $232.78
2025-04-21: $227.50
2025-04-22: $237.97

--- Testing get_company_info ---
  🏢 Getting company info for TSLA
Result: Company Profile: Tesla, Inc. (TSLA)

Sector: Consumer Cyclical
Industry: Auto Manufacturers
Country: United States
City: Austin
Website: https://www.tesla.com
Employees: 125665

Description: Tesla, Inc. designs, develops, manufactures, leases, and sells electric vehicles, and energy generation and storage systems in the United States, China, and internationally. The company operates in two segments, Automotive, and Energy Generation and Storage. The Automotive segment offers electric vehicles, as well as sells automotive regulatory credits. It provides sedans and sport utility vehicles through direct and used vehicle sales, a network of Tesla Superchargers, and in-app upgrades; and purchase financing and leasing services. This segment is also involved in the provision of non-warranty after-sales vehicle services, sale of used vehicles, retail merchandise, and vehicle insurance, as well as sale of products to third party customers; services for electric vehicles through its company-owned service locations, and Tesla mobile service technicians; and vehicle limited warranties and extended service plans. The Energy Generation and Storage segment engages in the design, manufacture, installation, sale, and leasing of solar energy generation and energy storage products, and related services to residential, commercial, and industrial customers and utilities through its website, stores, and galleries, as well as through a network of channel partners. This segment also offers service and repairs to its energy product customers, including under warranty; and various financing options to its solar customers. The company was formerly known as Tesla Motors, Inc. and changed its name to Tesla, Inc. in February 2017. Tesla, Inc. was incorporated in 2003 and is headquartered in Austin, Texas.

--- Testing get_financial_metrics ---
  💰 Getting financial metrics for TSLA
Result: Financial Metrics: Tesla, Inc. (TSLA)

Market Cap: $917.81 billion
P/E Ratio: 163.76
Dividend Yield: N/A
52-Week Range: $167.41 - $488.54

Revenue: $95.72 billion
Profit Margin: 6.38%
Return on Equity: 8.77%

Great! Our tools are working correctly and retrieving real-time financial data from Yahoo Finance.

3.6 Centralizing Configuration

Let’s create a centralized configuration file to manage settings across our application. Create src/common/config.py:

# src/common/config.py
"""
Centralized configuration module for MarketMind Agent.

This module provides centralized configuration for the entire application including:
1. Model defaults
2. System prompts
3. API key management
4. Logging configuration
"""

# Model configuration
DEFAULT_MODEL = "gpt-4.1-nano"
DEFAULT_MAX_ITERATIONS = 10  # Safety limit for conversation loops

# System prompts 
SYSTEM_PROMPT = """
You are MarketMind, a helpful financial assistant.

You have access to the following tools:
- get_stock_price: Retrieve the latest trading price for a stock ticker.
- get_stock_history: Get historical closing prices for a stock over a specified number of days.
- get_company_info: Provide a company profile, including sector, industry, and description.
- get_financial_metrics: Display key financial metrics such as market cap, P/E ratio, and revenue.

For any question about stock prices, company information, or financial metrics, always use the appropriate tool to get the most accurate and up-to-date information.  

Do not guess or fabricate financial data; rely on the tools for factual answers.  
        
If a question is outside your tools' scope, answer to the best of your ability or politely decline.  
        
Always respond in a clear, helpful manner.
"""

# API Key management
import os
from dotenv import load_dotenv

load_dotenv()

def check_api_key():
    """Check if the OpenAI API key is set and return it.
    
    Returns:
        str: The OpenAI API key
        
    Raises:
        ValueError: If the API key is not set
    """
    OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
    if not OPENAI_API_KEY:
        raise ValueError(
            "OpenAI API key not found. Please set the OPENAI_API_KEY environment variable "
            "or add it to a .env file."
        )
    return OPENAI_API_KEY

# Get the API key once at module import time
OPENAI_API_KEY = check_api_key()

# Logging configuration
import logging
import os
from datetime import datetime

def setup_logging(debug=False, module_loggers=None, log_to_file=False, console_output=False):
    """Set up logging configuration for the application.
    
    This function configures logging for the entire application. It supports:
    - Debug/Info log levels
    - File and/or console output
    - Module-specific logging levels
    - Silencing noisy third-party libraries
    
    Args:
        debug: Whether to enable debug logging (more verbose)
        module_loggers: List of module names to set to DEBUG level
        log_to_file: Whether to log to a file (creates a timestamped log file)
        console_output: Whether to output logs to console (should be False for CLI apps)
        
    Returns:
        log_filename: Path to the log file if enabled, None otherwise
    """
    # Determine log level based on debug flag
    root_level = logging.DEBUG if debug else logging.INFO
    
    # Configure basic logging
    handlers = []
    
    # Add console handler only if explicitly requested
    if console_output:
        console_handler = logging.StreamHandler()
        console_handler.setLevel(root_level)
        console_handler.setFormatter(logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        ))
        handlers.append(console_handler)
    
    # Add file handler if requested
    log_filename = None
    if log_to_file:
        log_dir = os.path.join(os.getcwd(), 'logs')
        os.makedirs(log_dir, exist_ok=True)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        log_filename = os.path.join(log_dir, f'marketmind_{timestamp}.log')
        
        file_handler = logging.FileHandler(log_filename)
        file_handler.setLevel(root_level)
        file_handler.setFormatter(logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        ))
        handlers.append(file_handler)
    
    # Configure root logger
    logging.basicConfig(
        level=root_level,
        handlers=handlers,
        force=True  # Override any existing configuration
    )
    
    # Silence specific third-party loggers to reduce noise in CLI applications
    logging.getLogger("yfinance").setLevel(logging.WARNING)
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("peewee").setLevel(logging.WARNING)
    logging.getLogger("httpx").setLevel(logging.WARNING)
    logging.getLogger("httpcore").setLevel(logging.WARNING)
    
    # Set specific module loggers
    if module_loggers:
        for module in module_loggers:
            logging.getLogger(module).setLevel(logging.DEBUG)
    
    return log_filename

# Default logging levels for specific modules
DEFAULT_DEBUG_MODULES = [
    'src.openai_agent_sdk',
    'src.agent_from_scratch',
    'src.cli',
    'openai.agents'
]

3.7 Understand the Tool Design

Let’s take a moment to understand the design of our tools:

  1. Function-Based Tools: Each tool is a Python function with a clear signature and docstring
  2. Input Validation: We validate and standardize inputs (e.g., converting ticker symbols to uppercase)
  3. Error Handling: We catch exceptions and return user-friendly error messages
  4. Informative Logging: We log actions with emoji indicators for better visibility
  5. Structured Output: We format the output as human-readable text
  6. Independent Operation: Tools can be used independently of any agent framework

This design makes our tools: - Reusable: They can be used with any agent implementation - Testable: They can be tested in isolation - Maintainable: Each tool has a single responsibility - User-friendly: They provide clear feedback and formatted output

3.8 Key Takeaways

In this chapter, we’ve: - Built four financial tools that retrieve real-time data from Yahoo Finance - Implemented comprehensive error handling and logging - Created a centralized configuration system - Designed our tools to be reusable across different agent implementations

These tools represent the “Action” component of our ABC Framework. They allow our agent to interact with the external world and retrieve information that the language model alone cannot access.

In the next chapter, we’ll integrate these tools with the OpenAI Agent SDK to create our first complete agent implementation.