Adicionar a API AppFunctions ao seu app

Este guia explica como integrar a API AppFunctions ao seu app Android, implementar a lógica de uma função e verificar se a integração funciona corretamente.

Compatibilidade de versões

Essa implementação exige que o compileSdk do projeto seja definido como o nível 36 da API ou mais recente.

Não é necessário verificar se as AppFunctions são compatíveis. Isso é processado automaticamente na biblioteca AppFunctions do Jetpack. AppFunctionManager retorna uma instância se o recurso for compatível e retorna nulo se não for.

Dependências

Adicione as dependências de biblioteca necessárias ao arquivo build.gradle.kts (ou build.gradle) do módulo e configure o plug-in KSP no módulo do app de nível superior, conforme mostrado:

# Add this to your app module at the top level. For multi module applications,
# you only need to specify this once.
ksp {
  arg("appfunctions:aggregateAppFunctions", "true")
}

dependencies {
  implementation("androidx.appfunctions:appfunctions:1.0.0-alpha09")
  implementation("androidx.appfunctions:appfunctions-service:1.0.0-alpha09")
  // If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP)
  // See Add the KSP plugin to your project
  ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha09")
}

Implementar a lógica do AppFunctions

Para implementar uma AppFunction no seu app Android, crie uma classe que implemente a lógica específica das AppFunctions. Isso envolve a criação de classes de dados serializáveis para parâmetros e respostas e, em seguida, o fornecimento da lógica principal no método da função.

O código a seguir mostra um exemplo de implementação para criar uma tarefa no app TODO, incluindo a definição de parâmetros e tipos de resposta personalizados e a lógica da função principal usando um repositório.

package com.example.android.appfunctions


import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionElementNotFoundException
import androidx.appfunctions.AppFunctionInvalidArgumentException
import androidx.appfunctions.service.AppFunction
import javax.inject.Inject
...

// Developers can provide additional parameters in the constructor if needed.
// This requires a custom factory setup in the next step.
class TaskFunctions @Inject constructor(
  private val taskRepository: TaskRepository
) {
  /** The parameter to create the task. */
  @AppFunctionSerializable(isDescribedByKDoc = true)
  data class CreateTaskParams(
    /** The title of the task. */
    val title: String,
    /** The content of the task. */
     val content: String
  )

  /** The user-created task. */
  @AppFunctionSerializable(isDescribedByKDoc = true)
  data class Task(
    /** The ID of the task. */
    val id: String,
    /** The title of the task. */
    val title: String,
    /** The content of the task. */
    val content: String
  )

  /**
   * Creates a task based on [createTaskParams].
   *
   * @param createTaskParams The parameter to describe how to create the task.
   */
  @AppFunction(isDescribedByKDoc = true)
  suspend fun createTask(
    appFunctionContext: AppFunctionContext,
    createTaskParams: CreateTaskParams,
  ): Task = withContext(Dispatchers.IO) {
    // Developers can use predefined exceptions to let the agent know
    // why it failed.
    if (createTaskParams.title == null && createTaskParams.content == null) {
      throw AppFunctionInvalidArgumentException("Title or content should be non-null")
    }

    val id = taskRepository.createTask(
             createTaskParams.title,
             createTaskParams.content)

    return taskRepository
        .getTask(id)
        ?.toTask()
        ?: throw AppFunctionElementNotFoundException("Task not found for ID = $id")
  }


  // Maps internal TaskEntity
  private fun TaskEntity.toTask() = Task(id = id, title = title, content = description)
}

Pontos principais sobre o código

  • Por padrão, uma implementação de AppFunction é executada na linha de execução da interface do Android. Portanto, uma operação de longa duração precisa fazer o seguinte:
    • Declare a AppFunction como uma função de suspensão.
    • Mude para um dispatcher de corrotina adequado quando a operação puder bloquear a linha de execução.
  • Quando isDescribedByKDoc é definido como true, a descrição da função ou a descrição serializável é codificada como parte do AppFunctionMetadata para ajudar o agente a entender como usar a AppFunction do app.

Opcional: use o Hilt para fornecer uma fábrica AppFunction personalizada

Se a classe de implementação AppFunction exigir dependências no construtor (como em TaskRepository no exemplo anterior), você precisará fornecer uma fábrica personalizada para que o sistema saiba como instanciá-la. Essa etapa é opcional e só é necessária se a classe de função tiver parâmetros de construtor. Este exemplo mostra como criar um AppFunctionFactory personalizado e configurá-lo na classe Application usando o Hilt para injeção de dependência.

