Open21

アツアツA2AツなGoogle ADKさわっていく🪭

noznoz

せっかくなのでGoogle ADKでエージェントを実装していく

Agno AIパッケージで実装してたらタイミング悪くでGoogle Agent Development Kitが公開された。
ドキュメントとこの辺を参考にする。

https://google.github.io/adk-docs/get-started/quickstart/

https://zenn.dev/google_cloud_jp/articles/c5fa102f468cdf#エージェント・アズ・ア・ツールの動作例

https://zenn.dev/hakoten/articles/06557100337fb3#task(タスク)

noznoz

はじめかた

各エージェントのコードの配置

.
├── agents                  # Contains individual agent samples 
│   ├── agent1              # Specific agent directory
│   │   └── README.md       # Agent-specific instructions    
│   ├── agent2
│   │   └── README.md
│   ├── ...   
│   └── README.md           # Overview and categorization of agents
└── README.md               # This file (Repository overview)

https://github.com/google/adk-samples?tab=readme-ov-file#-repository-structure

__init__.pyagent.py.envを各エージェントのフォルダに作成する

agents/
    agent/
        __init__.py
        agent.py
        .env
        README.md

https://google.github.io/adk-docs/get-started/quickstart/#project-structure

noznoz

チュートリアル見てみる

Colab向け
https://google.github.io/adk-docs/tutorials/agent-team/

CLI向け
https://github.com/google/adk-docs/tree/main/examples/python/tutorial/agent_team/adk-tutorial

ステップ1:最初のエージェント

エージェントの定義はよくある形。

エージェントの定義の部分
# Use one of the model constants defined earlier
AGENT_MODEL = MODEL_GEMINI_2_0_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}'.")

セッションSessionServiceと実行Runnerの管理設定がある。下記の実装ではセッションの会話履歴を全てメモリに埋め込むシンプルな実装。詳しくはステップ4。CLI向けにこの記述はない。

セッション管理の部分
# --- 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 = 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}'")

# --- 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}'.")

非同期処理したいためasyncを使って改めて関数を定義する。(あとでawaitつけて関数を使って非同期処理に。詳しくは公式doc

asyncで関数を定義
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}")

実行方法は以下。awaitをつけて関数を使うとrun_conversation内である関数の待機中に実行できる他の関数から進められていく。

会話を実行
# @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}")
noznoz

ステップ2:LiteLLMでマルチモデル化する[オプション]

LiteLLMパッケージを噛ませて、モデル名を指定するだけでLLMプロバイダー横断でモデルを切り替えることができる。MODEL_GPT_4Oにあらかじめモデル名を定義している前提。

from google.adk.models.lite_llm import LiteLlm
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
    )
noznoz

ステップ3:エージェントチームの構築 - 挨拶と送別会の委任

引数sub_agentsでタスクを割り振るエージェントを指定することができる。

Agentを宣言している部分
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]
    )
noznoz

ステップ4: セッション状態によるメモリとパーソナライゼーションの追加

状態管理:エージェントによる判断・操作の結果でタスクがどういう状態か

まず、session_service_stateful = InMemorySessionService()のようにセッションサービスのインスタンスを定義する。Agentインスタンスの宣言時に引数output_keyを指定しておき、Runnerインスタンスの引数session_serviceにセッションサービスを渡しておけば、エージェントの出力が決まったセッションのキーに収められていく。

新規のセッション・状態を初期化
# 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 = 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 = 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.")

状態を取得・更新するツール関数を作っておくといいらしい。例えば、「ユーザーが望む温度の単位」がすでに状態として入っており、それを読み取ることでLLMは出力するレポートにその単位を反映させることができる。また、「最後に調べた都市」を更新することで後の処理に流用でき、LLMの出力に依存しない統一的な表現で後続のプロンプトを生成することができる。ToolContextはツール関数の最後のパラメータとして定義されている場合、ADK によってToolContextの状態へ自動的に挿入されるとのこと。

