コンテンツにスキップ

ツール

ツールとは?

ADKの文脈において、ツールとはAIエージェントに提供される特定の能力を表し、エージェントが中核となるテキスト生成や推論能力を超えて、アクションを実行したり世界と対話したりすることを可能にします。有能なエージェントを基本的な言語モデルと区別するのは、多くの場合、ツールの効果的な使用方法です。

技術的には、ツールは通常、モジュール化されたコードコンポーネントです。例えば、Python/Javaの関数、クラスメソッド、あるいは別の特化したエージェントなど、明確に定義されたタスクを実行するように設計されています。これらのタスクは、多くの場合、外部システムやデータとの対話を伴います。

エージェントによるツール呼び出し

主な特徴

アクション指向: ツールは、次のような特定のアクションを実行します。

  • データベースへのクエリ発行
  • APIリクエストの作成(例: 天気データの取得、予約システム)
  • ウェブ検索
  • コードスニペットの実行
  • ドキュメントからの情報取得(RAG)
  • 他のソフトウェアやサービスとの対話

エージェントの能力拡張: ツールは、エージェントがリアルタイム情報にアクセスし、外部システムに影響を与え、訓練データに固有の知識の限界を克服することを可能にします。

事前定義されたロジックの実行: 重要なのは、ツールは開発者が定義した特定のロジックを実行する点です。エージェントの中核となる大規模言語モデル(LLM)のように、独自の独立した推論能力は持っていません。LLMは、どのツールを、いつ、どのような入力で使うかを推論しますが、ツール自体はその指定された関数を実行するだけです。

エージェントによるツールの使用方法

エージェントは、多くの場合、関数呼び出しを含むメカニズムを通じて動的にツールを活用します。このプロセスは一般的に次のステップに従います。

  1. 推論: エージェントのLLMが、システム指示、会話履歴、およびユーザーのリクエストを分析します。
  2. 選択: 分析に基づき、LLMはエージェントが利用可能なツールと各ツールを説明するdocstringを基に、実行するツールを(もしあれば)決定します。
  3. 呼び出し: LLMは、選択されたツールに必要な引数(入力)を生成し、その実行をトリガーします。
  4. 観察: エージェントは、ツールから返された出力(結果)を受け取ります。
  5. 最終化: エージェントは、ツールの出力を進行中の推論プロセスに組み込み、次の応答を形成したり、後続のステップを決定したり、目標が達成されたかどうかを判断したりします。

ツールは、エージェントの知的な中核(LLM)が複雑なタスクを達成するために必要に応じてアクセスし利用できる、専門的なツールキットのようなものだと考えてください。

ADKのツールタイプ

ADKは、いくつかのタイプのツールをサポートすることで柔軟性を提供します。

  1. 関数ツール: アプリケーションの特定のニーズに合わせて、あなたが作成するツール。
  2. 組み込みツール: 一般的なタスクのためにフレームワークによって提供される、すぐに使えるツール。 例: Google検索、コード実行、Retrieval-Augmented Generation (RAG)。
  3. サードパーティ製ツール: 人気のある外部ライブラリからツールをシームレスに統合します。 例: LangChainツール、CrewAIツール。

各ツールタイプの詳細情報と例については、上記の各ドキュメントページへのリンクをたどってください。

エージェントの指示におけるツールの参照

エージェントの指示の中で、ツールの関数名を使って直接参照することができます。ツールの関数名docstringが十分に説明的であれば、あなたの指示は主に大規模言語モデル(LLM)がいつツールを利用すべきかに焦点を当てることができます。これにより、明確さが促進され、モデルが各ツールの意図された使用方法を理解するのに役立ちます。

ツールが生成する可能性のある異なる戻り値をエージェントがどのように処理するかを明確に指示することが非常に重要です。例えば、ツールがエラーメッセージを返す場合、エージェントが操作を再試行すべきか、タスクをあきらめるべきか、あるいはユーザーに追加情報を要求すべきかを指示で指定する必要があります。

さらに、ADKはツールの連続使用をサポートしており、あるツールの出力が別のツールの入力として機能することができます。このようなワークフローを実装する場合、モデルが必要なステップを導くために、エージェントの指示内で意図されたツールの使用順序を記述することが重要です。

次の例は、エージェントが指示の中で関数名を参照することでツールを使用する方法を示しています。また、成功メッセージやエラーメッセージなど、ツールからの異なる戻り値を処理するようにエージェントを誘導する方法や、タスクを達成するために複数のツールを連続して使用する方法を編成する方法も示しています。

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

APP_NAME="weather_sentiment_agent"
USER_ID="user1234"
SESSION_ID="1234"
MODEL_ID="gemini-2.0-flash"

# Tool 1
def get_weather_report(city: str) -> dict:
    """Retrieves the current weather report for a specified city.

    Returns:
        dict: A dictionary containing the weather information with a 'status' key ('success' or 'error') and a 'report' key with the weather details if successful, or an 'error_message' if an error occurred.
    """
    if city.lower() == "london":
        return {"status": "success", "report": "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a chance of rain."}
    elif city.lower() == "paris":
        return {"status": "success", "report": "The weather in Paris is sunny with a temperature of 25 degrees Celsius."}
    else:
        return {"status": "error", "error_message": f"Weather information for '{city}' is not available."}

weather_tool = FunctionTool(func=get_weather_report)


# Tool 2
def analyze_sentiment(text: str) -> dict:
    """Analyzes the sentiment of the given text.

    Returns:
        dict: A dictionary with 'sentiment' ('positive', 'negative', or 'neutral') and a 'confidence' score.
    """
    if "good" in text.lower() or "sunny" in text.lower():
        return {"sentiment": "positive", "confidence": 0.8}
    elif "rain" in text.lower() or "bad" in text.lower():
        return {"sentiment": "negative", "confidence": 0.7}
    else:
        return {"sentiment": "neutral", "confidence": 0.6}

