Genera resultados estructurados

Si necesitas analizar las respuestas de la API de Prompt en ciertos formatos, como JSON, para su procesamiento posterior, usa la API de Structured Output.

Con la API de Structured Output, defines la estructura de salida de destino con clases y anotaciones de Kotlin. Luego, la API de Prompt muestra una respuesta en forma de tu objeto de Kotlin.

La generación de resultados estructurados es particularmente útil para tareas como las siguientes:

  • Extracción de entidades: Extrae campos estructurados (por ejemplo, nombre del evento, fecha, ubicación) de texto no estructurado.
  • Clasificación: Categoriza 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 de 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 muestra 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)
  • Versión 2.3.6 o superior del complemento KSP

Limitaciones

La API de Structured Output tiene las siguientes limitaciones:

  • Funciona solo en Kotlin.
  • ProGuard podría interferir con el análisis de tu clase anotada. Agrega tu clase anotada a tus reglas de conservación para excluirla 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 API de Prompt del ML Kit como una dependencia en tu archivo build.gradle.kts (o build.gradle) a nivel de la app, si aún no lo hiciste.

  2. Agrega el complemento KSP al archivo build.gradle.kts a nivel del proyecto. Usa una versión del complemento KSP que sea compatible con tu versión de Kotlin. Recomendamos la versión 2.3.6 o superior 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 de salida

Define la estructura de los datos que deseas que el modelo muestre 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 son compatibles dentro de una clase anotada con @Generable, junto con sus respectivas restricciones @Guide:

Tipo Descripción Restricciones @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 verdaderos o falsos 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 encapsular tu mensaje estándar y especificar la clase de salida 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,
    // 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}")
        }
    }
}

Maneja motivos de finalización y errores

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

Valores de finishReason

La propiedad finishReason puede tomar uno de los siguientes valores:

  • TypedFinishReason.STOP: El modelo terminó de generarse 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: La clase de destino o sus clases anidadas no tienen la anotación @Generable obligatoria.
  • TypedFinishReason.STRUCTURE_VALUES_INVALID: Los valores generados infringieron las restricciones definidas en las anotaciones @Guide (por ejemplo, valor fuera de rango, tamaño de lista fuera de 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 tiempo de desarrollo. Revisa la definición de tu 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 generados por el modelo no son válidos o no superan la verificación de restricciones. Este es un error en tiempo de ejecución. Si encuentras este error con frecuencia, considera las siguientes soluciones:
    • Refina las instrucciones del mensaje para guiar el modelo de forma más estricta.
    • Relaja las restricciones (como los límites de tamaño mínimo, máximo o de lista) en tus anotaciones @Guide si son demasiado restrictivas para las capacidades del modelo.
    • Implementa una estrategia de resguardo en tu app, como reintentar la solicitud o mostrar un estado predeterminado.

Cuenta tokens

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

Debido a que las solicitudes de resultados estructurados deben indicarle al modelo la estructura del esquema, el recuento de tokens solo en el texto del mensaje sin procesar (con una instancia de GenerateContentRequest) no es preciso. Para obtener un recuento de tokens preciso, debes pasar la instancia completa de GenerateTypedContentRequest, que incluye las configuraciones de esquema y clase de destino, al método countTokens():

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