ツールを定義する

ツールを使用すると、オンデバイス エージェントはアプリデータにアクセスし、バックエンド サービスと通信して、ユーザーに代わってアクションを実行できます。

ツールの仕組み

ユーザーがプロンプトを送信すると、AgentRunner クラスは 4 つのステージでエージェント ループを実行します。

  1. 観察: Gemini Nano は、登録済みのツール宣言(エージェントのアクション スペース)に照らして、ユーザーのプロンプトと現在の会話コンテキストを分析します。
  2. 理由: モデルは、アプリ内ツールが必要かどうかを判断します。ツールが必要な場合、Gemini Nano は関数名と型付き引数を含む構造化されたツール呼び出しを出力します。
  3. Act: AgentRunner インスタンスがこの呼び出しを自動的にインターセプトし、アプリ内の対応する Kotlin 関数を実行します。
  4. フィードバックを評価する: ツールの出力が Gemini Nano にフィードバックされます。モデルはフィードバックを評価し、終了条件が満たされる(目標を達成するか、maxTurns の上限に達する)まで、ユーザーに回答するか、別のツールを呼び出します。

Android AppFunctions を使用した宣言型ツール

ツールを定義するおすすめの方法は、Android AppFunctions(androidx.appfunctions)を使用して宣言的に定義することです。AppFunctions は、自動スキーマ生成を使用して型付きのアプリ内機能を宣言するための標準化されたプラットフォーム モデルを提供します。

1. Gradle の依存関係を追加する

モジュールの build.gradle.kts ファイルの genai-agent と一緒に AppFunctions アーティファクトを追加します。

dependencies {
    implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}

2. ツールメソッドに @AppFunctionDeclaration アノテーションを付ける

パブリック ツール メソッドに @AppFunctionDeclaration(isDescribedByKDoc = true) アノテーションを付けます。パラメータの説明は、標準の KDoc @param タグに記載されています。

AppFunctions では、戻り値がプリミティブ型(String、Int、Boolean など)か、@AppFunctionSerializable アノテーションでアノテーションが付けられた構造化データクラスである必要があります。

package com.example.ai.agent.tools

import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

/**
 * Structured details for an order.
 */
@AppFunctionSerializable
data class OrderDetails(
    val orderId: String,
    val status: String,
    val estimatedDelivery: String?,
    val itemCount: Int,
)

/**
 * Result returned when cancelling an order.
 */
@AppFunctionSerializable
data class CancelOrderResult(
    val orderId: String,
    val cancelled: Boolean,
    val message: String,
)

@AppFunctionServiceEntryPoint(
    serviceName = "OrderAppFunctionService",
    appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {

    private val orderRepository = OrderRepository()

    /**
     * Fetches details and delivery status for a customer order by ID.
     *
     * @param orderId The alphanumeric order identifier (for example, ORD-8492)
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun fetch_order_details(
        orderId: String
    ): OrderDetails = withContext(Dispatchers.IO) {
        val order = orderRepository.findOrder(orderId)
        if (order != null) {
            OrderDetails(
                orderId = order.id,
                status = order.status,
                estimatedDelivery = order.deliveryEstimate,
                itemCount = order.items.size,
            )
        } else {
            OrderDetails(
                orderId = orderId,
                status = "NOT_FOUND",
                estimatedDelivery = null,
                itemCount = 0,
            )
        }
    }

    /**
     * Requests a cancellation or return for an active order.
     *
     * @param orderId The order identifier to cancel
     * @param reason The reason provided by the user for cancellation
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun cancel_order(
        orderId: String,
        reason: String? = null
    ): CancelOrderResult = withContext(Dispatchers.IO) {
        val success = orderRepository.cancelOrder(orderId, reason)
        CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) {
                "Order cancellation processed."
            } else {
                "Order cannot be cancelled at this stage."
            }
        )
    }
}

生成されたサービスをアプリの AndroidManifest.xml ファイルに登録します。

<service
    android:name=".OrderAppFunctionService"
    android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
    android:exported="true">
    <property
        android:name="android.app.appfunctions.schema"
        android:value="app_functions_schema.xsd" />
    <property
        android:name="android.app.appfunctions.v2"
        android:value="order_app_functions.xml" />
    <intent-filter>
        <action android:name="android.app.appfunctions.AppFunctionService" />
    </intent-filter>
</service>

3. ACCESS_LEVEL_SELF 定数を使用して呼び出し元のアクセスを制限する

AppFunctions は、システムや外部アシスタントに公開する必要はありません。Android では、アクセス制御は、呼び出し元の ID を手動で確認するのではなく、呼び出し元のアクセスレベルによって管理されます。

デフォルトでは(ACCESS_LEVEL_ANDROID_TRUSTED 定数を使用)、EXECUTE_APP_FUNCTIONS システム権限を持つプラットフォーム認定のオンデバイス アシスタントが AppFunction を検出して実行できます。ただし、アプリの内部エージェント専用の機能(ユーザーのショッピング カートの検査、ローカル トークンへのアクセス、アプリ内アクションの実行など)を構築する場合は、ACCESS_LEVEL_SELF 定数を使用して、独自のアプリにのみアクセスを制限できます。

import androidx.appfunctions.AppFunctionAccessLevel
import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint
import androidx.appfunctions.ExperimentalAppFunctionsApi
import androidx.appfunctions.metadata.AppFunctionMetadata

@AppFunctionServiceEntryPoint(
    serviceName = "OrderAppFunctionService",
    appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {

    private val orderRepository = OrderRepository()

    /**
     * Cancels an order. Restricted exclusively to this app's internal agent.
     */
    @OptIn(ExperimentalAppFunctionsApi::class)
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun cancel_order(orderId: String): CancelOrderResult {
        // Only your app's internal agent can invoke this function
        val success = orderRepository.cancelOrder(orderId)
        return CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) "Cancelled." else "Failed."
        )
    }
}

