Gemini Nano-র সাহায্যে এজেন্টিক ওয়ার্কফ্লো তৈরি করুন

ML Kit-এর অন-ডিভাইস এজেন্টিক ওয়ার্কফ্লো (com.google.mlkit:genai-agent, AICore ডেভেলপার প্রিভিউ প্রোগ্রামের মাধ্যমে উপলভ্য) ব্যবহার করে, আপনি Gemini Nano এবং Android AICore সিস্টেম পরিষেবার মাধ্যমে পরিচালিত অটোনোমাস, মাল্টি-টার্ন কনভারসেশনাল এজেন্ট তৈরি করতে পারবেন।

GenerativeModel ক্লাসের উপরে তৈরি, যা সরাসরি মডেল অ্যাক্সেস প্রদান করে, AgentRunner ক্লাস সম্পূর্ণ এজেন্টিক লুপ অটোমেট করে: মাল্টি-স্টেপ অ্যাকশন প্ল্যান করা, টাইপ করা টুল (যেমন অ্যাপ রিপোজিটরি কোয়েরি করা, ব্যাকএন্ড API কল করা বা লোকাল ডেটাবেস পড়া) কল করা, টুল ফলাফল মূল্যায়ন করা এবং নেটওয়ার্ক লেটেন্সি ছাড়াই এবং সম্পূর্ণ ব্যবহারকারীর গোপনীয়তা সহ ডিভাইসে স্থানীয়ভাবে কথোপকথনের প্রসঙ্গ ম্যানেজ করা। GenerativeModel ক্লাস List<Content> অবজেক্ট (USER এবং MODEL ভূমিকা সহ) এবং sessionId ভ্যালু ব্যবহার করে ক্লায়েন্ট-ম্যানেজড মাল্টি-টার্ন ইতিহাস সাপোর্ট করে, AgentRunner ক্লাস ম্যানেজড মাল্টি-টার্ন সেশন (AgentSession ইন্টারফেস ব্যবহার করে) প্রদান করে যা টার্ন জুড়ে অটোমেটিক ইন-মেমরি কথোপকথনের ইতিহাস রেকর্ড করে যাতে আপনাকে ম্যানুয়ালি ট্রান্সক্রিপ্ট ম্যানেজ করতে না হয়। স্ট্যাটিক টুল ডিক্লারেশন এবং সিস্টেম নির্দেশাবলীও ডিভাইসে অটোমেটিক ক্যাশে করা হয়, যা অতিরিক্ত কনফিগারেশন ছাড়াই টাইম-টু-ফার্স্ট-টোকেন লেটেন্সি কমায়।

মূল কম্পোনেন্ট

  • Agent ক্লাস: Agent.builder() পদ্ধতি ব্যবহার করে নির্দেশাবলী, উপলভ্য টুল এবং এক্সিকিউশন গার্ডরেল ঘোষণা করে। অটোমেটিক maxTurns = 10-এ ডিফল্ট হয়ে যায়।
  • AgentRunner ক্লাস: GenerativeModel ক্লাসের উপরে তৈরি, অন-ডিভাইস মডেলের সাহায্যে স্বয়ংক্রিয় রিজনিং লুপ ও ম্যানেজ করা সেশন অর্কেস্ট্রেট করে। Dispatchers.IO-এ ডিফল্ট হিসেবে Main-safe, যেখানে temperature ডিফল্ট হিসেবে null-এ সেট করা থাকে (যা মডেলের ডিফল্ট হিসেবে কাজ করে; গ্রিডি ডিকোডিংয়ের জন্য temperature = 0.0f পাস করুন)।
  • AgentSession ইন্টারফেস: runner.getOrCreateSession(sessionId) পদ্ধতি ব্যবহার করে স্কোপড মাল্টি-টার্ন কথোপকথন সেশন হ্যান্ডেল পাওয়া যায়, যা runAsync(content) এবং close() পদ্ধতির মাধ্যমে টার্ন জুড়ে অটোমেটিক ইন-মেমরি কথোপকথনের ইতিহাস রেকর্ড করে।
  • AgentEvent ইন্টারফেস: টেক্সট টোকেন ও টুল আপডেট নির্গত করা রিঅ্যাক্টিভ Kotlin Flow স্ট্রিম।
  • AgentTool ক্লাস: মডেলের মাধ্যমে কল করা যায় এমন টাইপ করা ফাংশন, যা Android AppFunctions (@AppFunctionDeclaration) ব্যবহার করে সংজ্ঞায়িত করা হয়।
  • GenAiException ক্লাস: টাইপ করা ব্যতিক্রমের হায়ারার্কি (com.google.mlkit.genai.common.GenAiException) যা MAX_TURNS_EXCEEDED, NOT_AVAILABLE বা BUSY-এর মতো ব্যর্থতার অবস্থা সম্পর্কে সিগন্যাল দেয়।

