Premiers pas avec LiteRT-LM sur Android

L'API Kotlin de LiteRT-LM pour Android et JVM (Linux, macOS, Windows) avec des fonctionnalités telles que l'accélération GPU et NPU, la multimodalité et l'utilisation d'outils.

Introduction

Voici un exemple d'application de chat pour terminal créée avec l'API Kotlin :

import com.google.ai.edge.litertlm.*

suspend fun main() {
  Engine.setNativeMinLogSeverity(LogSeverity.ERROR) // Hide log for TUI app

  val engineConfig = EngineConfig(modelPath = "/path/to/model.litertlm")
  Engine(engineConfig).use { engine ->
    engine.initialize()

    engine.createConversation().use { conversation ->
      while (true) {
        print("\n>>> ")
        conversation.sendMessageAsync(readln()).collect { print(it) }
      }
    }
  }
}

Démonstration de l'exemple de code Kotlin

Pour essayer l'exemple ci-dessus, clonez le dépôt et exécutez-le avec example/Main.kt :

bazel run -c opt //kotlin/java/com/google/ai/edge/litertlm/example:main -- <abs_model_path>

Les modèles .litertlm disponibles se trouvent sur la communauté HuggingFace LiteRT. L'animation ci-dessus utilisait Gemma3-1B-IT.

Pour obtenir un exemple Android, consultez l'application Google AI Edge Gallery.

Premiers pas avec Gradle

Bien que LiteRT-LM soit développé avec Bazel, nous fournissons les packages Maven pour les utilisateurs de Gradle/Maven.

1. Ajouter la dépendance Gradle

dependencies {
    // For Android
    implementation("com.google.ai.edge.litertlm:litertlm-android:latest.release")

    // For JVM (Linux, MacOS, Windows)
    implementation("com.google.ai.edge.litertlm:litertlm-jvm:latest.release")
}

Vous trouverez les versions disponibles sur Google Maven dans litertlm-android et litertlm-jvm.

latest.release peut être utilisé pour obtenir la dernière version.

2. Initialiser le moteur

Engine est le point d'entrée de l'API. Initialisez-le avec le chemin d'accès et la configuration du modèle. N'oubliez pas de fermer le moteur pour libérer les ressources.