sentiment_tool = FunctionTool(func=analyze_sentiment)


# Agent
weather_sentiment_agent = Agent(
    model=MODEL_ID,
    name='weather_sentiment_agent',
    instruction="""You are a helpful assistant that provides weather information and analyzes the sentiment of user feedback.
**If the user asks about the weather in a specific city, use the 'get_weather_report' tool to retrieve the weather details.**
**If the 'get_weather_report' tool returns a 'success' status, provide the weather report to the user.**
**If the 'get_weather_report' tool returns an 'error' status, inform the user that the weather information for the specified city is not available and ask if they have another city in mind.**
**After providing a weather report, if the user gives feedback on the weather (e.g., 'That's good' or 'I don't like rain'), use the 'analyze_sentiment' tool to understand their sentiment.** Then, briefly acknowledge their sentiment.
You can handle these tasks sequentially if needed.""",
    tools=[weather_tool, sentiment_tool]
)

# Session and Runner
session_service = InMemorySessionService()
session = session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
runner = Runner(agent=weather_sentiment_agent, app_name=APP_NAME, session_service=session_service)


# Agent Interaction
def call_agent(query):
    content = types.Content(role='user', parts=[types.Part(text=query)])
    events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)

    for event in events:
        if event.is_final_response():
            final_response = event.content.parts[0].text
            print("Agent Response: ", final_response)

call_agent("weather in london?")
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import com.google.adk.tools.Annotations.Schema;
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext; // Ensure this import is correct
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class WeatherSentimentAgentApp {

  private static final String APP_NAME = "weather_sentiment_agent";
  private static final String USER_ID = "user1234";
  private static final String SESSION_ID = "1234";
  private static final String MODEL_ID = "gemini-2.0-flash";

  /**
   * Retrieves the current weather report for a specified city.
   *
   * @param city The city for which to retrieve the weather report.
   * @param toolContext The context for the tool.
   * @return A dictionary containing the weather information.
   */
  public static Map<String, Object> getWeatherReport(
      @Schema(name = "city")
      String city,
      @Schema(name = "toolContext")
      ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();

    if (city.toLowerCase(Locale.ROOT).equals("london")) {
      response.put("status", "success");
      response.put(
          "report",
          "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a"
              + " chance of rain.");
    } else if (city.toLowerCase(Locale.ROOT).equals("paris")) {
      response.put("status", "success");
      response.put(
          "report", "The weather in Paris is sunny with a temperature of 25 degrees Celsius.");
    } else {
      response.put("status", "error");
      response.put(
          "error_message", String.format("Weather information for '%s' is not available.", city));
    }
    return response;
  }

  /**
   * Analyzes the sentiment of the given text.
   *
   * @param text The text to analyze.
   * @param toolContext The context for the tool.
   * @return A dictionary with sentiment and confidence score.
   */
  public static Map<String, Object> analyzeSentiment(
      @Schema(name = "text")
      String text,
      @Schema(name = "toolContext")
      ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();
    String lowerText = text.toLowerCase(Locale.ROOT);
    if (lowerText.contains("good") || lowerText.contains("sunny")) {
      response.put("sentiment", "positive");
      response.put("confidence", 0.8);
    } else if (lowerText.contains("rain") || lowerText.contains("bad")) {
      response.put("sentiment", "negative");
      response.put("confidence", 0.7);
    } else {
      response.put("sentiment", "neutral");
      response.put("confidence", 0.6);
    }
    return response;
  }

  /**
   * Calls the agent with the given query and prints the final response.
   *
   * @param runner The runner to use.
   * @param query The query to send to the agent.
   */
  public static void callAgent(Runner runner, String query) {
    Content content = Content.fromParts(Part.fromText(query));

    InMemorySessionService sessionService = (InMemorySessionService) runner.sessionService();
    Session session =
        sessionService
            .createSession(APP_NAME, USER_ID, /* state= */ null, SESSION_ID)
            .blockingGet();

    runner
        .runAsync(session.userId(), session.id(), content)
        .forEach(
            event -> {
              if (event.finalResponse()
                  && event.content().isPresent()
                  && event.content().get().parts().isPresent()
                  && !event.content().get().parts().get().isEmpty()
                  && event.content().get().parts().get().get(0).text().isPresent()) {
                String finalResponse = event.content().get().parts().get().get(0).text().get();
                System.out.println("Agent Response: " + finalResponse);
              }
            });
  }

  public static void main(String[] args) throws NoSuchMethodException {
    FunctionTool weatherTool =
        FunctionTool.create(
            WeatherSentimentAgentApp.class.getMethod(
                "getWeatherReport", String.class, ToolContext.class));
    FunctionTool sentimentTool =
        FunctionTool.create(
            WeatherSentimentAgentApp.class.getMethod(
                "analyzeSentiment", String.class, ToolContext.class));

    BaseAgent weatherSentimentAgent =
        LlmAgent.builder()
            .model(MODEL_ID)
            .name("weather_sentiment_agent")
            .description("Weather Sentiment Agent")
            .instruction("""
                    You are a helpful assistant that provides weather information and analyzes the
                    sentiment of user feedback
                    **If the user asks about the weather in a specific city, use the
                    'get_weather_report' tool to retrieve the weather details.**
                    **If the 'get_weather_report' tool returns a 'success' status, provide the
                    weather report to the user.**
                    **If the 'get_weather_report' tool returns an 'error' status, inform the
                    user that the weather information for the specified city is not available
                    and ask if they have another city in mind.**
                    **After providing a weather report, if the user gives feedback on the
                    weather (e.g., 'That's good' or 'I don't like rain'), use the
                    'analyze_sentiment' tool to understand their sentiment.** Then, briefly
                    acknowledge their sentiment.
                    You can handle these tasks sequentially if needed.
                    """)
            .tools(ImmutableList.of(weatherTool, sentimentTool))
            .build();

    InMemorySessionService sessionService = new InMemorySessionService();
    Runner runner = new Runner(weatherSentimentAgent, APP_NAME, null, sessionService);

    // Change the query to ensure the tool is called with a valid city that triggers a "success"
    // response from the tool, like "london" (without the question mark).
    callAgent(runner, "weather in paris");
  }
}

