הגדרת כלים

הכלים מאפשרים לסוכן במכשיר לגשת לנתונים באפליקציה, לתקשר עם שירותים בקצה העורפי ולבצע פעולות בשם המשתמש.

איך הכלים פועלים

כשהמשתמש שולח הנחיה, המחלקה AgentRunner מריצה לולאת סוכן בארבעה שלבים:

  1. תצפית: Gemini Nano מנתח את ההנחיה של המשתמש ואת ההקשר הנוכחי של השיחה בהשוואה להצהרות הכלים הרשומות (מרחב הפעולה של הסוכן).
  2. הסיבה: המודל קובע אם נדרש כלי בתוך האפליקציה. אם צריך כלי, Gemini Nano מפעיל כלי מובנה שמכיל את שם הפונקציה והארגומנטים שהוקלדו.
  3. פעולה: המכונה של AgentRunner מיירטת באופן אוטומטי את הקריאה הזו ומפעילה את פונקציית Kotlin המתאימה באפליקציה.
  4. הערכת המשוב: הפלט של הכלי מוזן בחזרה ל-Gemini Nano. המודל מעריך את המשוב ועונה למשתמש או קורא לכלי אחר עד שמתקיים תנאי סיום (השלמת היעד או הגעה למגבלה של maxTurns).

כלים הצהרתיים עם Android AppFunctions

הדרך המומלצת להגדיר כלים היא באופן הצהרתי באמצעות Android AppFunctions‏ (androidx.appfunctions). פונקציות AppFunctions מספקות מודל פלטפורמה סטנדרטי להצהרה על יכולות מוקלדות בתוך האפליקציה, עם יצירת סכימה אוטומטית.

1. הוספת יחסי תלות של Gradle

מוסיפים את הארטיפקטים של AppFunctions לצד genai-agent בקובץ build.gradle.kts של המודול:

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, בקרת הגישה מנוהלת באמצעות רמות גישה של מבצע הקריאה, ולא באמצעות בדיקות ידניות של זהות מבצע הקריאה.

כברירת מחדל (באמצעות הקבוע 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."
        )
    }
}

הפלטפורמה תומכת בשלוש רמות גישה למשתתפים:

  • ‫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 במקום לחשוף את כל הפונקציות באפליקציה.

גילוי פרוגרמטי באמצעות השיטה AppFunctionAgentTools.discover()

אם האפליקציה צריכה לבדוק את פונקציות האפליקציה שנמצאו, לסנן אותן או לקשט אותן לפני הרישום אצל הנציג:

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)

תבנית Decorator והאדם שבתהליך

בפעולות רגישות, כמו ביטול הזמנות, תשלומים או מחיקת נתוני משתמשים, צריך לשמור על האדם שבתהליך (HITL) על ידי דרישה לאישור המשתמש. כל הכלים שנוספים לכיתה Agent מגיעים מ-Android AppFunctions, והכיתה AgentTool מספקת בנאי להקצאת גישה AgentTool(delegate) כדי שתוכלו לעטוף את הכלים של AppFunction שזוהו באמצעות דפוס Decorator:

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 מסתמך על תיאורי כלים ופרמטרים כדי להחליט מתי להפעיל כלי. חשוב להסביר בבירור מה מטרת הכלי ולציין את הפורמטים הצפויים של הארגומנטים (כמו תאריכים או מזהים).
  • החזרת מפות שגיאות מובְנות: החזרת מפות שגיאות תיאוריות כמו mapOf("error" to "Order not found") כשמתרחש כשל צפוי. כך Gemini Nano יכול להסביר למשתמש את הבעיה בצורה ברורה. אם כלי מסוים זורק חריגה לא צפויה, סביבת זמן הריצה תופסת אותה באופן אוטומטי ומחזירה את הודעת השגיאה למודל כדי שיבצע תיקון עצמי.
  • התמקדות בתוכן המוחזר: למודלים במכשיר יש חלונות הקשר קטנים יותר מאשר למודלים בשרת. החזירו רק את השדות שרלוונטיים למענה על השאילתה של המשתמש, במקום להציג שורות שלמות של מסד נתונים או תגובות רשת גולמיות.
  • הפעלת קלט/פלט ב-dispatcher של Dispatchers.IO: תמיד צריך לוודא שבקשות לאחזור מהרשת ושאילתות של מסד נתונים של Room בתוך הכלים מופעלות ב-dispatcher של Dispatchers.IO כדי למנוע חסימה של מבצע הקריאה של ה-coroutine.

השלבים הבאים