Remarque : Le chargement du modèle avec la méthode engine.initialize() peut prendre beaucoup de temps (jusqu'à 10 secondes, par exemple). Nous vous recommandons vivement d'appeler cette méthode sur un thread ou une coroutine en arrière-plan pour éviter de bloquer le thread UI.

import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig

val engineConfig = EngineConfig(
    modelPath = "/path/to/your/model.litertlm", // Replace with your model path
    backend = Backend.GPU(), // Or Backend.NPU(nativeLibraryDir = "...")
    // Optional: Pick a writable dir. This can improve 2nd load time.
    // cacheDir = "/tmp/" or context.cacheDir.path (for Android)
)

val engine = Engine(engineConfig)
engine.initialize()
// ... Use the engine to create a conversation ...

// Close the engine when done
engine.close()

Sur Android, pour utiliser le backend GPU, l'application doit demander explicitement les bibliothèques natives dépendantes en ajoutant ce qui suit à votre AndroidManifest.xml à l'intérieur de la balise <application> :

  <application>
    <uses-native-library android:name="libvndksupport.so" android:required="false"/>
    <uses-native-library android:name="libOpenCL.so" android:required="false"/>
  </application>

Pour utiliser le backend NPU, vous devrez peut-être spécifier le répertoire contenant les bibliothèques NPU. Sur Android, si les bibliothèques sont fournies avec votre application, définissez-le sur context.applicationInfo.nativeLibraryDir. Pour en savoir plus sur les bibliothèques natives NPU, consultez LiteRT-LM NPU.

val engineConfig = EngineConfig(
    modelPath = modelPath,
    backend = Backend.NPU(nativeLibraryDir = context.applicationInfo.nativeLibraryDir)
)

3. Créer une conversation

Une fois le moteur initialisé, créez une instance Conversation. Vous pouvez fournir un ConversationConfig pour personnaliser son comportement.

import com.google.ai.edge.litertlm.ConversationConfig
import com.google.ai.edge.litertlm.Message
import com.google.ai.edge.litertlm.SamplerConfig

// Optional: Configure the system instruction, initial messages, sampling
// parameters, etc.
val conversationConfig = ConversationConfig(
    systemInstruction = Contents.of("You are a helpful assistant."),
    initialMessages = listOf(
        Message.user("What is the capital city of the United States?"),
        Message.model("Washington, D.C."),
    ),
    samplerConfig = SamplerConfig(topK = 10, topP = 0.95, temperature = 0.8),
)

val conversation = engine.createConversation(conversationConfig)
// Or with default config:
// val conversation = engine.createConversation()

// ... Use the conversation ...

// Close the conversation when done
conversation.close()

Conversation implémente AutoCloseable. Vous pouvez donc utiliser le bloc use pour la gestion automatique des ressources pour les conversations ponctuelles ou de courte durée :

engine.createConversation(conversationConfig).use { conversation ->
    // Interact with the conversation
}

4. Envoyer des messages

Il existe trois façons d'envoyer des messages :

  • sendMessage(contents): Message : appel synchrone qui se bloque jusqu'à ce que le modèle renvoie une réponse complète. Cette méthode est plus simple pour les interactions de base entre les requêtes et les réponses.
  • sendMessageAsync(contents, callback) : appel asynchrone pour les réponses de streaming. Cette méthode est préférable pour les requêtes de longue durée ou lorsque vous souhaitez afficher la réponse au fur et à mesure de sa génération.
  • sendMessageAsync(contents): Flow<Message> : appel asynchrone qui renvoie un flux Kotlin pour les réponses de streaming. Il s'agit de l'approche recommandée pour les utilisateurs de coroutines.

Exemple synchrone :

import com.google.ai.edge.litertlm.Content
import com.google.ai.edge.litertlm.Message

print(conversation.sendMessage("What is the capital of France?"))

Exemple asynchrone avec rappel :

Utilisez sendMessageAsync pour envoyer un message au modèle et recevoir des réponses via un rappel.

import com.google.ai.edge.litertlm.Content
import com.google.ai.edge.litertlm.Message
import com.google.ai.edge.litertlm.MessageCallback
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

val callback = object : MessageCallback {
    override fun onMessage(message: Message) {
        print(message)
    }

    override fun onDone() {
        // Streaming completed
    }

    override fun onError(throwable: Throwable) {
        // Error during streaming
    }
}

conversation.sendMessageAsync("What is the capital of France?", callback)

Exemple asynchrone avec Flow :

Utilisez sendMessageAsync (sans l'argument de rappel) pour envoyer un message au modèle et recevoir des réponses via un flux Kotlin.

import com.google.ai.edge.litertlm.Content
import com.google.ai.edge.litertlm.Message
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

// Within a coroutine scope
conversation.sendMessageAsync("What is the capital of France?")
    .catch { ... } // Error during streaming
    .collect { print(it.toString()) }

5. Multimodalité

Les objets Message peuvent contenir différents types de Content, y compris Text, ImageBytes, ImageFile, AudioBytes et AudioFile.

// Initialize the `visionBackend` and/or the `audioBackend`
val engineConfig = EngineConfig(
    modelPath = "/path/to/your/model.litertlm", // Replace with your model path
    backend = Backend.CPU(), // Or Backend.GPU() or Backend.NPU(...)
    visionBackend = Backend.GPU(), // Or Backend.NPU(...)
    audioBackend = Backend.CPU(), // Or Backend.NPU(...)
)

// Sends a message with multi-modality.
// See the Content class for other variants.
conversation.sendMessage(Contents.of(
    Content.ImageFile("/path/to/image"),
    Content.AudioBytes(audioBytes), // ByteArray of the audio
    Content.Text("Describe this image and audio."),
))

6. Définir et utiliser des outils

Il existe deux façons de définir des outils :

  1. Avec les fonctions Kotlin (recommandé dans la plupart des cas)
  2. Avec la spécification OpenAPI (contrôle total de la spécification et de l'exécution de l'outil)

Définir des outils avec des fonctions Kotlin

Vous pouvez définir des fonctions Kotlin personnalisées comme outils que le modèle peut appeler pour effectuer des actions ou récupérer des informations.

Créez une classe implémentant ToolSet et annotez les méthodes avec @Tool et les paramètres avec @ToolParam.

import com.google.ai.edge.litertlm.Tool
import com.google.ai.edge.litertlm.ToolParam

class SampleToolSet: ToolSet {
    @Tool(description = "Get the current weather for a city")
    fun getCurrentWeather(
        @ToolParam(description = "The city name, e.g., San Francisco") city: String,
        @ToolParam(description = "Optional country code, e.g., US") country: String? = null,
        @ToolParam(description = "Temperature unit (celsius or fahrenheit). Default: celsius") unit: String = "celsius"
    ): Map<String, Any> {
        // In a real application, you would call a weather API here
        return mapOf("temperature" to 25, "unit" to  unit, "condition" to "Sunny")
    }

    @Tool(description = "Get the sum of a list of numbers.")
    fun sum(
        @ToolParam(description = "The numbers, could be floating point.") numbers: List<Double>,
    ): Double {
        return numbers.sum()
    }
}

En arrière-plan, l'API inspecte ces annotations et la signature de la fonction pour générer un schéma de style OpenAPI. Ce schéma décrit la fonctionnalité de l'outil, les paramètres (y compris leurs types et descriptions à partir de @ToolParam) et le type de retour au modèle linguistique.

Types de paramètres

Les types de paramètres annotés avec @ToolParam peuvent être String, Int, Boolean, Float, Double ou List de ces types (par exemple, List<String>). Utilisez des types pouvant avoir une valeur nulle (par exemple, String?) pour indiquer les paramètres pouvant être nuls. Définissez une valeur par défaut pour indiquer que le paramètre est facultatif et mentionnez la valeur par défaut dans la description de @ToolParam.

Type renvoyé

Le type renvoyé de votre fonction d'outil peut être n'importe quel type Kotlin. Le résultat sera converti en élément JSON avant d'être renvoyé au modèle.

  • Les types List sont convertis en tableaux JSON.
  • Les types Map sont convertis en objets JSON.
  • Les types primitifs (String, Number, Boolean) sont convertis en type primitif JSON correspondant.
  • Les autres types sont convertis en chaînes avec la méthode toString().

Pour les données structurées, il est recommandé de renvoyer Map ou une classe de données qui sera convertie en objet JSON.

Définir des outils avec la spécification OpenAPI

Vous pouvez également définir un outil en implémentant la classe OpenApiTool et en fournissant la description de l'outil sous forme de chaîne JSON conforme à la spécification OpenAPI. Cette méthode est utile si vous disposez déjà d'un schéma OpenAPI pour votre outil ou si vous avez besoin d'un contrôle précis sur la définition de l'outil.

import com.google.ai.edge.litertlm.OpenApiTool

class SampleOpenApiTool : OpenApiTool {

    override fun getToolDescriptionJsonString(): String {
        return """
        {
          "name": "addition",
          "description": "Add all numbers.",
          "parameters": {
            "type": "object",
            "properties": {
              "numbers": {
                "type": "array",
                "items": {
                  "type": "number"
                }
              },
              "description": "The list of numbers to sum."
            },
            "required": [
              "numbers"
            ]
          }
        }
        """.trimIndent() // Tip: trim to save tokens
    }

    override fun execute(paramsJsonString: String): String {
        // Parse paramsJsonString with your choice of parser/deserializer and
        // execute the tool.

        // Return the result as a JSON string
        return """{"result": 1.4142}"""
    }
}

Enregistrer des outils

Incluez des instances de vos outils dans ConversationConfig.

val conversation = engine.createConversation(
    ConversationConfig(
        tools = listOf(
            tool(SampleToolSet()),
            tool(SampleOpenApiTool()),
        ),
        // ... other configs
    )
)

// Send messages that might trigger the tool
conversation.sendMessageAsync("What's the weather like in London?", callback)

Le modèle décidera quand appeler l'outil en fonction de la conversation. Les résultats de l'exécution de l'outil sont automatiquement renvoyés au modèle pour générer la réponse finale.

Appel d'outils manuel

Par défaut, les appels d'outils générés par le modèle sont automatiquement exécutés par LiteRT-LM, et les résultats de l'exécution de l'outil sont automatiquement renvoyés au modèle pour générer la réponse suivante.

Si vous souhaitez exécuter manuellement des outils et renvoyer les résultats au modèle, vous pouvez définir automaticToolCalling dans ConversationConfig sur false.

val conversation = engine.createConversation(
    ConversationConfig(
        tools = listOf(
            tool(SampleOpenApiTool()),
        ),
        automaticToolCalling = false,
    )
)

Si vous désactivez l'appel d'outils automatique, vous devrez exécuter manuellement les outils et renvoyer les résultats au modèle dans le code de votre application. La méthode execute de OpenApiTool ne sera pas appelée automatiquement lorsque automaticToolCalling est défini sur false.

// Send a message that triggers a tool call.
val responseMessage = conversation.sendMessage("What's the weather like in London?")

// The model returns a Message with `toolCalls` populated.
if (responseMessage.toolCalls.isNotEmpty()) {
    val toolResponses = mutableListOf<Content.ToolResponse>()
    // There can be multiple tool calls in a single response.
    for (toolCall in responseMessage.toolCalls) {
        println("Model wants to call: ${toolCall.name} with arguments: ${toolCall.arguments}")

        // Execute the tool manually with your own logic. `executeTool` is just an example here.
        val toolResponseJson = executeTool(toolCall.name, toolCall.arguments)

        // Collect tool responses.
        toolResponses.add(Content.ToolResponse(toolCall.name, toolResponseJson))
    }

    // Use Message.tool to create the tool response message.
    val toolResponseMessage = Message.tool(Contents.of(toolResponses))

    // Send the tool response message to the model.
    val finalMessage = conversation.sendMessage(toolResponseMessage)
    println("Final answer: ${finalMessage.text}") // e.g., "The weather in London is 25c."
}

Exemple

Pour essayer l'utilisation d'outils, clonez le dépôt et exécutez-le avec example/ToolMain.kt :

bazel run -c opt //kotlin/java/com/google/ai/edge/litertlm/example:tool -- <abs_model_path>

Gestion des erreurs

Les méthodes d'API peuvent générer LiteRtLmJniException pour les erreurs de la couche native ou des exceptions Kotlin standards comme IllegalStateException pour les problèmes de cycle de vie. Encapsulez toujours les appels d'API dans des blocs try-catch. Le rappel onError dans MessageCallback signalera également les erreurs lors des opérations asynchrones.