Custom Function Tools
In Lesson 1, you used the built-in google_search tool to help an agent find information. But what if you need to do something the built-in tools can't handle?
Think of an agent as someone with a set of skills. Google search is one skill. But what if you need that agent to look up stock prices, translate text, query a database, or analyze images? You need to add new tools—custom capabilities specific to your domain.
Google ADK makes this straightforward: Write a Python function with type hints and a docstring. ADK automatically wraps it as a tool and makes it available to your agent.
In this lesson, you'll learn to create custom tools that integrate external APIs. You'll build a financial data tool using yfinance, then combine it with the search tool to create a richer agent that finds news AND enriches it with stock prices.
Understanding Function Tool Pattern
Google ADK's philosophy is simple: If it's a function with type hints and a docstring, it's a tool.
Here's the pattern:
def tool_name(param1: type1, param2: type2) -> return_type:
"""
Brief description of what the tool does.
Args:
param1: Description of param1.
param2: Description of param2.
Returns:
Description of return type.
"""
# Your implementation here
return result
The three essential parts:
- Type hints → Tell ADK what arguments to expect and what will be returned
- Docstring → Becomes the tool's description; helps the agent decide when to use it
- Implementation → Your actual code that executes when the agent calls the tool
Type hints are REQUIRED. Without them, ADK cannot generate the tool schema that the Gemini model needs to understand what arguments to provide.
Let's start with a simple example before building the financial tool:
from google.generativeai.agents import Agent, Tool
def celsius_to_fahrenheit(celsius: float) -> float:
"""
Convert temperature from Celsius to Fahrenheit.
Args:
celsius: Temperature in Celsius.
Returns:
Temperature in Fahrenheit.
"""
return (celsius * 9/5) + 32
# Create agent with the tool
agent = Agent(
name="TemperatureBot",
instruction="Help users convert temperatures. Use the celsius_to_fahrenheit tool when asked.",
tools=[
Tool.from_function(celsius_to_fahrenheit)
]
)
Output when you ask "What is 0 Celsius in Fahrenheit?":
0 Celsius is 32 Fahrenheit.
Notice what happened: The agent read the function signature and docstring, understood it could convert temperatures, called the function with the value 0, received 32 back, and incorporated it into its response. No decorators needed—just a function with types and documentation.
Building a Financial Data Tool
Now let's build something more complex. You'll create a tool that fetches stock data using yfinance:
from typing import Dict, List
import yfinance as yf
from google.generativeai.agents import Agent, Tool
def get_financial_context(tickers: List[str]) -> Dict[str, str]:
"""
Fetches current stock price and daily change for stock tickers.
Useful for enriching news articles with current market context.
Returns data even if some tickers fail.
Args:
tickers: A list of stock market tickers (e.g., ["GOOG", "NVDA"]).
Returns:
A dictionary mapping each ticker to formatted financial data.
Format: "ticker": "$price (±change%)"
If data unavailable: "ticker": "Price data not available."
"""
financial_data: Dict[str, str] = {}
for ticker_symbol in tickers:
try:
stock = yf.Ticker(ticker_symbol)
info = stock.info
# Get price (primaryTradePrice is most recent, regularMarketPrice is fallback)
price = info.get("currentPrice") or info.get("regularMarketPrice")
change_percent = info.get("regularMarketChangePercent")
if price is not None and change_percent is not None:
change_str = f"{change_percent * 100:+.2f}%"
financial_data[ticker_symbol] = f"${price:.2f} ({change_str})"
else:
financial_data[ticker_symbol] = "Price data not available."
except Exception as e:
# Graceful degradation: return informative error instead of crashing
financial_data[ticker_symbol] = f"Unable to fetch data (API error)"
return financial_data
Let's break down why this design matters:
Return type consistency: Always returns Dict[str, str], never raises exceptions
Graceful degradation: If one ticker fails, others still work
Informative format: Stock data is human-readable ("$150.25 (+2.50%)")
Error handling: Try-catch around each ticker, not the whole function
Docstring detail: Explains what the tool returns AND when it fails
Now let's use it in an agent:
agent = Agent(
name="ai_news_chat_assistant",
model="gemini-2.5-flash",
instruction="""You are a financial news agent. Your job is to:
1. Use google_search to find recent news about AI companies and tech stocks
2. Extract company names and stock tickers from the news
3. Use get_financial_context to fetch current stock prices for those companies
4. Present the news with financial context: "Company X announced Y.
Stock ticker: $price (change%)"
This helps users understand news impact on stock prices.""",
tools=[google_search, Tool.from_function(get_financial_context)]
)
When you run this:
from google.generativeai.agents import Runner
response = Runner.run_sync(
agent,
"What's the latest on OpenAI and Google's AI competition? Show me their stock prices."
)
print(response.final_output)
The agent's workflow:
- Calls
google_search("OpenAI latest news") - Receives news articles mentioning "OpenAI" and "Google"
- Extracts tickers: ["MSFT", "GOOGL"] (OpenAI is backed by Microsoft)
- Calls
get_financial_context(["MSFT", "GOOGL"]) - Gets back:
{"MSFT": "$420.50 (+1.23%)", "GOOGL": "$185.30 (+0.45%)"} - Synthesizes response: "OpenAI just announced... Microsoft (MSFT) is up 1.23% and Google (GOOGL) is up 0.45%..."
Tool Coordination Patterns
Pattern 1: Search + Enrich
Use one tool to gather raw information, another to add context:
def search_research(query: str) -> str:
"""Search for information about a topic."""
return google_search(query)
def get_financial_context(tickers: List[str]) -> Dict[str, str]:
"""Get stock prices for companies mentioned in research."""
# Implementation above
pass
agent = Agent(
instruction="Search for research, then enrich with financial data.",
tools=[search_research, get_financial_context]
)
The agent naturally chains these: search first → parse results → call financial tool.
Pattern 2: Multiple Enrichment
One primary tool, multiple secondary tools for different contexts:
def search_query(query: str) -> str:
"""Primary research tool."""
pass
def get_stock_price(ticker: str) -> str:
"""Get stock price context."""
pass
def get_news_sentiment(company: str) -> str:
"""Get sentiment analysis of recent news."""
pass
agent = Agent(
instruction="Search for information. If about companies, also get stock data and sentiment.",
tools=[search_query, get_stock_price, get_news_sentiment]
)
Pattern 3: Filtering + Validation
Use one tool to filter data, another to validate:
def search_articles(topic: str) -> List[str]:
"""Search and return article URLs."""
pass
def validate_source(url: str) -> bool:
"""Check if source is reputable (returns True/False)."""
pass
# Agent calls search_articles, then for each result, calls validate_source
# to filter out low-quality sources
Common Pitfalls & Solutions
Pitfall 1: Missing or Incomplete Type Hints
WRONG:
def get_price(ticker): # No type hints!
"""Get stock price."""
return yf.Ticker(ticker).info.get("currentPrice")
RIGHT:
def get_price(ticker: str) -> float:
"""Get stock price.
Args:
ticker: Stock ticker symbol (e.g., "GOOG").
Returns:
Current price as float, or 0.0 if unavailable.
"""
try:
price = yf.Ticker(ticker).info.get("currentPrice")
return price or 0.0
except Exception:
return 0.0
Why it matters: Without type hints, ADK cannot generate the tool schema. The agent won't know what type of arguments to pass.
Pitfall 2: Raising Exceptions Instead of Returning Errors
WRONG:
def get_stock_data(ticker: str) -> Dict:
"""Get stock data."""
stock = yf.Ticker(ticker) # Raises if network fails
return stock.info
RIGHT:
def get_stock_data(ticker: str) -> Dict[str, str]:
"""Get stock data.
Returns structured data even on error (graceful degradation).
"""
try:
stock = yf.Ticker(ticker)
return {
"ticker": ticker,
"price": str(stock.info.get("currentPrice", "unavailable")),
"status": "success"
}
except Exception as e:
return {
"ticker": ticker,
"price": "unavailable",
"status": f"error: {str(e)}"
}
Why it matters: If a tool raises an exception, the agent crashes. Graceful degradation means the agent can handle partial failures and continue working.
Pitfall 3: Inconsistent Return Format
WRONG:
def get_data(query: str): # Sometimes returns str, sometimes dict
"""Get data."""
if "price" in query:
return {"price": 100} # Returns dict
else:
return "No data" # Returns string
RIGHT:
def get_data(query: str) -> Dict[str, str]:
"""Get data in consistent format."""
if "price" in query:
return {"result": "price is $100"} # Always dict
else:
return {"result": "No data found"} # Always dict
Why it matters: Agents expect consistent return types. Inconsistency causes parsing errors.
Pitfall 4: Overly Complex Logic in Tools
WRONG:
def analyze_market(tickers: List[str]) -> str:
# 50 lines of analysis logic
# Complex calculations
# Multiple API calls
# Returns opaque string
pass
RIGHT:
def fetch_market_data(tickers: List[str]) -> Dict[str, float]:
"""Fetch raw market data."""
# Simple: just get the data
return {ticker: get_price(ticker) for ticker in tickers}
def analyze_market_trends(data: Dict[str, float]) -> str:
"""Analyze fetched data."""
# Agent or separate function handles analysis
# Tool stays focused: fetch or compute, not both
pass
Why it matters: Tools should be focused. One tool fetches data, another analyzes. This makes tools reusable across agents.
Type Hints for Tools
Google ADK supports these type hints:
# Primitives
def example(a: int, b: str, c: float, d: bool) -> str:
pass
# Collections
from typing import List, Dict, Tuple
def example(items: List[str], mapping: Dict[str, int]) -> Tuple[str, int]:
pass
# Optional (can be None)
from typing import Optional
def example(name: Optional[str]) -> Optional[str]:
pass
# Union (multiple types)
from typing import Union
def example(value: Union[str, int]) -> str:
pass
# Nested structures
def example(data: List[Dict[str, int]]) -> Dict[str, List[str]]:
pass
Key rule: Every parameter and return value must have a type hint. ADK uses these to generate the tool schema that Gemini uses to decide what arguments to pass.
Docstring Format for Clarity
Your docstring becomes the agent's understanding of your tool. Make it clear:
def process_data(items: List[str], threshold: float) -> List[str]:
"""
Filter items by a numeric threshold.
Use this tool when you need to filter a list of items
based on some quantitative measure.
Args:
items: List of item names to filter.
threshold: Numeric threshold for filtering (items with
values above this are included).
Returns:
Filtered list of items that meet the threshold.
Raises:
No exceptions raised; returns empty list if no items match.
"""
# Implementation
pass
The docstring tells the agent:
- What it does: Filter items by threshold
- When to use it: "when you need to filter"
- What arguments it expects: items (list), threshold (float)
- What it returns: filtered list
- Edge cases: Empty list if nothing matches
Adding Tools to Agents
Once you've created your tools, register them with the agent:
from google.generativeai.agents import Agent, Tool
# Define functions
def get_financial_data(tickers: List[str]) -> Dict[str, str]:
pass
def search_news(query: str) -> str:
pass
# Create agent with tools
agent = Agent(
name="NewsAgent",
instruction="Search for news and enrich with financial data.",
tools=[
Tool.from_function(get_financial_data),
Tool.from_function(search_news),
google_search # Built-in tool
]
)
The agent now has three tools available. It will call whichever is most appropriate for the user's request.
Try With AI
Prompt 1: Create Your First Custom Tool
Write a function tool called get_crypto_price that:
- Takes a cryptocurrency symbol (e.g., "bitcoin", "ethereum") as input
- Returns a formatted string with current price and 24-hour change
- Handles errors gracefully (no crashing if API is down)
- Has proper type hints (str -> str) and a clear docstring
Use a real API like CoinGecko (free, no key needed) or mock the data for testing.
What you're learning: The pattern of wrapping functions as tools with type hints, error handling, and clear docstrings that help agents understand when to use them.
Prompt 2: Build a Tool Chain
Create two function tools that work together:
1. search_products(category: str) -> str: Search for products in a category
2. compare_prices(product_names: List[str]) -> Dict[str, str]: Get prices for products
Then create an agent that can:
- Search for laptops
- Extract product names from the results
- Compare prices for those products
Show how the agent chains these tools automatically to answer:
"Find gaming laptops and show me the price comparison."
What you're learning: How tools coordinate to provide richer capabilities. The agent chains simple tools to accomplish complex tasks without you scripting each step.
Prompt 3: Error Handling & Robustness
Enhance your price comparison tool from Prompt 2 to handle:
- Network failures (API timeouts)
- Missing data for some products
- Rate limiting (some API calls fail, others succeed)
Test it with: "Compare prices for [10 product names, some valid, some invalid]"
After running, answer:
- Which products returned valid prices?
- Which failed and why?
- Did the tool crash or degrade gracefully?
What you're learning: Production-grade tools don't assume everything works perfectly. Graceful degradation—returning partial results instead of crashing—is what separates fragile tools from reliable ones that agents can depend on.
Reflect on Your Skill
You built a google-adk skill in Lesson 0. Test and improve it based on what you learned.
Test Your Skill
Using my google-adk skill, create a custom function tool with type hints and docstrings.
Does my skill handle FunctionTool creation, parameter validation, and graceful error handling?
Identify Gaps
Ask yourself:
- Did my skill include the pattern for type-annotated functions (required for ADK tool wrapping)?
- Did it demonstrate Tool.from_function() usage and error handling patterns?
Improve Your Skill
If you found gaps:
My google-adk skill is missing custom tool creation patterns.
Update it to include:
- Function tool pattern with complete type hints (str, List[str], Dict, etc.)
- Docstring format that helps agents understand when to use the tool
- Graceful error handling (return error dicts instead of raising exceptions)
- Tool registration with Agent via Tool.from_function()