Definiowanie narzędzi

Narzędzia umożliwiają agentowi na urządzeniu dostęp do danych aplikacji, komunikację z usługami backendu i wykonywanie działań w imieniu użytkownika.

Jak działają narzędzia

Gdy użytkownik prześle prompt, klasa AgentRunner wykonuje pętlę agenta obejmującą 4 etapy:

  1. Obserwacja: Gemini Nano analizuje prompt użytkownika i bieżący kontekst rozmowy pod kątem zarejestrowanych deklaracji narzędzi (przestrzeń działań agenta).
  2. Powód: model określa, czy narzędzie w aplikacji jest wymagane. Jeśli potrzebne jest narzędzie, Gemini Nano wysyła ustrukturyzowane wywołanie narzędzia zawierające nazwę funkcji i wpisane argumenty.
  3. Działanie: instancja AgentRunner automatycznie przechwytuje to wywołanie i wykonuje odpowiednią funkcję Kotlin w aplikacji.
  4. Ocena opinii: dane wyjściowe narzędzia są przekazywane z powrotem do Gemini Nano. Model ocenia opinię i odpowiada użytkownikowi lub wywołuje inne narzędzie, dopóki nie zostanie spełniony warunek zakończenia (osiągnięcie celu lub limitu maxTurns).

Narzędzia deklaratywne z funkcjami aplikacji na Androida

Zalecanym sposobem definiowania narzędzi jest deklaratywne użycie funkcji aplikacji na Androida (androidx.appfunctions). Funkcje aplikacji zapewniają ustandaryzowany model platformy do deklarowania typowanych funkcji w aplikacji z automatycznym generowaniem schematu.

1. Dodawanie zależności Gradle

Dodaj artefakty AppFunctions obok genai-agent w pliku build.gradle.kts modułu:

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. Dodaj do metod narzędzia adnotację @AppFunctionDeclaration.

Dodaj do metod narzędzia publicznego adnotację @AppFunctionDeclaration(isDescribedByKDoc = true). Opisy parametrów są dokumentowane w standardowych tagach KDoc @param.

Funkcje AppFunctions wymagają, aby zwracane wartości były typami podstawowymi (takimi jak String, Int lub Boolean) lub klasami danych strukturalnych z adnotacją @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."
            }
        )
    }
}

Zarejestruj wygenerowaną usługę w pliku AndroidManifest.xml aplikacji:

<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. Ograniczanie dostępu elementu wywołującego za pomocą stałej ACCESS_LEVEL_SELF

Funkcje AppFunctions nie muszą być publiczne dla systemu ani asystentów zewnętrznych. W Androidzie kontrola dostępu jest zarządzana za pomocą poziomów dostępu elementu wywołującego, a nie ręcznych kontroli tożsamości elementu wywołującego.

Domyślnie (przy użyciu stałej ACCESS_LEVEL_ANDROID_TRUSTED) funkcja AppFunction może być wykrywana i wykonywana przez certyfikowane przez platformę asystenty na urządzeniu, które mają uprawnienia systemowe EXECUTE_APP_FUNCTIONS. Jeśli jednak tworzysz funkcje przeznaczone wyłącznie dla wewnętrznego agenta aplikacji (np. sprawdzanie koszyków użytkowników, uzyskiwanie dostępu do lokalnych tokenów lub wykonywanie działań w aplikacji), możesz ograniczyć dostęp wyłącznie do własnej aplikacji, używając stałej 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."
        )
    }
}

Platforma obsługuje 3 poziomy dostępu rozmówcy:

  • ACCESS_LEVEL_SELF (100): ściśle ograniczone do samej aplikacji hosta. Zewnętrzni asystenci, interfejsy systemowe i inne aplikacje nie mogą wykrywać ani wywoływać funkcji przez IPC. Używaj tego poziomu w przypadku operacji poufnych lub narzędzi, które powinny obsługiwać tylko wewnętrznego agenta aplikacji.
  • ACCESS_LEVEL_SYSTEM (200): ograniczone do uprzywilejowanych wywołujących systemowych z uprawnieniem android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (np. systemowy Launcher lub Ostatnie) oraz samej aplikacji hosta.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, domyślny): otwarty dla certyfikowanych przez platformę agentów na urządzeniu z uprawnieniem EXECUTE_APP_FUNCTIONS, interfejsów systemowych i aplikacji hosta.

