Post 2: Giving Your Agent Purpose – Integrating Basic FunctionTools
Series: Agentic AI with Google ADK
The Power of Tools in ADK
In our previous post, we constructed a basic LlmAgent capable of greeting the user. While this demonstrated fundamental ADK concepts, a truly effective agent must be able to perform actions and interact with its environment beyond generating text. This is where "Tools" become indispensable. In the Agent Development Kit (ADK), Tools are the functional building blocks that allow agents to transcend the limitations of pure text generation. They are typically Python functions designed to execute specific, real-world actions, such as querying databases, calling external APIs, performing calculations, or accessing local files.
Tools are what endow an agent with the ability to act—a core characteristic of agentic AI. They bridge the gap between an LLM's reasoning capabilities and the ability to effect change or retrieve information from the external world, thereby overcoming inherent LLM limitations like knowledge cutoffs or the inability to perform live computations. ADK categorizes tools based on their origin and functionality, including FunctionTools (custom Python functions), built-in tools (e.g., search), third-party tools, and OpenAPI tools, among others. This post will focus on the foundational FunctionTool.
The core characteristics of tools in ADK can be summarized as:
- Action-Oriented: They are purpose-built to carry out specific tasks.
- Capability Extenders: They provide agents with access to real-time, real-world data and allow them to trigger side effects.
- Execute Predefined Logic: Tools execute specific, developer-defined logic, ensuring a degree of predictability in their operation.
A significant aspect of ADK's design is its developer-friendly approach to tool integration. For instance, standard Python functions can be directly added to an agent's tool list, and ADK will automatically wrap them as FunctionTools. This substantially lowers the barrier to entry, allowing developers to readily leverage existing Python code and expertise within the agentic framework, aligning with ADK's objective of making agent development analogous to conventional software development.
Defining Custom Python FunctionTools
The simplest and often most direct way to extend an agent's capabilities is by defining custom Python functions and making them available as FunctionTools. The ADK framework handles much of the underlying mechanism for how the LLM interacts with these functions. However, for this interaction to be effective, careful attention must be paid to the function's definition, particularly its docstring, parameters, and return values.
Key Considerations for Defining FunctionTools:
- Python Function as the Basis: Any standard Python function can serve as the foundation for a FunctionTool. When such a function is included in an agent's tools list, ADK automatically wraps it.
- Parameters:
- Function parameters should utilize standard JSON-serializable types (e.g., str, int, float, bool, list, dict).
- Type hints are highly recommended for clarity and to aid the LLM in understanding expected inputs.
- It is crucial to avoid setting default values for parameters in the function signature, as the LLM may not correctly interpret or utilize them.
- Return Values:
- The preferred return type for a FunctionTool is a Python dict. This structure allows for returning multiple pieces of information in an organized manner, including status indicators or error messages.
- If a function returns a type other than a dictionary (e.g., a string or a number), ADK will automatically wrap it into a dictionary with a single key named "result".
- Return values should be as descriptive as possible. For instance, instead of merely returning None upon failure, a dictionary like {"status": "error", "error_message": "Detailed error description"} provides much richer information for the LLM to process and act upon. The LLM, not just code, needs to understand the tool's output.
- The Critical Role of Docstrings:
- The docstring of a Python function designated as a tool is of paramount importance. The ADK framework uses the docstring as the description of the tool that is provided to the LLM.
- A well-crafted docstring must clearly explain:
- What the tool does (its purpose).
- When the LLM should consider using it.
- What arguments (parameters) the tool expects, including their types and meaning.
- What information the tool returns, detailing the structure of the return dictionary if applicable.
- The LLM's ability to correctly identify the need for a tool, select the appropriate tool, and provide the correct arguments hinges heavily on the clarity and accuracy of its docstring. This effectively means that natural language understanding by the LLM is an integral part of the "code execution" flow within an agent that uses tools.
The practice of returning structured dictionaries, particularly those including status fields, enables more robust error handling and sophisticated decision-making by the LLM. The LLM can be instructed to inspect these status fields and react contextually, for example, by informing the user of a tool execution error or attempting an alternative action.
Travel Planner v0.2: Adding a Mock Flight Information Tool
Let's enhance our TravelPlannerAgent by giving it its first tool: a function that provides mock flight information. This will allow the agent to respond to basic flight queries.
Code Implementation:
We will define a Python function get_mock_flight_info and then integrate it into our LlmAgent.
File: travel_planner_v0_2/agent.py
from google.adk.agents import LlmAgent
def get_mock_flight_info(destination: str, date: str) -> dict:
"""
Retrieves _mock_ flight information for a given destination and date.
This tool should be used when a user explicitly asks for flight details
for a specific city and travel date.
Args:
destination (str): The desired flight destination city (e.g., "Paris", "London").
date (str): The desired date of travel in YYYY-MM-DD format (e.g., "2025-12-25").
Returns:
dict: A dictionary containing mock flight details including flight_number,
departure_time, arrival_time, airline, and price if successful.
If no flights are found for the given destination, or if the destination
is not supported by this mock tool, it returns a dictionary
with a 'status' of 'error' and an explanatory 'message'.
Example success: {"status": "success", "details": {"flight_number": "TP100",...}}
Example error: {"status": "error", "message": "No flights for Rome."}
"""
print(f"--- Tool: get_mock_flight_info called for destination: {destination}, date: {date} ---")
# Mock data - limited for demonstration
flights_db = {
"paris": {"flight_number": "TP100", "departure_time": "08:00 AM", "arrival_time": "10:00 AM CET", "airline": "Air Travel", "price": "250 EUR"},
"london": {"flight_number": "TP200", "departure_time": "09:30 AM", "arrival_time": "10:00 AM GMT", "airline": "Fly UK", "price": "150 GBP"},
"tokyo": {"flight_number": "TP300", "departure_time": "11:00 PM", "arrival_time": "03:00 PM JST +1", "airline": "Sakura Flights", "price": "700 USD"}
}
normalized_destination = destination.lower()
if normalized_destination in flights_db:
return {"status": "success", "details": flights_db[normalized_destination]}
else:
return {"status": "error", "message": f"Sorry, no mock flights currently available for {destination} on {date}."}
travel_planner_agent_v0_2 = LlmAgent(
name="TravelPlannerAgent_v0_2",
model="gemini-2.0-flash", # Or your preferred model
instruction="You are a helpful and efficient travel planner. "
"If the user asks for flight information for a specific destination and date, "
"you MUST use the 'get_mock_flight_info' tool to find the details. "
"When presenting flight information, clearly state all details provided by the tool. "
"If the tool indicates an error or no flights are found, inform the user politely. "
"For general greetings or other queries, respond naturally.",
description="A travel planner agent that can provide mock flight information using a function tool.",
tools=[get_mock_flight_info] # The new tool is added here
)
travel_planner_v0_2/agent.py
from.agent import travel_planner_agent_v0_2
travel_planner_v0_2/__init__.py
import asyncio
import os
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai.types import Content, Part
from dotenv import load_dotenv
load_dotenv()
from travel_planner_v0_2.agent import travel_planner_agent_v0_2
async def run_interaction(runner, user_id, session_id, agent_name, query):
print(f"\n>>> User: {query}")
user_content = Content(role='user', parts=[Part(text=query)])
final_response_text = f"{agent_name} did not provide a final response."
try:
async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=user_content):
# You can inspect various event types here for debugging
# print(f"DEBUG Event: {event.type}, Content: {event.content}")
if event.is_final_response() and event.content and event.content.parts:
final_response_text = event.content.parts.text
print(f"<<< {agent_name}: {final_response_text}")
except Exception as e:
print(f"An error occurred during agent execution for query '{query}': {e}")
async def main():
if os.getenv("GOOGLE_GENAI_USE_VERTEXAI") == "True":
if not os.getenv("GOOGLE_CLOUD_PROJECT") or not os.getenv("GOOGLE_CLOUD_LOCATION"):
print("Error: GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION must be set in.env for Vertex AI.")
return
APP_NAME = "travel_planner_app_v0_2"
USER_ID = "test_user_v0_2"
SESSION_ID = "session_travel_v0_2"
session_service = InMemorySessionService()
session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
runner = Runner(
agent=travel_planner_agent_v0_2,
app_name=APP_NAME,
session_service=session_service
)
print(f"--- Running Travel Planner v0.2: Agent with Mock Flight Tool ({travel_planner_agent_v0_2.model}) ---")
await run_interaction(runner, USER_ID, SESSION_ID, travel_planner_agent_v0_2.name, "Hello there!")
await run_interaction(runner, USER_ID, SESSION_ID, travel_planner_agent_v0_2.name, "I'd like to find flights to Paris for 2025-10-15.")
await run_interaction(runner, USER_ID, SESSION_ID, travel_planner_agent_v0_2.name, "Are there any flights to Rome on 2025-11-20?")
session_service.delete_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
if __name__ == "__main__":
asyncio.run(main())
run_planner_v0_2.py (in the parent directory)
Observations from v0.2:
- Tool Invocation: When the user asks for flights to "Paris," the LlmAgent, guided by its instruction and the docstring of get_mock_flight_info, should recognize the need to use the tool. The ADK framework will then call the Python function, passing "Paris" and "2025-10-15" as arguments.
- Response Generation: The dictionary returned by get_mock_flight_info is then passed back to the LLM. The LLM, again guided by the agent's instruction, formulates a natural language response to the user based on the tool's output. For example, if the tool returns flight details, the agent might say, "I found a flight to Paris: Flight TP100 with Air Travel, departing at 08:00 AM, for 250 EUR." If the tool returns an error for "Rome," the agent should convey that information.
- Importance of Instructions: The agent's instruction is updated to explicitly mention the get_mock_flight_info tool and guide the LLM on when to use it and how to handle its output (both success and error cases). Without this, the LLM would not know the tool exists or how to interpret its results.
- Contract Simulation: Even with mock data, the process of defining the tool (its expected inputs, outputs, and purpose via the docstring) forces a consideration of the tool's "contract." This early design thinking is invaluable. It establishes a clear interface for the tool, which is essential for maintainability and becomes critical before integrating more complex external APIs, as the agent's interaction pattern with the tool's interface can remain consistent even if the underlying tool implementation changes.
Next Steps
Our TravelPlannerAgent can now provide (mock) flight information. In the next post, we will delve into structured data handling. We will explore how to use Pydantic models with LlmAgent to define input_schema for receiving structured queries and output_schema for generating structured JSON responses, making our agent's interactions more robust and predictable.
Further Reading:
- ADK Function Tools: https://google.github.io/adk-docs/tools/function-tools/
ADK Quickstart (Tool Example): https://cloud.google.com/vertex-ai/generative-ai/docs/agent-development-kit/quickstart