ツールコンテキスト

より高度なシナリオのために、ADKでは特別なパラメータtool_context: ToolContextを含めることで、ツール関数内から追加のコンテキスト情報にアクセスできます。これを関数シグネチャに含めることで、エージェントの実行中にツールが呼び出されると、ADKは自動的にToolContextクラスのインスタンスを提供します。

ToolContextは、いくつかの重要な情報と制御レバーへのアクセスを提供します。

  • state: State: 現在のセッションの状態を読み書きします。ここで行われた変更は追跡され、永続化されます。

  • actions: EventActions: ツール実行後のエージェントの後続アクションに影響を与えます(例: 要約のスキップ、別のエージェントへの移譲)。

  • function_call_id: str: この特定のツール呼び出しに対してフレームワークによって割り当てられた一意の識別子。認証応答との追跡や相関に役立ちます。これは、単一のモデル応答内で複数のツールが呼び出される場合にも役立ちます。

  • function_call_event_id: str: この属性は、現在のツール呼び出しをトリガーしたイベントの一意の識別子を提供します。これは、追跡やロギングの目的で役立ちます。

  • auth_response: Any: このツール呼び出しの前に認証フローが完了した場合の認証応答/資格情報を含みます。

  • サービスへのアクセス: ArtifactsやMemoryなどの設定済みサービスと対話するためのメソッド。

ツール関数のdocstringにtool_contextパラメータを含めるべきではないことに注意してください。ToolContextは、LLMがツール関数を呼び出すことを決定したにADKフレームワークによって自動的に注入されるため、LLMの意思決定には関係なく、含めるとLLMを混乱させる可能性があります。

状態管理

tool_context.state属性は、現在のセッションに関連付けられた状態への直接的な読み書きアクセスを提供します。これは辞書のように動作しますが、すべての変更が差分として追跡され、セッションサービスによって永続化されることを保証します。これにより、ツールは異なる対話やエージェントのステップ間で情報を維持および共有できます。

  • 状態の読み取り: 標準的な辞書アクセス (tool_context.state['my_key']) または .get() メソッド (tool_context.state.get('my_key', default_value)) を使用します。

  • 状態への書き込み: 値を直接割り当てます (tool_context.state['new_key'] = 'new_value')。これらの変更は、結果として得られるイベントのstate_deltaに記録されます。

  • 状態プレフィックス: 標準の状態プレフィックスを覚えておいてください。

    • app:*: アプリケーションのすべてのユーザーで共有されます。

    • user:*: 現在のユーザーに固有で、すべてのセッションで共有されます。

    • (プレフィックスなし): 現在のセッションに固有です。

    • temp:*: 一時的で、呼び出し間で永続化されません(単一のrun呼び出し内でデータを渡すのに役立ちますが、LLM呼び出し間で動作するツールコンテキスト内では一般的にあまり有用ではありません)。

from google.adk.tools import ToolContext, FunctionTool

def update_user_preference(preference: str, value: str, tool_context: ToolContext):
    """Updates a user-specific preference."""
    user_prefs_key = "user:preferences"
    # Get current preferences or initialize if none exist
    preferences = tool_context.state.get(user_prefs_key, {})
    preferences[preference] = value
    # Write the updated dictionary back to the state
    tool_context.state[user_prefs_key] = preferences
    print(f"Tool: Updated user preference '{preference}' to '{value}'")
    return {"status": "success", "updated_preference": preference}

pref_tool = FunctionTool(func=update_user_preference)

# In an Agent:
# my_agent = Agent(..., tools=[pref_tool])

# When the LLM calls update_user_preference(preference='theme', value='dark', ...):
# The tool_context.state will be updated, and the change will be part of the
# resulting tool response event's actions.state_delta.
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext;

// ユーザー固有のテーマ設定を更新します。
public Map<String, String> updateUserThemePreference(String value, ToolContext toolContext) {
  String userPrefsKey = "user:preferences:theme";

  // 現在の設定を取得、なければ初期化
  String preference = toolContext.state().getOrDefault(userPrefsKey, "").toString();
  if (preference.isEmpty()) {
    preference = value;
  }

  // 更新された辞書を状態に書き戻す
  toolContext.state().put("user:preferences", preference);
  System.out.printf("ツール: ユーザー設定 %s を %s に更新しました", userPrefsKey, preference);

  return Map.of("status", "success", "updated_preference", toolContext.state().get(userPrefsKey).toString());
  // LLMがupdateUserThemePreference("dark")を呼び出すと:
  // toolContext.stateが更新され、その変更は結果として得られる
  // ツール応答イベントのactions.stateDeltaの一部となります。
}

エージェントフローの制御

