Skip to content

Build Your First Intelligent Agent Team: A Progressive Weather Bot with ADK构建你的第一个智能体团队:使用 ADK 开发渐进式天气机器人

Google Colaboratory logo Open in Colab 在 Colab 中打开

This tutorial extends from the Multi-tool agent project. Now, you're ready to dive deeper and construct a more sophisticated, multi-agent system.本教程基于“多工具智能体”项目进行扩展。现在,你已经准备好深入探索并构建一个更复杂的智能体系统。

We'll embark on building a Weather Bot agent team, progressively layering advanced features onto a simple foundation. Starting with a single agent that can look up weather, we will incrementally add capabilities like:我们将着手构建一个天气机器人智能体团队,在简单的基础上逐步增加高级功能。从一个能够查询天气的单一智能体开始,我们将逐步添加以下能力:

  • Leveraging different AI models (Gemini, GPT, Claude).利用不同的 AI 模型(Gemini、GPT、Claude)。
  • Designing specialized sub-agents for distinct tasks (like greetings and farewells).为不同任务(如问候和告别)设计专门的子智能体。
  • Enabling intelligent delegation between agents.实现智能体之间的智能委派。
  • Giving agents memory using persistent session state.通过持久化会话状态赋予智能体记忆能力。
  • Implementing crucial safety guardrails using callbacks.使用回调函数实现关键的安全防护措施。

Why a Weather Bot Team?为什么选择天气机器人团队?

This use case, while seemingly simple, provides a practical and relatable canvas to explore core ADK concepts essential for building complex, real-world agentic applications. You'll learn how to structure interactions, manage state, ensure safety, and orchestrate multiple AI "brains" working together.这个用例虽然看似简单,但提供了一个实用且易于理解的场景,能够帮助你探索构建复杂、现实世界智能体应用所必需的 ADK 核心概念。你将学习如何构建交互、管理状态、确保安全,以及编排多个协同工作的 AI“大脑”。

What is ADK Again?ADK 到底是什么?

As a reminder, ADK is a Python framework designed to streamline the development of applications powered by Large Language Models (LLMs). It offers robust building blocks for creating agents that can reason, plan, utilize tools, interact dynamically with users, and collaborate effectively within a team.提醒一下,ADK 是一个 Python 框架,旨在简化由大语言模型(LLM)驱动的应用开发。它提供了强大的构建模块,用于创建能够推理、规划、使用工具、与用户进行动态交互,并在团队中有效协作的智能体。

In this advanced tutorial, you will master:在本高级教程中,你将掌握:

  • ✅ Tool Definition & Usage: Crafting Python functions (tools) that grant agents specific abilities (like fetching data) and instructing agents on how to use them effectively.✅ 工具定义与使用:编写 Python 函数(工具),赋予智能体特定能力(如获取数据),并指导智能体如何有效使用它们。
  • ✅ Multi-LLM Flexibility: Configuring agents to utilize various leading LLMs (Gemini, GPT-4o, Claude Sonnet) via LiteLLM integration, allowing you to choose the best model for each task.✅ 多 LLM 灵活性:通过 LiteLLM 集成,配置智能体以使用各种领先的 LLM(Gemini、GPT-4o、Claude Sonnet),从而为每个任务选择最合适的模型。
  • ✅ Agent Delegation & Collaboration: Designing specialized sub-agents and enabling automatic routing (auto flow) of user requests to the most appropriate agent within a team.✅ 智能体委派与协作:设计专门的子智能体,并实现用户请求向团队内最合适智能体的自动路由(自动流转)。
  • ✅ Session State for Memory: Utilizing Session State and ToolContext to enable agents to remember information across conversational turns, leading to more contextual interactions.✅ 会话状态记忆:利用会话状态(Session State)和工具上下文(ToolContext),使智能体能够跨对话轮次记忆信息,从而实现更具上下文关联的交互。
  • ✅ Safety Guardrails with Callbacks: Implementing before_model_callback and before_tool_callback to inspect, modify, or block requests/tool usage based on predefined rules, enhancing application safety and control.✅ 使用回调函数的安全防护:实现 before_model_callback 和 before_tool_callback,根据预定义规则检查、修改或拦截请求/工具使用,从而增强应用的安全性与可控性。

End State Expectation:预期最终状态:

By completing this tutorial, you will have built a functional multi-agent Weather Bot system. This system will not only provide weather information but also handle conversational niceties, remember the last city checked, and operate within defined safety boundaries, all orchestrated using ADK.完成本教程后,你将构建出一个功能完备的多智能体天气机器人系统。该系统不仅能提供天气信息,还能处理对话礼仪、记住上次查询的城市,并在定义的安全边界内运行,所有这一切都由 ADK 进行编排。

Prerequisites:先决条件:

  • ✅ Solid understanding of Python programming.✅ 对 Python 编程有扎实的理解。
  • ✅ Familiarity with Large Language Models (LLMs), APIs, and the concept of agents.✅ 熟悉大语言模型(LLM)、API 以及智能体的概念。
  • ❗ Crucially: Completion of the ADK Quickstart tutorial(s) or equivalent foundational knowledge of ADK basics (Agent, Runner, SessionService, basic Tool usage). This tutorial builds directly upon those concepts.❗ 关键要求:已完成 ADK 快速入门教程,或具备同等的 ADK 基础知识(Agent、Runner、SessionService、基本工具使用)。本教程直接建立在这些概念之上。
  • ✅ API Keys for the LLMs you intend to use (e.g., Google AI Studio for Gemini, OpenAI Platform, Anthropic Console).✅ 你打算使用的 LLM 的 API 密钥(例如:用于 Gemini 的 Google AI Studio、OpenAI 平台、Anthropic 控制台)。

Note on Execution Environment:关于执行环境的说明:

This tutorial is structured for interactive notebook environments like Google Colab, Colab Enterprise, or Jupyter notebooks. Please keep the following in mind:本教程专为交互式笔记本环境(如 Google Colab、Colab Enterprise 或 Jupyter Notebook)设计。请注意以下事项:

  • Running Async Code: Notebook environments handle asynchronous code differently. You'll see examples using await (suitable when an event loop is already running, common in notebooks) or asyncio.run() (often needed when running as a standalone .py script or in specific notebook setups). The code blocks provide guidance for both scenarios.运行异步代码:笔记本环境处理异步代码的方式有所不同。你将看到使用 await 的示例(适用于事件循环已经在运行的情况,这在笔记本中很常见)或 asyncio.run()(通常在作为独立 .py 脚本运行或在特定笔记本设置中需要)。代码块为这两种场景提供了指导。
  • Manual Runner/Session Setup: The steps involve explicitly creating Runner and SessionService instances. This approach is shown because it gives you fine-grained control over the agent's execution lifecycle, session management, and state persistence.手动 Runner/Session 设置:这些步骤涉及显式创建 Runner 和 SessionService 实例。展示此方法是因为它能让你对智能体的执行生命周期、会话管理和状态持久化进行精细控制。

Alternative: Using ADK's Built-in Tools (Web UI / CLI / API Server)替代方案:使用 ADK 内置工具(Web UI / CLI / API 服务器)

If you prefer a setup that handles the runner and session management automatically using ADK's standard tools, you can find the equivalent code structured for that purpose here. That version is designed to be run directly with commands like adk web (for a web UI), adk run (for CLI interaction), or adk api_server (to expose an API). Please follow the README.md instructions provided in that alternative resource.如果你更喜欢使用 ADK 标准工具自动处理运行器和会话管理的设置,可以在此处找到为此目的构建的等效代码。该版本旨在直接通过命令运行,如 adk web(用于 Web UI)、adk run(用于 CLI 交互)或 adk api_server(用于公开 API)。请遵循该替代资源中提供的 README.md 说明。


Ready to build your agent team? Let's dive in!准备好构建你的智能体团队了吗?让我们开始吧!

Note: This tutorial works with adk version 1.0.0 and above注意:本教程适用于 adk 1.0.0 及以上版本。

# @title Step 0: Setup and Installation
# Install ADK and LiteLLM for multi-model support

!pip install google-adk -q
!pip install "litellm>=1.84" -q

print("Installation complete.")
# @title Import necessary libraries
import os
import asyncio
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm # For multi-model support
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai import types # For creating message Content/Parts

import warnings
# Ignore all warnings
warnings.filterwarnings("ignore")

import logging
logging.basicConfig(level=logging.ERROR)

print("Libraries imported.")
# @title Configure API Keys (Replace with your actual keys!)

# --- IMPORTANT: Replace placeholders with your real API keys ---

# Gemini API Key (Get from Google AI Studio: https://aistudio.google.com/app/apikey)
os.environ["GOOGLE_API_KEY"] = "YOUR_GOOGLE_API_KEY" # <--- REPLACE

# [Optional]
# OpenAI API Key (Get from OpenAI Platform: https://platform.openai.com/api-keys)
os.environ['OPENAI_API_KEY'] = 'YOUR_OPENAI_API_KEY' # <--- REPLACE

# [Optional]
# Anthropic API Key (Get from Anthropic Console: https://console.anthropic.com/settings/keys)
os.environ['ANTHROPIC_API_KEY'] = 'YOUR_ANTHROPIC_API_KEY' # <--- REPLACE

# --- Verify Keys (Optional Check) ---
print("API Keys Set:")
print(f"Google API Key set: {'Yes' if os.environ.get('GOOGLE_API_KEY') and os.environ['GOOGLE_API_KEY'] != 'YOUR_GOOGLE_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")
print(f"OpenAI API Key set: {'Yes' if os.environ.get('OPENAI_API_KEY') and os.environ['OPENAI_API_KEY'] != 'YOUR_OPENAI_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")
print(f"Anthropic API Key set: {'Yes' if os.environ.get('ANTHROPIC_API_KEY') and os.environ['ANTHROPIC_API_KEY'] != 'YOUR_ANTHROPIC_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")

# Configure ADK to use API keys directly (not Agent Platform for this multi-model setup)
os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "False"


# @markdown **Security Note:** It's best practice to manage API keys securely (e.g., using Colab Secrets or environment variables) rather than hardcoding them directly in the notebook. Replace the placeholder strings above.
# --- Define Model Constants for easier use ---

# More supported models can be referenced here: https://ai.google.dev/gemini-api/docs/models#model-variations
MODEL_GEMINI_FLASH = "gemini-flash-latest"

# More supported models can be referenced here: https://docs.litellm.ai/docs/providers/openai#openai-chat-completion-models
MODEL_GPT_4O = "openai/gpt-4.1" # You can also try: gpt-4.1-mini, gpt-4o etc.

# More supported models can be referenced here: https://docs.litellm.ai/docs/providers/anthropic
MODEL_CLAUDE_SONNET = "claude-sonnet-4-6" # You can also try: claude-opus-4-6, etc

print("\nEnvironment configured.")

Step 1: Your First Agent - Basic Weather Lookup第 1 步:你的第一个智能体 - 基础天气查询

Let's begin by building the fundamental component of our Weather Bot: a single agent capable of performing a specific task – looking up weather information. This involves creating two core pieces:让我们从构建天气机器人的基础组件开始:一个能够执行特定任务(查询天气信息)的单一智能体。这涉及创建两个核心部分:

  1. A Tool: A Python function that equips the agent with the ability to fetch weather data.工具:一个 Python 函数,赋予智能体获取天气数据的能力。
  2. An Agent: The AI "brain" that understands the user's request, knows it has a weather tool, and decides when and how to use it.智能体:AI“大脑”,负责理解用户请求、识别已有的天气工具,并决定何时以及如何使用它。

1. Define the Tool (get_weather)1. 定义工具 (get_weather)

In ADK, Tools are the building blocks that give agents concrete capabilities beyond just text generation. They are typically regular Python functions that perform specific actions, like calling an API, querying a database, or performing calculations.在 ADK 中,工具是赋予智能体除文本生成之外具体能力的构建模块。它们通常是执行特定动作的常规 Python 函数,例如调用 API、查询数据库或执行计算。

Our first tool will provide a mock weather report. This allows us to focus on the agent structure without needing external API keys yet. Later, you could easily swap this mock function with one that calls a real weather service.我们的第一个工具将提供模拟天气报告。这使我们能够专注于智能体结构,而无需立即使用外部 API 密钥。稍后,你可以轻松地将此模拟函数替换为调用真实天气服务的函数。

Key Concept: Docstrings are Crucial! The agent's LLM relies heavily on the function's docstring to understand:核心概念:文档字符串(Docstrings)至关重要!智能体的 LLM 很大程度上依赖于函数的文档字符串来理解:

  • What the tool does.该工具的功能。
  • When to use it.何时使用它。
  • What arguments it requires (city: str).它需要什么参数 (city: str)。
  • What information it returns.它返回什么信息。

Best Practice: Write clear, descriptive, and accurate docstrings for your tools. This is essential for the LLM to use the tool correctly.最佳实践:为你的工具编写清晰、描述性强且准确的文档字符串。这对 LLM 正确使用工具至关重要。

# @title Define the get_weather Tool
def get_weather(city: str) -> dict:
    """Retrieves the current weather report for a specified city.

    Args:
        city (str): The name of the city (e.g., "New York", "London", "Tokyo").

    Returns:
        dict: A dictionary containing the weather information.
              Includes a 'status' key ('success' or 'error').
              If 'success', includes a 'report' key with weather details.
              If 'error', includes an 'error_message' key.
    """
    print(f"--- Tool: get_weather called for city: {city} ---") # Log tool execution
    city_normalized = city.lower().replace(" ", "") # Basic normalization

    # Mock weather data
    mock_weather_db = {
        "newyork": {"status": "success", "report": "The weather in New York is sunny with a temperature of 25°C."},
        "london": {"status": "success", "report": "It's cloudy in London with a temperature of 15°C."},
        "tokyo": {"status": "success", "report": "Tokyo is experiencing light rain and a temperature of 18°C."},
    }

    if city_normalized in mock_weather_db:
        return mock_weather_db[city_normalized]
    else:
        return {"status": "error", "error_message": f"Sorry, I don't have weather information for '{city}'."}

# Example tool usage (optional test)
print(get_weather("New York"))
print(get_weather("Paris"))

2. Define the Agent (weather_agent)2. 定义智能体 (weather_agent)

Now, let's create the Agent itself. An Agent in ADK orchestrates the interaction between the user, the LLM, and the available tools.现在,让我们创建智能体本身。ADK 中的智能体负责编排用户、LLM 和可用工具之间的交互。

We configure it with several key parameters:我们使用几个关键参数对其进行配置:

  • name: A unique identifier for this agent (e.g., "weather_agent_v1").name:此智能体的唯一标识符(例如 "weather_agent_v1")。
  • model: Specifies which LLM to use (e.g., MODEL_GEMINI_FLASH). We'll start with a specific Gemini model.model:指定使用的 LLM(例如 MODEL_GEMINI_FLASH)。我们将从特定的 Gemini 模型开始。
  • description: A concise summary of the agent's overall purpose. This becomes crucial later when other agents need to decide whether to delegate tasks to this agent.description:智能体总体目标的简明摘要。这在稍后其他智能体需要决定是否将任务委派给该智能体时至关重要。
  • instruction: Detailed guidance for the LLM on how to behave, its persona, its goals, and specifically how and when to utilize its assigned tools.instruction:为 LLM 提供的详细指导,包括其行为方式、角色设定、目标,以及具体如何和何时利用其分配的工具。
  • tools: A list containing the actual Python tool functions the agent is allowed to use (e.g., [get_weather]).tools:包含智能体被允许使用的实际 Python 工具函数的列表(例如 [get_weather])。

