Générer une sortie structurée

Si vous devez analyser les réponses de l'API Prompt dans certains formats, tels que JSON, pour un traitement ultérieur, utilisez l'API Structured Output.

Avec l'API Structured Output, vous définissez la structure de sortie cible à l'aide de classes et d'annotations Kotlin. L'API Prompt renvoie ensuite une réponse sous la forme de votre objet Kotlin.

La génération de résultats structurés est particulièrement utile pour les tâches suivantes :

  • Extraction d'entités : extraction de champs structurés (par exemple, nom de l'événement, date, lieu) à partir de texte non structuré.
  • Classification : catégorisation du texte saisi dans des catégories prédéfinies.
  • Sérialisation des données : conversion des entrées utilisateur non structurées dans un format adapté au stockage dans une base de données ou aux appels d'API.

Prérequis

Pour vérifier que l'API Structured Output est disponible sur l'appareil, utilisez l'API isStructuredOutputFeatureAvailable(). L'API renvoie true si l'API Structured Output est disponible sur l'appareil, et false dans le cas contraire.

suspend fun isStructuredOutputFeatureAvailable(): Boolean

L'API Structured Output présente également les exigences suivantes :

  • Android (niveau d'API 26 ou version ultérieure) : minSdk 26
  • Plug-in KSP version 2.3.6 ou ultérieure

Limites

L'API Structured Output présente les limites suivantes :

  • Ne fonctionne qu'en Kotlin.
  • ProGuard peut interférer avec l'analyse de votre classe annotée. Ajoutez votre classe annotée à vos règles de conservation pour l'exclure de ProGuard si vous rencontrez des erreurs d'analyse, par exemple :
# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }

Configurer le projet

Pour commencer à utiliser l'API Structured Output, procédez comme suit :

  1. Ajoutez l'API ML Kit Prompt en tant que dépendance dans le fichier build.gradle.kts (ou build.gradle) au niveau de votre application, si vous ne l'avez pas déjà fait.

  2. Ajoutez le plug-in KSP au fichier build.gradle.kts au niveau du projet. Utilisez une version du plug-in KSP compatible avec votre version de Kotlin. Nous vous recommandons la version 2.3.6 ou ultérieure de KSP.

    dependencies {
        ...
        classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6"
    }
    
  3. Ajoutez les dépendances du compilateur structuré à votre fichier build.gradle.kts au niveau de l'application :

    dependencies {
        ...
        ksp("com.google.mlkit:genai-schema-compiler:1.0.0-alpha1")
    }
    

Définir la structure de sortie

Définissez la structure des données que vous souhaitez que le modèle renvoie à l'aide des classes de données Kotlin. Il existe deux annotations principales pour définir la structure de sortie :

  • Utilisez l'annotation @Generable pour définir la classe comme cible pour la sortie structurée.
  • Utilisez les annotations @Guide sur les propriétés de classe pour fournir des descriptions et des contraintes qui guident la sortie du modèle.

L'exemple suivant définit une structure pour extraire des informations sur les plantes :

import com.google.mlkit.genai.schema.annotations.Generable
import com.google.mlkit.genai.schema.annotations.Guide

@Generable
data class PlantList(
    @Guide(description = "The list of plants found", minItems = 1, maxItems = 5)
    val plants: List<Plant>
)

@Generable("Information about a plant species")
data class Plant(
    @Guide(description = "The common name of the plant")
    val commonName: String,
    
    @Guide(description = "The full latin scientific name of the plant")
    val scientificName: String,
    
    @Guide(
        description = "The maximum height of the plant in centimeters.",
        minimum = 1.0,
        maximum = 10000.0
    )
    val maxHeightCm: Int,
    
    @Guide(description = "Whether the plant is poisonous or not")
    val isPoisonous: Boolean?,
    
    @Guide(
        description = "The primary continent where this plant is native to",
        enumValues = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"]
    )
    val nativeContinent: String
)

Types et contraintes acceptés

Les types suivants sont acceptés dans une classe annotée @Generable, ainsi que leurs contraintes @Guide respectives :

Type Description Contraintes @Guide acceptées
String Pour le texte. description, enumValues
Double/Float Pour les nombres à virgule flottante. description, minimum, maximum
Int/Long Pour les nombres entiers. description, minimum, maximum
Boolean Pour les valeurs "true"/"false". description
List<T> Pour obtenir la liste des types ou des classes @Generable imbriquées acceptés. description, minItems, maxItems
List<String> Pour les listes de valeurs String. description, enumValues, minItems, maxItems
Classe @Generable Pour les objets structurés imbriqués. description

Générer du contenu structuré

Pour demander une sortie structurée, utilisez la fonction d'assistance generateTypedContentRequest pour encapsuler votre requête standard et spécifier la classe de sortie cible.

// 1. Initialize your GenerativeModel as usual
val generativeModel = Generation.getClient()

// 2. Prepare the prompt text
val promptText = "List some common plants found in California."
val baseRequest = GenerateContentRequest.Builder(TextPart(promptText)).build()

// 3. Create the typed request, specifying the target class (e.g., PlantList)
val typedRequest = generateTypedContentRequest(
    generateContentRequest = baseRequest,
    outputClass = PlantList::class,
    // Instructs ML Kit to include the generated schema structure in the prompt
    // sent to AICore. This should always be set to `true` unless the model
    // already knows what output format to use.
    includeSchemaInPrompt = true
)

// 4. Run the inference
try {
    val typedResponse = generativeModel.generateContent(typedRequest)
    
    // 5. Access the parsed object
    // The response candidates contain the parsed object of type T (PlantList in this case)
    val plantList: PlantList? = typedResponse.candidates.firstOrNull()?.response
    
    if (plantList != null) {
        // Process the structured data
        for (plant in plantList.plants) {
            Log.d("StructuredOutput", "Found plant: ${plant.commonName} (${plant.scientificName})")
        }
    } else {
        Log.e("StructuredOutput", "Failed to parse response into the desired structure.")
        
        // Inspect finish reason for details
        val finishReason = typedResponse.candidates.firstOrNull()?.finishReason
        Log.d("StructuredOutput", "Finish reason: $finishReason")
    }
} catch (e: GenAiException) {
    // Handle API errors
    when (e.errorCode) {
        GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS -> {
            Log.e("StructuredOutput", "The class structure is not supported.")
        }
        GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE -> {
            Log.e("StructuredOutput", "The model generated values that violate the schema constraints.")
        }
        else -> {
            Log.e("StructuredOutput", "API error: ${e.message}")
        }
    }
}

Gérer les motifs de fin et les erreurs

Lorsque vous utilisez l'API Structured Output, vous devez gérer les exceptions potentielles générées par l'API et inspecter la propriété finishReason dans les candidats de réponse si la réponse analysée est nulle.

Valeurs finishReason

La propriété finishReason peut prendre l'une des valeurs suivantes :

  • TypedFinishReason.STOP : le modèle a terminé la génération avec succès et la sortie correspond au schéma.
  • TypedFinishReason.MAX_TOKENS : le modèle s'est arrêté, car il a atteint la limite de jetons. La sortie peut être incomplète.
  • TypedFinishReason.PARSE_CLASS_ERROR : le modèle a terminé la génération, mais le fichier JSON obtenu n'a pas pu être analysé dans la classe Kotlin cible.
  • TypedFinishReason.STRUCTURE_NOT_ANNOTATED : la classe cible ou ses classes imbriquées ne comportent pas l'annotation @Generable requise.
  • TypedFinishReason.STRUCTURE_VALUES_INVALID : les valeurs générées ne respectaient pas les contraintes définies dans les annotations @Guide (par exemple, valeur hors plage, taille de la liste hors limites).
  • TypedFinishReason.OTHER : la génération a été arrêtée pour d'autres raisons.

Exceptions

L'API Structured Output peut générer GenAiException avec les codes d'erreur suivants :

  • GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS (-104) : la structure de la classe annotée n'est pas valide ou contient des types non compatibles. Il s'agit généralement d'une erreur de configuration au moment du développement. Examinez la définition de votre classe de données @Generable pour vérifier que tous les types de propriétés sont compatibles et qu'il n'y a pas de dépendances circulaires.
  • GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE (-105) : les valeurs générées par le modèle ne sont pas valides ou ne respectent pas les contraintes. Il s'agit d'une erreur d'exécution. Si vous rencontrez fréquemment cette erreur, essayez les solutions suivantes :
    • Affiner les instructions de votre requête pour guider le modèle de manière plus stricte.
    • Relâchez les contraintes (comme les limites de taille minimale, maximale ou de liste) dans vos annotations @Guide si elles sont trop restrictives pour les capacités du modèle.
    • Mettre en œuvre une stratégie de secours dans votre application, par exemple en réessayant la requête ou en affichant un état par défaut.

Compter les jetons

Pour vérifier si votre requête structurée respecte la limite de jetons d'entrée, calculez le nombre de jetons à l'aide de la méthode countTokens().

Étant donné que les requêtes de sortie structurée doivent indiquer au modèle la structure du schéma, le décompte des jetons sur le texte brut du prompt (à l'aide d'une instance GenerateContentRequest) n'est pas précis. Pour obtenir un nombre de jetons précis, vous devez transmettre l'instance GenerateTypedContentRequest complète, qui inclut votre classe cible et vos configurations de schéma, à la méthode countTokens() :

suspend fun <T : Any> countTokens(request: GenerateTypedContentRequest<T>): CountTokensResponse