tool_context.actions属性(JavaではToolContext.actions())は、EventActionsオブジェクトを保持します。このオブジェクトの属性を変更することで、ツールは実行完了後のエージェントやフレームワークの動作に影響を与えることができます。

  • skip_summarization: bool: (デフォルト: False) Trueに設定すると、通常ツールの出力を要約するLLM呼び出しをバイパスするようにADKに指示します。これは、ツールの戻り値がすでにユーザー向けのメッセージである場合に便利です。

  • transfer_to_agent: str: これを別のエージェントの名前に設定します。フレームワークは現在のエージェントの実行を停止し、会話の制御を指定されたエージェントに移譲します。これにより、ツールは動的により専門的なエージェントにタスクを引き渡すことができます。

  • escalate: bool: (デフォルト: False) これをTrueに設定すると、現在のエージェントがリクエストを処理できず、制御を親エージェントに(階層内にある場合)渡すべきであることを示します。LoopAgentでは、サブエージェントのツールでescalate=Trueを設定するとループが終了します。

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools import ToolContext
from google.genai import types

APP_NAME="customer_support_agent"
USER_ID="user1234"
SESSION_ID="1234"


def check_and_transfer(query: str, tool_context: ToolContext) -> str:
    """Checks if the query requires escalation and transfers to another agent if needed."""
    if "urgent" in query.lower():
        print("Tool: Detected urgency, transferring to the support agent.")
        tool_context.actions.transfer_to_agent = "support_agent"
        return "Transferring to the support agent..."
    else:
        return f"Processed query: '{query}'. No further action needed."

escalation_tool = FunctionTool(func=check_and_transfer)

main_agent = Agent(
    model='gemini-2.0-flash',
    name='main_agent',
    instruction="""You are the first point of contact for customer support of an analytics tool. Answer general queries. If the user indicates urgency, use the 'check_and_transfer' tool.""",
    tools=[check_and_transfer]
)

support_agent = Agent(
    model='gemini-2.0-flash',
    name='support_agent',
    instruction="""You are the dedicated support agent. Mentioned you are a support handler and please help the user with their urgent issue."""
)

main_agent.sub_agents = [support_agent]

# Session and Runner
session_service = InMemorySessionService()
session = session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
runner = Runner(agent=main_agent, app_name=APP_NAME, session_service=session_service)


# Agent Interaction
def call_agent(query):
    content = types.Content(role='user', parts=[types.Part(text=query)])
    events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)

    for event in events:
        if event.is_final_response():
            final_response = event.content.parts[0].text
            print("Agent Response: ", final_response)

call_agent("this is urgent, i cant login")
import com.google.adk.agents.LlmAgent;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import com.google.adk.tools.Annotations.Schema;
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class CustomerSupportAgentApp {

  private static final String APP_NAME = "customer_support_agent";
  private static final String USER_ID = "user1234";
  private static final String SESSION_ID = "1234";
  private static final String MODEL_ID = "gemini-2.0-flash";

  /**
   * Checks if the query requires escalation and transfers to another agent if needed.
   *
   * @param query The user's query.
   * @param toolContext The context for the tool.
   * @return A map indicating the result of the check and transfer.
   */
  public static Map<String, Object> checkAndTransfer(
      @Schema(name = "query", description = "the user query")
      String query,
      @Schema(name = "toolContext", description = "the tool context")
      ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();
    if (query.toLowerCase(Locale.ROOT).contains("urgent")) {
      System.out.println("Tool: Detected urgency, transferring to the support agent.");
      toolContext.actions().setTransferToAgent("support_agent");
      response.put("status", "transferring");
      response.put("message", "Transferring to the support agent...");
    } else {
      response.put("status", "processed");
      response.put(
          "message", String.format("Processed query: '%s'. No further action needed.", query));
    }
    return response;
  }

  /**
   * Calls the agent with the given query and prints the final response.
   *
   * @param runner The runner to use.
   * @param query The query to send to the agent.
   */
  public static void callAgent(Runner runner, String query) {
    Content content =
        Content.fromParts(Part.fromText(query));

    InMemorySessionService sessionService = (InMemorySessionService) runner.sessionService();
    // Fixed: session ID does not need to be an optional.
    Session session =
        sessionService
            .createSession(APP_NAME, USER_ID, /* state= */ null, SESSION_ID)
            .blockingGet();

    runner
        .runAsync(session.userId(), session.id(), content)
        .forEach(
            event -> {
              if (event.finalResponse()
                  && event.content().isPresent()
                  && event.content().get().parts().isPresent()
                  && !event.content().get().parts().get().isEmpty()
                  && event.content().get().parts().get().get(0).text().isPresent()) {
                String finalResponse = event.content().get().parts().get().get(0).text().get();
                System.out.println("Agent Response: " + finalResponse);
              }
            });
  }

  public static void main(String[] args) throws NoSuchMethodException {
    FunctionTool escalationTool =
        FunctionTool.create(
            CustomerSupportAgentApp.class.getMethod(
                "checkAndTransfer", String.class, ToolContext.class));

    LlmAgent supportAgent =
        LlmAgent.builder()
            .model(MODEL_ID)
            .name("support_agent")
            .description("""
                The dedicated support agent.
                Mentions it is a support handler and helps the user with their urgent issue.
            """)
            .instruction("""
                You are the dedicated support agent.
                Mentioned you are a support handler and please help the user with their urgent issue.
            """)
            .build();

    LlmAgent mainAgent =
        LlmAgent.builder()
            .model(MODEL_ID)
            .name("main_agent")
            .description("""
                The first point of contact for customer support of an analytics tool.
                Answers general queries.
                If the user indicates urgency, uses the 'check_and_transfer' tool.
                """)
            .instruction("""
                You are the first point of contact for customer support of an analytics tool.
                Answer general queries.
                If the user indicates urgency, use the 'check_and_transfer' tool.
                """)
            .tools(ImmutableList.of(escalationTool))
            .subAgents(supportAgent)
            .build();
    // Fixed: LlmAgent.subAgents() expects 0 arguments.
    // Sub-agents are now added to the main agent via its builder,
    // as `subAgents` is a property that should be set during agent construction
    // if it's not dynamically managed.

    InMemorySessionService sessionService = new InMemorySessionService();
    Runner runner = new Runner(mainAgent, APP_NAME, null, sessionService);

    // Agent Interaction
    callAgent(runner, "this is urgent, i cant login");
  }
}
解説
  • main_agentsupport_agentの2つのエージェントを定義します。main_agentは最初の接点となるように設計されています。
  • check_and_transferツールは、main_agentによって呼び出されると、ユーザーのクエリを調べます。
  • クエリに "urgent" という単語が含まれている場合、ツールはtool_context、具体的にはtool_context.actionsにアクセスし、transfer_to_agent属性をsupport_agentに設定します。
  • このアクションは、フレームワークに対して会話の制御をsupport_agentという名前のエージェントに移譲するよう指示します。
  • main_agentが緊急のクエリを処理すると、check_and_transferツールが移譲をトリガーします。その後の応答は、理想的にはsupport_agentから来ることになります。
  • 緊急性のない通常のクエリの場合、ツールは移譲をトリガーせずに単に処理します。