Best Practice: Provide clear and specific instruction prompts. The more detailed the instructions, the better the LLM can understand its role and how to use its tools effectively. Be explicit about error handling if needed.最佳实践:提供清晰具体的指令提示词。指令越详细,LLM 就越能理解其角色以及如何有效地使用其工具。如有需要,请明确说明错误处理方式。

Best Practice: Choose descriptive name and description values. These are used internally by ADK and are vital for features like automatic delegation (covered later).最佳实践:选择具有描述性的 name 和 description 值。它们在 ADK 内部使用,对于自动委派等功能(稍后介绍)至关重要。

# @title Define the Weather Agent
# Use one of the model constants defined earlier
AGENT_MODEL = MODEL_GEMINI_FLASH # Starting with Gemini

weather_agent = Agent(
    name="weather_agent_v1",
    model=AGENT_MODEL, # Can be a string for Gemini or a LiteLlm object
    description="Provides weather information for specific cities.",
    instruction="You are a helpful weather assistant. "
                "When the user asks for the weather in a specific city, "
                "use the 'get_weather' tool to find the information. "
                "If the tool returns an error, inform the user politely. "
                "If the tool is successful, present the weather report clearly.",
    tools=[get_weather], # Pass the function directly
)

print(f"Agent '{weather_agent.name}' created using model '{AGENT_MODEL}'.")

3. Setup Runner and Session Service3. 设置 Runner 和 Session Service

To manage conversations and execute the agent, we need two more components:为了管理对话并执行智能体,我们需要另外两个组件:

  • SessionService: Responsible for managing conversation history and state for different users and sessions. The InMemorySessionService is a simple implementation that stores everything in memory, suitable for testing and simple applications. It keeps track of the messages exchanged. We'll explore state persistence more in Step 4.SessionService:负责管理不同用户和会话的对话历史和状态。InMemorySessionService 是一种简单的实现,将所有内容存储在内存中,适用于测试和简单的应用程序。它会跟踪交换的消息。我们将在第 4 步中进一步探讨状态持久化。
  • Runner: The engine that orchestrates the interaction flow. It takes user input, routes it to the appropriate agent, manages calls to the LLM and tools based on the agent's logic, handles session updates via the SessionService, and yields events representing the progress of the interaction.Runner:编排交互流程的引擎。它接收用户输入,将其路由到合适的智能体,基于智能体的逻辑管理对 LLM 和工具的调用,通过 SessionService 处理会话更新,并产生代表交互进度的事件。
# @title Setup Session Service and Runner

# --- Session Management ---
# Key Concept: SessionService stores conversation history & state.
# InMemorySessionService is simple, non-persistent storage for this tutorial.
session_service = InMemorySessionService()

# Define constants for identifying the interaction context
APP_NAME = "weather_tutorial_app"
USER_ID = "user_1"
SESSION_ID = "session_001" # Using a fixed ID for simplicity

# Create the specific session where the conversation will happen
session = await session_service.create_session(
    app_name=APP_NAME,
    user_id=USER_ID,
    session_id=SESSION_ID
)
print(f"Session created: App='{APP_NAME}', User='{USER_ID}', Session='{SESSION_ID}'")

# --- OR ---

# Uncomment the following lines if running as a standard Python script (.py file):

# from google.adk.sessions import Session
#
# async def init_session(app_name:str,user_id:str,session_id:str) -> Session:
#     session = await session_service.create_session(
#         app_name=app_name,
#         user_id=user_id,
#         session_id=session_id
#     )
#     print(f"Session created: App='{app_name}', User='{user_id}', Session='{session_id}'")
#     return session
#
# session = asyncio.run(init_session(APP_NAME,USER_ID,SESSION_ID))

# --- Runner ---
# Key Concept: Runner orchestrates the agent execution loop.
runner = Runner(
    agent=weather_agent, # The agent we want to run
    app_name=APP_NAME,   # Associates runs with our app
    session_service=session_service # Uses our session manager
)
print(f"Runner created for agent '{runner.agent.name}'.")

4. Interact with the Agent4. 与智能体交互

We need a way to send messages to our agent and receive its responses. Since LLM calls and tool executions can take time, ADK's Runner operates asynchronously.我们需要一种向智能体发送消息并接收其响应的方法。由于 LLM 调用和工具执行可能需要时间,ADK 的 Runner 是异步运行的。

We'll define an async helper function (call_agent_async) that:我们将定义一个异步辅助函数 (call_agent_async),它:

  1. Takes a user query string.接收用户查询字符串。
  2. Packages it into the ADK Content format.将其封装为 ADK 内容格式。
  3. Calls runner.run_async, providing the user/session context and the new message.调用 runner.run_async,提供用户/会话上下文和新消息。
  4. Iterates through the Events yielded by the runner. Events represent steps in the agent's execution (e.g., tool call requested, tool result received, intermediate LLM thought, final response).遍历 Runner 产生的事件。事件代表智能体执行中的步骤(例如,请求工具调用、收到工具结果、中间 LLM 思考、最终响应)。
  5. Identifies and prints the final response event using event.is_final_response().识别并使用 event.is_final_response() 打印最终响应事件。

Why async? Interactions with LLMs and potentially tools (like external APIs) are I/O-bound operations. Using asyncio allows the program to handle these operations efficiently without blocking execution.为什么使用异步?与 LLM 以及潜在工具(如外部 API)的交互是 I/O 密集型操作。使用 asyncio 允许程序高效地处理这些操作,而不会阻塞执行。

# @title Define Agent Interaction Function

from google.genai import types # For creating message Content/Parts

async def call_agent_async(query: str, runner, user_id, session_id):
  """Sends a query to the agent and prints the final response."""
  print(f"\n>>> User Query: {query}")

  # Prepare the user's message in ADK format
  content = types.Content(role='user', parts=[types.Part(text=query)])

  final_response_text = "Agent did not produce a final response." # Default

  # Key Concept: run_async executes the agent logic and yields Events.
  # We iterate through events to find the final answer.
  async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content):
      # You can uncomment the line below to see *all* events during execution
      # print(f"  [Event] Author: {event.author}, Type: {type(event).__name__}, Final: {event.is_final_response()}, Content: {event.content}")

      # Key Concept: is_final_response() marks the concluding message for the turn.
      if event.is_final_response():
          if event.content and event.content.parts:
             # Assuming text response in the first part
             final_response_text = event.content.parts[0].text
          elif event.actions and event.actions.escalate: # Handle potential errors/escalations
             final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}"
          # Add more checks here if needed (e.g., specific error codes)
          break # Stop processing events once the final response is found

  print(f"<<< Agent Response: {final_response_text}")

5. Run the Conversation5. 运行对话

Finally, let's test our setup by sending a few queries to the agent. We wrap our async calls in a main async function and run it using await.最后,让我们通过向智能体发送几个查询来测试我们的设置。我们将异步调用包装在一个主异步函数中,并使用 await 运行它。

Watch the output:观察输出:

  • See the user queries.查看用户查询。
  • Notice the --- Tool: get_weather called... --- logs when the agent uses the tool.注意智能体使用工具时显示的 --- Tool: get_weather called... --- 日志。
  • Observe the agent's final responses, including how it handles the case where weather data isn't available (for Paris).观察智能体的最终响应,包括它如何处理无法获得天气数据的情况(针对巴黎)。
# @title Run the Initial Conversation

# We need an async function to await our interaction helper
async def run_conversation():
    await call_agent_async("What is the weather like in London?",
                                       runner=runner,
                                       user_id=USER_ID,
                                       session_id=SESSION_ID)

    await call_agent_async("How about Paris?",
                                       runner=runner,
                                       user_id=USER_ID,
                                       session_id=SESSION_ID) # Expecting the tool's error message

    await call_agent_async("Tell me the weather in New York",
                                       runner=runner,
                                       user_id=USER_ID,
                                       session_id=SESSION_ID)

# Execute the conversation using await in an async context (like Colab/Jupyter)
await run_conversation()

# --- OR ---

# Uncomment the following lines if running as a standard Python script (.py file):
# import asyncio
# if __name__ == "__main__":
#     try:
#         asyncio.run(run_conversation())
#     except Exception as e:
#         print(f"An error occurred: {e}")

Congratulations! You've successfully built and interacted with your first ADK agent. It understands the user's request, uses a tool to find information, and responds appropriately based on the tool's result.恭喜!你已经成功构建并与你的第一个 ADK 智能体进行了交互。它能理解用户的请求,使用工具查找信息,并根据工具的结果做出适当的响应。

In the next step, we'll explore how to easily switch the underlying Language Model powering this agent.在下一步中,我们将探索如何轻松切换为该智能体提供支持的底层语言模型。

Step 2: Going Multi-Model with LiteLLM [Optional]第 2 步:使用 LiteLLM 实现多模型支持 [可选]

In Step 1, we built a functional Weather Agent powered by a specific Gemini model. While effective, real-world applications often benefit from the flexibility to use different Large Language Models (LLMs). Why?在第 1 步中,我们构建了一个由特定 Gemini 模型驱动的功能性天气智能体。虽然有效,但现实世界的应用通常受益于使用不同大语言模型(LLM)的灵活性。为什么?

  • Performance: Some models excel at specific tasks (e.g., coding, reasoning, creative writing).性能:某些模型在特定任务上表现出色(例如编码、推理、创意写作)。
  • Cost: Different models have varying price points.成本:不同模型有不同的定价。
  • Capabilities: Models offer diverse features, context window sizes, and fine-tuning options.能力:模型提供多样化的功能、上下文窗口大小和微调选项。
  • Availability/Redundancy: Having alternatives ensures your application remains functional even if one provider experiences issues.可用性/冗余:拥有替代方案可确保即使某个提供商出现问题,你的应用也能保持功能正常。

ADK makes switching between models seamless through its integration with the LiteLLM library. LiteLLM acts as a consistent interface to over 100 different LLMs.ADK 通过与 LiteLLM 库的集成,使模型之间的切换变得无缝。LiteLLM 充当了 100 多种不同 LLM 的统一接口。

In this step, we will:在此步骤中,我们将:

  1. Learn how to configure an ADK Agent to use models from providers like OpenAI (GPT) and Anthropic (Claude) using the LiteLlm wrapper.学习如何配置 ADK 智能体,使用 LiteLlm 包装器调用来自 OpenAI (GPT) 和 Anthropic (Claude) 等提供商的模型。
  2. Define, configure (with their own sessions and runners), and immediately test instances of our Weather Agent, each backed by a different LLM.定义、配置(使用各自的会话和运行器)并立即测试我们的天气智能体实例,每个实例都由不同的 LLM 支持。
  3. Interact with these different agents to observe potential variations in their responses, even when using the same underlying tool.与这些不同的智能体交互,以观察它们在响应上的潜在差异,即使使用的是相同的底层工具。

1. Import LiteLlm1. 导入 LiteLlm

We imported this during the initial setup (Step 0), but it's the key component for multi-model support:我们在初始设置(第 0 步)中导入了它,它是支持多模型的关键组件:

# @title 1. Import LiteLlm
from google.adk.models.lite_llm import LiteLlm

2. Define and Test Multi-Model Agents2. 定义并测试多模型智能体