状態を確認するツールの実装例
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.")

チーム型のエージェントを定義する。状態管理を利用することで出力の揺れを抑えることができそう。root_agent_statefulを見るとAgent関数でoutput_key="last_weather_report"を指定することで、最終出力をセッションに収めることができる。runner_root_statefulで、さっき初期化しておいたセッションのインスタンスを指定して実行開始。

サブエージェントを含めたルートエージェントを状態を更新する形で実装
# 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_2_0_FLASH etc. are defined

# --- Redefine Greeting Agent (from Step 3) ---
greeting_agent = None
try:
    greeting_agent = Agent(
        model=MODEL_GEMINI_2_0_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_2_0_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_2_0_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.")
noznoz

ステップ5: 安全性の追加 - ガードレールの入力before_model_callback

エージェントの動作を制限するため、プロンプトに特定のキーワードがある場合に止めることができるコールバック機能があるとのこと。下記が使用例。

使用例 説明
入力バリデーション/フィルタリング ユーザー入力が基準を満たしているか、または許可されないコンテンツ(個人情報やキーワードなど)が含まれているかをチェックします。
ガードレール 有害なリクエスト、オフトピックなリクエスト、ポリシー違反のリクエストがLLMによって処理されないようにします。
動的プロンプト修正 送信直前にLLMリクエストコンテキストにタイムリーな情報(セッションの状態など)を追加します。

まずはBLOCKという単語があれば実行させない関数を定義

`before_model_callback`ガードレールを定義
# 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.")

ルートエージェントの定義するとき、before_model_callback=block_keyword_guardrailとして引数に含める。このケースだとユーザーがBLOCKという単語を含めると実行が終了することになる。

`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_2_0_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_2_0_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_2_0_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],
        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")
noznoz

ステップ6: 安全性の追加 - ツール引数ガードレール(before_tool_callback)

あぶないプロンプトを弾くほかに、エージェントがツールに入力する引数も制限することができるらしい。

機能 説明
引数の検証 LLM によって提供された引数が有効であるか、許容範囲内であるか、または予期される形式に準拠しているかどうかを確認します。
リソース保護 コストがかかったり、制限されたデータにアクセスしたり、望ましくない副作用 (特定のパラメータに対する API 呼び出しをブロックするなど) を引き起こす可能性のある入力でツールが呼び出されないようにします。
動的な引数の変更 ツールを実行する前に、セッション状態またはその他のコンテキスト情報に基づいて引数を調整します。

まずはコールバック関数定義する。

`before_tool_callback`ガードレールを定義する
# 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.")

ルートエージェントに引数before_tool_callbackでコールバック関数を渡せばツール関数の引数に対するガードレールを実装できる。

ルートエージェントを定義しなおす
# --- 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_2_0_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_2_0_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_2_0_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.")
noznoz

同じようなエージェントのクラスがある?

引数おなじなのに名前が違う以下の2つ。

from google.adk.agents import LlmAgent
from google.adk.agents import Agent

どっちでも呼べる、、一緒??開発時にブレてたんかな。

The LlmAgent (often aliased simply as Agent) is a core component in ADK,
llm-agents.md

その通りで、パッケージの実装みにいくとLlmAgentのエイリアスとしてAgentがあるだけだった

475 Agent: TypeAlias = LlmAgent

google/adk/agents/llm_agent.py

noznoz

Cloud Run にデプロイしたい。

ファイル構造は?

FastAPIをフレームワークとしてCloud Runでデプロイできる。FastAPIとして動かすためにfrom google.adk.cli.fast_api import get_fast_api_appが用意されている。デプロイ時の引数にIAPを含めると組織内の人だけアクセスできる仕様に。

your-project-directory/
├── capital_agent/
│   ├── __init__.py
│   └── agent.py       # Your agent code (see "Agent sample" tab)
├── main.py            # FastAPI application entry point
├── requirements.txt   # Python dependencies
├── .env
└── Dockerfile         # Container build instructions

docs/deploy/cloud-run.md

こちらが参考になりました。

https://zenn.dev/chips0711/articles/7b84ad816dfe9c

デプロイ方法

iapでアクセス制限をできるだけ手軽につけたかったけど、ベータ版gcloud beta run deployにはある--iapコマンドが本番版のgcloud run deployになかた。ずっと使ってるGitHub Actionsのgoogle-github-actions/auth@v2が通らないのでこちら

gcloud beta run deploy adk-agent \
--region $GOOGLE_CLOUD_LOCATION \
--project $GOOGLE_CLOUD_PROJECT \
--image asia-northeast1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/samples/adk-python \
--iap \
--set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_VERTEXAI=$GOOGLE_GENAI_USE_VERTEXAI"

ADK + Cloud Run を動かす

こちらが参考になりました

https://zenn.dev/satohjohn/articles/b23bd65c289257

ちなみにiapは組織ポリシーは地域?かなんかで設定して、サービスのポリシーはデプロイ後に組織のドメインを指定しました。

noznoz

自作ツール関数のアノテーション

シンプルな関数にしてくれ的なエラー返ってくる。

  • def func(args) -> Noneはダメらしい。戻り値としてNone以外で何らかの型を返す必要がある。
  • datetimeは引数として使えない
ValueError: Failed to parse the parameter return_value: None of function {自作ツール関数名} for automatic function calling. Automatic function calling works best with simpler function signature schema, consider manually parse your function declaration for function {自作ツール関数名}.
noznoz

ガードレール関数

ガードレール関数は一つしか受けつてない?とりあえずリストで渡したらエラーに。

pydantic_core._pydantic_core.ValidationError: 1 validation error for LlmAgent
before_tool_callback
  Input should be callable [type=callable_type, input_value=[<bound method CloudRunGu...object at 0x14dca3980>>], input_type=list]
noznoz

LiteLLM

litellmでgeminiをラップしてツール関数に使おうとしたらvertex ai経由だとubuntu上で使えない模様?

https://github.com/BerriAI/litellm/issues/9863

Google AI Studioはモデル名の指定が他プロバイダと異なる、、結局動かんかったケド。

# Define model constants for cleaner code
MODEL_GEMINI_PRO = "gemini-1.5-pro"
MODEL_GPT_4O = "openai/gpt-4o"
MODEL_CLAUDE_SONNET = "anthropic/claude-3-sonnet-20240229"

LiteLlmのバージョン落としてもだめ。同じエラー。

ADKのAgentでOCR用のエージェント作ってagent_toolで使おうとしたけど、ファイル一個ずつ指定することになったり(不安定)、base64エンコード文字列をプロンプトで渡すことになったり(コンテキスト長すぎる)と意味ないことに。

結局geminiのsdk(from google import genai)で書き直した。

そもそもAgent関数でモデル指定する場合に、gemini系はLiteLlmでラッパーするとダメみたい。

noznoz

get_fast_api_app の使い方

Cloud Run のデプロイする場合に、main.pyで使うget_fast_api_app関数がわかりゃん。パッケージの実装コードを中心に調べてみた👀

参考

主要な機能

  1. セッション管理:↗︎ソース

    • session_service_uri パラメータを使用して、セッションの保存方法を設定できます
    • 空の文字列を指定すると InMemorySessionService が使用されます(一時的なセッション)
    • データベース URL を指定すると DatabaseSessionService が使用されます(永続的なセッション)
    • agentengine:// で始まる URL を指定すると VertexAiSessionService が使用されます
  2. 成果物ストレージ:↗︎ソース

    • artifact_service_uri パラメータで成果物の保存場所を指定できます
    • Google Cloud Storage の場合は gs:// で始まる URI を指定します
    • 指定しない場合は InMemoryArtifactService が使用されます
  3. ウェブインターフェース:[↗︎ソース]

    • web パラメータを True に設定すると、ウェブインターフェースが提供されます
    • これにより、ブラウザベースの対話型インターフェースが利用可能になります
  4. CORS 設定:↗︎ソース

    • allow_origins パラメータで Cross-Origin Resource Sharing を設定できます
    • API を異なるドメインから呼び出せるようにするために使用します
  5. トレーシング:↗︎ソース

    • trace_to_cloud パラメータを True に設定すると、Google Cloud へのトレースが有効になります
    • デバッグや監視に役立ちます

詳しい解説

`get_fast_api_app` 関数の詳細解説

google.adk.cli.fast_api モジュールの get_fast_api_app 関数は、Google ADK (Agent Development Kit) を使用してエージェントを FastAPI ウェブサーバーとして提供するための重要な関数です。以下にその詳細な使い方と機能を解説します。

基本的な機能

def get_fast_api_app(
    *,
    agents_dir: str,  # エージェントのディレクトリパス
    session_service_uri: str = "",  # セッションデータベースの URL
    artifact_service_uri: Optional[str] = None,  # 成果物ストレージの URI
    allow_origins: Optional[list[str]] = None,  # CORS 許可オリジン
    web: bool,  # ウェブインターフェースを提供するかどうか
    trace_to_cloud: bool = False,  # クラウドへのトレースを有効にするかどうか
    lifespan: Optional[Lifespan[FastAPI]] = None,  # FastAPI のライフスパン
) -> FastAPI:

パラメータの詳細

  1. agents_dir

    • エージェントのディレクトリパスを指定します
    • このディレクトリ内の各サブディレクトリがエージェントとして認識されます
  2. session_service_uri

    • セッション管理のためのデータベース URL を指定します
    • 空文字列の場合: InMemorySessionService が使用される(一時的なセッション)
    • データベース URL の場合: DatabaseSessionService が使用される(永続的なセッション)
    • agentengine:// で始まる URL の場合: VertexAiSessionService が使用される
    • 例: sqlite:///./sessions.db(SQLite を使用する場合)
  3. artifact_service_uri

    • エージェントが生成する成果物の保存場所を指定します
    • gs:// で始まる URI の場合: Google Cloud Storage が使用される
    • 指定しない場合: InMemoryArtifactService が使用される(一時的な保存)
  4. allow_origins

    • CORS (Cross-Origin Resource Sharing) の設定を行います
    • 異なるドメインからの API 呼び出しを許可するために使用します
    • 例: ["http://localhost", "http://localhost:8080", "*"]
  5. web

    • True の場合: ブラウザベースの対話型インターフェースが提供されます
    • False の場合: API エンドポイントのみが提供されます
    • 注意: web=True に設定すると、カスタムエンドポイントにアクセスできない問題が報告されています(Issue #51)
  6. trace_to_cloud

    • True の場合: Google Cloud へのトレースが有効になります
    • デバッグや監視に役立ちます
    • 環境変数 GOOGLE_CLOUD_PROJECT が設定されている必要があります
  7. lifespan

    • FastAPI アプリケーションのライフサイクル管理を行います
    • カスタムの起動・終了処理を設定できます

主要な機能

  1. セッション管理

    • ユーザーとエージェントの対話履歴を保存・管理します
    • データベースを使用した永続的なセッション管理が可能です
    • Cloud SQL や AlloyDB などの高度なデータベースサービスとの連携も可能です
  2. 成果物ストレージ

    • エージェントが生成するファイルや画像などの成果物を保存します
    • Google Cloud Storage との連携が組み込まれています
  3. API エンドポイント

    • エージェントとの対話のための RESTful API を自動的に設定します
    • セッション管理、エージェント実行、評価機能などの API が含まれます
  4. ウェブインターフェース

    • ブラウザベースの対話型インターフェースを提供します
    • エージェントのテストやデモに便利です
  5. トレーシング

    • エージェントの実行過程を追跡・記録します
    • デバッグや監視に役立ちます

発展的な使い方

  1. データベース設定の拡張

    • 将来的に session_db_kwargs パラメータが追加される予定です(Issue #1287)
    • 接続プールのサイズやタイムアウトなどの詳細な設定が可能になります
    • Cloud SQL や AlloyDB などの高度なデータベースサービスとの連携が強化されます
  2. カスタムエンドポイントの追加

    • 返される FastAPI アプリケーションに独自のエンドポイントを追加できます
    • 例:
      app = get_fast_api_app(...)
      
      @app.get("/custom")
      async def custom_endpoint():
          return {"message": "Custom endpoint"}
      
  3. 評価機能の活用

    • エージェントの評価セットを作成・管理するための機能が含まれています
    • エージェントのパフォーマンスをテストするのに役立ちます

実際の使用例

from google.adk.cli.fast_api import get_fast_api_app
import os
import uvicorn

# エージェントディレクトリの設定
AGENT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "agents")

# FastAPI アプリケーションの取得
app = get_fast_api_app(
    agents_dir=AGENT_DIR,
    session_db_url="sqlite:///./sessions.db",  # SQLite を使用する場合
    allow_origins=["http://localhost", "http://localhost:8080", "*"],  # CORS 設定
    web=True,  # ウェブインターフェースを有効化
)

# サーバーの起動
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

この関数は、Google ADK を使用してエージェントを API として提供する際の標準的な方法であり、特に Cloud Run などのサービスにデプロイする場合に便利です。

noznoz

get_fast_api_app関数でVertexAiSessionService使いたいゾ❗️

背景

AIエージェントをADKで作成・Cloud Runでデプロイする場合、会話歴(セッション)を簡単・標準・低費用で管理するにはAgent Engineが良さそう。正確にはAgent Engineインスタンスのセッション管理機能だけ使う。

セッションはInMemorySessionService、DatabaseSessionService、VertexAiSessionServiceから選ぶことができ、GCPでデプロイするならVertexAiSessionServiceを使うことで費用を抑えることができそうです。
Google Agent Engineにデプロイされ、計算時間に応じた従量課金となります。
Agent Development KitのセッションがDBに作成するスキーマを調べてみた

疑問

get_fast_api_app関数の引数session_service_uriでセッションの保存先を指定できる。agentengine://で始まるURLを渡せばいいっぽい。どうやってこのURLを取得できる?

調べてみた

わきゃらんかった、、下記によるとpythonのagent_enginesクラスからAgent Engineインスタンスの作成はできる。

import vertexai
from vertexai import agent_engines

# Create an agent engine instance
agent_engine = agent_engines.create()

ただ、APIのURLを見ると下記な感じ。agentengine://ではじまってない。

https://LOCATION-aiplatform.googleapis.com/v1beta1/projects/PROJECT_ID/locations/LOCATION/reasoningEngines/AGENT_ENGINE_ID/sessions
直接 API 呼び出しを使用してセッションを管理する

LOCATION, PROJECT_ID, AGENT_ENGINE_IDは環境依存。

agentengine://を検索しても何も出てこないので、Agent Engine側でも未実装?かもしれない。一旦、エージェント自体をデプロイするCloud Runのインスタンス上でSQLサーバーを起動させるといいかも?

noznoz

Cloud Runで複数のGCSバケットをマウントする際の落とし穴と解決策

Cloud Runでアプリケーションをデプロイする際、複数のGoogle Cloud Storage (GCS) バケットにアクセスしたいケースはよくありますよね。特に、アプリケーションのデータやログ、設定などを別々のバケットに保存したい場合に便利です。

gcloud run deploy コマンドを使ってバケットをマウントする際、少しハマりやすいポイントがあるので、その解決策を共有します。

はじめに: よくある間違い

複数のGCSバケットを一度にマウントしようとして、以下のようなコマンドを試す方がいらっしゃいます。これは一見正しそうに見えますが、残念ながら意図通りに動作しません

❌ 間違ったコマンド例 (旧)

gcloud beta run deploy my-service \
  --region us-central1 \
  --source . \
  --add-volume=name=bucket1-vol,type=cloud-storage,bucket=my-bucket-1,name=bucket2-vol,type=cloud-storage,bucket=my-bucket-2 \
  --add-volume-mount=volume=bucket1-vol,mount-path=/mnt/bucket1,volume=bucket2-vol,mount-path=/mnt/bucket2

このコマンドを実行すると、一部のバケットしかマウントされなかったり、マウントパスがおかしくなったりすることがあります。原因は、--add-volume--add-volume-mount パラメータが、1つの引数で複数の定義を受け付けないためです。カンマで区切って複数書くと、最初の定義以降が無視されたり、正しく解釈されなかったりします。

実際にこの方法を試した際、私もバケット名の間違い(例: test-takashou-ai-agent とすべきところを test-takashou-ai-agents と入力していたなど、スペルミスもよくあります!)と相まって、デプロイが失敗する経験をしました。

解決策: パラメータを個別に指定する

複数のバケットをマウントする場合、--add-volume--add-volume-mountそれぞれのバケットに対して個別に指定する必要があります。

✅ 正しいコマンド例 (新)

gcloud beta run deploy my-service \
  --region us-central1 \
  --source . \
  \
  --add-volume=name=bucket1-vol,type=cloud-storage,bucket=my-bucket-1 \
  --add-volume-mount=volume=bucket1-vol,mount-path=/mnt/bucket1 \
  \
  --add-volume=name=bucket2-vol,type=cloud-storage,bucket=my-bucket-2 \
  --add-volume-mount=volume=bucket2-vol,mount-path=/mnt/bucket2

この方法でコマンドを実行すれば、各バケットが意図した通りにマウントされ、アプリケーションからアクセスできるようになります。

ポイント:

  • 1ボリューム1パラメータ: name, type, bucket を含む --add-volume と、volume, mount-path を含む --add-volume-mount は、それぞれ1つのバケットにつき1組ずつ指定します。
  • バケット名の正確性: コマンドで指定するbucket名と、実際のGCSバケット名が完全に一致しているか、もう一度確認しましょう。(私の場合はsの付け忘れが原因でした!)
  • サービスアカウントの権限: Cloud Runサービスが使用するサービスアカウントに、マウントするGCSバケットへの適切なアクセス権限(例: Storage オブジェクト閲覧者)が付与されていることを確認してください。
noznoz

当たり前:Cloud Run とバケットのリージョン違いで激遅レスポンスに

背景

Cloud Runにマウントしたバケットにadkのセッション用のDBを配置したところ、web ui上のレスポンスが激遅に。

原因

Cloud Runのデプロイするリージョンとバケットのリージョンを揃えたところ激速に。よくあるリージョンを揃え忘れるやつが原因そう。

修正前

  • 起動:10秒
  • チャット:4秒

修正後

  • 起動:3秒
  • チャット:2秒
noznoz

artifactの読み込みはload_artifacts関数じゃないとだめ?

画像の受け渡しができなくて困っている

https://zenn.dev/uxoxu/books/adk-docs-japanese/viewer/artifacts-index

get_fast_api_app関数はartifactのuriを指定できる

https://github.com/google/adk-python/blob/4ae4c69c32d0cf5340362fb5c967d53979d36283/src/google/adk/cli/fast_api.py#L223

artifactへの保存の例

https://github.com/google/adk-samples/blob/5987a9e02d17e979b5ae9847ebf3c38a2126ad20/python/agents/image-scoring/image_scoring/sub_agents/image/tools/image_generation_tool.py#L44

artifactから取得の例

インストラクションから指定({artifact.<保存した名前>)とか試したけどなーんか動かない。同じようなIssueがこちらに。散々試した結果、下記のように読み込んだload_artifacts_toolAgent関数のtools関数に渡せばいいとのこと。(同志、、)

from google.adk.tools.load_artifacts_tool import load_artifacts_tool

https://github.com/google/adk-python/issues/660#issuecomment-2947856334