この例は、ツールがToolContext内のEventActionsを通じて、別の専門エージェントに制御を移譲することによって、会話の流れに動的に影響を与える方法を示しています。

認証

python_only

ToolContextは、認証が必要なAPIと対話するツールにメカニズムを提供します。ツールが認証を処理する必要がある場合、以下を使用することがあります。

  • auth_response: ツールが呼び出される前にフレームワークによって認証がすでに処理されていた場合(RestApiToolやOpenAPIのセキュリティスキームで一般的)の資格情報(例: トークン)を含みます。

  • request_credential(auth_config: dict): ツールが認証が必要であると判断したが、資格情報が利用できない場合にこのメソッドを呼び出します。これは、提供されたauth_configに基づいて認証フローを開始するようフレームワークに指示します。

  • get_auth_response(): 後続の呼び出しで(request_credentialが正常に処理された後)、ユーザーが提供した資格情報を取得するためにこのメソッドを呼び出します。

認証フロー、設定、および例の詳細な説明については、専用のツール認証ドキュメントページを参照してください。

コンテキストを意識したデータアクセス方法

これらのメソッドは、ツールが設定済みのサービスによって管理される、セッションやユーザーに関連付けられた永続的なデータと対話するための便利な方法を提供します。

  • list_artifacts() (またはJavaでは listArtifacts()): artifact_serviceを介して現在セッションに保存されているすべてのアーティファクトのファイル名(またはキー)のリストを返します。アーティファクトは通常、ユーザーによってアップロードされたり、ツール/エージェントによって生成されたりするファイル(画像、ドキュメントなど)です。

  • load_artifact(filename: str): artifact_serviceからファイル名で特定のアーティファクトを取得します。オプションでバージョンを指定できます。省略した場合は最新バージョンが返されます。アーティファクトデータとMIMEタイプを含むgoogle.genai.types.Partオブジェクトを返します。見つからない場合はNoneを返します。

  • save_artifact(filename: str, artifact: types.Part): アーティファクトの新しいバージョンをartifact_serviceに保存します。新しいバージョン番号(0から始まる)を返します。

  • search_memory(query: str) python_only

    設定されたmemory_serviceを使用してユーザーの長期記憶をクエリします。これは、過去の対話や保存された知識から関連情報を取得するのに役立ちます。SearchMemoryResponseの構造は特定のメモリサービスの実装に依存しますが、通常は関連するテキストのスニペットや会話の抜粋を含みます。

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.adk.tools import ToolContext, FunctionTool
from google.genai import types


def process_document(
    document_name: str, analysis_query: str, tool_context: ToolContext
) -> dict:
    """Analyzes a document using context from memory."""

    # 1. Load the artifact
    print(f"Tool: Attempting to load artifact: {document_name}")
    document_part = tool_context.load_artifact(document_name)

    if not document_part:
        return {"status": "error", "message": f"Document '{document_name}' not found."}

    document_text = document_part.text  # Assuming it's text for simplicity
    print(f"Tool: Loaded document '{document_name}' ({len(document_text)} chars).")

    # 2. Search memory for related context
    print(f"Tool: Searching memory for context related to: '{analysis_query}'")
    memory_response = tool_context.search_memory(
        f"Context for analyzing document about {analysis_query}"
    )
    memory_context = "\n".join(
        [
            m.events[0].content.parts[0].text
            for m in memory_response.memories
            if m.events and m.events[0].content
        ]
    )  # Simplified extraction
    print(f"Tool: Found memory context: {memory_context[:100]}...")

    # 3. Perform analysis (placeholder)
    analysis_result = f"Analysis of '{document_name}' regarding '{analysis_query}' using memory context: [Placeholder Analysis Result]"
    print("Tool: Performed analysis.")

    # 4. Save the analysis result as a new artifact
    analysis_part = types.Part.from_text(text=analysis_result)
    new_artifact_name = f"analysis_{document_name}"
    version = await tool_context.save_artifact(new_artifact_name, analysis_part)
    print(f"Tool: Saved analysis result as '{new_artifact_name}' version {version}.")

    return {
        "status": "success",
        "analysis_artifact": new_artifact_name,
        "version": version,
    }


doc_analysis_tool = FunctionTool(func=process_document)

