Ingeniera de Relaciones con Desarrolladores
Ingeniero de software
Lleva las funciones de IA de Android al siguiente nivel creando agentes autónomos integrados en el dispositivo. En esta sesión, analizaremos las nuevas APIs de agentes de ML Kit, que se compilan directamente sobre las APIs de acceso directo a modelos que ya conoces.
Con la app de código abierto Jetpacker, aprenderás a encadenar acciones de varios pasos en datos locales, administrar sesiones de varios turnos con estado con contextos aislados y conectar Android MCP para potenciar tanto los asistentes integrados en la app como las plataformas a nivel del sistema de Android con una sola base de código.
AgentRunner coordina la ejecución de herramientas y proporciona métodos de sesión para conversaciones de varios turnos sin perder el contexto.Transcripción
Me llamo Jolanda Verhoef.
Soy ingeniero de Relaciones con Desarrolladores en Android y hoy me acompaña Victoria Lestari, que forma parte del equipo de ingeniería de AICore.
¡Hola!
Gracias por acompañarnos esta mañana.
Por lo tanto, nuestro equipo ha estado colaborando estrechamente durante años para sacar los modelos de IA de vanguardia del estado del arte de la nube y ejecutarlos directamente en dispositivos Android.
Y estos esfuerzos se agrupan en el paquete de IA generativa de ML Kit.
Por un lado, en este paquete de IA generativa de ML Kit, tenemos APIs de alto nivel para soluciones listas para usar, como resúmenes de texto, descripciones de imágenes y reconocimiento de voz.
Por otro lado, también abrimos el acceso directo al modelo integrado en el dispositivo, Gemini Nano.
Hoy, daremos un paso más.
Por lo tanto, con este acceso directo, te permitiremos crear agentes integrados en la app, agentes que pueden razonar en varios turnos, que pueden guardar el contexto de la conversación y que pueden realizar acciones en tu app.
¿Qué queremos decir con eso, Victoria?
Sí.
Hasta ahora, cuando hablamos de casos de uso integrados en el dispositivo, pensamos en tareas más cortas, sencillas y aisladas, como detectar una posible estafa o resumir una llamada telefónica.
Y cuando queremos crear algo más complejo, como hacer preguntas sobre tu catálogo de fotos o preguntarle a Google Maps, naturalmente recurrimos a modelos de nube grandes.
Por lo tanto, los modelos en la nube ofrecen la ventaja de un procesamiento inmenso y datos en tiempo real, pero la IA integrada en el dispositivo abre nuevas posibilidades para la baja latencia y el procesamiento de datos locales y en tiempo real.
Por lo tanto, Gemini Nano ya amplió lo que puedes hacer en tus dispositivos.
Un ejemplo de esto es Capturas de Pixel, que te permite buscar capturas de pantalla locales y hacer preguntas sobre ellas.
Por lo tanto, cuando ejecutamos agentes integrados en el dispositivo, necesitamos un gran salto en la capacidad del modelo, y por eso estamos creando Gemini Nano 4, que puede razonar en varios pasos y realizar acciones reales.
Sin embargo, esta nueva función solo es importante si los desarrolladores tienen dispositivos reales a los que orientar sus apps.
La buena noticia es que Gemini Nano 4 ya está disponible en algunos dispositivos insignia, y también puedes probarlo en muchos más dispositivos con la versión preliminar para desarrolladores de AICore.
Además, estamos en camino de alcanzar los 100 millones de dispositivos a principios del próximo año.
¿Qué significa esto?
Esto significa que puedes comenzar a crear y probar estas experiencias hoy, sabiendo que lo que estás creando hoy se escalará a un público masivo.
Ya hablamos de Gemini Nano 4 y de su alcance potencial, así que ahora hablemos de lo que realmente significan las capacidades agénticas.
Por lo tanto, la IA agéntica puede planificar tareas complejas, dividirlas en varios pasos y, luego, ejecutarlos en tu app.
Por ejemplo, nuestro modelo de Gemini Nano integrado en el dispositivo puede consultar tu base de datos local, recuperar datos del backend, razonar y responder tus preguntas complejas, y también activar el flujo de trabajo de la aplicación nativa en tu dispositivo.
Pero ¿cómo pasa una IA de crear un plan a realizar acciones reales?
Y este es el tema de hoy: la llamada a funciones.
Es el motor principal de la capacidad agéntica.
Traduce el lenguaje natural en la ejecución del sistema y lleva al modelo de razonamientos pasivos a una ejecución activa seleccionando y controlando las APIs locales necesarias para resolver las tareas complejas.
También nos complace anunciar que la llamada a funciones es compatible de forma nativa con Gemini Nano 4.
Actualmente, está disponible en la versión preliminar para desarrolladores de AICore y pronto se lanzará en todos los dispositivos compatibles con Nano 4.
Es genial que tengamos este tipo de capacidades en el modelo, pero una capacidad en un modelo aún no te brinda las APIs como desarrollador.
Por lo tanto, también estuvimos trabajando para agregar las APIs adecuadas al paquete de IA generativa de ML Kit para asegurarnos de que crear un agente sea realmente sencillo.
Por lo tanto, comenzamos con las APIs de acceso directo que ya teníamos y les agregamos tipos de funciones y tipos de historial de conversaciones.
Y con estas dos nuevas incorporaciones a la API de Prompt de ML Kit, ahora podrás compilar estas capacidades agénticas de bajo nivel en tu app.
Sin embargo, hay muchas cosas que debes hacer por tu cuenta en ese punto.
Por lo tanto, debes ejecutar las funciones de forma manual, guardar el historial de llamadas y, además, realizar un bucle por varias llamadas.
Por lo tanto, si hay varias llamadas que debes realizar, tendrás que crear algún tipo de sistema de bucle por tu cuenta y tendrás que crear tus propias barreras de seguridad para el sistema.
Eso es posible, pero, por supuesto, queremos que sea rápido y fácil.
Por lo tanto, además de esta biblioteca de la API de Prompt, también agregamos una nueva biblioteca de flujos de agentes.
Puedes encontrarlo con com.mlkit.genai:agent-api, y actualmente está disponible en versión alfa.
Ahora, con esta nueva API, hacemos gran parte del trabajo pesado por ti.
Por lo tanto, en lugar de tener que realizar muchos pasos manuales, con solo unas pocas líneas de código, podrás crear un agente.
Y eso es exactamente lo que haremos hoy.
Por lo tanto, veremos cómo funcionan estas APIs con nuestra app de ejemplo Jetpacker.
Ya vieron a Jetpacker en Google I/O y durante el transcurso de esta conferencia.
Pero lo que puedes hacer es escanear este código QR y, si te interesa, puedes echar un vistazo al código, puedes dirigir a tu agente al código, puedes interactuar con el código y puedes obtener más información sobre cómo implementamos todas estas funciones.
Jetpacker es un asistente de viajes moderno que creamos básicamente para mostrar muchas funciones inteligentes diferentes.
Por lo tanto, ya usa modelos integrados en el dispositivo y en la nube para todo tipo de API de Prompt, pero lo que vamos a hacer ahora es agregarle un agente.
Y para este agente, Jetpacker, usaremos la nueva API de agentes de IA generativa de ML Kit de la que acabamos de hablar, que usará el sistema subyacente de AI Core que se ejecuta en dispositivos Android para comunicarse con el modelo de Gemini Nano.
Ahora, puedo hablar todo lo que quiera, y hablaremos bastante, pero, por supuesto, queremos mostrar esto.
Cambiemos al teléfono y mostremos esto como una demostración.
Aquí tengo un Pixel 11 Pro.
Creo que podemos cambiar al Pro y deberías poder verlo.
Ahí vamos, ahí está.
Como puedes ver, tenemos la app de Jetpacker ejecutándose aquí.
Tiene varios de nuestros próximos viajes, y puedo ver el viaje actual, y en nuestro viaje actual, puedes ver que actualmente es 29 de octubre, por lo que tenemos nuestro taller de IA integrada en el dispositivo, la presentación de demostración, y mañana tengo un crucero por la bahía del Golden Gate y una visita al museo.
Todo eso es genial.
Ahora, podemos ir a la pantalla de gastos, donde hacemos un seguimiento del presupuesto de este viaje.
Y, como puedes ver en la parte inferior derecha, hay un nuevo destello de agente, que es el agente integrado en el dispositivo que creamos.
Comencemos con un comando bastante sencillo.
Creo que podemos pedirle que agregue un gasto específico.
Por ejemplo, digamos: "Agrega un gasto de USD 45 por el taxi a Mountain View".
Aquí vamos.
Se está preparando el gasto y se agregó.
Asombroso.
Bien, la demostración 1 funcionó.
Volvamos a las diapositivas y veamos cómo funcionó.
En primer lugar, como habrás notado, estaba usando mi voz, y esa voz también usa el modelo de Gemini Nano con la API de reconocimiento de voz para transcribir lo que estaba diciendo a texto.
Luego, ese texto es excelente y podemos enviarlo al modelo de Gemini Nano.
¿Pero qué haría?
Por ejemplo, le digo que agregue algo a los gastos, le digo algo sobre USD 45, pero no sabe realmente cómo actuar, ¿verdad?
En este punto, solo me dirá: "No sé de qué estás hablando".
Por lo tanto, lo que queremos hacer es agregar un poco más a esa instrucción.
Podemos agregar herramientas a la instrucción, de todo tipo. En este caso, agregaré obtener el presupuesto del viaje, el itinerario, etcétera. Si pasas esto al modelo Nano con nuestras APIs, comprenderá automáticamente que, en lugar de devolver texto, ahora puede devolver una llamada a función.
En nuestro caso, dirá: "En lugar de responder con texto, agregaré un gasto de USD 45 por el taxi a Mountain View".
Eso es genial, pero, Victoria, ¿cómo funciona eso en realidad?
¿Cómo lo construimos?
Sí.
Entonces, ¿cómo definimos las herramientas en nuestro código?
Esto es lo que voy a explicar.
Por lo tanto, con el MCP de Android, usamos la biblioteca de AppFunctions de Jetpack para definir y publicar estas herramientas.
Por lo tanto, la biblioteca de AppFunctions puede exponer los servicios, los datos y las acciones de tu app en el registro del SO Android para que podamos reutilizar exactamente las mismas funciones para declarar y describir las herramientas y usarlas en tu app.
Esto también permite que el usuario complete esta tarea a través de agentes y de interacciones a nivel del sistema.
Te daré un ejemplo concreto en el código.
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}Como puedes ver, este es un ejemplo de un AppFunctionDeclaration.
Puedes ver que el comentario de KDoc en la parte superior describe lo que hace la función y sus parámetros, todo en lenguaje natural.
Por lo tanto, la práctica recomendada es dar la descripción más clara posible, ya que se la enviaremos al modelo.
Y, luego, puedes ver debajo de ella, hay una anotación, el @AppFunctionAccessLevel.
Por lo tanto, esto limita la visibilidad de la función.
En este ejemplo, tenemos ACCESS_LEVEL_SELF, lo que significa que estas funciones que estamos escribiendo solo son visibles para tu app, los agentes de tu app.
Luego, podemos definir e implementar la función de suspensión en sí, y podemos escribir tantas funciones de la app como podamos y agruparlas todas en una sola clase de servicio.
Bien.
Entonces, con eso tenemos las herramientas y podemos escribirlas, lo cual es genial.
Pero, por supuesto, para crear un agente, necesitamos un poco más que solo esas AppFunctions.
Por lo tanto, tenemos tres partes más que es importante conocer cuando escribes un agente.
En primer lugar, definimos el agente.
Por lo tanto, definimos las instrucciones del sistema, las barreras de seguridad, la configuración y cómo trabajar con él.
También tenemos un AgentRunner.
AgentRunner es lo que ejecuta el agente por nosotros.
Por lo tanto, esto se encarga del mecanismo de bucle del que estábamos hablando.
Por último, tenemos AgentEvents, que es una forma de que el sistema, la API, nos informe lo que está haciendo.
Por lo tanto, nos indicará cuándo devuelve texto, cuándo realiza una llamada a la herramienta, etcétera, para que podamos mostrarlo en nuestra IU.
Bien.
Veamos el código real.
Bien.
Antes de continuar con la escritura del código, primero debemos verificar si la llamada a funciones está disponible en tu dispositivo, ya que solo se admite en Gemini Nano 4.
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}Primero, debemos verificar llamando al modelo generativo getAvailableFeatures.
Y podemos simplemente verificar si la llamada a funciones aparece en las funciones.
Si no es así, podemos implementar un resguardo correcto.
Bien.
Ahora, pasemos al código.
if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}Sabemos que la llamada a funciones es compatible con nuestro dispositivo y, luego, podemos simplemente escribir start the a-- build the agent, usando el Agent.builder.
Luego, podemos establecer su nombre.
Podemos setInstructions para definir el contexto y el arquetipo.
Por último, llamamos a enableAppFunctions.
Por lo tanto, esta función llamará, recorrerá y descubrirá automáticamente todas las AppFunctions que acabamos de enumerar antes, y acabamos de compilar el agente.
A continuación, debemos inicializar el AgentRunner.
Por lo tanto, este ejecutor de agentes administrará el bucle de ejecución por nosotros.
Simplemente lo llamamos llamando a un instructor, constra-- constructor y pasándole el agente.
Por último, podemos unir el comando por voz transcrito en un bloque de contenido para crear la instrucción.
Luego, llamamos al ejecutor.
Llamamos a esta función runSingleTurnAsync en la instrucción, y esto iniciará el agente, ejecutará la inferencia y realizará toda la inferencia de IA.
Y si solo queremos transmitir la respuesta de texto final del modelo a nuestra IU, simplemente llamamos a textTokens, y se recopilará la respuesta a medida que llegue.
Eso es todo.
De hecho, son solo unas pocas líneas de código, por lo que no bromeábamos cuando dijimos eso.
Por lo tanto, con solo estas pocas líneas de código, tienes un agente.
Ese agente hará lo que quieras.
Nos dará una respuesta de texto.
Pero, por supuesto, en esta primera demostración que mostramos, solo estábamos haciendo un comando muy simple, ¿verdad?
Por ejemplo, ejecuté un comando y realizó una acción, y eso fue todo, por lo que aún no es un agente completo, ¿verdad?
Así que hagamos las cosas un poco más interesantes.
Para la siguiente parte, quiero dar un comando, pero quiero que sea un poco más difícil para el agente.
Volvamos a mi dispositivo de demostración y veamos si podemos hacerlo un poco más difícil.
Ya estamos allí.
Asombroso.
Como dije, tenemos planeada una visita al museo para mañana.
Y para esa visita al museo, si abro el evento, puedes ver que el precio de la entrada es de USD 25.
Ahora, veamos si nuestro agente es capaz de descubrirlo por sí mismo.
Volveré a la pantalla de gastos y le daré un comando: "Agrega mi entrada al museo a mis gastos".
Aquí vamos.
Y ahora, como puedes ver, está revisando mi itinerario, encontrando la visita al museo y agregándola a mis gastos.
Aquí, se necesitan tres pasos para alcanzar el objetivo, y también me dio una respuesta de texto final.
Nuevamente, todo esto funciona de forma automática.
Viste la velocidad real de ese modelo en el dispositivo.
Volvamos a las diapositivas y veamos cómo funciona.
Como puedes ver, cambiamos un poco las cosas en comparación con nuestra primera demostración.
En este caso, como puedes ver, no le estaba hablando del museo ni del gasto, por lo que, cuando el modelo recibió la instrucción y las herramientas, intentó decidir qué hacer primero.
Entonces, aquí, decide que necesita saber más sobre mi itinerario actual, por lo que llamará a getItinerary.
Obtiene una respuesta y, luego, sabe que hay una visita a un museo en este itinerario, que probablemente sea de la que estaba hablando.
Busquemos los detalles de ese evento específico.
Y, entre todos los demás metadatos de ese evento específico, verá que es de USD 25.
Entiende, regresa y comprende que necesita llamar a agregar un gasto.
Aquí vemos que se producen varios pasos uno tras otro.
Victoria, ¿puedes profundizar un poco más en eso?
Sí.
Lo que Jolanda acaba de describir es un ejemplo concreto de la demostración que acaba de mostrar, pero aquí, lo llevaré un poco más a una capa de abstracción conceptual.
Por lo tanto, esto sucederá cada vez que hagamos una llamada a funciones de varios pasos.
En primer lugar, esta es nuestra app y ese es el modelo de Gemini Nano.
El usuario comenzará enviando la consulta y la lista de herramientas disponibles al modelo.
Por lo tanto, el modelo usará esta información para decidir qué enviar de vuelta.
Y, luego, supongamos que, en el primer caso, devuelve una llamada a la herramienta, que llamaremos llamada a la herramienta 1.
Luego, la app ejecutará esta llamada a la herramienta y tendrá una respuesta de la herramienta.
Luego, lo que enviará de vuelta al modelo es, en realidad, el historial de conversación completo, que incluye la consulta original, la lista de herramientas, la primera llamada a la herramienta y la primera respuesta de la herramienta.
Luego, el modelo, nuevamente, usa esta información para decidir qué hacer a continuación.
Por lo tanto, puede seguir yendo y viniendo hasta que obtengamos el resultado final.
Por lo tanto, supongamos que el modelo devolverá la enésima llamada a la herramienta y, luego, la app devolverá todo el historial de conversación, de nuevo.
Y cuando el modelo esté satisfecho, es decir, sepa que se completó la consulta, enviará una respuesta final a la app, y luego la app podrá mostrar en la IU que, bueno, ya terminó.
Bien.
Ahora, explicaré cómo implementamos esto en el código.
/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }Recuerda el ejemplo del código de un solo paso. Este es el código de varios pasos, por lo que se ve muy similar, y el punto más importante es escribir el KDoc de forma muy, muy clara.
Por lo tanto, en este ejemplo, la función getItinerary indica de forma explícita que solo devuelve un resumen de alto nivel de los eventos.
Por lo tanto, también mencionamos que debemos llamar a getEventDetails con un eventId si queremos tener un detalle de viaje más específico.
Y, luego, getEventDetails también tiene el comentario de KDoc, que dice que puede recuperar detalles, como los precios de los boletos y otras notas.
Por lo tanto, si usamos el comentario y lo describimos con claridad, podemos guiar al modelo para que sepa cómo interactúan las herramientas entre sí.
Bien.
Así es como creamos el agente para que realice todo este bucle, este bucle de agentes.
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}Se parece mucho al ejemplo de un solo paso, solo que aquí hay dos adiciones.
Primero, debemos establecer la cantidad máxima de turnos y, en este ejemplo, establecemos cinco.
¿Qué hace?
Esto evita que el modelo entre en un bucle infinito.
Supongamos que tiene una instrucción ambigua o que tal vez sigue llamando a una herramienta y a otra, y luego vuelve a la primera, por lo que, si establecemos esto, se detendrá y no seguirá en bucle.
Y, luego, seguimos llamando a la misma función para ejecutarla, runSingleTurnAsync.
Por lo tanto, aunque se llama de un solo turno, en realidad, un turno significa una consulta del usuario.
Por lo tanto, la expectativa del usuario final es que yo envíe una consulta y tú devuelvas una respuesta, sin que realmente me importe mucho lo que sucedió de forma interna, como cuántas veces se repite, etcétera.
Por lo tanto, podemos seguir usando la misma función.
Y, además, como la llamada a la herramienta de varios pasos lleva un poco más de tiempo, también viste en la demostración que muestra qué función está llamando en este momento, qué se está ejecutando en este momento y si ya terminó.
Por lo tanto, queremos mostrar el progreso en vivo en la IU.
Por lo tanto, en lugar de solo recopilar la respuesta de texto, podemos mostrar el nombre de la herramienta, el event.name, cada vez que el AgentEvent emita un FunctionCall o un FunctionResponse, y cuando termine, podemos devolver el TextMessage como antes.
¡Asombroso!
Por lo tanto, llamamos a esta ejecución asíncrona de un solo turno, lo que, por supuesto, supone que también hay otro tipo de API, ¿verdad?
Sí.
Por supuesto, no basta con dar un solo paso.
Queremos hacer un proceso de varios pasos.
A continuación, hablemos de las sesiones de varios turnos. Creo que podemos volver a mi demostración y hacerla un poco más difícil.
Estábamos hablando de que tenemos una visita al museo, pero también teníamos ese crucero por la bahía del Golden Gate.
Así que, para ese, quiero saber si tengo suficiente presupuesto antes de reservarlo.
Así que hagamos una pregunta.
Veamos.
Estoy pensando en llevar a mi equipo al crucero por la bahía del Golden Gate mañana y pensé que costaba alrededor de USD 180.
Si hago eso, ¿cuál sería mi presupuesto restante?
¿Qué está haciendo?
Responde cuál será mi presupuesto restante.
Por lo tanto, comprende el itinerario actual.
Tiene esa información y puede decir eso.
Pero también puedo hacer una pregunta adicional aquí.
Bien, eso es bueno.
¿Puedes agregarlo a mis gastos?
Ahí vamos, y se ejecutará.
Ah, necesita la moneda, así que, genial, efecto de demostración.
Bien, entonces está en dólares.
USD 180.
Aquí vamos.
Es la primera vez que sucede, qué genial.
Bien, ¿qué está haciendo?
Me dice que tomó tres turnos y que se agregó a los gastos, y lo que también hace es permitirme actualizar esta sesión.
Tengo un botón de actualización en la parte superior y, si lo presiono, borro todo el historial y puedo empezar de nuevo.
Con eso, creo que podemos volver a nuestras diapositivas.
Tenemos las demostraciones en funcionamiento, todas hacen lo que queremos, así que creo que podemos volver al código.
Sí.
Eso es realmente genial.
Como puedes ver, Jolanda no mencionó, solo dice, oh, está en dólares, no dice que es para el boleto del crucero por la bahía del Golden Gate.
¿Cómo conoce el modelo todo el contexto?
Esto es un poco diferente de lo que te mostré en los dos fragmentos de código anteriores.
// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()Por lo tanto, en lugar de usar AgentRunner y runSingleTurnAsync, debemos crear una sesión.
Aquí llamamos a runner.getOrCreateSession con tripId.
¿Por qué necesitamos crear una sesión?
Porque el runSingleTurnAsync restablecerá la memoria, por lo que no tendrá contexto.
No podremos tener una conversación de varios turnos.
Por lo tanto, cuando quieras tener una conversación de varios turnos, usaremos la sesión, la sesión del agente.
Luego, pasamos el tripId.
¿Por qué necesitamos hacer eso?
Como ya viste, la app de Jetpacker administra varios viajes.
Hay un viaje a ADS, un viaje a París y, tal vez, un viaje a Tokio.
Por lo tanto, queremos mantener el contexto dentro del viaje.
No queremos meternos en problemas, ¿sabes?
Entonces, en lugar de llamar a runSingleTurnAsync, tenemos runAsync.
Esto es diferente.
Esto se ejecuta en la sesión y, luego, seguimos teniendo la misma instrucción.
Y, luego, podemos emitir AgentEvent, FunctionCall, FunctionResponse y TextMessage como antes, es lo mismo.
Y, luego, cuando Jolanda hizo clic en el botón de nueva conversación, el botón de actualización, podemos tener session.delete.
Sí.
Bien.
Hicimos todo esto con las nuevas APIs de agentes, lo que es increíble y funcionará de maravillas para muchos de tus casos de uso.
Por supuesto, siempre queremos que las cosas sean rápidas o sencillas, pero también queremos que los casos más complejos sean posibles para ti.
Por lo tanto, queremos asegurarnos de que, si lo necesitas, también puedas usar las APIs de acceso directo para compilar una versión de nivel inferior de estos elementos.
Victoria, ¿podrías explicarnos qué significa y cuándo volveríamos a este nivel de acceso directo?
Sí, claro, Jolanda.
Es posible que ya conozcas la API de Prompt para este acceso directo.
Por lo tanto, también admitimos la llamada a funciones aquí y tenemos dos casos de uso en los que preferiríamos usar esta API en lugar del flujo de agentes más sencillo.
El primero es un caso de uso simple, muy simple.
Recuerda que, en el flujo de agentes, enviamos toda la conversación, la consulta, las herramientas, la llamada a la herramienta, la respuesta de la herramienta, todo de vuelta al modelo.
Y tal vez haya algunos casos de uso en los que no queramos hacer eso, solo queramos enviar la instrucción, por ejemplo.
Supongamos que queremos enviar una instrucción al modelo que devuelva una llamada a la herramienta y, luego, no queremos devolver la llamada a la herramienta, sino que solo queremos enviar otra instrucción al modelo, de modo que podamos editar el historial allí, como si tuviéramos control total sobre el historial.
Aquí te daré un ejemplo más concreto.
// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)Supongamos que tenemos una app que puede cambiar el tema de fondo de tu dispositivo.
Supongamos que el usuario envía esta consulta: "Cambia el fondo por un atardecer".
Y, luego, nuestra app ejecutó esta función, y el fondo cambió a un atardecer.
Y, luego, el usuario cambió de opinión y dijo: "No, me refería a una playa".
Lo que sucede aquí es que no necesitamos enviar el estado completo de que el dispositivo ya cambió el fondo a un atardecer y que ahora quiere cambiarlo a una playa.
No, no es necesario.
Solo necesitamos enviar la primera y la segunda instrucción, ya que la primera contiene la información de un fondo.
Por lo tanto, sabe que, oh, esto significa que quiere cambiar el fondo a una playa.
Por lo tanto, podemos usar la API de Prompt para hacerlo.
Tenemos el control total del historial, guardamos el token de contexto integrado en el dispositivo, porque, como sabes, el dispositivo tiene una ventana de tokens más limitada y, con esto, podemos guardar más tokens.
Y el segundo caso de uso es, en realidad, el extremo opuesto.
Por lo tanto, si tenemos casos de uso complejos que la API de agentes aún no puede admitir, por ejemplo, si tenemos sistemas multiagente en los que varios subagentes interactúan entre sí para resolver una tarea compleja, o un sistema híbrido en el que tenemos modelos integrados en el dispositivo, pero también queremos tener modelos en la nube, o si queremos tener una compactación personalizada de la memoria y el contexto.
Esto se relaciona con el historial, como el control total del historial que mencioné antes.
Supongamos que, como la ventana de contexto es limitada, no queremos enviar todo de vuelta al modelo, sino que solo queremos resumir la parte más importante o, tal vez, la más reciente, para tener un control total sobre esto. Entonces, usamos la API de acceso directo.
Y, para estos casos de uso, podemos usar frameworks como el Kit de desarrollo de agentes (ADK) o Crew, o también puedes crear tu propio framework de organización personalizado.
Bien.
Eso es todo lo que teníamos sobre la API de agentes.
Sin embargo, hay un tema más que nos gustaría abordar.
Hemos estado hablando de los agentes integrados en la app todo el tiempo, por lo que estás creando tu propia experiencia de agentes dentro de tu propia app, pero también estuvimos hablando de cómo usamos AppFunctions, y ¿por qué usarías AppFunctions si lo único que haces es dentro de tu propia app?
Bueno, sabemos que a los desarrolladores como tú no les gusta hacer las cosas dos veces, ¿verdad?
No nos gusta copiar y pegar código, ni tener que duplicar ninguna lógica empresarial.
Por lo tanto, con AppFunctions, en lugar de permitir que solo nuestro agente integrado en la app las consuma, con un cambio muy simple, podemos hacer que nuestras herramientas también estén disponibles para los agentes del sistema.
¿Cómo se ve eso con el código?
Sí.
Por lo tanto, con AppFunction, solo necesitamos escribir una vez y, luego, podemos exponerlas a diferentes niveles de capas según queramos.
Recuerda que antes teníamos ACCESS_LEVEL_SELF, que solo expone las funciones que escribimos para nuestro agente integrado en la app, pero también podemos cambiar ese nivel de acceso a ACCESS_LEVEL_ANDROID_TRUSTED.
Esto expondrá las funciones a agentes de confianza en el SO Android.
Por lo tanto, no es necesario volver a escribir las funciones, los comandos ni la lógica de la base de datos.
AppFunctions ya actúa como la voz de tu app tanto interna como externamente.
También queremos una demostración de eso, ¿verdad?
Bien, volvamos al teléfono y, para nuestra última demostración, salgamos de la app de Jetpacker.
Así que podemos salir y acceder a Gemini.
Por lo tanto, en Gemini, podemos llamar a @Jetpacker.
Ahora bien, un aspecto que se debe tener en cuenta es que esta función solo está disponible en versión beta limitada, por lo que no estará disponible para todo el mundo hoy mismo, pero es posible probarla.
Veamos qué pasa con Jetpacker.
¿Qué deberíamos hacer?
Tomé un café esta mañana, ¿no?
¿Puedes agregar un café de USD 7 a los gastos del viaje de ADS, por favor?
Aquí vamos.
Y lo que puedes ver es que se conecta a la app de Jetpacker.
Eso es.
Agregó el gasto por mí.
Básicamente, vimos cómo podemos usar todo en Jetpacker.
Lo bueno de Jetpacker es que también podemos activar el modo de avión.
No lo mostramos hoy, pero podemos activar el modo de avión, y todo lo que vieron en las demostraciones, y quiero mostrarlo, porque quiero asegurarme de que todos confíen en mí, así que desactivemos la conexión Wi-Fi.
¿Alguien tiene un buen ejemplo de demostración, un gasto que pueda agregar?
Veamos.
Tal vez podamos hacer ¿Qué es?
Oh, el alquiler de autos sería bueno, ¿verdad?
Bien.
Digamos: "Agrega un gasto de USD 300 por el alquiler del auto".
Aquí vamos, y lo entendió.
Bien.
Asombroso.
Bien, volvamos a las diapositivas.
Estábamos hablando de AppFunctions.
Sé que lo mencionamos muy, muy brevemente.
Si quieres saber más, también hay otra charla de Dan y Sarah en la que se explica con mucho más detalle cómo puedes habilitar esto, cómo puedes probarlo, qué herramientas tenemos, etcétera.
Hoy repasamos muchos temas diferentes.
Vimos los tipos de un solo paso, de varios pasos y de varios turnos, y, por último, también vimos cómo puedes usar la AppFunction del sistema.
Ahora, con eso, creo que tenemos un par de sugerencias antes de irnos.
Sí.
Recuerda estos tres consejos cuando comiences a crear estas experiencias de agentes.
La primera es mantener el enfoque de los conjuntos de herramientas.
Por lo tanto, recuerda que los modelos integrados en el dispositivo tienen una capacidad limitada debido a su escala más pequeña.
Por lo tanto, en lugar de registrar decenas de herramientas, es mejor usar una cantidad menor.
Y, como dije antes, escribe descripciones claras de KDoc, porque eso es lo que usa el modelo para decidir qué va a devolver.
Y, luego, también deberíamos establecer un límite máximo de turnos.
Esto también evita que el modelo entre en un bucle infinito si se confunde.
La implicación es que podríamos agotar la batería del usuario.
Por lo tanto, si tenemos esto, podemos ahorrar la batería del usuario.
Por último, debemos verificar la disponibilidad si la llamada a funciones ya es compatible con el dispositivo y, luego, implementar un resguardo correcto.
Por ejemplo, si no es compatible, puedes consultar los modelos de la nube o tu IU estándar.
Bien, creo que podemos cerrar este tema con nuestros recursos.
Una vez más, les mostraré los códigos QR.
Échale un vistazo.
Creo que una de las cosas que hay que tener en cuenta es que también tenemos Android Skills para esto.
Por lo tanto, no tienes que profundizar en cada detalle.
Tenemos documentación completa y una habilidad de Android para acompañar esta nueva API de agentes.
Así que échale un vistazo.
Descarga la Versión preliminar para desarrolladores de AICore para probar todas estas funciones. ¡Feliz programación!
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)