import android.app.Application
import androidx.appfunctions.service.AppFunctionConfiguration
import com.example.android.appfunctions.TaskFunctions
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class TodoApplication : Application(), AppFunctionConfiguration.Provider {
  @Inject lateinit var taskFunctions: TaskFunctions
  override fun onCreate() {
    super.onCreate()
  }
  // This shows how AppFunctions works with Hilt.
  override val appFunctionConfiguration: AppFunctionConfiguration
    get() =
      AppFunctionConfiguration.Builder()
        .addEnclosingClassFactory(TaskFunctions::class.java) { taskFunctions }
        .build()
}

Opcional: alternar a disponibilidade de AppFunction no momento da execução

Use a API AppFunctionManager para ativar ou desativar funções explicitamente ao controlar o acesso às AppFunctions. O gating pode ser útil quando determinados recursos do app não estão disponíveis para todos os usuários. Ao ativar ou desativar dinamicamente as AppFunctions, o sistema de inteligência sabe exatamente quais recursos estão disponíveis para o usuário a qualquer momento.

Para restringir com segurança as AppFunctions que exigem um estado de conta específico, siga um processo de duas etapas:

Etapa 1. Desativar a função por padrão

Para evitar que a função fique acessível antes que a flag de recurso seja verificada, defina o parâmetro isEnabled da anotação @AppFunction como false.

@AppFunction(isEnabled = false, isDescribedByKDoc = true)
suspend fun createTask(...) { ... }

Etapa 2: Ativar dinamicamente a função no ambiente de execução

Para cada classe AppFunction, o compilador gera uma classe correspondente que contém constantes de ID de função (usando um sufixo Ids). É possível usar essas constantes de ID geradas com o método setAppFunctionEnabled de AppFunctionManagerCompat para mudar o estado ativado de uma função durante a execução.

import androidx.appfunctions.AppFunctionManager
// Assuming there is a hook API to observe user state or feature flags
suspend fun onFeatureEnabled() {
    try {
        AppFunctionManager.getInstance(context)
            .setAppFunctionEnabled(
                // Function ID is generated for developer to get
                TaskFunctionsIds.CREATE_TASK_ID,
                AppFunctionManagerCompat.APP_FUNCTION_STATE_ENABLED,
            )
    } catch (e: Exception) {
        // Handle exception: AppFunctions indexation may not be fully completed
        // upon initial app startup.
    }
}

suspend fun onFeatureDisabled() {
    AppFunctionManagerCompat.getInstance(context)
        .setAppFunctionEnabled(
            TaskFunctionsIds.CREATE_TASK_ID,
            AppFunctionManagerCompat.APP_FUNCTION_STATE_DISABLED,
        )
}

Considerações sobre os tipos de funcionalidades a serem disponibilizadas

A segurança é sempre fundamental. Ao escolher quais recursos do seu app disponibilizar como AppFunctions, é importante lembrar que os agentes do sistema podem processar consultas do usuário no servidor para aproveitar recursos avançados de LLM.

Para oferecer uma ótima experiência ao usuário e evitar a exposição de informações sensíveis, recomendamos seguir estas diretrizes:

  • Funcionalidade que se beneficia da linguagem natural: disponibilize tarefas que sejam mais fáceis de expressar em uma conversa do que por navegação manual na interface do usuário.
  • Acesso restrito: crie AppFunctions que só dão ao agente acesso a dados e ações necessários para atender à solicitação específica do usuário.
  • Informações não sensíveis: compartilhe apenas dados que não sejam altamente pessoais ou confidenciais, ou dados que o usuário concorda explicitamente em compartilhar no contexto da ação.
  • Confirmação inequívoca para qualquer ação destrutiva: tenha muito cuidado com funções que realizam ações destrutivas, como exclusão de dados. Embora o agente possa invocar essas ações, o app precisa incluir uma etapa de confirmação própria e usar uma linguagem clara e sem ambiguidades sobre as intenções. Também é útil adicionar mais de uma etapa de confirmação para garantir que o usuário esteja ciente do que está sendo solicitado.

Verificar a integração do AppFunction

Para verificar se você integrou corretamente o AppFunctions, use adb shell cmd app_function.

Use adb shell cmd app_function list-app-functions | grep --after-context 10 $myPackageName para ver detalhes das AppFunctions que seu app oferece.

No Gemini no Android Studio ou em outros agentes da sua escolha, forneça um comando como o seguinte.

Execute `adb shell cmd app_function` to learn how the tool works, then act as a
chat agent aiming to invoke AppFunctions to fulfil user prompts for this app.
Rely on the AppFunction description as instructions.