পূর্বশর্ত ও ডিপেন্ডেন্সি

Agent API ইন্টিগ্রেট করার আগে আপনার ডেভেলপমেন্ট এনভায়রনমেন্ট ও Gradle কনফিগারেশন নিম্নলিখিত প্রয়োজনীয়তা পূরণ করছে কিনা তা নিশ্চিত করুন।

প্ল্যাটফর্ম ও হার্ডওয়্যার সংক্রান্ত প্রয়োজনীয়তা

Gradle কনফিগারেশন

আপনার মডিউলের build.gradle.kts ফাইলে genai-agent ডিপেন্ডেন্সি যোগ করুন:

dependencies {
    // Gemini Nano agentic workflows
    implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")

    // Optional: Only required when defining tools using Android AppFunctions
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}

genai-agent-এর জন্য অ্যানোটেশন প্রসেসর বা কম্পাইলার প্লাগ-ইন প্রয়োজন না হলেও, androidx.appfunctions-এর মাধ্যমে টুল সংজ্ঞায়িত করার জন্য appfunctions-compiler KSP প্লাগ-ইন প্রয়োজন।

কুইকস্টার্ট: এজেন্ট শুরু করুন

নিম্নলিখিত কোড স্যাম্পেল থেকে জানা যায় যে কীভাবে অন-ডিভাইস এজেন্ট ও রানার ইনস্ট্যান্টিয়েট করতে হয়:

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

// Configure the agent with system instructions
val agent = Agent.builder()
    .setInstructions("You are a helpful on-device assistant.")
    .build()

// Create the runner to execute prompts
val runner = AgentRunner(agent)

টুল সংজ্ঞায়িত করুন

মডেলটি অ্যাপ ডেটা কোয়েরি করতে, ব্যাকএন্ড পরিষেবার সাথে যোগাযোগ করতে বা অ্যাপ-মধ্যস্থ অ্যাকশন ট্রিগার করতে টাইপ করা টুল কল করতে পারে। Android AppFunctions (androidx.appfunctions) ব্যবহার করে টুল ডিক্লারেটিভভাবে সংজ্ঞায়িত করা হয়:

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

@AppFunctionSerializable
data class OrderDetails(
    val orderId: String,
    val status: String,
    val eta: String,
)

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

    /**
     * Fetches details and delivery status for an in-app order.
     *
     * @param orderId The alphanumeric order identifier
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun fetch_order_details(
        orderId: String
    ): OrderDetails = withContext(Dispatchers.IO) {
        // Query local database or call your backend API
        OrderDetails(
            orderId = orderId,
            status = "Out for delivery",
            eta = "2:30 PM",
        )
    }
}

আপনার অ্যাপের 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 কনস্ট্যান্ট ব্যবহার করে), AppFunctions-কে 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() {

    /**
     * Cancels an order. Restricted strictly to this app's internal agent.
     */
    @OptIn(ExperimentalAppFunctionsApi::class)
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    fun cancel_order(
        orderId: String
    ): String {
        // Only your app's internal agent can invoke this function
        return "Order $orderId cancelled successfully."
    }
}

