تعریف کردن ابزارها

ابزارها به نماینده درون‌دستگاهی شما امکان می‌دهند به داده‌های درون‌برنامه دسترسی داشته باشد، با سرویس‌های پس‌بک ارتباط برقرار کند، و ازطرف کاربر اقداماتی انجام دهد.

نحوه کارکرد ابزارها

وقتی کاربر پیام‌واره‌ای ارسال می‌کند، کلاس AgentRunner یک حلقه عامل‌گرا را در چهار مرحله اجرا می‌کند:

  1. مشاهده: Gemini Nano پیام‌واره کاربر و بافت مکالمه فعلی را درمقابل اعلامیه‌های ابزار ثبت‌شده (فضای کنش عامل) تجزیه‌وتحلیل می‌کند.
  2. دلیل: مدل تعیین می‌کند که آیا ابزار درون‌برنامه‌ای لازم است یا نه. اگر ابزاری موردنیاز باشد، Gemini Nano یک فراخوانی ابزار ساختاریافته حاوی نام تابع و آرگومان‌های تایپ‌شده صادر می‌کند.
  3. اقدام: نمونه AgentRunner به‌طور خودکار این فراخوانی را رهگیری می‌کند و تابع Kotlin مربوطه را در برنامه‌تان اجرا می‌کند.
  4. ارزیابی بازخورد: خروجی ابزار به Gemini Nano بازخورد داده می‌شود. مدل بازخورد را ارزیابی می‌کند و یا به کاربر پاسخ می‌دهد یا ابزار دیگری را فراخوانی می‌کند تا زمانی که شرط پایان برآورده شود (تکمیل هدف یا رسیدن به حد maxTurns).

ابزارهای اعلانی با Android AppFunctions

روش توصیه‌شده برای تعریف ابزارها، روش اعلانی بااستفاده از Android AppFunctions (androidx.appfunctions) است. AppFunctions یک مدل پلاتفرم استانداردشده برای اعلام قابلیت‌های درون‌برنامه تایپ‌شده با تولید خودکار طرح‌واره ارائه می‌دهد.

۱. افزودن وابستگی‌های 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")
}

۲. حاشیه‌نویسی روش‌های ابزار با حاشیه‌نویسی @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>

۳. محدود کردن دسترسی تماس‌گیرنده با ثابت ACCESS_LEVEL_SELF

AppFunctions لازم نیست برای سیستم یا دستیارهای خارجی عمومی باشد. در Android، کنترل دسترسی ازطریق سطوح دسترسی تماس‌گیرنده مدیریت می‌شود، نه ازطریق بررسی دستی هویت تماس‌گیرنده.

به‌طور پیش‌فرض (بااستفاده از ثابت ACCESS_LEVEL_ANDROID_TRUSTED)، یک AppFunction می‌تواند توسط دستیارهای درون‌دستگاهی دارای گواهینامه پلاتفرم که اجازه سیستم EXECUTE_APP_FUNCTIONS را دارند، کشف و اجرا شود. بااین‌حال، وقتی قابلیت‌هایی را می‌سازید که صرفاً برای عامل داخلی برنامه‌تان درنظر گرفته شده‌اند (مثل بررسی سبد خرید کاربر، دسترسی به توکن‌های محلی، یا انجام کنش‌های درون‌برنامه)، می‌توانید دسترسی را منحصراً به برنامه خودتان محدود کنید. برای این کار، از ثابت 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 را دارند، سطوح سیستم، و برنامه میزبان باز است.

۴. ثبت کردن ابزارها با نماینده

می‌توانید 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()

وقتی برنامه‌تان نیاز دارد AppFunctions کشف‌شده را بازرسی کند، آن‌ها را فیلتر کند، یا قبل‌از ثبت در عامل، آن‌ها را تزئین کند:

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 کشف‌شده را بااستفاده از الگوی 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 اجازه می‌دهد تا مشکل را به‌طور مناسب برای کاربر توضیح دهد. اگر استثنای غیرمنتظره‌ای توسط ابزار ایجاد شود، زمان اجرا به‌طور خودکار آن را دریافت می‌کند و پیام خطا را برای خوداصلاحی به مدل برمی‌گرداند.
  • بار داده‌های برگشتی را متمرکز نگه دارید: مدل‌های درون‌دستگاهی نسبت‌به مدل‌های سرور پنجره‌های زمینه‌ای کوچک‌تری دارند. به‌جای تخلیه کل ردیف‌های پایگاه داده یا پاسخ‌های خام شبکه، فقط فیلدهای مربوط به پاسخ به پرسمان کاربر را برگردانید.
  • ورودی/خروجی را در توزیع‌کننده Dispatchers.IO اجرا کنید: همیشه مطمئن شوید که درخواست‌های شبکه و پرسمان‌های پایگاه داده Room در ابزارها در توزیع‌کننده Dispatchers.IO اجرا می‌شوند تا از مسدود شدن فراخواننده روال هم‌زمان جلوگیری شود.

مراحل بعدی