# In an Agent:
# Assume artifact 'report.txt' was previously saved.
# Assume memory service is configured and has relevant past data.
# my_agent = Agent(..., tools=[doc_analysis_tool], artifact_service=..., memory_service=...)
// メモリからのコンテキストを使用してドキュメントを分析します。
// Callback ContextやLoadArtifactsツールを使用して、アーティファクトを一覧表示、読み込み、保存することもできます。
public static @NonNull Maybe<ImmutableMap<String, Object>> processDocument(
    @Annotations.Schema(description = "分析するドキュメントの名前。") String documentName,
    @Annotations.Schema(description = "分析のためのクエリ。") String analysisQuery,
    ToolContext toolContext) {

  // 1. 利用可能なすべてのアーティファクトを一覧表示
  System.out.printf(
      "利用可能なすべてのアーティファクトを一覧表示: %s:", toolContext.listArtifacts().blockingGet());

  // 2. アーティファクトをメモリに読み込む
  System.out.println("ツール: アーティファクトの読み込み試行: " + documentName);
  Part documentPart = toolContext.loadArtifact(documentName, Optional.empty()).blockingGet();
  if (documentPart == null) {
    System.out.println("ツール: ドキュメント '" + documentName + "' が見つかりません。");
    return Maybe.just(
        ImmutableMap.<String, Object>of(
            "status", "error", "message", "ドキュメント '" + documentName + "' が見つかりません。"));
  }
  String documentText = documentPart.text().orElse("");
  System.out.println(
      "ツール: ドキュメント '" + documentName + "' (" + documentText.length() + " 文字) を読み込みました。");

  // 3. 分析の実行(プレースホルダー)
  String analysisResult =
      "'"
          + documentName
          + "' の '"
          + analysisQuery
          + "' に関する分析 [プレースホルダー分析結果]";
  System.out.println("ツール: 分析を実行しました。");

  // 4. 分析結果を新しいアーティファクトとして保存
  Part analysisPart = Part.fromText(analysisResult);
  String newArtifactName = "analysis_" + documentName;

  toolContext.saveArtifact(newArtifactName, analysisPart);

  return Maybe.just(
      ImmutableMap.<String, Object>builder()
          .put("status", "success")
          .put("analysis_artifact", newArtifactName)
          .build());
}
// FunctionTool processDocumentTool =
//      FunctionTool.create(ToolContextArtifactExample.class, "processDocument");
// エージェントに、この関数ツールを含めます。
// LlmAgent agent = LlmAgent().builder().tools(processDocumentTool).build();

ToolContextを活用することで、開発者はADKのアーキテクチャとシームレスに統合し、エージェント全体の能力を向上させる、より洗練されたコンテキスト対応のカスタムツールを作成できます。

効果的なツール関数の定義

メソッドや関数をADKツールとして使用する場合、その定義方法がエージェントの正しい使用能力に大きく影響します。エージェントの大規模言語モデル(LLM)は、関数の名前パラメータ(引数)型ヒント、そしてdocstring / ソースコードコメントに大きく依存して、その目的を理解し、正しい呼び出しを生成します。

