ADK用カスタムツール¶
ADKエージェントワークフローでは、ツールはADKエージェントがアクションを実行するために呼び出すことができる、構造化された入力と出力を持つプログラミング関数です。ADKツールは、Geminiやその他の生成AIモデルで関数呼び出しを使用する方法と同様に機能します。ADKツールを使用して、次のようなさまざまなアクションやプログラミング関数を実行できます。
- データベースのクエリ
- APIリクエストの作成:天気データの取得、予約システム
- ウェブ検索
- コードスニペットの実行
- ドキュメントからの情報取得(RAG)
- 他のソフトウェアやサービスとの対話
ツールとは?¶
ADKのコンテキストでは、ツールはAIエージェントに提供される特定の機能を表し、エージェントがコアのテキスト生成と推論能力を超えてアクションを実行し、世界と対話できるようにします。有能なエージェントを基本的な言語モデルと区別するのは、多くの場合、ツールの効果的な使用です。
技術的には、ツールは通常、Python/Java関数、クラスメソッド、さらには別の特殊なエージェントのようなモジュール式のコードコンポーネントであり、明確に定義されたタスクを実行するように設計されています。これらのタスクには、多くの場合、外部システムまたはデータとの対話が含まれます。

主な特徴¶
アクション指向: ツールは、情報検索、API呼び出し、計算実行など、エージェントの特定のアクションを実行します。
エージェント機能の拡張: エージェントがリアルタイム情報にアクセスし、外部システムに影響を与え、トレーニングデータに固有の知識の制限を克服できるようにします。
事前定義されたロジックの実行: 重要なことに、ツールは開発者が定義した特定のロジックを実行します。エージェントのコアである大規模言語モデル(LLM)のような独立した推論機能は備えていません。LLMは、どのツールをいつ、どの入力で使用するかを推論しますが、ツール自体は指定された関数を実行するだけです。
エージェントによるツールの使用方法¶
エージェントは、多くの場合、関数呼び出しを含むメカニズムを介して動的にツールを活用します。このプロセスは通常、次の手順に従います。
- 推論: エージェントのLLMは、システム命令、会話履歴、およびユーザーリクエストを分析します。
- 選択: 分析に基づいて、LLMは、エージェントで利用可能なツールと各ツールを説明するdocstringに基づいて、実行するツール(もしあれば)を決定します。
- 呼び出し: LLMは、選択したツールに必要な引数(入力)を生成し、その実行をトリガーします。
- 観察: エージェントは、ツールによって返された出力(結果)を受け取ります。
- 最終処理: エージェントは、ツールの出力を進行中の推論プロセスに組み込んで、次の応答を作成し、後続のステップを決定するか、目標が達成されたかどうかを判断します。
ツールは、エージェントのインテリジェントコア(LLM)が複雑なタスクを達成するために必要に応じてアクセスして利用できる特殊なツールキットと考えてください。
ADKのツールタイプ¶
ADKは、いくつかのタイプのツールをサポートすることで柔軟性を提供します。
- 関数ツール: 特定のアプリケーションのニーズに合わせて作成したツール。
- 関数/メソッド: コードで標準の同期関数またはメソッドを定義します(例:Python def)。
- ツールとしてのエージェント: 別の、潜在的に特殊化されたエージェントを親エージェントのツールとして使用します。
- 長時間実行関数ツール: 非同期操作を実行したり、完了にかなりの時間がかかるツールをサポートします。
- 組み込みツール: 一般的なタスクのためにフレームワークによって提供されるすぐに使えるツール。 例:Google検索、コード実行、検索拡張生成(RAG)。
- サードパーティツール: 人気のある外部ライブラリのツールをシームレスに統合します。
各ツールタイプの詳細情報と例については、上記のリンク先の各ドキュメントページに移動してください。
エージェントの指示におけるツールの参照¶
エージェントの指示内で、関数名を使用してツールを直接参照できます。ツールの関数名とdocstringが十分に説明的であれば、指示は主に大規模言語モデル(LLM)がツールをいつ利用すべきかに焦点を当てることができます。これにより、明確さが促進され、モデルが各ツールの意図された使用法を理解するのに役立ちます。
ツールが生成する可能性のあるさまざまな戻り値を処理する方法をエージェントに明確に指示することが重要です。たとえば、ツールがエラーメッセージを返す場合、指示では、エージェントが操作を再試行するか、タスクをあきらめるか、ユーザーに追加情報を要求するかを指定する必要があります。
さらに、ADKは、あるツールの出力が別のツールの入力として機能するツールの順次使用をサポートしています。このようなワークフローを実装する場合、必要な手順をモデルにガイドするために、エージェントの指示内で意図されたツール使用シーケンスを記述することが重要です。
例¶
次の例は、エージェントが指示で関数名を参照してツールを使用する方法を示しています。また、成功またはエラーメッセージなど、ツールからのさまざまな戻り値を処理するようにエージェントをガイドする方法、およびタスクを達成するために複数のツールの順次使用を調整する方法も示しています。
# 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.
import asyncio
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]
)
async def main():
"""Main function to run the agent asynchronously."""
# Session and Runner Setup
session_service = InMemorySessionService()
# Use 'await' to correctly create the session
await 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
query = "weather in london?"
print(f"User Query: {query}")
content = types.Content(role='user', parts=[types.Part(text=query)])
# The runner's run method handles the async loop internally
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)
# Standard way to run the main async function
if __name__ == "__main__":
asyncio.run(main())
// 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.
package main
import (
"context"
"fmt"
"log"
"strings"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/genai"
)
type getWeatherReportArgs struct {
City string `json:"city" jsonschema:"The city for which to get the weather report."`
}
type getWeatherReportResult struct {
Status string `json:"status"`
Report string `json:"report,omitempty"`
}
func getWeatherReport(ctx tool.Context, args getWeatherReportArgs) (getWeatherReportResult, error) {
if strings.ToLower(args.City) == "london" {
return getWeatherReportResult{Status: "success", Report: "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a chance of rain."}, nil
}
if strings.ToLower(args.City) == "paris" {
return getWeatherReportResult{Status: "success", Report: "The weather in Paris is sunny with a temperature of 25 degrees Celsius."}, nil
}
return getWeatherReportResult{}, fmt.Errorf("weather information for '%s' is not available.", args.City)
}
type analyzeSentimentArgs struct {
Text string `json:"text" jsonschema:"The text to analyze for sentiment."`
}
type analyzeSentimentResult struct {
Sentiment string `json:"sentiment"`
Confidence float64 `json:"confidence"`
}
func analyzeSentiment(ctx tool.Context, args analyzeSentimentArgs) (analyzeSentimentResult, error) {
if strings.Contains(strings.ToLower(args.Text), "good") || strings.Contains(strings.ToLower(args.Text), "sunny") {
return analyzeSentimentResult{Sentiment: "positive", Confidence: 0.8}, nil
}
if strings.Contains(strings.ToLower(args.Text), "rain") || strings.Contains(strings.ToLower(args.Text), "bad") {
return analyzeSentimentResult{Sentiment: "negative", Confidence: 0.7}, nil
}
return analyzeSentimentResult{Sentiment: "neutral", Confidence: 0.6}, nil
}
func main() {
ctx := context.Background()
model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{})
if err != nil {
log.Fatal(err)
}
weatherTool, err := functiontool.New(
functiontool.Config{
Name: "get_weather_report",
Description: "Retrieves the current weather report for a specified city.",
},
getWeatherReport,
)
if err != nil {
log.Fatal(err)
}
sentimentTool, err := functiontool.New(
functiontool.Config{
Name: "analyze_sentiment",
Description: "Analyzes the sentiment of the given text.",
},
analyzeSentiment,
)
if err != nil {
log.Fatal(err)
}
weatherSentimentAgent, err := llmagent.New(llmagent.Config{
Name: "weather_sentiment_agent",
Model: model,
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: []tool.Tool{weatherTool, sentimentTool},
})
if err != nil {
log.Fatal(err)
}
sessionService := session.InMemoryService()
runner, err := runner.New(runner.Config{
AppName: "weather_sentiment_agent",
Agent: weatherSentimentAgent,
SessionService: sessionService,
})
if err != nil {
log.Fatal(err)
}
session, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: "weather_sentiment_agent",
UserID: "user1234",
})
if err != nil {
log.Fatal(err)
}
run(ctx, runner, session.Session.ID(), "weather in london?")
run(ctx, runner, session.Session.ID(), "I don't like rain.")
}
func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) {
fmt.Printf("\n> %s\n", prompt)
events := r.Run(
ctx,
"user1234",
sessionID,
genai.NewContentFromText(prompt, genai.RoleUser),
agent.RunConfig{
StreamingMode: agent.StreamingModeNone,
},
)
for event, err := range events {
if err != nil {
log.Fatalf("ERROR during agent execution: %v", err)
}
if event.Content.Parts[0].Text != "" {
fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text)
}
}
}
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: ツールが呼び出される前にフレームワークによって認証がすでに処理されていた場合、認証応答/資格情報が含まれます(RestApiToolおよびOpenAPIセキュリティスキームで一般的です)。 -
サービスへのアクセス:アーティファクトやメモリなどの構成済みサービスと対話するためのメソッド。
ツール関数の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:*: 一時的で、呼び出し間で永続化されません(単一の実行呼び出し内でデータを渡すのに役立ちますが、通常、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 (
"fmt"
"google.golang.org/adk/tool"
)
type updateUserPreferenceArgs struct {
Preference string `json:"preference" jsonschema:"The name of the preference to set."`
Value string `json:"value" jsonschema:"The value to set for the preference."`
}
type updateUserPreferenceResult struct {
UpdatedPreference string `json:"updated_preference"`
}
func updateUserPreference(ctx tool.Context, args updateUserPreferenceArgs) (*updateUserPreferenceResult, error) {
userPrefsKey := "user:preferences"
val, err := ctx.State().Get(userPrefsKey)
if err != nil {
val = make(map[string]any)
}
preferencesMap, ok := val.(map[string]any)
if !ok {
preferencesMap = make(map[string]any)
}
preferencesMap[args.Preference] = args.Value
if err := ctx.State().Set(userPrefsKey, preferencesMap); err != nil {
return nil, err
}
fmt.Printf("Tool: Updated user preference '%s' to '%s'\n", args.Preference, args.Value)
return &updateUserPreferenceResult{
UpdatedPreference: args.Preference,
}, nil
}
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()、Goではtool.Context.Actions())は、EventActionsオブジェクトを保持します。このオブジェクトの属性を変更すると、ツールが実行を完了した後にエージェントまたはフレームワークが何をするかに影響を与えることができます。
-
skip_summarization: bool: (デフォルト:False)Trueに設定すると、ADKに通常ツールの出力を要約するLLM呼び出しをバイパスするように指示します。これは、ツールの戻り値がすでにユーザーがすぐに使えるメッセージである場合に役立ちます。 -
transfer_to_agent: str: これを別のエージェントの名前に設定します。フレームワークは現在のエージェントの実行を停止し、会話の制御を指定されたエージェントに転送します。これにより、ツールはタスクをより専門的なエージェントに動的に引き渡すことができます。 -
escalate: bool: (デフォルト:False)これをTrueに設定すると、現在のエージェントがリクエストを処理できず、制御を親エージェント(階層内にある場合)に渡す必要があることを示します。LoopAgentでは、サブエージェントのツールでescalate=Trueを設定するとループが終了します。
例¶
# 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.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
async def setup_session_and_runner():
session_service = InMemorySessionService()
session = await 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)
return session, runner
# Agent Interaction
async def call_agent_async(query):
content = types.Content(role='user', parts=[types.Part(text=query)])
session, runner = await setup_session_and_runner()
events = runner.run_async(user_id=USER_ID, session_id=SESSION_ID, new_message=content)
async for event in events:
if event.is_final_response():
final_response = event.content.parts[0].text
print("Agent Response: ", final_response)
# Note: In Colab, you can directly use 'await' at the top level.
# If running this code as a standalone Python script, you'll need to use asyncio.run() or manage the event loop.
await call_agent_async("this is urgent, i cant login")
// 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.
package main
import (
"context"
"fmt"
"log"
"strings"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/genai"
)
type checkAndTransferArgs struct {
Query string `json:"query" jsonschema:"The user's query to check for urgency."`
}
type checkAndTransferResult struct {
Status string `json:"status"`
}
func checkAndTransfer(ctx tool.Context, args checkAndTransferArgs) (checkAndTransferResult, error) {
if strings.Contains(strings.ToLower(args.Query), "urgent") {
fmt.Println("Tool: Detected urgency, transferring to the support agent.")
ctx.Actions().TransferToAgent = "support_agent"
return checkAndTransferResult{Status: "Transferring to the support agent..."}, nil
}
return checkAndTransferResult{Status: fmt.Sprintf("Processed query: '%s'. No further action needed.", args.Query)}, nil
}
func main() {
ctx := context.Background()
model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{})
if err != nil {
log.Fatal(err)
}
supportAgent, err := llmagent.New(llmagent.Config{
Name: "support_agent",
Model: model,
Instruction: "You are the dedicated support agent. Mentioned you are a support handler and please help the user with their urgent issue.",
})
if err != nil {
log.Fatal(err)
}
checkAndTransferTool, err := functiontool.New(
functiontool.Config{
Name: "check_and_transfer",
Description: "Checks if the query requires escalation and transfers to another agent if needed.",
},
checkAndTransfer,
)
if err != nil {
log.Fatal(err)
}
mainAgent, err := llmagent.New(llmagent.Config{
Name: "main_agent",
Model: model,
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: []tool.Tool{checkAndTransferTool},
SubAgents: []agent.Agent{supportAgent},
})
if err != nil {
log.Fatal(err)
}
sessionService := session.InMemoryService()
runner, err := runner.New(runner.Config{
AppName: "customer_support_agent",
Agent: mainAgent,
SessionService: sessionService,
})
if err != nil {
log.Fatal(err)
}
session, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: "customer_support_agent",
UserID: "user1234",
})
if err != nil {
log.Fatal(err)
}
run(ctx, runner, session.Session.ID(), "this is urgent, i cant login")
}
func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) {
fmt.Printf("\n> %s\n", prompt)
events := r.Run(
ctx,
"user1234",
sessionID,
genai.NewContentFromText(prompt, genai.RoleUser),
agent.RunConfig{
StreamingMode: agent.StreamingModeNone,
},
)
for event, err := range events {
if err != nil {
log.Fatalf("ERROR during agent execution: %v", err)
}
if event.Content.Parts[0].Text != "" {
fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text)
}
}
}
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_agentとsupport_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を介して、別の特殊なエージェントに制御を転送することで、会話の流れに動的に影響を与えることができる方法を示しています。
認証¶
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にアーティファactの新しいバージョンを保存します。新しいバージョン番号(0から始まる)を返します。 -
search_memory(query: str): (ADK PythonおよびGoでサポート) 構成済みの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=...)
// 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.
package main
import (
"fmt"
"google.golang.org/adk/tool"
"google.golang.org/genai"
)
type processDocumentArgs struct {
DocumentName string `json:"document_name" jsonschema:"The name of the document to be processed."`
AnalysisQuery string `json:"analysis_query" jsonschema:"The query for the analysis."`
}
type processDocumentResult struct {
Status string `json:"status"`
AnalysisArtifact string `json:"analysis_artifact,omitempty"`
Version int64 `json:"version,omitempty"`
Message string `json:"message,omitempty"`
}
func processDocument(ctx tool.Context, args processDocumentArgs) (*processDocumentResult, error) {
fmt.Printf("Tool: Attempting to load artifact: %s\n", args.DocumentName)
// List all artifacts
listResponse, err := ctx.Artifacts().List(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list artifacts")
}
fmt.Println("Tool: Available artifacts:")
for _, file := range listResponse.FileNames {
fmt.Printf(" - %s\n", file)
}
documentPart, err := ctx.Artifacts().Load(ctx, args.DocumentName)
if err != nil {
return nil, fmt.Errorf("document '%s' not found", args.DocumentName)
}
fmt.Printf("Tool: Loaded document '%s' of size %d bytes.\n", args.DocumentName, len(documentPart.Part.InlineData.Data))
// 3. Search memory for related context
fmt.Printf("Tool: Searching memory for context related to: '%s'\n", args.AnalysisQuery)
memoryResp, err := ctx.SearchMemory(ctx, args.AnalysisQuery)
if err != nil {
fmt.Printf("Tool: Error searching memory: %v\n", err)
}
memoryResultCount := 0
if memoryResp != nil {
memoryResultCount = len(memoryResp.Memories)
}
fmt.Printf("Tool: Found %d memory results.\n", memoryResultCount)
analysisResult := fmt.Sprintf("Analysis of '%s' regarding '%s' using memory context: [Placeholder Analysis Result]", args.DocumentName, args.AnalysisQuery)
fmt.Println("Tool: Performed analysis.")
analysisPart := genai.NewPartFromText(analysisResult)
newArtifactName := fmt.Sprintf("analysis_%s", args.DocumentName)
version, err := ctx.Artifacts().Save(ctx, newArtifactName, analysisPart)
if err != nil {
return nil, fmt.Errorf("failed to save artifact")
}
fmt.Printf("Tool: Saved analysis result as '%s' version %d.\n", newArtifactName, version.Version)
return &processDocumentResult{
Status: "success",
AnalysisArtifact: newArtifactName,
Version: version.Version,
}, nil
}
// メモリのコンテキストを使用してドキュメントを分析します。
// コールバックコンテキストまたは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_weather、searchDocuments、schedule_meeting)。 run、process、handle_dataなどの一般的な名前や、doStuffなどの過度に曖昧な名前は避けてください。優れた説明があっても、do_stuffのような名前は、モデルがツールをいつ使用するか、たとえばcancelFlightと比較して混乱させる可能性があります。- LLMは、ツール選択中に主要な識別子として関数名を使用します。
- アクションを明確に示す、説明的な動詞-名詞ベースの名前を使用します(例:
-
パラメーター(引数):
- 関数には任意の数のパラメーターを含めることができます。
- 明確で説明的な名前を使用します(例:
cの代わりにcity、qの代わりにsearch_query)。 - Pythonですべてのパラメーターに型ヒントを提供します(例:
city: str、user_id: int、items: list[str])。これは、ADKがLLMの正しいスキーマを生成するために不可欠です。 - すべてのパラメータータイプがJSONシリアル化可能であることを確認します。
str、int、float、bool、list、dictなどのすべてのJavaプリミティブおよび標準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': 'データベース接続に失敗しました'}を返します。 - モデルのツール実行の結果を明確に示すために、
statusキー('success'、'error'、'pending'、'ambiguous'など)を含めることは強く推奨されるプラクティスです。
- 関数の戻り値は、Pythonでは辞書(
-
Docstring / ソースコードコメント:
- これは重要です。 docstringは、LLMの主要な説明情報源です。
- ツールが何をするかを明確に記述します。 その目的と制限について具体的に記述します。
- ツールをいつ使用すべきかを説明します。 LLMの意思決定をガイドするために、コンテキストまたはシナリオ例を提供します。
- 各パラメーターを明確に説明します。 LLMがその引数に提供する必要がある情報を説明します。
- 予想される
dict戻り値の構造と意味、特にさまざまなstatus値と関連するデータキーを説明します。 - 挿入されたToolContextパラメーターを説明しないでください。LLMが知る必要のあるパラメーターではないため、docstringの説明内でオプションの
tool_context: ToolContextパラメーターに言及しないでください。ToolContextは、LLMがそれを呼び出すことを決定した後にADKによって挿入されます。
良い定義の例:
def lookup_order_status(order_id: str) -> dict:
"""顧客の注文の現在のステータスをIDを使用して取得します。
ユーザーが特定の注文のステータスを明示的に尋ね、注文IDを提供した場合にのみ、このツールを使用してください。
一般的な問い合わせには使用しないでください。
引数:
order_id:検索する注文の一意の識別子。
戻り値:
結果を示す辞書。
成功した場合、ステータスは「success」で、「order」辞書が含まれます。
失敗した場合、ステータスは「error」で、「error_message」が含まれます。
成功例:{'status': 'success', 'order': {'state': 'shipped', 'tracking_number': '1Z9...'}}
エラー例:{'status': 'error', 'error_message': '注文IDが見つかりません。'}
"""
# ...ステータスを取得するための関数実装...
if status_details := fetch_status_from_backend(order_id):
return {
"status": "success",
"order": {
"state": status_details.state,
"tracking_number": status_details.tracking,
},
}
else:
return {"status": "error", "error_message": f"注文ID {order_id} が見つかりません。"}
import (
"fmt"
"google.golang.org/adk/tool"
)
type lookupOrderStatusArgs struct {
OrderID string `json:"order_id" jsonschema:"The ID of the order to look up."`
}
type order struct {
State string `json:"state"`
TrackingNumber string `json:"tracking_number"`
}
type lookupOrderStatusResult struct {
Status string `json:"status"`
Order order `json:"order,omitempty"`
}
func lookupOrderStatus(ctx tool.Context, args lookupOrderStatusArgs) (*lookupOrderStatusResult, error) {
// ... function implementation to fetch status ...
statusDetails, ok := fetchStatusFromBackend(args.OrderID)
if !ok {
return nil, fmt.Errorf("order ID %s not found", args.OrderID)
}
return &lookupOrderStatusResult{
Status: "success",
Order: order{
State: statusDetails.State,
TrackingNumber: statusDetails.Tracking,
},
}, nil
}
/**
* 指定された都市の現在の天気予報を取得します。
*
* @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にカスタム関数ツールを効果的に活用するために必要な明確さと構造を提供し、より有能で信頼性の高いエージェントの動作につながります。
ツールセット:ツールのグループ化と動的な提供¶
個々のツールを超えて、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_context(ReadonlyContextのインスタンス)を受け取ります。このコンテキストは、現在のセッション状態(readonly_context.state)、エージェント名、呼び出しIDなどの情報への読み取り専用アクセスを提供します。ツールセットは、このコンテキストを使用して、返すツールを動的に決定できます。 BaseToolインスタンス(FunctionTool、RestApiToolなど)のlistを必ず返す必要があります。
- オプションの
-
async def close(self) -> None:この非同期メソッドは、エージェントサーバーがシャットダウンしているときやRunnerが閉じられているときなど、ツールセットが不要になったときにADKフレームワークによって呼び出されます。このメソッドを実装して、ネットワーク接続のクローズ、ファイルハンドルの解放、ツールセットによって管理されるその他のリソースのクリーンアップなど、必要なクリーンアップを実行します。
エージェントでのツールセットの使用¶
BaseToolset実装のインスタンスを、個々のBaseToolインスタンスとともにLlmAgentのtoolsリストに直接含めることができます。
エージェントが初期化されるか、利用可能な機能を決定する必要がある場合、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
)
この例では:
SimpleMathToolsetはBaseToolsetを実装し、そのget_tools()メソッドはadd_numbersとsubtract_numbersのFunctionToolインスタンスを返します。また、プレフィックスを使用して名前をカスタマイズします。calculator_agentは、個々のgreet_toolとSimpleMathToolsetのインスタンスの両方で構成されます。calculator_agentが実行されると、ADKはmath_toolset_instance.get_tools()を呼び出します。エージェントのLLMは、ユーザーリクエストを処理するためにgreet_user、calculator_add_numbers、およびcalculator_subtract_numbersにアクセスできるようになります。add_numbersツールはtool_context.stateへの書き込みを示し、エージェントの指示はこの状態の読み取りに言及しています。close()メソッドは、ツールセットが保持するリソースが解放されるように呼び出されます。
ツールセットは、ADKエージェントにツールのコレクションを整理、管理、動的に提供するための強力な方法を提供し、よりモジュール式で保守性が高く、適応性のあるエージェントアプリケーションにつながります。