Genera resultados estructurados

Si necesitas analizar las respuestas de Gemini Nano en ciertos formatos, como JSON, para un procesamiento posterior, usa la API de Structured Output con acceso directo al modelo.

Con la API de Structured Output, defines la estructura de salida de destino con clases y anotaciones de Kotlin. Luego, el acceso directo al modelo devuelve una respuesta en el formato de tu objeto de Kotlin.

Generar resultados estructurados es particularmente útil para tareas como las siguientes:

  • Extracción de entidades: Extrae campos estructurados (por ejemplo, nombre del evento, fecha y ubicación) de texto no estructurado.
  • Clasificación: Clasifica el texto de entrada en categorías predefinidas.
  • Serialización de datos: Convierte la entrada del usuario no estructurada en un formato adecuado para el almacenamiento en bases de datos o las llamadas a la API.

Requisitos previos

Para verificar que la API de Structured Output esté disponible en el dispositivo, usa la API de isStructuredOutputFeatureAvailable(). La API devuelve true si la API de Structured Output está disponible en el dispositivo y false en caso contrario.

suspend fun isStructuredOutputFeatureAvailable(): Boolean

La API de Structured Output también tiene los siguientes requisitos:

  • Nivel de API de Android 26 o superior (minSdk 26)
  • Complemento de KSP versión 2.3.6 o posterior

Limitaciones

La API de Structured Output tiene las siguientes limitaciones:

  • Solo funciona en Kotlin.
  • ProGuard podría interferir en el análisis de tu clase anotada. Agrega tu clase anotada a tus reglas de conservación para excluirlas de ProGuard si obtienes errores de análisis, por ejemplo:
# Keep classes used by structured output for deserialization for release builds.
-keep class com.google.mlkit.genai.demo.kotlin.Plant { *; }

Configurar proyecto

Para comenzar a usar la API de Structured Output, sigue estos pasos:

  1. Agrega la dependencia de com.google.mlkit:genai-prompt en tu archivo build.gradle.kts (o build.gradle) a nivel de la app, si aún no lo hiciste.

  2. Agrega el complemento de KSP al archivo build.gradle.kts a nivel del proyecto. Usa una versión del complemento de KSP que sea compatible con tu versión de Kotlin. Te recomendamos la versión 2.3.6 o una posterior de KSP.

    dependencies {
        ...
        classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.3.6"
    }
    
  3. Agrega las dependencias del compilador estructurado al archivo build.gradle.kts a nivel de la app:

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

Define la estructura del resultado

Define la estructura de los datos que quieres que devuelva el modelo con las clases de datos de Kotlin. Existen dos anotaciones principales para definir la estructura de salida:

  • Usa la anotación @Generable para definir la clase como un destino para el resultado estructurado.
  • Usa las anotaciones @Guide en las propiedades de la clase para proporcionar descripciones y restricciones que guíen el resultado del modelo.

En el siguiente ejemplo, se define una estructura para extraer información de plantas:

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
)

Tipos y restricciones admitidos

Los siguientes tipos se admiten dentro de una clase anotada con @Generable, junto con sus respectivas restricciones de @Guide:

Tipo Descripción Restricciones de @Guide admitidas
String Para texto description, enumValues
Double/Float Para números de punto flotante. description, minimum, maximum
Int/Long Para números enteros. description, minimum, maximum
Boolean Para valores de verdadero o falso. description
List<T> Para listas de tipos admitidos o clases @Generable anidadas. description, minItems, maxItems
List<String> Para listas de valores String. description, enumValues, minItems, maxItems
Clase @Generable Para objetos estructurados anidados. description

Genera contenido estructurado

Para solicitar un resultado estructurado, usa la función auxiliar generateTypedContentRequest para unir tu instrucción estándar y especificar la clase de resultado de destino.

// 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
)

// 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}")
        }
    }
}

Controla los motivos de finalización y los errores

Cuando uses la API de Structured Output, debes controlar las posibles excepciones que arroje la API y, si la respuesta analizada es nula, inspeccionar la propiedad finishReason en los candidatos de respuesta.

Valores de finishReason

La propiedad finishReason puede tener uno de los siguientes valores:

  • TypedFinishReason.STOP: El modelo terminó de generar correctamente y el resultado coincide con el esquema.
  • TypedFinishReason.MAX_TOKENS: El modelo se detuvo porque alcanzó el límite de tokens. Es posible que el resultado esté incompleto.
  • TypedFinishReason.PARSE_CLASS_ERROR: El modelo completó la generación, pero el JSON resultante no se pudo analizar en la clase de Kotlin de destino.
  • TypedFinishReason.STRUCTURE_NOT_ANNOTATED: A la clase de destino o a sus clases anidadas les falta la anotación @Generable requerida.
  • TypedFinishReason.STRUCTURE_VALUES_INVALID: Los valores generados infringieron las restricciones definidas en las anotaciones @Guide (por ejemplo, valor fuera de rango, tamaño de la lista fuera de los límites).
  • TypedFinishReason.OTHER: La generación se detuvo por otros motivos.

Excepciones

La API de Structured Output puede arrojar GenAiException con los siguientes códigos de error:

  • GenAiException.STRUCTURED_OUTPUT_INVALID_CLASS (-104): La estructura de la clase anotada no es válida o contiene tipos no admitidos. Por lo general, se trata de un error de configuración en el tiempo de desarrollo. Revisa la definición de la clase de datos @Generable para verificar que se admitan todos los tipos de propiedades y que no haya dependencias circulares.
  • GenAiException.STRUCTURED_OUTPUT_INVALID_VALUE (-105): Los valores que genera el modelo no son válidos o no cumplen con la verificación de restricciones. Este es un error de tiempo de ejecución. Si encuentras este error con frecuencia, considera las siguientes soluciones:
    • Refinar las instrucciones de la instrucción para guiar al modelo de forma más estricta
    • Flexibiliza las restricciones (como los límites de tamaño mínimo, máximo o de lista) en tus anotaciones de @Guide si son demasiado restrictivas para las capacidades del modelo.
    • Implementar una estrategia de resguardo en tu app, como volver a intentar la solicitud o mostrar un estado predeterminado

Cuenta tokens

Para verificar si tu instrucción estructurada está dentro del límite de tokens de entrada, calcula el recuento de tokens con el método countTokens().

Debido a que las solicitudes de salida estructurada deben indicarle al modelo la estructura del esquema, el recuento de tokens solo en el texto de la instrucción sin procesar (con una instancia de GenerateContentRequest) no es preciso. Para obtener un recuento de tokens preciso, debes pasar la instancia de GenerateTypedContentRequest completa, que incluye la clase de destino y los parámetros de configuración del esquema, al método countTokens():

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