ACCESS_LEVEL_SELF কনস্ট্যান্টের মাধ্যমে সীমাবদ্ধ করা হলে, প্ল্যাটফর্মটি এক্সটার্নাল সারফেস এবং সিস্টেম অ্যাসিস্ট্যান্টকে IPC-তে ফাংশনটি আবিষ্কার বা এক্সিকিউট করা থেকে ব্লক করে।

এজেন্ট কনফিগার করুন

সিস্টেম নির্দেশাবলী সহ একটি Agent ইনস্ট্যান্স কনফিগার করুন এবং AppFunctions চালু করুন, তারপর AgentRunner ইনস্ট্যান্স শুরু করুন:

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

// Configure the agent with system instructions and enable AppFunctions
val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions(
        "You are a helpful assistant for looking up store orders. " +
        "Use available tools to fetch order information."
    )
    .enableAppFunctions(true) // Automatically discovers all registered AppFunctions in your app
    .build()

// Initialize the runner
val runner = AgentRunner(agent)

বিকল্প হিসেবে, সব ইন-অ্যাপ ফাংশন দেখানোর পরিবর্তে নির্দিষ্ট AppFunction আইডিতে ডিসকভারি স্কোপ করতে addAppFunctionIds("fetch_order_details", "cancel_order") মেথড কল করুন।

এজেন্ট ডিফল্ট

  • টার্ন লিমিট: Agent.Builder ক্লাস ডিফল্ট maxTurns = 10 অটোমেটিক টার্মিনেশন কন্ডিশন হিসেবে কাজ করে যাতে টুল-কলিং লুপ অসীম না হয়ে যায়। স্ট্যান্ডার্ড কথোপকথন বা টুল-কলিং ওয়ার্কফ্লোর জন্য আপনাকে ম্যানুয়ালি setMaxTurns(10) মেথড কল করতে হবে না।
  • ডিটারমিনিস্টিক গ্রিডি ডিকোডিং: AgentRunner ক্লাস ডিফল্ট temperature প্যারামিটারকে null-এ সেট করে, যা অন্তর্নিহিত মডেলের ডিফল্টকে স্থগিত করে। অন-ডিভাইসে নির্ধারক টুল বেছে নেওয়া এবং JSON স্কিমা মেনে চলার জন্য, AgentRunner কনস্ট্রাক্টরে temperature = 0.0f (temperature) পাস করুন। ক্লাউড-স্টাইল তাপমাত্রা (> 0.4f) সেট করা এড়িয়ে চলুন, যা মোবাইল মডেলে হ্যালুসিনেশন ঝুঁকি ও স্কিমা ডিগ্রেডেশন বাড়ায়।

মাল্টি-টার্ন এক্সিকিউশন ও স্ট্রিমিং ইভেন্ট

Android ViewModel ক্লাসের মধ্যে runner.getOrCreateSession(sessionId) পদ্ধতি ব্যবহার করে AgentSession ইনস্ট্যান্স পান। সেশনটি রিয়েল-টাইমে টেক্সট টোকেন ও টুল এক্সিকিউশন আপডেট এমিট করে এমন একটি Flow<AgentEvent> স্ট্রিম রিটার্ন করে:

package com.example.ai.agent

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.mlkit.genai.agent.AgentEvent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AgentSession
import com.google.mlkit.genai.common.GenAiException
import com.google.mlkit.genai.prompt.Content
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class AgentUiState(
    val messages: List<String> = emptyList(),
    val currentResponse: String = "",
    val activeToolStatus: String? = null,
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

class ChatViewModel(
    private val runner: AgentRunner,
    private val sessionId: String = "chat_session"
) : ViewModel() {

    // Scoped session preserves conversation history across configuration changes (screen rotations)
    private val session: AgentSession = runner.getOrCreateSession(sessionId)

    private val _uiState = MutableStateFlow(AgentUiState())
    val uiState: StateFlow<AgentUiState> = _uiState.asStateFlow()

    fun sendMessage(userText: String) {
        _uiState.update {
            it.copy(
                messages = it.messages + userText,
                currentResponse = "",
                activeToolStatus = null,
                isLoading = true,
                errorMessage = null
            )
        }

        viewModelScope.launch {
            val content = Content.builder().text(userText).build()
            session.runAsync(content)
                .catch { e ->
                    val message = when {
                        e is GenAiException && e.errorCode == GenAiException.ErrorCode.MAX_TURNS_EXCEEDED ->
                            "Agent exceeded the maximum reasoning turns allowed."
                        e is GenAiException && e.errorCode == GenAiException.ErrorCode.NOT_AVAILABLE ->
                            "On-device AI is currently unavailable."
                        e is GenAiException && e.errorCode == GenAiException.ErrorCode.BUSY ->
                            "On-device AI is busy. Please try again."
                        else -> e.localizedMessage ?: "Agent error"
                    }
                    _uiState.update {
                        it.copy(
                            isLoading = false,
                            activeToolStatus = null,
                            errorMessage = message
                        )
                    }
                }
                .collect { event ->
                    when (event) {
                        is AgentEvent.TextMessage -> {
                            _uiState.update {
                                it.copy(
                                    currentResponse = if (event.isPartial) {
                                        it.currentResponse + event.text
                                    } else {
                                        event.text.ifEmpty { it.currentResponse }
                                    },
                                    activeToolStatus = null
                                )
                            }
                        }
                        is AgentEvent.FunctionCall -> {
                            _uiState.update {
                                it.copy(activeToolStatus = "Executing ${event.name}...")
                            }
                        }
                        is AgentEvent.FunctionResponse -> {
                            _uiState.update {
                                it.copy(activeToolStatus = "Received data from ${event.name}")
                            }
                        }
                        else -> {}
                    }
                }

            // Finalize message once stream completes
            _uiState.update {
                it.copy(
                    messages = it.messages + it.currentResponse,
                    currentResponse = "",
                    activeToolStatus = null,
                    isLoading = false
                )
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Cleanly evicts conversation memory from RAM when ViewModel is destroyed
        session.close()
    }
}

সরাসরি টেক্সট টোকেন স্ট্রিম করুন

কনভারসেশনাল ইন্টারফেসের জন্য যেখানে টুল এক্সিকিউশন সংক্রান্ত বিবরণ UI-তে আলাদাভাবে দেখানোর প্রয়োজন নেই, সেখানে Flow<AgentEvent> ইন্টারফেসে textTokens() এক্সটেনশন ফাংশন ব্যবহার করুন:

import com.google.mlkit.genai.agent.textTokens
import com.google.mlkit.genai.prompt.Content

val content = Content.builder().text("What is my order status?").build()
session.runAsync(content)
    .textTokens()
    .collect { token ->
        print(token)
    }

সিঙ্গেল-টার্ন এক্সিকিউশন

একবারই করতে হবে এমন টাস্কের ক্ষেত্রে, যেখানে ডায়ালগ ইতিহাস RAM-এ সেভ করে রাখার প্রয়োজন নেই (যেমন, ফটো ট্যাগিং বা ক্ষণস্থায়ী টেক্সট সারসংক্ষেপ), সরাসরি runner.runSingleTurnAsync(content) মেথড কল করুন:

// Ephemeral single-turn: cleans up session state from RAM automatically upon completion
val content = Content.builder()
    .text("Summarize the order history for customer #1042: ...")
    .build()

runner.runSingleTurnAsync(content)
    .textTokens()
    .collect { token ->
        displaySummary(token)
    }

সরাসরি runner.runSingleTurnAsync(content) পদ্ধতি ব্যবহার করার সময়, AgentRunner ইনস্ট্যান্স ইন্টার্নালি একটি ক্ষণস্থায়ী সেশন আইডি বরাদ্দ করে এবং সম্পূর্ণ হওয়ার পরে মেমরি থেকে কথোপকথনের অবস্থা অটোমেটিক মুছে দেয়, যা মেমরি লিক প্রতিরোধ করে।

সেশন লাইফসাইকেল ও মেমোরি ম্যানেজমেন্ট

মাল্টি-টার্ন ডায়ালগের ইতিহাস মেমরিতে স্টোর করা হয় এবং এর sessionId ভ্যালু দ্বারা ইন্ডেক্স করা হয়:

  • কনফিগারেশন পরিবর্তন: Android ViewModel ক্লাসের মধ্যে AgentSession ইনস্ট্যান্স ধরে রাখলে, প্রতিবার স্ক্রিন ঘোরানোর সময় স্ট্রিং প্লাম্বিং না করেই অটোমেটিক কথোপকথনের ইতিহাস সংরক্ষিত থাকে।
  • সেশন এভিকশন: কোনও কথোপকথন থ্রেড বাতিল করা হলে, সিঙ্ক্রোনাসভাবে সেশন এভিক্ট করতে session.delete() মেথড কল করুন অথবা RAM খালি করতে ViewModel.onCleared() মেথডে নন-সাসপেন্ডিং session.close() মেথড কল করুন।
  • বাল্ক ইভিকশন: নির্দিষ্ট সেশন খালি করতে runner.deleteSession(sessionId) মেথড কল করুন অথবা ক্যাশে করা সব কথোপকথন ইভিক্ট করতে runner.clearAllSessions() মেথড কল করুন।
  • রানার লাইফসাইকেল: AgentRunner ক্লাস AutoCloseable ইন্টারফেস প্রয়োগ করে। বিভিন্ন স্ক্রিনে রানার শেয়ার করার সময়, প্রতিটি স্ক্রিনের ViewModel.onCleared() পদ্ধতিতে আলাদা আলাদা AgentSession হ্যান্ডেল বন্ধ করুন এবং রানার সম্পূর্ণভাবে বন্ধ করার সময় runner.close() পদ্ধতি কল করুন।
  • একই সাথে চলা সেশন: একটি AgentRunner ইনস্ট্যান্স একই সাথে একাধিক স্বতন্ত্র সেশন হোস্ট করতে পারে। প্রতিটি sessionId ভ্যালু একটি বিচ্ছিন্ন কথোপকথনের ইতিহাস বজায় রাখে।

GenAiException ক্লাস ব্যবহার করে সমস্যা ম্যানেজ করা

এজেন্ট এক্সিকিউশনের সময় রানটাইম ব্যর্থতা, যেমন যখন এজেন্ট কোনও উত্তর তৈরি না করেই সর্বাধিক টুল টার্নে পৌঁছায় বা যখন অন-ডিভাইস AI সাময়িকভাবে অনুপলভ্য থাকে, তখন GenAiException ব্যতিক্রম (com.google.mlkit.genai.common.GenAiException) হিসাবে নির্গত বা থ্রো করা হয়। Kotlin Coroutines Flow catch অপারেটর ব্যবহার করে প্রতিক্রিয়াশীলভাবে ব্যর্থতাগুলি ম্যানেজ করুন:

session.runAsync(content)
    .catch { e ->
        if (e is GenAiException) {
            when (e.errorCode) {
                GenAiException.ErrorCode.MAX_TURNS_EXCEEDED -> {
                    // Turn limit exceeded
                }
                GenAiException.ErrorCode.NOT_AVAILABLE -> {
                    // Gemini Nano or AICore unavailable
                }
                GenAiException.ErrorCode.BUSY -> {
                    // System busy or rate limit reached
                }
                else -> {
                    // Other GenAiException errors
                }
            }
        }
    }
    .collect { event ->
        // Process response events
    }

'কী' সংক্রান্ত সমস্যার কোড

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): এজেন্টটি চূড়ান্ত উত্তর জেনারেট না করেই সর্বাধিক টার্ন সীমা (maxTurns সীমা) অতিক্রম করেছে।
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): Gemini Nano মডেল বা AICore সিস্টেম পরিষেবা উপলভ্য বা রেডি নেই।
  • GenAiException.ErrorCode.BUSY (9): সিস্টেম ব্যস্ত আছে অথবা কলিং অ্যাপের কোটা শেষ হয়ে গেছে।