効果的なツール関数を定義するための主要なガイドラインは次のとおりです。

  • 関数名:

    • アクションを明確に示す、動詞-名詞ベースの記述的な名前を使用します(例: get_weathersearchDocumentsschedule_meeting)。
    • runprocesshandle_dataのような一般的な名前や、doStuffのような過度に曖昧な名前は避けてください。良い説明があっても、do_stuffのような名前は、例えばcancelFlightといつ使い分けるべきかモデルを混乱させる可能性があります。
    • LLMは、ツール選択時に主要な識別子として関数名を使用します。
  • パラメータ(引数):

    • 関数は任意の数のパラメータを持つことができます。
    • 明確で記述的な名前を使用します(例: cではなくcityqではなくsearch_query)。
    • Pythonではすべてのパラメータに型ヒントを提供します(例: city: struser_id: intitems: list[str])。これは、ADKがLLMのために正しいスキーマを生成するために不可欠です。
    • すべてのパラメータ型がJSONシリアライズ可能であることを確認してください。strintfloatboollistdictのような標準的なPython型とその組み合わせは一般的に安全です。複雑なカスタムクラスインスタンスは、明確なJSON表現がない限り、直接のパラメータとして避けてください。
    • パラメータにデフォルト値を設定しないでください。例: def my_func(param1: str = "default")。デフォルト値は、関数呼び出し生成時に基盤となるモデルによって確実にサポートまたは使用されるわけではありません。すべての必要な情報は、LLMがコンテキストから導き出すか、不足している場合は明示的に要求する必要があります。
    • self / clsは自動的に処理されます: self(インスタンスメソッド用)やcls(クラスメソッド用)のような暗黙のパラメータは、ADKによって自動的に処理され、LLMに示されるスキーマから除外されます。ツールがLLMに提供を要求する論理的なパラメータに対してのみ、型ヒントと説明を定義する必要があります。
  • 戻り値の型:

    • 関数の戻り値は、Pythonでは辞書(dict、JavaではMapなければなりません
    • 関数が辞書以外の型(例: 文字列、数値、リスト)を返す場合、ADKフレームワークは結果をモデルに返す前に、自動的に{'result': your_original_return_value}のような辞書/Mapにラップします。
    • 辞書/Mapのキーと値を、LLMが簡単に理解できるように記述的に設計してください。モデルが次のステップを決定するためにこの出力を読むことを忘れないでください。
    • 意味のあるキーを含めてください。例えば、500のようなエラーコードだけを返すのではなく、{'status': 'error', 'error_message': 'Database connection failed'}のように返します。
    • モデルに対してツール実行の結果を明確に示すために、statusキー(例: 'success''error''pending''ambiguous')を含めることは強く推奨される実践です。
  • Docstring / ソースコードコメント:

    • これは非常に重要です。 docstringは、LLMにとっての主要な説明情報源です。
    • ツールが何をするかを明確に記述してください。 その目的と制限について具体的に説明してください。
    • ツールをいつ使用すべきかを説明してください。 LLMの意思決定を導くためのコンテキストや使用例を提供してください。
    • 各パラメータを明確に説明してください。 LLMがその引数にどのような情報を提供する必要があるかを説明してください。
    • 期待されるdictの戻り値の構造と意味、特に異なるstatusの値と関連するデータキーについて説明してください。
    • 注入されるToolContextパラメータは記述しないでください。オプションのtool_context: ToolContextパラメータは、LLMが知る必要のあるパラメータではないため、docstringの説明内では言及を避けてください。ToolContextは、LLMがそれを呼び出すことを決定したにADKによって注入されます。

    良い定義の例:

def lookup_order_status(order_id: str) -> dict:
  """IDを使用して顧客の注文の現在のステータスを取得します。

  ユーザーが特定の注文のステータスを明示的に尋ね、注文IDを
  提供した場合にのみ、このツールを使用してください。一般的な問い合わせには
  使用しないでください。

  Args:
      order_id: 検索する注文の一意の識別子。

  Returns:
      注文ステータスを含む辞書。
      考えられるステータス: 'shipped', 'processing', 'pending', 'error'。
      成功例: {'status': 'shipped', 'tracking_number': '1Z9...'}
      エラー例: {'status': 'error', 'error_message': '注文IDが見つかりません。'}
  """
  # ... ステータスを取得する関数の実装 ...
  if status := fetch_status_from_backend(order_id):
       return {"status": status.state, "tracking_number": status.tracking} # 構造の例
  else:
       return {"status": "error", "error_message": f"注文ID {order_id} が見つかりません。"}
/**
 * 指定された都市の現在の天気予報を取得します。
 *
 * @param city 天気予報を取得する都市。
 * @param toolContext ツールのコンテキスト。
 * @return 天気情報を含む辞書。
 */
public static Map<String, Object> getWeatherReport(String city, ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();
    if (city.toLowerCase(Locale.ROOT).equals("london")) {
        response.put("status", "success");
        response.put(
                "report",
                "ロンドンの現在の天気は曇りで、気温は18度、雨の可能性があります。");
    } else if (city.toLowerCase(Locale.ROOT).equals("paris")) {
        response.put("status", "success");
        response.put("report", "パリの天気は晴れで、気温は25度です。");
    } else {
        response.put("status", "error");
        response.put("error_message", String.format("'%s'の天気情報は利用できません。", city));
    }
    return response;
}
  • 単純さと焦点:
    • ツールは焦点を絞る: 各ツールは、理想的には1つの明確に定義されたタスクを実行すべきです。
    • パラメータは少ない方が良い: モデルは一般的に、多くのオプションや複雑なパラメータを持つツールよりも、少数の明確に定義されたパラメータを持つツールをより確実に扱います。
    • 単純なデータ型を使用する: 可能な限り、パラメータとして複雑なカスタムクラスや深くネストされた構造よりも、基本的な型(Pythonではstr, int, bool, float, List[str]、Javaではint, byte, short, long, float, double, boolean, char)を優先してください。
    • 複雑なタスクを分解する: 複数の異なる論理ステップを実行する関数を、より小さく、より焦点の合ったツールに分割します。例えば、単一のupdate_user_profile(profile: ProfileObject)ツールの代わりに、update_user_name(name: str)update_user_address(address: str)update_user_preferences(preferences: list[str])などの個別のツールを検討してください。これにより、LLMが正しい能力を選択して使用することが容易になります。

これらのガイドラインに従うことで、LLMがカスタム関数ツールを効果的に活用するために必要な明確さと構造を提供し、より有能で信頼性の高いエージェントの振る舞いにつながります。

ツールセット: ツールのグループ化と動的な提供 python_only

個々のツールを超えて、ADKはBaseToolsetインターフェース(google.adk.tools.base_toolsetで定義)を介してツールセットの概念を導入します。ツールセットを使用すると、BaseToolインスタンスのコレクションを、多くの場合動的に、エージェントに管理および提供できます。

このアプローチは、次のような場合に有益です。

  • 関連ツールの整理: 共通の目的を果たすツールをグループ化します(例: 数学演算のためのすべてのツール、または特定のAPIと対話するすべてのツール)。
  • 動的なツールの可用性: エージェントが現在のコンテキスト(例: ユーザーの権限、セッションの状態、またはその他の実行時条件)に基づいて異なるツールを利用できるようにします。ツールセットのget_toolsメソッドは、どのツールを公開するかを決定できます。
  • 外部ツールプロバイダーの統合: ツールセットは、OpenAPI仕様やMCPサーバーのような外部システムからのツールのアダプターとして機能し、それらをADK互換のBaseToolオブジェクトに変換できます。

BaseToolsetインターフェース

ADKでツールセットとして機能するクラスは、BaseToolset抽象基底クラスを実装する必要があります。このインターフェースは主に2つのメソッドを定義します。

  • async def get_tools(...) -> list[BaseTool]: これはツールセットのコアメソッドです。ADKエージェントが利用可能なツールを知る必要がある場合、そのtoolsリストで提供される各BaseToolsetインスタンスに対してget_tools()を呼び出します。

    • オプションのreadonly_contextReadonlyContextのインスタンス)を受け取ります。このコンテキストは、現在のセッション状態(readonly_context.state)、エージェント名、呼び出しIDなどの情報への読み取り専用アクセスを提供します。ツールセットはこのコンテキストを使用して、どのツールを返すかを動的に決定できます。
    • BaseToolインスタンスのlist返さなければなりません(例: FunctionToolRestApiTool)。
  • async def close(self) -> None: この非同期メソッドは、ツールセットが不要になったとき、例えばエージェントサーバーがシャットダウンするときやRunnerが閉じられるときに、ADKフレームワークによって呼び出されます。ネットワーク接続のクローズ、ファイルハンドルの解放、またはツールセットによって管理される他のリソースのクリーンアップなど、必要なクリーンアップ処理を実行するためにこのメソッドを実装します。