4. Rejestrowanie narzędzi w agencie

Funkcje AppFunctions możesz zarejestrować w agencie automatycznie za pomocą metody enableAppFunctions() lub automatycznie za pomocą metody AppFunctionAgentTools.discover():

Automatyczne wykrywanie za pomocą metody enableAppFunctions()

W przypadku prostych aplikacji, w których funkcje AppFunctions są wykonywane automatycznie, gdy model je wywołuje:

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)

Opcjonalnie możesz wywołać metodę addAppFunctionIds("fetch_order_details", "cancel_order"), aby ograniczyć wykrywanie wyłącznie do określonych identyfikatorów AppFunction, zamiast udostępniać wszystkie funkcje w aplikacji.

Automatyczne wykrywanie za pomocą metody AppFunctionAgentTools.discover()

Gdy aplikacja musi sprawdzić wykryte funkcje AppFunctions, przefiltrować je lub udekorować przed zarejestrowaniem ich u agenta:

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)

Wzorzec dekoratora i proces z udziałem człowieka

Operacje wrażliwe, takie jak anulowanie zamówienia, płatności czy usunięcie danych użytkownika, powinny wymagać potwierdzenia przez człowieka. Wszystkie narzędzia dodane do klasy Agent pochodzą z funkcji aplikacji na Androida, a klasa AgentTool udostępnia konstruktor delegujący AgentTool(delegate), dzięki czemu możesz opakować wykryte narzędzia funkcji aplikacji za pomocą wzorca dekoratora:

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)
    }
}

Sprawdzone metody projektowania narzędzi

  • Określ zakres przestrzeni działań: zadbaj o to, aby przestrzeń działań agenta (zestaw narzędzi i uprawnień udostępnionych agentowi) była wystarczająco duża, aby mógł on wykonać swoje zadanie, używając metody addAppFunctionIds() i stałej ACCESS_LEVEL_SELF. Zbyt duża przestrzeń działań sprawia, że model na urządzeniu jest bardziej podatny na błędy i zużywa tokeny okna kontekstu.
  • Opisy powinny być precyzyjne: Gemini Nano decyduje o wywołaniu narzędzia na podstawie opisów narzędzi i parametrów. Jasno określ cel narzędzia i podaj oczekiwane formaty argumentów (np. daty lub identyfikatory).
  • Zwracanie uporządkowanych map błędów: zwracaj opisowe mapy błędów, takie jak mapOf("error" to "Order not found"), gdy wystąpi oczekiwany błąd. Dzięki temu Gemini Nano może w przystępny sposób wyjaśnić użytkownikowi problem. Jeśli narzędzie zgłosi nieoczekiwany wyjątek, środowisko wykonawcze automatycznie go przechwyci i przekaże komunikat o błędzie z powrotem do modelu w celu samokorekty.
  • Zwracane dane powinny być skoncentrowane: modele na urządzeniu mają mniejsze okna kontekstu niż modele serwerowe. Zwracaj tylko pola istotne dla odpowiedzi na zapytanie użytkownika, zamiast zrzucać całe wiersze bazy danych lub nieprzetworzone odpowiedzi sieciowe.
  • Uruchamiaj operacje wejścia/wyjścia w mechanizmie dispatcher Dispatchers.IO: zawsze upewnij się, że żądania sieciowe i zapytania do bazy danych Room w narzędziach są wykonywane w mechanizmie dispatcher Dispatchers.IO, aby zapobiec blokowaniu elementu wywołującego współprogram.

Dalsze kroki