Instead of passing only a model name string (which defaults to Google's Gemini models), we wrap the desired model identifier string within the LiteLlm class.我们不再只传递模型名称字符串(默认为 Google 的 Gemini 模型),而是将所需的模型标识符字符串包装在 LiteLlm 类中。

  • Key Concept: LiteLlm Wrapper: The LiteLlm(model="provider/model_name") syntax tells ADK to route requests for this agent through the LiteLLM library to the specified model provider.核心概念:LiteLlm 包装器:LiteLlm(model="provider/model_name") 语法告诉 ADK 将此智能体的请求通过 LiteLLM 库路由到指定的模型提供商。

Make sure you have configured the necessary API keys for OpenAI and Anthropic in Step 0. We'll use the call_agent_async function (defined earlier, which now accepts runner, user_id, and session_id) to interact with each agent immediately after its setup.确保你在第 0 步中配置了 OpenAI 和 Anthropic 所需的 API 密钥。我们将使用 call_agent_async 函数(之前定义的,现在接受 runner、user_id 和 session_id)在每个智能体设置后立即与其交互。

Each block below will:下面的每个代码块将:

  • Define the agent using a specific LiteLLM model (MODEL_GPT_4O or MODEL_CLAUDE_SONNET).使用特定的 LiteLLM 模型(MODEL_GPT_4O 或 MODEL_CLAUDE_SONNET)定义智能体。
  • Create a new, separate InMemorySessionService and session specifically for that agent's test run. This keeps the conversation histories isolated for this demonstration.为该智能体的测试运行创建一个新的、独立的 InMemorySessionService 和会话。这在本次演示中隔离了对话历史。
  • Create a Runner configured for the specific agent and its session service.创建一个为特定智能体及其会话服务配置的 Runner。
  • Immediately call call_agent_async to send a query and test the agent.立即调用 call_agent_async 发送查询并测试智能体。

Best Practice: Use constants for model names (like MODEL_GPT_4O, MODEL_CLAUDE_SONNET defined in Step 0) to avoid typos and make code easier to manage.最佳实践:使用模型名称常量(如第 0 步中定义的 MODEL_GPT_4O、MODEL_CLAUDE_SONNET)以避免拼写错误并使代码更易于管理。

Error Handling: We wrap the agent definitions in try...except blocks. This prevents the entire code cell from failing if an API key for a specific provider is missing or invalid, allowing the tutorial to proceed with the models that are configured.错误处理:我们将智能体定义包装在 try...except 块中。这可以防止在特定提供商的 API 密钥缺失或无效时整个代码单元失败,从而允许教程继续使用已配置的模型。

First, let's create and test the agent using OpenAI's GPT-4o.首先,让我们使用 OpenAI 的 GPT-4o 创建并测试智能体。

# @title Define and Test GPT Agent

# Make sure 'get_weather' function from Step 1 is defined in your environment.
# Make sure 'call_agent_async' is defined from earlier.

# --- Agent using GPT-4o ---
weather_agent_gpt = None # Initialize to None
runner_gpt = None      # Initialize runner to None

try:
    weather_agent_gpt = Agent(
        name="weather_agent_gpt",
        # Key change: Wrap the LiteLLM model identifier
        model=LiteLlm(model=MODEL_GPT_4O),
        description="Provides weather information (using GPT-4o).",
        instruction="You are a helpful weather assistant powered by GPT-4o. "
                    "Use the 'get_weather' tool for city weather requests. "
                    "Clearly present successful reports or polite error messages based on the tool's output status.",
        tools=[get_weather], # Re-use the same tool
    )
    print(f"Agent '{weather_agent_gpt.name}' created using model '{MODEL_GPT_4O}'.")

    # InMemorySessionService is simple, non-persistent storage for this tutorial.
    session_service_gpt = InMemorySessionService() # Create a dedicated service

    # Define constants for identifying the interaction context
    APP_NAME_GPT = "weather_tutorial_app_gpt" # Unique app name for this test
    USER_ID_GPT = "user_1_gpt"
    SESSION_ID_GPT = "session_001_gpt" # Using a fixed ID for simplicity

    # Create the specific session where the conversation will happen
    session_gpt = await session_service_gpt.create_session(
        app_name=APP_NAME_GPT,
        user_id=USER_ID_GPT,
        session_id=SESSION_ID_GPT
    )
    print(f"Session created: App='{APP_NAME_GPT}', User='{USER_ID_GPT}', Session='{SESSION_ID_GPT}'")

    # Create a runner specific to this agent and its session service
    runner_gpt = Runner(
        agent=weather_agent_gpt,
        app_name=APP_NAME_GPT,       # Use the specific app name
        session_service=session_service_gpt # Use the specific session service
        )
    print(f"Runner created for agent '{runner_gpt.agent.name}'.")

    # --- Test the GPT Agent ---
    print("\n--- Testing GPT Agent ---")
    # Ensure call_agent_async uses the correct runner, user_id, session_id
    await call_agent_async(query = "What's the weather in Tokyo?",
                           runner=runner_gpt,
                           user_id=USER_ID_GPT,
                           session_id=SESSION_ID_GPT)
    # --- OR ---

    # Uncomment the following lines if running as a standard Python script (.py file):
    # import asyncio
    # if __name__ == "__main__":
    #     try:
    #         asyncio.run(call_agent_async(query = "What's the weather in Tokyo?",
    #                      runner=runner_gpt,
    #                       user_id=USER_ID_GPT,
    #                       session_id=SESSION_ID_GPT)
    #     except Exception as e:
    #         print(f"An error occurred: {e}")

except Exception as e:
    print(f"❌ Could not create or run GPT agent '{MODEL_GPT_4O}'. Check API Key and model name. Error: {e}")

Next, we'll do the same for Anthropic's Claude Sonnet.接下来,我们将对 Anthropic 的 Claude Sonnet 执行相同的操作。

# @title Define and Test Claude Agent

# Make sure 'get_weather' function from Step 1 is defined in your environment.
# Make sure 'call_agent_async' is defined from earlier.

# --- Agent using Claude Sonnet ---
weather_agent_claude = None # Initialize to None
runner_claude = None      # Initialize runner to None

try:
    weather_agent_claude = Agent(
        name="weather_agent_claude",
        # Key change: Wrap the LiteLLM model identifier
        model=LiteLlm(model=MODEL_CLAUDE_SONNET),
        description="Provides weather information (using Claude Sonnet).",
        instruction="You are a helpful weather assistant powered by Claude Sonnet. "
                    "Use the 'get_weather' tool for city weather requests. "
                    "Analyze the tool's dictionary output ('status', 'report'/'error_message'). "
                    "Clearly present successful reports or polite error messages.",
        tools=[get_weather], # Re-use the same tool
    )
    print(f"Agent '{weather_agent_claude.name}' created using model '{MODEL_CLAUDE_SONNET}'.")

    # InMemorySessionService is simple, non-persistent storage for this tutorial.
    session_service_claude = InMemorySessionService() # Create a dedicated service

    # Define constants for identifying the interaction context
    APP_NAME_CLAUDE = "weather_tutorial_app_claude" # Unique app name
    USER_ID_CLAUDE = "user_1_claude"
    SESSION_ID_CLAUDE = "session_001_claude" # Using a fixed ID for simplicity

    # Create the specific session where the conversation will happen
    session_claude = await session_service_claude.create_session(
        app_name=APP_NAME_CLAUDE,
        user_id=USER_ID_CLAUDE,
        session_id=SESSION_ID_CLAUDE
    )
    print(f"Session created: App='{APP_NAME_CLAUDE}', User='{USER_ID_CLAUDE}', Session='{SESSION_ID_CLAUDE}'")

    # Create a runner specific to this agent and its session service
    runner_claude = Runner(
        agent=weather_agent_claude,
        app_name=APP_NAME_CLAUDE,       # Use the specific app name
        session_service=session_service_claude # Use the specific session service
        )
    print(f"Runner created for agent '{runner_claude.agent.name}'.")

    # --- Test the Claude Agent ---
    print("\n--- Testing Claude Agent ---")
    # Ensure call_agent_async uses the correct runner, user_id, session_id
    await call_agent_async(query = "Weather in London please.",
                           runner=runner_claude,
                           user_id=USER_ID_CLAUDE,
                           session_id=SESSION_ID_CLAUDE)

    # --- OR ---

    # Uncomment the following lines if running as a standard Python script (.py file):
    # import asyncio
    # if __name__ == "__main__":
    #     try:
    #         asyncio.run(call_agent_async(query = "Weather in London please.",
    #                      runner=runner_claude,
    #                       user_id=USER_ID_CLAUDE,
    #                       session_id=SESSION_ID_CLAUDE)
    #     except Exception as e:
    #         print(f"An error occurred: {e}")


except Exception as e:
    print(f"❌ Could not create or run Claude agent '{MODEL_CLAUDE_SONNET}'. Check API Key and model name. Error: {e}")

Observe the output carefully from both code blocks. You should see:仔细观察两个代码块的输出。你应该看到:

  1. Each agent (weather_agent_gpt, weather_agent_claude) is created successfully (if API keys are valid).每个智能体(weather_agent_gpt, weather_agent_claude)都成功创建(如果 API 密钥有效)。
  2. A dedicated session and runner are set up for each.为每个智能体设置了专用的会话和运行器。
  3. Each agent correctly identifies the need to use the get_weather tool when processing the query (you'll see the --- Tool: get_weather called... --- log).每个智能体在处理查询时都能正确识别使用 get_weather 工具的必要性(你会看到 --- Tool: get_weather called... --- 日志)。
  4. The underlying tool logic remains identical, always returning our mock data.底层工具逻辑保持不变,始终返回我们的模拟数据。
  5. However, the final textual response generated by each agent might differ slightly in phrasing, tone, or formatting. This is because the instruction prompt is interpreted and executed by different LLMs (GPT-4o vs. Claude Sonnet).然而,每个智能体生成的最终文本响应在措辞、语气或格式上可能会略有不同。这是因为指令提示词是由不同的 LLM(GPT-4o 与 Claude Sonnet)解释并执行的。

This step demonstrates the power and flexibility ADK + LiteLLM provide. You can easily experiment with and deploy agents using various LLMs while keeping your core application logic (tools, fundamental agent structure) consistent.此步骤展示了 ADK + LiteLLM 提供的强大功能和灵活性。你可以轻松地使用各种 LLM 试验和部署智能体,同时保持核心应用逻辑(工具、基本智能体结构)的一致性。

In the next step, we'll move beyond a single agent and build a small team where agents can delegate tasks to each other!在下一步中,我们将超越单一智能体,构建一个小团队,让智能体之间能够互相委派任务!


Step 3: Building an Agent Team - Delegation for Greetings & Farewells第 3 步:构建智能体团队 - 问候与告别的委派

In Steps 1 and 2, we built and experimented with a single agent focused solely on weather lookups. While effective for its specific task, real-world applications often involve handling a wider variety of user interactions. We could keep adding more tools and complex instructions to our single weather agent, but this can quickly become unmanageable and less efficient.在第 1 和第 2 步中,我们构建并试验了一个仅专注于天气查询的单一智能体。虽然对于特定任务很有效,但现实世界的应用通常涉及处理更广泛的用户交互。我们可以继续在单一天气智能体中添加更多工具和复杂的指令,但这很快会变得难以管理且效率低下。

A more robust approach is to build an Agent Team. This involves:一种更稳健的方法是构建一个智能体团队。这涉及:

  1. Creating multiple, specialized agents, each designed for a specific capability (e.g., one for weather, one for greetings, one for calculations).创建多个专门的智能体,每个智能体专为特定能力设计(例如:一个负责天气,一个负责问候,一个负责计算)。
  2. Designating a root agent (or orchestrator) that receives the initial user request.指定一个根智能体(或编排器)来接收初始用户请求。
  3. Enabling the root agent to delegate the request to the most appropriate specialized sub-agent based on the user's intent.使根智能体能够根据用户的意图将请求委派给最合适的专业子智能体。

Why build an Agent Team?为什么要构建智能体团队?

  • Modularity: Easier to develop, test, and maintain individual agents.模块化:更易于开发、测试和维护单个智能体。
  • Specialization: Each agent can be fine-tuned (instructions, model choice) for its specific task.专业化:每个智能体都可以针对其特定任务进行微调(指令、模型选择)。
  • Scalability: Simpler to add new capabilities by adding new agents.可扩展性:通过添加新智能体来增加新功能更简单。
  • Efficiency: Allows using potentially simpler/cheaper models for simpler tasks (like greetings).效率:允许在简单任务(如问候)中使用可能更简单/更便宜的模型。

In this step, we will:

  1. Define simple tools for handling greetings (say_hello) and farewells (say_goodbye).定义用于处理问候 (say_hello) 和告别 (say_goodbye) 的简单工具。
  2. Create two new specialized sub-agents: greeting_agent and farewell_agent.创建两个新的专业子智能体:greeting_agent 和 farewell_agent。
  3. Update our main weather agent (weather_agent_v2) to act as the root agent.更新我们的主天气智能体 (weather_agent_v2) 以充当根智能体。
  4. Configure the root agent with its sub-agents, enabling automatic delegation.使用其子智能体配置根智能体,实现自动委派。
  5. Test the delegation flow by sending different types of requests to the root agent.通过向根智能体发送不同类型的请求来测试委派流程。

1. Define Tools for Sub-Agents1. 定义子智能体工具

First, let's create the simple Python functions that will serve as tools for our new specialist agents. Remember, clear docstrings are vital for the agents that will use them.首先,让我们创建将作为我们新专业智能体工具的简单 Python 函数。记住,清晰的文档字符串对于将要使用它们的智能体至关重要。

# @title Define Tools for Greeting and Farewell Agents
from typing import Optional # Make sure to import Optional

# Ensure 'get_weather' from Step 1 is available if running this step independently.
# def get_weather(city: str) -> dict: ... (from Step 1)

def say_hello(name: Optional[str] = None) -> str:
    """Provides a simple greeting. If a name is provided, it will be used.

    Args:
        name (str, optional): The name of the person to greet. Defaults to a generic greeting if not provided.

    Returns:
        str: A friendly greeting message.
    """
    if name:
        greeting = f"Hello, {name}!"
        print(f"--- Tool: say_hello called with name: {name} ---")
    else:
        greeting = "Hello there!" # Default greeting if name is None or not explicitly passed
        print(f"--- Tool: say_hello called without a specific name (name_arg_value: {name}) ---")
    return greeting

def say_goodbye() -> str:
    """Provides a simple farewell message to conclude the conversation."""
    print(f"--- Tool: say_goodbye called ---")
    return "Goodbye! Have a great day."

print("Greeting and Farewell tools defined.")

# Optional self-test
print(say_hello("Alice"))
print(say_hello()) # Test with no argument (should use default "Hello there!")
print(say_hello(name=None)) # Test with name explicitly as None (should use default "Hello there!")

2. Define the Sub-Agents (Greeting & Farewell)2. 定义子智能体(问候与告别)

Now, create the Agent instances for our specialists. Notice their highly focused instruction and, critically, their clear description. The description is the primary information the root agent uses to decide when to delegate to these sub-agents.现在,为我们的专家创建智能体实例。注意它们高度集中的指令,以及至关重要的清晰描述。描述是根智能体决定何时委派给这些子智能体的主要信息。

Best Practice: Sub-agent description fields should accurately and concisely summarize their specific capability. This is crucial for effective automatic delegation.最佳实践:子智能体描述字段应准确且简洁地总结其特定能力。这对有效的自动委派至关重要。

Best Practice: Sub-agent instruction fields should be tailored to their limited scope, telling them exactly what to do and what not to do (e.g., "Your only task is...").最佳实践:子智能体指令字段应根据其有限的范围进行定制,明确告诉它们该做什么和不该做什么(例如,“你唯一的任务是...”)。

# @title Define Greeting and Farewell Sub-Agents

# If you want to use models other than Gemini, Ensure LiteLlm is imported and API keys are set (from Step 0/2)
# from google.adk.models.lite_llm import LiteLlm
# MODEL_GPT_4O, MODEL_CLAUDE_SONNET etc. should be defined
# Or else, continue to use: model = MODEL_GEMINI_FLASH

# --- Greeting Agent ---
greeting_agent = None
try:
    greeting_agent = Agent(
        # Using a potentially different/cheaper model for a simple task
        model = MODEL_GEMINI_FLASH,
        # model=LiteLlm(model=MODEL_GPT_4O), # If you would like to experiment with other models
        name="greeting_agent",
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting to the user. "
                    "Use the 'say_hello' tool to generate the greeting. "
                    "If the user provides their name, make sure to pass it to the tool. "
                    "Do not engage in any other conversation or tasks.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.", # Crucial for delegation
        tools=[say_hello],
    )
    print(f"✅ Agent '{greeting_agent.name}' created using model '{greeting_agent.model}'.")
except Exception as e:
    print(f"❌ Could not create Greeting agent. Check API Key ({greeting_agent.model}). Error: {e}")

# --- Farewell Agent ---
farewell_agent = None
try:
    farewell_agent = Agent(
        # Can use the same or a different model
        model = MODEL_GEMINI_FLASH,
        # model=LiteLlm(model=MODEL_GPT_4O), # If you would like to experiment with other models
        name="farewell_agent",
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message. "
                    "Use the 'say_goodbye' tool when the user indicates they are leaving or ending the conversation "
                    "(e.g., using words like 'bye', 'goodbye', 'thanks bye', 'see you'). "
                    "Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.", # Crucial for delegation
        tools=[say_goodbye],
    )
    print(f"✅ Agent '{farewell_agent.name}' created using model '{farewell_agent.model}'.")
except Exception as e:
    print(f"❌ Could not create Farewell agent. Check API Key ({farewell_agent.model}). Error: {e}")

3. Define the Root Agent (Weather Agent v2) with Sub-Agents3. 定义带有子智能体的根智能体(天气智能体 v2)

Now, we upgrade our weather_agent. The key changes are:现在,我们升级我们的 weather_agent。关键变化是:

  • Adding the sub_agents parameter: We pass a list containing the greeting_agent and farewell_agent instances we just created.添加 sub_agents 参数:我们传递一个包含刚刚创建的 greeting_agent 和 farewell_agent 实例的列表。
  • Updating the instruction: We explicitly tell the root agent about its sub-agents and when it should delegate tasks to them.更新指令:我们明确告知根智能体关于其子智能体的信息,以及何时应该将任务委派给它们。

Key Concept: Automatic Delegation (Auto Flow) By providing the sub_agents list, ADK enables automatic delegation. When the root agent receives a user query, its LLM considers not only its own instructions and tools but also the description of each sub-agent. If the LLM determines that a query aligns better with a sub-agent's described capability (e.g., "Handles simple greetings"), it will automatically generate a special internal action to transfer control to that sub-agent for that turn. The sub-agent then processes the query using its own model, instructions, and tools.核心概念:自动委派(自动流转):通过提供 sub_agents 列表,ADK 启用了自动委派。当根智能体收到用户查询时,其 LLM 不仅会考虑自己的指令和工具,还会考虑每个子智能体的描述。如果 LLM 确定查询与某个子智能体的描述能力(例如“处理简单问候”)更匹配,它将自动生成一个特殊的内部动作,将该轮次的控制权转移给该子智能体。然后,子智能体使用其自己的模型、指令和工具处理查询。

Best Practice: Ensure the root agent's instructions clearly guide its delegation decisions. Mention the sub-agents by name and describe the conditions under which delegation should occur.最佳实践:确保根智能体的指令清晰地引导其委派决策。按名称提及子智能体,并描述应发生委派的条件。

# @title Define the Root Agent with Sub-Agents

# Ensure sub-agents were created successfully before defining the root agent.
# Also ensure the original 'get_weather' tool is defined.
root_agent = None
runner_root = None # Initialize runner

if greeting_agent and farewell_agent and 'get_weather' in globals():
    # Let's use a capable Gemini model for the root agent to handle orchestration
    root_agent_model = MODEL_GEMINI_FLASH

    weather_agent_team = Agent(
        name="weather_agent_v2", # Give it a new version name
        model=root_agent_model,
        description="The main coordinator agent. Handles weather requests and delegates greetings/farewells to specialists.",
        instruction="You are the main Weather Agent coordinating a team. Your primary responsibility is to provide weather information. "
                    "Use the 'get_weather' tool ONLY for specific weather requests (e.g., 'weather in London'). "
                    "You have specialized sub-agents: "
                    "1. 'greeting_agent': Handles simple greetings like 'Hi', 'Hello'. Delegate to it for these. "
                    "2. 'farewell_agent': Handles simple farewells like 'Bye', 'See you'. Delegate to it for these. "
                    "Analyze the user's query. If it's a greeting, delegate to 'greeting_agent'. If it's a farewell, delegate to 'farewell_agent'. "
                    "If it's a weather request, handle it yourself using 'get_weather'. "
                    "For anything else, respond appropriately or state you cannot handle it.",
        tools=[get_weather], # Root agent still needs the weather tool for its core task
        # Key change: Link the sub-agents here!
        sub_agents=[greeting_agent, farewell_agent]
    )
    print(f"✅ Root Agent '{weather_agent_team.name}' created using model '{root_agent_model}' with sub-agents: {[sa.name for sa in weather_agent_team.sub_agents]}")

else:
    print("❌ Cannot create root agent because one or more sub-agents failed to initialize or 'get_weather' tool is missing.")
    if not greeting_agent: print(" - Greeting Agent is missing.")
    if not farewell_agent: print(" - Farewell Agent is missing.")
    if 'get_weather' not in globals(): print(" - get_weather function is missing.")

4. Interact with the Agent Team4. 与智能体团队交互

Now that we've defined our root agent (weather_agent_team - Note: Ensure this variable name matches the one defined in the previous code block, likely # @title Define the Root Agent with Sub-Agents, which might have named it root_agent) with its specialized sub-agents, let's test the delegation mechanism.现在我们已经定义了根智能体 (weather_agent_team - 注意:确保此变量名与上一个代码块中定义的匹配,即 # @title Define the Root Agent with Sub-Agents,可能命名为 root_agent) 及其专业子智能体,让我们测试委派机制。

The following code block will:以下代码块将:

  1. Define an async function run_team_conversation.定义一个异步函数 run_team_conversation。
  2. Inside this function, create a new, dedicated InMemorySessionService and a specific session (session_001_agent_team) just for this test run. This isolates the conversation history for testing the team dynamics.在此函数内部,创建一个新的、专用的 InMemorySessionService 和一个特定会话 (session_001_agent_team) 仅用于此测试运行。这隔离了对话历史以测试团队动态。
  3. Create a Runner (runner_agent_team) configured to use our weather_agent_team (the root agent) and the dedicated session service.创建一个 Runner (runner_agent_team),配置为使用我们的 weather_agent_team(根智能体)和专用的会话服务。
  4. Use our updated call_agent_async function to send different types of queries (greeting, weather request, farewell) to the runner_agent_team. We explicitly pass the runner, user ID, and session ID for this specific test.使用我们更新后的 call_agent_async 函数向 runner_agent_team 发送不同类型的查询(问候、天气请求、告别)。我们显式传递此特定测试的运行器、用户 ID 和会话 ID。
  5. Immediately execute the run_team_conversation function.立即执行 run_team_conversation 函数。

We expect the following flow:我们预期流程如下:

  1. The "Hello there!" query goes to runner_agent_team.“Hello there!” 查询发送给 runner_agent_team。
  2. The root agent (weather_agent_team) receives it and, based on its instructions and the greeting_agent's description, delegates the task.根智能体 (weather_agent_team) 收到它,并根据其指令和 greeting_agent 的描述,委派该任务。
  3. greeting_agent handles the query, calls its say_hello tool, and generates the response.greeting_agent 处理查询,调用其 say_hello 工具,并生成响应。
  4. The "What is the weather in New York?" query is not delegated and is handled directly by the root agent using its get_weather tool.“What is the weather in New York?” 查询不被委派,由根智能体直接使用其 get_weather 工具处理。
  5. The "Thanks, bye!" query is delegated to the farewell_agent, which uses its say_goodbye tool.“Thanks, bye!” 查询被委派给 farewell_agent,它使用其 say_goodbye 工具。
# @title Interact with the Agent Team
import asyncio # Ensure asyncio is imported

# Ensure the root agent (e.g., 'weather_agent_team' or 'root_agent' from the previous cell) is defined.
# Ensure the call_agent_async function is defined.

# Check if the root agent variable exists before defining the conversation function
root_agent_var_name = 'root_agent' # Default name from Step 3 guide
if 'weather_agent_team' in globals(): # Check if user used this name instead
    root_agent_var_name = 'weather_agent_team'
elif 'root_agent' not in globals():
    print("⚠️ Root agent ('root_agent' or 'weather_agent_team') not found. Cannot define run_team_conversation.")
    # Assign a dummy value to prevent NameError later if the code block runs anyway
    root_agent = None # Or set a flag to prevent execution

# Only define and run if the root agent exists
if root_agent_var_name in globals() and globals()[root_agent_var_name]:
    # Define the main async function for the conversation logic.
    # The 'await' keywords INSIDE this function are necessary for async operations.
    async def run_team_conversation():
        print("\n--- Testing Agent Team Delegation ---")
        session_service = InMemorySessionService()
        APP_NAME = "weather_tutorial_agent_team"
        USER_ID = "user_1_agent_team"
        SESSION_ID = "session_001_agent_team"
        session = await session_service.create_session(
            app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
        )
        print(f"Session created: App='{APP_NAME}', User='{USER_ID}', Session='{SESSION_ID}'")

        actual_root_agent = globals()[root_agent_var_name]
        runner_agent_team = Runner( # Or use InMemoryRunner
            agent=actual_root_agent,
            app_name=APP_NAME,
            session_service=session_service
        )
        print(f"Runner created for agent '{actual_root_agent.name}'.")

        # --- Interactions using await (correct within async def) ---
        await call_agent_async(query = "Hello there!",
                               runner=runner_agent_team,
                               user_id=USER_ID,
                               session_id=SESSION_ID)
        await call_agent_async(query = "What is the weather in New York?",
                               runner=runner_agent_team,
                               user_id=USER_ID,
                               session_id=SESSION_ID)
        await call_agent_async(query = "Thanks, bye!",
                               runner=runner_agent_team,
                               user_id=USER_ID,
                               session_id=SESSION_ID)

    # --- Execute the `run_team_conversation` async function ---
    # Choose ONE of the methods below based on your environment.
    # Note: This may require API keys for the models used!

    # METHOD 1: Direct await (Default for Notebooks/Async REPLs)
    # If your environment supports top-level await (like Colab/Jupyter notebooks),
    # it means an event loop is already running, so you can directly await the function.
    print("Attempting execution using 'await' (default for notebooks)...")
    await run_team_conversation()

    # METHOD 2: asyncio.run (For Standard Python Scripts [.py])
    # If running this code as a standard Python script from your terminal,
    # the script context is synchronous. `asyncio.run()` is needed to
    # create and manage an event loop to execute your async function.
    # To use this method:
    # 1. Comment out the `await run_team_conversation()` line above.
    # 2. Uncomment the following block:
    """
    import asyncio
    if __name__ == "__main__": # Ensures this runs only when script is executed directly
        print("Executing using 'asyncio.run()' (for standard Python scripts)...")
        try:
            # This creates an event loop, runs your async function, and closes the loop.
            asyncio.run(run_team_conversation())
        except Exception as e:
            print(f"An error occurred: {e}")
    """

else:
    # This message prints if the root agent variable wasn't found earlier
    print("\n⚠️ Skipping agent team conversation execution as the root agent was not successfully defined in a previous step.")

Look closely at the output logs, especially the --- Tool: ... called --- messages. You should observe:仔细查看输出日志,特别是 --- Tool: ... called --- 消息。你应该观察到:

  • For "Hello there!", the say_hello tool was called (indicating greeting_agent handled it).对于 “Hello there!”,调用了 say_hello 工具(表明 greeting_agent 处理了它)。
  • For "What is the weather in New York?", the get_weather tool was called (indicating the root agent handled it).对于 “What is the weather in New York?”,调用了 get_weather 工具(表明根智能体处理了它)。
  • For "Thanks, bye!", the say_goodbye tool was called (indicating farewell_agent handled it).对于 “Thanks, bye!”,调用了 say_goodbye 工具(表明 farewell_agent 处理了它)。

This confirms successful automatic delegation! The root agent, guided by its instructions and the descriptions of its sub_agents, correctly routed user requests to the appropriate specialist agent within the team.这证实了自动委派成功!根智能体在指令和子智能体描述的引导下,正确地将用户请求路由到了团队内合适的专家智能体。

You've now structured your application with multiple collaborating agents. This modular design is fundamental for building more complex and capable agent systems. In the next step, we'll give our agents the ability to remember information across turns using session state.你现在已经用多个协作智能体构建了你的应用。这种模块化设计是构建更复杂、更有能力智能体系统的基础。在下一步中,我们将赋予智能体使用会话状态跨轮次记忆信息的能力。

Step 4: Adding Memory and Personalization with Session State第 4 步:通过会话状态添加记忆与个性化

So far, our agent team can handle different tasks through delegation, but each interaction starts fresh – the agents have no memory of past conversations or user preferences within a session. To create more sophisticated and context-aware experiences, agents need memory. ADK provides this through Session State.到目前为止,我们的智能体团队可以通过委派处理不同任务,但每次交互都是全新的开始——智能体在会话中对过去的对话或用户偏好没有任何记忆。为了创建更复杂且具有上下文感知的体验,智能体需要记忆。ADK 通过会话状态(Session State)提供此功能。

What is Session State?什么是会话状态?

  • It's a Python dictionary (session.state) tied to a specific user session (identified by APP_NAME, USER_ID, SESSION_ID).它是一个绑定到特定用户会话(由 APP_NAME、USER_ID、SESSION_ID 标识)的 Python 字典 (session.state)。
  • It persists information across multiple conversational turns within that session.它在会话内的多个对话轮次中持久保存信息。
  • Agents and Tools can read from and write to this state, allowing them to remember details, adapt behavior, and personalize responses.智能体和工具可以从该状态读取或写入该状态,从而允许它们记住细节、调整行为并个性化响应。

How Agents Interact with State:智能体如何与状态交互:

  1. ToolContext (Primary Method): Tools can accept a ToolContext object (automatically provided by ADK for any parameter annotated ToolContext, whatever its position). This object gives direct access to the session state via tool_context.state, allowing tools to read preferences or save results during execution.ToolContext(主要方法):工具可以接受一个 ToolContext 对象(ADK 会自动为任何标注为 ToolContext 的参数提供该对象,无论其位置如何)。该对象通过 tool_context.state 提供对会话状态的直接访问,允许工具在执行期间读取偏好或保存结果。
  2. output_key (Auto-Save Agent Response): An Agent can be configured with an output_key="your_key". ADK will then automatically save the agent's final textual response for a turn into session.state["your_key"].output_key(自动保存智能体响应):智能体可以配置 output_key="your_key"。ADK 将自动把智能体在该轮次的最终文本响应保存到 session.state["your_key"] 中。

In this step, we will enhance our Weather Bot team by:在此步骤中,我们将通过以下方式增强我们的天气机器人团队:

  1. Using a new InMemorySessionService to demonstrate state in isolation.使用新的 InMemorySessionService 来演示隔离状态。
  2. Initializing session state with a user preference for temperature_unit.初始化具有 temperature_unit 用户偏好的会话状态。
  3. Creating a state-aware version of the weather tool (get_weather_stateful) that reads this preference via ToolContext and adjusts its output format (Celsius/Fahrenheit).创建一个具有状态感知能力的天气工具版本 (get_weather_stateful),它通过 ToolContext 读取此偏好并调整其输出格式(摄氏度/华氏度)。
  4. Updating the root agent to use this stateful tool and configuring it with an output_key to automatically save its final weather report to the session state.更新根智能体以使用此有状态工具,并配置 output_key 以自动将其最终天气报告保存到会话状态。
  5. Running a conversation to observe how the initial state affects the tool, how manual state changes alter subsequent behavior, and how output_key persists the agent's response.运行对话以观察初始状态如何影响工具、手动状态更改如何改变后续行为,以及 output_key 如何持久化智能体的响应。

1. Initialize New Session Service and State1. 初始化新的会话服务和状态

To clearly demonstrate state management without interference from prior steps, we'll instantiate a new InMemorySessionService. We'll also create a session with an initial state defining the user's preferred temperature unit.为了清楚地演示状态管理而不受之前步骤的干扰,我们将实例化一个新的 InMemorySessionService。我们还将创建一个会话,其初始状态定义了用户的首选温度单位。

# @title 1. Initialize New Session Service and State

# Import necessary session components
from google.adk.sessions import InMemorySessionService

# Create a NEW session service instance for this state demonstration
session_service_stateful = InMemorySessionService()
print("✅ New InMemorySessionService created for state demonstration.")

# Define a NEW session ID for this part of the tutorial
SESSION_ID_STATEFUL = "session_state_demo_001"
USER_ID_STATEFUL = "user_state_demo"

# Define initial state data - user prefers Celsius initially
initial_state = {
    "user_preference_temperature_unit": "Celsius"
}

# Create the session, providing the initial state
session_stateful = await session_service_stateful.create_session(
    app_name=APP_NAME, # Use the consistent app name
    user_id=USER_ID_STATEFUL,
    session_id=SESSION_ID_STATEFUL,
    state=initial_state # <<< Initialize state during creation
)
print(f"✅ Session '{SESSION_ID_STATEFUL}' created for user '{USER_ID_STATEFUL}'.")

# Verify the initial state was set correctly
retrieved_session = await session_service_stateful.get_session(app_name=APP_NAME,
                                                         user_id=USER_ID_STATEFUL,
                                                         session_id = SESSION_ID_STATEFUL)
print("\n--- Initial Session State ---")
if retrieved_session:
    print(retrieved_session.state)
else:
    print("Error: Could not retrieve session.")

2. Create State-Aware Weather Tool (get_weather_stateful)2. 创建具有状态感知能力的天气工具 (get_weather_stateful)

Now, we create a new version of the weather tool. Its key feature is accepting tool_context: ToolContext which allows it to access tool_context.state. It will read the user_preference_temperature_unit and format the temperature accordingly.现在,我们创建天气工具的新版本。它的关键特性是接受 tool_context: ToolContext,这允许它访问 tool_context.state。它将读取 user_preference_temperature_unit 并相应地格式化温度。

  • Key Concept: ToolContext This object is the bridge allowing your tool logic to interact with the session's context, including reading and writing state variables. ADK finds the parameter by its ToolContext annotation and injects it automatically, so it can sit at any position in your tool function's signature. The annotated parameter is also hidden from the schema the LLM sees.核心概念:ToolContext:此对象是允许你的工具逻辑与会话上下文交互的桥梁,包括读取和写入状态变量。ADK 通过 ToolContext 注解找到该参数并自动注入它,因此它可以位于你工具函数签名的任何位置。标注的参数也会对 LLM 看到的模式隐藏。

  • Best Practice: When reading from state, use dictionary.get('key', default_value) to handle cases where the key might not exist yet, ensuring your tool doesn't crash.最佳实践:从状态读取时,使用 dictionary.get('key', default_value) 来处理键可能尚不存在的情况,确保你的工具不会崩溃。

from google.adk.tools.tool_context import ToolContext

def get_weather_stateful(city: str, tool_context: ToolContext) -> dict:
    """Retrieves weather, converts temp unit based on session state."""
    print(f"--- Tool: get_weather_stateful called for {city} ---")

    # --- Read preference from state ---
    preferred_unit = tool_context.state.get("user_preference_temperature_unit", "Celsius") # Default to Celsius
    print(f"--- Tool: Reading state 'user_preference_temperature_unit': {preferred_unit} ---")

    city_normalized = city.lower().replace(" ", "")

    # Mock weather data (always stored in Celsius internally)
    mock_weather_db = {
        "newyork": {"temp_c": 25, "condition": "sunny"},
        "london": {"temp_c": 15, "condition": "cloudy"},
        "tokyo": {"temp_c": 18, "condition": "light rain"},
    }

    if city_normalized in mock_weather_db:
        data = mock_weather_db[city_normalized]
        temp_c = data["temp_c"]
        condition = data["condition"]

        # Format temperature based on state preference
        if preferred_unit == "Fahrenheit":
            temp_value = (temp_c * 9/5) + 32 # Calculate Fahrenheit
            temp_unit = "°F"
        else: # Default to Celsius
            temp_value = temp_c
            temp_unit = "°C"

        report = f"The weather in {city.capitalize()} is {condition} with a temperature of {temp_value:.0f}{temp_unit}."
        result = {"status": "success", "report": report}
        print(f"--- Tool: Generated report in {preferred_unit}. Result: {result} ---")

        # Example of writing back to state (optional for this tool)
        tool_context.state["last_city_checked_stateful"] = city
        print(f"--- Tool: Updated state 'last_city_checked_stateful': {city} ---")

        return result
    else:
        # Handle city not found
        error_msg = f"Sorry, I don't have weather information for '{city}'."
        print(f"--- Tool: City '{city}' not found. ---")
        return {"status": "error", "error_message": error_msg}

print("✅ State-aware 'get_weather_stateful' tool defined.")

3. Redefine Sub-Agents and Update Root Agent3. 重新定义子智能体并更新根智能体

To ensure this step is self-contained and builds correctly, we first redefine the greeting_agent and farewell_agent exactly as they were in Step 3. Then, we define our new root agent (weather_agent_v4_stateful):为了确保此步骤是自包含的并正确构建,我们首先完全按照第 3 步中的定义重新定义 greeting_agent 和 farewell_agent。然后,定义我们的新根智能体 (weather_agent_v4_stateful):

  • It uses the new get_weather_stateful tool.它使用新的 get_weather_stateful 工具。
  • It includes the greeting and farewell sub-agents for delegation.它包含用于委派的问候和告别子智能体。
  • Crucially, it sets output_key="last_weather_report" which automatically saves its final weather response to the session state.关键在于,它设置了 output_key="last_weather_report",这会自动将其最终天气响应保存到会话状态中。
# @title 3. Redefine Sub-Agents and Update Root Agent with output_key

# Ensure necessary imports: Agent, LiteLlm, Runner
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.adk.runners import Runner
# Ensure tools 'say_hello', 'say_goodbye' are defined (from Step 3)
# Ensure model constants MODEL_GPT_4O, MODEL_GEMINI_FLASH etc. are defined

# --- Redefine Greeting Agent (from Step 3) ---
greeting_agent = None
try:
    greeting_agent = Agent(
        model=MODEL_GEMINI_FLASH,
        name="greeting_agent",
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting using the 'say_hello' tool. Do nothing else.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.",
        tools=[say_hello],
    )
    print(f"✅ Agent '{greeting_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Greeting agent. Error: {e}")

# --- Redefine Farewell Agent (from Step 3) ---
farewell_agent = None
try:
    farewell_agent = Agent(
        model=MODEL_GEMINI_FLASH,
        name="farewell_agent",
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message using the 'say_goodbye' tool. Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.",
        tools=[say_goodbye],
    )
    print(f"✅ Agent '{farewell_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Farewell agent. Error: {e}")

# --- Define the Updated Root Agent ---
root_agent_stateful = None
runner_root_stateful = None # Initialize runner

# Check prerequisites before creating the root agent
if greeting_agent and farewell_agent and 'get_weather_stateful' in globals():

    root_agent_model = MODEL_GEMINI_FLASH # Choose orchestration model

    root_agent_stateful = Agent(
        name="weather_agent_v4_stateful", # New version name
        model=root_agent_model,
        description="Main agent: Provides weather (state-aware unit), delegates greetings/farewells, saves report to state.",
        instruction="You are the main Weather Agent. Your job is to provide weather using 'get_weather_stateful'. "
                    "The tool will format the temperature based on user preference stored in state. "
                    "Delegate simple greetings to 'greeting_agent' and farewells to 'farewell_agent'. "
                    "Handle only weather requests, greetings, and farewells.",
        tools=[get_weather_stateful], # Use the state-aware tool
        sub_agents=[greeting_agent, farewell_agent], # Include sub-agents
        output_key="last_weather_report" # <<< Auto-save agent's final weather response
    )
    print(f"✅ Root Agent '{root_agent_stateful.name}' created using stateful tool and output_key.")

    # --- Create Runner for this Root Agent & NEW Session Service ---
    runner_root_stateful = Runner(
        agent=root_agent_stateful,
        app_name=APP_NAME,
        session_service=session_service_stateful # Use the NEW stateful session service
    )
    print(f"✅ Runner created for stateful root agent '{runner_root_stateful.agent.name}' using stateful session service.")

else:
    print("❌ Cannot create stateful root agent. Prerequisites missing.")
    if not greeting_agent: print(" - greeting_agent definition missing.")
    if not farewell_agent: print(" - farewell_agent definition missing.")
    if 'get_weather_stateful' not in globals(): print(" - get_weather_stateful tool missing.")

4. Interact and Test State Flow4. 交互并测试状态流

Now, let's execute a conversation designed to test the state interactions using the runner_root_stateful (associated with our stateful agent and the session_service_stateful). We'll use the call_agent_async function defined earlier, ensuring we pass the correct runner, user ID (USER_ID_STATEFUL), and session ID (SESSION_ID_STATEFUL).现在,让我们执行一个旨在测试状态交互的对话,使用 runner_root_stateful(与我们的有状态智能体和 session_service_stateful 相关联)。我们将使用之前定义的 call_agent_async 函数,确保传递正确的运行器、用户 ID (USER_ID_STATEFUL) 和会话 ID (SESSION_ID_STATEFUL)。

The conversation flow will be:对话流程将是:

  1. Check weather (London): The get_weather_stateful tool should read the initial "Celsius" preference from the session state initialized in Section 1. The root agent's final response (the weather report in Celsius) should get saved to state['last_weather_report'] via the output_key configuration.查询天气(伦敦):get_weather_stateful 工具应读取在第 1 节中初始化的会话状态中的初始“摄氏度”偏好。根智能体的最终响应(摄氏度天气报告)应通过 output_key 配置保存到 state['last_weather_report']。
  2. Manually update state: We will directly modify the state stored within the InMemorySessionService instance (session_service_stateful).
    • Why direct modification? The session_service.get_session() method returns a copy of the session. Modifying that copy wouldn't affect the state used in subsequent agent runs. For this testing scenario with InMemorySessionService, we access the internal sessions dictionary to change the actual stored state value for user_preference_temperature_unit to "Fahrenheit". Note: In real applications, state changes are typically triggered by tools or agent logic returning EventActions(state_delta=...), not direct manual updates.为什么直接修改?session_service.get_session() 方法返回会话的副本。修改该副本不会影响后续智能体运行中使用的状态。对于使用 InMemorySessionService 的此测试场景,我们访问内部 sessions 字典来更改 user_preference_temperature_unit 的实际存储状态值,将其改为“华氏度”。注意:在实际应用中,状态更改通常由返回 EventActions(state_delta=...) 的工具或智能体逻辑触发,而不是直接手动更新。
  3. Check weather again (New York): The get_weather_stateful tool should now read the updated "Fahrenheit" preference from the state and convert the temperature accordingly. The root agent's new response (weather in Fahrenheit) will overwrite the previous value in state['last_weather_report'] due to the output_key.再次查询天气(纽约):get_weather_stateful 工具现在应从状态中读取更新后的“华氏度”偏好,并相应地转换温度。由于 output_key 的存在,根智能体的新响应(华氏度天气)将覆盖 state['last_weather_report'] 中的先前值。
  4. Greet the agent: Verify that delegation to the greeting_agent still works correctly alongside the stateful operations.问候代理:验证在进行有状态操作的同时,委托给 greeting_agent 的功能是否依然有效。
  5. Inspect final state: After the conversation, we retrieve the session one last time (getting a copy) and print its state to confirm the user_preference_temperature_unit is indeed "Fahrenheit", observe the final value saved by output_key (which will be the previous weather report in this run), and see the last_city_checked_stateful value written by the tool.检查最终状态:对话结束后,我们最后一次检索会话(获取副本)并打印其状态,以确认 user_preference_temperature_unit 的值确实为“Fahrenheit”(华氏度),查看由 output_key 保存的最终值(在本轮运行中,该值将是之前的天气报告),并观察工具写入的 last_city_checked_stateful 值。
# @title 4. Interact to Test State Flow and output_key
import asyncio # Ensure asyncio is imported

# Ensure the stateful runner (runner_root_stateful) is available from the previous cell
# Ensure call_agent_async, USER_ID_STATEFUL, SESSION_ID_STATEFUL, APP_NAME are defined

if 'runner_root_stateful' in globals() and runner_root_stateful:
    # Define the main async function for the stateful conversation logic.
    # The 'await' keywords INSIDE this function are necessary for async operations.
    async def run_stateful_conversation():
        print("\n--- Testing State: Temp Unit Conversion & output_key ---")

        # 1. Check weather (Uses initial state: Celsius)
        print("--- Turn 1: Requesting weather in London (expect Celsius) ---")
        await call_agent_async(query= "What's the weather in London?",
                               runner=runner_root_stateful,
                               user_id=USER_ID_STATEFUL,
                               session_id=SESSION_ID_STATEFUL
                              )

        # 2. Manually update state preference to Fahrenheit - DIRECTLY MODIFY STORAGE
        print("\n--- Manually Updating State: Setting unit to Fahrenheit ---")
        try:
            # Access the internal storage directly - THIS IS SPECIFIC TO InMemorySessionService for testing
            # NOTE: In production with persistent services (Database, VertexAI), you would
            # typically update state via agent actions or specific service APIs if available,
            # not by direct manipulation of internal storage.
            stored_session = session_service_stateful.sessions[APP_NAME][USER_ID_STATEFUL][SESSION_ID_STATEFUL]
            stored_session.state["user_preference_temperature_unit"] = "Fahrenheit"
            # Optional: You might want to update the timestamp as well if any logic depends on it
            # import time
            # stored_session.last_update_time = time.time()
            print(f"--- Stored session state updated. Current 'user_preference_temperature_unit': {stored_session.state.get('user_preference_temperature_unit', 'Not Set')} ---") # Added .get for safety
        except KeyError:
            print(f"--- Error: Could not retrieve session '{SESSION_ID_STATEFUL}' from internal storage for user '{USER_ID_STATEFUL}' in app '{APP_NAME}' to update state. Check IDs and if session was created. ---")
        except Exception as e:
             print(f"--- Error updating internal session state: {e} ---")

        # 3. Check weather again (Tool should now use Fahrenheit)
        # This will also update 'last_weather_report' via output_key
        print("\n--- Turn 2: Requesting weather in New York (expect Fahrenheit) ---")
        await call_agent_async(query= "Tell me the weather in New York.",
                               runner=runner_root_stateful,
                               user_id=USER_ID_STATEFUL,
                               session_id=SESSION_ID_STATEFUL
                              )

        # 4. Test basic delegation (should still work)
        # The greeting is authored by the delegated sub-agent, not the root agent,
        # so output_key does NOT fire: 'last_weather_report' keeps the NY report.
        print("\n--- Turn 3: Sending a greeting ---")
        await call_agent_async(query= "Hi!",
                               runner=runner_root_stateful,
                               user_id=USER_ID_STATEFUL,
                               session_id=SESSION_ID_STATEFUL
                              )

    # --- Execute the `run_stateful_conversation` async function ---
    # Choose ONE of the methods below based on your environment.

    # METHOD 1: Direct await (Default for Notebooks/Async REPLs)
    # If your environment supports top-level await (like Colab/Jupyter notebooks),
    # it means an event loop is already running, so you can directly await the function.
    print("Attempting execution using 'await' (default for notebooks)...")
    await run_stateful_conversation()

    # METHOD 2: asyncio.run (For Standard Python Scripts [.py])
    # If running this code as a standard Python script from your terminal,
    # the script context is synchronous. `asyncio.run()` is needed to
    # create and manage an event loop to execute your async function.
    # To use this method:
    # 1. Comment out the `await run_stateful_conversation()` line above.
    # 2. Uncomment the following block:
    """
    import asyncio
    if __name__ == "__main__": # Ensures this runs only when script is executed directly
        print("Executing using 'asyncio.run()' (for standard Python scripts)...")
        try:
            # This creates an event loop, runs your async function, and closes the loop.
            asyncio.run(run_stateful_conversation())
        except Exception as e:
            print(f"An error occurred: {e}")
    """

    # --- Inspect final session state after the conversation ---
    # This block runs after either execution method completes.
    print("\n--- Inspecting Final Session State ---")
    final_session = await session_service_stateful.get_session(app_name=APP_NAME,
                                                         user_id= USER_ID_STATEFUL,
                                                         session_id=SESSION_ID_STATEFUL)
    if final_session:
        # Use .get() for safer access to potentially missing keys
        print(f"Final Preference: {final_session.state.get('user_preference_temperature_unit', 'Not Set')}")
        print(f"Final Last Weather Report (from output_key): {final_session.state.get('last_weather_report', 'Not Set')}")
        print(f"Final Last City Checked (by tool): {final_session.state.get('last_city_checked_stateful', 'Not Set')}")
        # Print full state for detailed view
        # print(f"Full State Dict: {final_session.state}") # For detailed view
    else:
        print("\n❌ Error: Could not retrieve final session state.")

else:
    print("\n⚠️ Skipping state test conversation. Stateful root agent runner ('runner_root_stateful') is not available.")

By reviewing the conversation flow and the final session state printout, you can confirm:通过回顾对话流程和最终的会话状态打印输出,您可以确认:

  • State Read: The weather tool (get_weather_stateful) correctly read user_preference_temperature_unit from state, initially using "Celsius" for London.状态读取:天气工具 (get_weather_stateful) 正确地从状态中读取了 user_preference_temperature_unit,最初使用的是伦敦的“Celsius”(摄氏度)。
  • State Update: The direct modification successfully changed the stored preference to "Fahrenheit".状态更新:直接修改成功地将存储的偏好设置更改为“Fahrenheit”。
  • State Read (Updated): The tool subsequently read "Fahrenheit" when asked for New York's weather and performed the conversion.状态读取(已更新):随后在查询纽约天气时,该工具读取到了“Fahrenheit”并执行了转换。
  • Tool State Write: The tool successfully wrote the last_city_checked_stateful ("New York" after the second weather check) into the state via tool_context.state.工具状态写入:该工具通过 tool_context.state 成功将 last_city_checked_stateful(第二次天气查询后的“New York”)写入了状态中。
  • Delegation: The delegation to the greeting_agent for "Hi!" functioned correctly even after state modifications.委托:即使在修改状态后,委托给 greeting_agent 处理“Hi!”的功能依然正常运作。
  • output_key: The output_key="last_weather_report" successfully saved the root agent's final response for each turn where the root agent was the one ultimately responding. In this sequence, the final greeting ("Hello, there!") was generated by the delegated sub-agent rather than the root agent, so output_key was not triggered on that final turn, leaving the previous weather report intact in the session state.output_key:output_key="last_weather_report" 成功保存了根代理最终响应的每一轮对话内容(前提是根代理为最终响应者)。在此序列中,最后的问候(“Hello, there!”)是由委托的子代理生成的,而非根代理,因此最后一轮对话并未触发 output_key,从而在会话状态中保留了之前的天气报告。
  • Final State: The final check confirms the preference persisted as "Fahrenheit".最终状态:最后检查确认偏好设置已持久化为“Fahrenheit”。

You've now successfully integrated session state to personalize agent behavior using ToolContext, manually manipulated state for testing InMemorySessionService, and observed how output_key provides a simple mechanism for saving the agent's last response to state. This foundational understanding of state management is key as we proceed to implement safety guardrails using callbacks in the next steps.您现在已经成功集成了会话状态,利用 ToolContext 实现代理行为的个性化,手动操作状态以测试 InMemorySessionService,并观察了 output_key 如何提供一种简单的机制将代理的最后响应保存到状态中。这种对状态管理的基础理解对于我们接下来通过回调实现安全防护措施至关重要。


Step 5: Adding Safety - Input Guardrail with before_model_callback第 5 步:增加安全性 - 使用 before_model_callback 的输入防护栏

Our agent team is becoming more capable, remembering preferences and using tools effectively. However, in real-world scenarios, we often need safety mechanisms to control the agent's behavior before potentially problematic requests even reach the core Large Language Model (LLM).我们的代理团队能力正不断增强,能够记住偏好并有效使用工具。然而,在实际应用场景中,我们通常需要安全机制来在潜在的问题请求到达核心大语言模型 (LLM) 之前控制代理的行为。

ADK provides Callbacks – functions that allow you to hook into specific points in the agent's execution lifecycle. The before_model_callback is particularly useful for input safety.ADK 提供了回调 (Callbacks) 功能——即允许您挂载到代理执行生命周期中特定点的函数。before_model_callback 对于输入安全尤为有用。

What is before_model_callback?什么是 before_model_callback?

  • It's a Python function you define that ADK executes just before an agent sends its compiled request (including conversation history, instructions, and the latest user message) to the underlying LLM.这是一个由您定义的 Python 函数,ADK 会在代理将编译后的请求(包括对话历史、指令和最新的用户消息)发送给底层 LLM 之前执行该函数。
  • Purpose: Inspect the request, modify it if necessary, or block it entirely based on predefined rules.目的:检查请求,必要时进行修改,或根据预定义的规则完全拦截请求。

Common Use Cases:常见用例:

  • Input Validation/Filtering: Check if user input meets criteria or contains disallowed content (like PII or keywords).输入验证/过滤:检查用户输入是否符合标准或包含禁止内容(如个人身份信息 PII 或敏感关键词)。
  • Guardrails: Prevent harmful, off-topic, or policy-violating requests from being processed by the LLM.防护栏:防止有害、离题或违反政策的请求被 LLM 处理。
  • Dynamic Prompt Modification: Add timely information (e.g., from session state) to the LLM request context just before sending.动态提示词修改:在发送前将及时信息(例如来自会话状态的信息)添加到 LLM 请求上下文中。

How it Works:工作原理:

  1. Define a function accepting callback_context: CallbackContext and llm_request: LlmRequest.定义一个接受 callback_context: CallbackContext 和 llm_request: LlmRequest 的函数。

    • callback_context: Provides access to agent info, session state (callback_context.state), etc.callback_context:提供对代理信息、会话状态 (callback_context.state) 等的访问权限。
    • llm_request: Contains the full payload intended for the LLM (contents, config).llm_request:包含旨在发送给 LLM 的完整负载(内容、配置)。
  2. Inside the function:在函数内部:

    • Inspect: Examine llm_request.contents (especially the last user message).检查:检查 llm_request.contents(特别是最后一条用户消息)。
    • Modify (Use Caution): You can change parts of llm_request.修改(请谨慎操作):您可以更改 llm_request 的部分内容。
    • Block (Guardrail): Return an LlmResponse object. ADK will send this response back immediately, skipping the LLM call for that turn.拦截(防护栏):返回一个 LlmResponse 对象。ADK 将立即发送此响应,跳过该轮对话的 LLM 调用。
    • Allow: Return None. ADK proceeds to call the LLM with the (potentially modified) request.允许:返回 None。ADK 继续使用(可能已修改的)请求调用 LLM。

In this step, we will:

  1. Define a before_model_callback function (block_keyword_guardrail) that checks the user's input for a specific keyword ("BLOCK").定义一个 before_model_callback 函数 (block_keyword_guardrail),用于检查用户输入中是否包含特定关键词(“BLOCK”)。
  2. Update our stateful root agent (weather_agent_v4_stateful from Step 4) to use this callback.更新我们的有状态根代理(第 4 步中的 weather_agent_v4_stateful)以使用此回调。
  3. Create a new runner associated with this updated agent but using the same stateful session service to maintain state continuity.创建一个与此更新后的代理相关联的新运行器,但使用相同的有状态会话服务以保持状态连续性。
  4. Test the guardrail by sending both normal and keyword-containing requests.通过发送正常请求和包含关键词的请求来测试防护栏。

1. Define the Guardrail Callback Function1. 定义防护栏回调函数

This function will inspect the last user message within the llm_request content. If it finds "BLOCK" (case-insensitive), it constructs and returns an LlmResponse to block the flow; otherwise, it returns None.此函数将检查 llm_request 内容中的最后一条用户消息。如果发现“BLOCK”(不区分大小写),它将构建并返回一个 LlmResponse 来拦截流程;否则,返回 None。

# @title 1. Define the before_model_callback Guardrail

# Ensure necessary imports are available
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.genai import types # For creating response content
from typing import Optional

def block_keyword_guardrail(
    callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
    """
    Inspects the latest user message for 'BLOCK'. If found, blocks the LLM call
    and returns a predefined LlmResponse. Otherwise, returns None to proceed.
    """
    agent_name = callback_context.agent_name # Get the name of the agent whose model call is being intercepted
    print(f"--- Callback: block_keyword_guardrail running for agent: {agent_name} ---")

    # Extract the text from the latest user message in the request history
    last_user_message_text = ""
    if llm_request.contents:
        # Find the most recent message with role 'user'
        for content in reversed(llm_request.contents):
            if content.role == 'user' and content.parts:
                # Assuming text is in the first part for simplicity
                if content.parts[0].text:
                    last_user_message_text = content.parts[0].text
                    break # Found the last user message text

    print(f"--- Callback: Inspecting last user message: '{last_user_message_text[:100]}...' ---") # Log first 100 chars

    # --- Guardrail Logic ---
    keyword_to_block = "BLOCK"
    if keyword_to_block in last_user_message_text.upper(): # Case-insensitive check
        print(f"--- Callback: Found '{keyword_to_block}'. Blocking LLM call! ---")
        # Optionally, set a flag in state to record the block event
        callback_context.state["guardrail_block_keyword_triggered"] = True
        print(f"--- Callback: Set state 'guardrail_block_keyword_triggered': True ---")

        # Construct and return an LlmResponse to stop the flow and send this back instead
        return LlmResponse(
            content=types.Content(
                role="model", # Mimic a response from the agent's perspective
                parts=[types.Part(text=f"I cannot process this request because it contains the blocked keyword '{keyword_to_block}'.")],
            )
            # Note: You could also set an error_message field here if needed
        )
    else:
        # Keyword not found, allow the request to proceed to the LLM
        print(f"--- Callback: Keyword not found. Allowing LLM call for {agent_name}. ---")
        return None # Returning None signals ADK to continue normally

print("✅ block_keyword_guardrail function defined.")

2. Update Root Agent to Use the Callback2. 更新根代理以使用该回调

We redefine the root agent, adding the before_model_callback parameter and pointing it to our new guardrail function. We'll give it a new version name for clarity.我们重新定义根代理,添加 before_model_callback 参数并将其指向我们新的防护栏函数。为了清晰起见,我们将为其赋予一个新的版本名称。

Important: We need to redefine the sub-agents (greeting_agent, farewell_agent) and the stateful tool (get_weather_stateful) within this context if they are not already available from previous steps, ensuring the root agent definition has access to all its components.重要提示:如果子代理(greeting_agent, farewell_agent)和有状态工具 (get_weather_stateful) 尚未从之前的步骤中获得,我们需要在此上下文中重新定义它们,确保根代理定义能够访问其所有组件。

# @title 2. Update Root Agent with before_model_callback


# --- Redefine Sub-Agents (Ensures they exist in this context) ---
greeting_agent = None
try:
    # Use a defined model constant
    greeting_agent = Agent(
        model=MODEL_GEMINI_FLASH,
        name="greeting_agent", # Keep original name for consistency
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting using the 'say_hello' tool. Do nothing else.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.",
        tools=[say_hello],
    )
    print(f"✅ Sub-Agent '{greeting_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Greeting agent. Check Model/API Key ({greeting_agent.model}). Error: {e}")

farewell_agent = None
try:
    # Use a defined model constant
    farewell_agent = Agent(
        model=MODEL_GEMINI_FLASH,
        name="farewell_agent", # Keep original name
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message using the 'say_goodbye' tool. Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.",
        tools=[say_goodbye],
    )
    print(f"✅ Sub-Agent '{farewell_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Farewell agent. Check Model/API Key ({farewell_agent.model}). Error: {e}")


# --- Define the Root Agent with the Callback ---
root_agent_model_guardrail = None
runner_root_model_guardrail = None

# Check all components before proceeding
if greeting_agent and farewell_agent and 'get_weather_stateful' in globals() and 'block_keyword_guardrail' in globals():

    # Use a defined model constant
    root_agent_model = MODEL_GEMINI_FLASH

    root_agent_model_guardrail = Agent(
        name="weather_agent_v5_model_guardrail", # New version name for clarity
        model=root_agent_model,
        description="Main agent: Handles weather, delegates greetings/farewells, includes input keyword guardrail.",
        instruction="You are the main Weather Agent. Provide weather using 'get_weather_stateful'. "
                    "Delegate simple greetings to 'greeting_agent' and farewells to 'farewell_agent'. "
                    "Handle only weather requests, greetings, and farewells.",
        tools=[get_weather_stateful],
        sub_agents=[greeting_agent, farewell_agent], # Reference the redefined sub-agents
        output_key="last_weather_report", # Keep output_key from Step 4
        before_model_callback=block_keyword_guardrail # <<< Assign the guardrail callback
    )
    print(f"✅ Root Agent '{root_agent_model_guardrail.name}' created with before_model_callback.")

    # --- Create Runner for this Agent, Using SAME Stateful Session Service ---
    # Ensure session_service_stateful exists from Step 4
    if 'session_service_stateful' in globals():
        runner_root_model_guardrail = Runner(
            agent=root_agent_model_guardrail,
            app_name=APP_NAME, # Use consistent APP_NAME
            session_service=session_service_stateful # <<< Use the service from Step 4
        )
        print(f"✅ Runner created for guardrail agent '{runner_root_model_guardrail.agent.name}', using stateful session service.")
    else:
        print("❌ Cannot create runner. 'session_service_stateful' from Step 4 is missing.")

else:
    print("❌ Cannot create root agent with model guardrail. One or more prerequisites are missing or failed initialization:")
    if not greeting_agent: print("   - Greeting Agent")
    if not farewell_agent: print("   - Farewell Agent")
    if 'get_weather_stateful' not in globals(): print("   - 'get_weather_stateful' tool")
    if 'block_keyword_guardrail' not in globals(): print("   - 'block_keyword_guardrail' callback")

3. Interact to Test the Guardrail3. 交互以测试防护栏

Let's test the guardrail's behavior. We'll use the same session (SESSION_ID_STATEFUL) as in Step 4 to show that state persists across these changes.让我们测试防护栏的行为。我们将使用与第 4 步相同的会话 (SESSION_ID_STATEFUL),以展示状态在这些更改中依然保持不变。

  1. Send a normal weather request (should pass the guardrail and execute).发送一个正常的天气请求(应通过防护栏并执行)。
  2. Send a request containing "BLOCK" (should be intercepted by the callback).发送一个包含“BLOCK”的请求(应被回调拦截)。
  3. Send a greeting (should pass the root agent's guardrail, be delegated, and execute normally).发送一个问候(应通过根代理的防护栏,被委托,并正常执行)。
# @title 3. Interact to Test the Model Input Guardrail
import asyncio # Ensure asyncio is imported

# Ensure the runner for the guardrail agent is available
if 'runner_root_model_guardrail' in globals() and runner_root_model_guardrail:
    # Define the main async function for the guardrail test conversation.
    # The 'await' keywords INSIDE this function are necessary for async operations.
    async def run_guardrail_test_conversation():
        print("\n--- Testing Model Input Guardrail ---")

        # Use the runner for the agent with the callback and the existing stateful session ID
        # Define a helper lambda for cleaner interaction calls
        interaction_func = lambda query: call_agent_async(query,
                                                         runner_root_model_guardrail,
                                                         USER_ID_STATEFUL, # Use existing user ID
                                                         SESSION_ID_STATEFUL # Use existing session ID
                                                        )
        # 1. Normal request (Callback allows, should use Fahrenheit from previous state change)
        print("--- Turn 1: Requesting weather in London (expect allowed, Fahrenheit) ---")
        await interaction_func("What is the weather in London?")

        # 2. Request containing the blocked keyword (Callback intercepts)
        print("\n--- Turn 2: Requesting with blocked keyword (expect blocked) ---")
        await interaction_func("BLOCK the request for weather in Tokyo") # Callback should catch "BLOCK"

        # 3. Normal greeting (Callback allows root agent, delegation happens)
        print("\n--- Turn 3: Sending a greeting (expect allowed) ---")
        await interaction_func("Hello again")

    # --- Execute the `run_guardrail_test_conversation` async function ---
    # Choose ONE of the methods below based on your environment.

    # METHOD 1: Direct await (Default for Notebooks/Async REPLs)
    # If your environment supports top-level await (like Colab/Jupyter notebooks),
    # it means an event loop is already running, so you can directly await the function.
    print("Attempting execution using 'await' (default for notebooks)...")
    await run_guardrail_test_conversation()

    # METHOD 2: asyncio.run (For Standard Python Scripts [.py])
    # If running this code as a standard Python script from your terminal,
    # the script context is synchronous. `asyncio.run()` is needed to
    # create and manage an event loop to execute your async function.
    # To use this method:
    # 1. Comment out the `await run_guardrail_test_conversation()` line above.
    # 2. Uncomment the following block:
    """
    import asyncio
    if __name__ == "__main__": # Ensures this runs only when script is executed directly
        print("Executing using 'asyncio.run()' (for standard Python scripts)...")
        try:
            # This creates an event loop, runs your async function, and closes the loop.
            asyncio.run(run_guardrail_test_conversation())
        except Exception as e:
            print(f"An error occurred: {e}")
    """

    # --- Inspect final session state after the conversation ---
    # This block runs after either execution method completes.
    # Optional: Check state for the trigger flag set by the callback
    print("\n--- Inspecting Final Session State (After Guardrail Test) ---")
    # Use the session service instance associated with this stateful session
    final_session = await session_service_stateful.get_session(app_name=APP_NAME,
                                                         user_id=USER_ID_STATEFUL,
                                                         session_id=SESSION_ID_STATEFUL)
    if final_session:
        # Use .get() for safer access
        print(f"Guardrail Triggered Flag: {final_session.state.get('guardrail_block_keyword_triggered', 'Not Set (or False)')}")
        print(f"Last Weather Report: {final_session.state.get('last_weather_report', 'Not Set')}") # Should be London weather if successful
        print(f"Temperature Unit: {final_session.state.get('user_preference_temperature_unit', 'Not Set')}") # Should be Fahrenheit
        # print(f"Full State Dict: {final_session.state}") # For detailed view
    else:
        print("\n❌ Error: Could not retrieve final session state.")

else:
    print("\n⚠️ Skipping model guardrail test. Runner ('runner_root_model_guardrail') is not available.")

Observe the execution flow:观察执行流程:

  1. London Weather: The callback runs for weather_agent_v5_model_guardrail, inspects the message, prints "Keyword not found. Allowing LLM call.", and returns None. The agent proceeds, calls the get_weather_stateful tool (which uses the "Fahrenheit" preference from Step 4's state change), and returns the weather. This response updates last_weather_report via output_key.伦敦天气:回调针对 weather_agent_v5_model_guardrail 运行,检查消息,打印“Keyword not found. Allowing LLM call.”,并返回 None。代理继续执行,调用 get_weather_stateful 工具(使用第 4 步状态更改中的“Fahrenheit”偏好),并返回天气信息。该响应通过 output_key 更新 last_weather_report。
  2. BLOCK Request: The callback runs again for weather_agent_v5_model_guardrail, inspects the message, finds "BLOCK", prints "Blocking LLM call!", sets the state flag, and returns the predefined LlmResponse. The agent's underlying LLM is never called for this turn. The user sees the callback's blocking message.BLOCK 请求:回调再次针对 weather_agent_v5_model_guardrail 运行,检查消息,发现“BLOCK”,打印“Blocking LLM call!”,设置状态标志,并返回预定义的 LlmResponse。代理的底层 LLM 在此轮对话中从未被调用。用户将看到回调的拦截消息。
  3. Hello Again: The callback runs for weather_agent_v5_model_guardrail, allows the request. The root agent then delegates to greeting_agent. Note: The before_model_callback defined on the root agent does NOT automatically apply to sub-agents. The greeting_agent proceeds normally, calls its say_hello tool, and returns the greeting.再次问候:回调针对 weather_agent_v5_model_guardrail 运行,允许该请求。根代理随后委托给 greeting_agent。注意:在根代理上定义的 before_model_callback 不会自动应用于子代理。greeting_agent 正常进行,调用其 say_hello 工具,并返回问候语。

You have successfully implemented an input safety layer! The before_model_callback provides a powerful mechanism to enforce rules and control agent behavior before expensive or potentially risky LLM calls are made. Next, we'll apply a similar concept to add guardrails around tool usage itself.您已成功实现了一个输入安全层!before_model_callback 提供了一种强大的机制,可以在进行昂贵或潜在风险的 LLM 调用之前执行规则并控制代理行为。接下来,我们将应用类似的概念,围绕工具使用本身添加防护栏。

Step 6: Adding Safety - Tool Argument Guardrail (before_tool_callback)第 6 步:增加安全性 - 工具参数防护栏 (before_tool_callback)

In Step 5, we added a guardrail to inspect and potentially block user input before it reached the LLM. Now, we'll add another layer of control after the LLM has decided to use a tool but before that tool actually executes. This is useful for validating the arguments the LLM wants to pass to the tool.在第 5 步中,我们添加了一个防护栏,用于在用户输入到达 LLM 之前对其进行检查并可能拦截。现在,我们将添加另一层控制,在 LLM 决定使用工具之后、工具实际执行之前进行拦截。这对于验证 LLM 想要传递给工具的参数非常有用。

ADK provides the before_tool_callback for this precise purpose.ADK 正是为此提供了 before_tool_callback。

What is before_tool_callback?什么是 before_tool_callback?

  • It's a Python function executed just before a specific tool function runs, after the LLM has requested its use and decided on the arguments.这是一个在特定工具函数运行之前执行的 Python 函数,发生在 LLM 请求使用该工具并确定参数之后。
  • Purpose: Validate tool arguments, prevent tool execution based on specific inputs, modify arguments dynamically, or enforce resource usage policies.目的:验证工具参数、根据特定输入防止工具执行、动态修改参数或强制执行资源使用策略。

Common Use Cases:

  • Argument Validation: Check if arguments provided by the LLM are valid, within allowed ranges, or conform to expected formats.参数验证:检查 LLM 提供的参数是否有效、在允许范围内或符合预期格式。
  • Resource Protection: Prevent tools from being called with inputs that might be costly, access restricted data, or cause unwanted side effects (e.g., blocking API calls for certain parameters).资源保护:防止工具被可能代价高昂、访问受限数据或导致意外副作用的输入调用(例如,阻止针对某些参数的 API 调用)。
  • Dynamic Argument Modification: Adjust arguments based on session state or other contextual information before the tool runs.动态参数修改:在工具运行前,根据会话状态或其他上下文信息调整参数。

How it Works:

  1. Define a function accepting tool: BaseTool, args: Dict[str, Any], and tool_context: ToolContext.定义一个接受 tool: BaseTool, args: Dict[str, Any] 和 tool_context: ToolContext 的函数。

    • tool: The tool object about to be called (inspect tool.name).tool:即将被调用的工具对象(检查 tool.name)。
    • args: The dictionary of arguments the LLM generated for the tool.args:LLM 为工具生成的参数字典。
    • tool_context: Provides access to session state (tool_context.state), agent info, etc.tool_context:提供对会话状态 (tool_context.state)、代理信息等的访问权限。
  2. Inside the function:

    • Inspect: Examine the tool.name and the args dictionary.检查:检查 tool.name 和 args 字典。
    • Modify: Change values within the args dictionary directly. If you return None, the tool runs with these modified args.修改:直接更改 args 字典中的值。如果返回 None,工具将使用这些修改后的参数运行。
    • Block/Override (Guardrail): Return a dictionary. ADK treats this dictionary as the result of the tool call, completely skipping the execution of the original tool function. The dictionary should ideally match the expected return format of the tool it's blocking.拦截/覆盖(防护栏):返回一个字典。ADK 将此字典视为工具调用的结果,完全跳过原始工具函数的执行。该字典理想情况下应匹配其所拦截工具的预期返回格式。
    • Allow: Return None. ADK proceeds to execute the actual tool function with the (potentially modified) arguments.允许:返回 None。ADK 继续使用(可能已修改的)参数执行实际的工具函数。

In this step, we will:

  1. Define a before_tool_callback function (block_paris_tool_guardrail) that specifically checks if the get_weather_stateful tool is called with the city "Paris".定义一个 before_tool_callback 函数 (block_paris_tool_guardrail),专门检查 get_weather_stateful 工具是否以城市“Paris”被调用。
  2. If "Paris" is detected, the callback will block the tool and return a custom error dictionary.如果检测到“Paris”,回调将拦截工具并返回一个自定义错误字典。
  3. Update our root agent (weather_agent_v6_tool_guardrail) to include both the before_model_callback and this new before_tool_callback.更新我们的根代理 (weather_agent_v6_tool_guardrail) 以同时包含 before_model_callback 和此新的 before_tool_callback。
  4. Create a new runner for this agent, using the same stateful session service.为此代理创建一个新的运行器,使用相同的有状态会话服务。
  5. Test the flow by requesting weather for allowed cities and the blocked city ("Paris").通过请求允许城市的城市天气和被拦截的城市(“Paris”)来测试流程。

1. Define the Tool Guardrail Callback Function1. 定义工具防护栏回调函数

This function targets the get_weather_stateful tool. It checks the city argument. If it's "Paris", it returns an error dictionary that looks like the tool's own error response. Otherwise, it allows the tool to run by returning None.此函数针对 get_weather_stateful 工具。它检查 city 参数。如果是“Paris”,它将返回一个看起来像工具自身错误响应的错误字典。否则,它通过返回 None 允许工具运行。

# @title 1. Define the before_tool_callback Guardrail

# Ensure necessary imports are available
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.tool_context import ToolContext
from typing import Optional, Dict, Any # For type hints

def block_paris_tool_guardrail(
    tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict]:
    """
    Checks if 'get_weather_stateful' is called for 'Paris'.
    If so, blocks the tool execution and returns a specific error dictionary.
    Otherwise, allows the tool call to proceed by returning None.
    """
    tool_name = tool.name
    agent_name = tool_context.agent_name # Agent attempting the tool call
    print(f"--- Callback: block_paris_tool_guardrail running for tool '{tool_name}' in agent '{agent_name}' ---")
    print(f"--- Callback: Inspecting args: {args} ---")

    # --- Guardrail Logic ---
    target_tool_name = "get_weather_stateful" # Match the function name used by FunctionTool
    blocked_city = "paris"

    # Check if it's the correct tool and the city argument matches the blocked city
    if tool_name == target_tool_name:
        city_argument = args.get("city", "") # Safely get the 'city' argument
        if city_argument and city_argument.lower() == blocked_city:
            print(f"--- Callback: Detected blocked city '{city_argument}'. Blocking tool execution! ---")
            # Optionally update state
            tool_context.state["guardrail_tool_block_triggered"] = True
            print(f"--- Callback: Set state 'guardrail_tool_block_triggered': True ---")

            # Return a dictionary matching the tool's expected output format for errors
            # This dictionary becomes the tool's result, skipping the actual tool run.
            return {
                "status": "error",
                "error_message": f"Policy restriction: Weather checks for '{city_argument.capitalize()}' are currently disabled by a tool guardrail."
            }
        else:
             print(f"--- Callback: City '{city_argument}' is allowed for tool '{tool_name}'. ---")
    else:
        print(f"--- Callback: Tool '{tool_name}' is not the target tool. Allowing. ---")


    # If the checks above didn't return a dictionary, allow the tool to execute
    print(f"--- Callback: Allowing tool '{tool_name}' to proceed. ---")
    return None # Returning None allows the actual tool function to run

print("✅ block_paris_tool_guardrail function defined.")

2. Update Root Agent to Use Both Callbacks2. 更新根代理以同时使用两个回调

We redefine the root agent again (weather_agent_v6_tool_guardrail), this time adding the before_tool_callback parameter alongside the before_model_callback from Step 5.我们再次重新定义根代理 (weather_agent_v6_tool_guardrail),这次在第 5 步的 before_model_callback 基础上添加了 before_tool_callback 参数。

Self-Contained Execution Note: Similar to Step 5, ensure all prerequisites (sub-agents, tools, before_model_callback) are defined or available in the execution context before defining this agent.自包含执行说明:与第 5 步类似,在定义此代理之前,请确保所有先决条件(子代理、工具、before_model_callback)都已定义或在执行上下文中可用。

# @title 2. Update Root Agent with BOTH Callbacks (Self-Contained)

# --- Ensure Prerequisites are Defined ---
# (Include or ensure execution of definitions for: Agent, LiteLlm, Runner, ToolContext,
#  MODEL constants, say_hello, say_goodbye, greeting_agent, farewell_agent,
#  get_weather_stateful, block_keyword_guardrail, block_paris_tool_guardrail)

# --- Redefine Sub-Agents (Ensures they exist in this context) ---
greeting_agent = None
try:
    # Use a defined model constant
    greeting_agent = Agent(
        model=MODEL_GEMINI_FLASH,
        name="greeting_agent", # Keep original name for consistency
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting using the 'say_hello' tool. Do nothing else.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.",
        tools=[say_hello],
    )
    print(f"✅ Sub-Agent '{greeting_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Greeting agent. Check Model/API Key ({greeting_agent.model}). Error: {e}")

farewell_agent = None
try:
    # Use a defined model constant
    farewell_agent = Agent(
        model=MODEL_GEMINI_FLASH,
        name="farewell_agent", # Keep original name
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message using the 'say_goodbye' tool. Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.",
        tools=[say_goodbye],
    )
    print(f"✅ Sub-Agent '{farewell_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Farewell agent. Check Model/API Key ({farewell_agent.model}). Error: {e}")

# --- Define the Root Agent with Both Callbacks ---
root_agent_tool_guardrail = None
runner_root_tool_guardrail = None

if ('greeting_agent' in globals() and greeting_agent and
    'farewell_agent' in globals() and farewell_agent and
    'get_weather_stateful' in globals() and
    'block_keyword_guardrail' in globals() and
    'block_paris_tool_guardrail' in globals()):

    root_agent_model = MODEL_GEMINI_FLASH

    root_agent_tool_guardrail = Agent(
        name="weather_agent_v6_tool_guardrail", # New version name
        model=root_agent_model,
        description="Main agent: Handles weather, delegates, includes input AND tool guardrails.",
        instruction="You are the main Weather Agent. Provide weather using 'get_weather_stateful'. "
                    "Delegate greetings to 'greeting_agent' and farewells to 'farewell_agent'. "
                    "Handle only weather, greetings, and farewells.",
        tools=[get_weather_stateful],
        sub_agents=[greeting_agent, farewell_agent],
        output_key="last_weather_report",
        before_model_callback=block_keyword_guardrail, # Keep model guardrail
        before_tool_callback=block_paris_tool_guardrail # <<< Add tool guardrail
    )
    print(f"✅ Root Agent '{root_agent_tool_guardrail.name}' created with BOTH callbacks.")

    # --- Create Runner, Using SAME Stateful Session Service ---
    if 'session_service_stateful' in globals():
        runner_root_tool_guardrail = Runner(
            agent=root_agent_tool_guardrail,
            app_name=APP_NAME,
            session_service=session_service_stateful # <<< Use the service from Step 4/5
        )
        print(f"✅ Runner created for tool guardrail agent '{runner_root_tool_guardrail.agent.name}', using stateful session service.")
    else:
        print("❌ Cannot create runner. 'session_service_stateful' from Step 4/5 is missing.")

else:
    print("❌ Cannot create root agent with tool guardrail. Prerequisites missing.")

3. Interact to Test the Tool Guardrail3. 交互以测试工具防护栏

Let's test the interaction flow, again using the same stateful session (SESSION_ID_STATEFUL) from the previous steps.让我们测试交互流程,再次使用之前步骤中相同的有状态会话 (SESSION_ID_STATEFUL)。

  1. Request weather for "New York": Passes both callbacks, tool executes (using Fahrenheit preference from state).请求“New York”的天气:通过两个回调,工具执行(使用状态中的 Fahrenheit 偏好)。
  2. Request weather for "Paris": Passes before_model_callback. LLM decides to call get_weather_stateful(city='Paris'). before_tool_callback intercepts, blocks the tool, and returns the error dictionary. Agent relays this error.请求“Paris”的天气:通过 before_model_callback。LLM 决定调用 get_weather_stateful(city='Paris')。before_tool_callback 拦截、阻止工具并返回错误字典。代理转发此错误。
  3. Request weather for "London": Passes both callbacks, tool executes normally.请求“London”的天气:通过两个回调,工具正常执行。
# @title 3. Interact to Test the Tool Argument Guardrail
import asyncio # Ensure asyncio is imported

# Ensure the runner for the tool guardrail agent is available
if 'runner_root_tool_guardrail' in globals() and runner_root_tool_guardrail:
    # Define the main async function for the tool guardrail test conversation.
    # The 'await' keywords INSIDE this function are necessary for async operations.
    async def run_tool_guardrail_test():
        print("\n--- Testing Tool Argument Guardrail ('Paris' blocked) ---")

        # Use the runner for the agent with both callbacks and the existing stateful session
        # Define a helper lambda for cleaner interaction calls
        interaction_func = lambda query: call_agent_async(query,
                                                         runner_root_tool_guardrail,
                                                         USER_ID_STATEFUL, # Use existing user ID
                                                         SESSION_ID_STATEFUL # Use existing session ID
                                                        )
        # 1. Allowed city (Should pass both callbacks, use Fahrenheit state)
        print("--- Turn 1: Requesting weather in New York (expect allowed) ---")
        await interaction_func("What's the weather in New York?")

        # 2. Blocked city (Should pass model callback, but be blocked by tool callback)
        print("\n--- Turn 2: Requesting weather in Paris (expect blocked by tool guardrail) ---")
        await interaction_func("How about Paris?") # Tool callback should intercept this

        # 3. Another allowed city (Should work normally again)
        print("\n--- Turn 3: Requesting weather in London (expect allowed) ---")
        await interaction_func("Tell me the weather in London.")

    # --- Execute the `run_tool_guardrail_test` async function ---
    # Choose ONE of the methods below based on your environment.

    # METHOD 1: Direct await (Default for Notebooks/Async REPLs)
    # If your environment supports top-level await (like Colab/Jupyter notebooks),
    # it means an event loop is already running, so you can directly await the function.
    print("Attempting execution using 'await' (default for notebooks)...")
    await run_tool_guardrail_test()

    # METHOD 2: asyncio.run (For Standard Python Scripts [.py])
    # If running this code as a standard Python script from your terminal,
    # the script context is synchronous. `asyncio.run()` is needed to
    # create and manage an event loop to execute your async function.
    # To use this method:
    # 1. Comment out the `await run_tool_guardrail_test()` line above.
    # 2. Uncomment the following block:
    """
    import asyncio
    if __name__ == "__main__": # Ensures this runs only when script is executed directly
        print("Executing using 'asyncio.run()' (for standard Python scripts)...")
        try:
            # This creates an event loop, runs your async function, and closes the loop.
            asyncio.run(run_tool_guardrail_test())
        except Exception as e:
            print(f"An error occurred: {e}")
    """

    # --- Inspect final session state after the conversation ---
    # This block runs after either execution method completes.
    # Optional: Check state for the tool block trigger flag
    print("\n--- Inspecting Final Session State (After Tool Guardrail Test) ---")
    # Use the session service instance associated with this stateful session
    final_session = await session_service_stateful.get_session(app_name=APP_NAME,
                                                         user_id=USER_ID_STATEFUL,
                                                         session_id= SESSION_ID_STATEFUL)
    if final_session:
        # Use .get() for safer access
        print(f"Tool Guardrail Triggered Flag: {final_session.state.get('guardrail_tool_block_triggered', 'Not Set (or False)')}")
        print(f"Last Weather Report: {final_session.state.get('last_weather_report', 'Not Set')}") # Should be London weather if successful
        print(f"Temperature Unit: {final_session.state.get('user_preference_temperature_unit', 'Not Set')}") # Should be Fahrenheit
        # print(f"Full State Dict: {final_session.state}") # For detailed view
    else:
        print("\n❌ Error: Could not retrieve final session state.")

else:
    print("\n⚠️ Skipping tool guardrail test. Runner ('runner_root_tool_guardrail') is not available.")

Analyze the output:分析输出:

  1. New York: The before_model_callback allows the request. The LLM requests get_weather_stateful. The before_tool_callback runs, inspects the args ({'city': 'New York'}), sees it's not "Paris", prints "Allowing tool..." and returns None. The actual get_weather_stateful function executes, reads "Fahrenheit" from state, and returns the weather report. The agent relays this, and it gets saved via output_key.New York:before_model_callback 允许该请求。LLM 请求 get_weather_stateful。before_tool_callback 运行,检查参数 ({'city': 'New York'}),发现不是“Paris”,打印“Allowing tool...”并返回 None。实际的 get_weather_stateful 函数执行,从状态读取“Fahrenheit”,并返回天气报告。代理转发此信息,并通过 output_key 保存。
  2. Paris: The before_model_callback allows the request. The LLM requests get_weather_stateful(city='Paris'). The before_tool_callback runs, inspects the args, detects "Paris", prints "Blocking tool execution!", sets the state flag, and returns the error dictionary {'status': 'error', 'error_message': 'Policy restriction...'}. The actual get_weather_stateful function is never executed. The agent receives the error dictionary as if it were the tool's output and formulates a response based on that error message.Paris:before_model_callback 允许该请求。LLM 请求 get_weather_stateful(city='Paris')。before_tool_callback 运行,检查参数,检测到“Paris”,打印“Blocking tool execution!”,设置状态标志,并返回错误字典 {'status': 'error', 'error_message': 'Policy restriction...'}。实际的 get_weather_stateful 函数从未被执行。代理接收到该错误字典,就好像它是工具的输出一样,并基于该错误消息制定响应。
  3. London: Behaves like New York, passing both callbacks and executing the tool successfully. The new London weather report overwrites the last_weather_report in the state.London:表现与 New York 一样,通过两个回调并成功执行工具。新的伦敦天气报告覆盖了状态中的 last_weather_report。

You've now added a crucial safety layer controlling not just what reaches the LLM, but also how the agent's tools can be used based on the specific arguments generated by the LLM. Callbacks like before_model_callback and before_tool_callback are essential for building robust, safe, and policy-compliant agent applications.您现在添加了一个关键的安全层,不仅控制进入 LLM 的内容,还根据 LLM 生成的具体参数控制代理工具的使用方式。像 before_model_callback 和 before_tool_callback 这样的回调对于构建健壮、安全且符合政策的代理应用程序至关重要。


Conclusion: Your Agent Team is Ready!结论:您的代理团队准备就绪!

Congratulations! You've successfully journeyed from building a single, basic weather agent to constructing a sophisticated, multi-agent team using the Agent Development Kit (ADK).祝贺您!您已成功完成了从构建一个基础的天气代理,到使用代理开发套件 (ADK) 构建一个复杂的、多代理团队的旅程。

Let's recap what you've accomplished:让我们回顾一下您所取得的成就:

  • You started with a fundamental agent equipped with a single tool (get_weather).您从一个具备单一工具 (get_weather) 的基础代理开始。
  • You explored ADK's multi-model flexibility using LiteLLM, running the same core logic with different LLMs like Gemini, GPT-4o, and Claude.您探索了使用 LiteLLM 的多模型灵活性,通过 Gemini、GPT-4o 和 Claude 等不同 LLM 运行相同的核心逻辑。
  • You embraced modularity by creating specialized sub-agents (greeting_agent, farewell_agent) and enabling automatic delegation from a root agent.您通过创建专用子代理(greeting_agent, farewell_agent)并启用来自根代理的自动委托,拥抱了模块化。
  • You gave your agents memory using Session State, allowing them to remember user preferences (temperature_unit) and past interactions (output_key).您使用会话状态赋予了代理记忆,使它们能够记住用户偏好 (temperature_unit) 和过往交互 (output_key)。
  • You implemented crucial safety guardrails using both before_model_callback (blocking specific input keywords) and before_tool_callback (blocking tool execution based on arguments like the city "Paris").您使用 before_model_callback(拦截特定输入关键词)和 before_tool_callback(根据参数(如城市“Paris”)拦截工具执行)实现了关键的安全防护栏。

Through building this progressive Weather Bot team, you've gained hands-on experience with core ADK concepts essential for developing complex, intelligent applications.通过构建这个渐进式天气机器人团队,您获得了开发复杂、智能应用程序所必需的核心 ADK 概念的实践经验。

Key Takeaways:关键要点:

  • Agents & Tools: The fundamental building blocks for defining capabilities and reasoning. Clear instructions and docstrings are paramount.代理与工具:定义能力和推理的基本构建块。清晰的指令和文档字符串至关重要。
  • Runners & Session Services: The engine and memory management system that orchestrate agent execution and maintain conversational context.运行器与会话服务:编排代理执行并维护对话上下文的引擎和内存管理系统。
  • Delegation: Designing multi-agent teams allows for specialization, modularity, and better management of complex tasks. Agent description is key for auto-flow.委托:设计多代理团队允许专业化、模块化以及更好地管理复杂任务。代理描述对于自动流程至关重要。
  • Session State (ToolContext, output_key): Essential for creating context-aware, personalized, and multi-turn conversational agents.会话状态 (ToolContext, output_key):对于创建具有上下文感知、个性化和多轮对话的代理至关重要。
  • Callbacks (before_model, before_tool): Powerful hooks for implementing safety, validation, policy enforcement, and dynamic modifications before critical operations (LLM calls or tool execution).回调 (before_model, before_tool):在关键操作(LLM 调用或工具执行)之前实现安全性、验证、政策执行和动态修改的强大钩子。
  • Flexibility (LiteLlm): ADK empowers you to choose the best LLM for the job, balancing performance, cost, and features.灵活性 (LiteLlm):ADK 使您能够选择最适合工作的 LLM,平衡性能、成本和功能。

Where to Go Next?接下来做什么?

Your Weather Bot team is a great starting point. Here are some ideas to further explore ADK and enhance your application:您的天气机器人团队是一个很好的起点。以下是一些进一步探索 ADK 并增强您的应用程序的想法:

  1. Real Weather API: Replace the mock_weather_db in your get_weather tool with a call to a real weather API (like OpenWeatherMap, WeatherAPI).真实天气 API:将 get_weather 工具中的 mock_weather_db 替换为对真实天气 API(如 OpenWeatherMap, WeatherAPI)的调用。
  2. More Complex State: Store more user preferences (e.g., preferred location, notification settings) or conversation summaries in the session state.更复杂的状态:在会话状态中存储更多用户偏好(如首选位置、通知设置)或对话摘要。
  3. Refine Delegation: Experiment with different root agent instructions or sub-agent descriptions to fine-tune the delegation logic. Could you add a "forecast" agent?优化委托:尝试不同的根代理指令或子代理描述,以微调委托逻辑。您可以添加一个“预报”代理吗?
  4. Advanced Callbacks:
    • Use after_model_callback to potentially reformat or sanitize the LLM's response after it's generated.使用 after_model_callback 在 LLM 响应生成后对其进行潜在的重新格式化或清理。
    • Use after_tool_callback to process or log the results returned by a tool.使用 after_tool_callback 处理或记录工具返回的结果。
    • Implement before_agent_callback or after_agent_callback for agent-level entry/exit logic.实现 before_agent_callback 或 after_agent_callback 以进行代理级的进入/退出逻辑。
  5. Error Handling: Improve how the agent handles tool errors or unexpected API responses. Maybe add retry logic within a tool.错误处理:改进代理处理工具错误或意外 API 响应的方式。也许可以在工具内添加重试逻辑。
  6. Persistent Session Storage: Consider changing InMemorySessionService to one of ADK's persistent implementations, such as DatabaseSessionService which is SQLAlchemy-backed and installed with pip install google-adk[db], or VertexAiSessionService. For more information, see the Session page.持久会话存储:考虑将 InMemorySessionService 更改为 ADK 的持久实现之一,例如使用 SQLAlchemy 支持并安装了 pip install google-adk[db] 的 DatabaseSessionService,或者 VertexAiSessionService。有关详细信息,请参阅会话页面。
  7. Streaming UI: Integrate your agent team with a web framework (like FastAPI, as shown in the ADK Streaming Quickstart) to create a real-time chat interface.流式 UI:将您的代理团队与 Web 框架(如 ADK 流式快速入门中展示的 FastAPI)集成,以创建实时聊天界面。

The Agent Development Kit provides a robust foundation for building sophisticated LLM-powered applications. By mastering the concepts covered in this tutorial – tools, state, delegation, and callbacks – you are well-equipped to tackle increasingly complex agentic systems.代理开发套件为构建复杂的 LLM 驱动应用程序提供了坚实的基础。通过掌握本教程中涵盖的概念——工具、状态、委托和回调,您已具备应对日益复杂的代理系统的能力。

Happy building!祝您开发愉快!