エージェントでのツールセットの使用

LlmAgenttoolsリストに、個々のBaseToolインスタンスと並べて、BaseToolset実装のインスタンスを直接含めることができます。

エージェントが初期化されるとき、または利用可能な能力を決定する必要があるとき、ADKフレームワークはtoolsリストを反復処理します。

  • アイテムがBaseToolインスタンスの場合、直接使用されます。
  • アイテムがBaseToolsetインスタンスの場合、そのget_tools()メソッドが(現在のReadonlyContextで)呼び出され、返されたBaseToolのリストがエージェントの利用可能なツールに追加されます。

例: 簡単な数学ツールセット

簡単な算術演算を提供するツールセットの基本的な例を作成しましょう。

# 1. Define the individual tool functions
def add_numbers(a: int, b: int, tool_context: ToolContext) -> Dict[str, Any]:
    """Adds two integer numbers.
    Args:
        a: The first number.
        b: The second number.
    Returns:
        A dictionary with the sum, e.g., {'status': 'success', 'result': 5}
    """
    print(f"Tool: add_numbers called with a={a}, b={b}")
    result = a + b
    # Example: Storing something in tool_context state
    tool_context.state["last_math_operation"] = "addition"
    return {"status": "success", "result": result}


def subtract_numbers(a: int, b: int) -> Dict[str, Any]:
    """Subtracts the second number from the first.
    Args:
        a: The first number.
        b: The second number.
    Returns:
        A dictionary with the difference, e.g., {'status': 'success', 'result': 1}
    """
    print(f"Tool: subtract_numbers called with a={a}, b={b}")
    return {"status": "success", "result": a - b}


# 2. Create the Toolset by implementing BaseToolset
class SimpleMathToolset(BaseToolset):
    def __init__(self, prefix: str = "math_"):
        self.prefix = prefix
        # Create FunctionTool instances once
        self._add_tool = FunctionTool(
            func=add_numbers,
            name=f"{self.prefix}add_numbers",  # Toolset can customize names
        )
        self._subtract_tool = FunctionTool(
            func=subtract_numbers, name=f"{self.prefix}subtract_numbers"
        )
        print(f"SimpleMathToolset initialized with prefix '{self.prefix}'")

    async def get_tools(
        self, readonly_context: Optional[ReadonlyContext] = None
    ) -> List[BaseTool]:
        print(f"SimpleMathToolset.get_tools() called.")
        # Example of dynamic behavior:
        # Could use readonly_context.state to decide which tools to return
        # For instance, if readonly_context.state.get("enable_advanced_math"):
        #    return [self._add_tool, self._subtract_tool, self._multiply_tool]

        # For this simple example, always return both tools
        tools_to_return = [self._add_tool, self._subtract_tool]
        print(f"SimpleMathToolset providing tools: {[t.name for t in tools_to_return]}")
        return tools_to_return

    async def close(self) -> None:
        # No resources to clean up in this simple example
        print(f"SimpleMathToolset.close() called for prefix '{self.prefix}'.")
        await asyncio.sleep(0)  # Placeholder for async cleanup if needed


# 3. Define an individual tool (not part of the toolset)
def greet_user(name: str = "User") -> Dict[str, str]:
    """Greets the user."""
    print(f"Tool: greet_user called with name={name}")
    return {"greeting": f"Hello, {name}!"}


greet_tool = FunctionTool(func=greet_user)

# 4. Instantiate the toolset
math_toolset_instance = SimpleMathToolset(prefix="calculator_")

# 5. Define an agent that uses both the individual tool and the toolset
calculator_agent = LlmAgent(
    name="CalculatorAgent",
    model="gemini-2.0-flash",  # Replace with your desired model
    instruction="You are a helpful calculator and greeter. "
    "Use 'greet_user' for greetings. "
    "Use 'calculator_add_numbers' to add and 'calculator_subtract_numbers' to subtract. "
    "Announce the state of 'last_math_operation' if it's set.",
    tools=[greet_tool, math_toolset_instance],  # Individual tool  # Toolset instance
)

この例では:

  • SimpleMathToolsetBaseToolsetを実装し、そのget_tools()メソッドはadd_numberssubtract_numbersのためのFunctionToolインスタンスを返します。また、プレフィックスを使用してそれらの名前をカスタマイズします。
  • calculator_agentは、個々のgreet_toolSimpleMathToolsetのインスタンスの両方で構成されています。
  • calculator_agentが実行されると、ADKはmath_toolset_instance.get_tools()を呼び出します。エージェントのLLMは、ユーザーのリクエストを処理するためにgreet_usercalculator_add_numbers、およびcalculator_subtract_numbersにアクセスできるようになります。
  • add_numbersツールはtool_context.stateへの書き込みを示し、エージェントの指示ではこの状態の読み取りについて言及しています。
  • close()メソッドが呼び出され、ツールセットが保持するリソースが確実に解放されます。

ツールセットは、ADKエージェントにツールのコレクションを整理、管理、動的に提供する強力な方法を提供し、よりモジュール化され、保守可能で、適応性のあるエージェントアプリケーションにつながります。