このプラットフォームは、次の 3 つの呼び出し元アクセスレベルをサポートしています。

  • ACCESS_LEVEL_SELF(100): ホスト アプリケーション自体に厳密に制限されます。外部アシスタント、システム サーフェス、その他のアプリは、IPC 経由で関数を検出または呼び出すことはできません。このレベルは、アプリの内部エージェントのみに提供する必要がある機密性の高いオペレーションやツールに使用します。
  • ACCESS_LEVEL_SYSTEM(200): android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM 権限を保持する特権システム呼び出し元(システム ランチャーや最近使ったアイテムなど)とホストアプリ自体に制限されます。
  • ACCESS_LEVEL_ANDROID_TRUSTED(300、デフォルト): EXECUTE_APP_FUNCTIONS 権限を保持するプラットフォーム認定のオンデバイス エージェント、システム サーフェス、ホストアプリに公開されます。

4. エージェントにツールを登録する

AppFunctions をエージェントに登録するには、enableAppFunctions() メソッドを使用して自動的に登録するか、AppFunctionAgentTools.discover() メソッドを使用してプログラムで登録します。

enableAppFunctions() メソッドを使用した自動検出

モデルが呼び出したときに AppFunctions が自動的に実行されるシンプルなアプリの場合:

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .enableAppFunctions(true) // Automatically discovers all AppFunctions in your app package
    .setMaxTurns(10)
    .build()

val runner = AgentRunner(agent)

必要に応じて、addAppFunctionIds("fetch_order_details", "cancel_order") メソッドを呼び出して、すべてのアプリ内機能を公開するのではなく、特定の AppFunction ID に厳密に検出範囲を限定します。

AppFunctionAgentTools.discover() メソッドを使用したプログラムによる検出

アプリが、検出された AppFunction を検査、フィルタ、または装飾してからエージェントに登録する必要がある場合:

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AppFunctionAgentTools

val discoveredTools = AppFunctionAgentTools.discover(
    enabledAppFunctionIds = listOf("fetch_order_details", "cancel_order") // Optional ID filter
)

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .addTools(discoveredTools)
    .build()

val runner = AgentRunner(agent)

デコレータ パターンと人間参加型

注文のキャンセル、支払い、ユーザーデータの削除などの機密性の高いオペレーションでは、ユーザーの確認を必須にすることで、人間参加型(HITL)を維持する必要があります。Agent クラスに追加されたすべてのツールは Android AppFunctions から取得されます。AgentTool クラスは委任コンストラクタ AgentTool(delegate) を提供するため、デコレータ パターンを使用して検出された AppFunction ツールをラップできます。

import com.google.mlkit.genai.agent.AgentTool

class HitlConfirmationTool(
    delegate: AgentTool,
    private val onConfirm: suspend (toolName: String, args: Map<String, Any>) -> Boolean
) : AgentTool(delegate) {

    override suspend fun execute(args: Map<String, Any>): Map<String, Any?> {
        val confirmed = onConfirm(name, args)
        if (!confirmed) {
            return mapOf(
                "status" to "CANCELLED_BY_USER",
                "message" to "Action was declined by the user."
            )
        }
        return checkNotNull(delegate).execute(args)
    }
}

ツール設計のベスト プラクティス

  • アクション スペースをスコープする: addAppFunctionIds() メソッドと ACCESS_LEVEL_SELF 定数を使用して、エージェントのアクション スペース(エージェントに公開されるツールと権限のセット)をタスクに十分な大きさに保ちます。アクション スペースが大きすぎると、オンデバイス モデルでエラーが発生しやすくなり、コンテキスト ウィンドウ トークンが消費されます。
  • 説明を明示的にする: Gemini Nano は、ツールとパラメータの説明に基づいて、ツールを呼び出すタイミングを決定します。ツールの目的を明確にし、想定される引数の形式(日付や ID など)を記述します。
  • 構造化されたエラーマップを返す: 予期されるエラーが発生したときに、mapOf("error" to "Order not found") などの説明的なエラーマップを返します。これにより、Gemini Nano はユーザーに問題を適切に説明できます。ツールによって予期しない例外がスローされた場合、ランタイムは自動的にそれをキャッチし、エラー メッセージをモデルに反映して自己修正を行います。
  • 戻りペイロードを絞り込む: オンデバイス モデルは、サーバーモデルよりもコンテキスト ウィンドウが小さくなります。データベース行全体や未加工のネットワーク レスポンスをダンプするのではなく、ユーザーのクエリへの回答に関連するフィールドのみを返します。
  • Dispatchers.IO ディスパッチャで I/O を実行する: ツール内のネットワーク リクエストと Room データベース クエリが Dispatchers.IO ディスパッチャで実行されるようにして、コルーチン呼び出し元がブロックされないようにします。

次のステップ