تمنح الأدوات وكيلك على الجهاز فقط إمكانية الوصول إلى بيانات التطبيق والتواصل مع خدمات الخلفية وتنفيذ الإجراءات نيابةً عن المستخدم.
طريقة عمل الأدوات
عندما يرسل المستخدم طلبًا، تنفّذ فئة AgentRunner
حلقة وكيل على أربع مراحل:
- الملاحظة: يحلّل Gemini Nano طلب المستخدم وسياق المحادثة الحالي مقارنةً بتعريفات الأدوات المسجَّلة (مساحة الإجراءات الخاصة بالوكيل).
- السبب: يحدّد النموذج ما إذا كانت هناك حاجة إلى أداة داخل التطبيق. إذا كانت هناك حاجة إلى أداة، يُصدر Gemini Nano طلبًا منظّمًا لاستدعاء الأداة يحتوي على اسم الدالة والوسيطات المكتوبة.
- التنفيذ: يعترض مثيل
AgentRunnerهذا الاستدعاء تلقائيًا وينفّذ دالة Kotlin المقابلة في تطبيقك. - تقييم الملاحظات: تتم إعادة إدخال ناتج الأداة في 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)، يمكن اكتشاف 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وواجهات النظام والتطبيق المضيف.
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()
عندما يحتاج تطبيقك إلى فحص دوال 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)
نمط Decorator والمشاركة البشرية
يجب أن تتطلّب العمليات الحسّاسة، مثل إلغاء الطلبات أو الدفعات أو حذف بيانات المستخدمين، مشاركة بشرية من خلال طلب تأكيد من المستخدم. جميع الأدوات التي تمت إضافتها إلى الفئة 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لمنع حظر المتصل بالدالة الروتينية المشتركة.
الخطوات التالية
- كيفية إدارة الجلسات ومراحل النشاط: تعرَّف على كيفية الحفاظ على سياق المحادثة عند تدوير التطبيق وإزالة الجلسات.
- التعامل مع الرموز المميّزة للنصوص المتدفقة وحالات تنفيذ الأدوات في أحداث البث ودمج واجهة المستخدم