OpenAI com Android (Retrofit + Dagger Hilt + Compose UI)

OpenAI com Android (Retrofit + Dagger Hilt + Compose UI)

Neste artigo vamos falar sobre OpenAI e como integrar essa tecnologia com Android usando as bibliotecas do Jetpack, no caso Dagger Hilt e Compose UI, e também Retrofit para a camada de comunicação com os servidores da OpenAI.

Primeiramente, o que é OpenAI e qual sua importância na tecnologia de aplicativos?

A OpenAI é uma biblioteca de inteligência artificial que disponibiliza diversos modelos de linguagem avançados, como o GPT-3 e suas variantes. A importância da OpenAI na tecnologia de aplicativos está relacionada à capacidade de seus modelos de linguagem em entender e gerar texto de maneira sofisticada. Esses modelos podem ser utilizados em uma ampla gama de aplicações, como chatbots, assistentes virtuais, sistemas de tradução automática, geração de conteúdo, análise de dados, entre outros.

Ao utilizar os modelos da OpenAI, os desenvolvedores de aplicativos podem fornecer uma interação mais natural e eficaz com os usuários, permitindo uma compreensão avançada de linguagem natural e uma geração de texto mais precisa e coerente. Isso pode melhorar a experiência do usuário, aumentar a produtividade e permitir o desenvolvimento de aplicativos mais poderosos e complexos.

Além disso, a OpenAI tem um papel importante na pesquisa e avanço da IA. Seus modelos de linguagem estão entre os mais avançados do mundo e têm impulsionado o desenvolvimento de novas abordagens e aplicações na área. A empresa também tem um compromisso com a ética e a segurança da IA, buscando garantir que a tecnologia seja utilizada de maneira responsável e para o bem da sociedade.

Para este artigo, iremos usar o modelo 'text-davinci-003', um dos mais eficazes e complexos, com uma forte linguagem bem natural, porém também é um dos modelos mais custosos, por isso iremos utilizar o valor gratuito oferecido pela OpenAI na criação da nossa conta. Crie a sua em: https://platform.openai.com/.

Se comunicar com a OpenAI é muito simples, utilizamos uma request disponibilizada pela plataforma e enviamos o atributo chamado "prompt", correspondente ao input da pergunta que desejamos fazer aos modelos de AI (Artificial Inteligence ou Inteligência Artificial) no corpo dessa request. O resultado é retornado em formato de mensagem string que podemos utilizar nos nossos aplicativos e sites.

O que é Jetpack do Android?

O Jetpack é um conjunto de bibliotecas, ferramentas e diretrizes oferecido pelo Google para facilitar o desenvolvimento de aplicativos Android. Ele visa simplificar tarefas comuns, fornecer melhores práticas e oferecer componentes prontos para uso, permitindo que os desenvolvedores criem aplicativos de alta qualidade de forma mais eficiente. Nesse artigo iremos usar 2 bibliotecas recomendadas pelo JetPack:

Dagger Hilt: O Dagger Hilt é uma estrutura de injeção de dependência para o desenvolvimento de aplicativos Android. A injeção de dependência é um padrão de projeto que permite que as dependências de um objeto sejam fornecidas por meio de uma fonte externa, em vez de serem criadas internamente. Isso ajuda a melhorar a modularidade, testabilidade e reutilização de código, além de facilitar a manutenção e a compreensão do aplicativo.

Compose: O Compose é um framework de interface do usuário e oferece uma abordagem declarativa e reativa para a construção de interfaces de usuário, o que significa que você descreve como a interface deve ser exibida com base no estado atual do aplicativo. Em vez de criar e manipular manualmente as visualizações tradicionais do Android através de layouts XML (como TextViews e ImageViews), você utiliza componentes dinâmico do Compose para definir a aparência e o comportamento da interface.

Criação do App

Faremos um aplicativo que recebe através de um campo de texto uma pergunta escrita pelo usuário e disponibiliza um botão que o usuário pode clicar para obter a resposta da sua pergunta. A pergunta será feita através de uma requisição com "prompt" para o modelo "text-davinci-003" da OpenAI que retornará a resposta que mostraremos em tela para o usuário. Para fazer este app vamos usar Compose, Dagger Hilt e Retrofit para se comunicar com a OpenAI.

Etapa 1: Crie sua conta na plataforma OpenAI e prossiga no caminho "Personal > View API Keys > Create new secret key > Create secret key" para criar sua chave de acesso. É muito importante copiar e colar o valor que vai aparecer, será sua única chance de fazer isso, mas caso se esqueça, apague a chave e crie uma nova.

Etapa 2: Crie um novo projeto Android e certifique-se que possui as seguintes dependências no build.gradle a nível de app:

// Dependências do Android Compose

implementation 'androidx.compose.ui:ui:1.4.3'

implementation 'androidx.compose.material:material:1.4.3'

implementation 'androidx.compose.runtime:runtime:1.4.3'

implementation 'androidx.compose.ui:ui-tooling:1.4.3'

implementation 'androidx.compose.foundation:foundation:1.4.3'

implementation 'androidx.compose.material:material-icons-extended:1.4.3'

implementation 'androidx.activity:activity-compose:1.7.2'

implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'


// Dependências do Retrofit

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'


// Dagger Hilt

implementation "com.google.dagger:hilt-android:2.44"

kapt "com.google.dagger:hilt-compiler:2.44"


Assim como os plugins:

plugins {

   id 'com.android.application'

   id 'org.jetbrains.kotlin.android'

   id 'kotlin-kapt'

   id 'com.google.dagger.hilt.android'

}


E no build.gradle a nível de projeto, garante que possui os plugins:

plugins {

   id 'com.android.application' version '8.0.1' apply false

   id 'com.android.library' version '8.0.1' apply false

   id 'org.jetbrains.kotlin.android' version '1.8.20' apply false

   id 'com.google.dagger.hilt.android' version '2.44' apply false

}


Etapa 3: Para criar a estrutura do Dagger Hilt, primeiramente vamos criar um custom application, estendendo de Application chamado OpenAIApplication e adicionar a notação @HiltAndroidApp:

@HiltAndroidApp

class OpenAIApplication: Application()


Então no AndroidManifest.xml, devemos configurar o <application …>...</application> com o nome da nossa classe de custom application. Vamos aproveitar e já adicionar também a permissão para conexão com a Internet, visto que será necessário para a comunicação com o servidor do OpenAI.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

   xmlns:tools="http://schemas.android.com/tools">


   <uses-permission android:name="android.permission.INTERNET"/>


   <application

       android:name=".OpenAIApplication"

               ...>

         ...

       </application>

</manifest>


Etapa 4: Para criar a camada de comunicação com o servidor da OpenAI, primeiro vamos criar 2 modelos de dado, iniciando pelo dado de requisição para a OpenAI:

data class CompletionRequest(

   @SerializedName("model")

   val model: String,

   @SerializedName("prompt")

   val prompt: String,

   @SerializedName("max_tokens")

   val maxTokens: Int,

   @SerializedName("temperature")

   var temperature: Float,

   @SerializedName("top_p")

   val topP: Float,

   @SerializedName("frequency_penalty")

   val frequencyPenalty: Int,

   @SerializedName("presence_penalty")

   val presencePenalty: Int,

   @SerializedName("stop")

   val stop: String

)


O atributo prompt será o valor que enviaremos como input da pergunta que o usuário irá fazer a AI. O atributo model corresponde ao modelo de AI (Artificial Intelligence) que desejamos usar. Os demais atributos são opcionais e podem ser utilizados para fazer requisições mais detalhadas e complexas.

Precisamos também modelar o objeto da resposta que iremos receber da request para a OpenAI, por isso iremos criar o data class CompletionResponse, contendo os atributos mostrados na documentação do OpenAI.

data class CompletionResponse(

   @SerializedName("choices")

   val choices: List<Choice>,


   @SerializedName("created")

   val created: Int,


   @SerializedName("id")

   val id: String,


   @SerializedName("model")

   val model: String,


   @SerializedName("object")

   val `object`: String,

) {

   data class Choice(

       val text: String,

       val finishReason: String,

       val index: Int

   )

}


O sub objeto Choice e seu atributo text será, de fato, a resposta dada pelo modelo de AI para a pergunta feita através do prompt. Perceba que a OpenAI retorna uma lista com várias respostas, mas iremos utilizar sempre a primeira para fins de estudo.

Agora vamos criar uma interface OpenAiApi para criar a requisição com método POST:

interface OpenAiApi {

   companion object {

       const val BASE_URL = "https://api.openai.com/v1/"

       const val API_KEY = "..."

   }


   @POST("completions")

   suspend fun getMathAnswer(

       @Body completionRequest: CompletionRequest

   ): CompletionResponse

}


Observação: Não é seguro ter sua Api Key dentro do código explicitamente, mas faremos assim, pois este tópico não é o foco deste artigo.

Agora criamos um objeto AppModule para ser o Singleton que constrói o grafo de dependências. Para isso usamos a notação @Module e @InstallIn(SingletonComponent::class). E dentro deste módulo vamos providenciar o @Provide do serviço Retrofit que implementa a nossa interface OpenAiApi.

@Module

@InstallIn(SingletonComponent::class)

object AppModule {


   @Provides

   @Singleton

   fun provideApiService(): OpenAiApi {

       val retrofit = Retrofit.Builder()

           .baseUrl(OpenAiApi.BASE_URL)

           .client(

               OkHttpClient.Builder()

                   .addInterceptor { chain ->

                       val newRequest = chain.request().newBuilder()

                           .addHeader("Authorization", "Bearer ${OpenAiApi.API_KEY}")

                           .build()

                       chain.proceed(newRequest)

                   }

                   .build()

           )

           .addConverterFactory(GsonConverterFactory.create())

           .build()


       return retrofit.create(OpenAiApi::class.java)

   }

}


Etapa 5: Para completar nossa camada de comunicação com a API da OpenAI, vamos criar nosso Data Source (fonte de dados) e nosso Repository (repositório que dispõe o dado a partir de alguma fonte).

Iniciaremos pelo OpenAiDataSource que precisa injetar a dependência OpenAiApi em seu construtor para que possamos realizar a requisição ao servidor da OpenAI, para isso utilizaremos a notação @Inject.

class OpenAiDataSource @Inject constructor(

   private val openAiApi: OpenAiApi

) {

   suspend fun getMathAnswer(equation: CompletionRequest): CompletionResponse {

       return openAiApi.getMathAnswer(equation)

   }

}

Agora faremos o OpenAiRepository que precisa injetar a dependência do OpenAiDataSource utilizando a mesma notação @Inject no construtor. A camada Repository é uma abstração da camada de comunicação com a fonte de dados, o resto do app poderá requisitar do Repository a informação que for necessária, sem se preocupar com qual a fonte de dados. Aqui no Repository poderíamos ter além do OpenAiDataSource, outra fonte de dados para nossa requisição, como por exemplo um banco local.

class OpenAiRepository @Inject constructor(

   private val dataSource: OpenAiDataSource

) {

   suspend fun getMathAnswer(equation: CompletionRequest): CompletionResponse {

       return dataSource.getMathAnswer(equation)

   }

}

Etapa 6: Faremos agora a última camada de lógica antes da camada de interface do usuário, vamos preparar os dados consumidos do Repository para serem acessados pela interface do usuário através de uma entidade do tipo ViewModel, e para isso, usaremos a notação @HiltViewModel. Agora, para injetar o Repository, iremos usar a notação @Inject.

@HiltViewModel

class MainViewModel @Inject constructor(

   private val openAiRepository: OpenAiRepository,

): ViewModel() {


   private var _answer = mutableStateOf<CompletionResponse?>(null)

   var answer: State<CompletionResponse?> = _answer


   fun getMathAnswer(prompt: CompletionRequest) {

       viewModelScope.launch {

           _answer.value = openAiRepository.getMathAnswer(prompt)

       }

   }

}

Neste ViewModel, teremos o Mutable State privado _answer que corresponde ao valor que recebe o resultado da requisição feita através do openAiRepository.getMathAnswer(prompt). O State público answer observará o resultado de _answer. Fazemos esse wrap para impossibilitar a View de alterar o valor de _answer, pois a View deve apenas reagir aos resultados e não interferir neles.

Etapa 7: Finalmente, vamos criar nossa View alterando o MainActivity e preparando ele para receber uma injeção do MainViewModel. Para isso, usaremos a notação @AndroidEntryPoint e a injeção by viewModels() para adquirir uma instância do MainViewModel.

@AndroidEntryPoint

class MainActivity : ComponentActivity() {

   private val viewModel: MainViewModel by viewModels()


   override fun onCreate(savedInstanceState: Bundle?) {

       super.onCreate(savedInstanceState)

       setContent {

           MyApp(viewModel)

       }

   }

}


Agora no setContent dentro de onCreate temos o MyApp, que é uma função @Composable que define os componentes visuais para este app. Para construir a interface como descrevemos inicialmente, vamos utilizar um TextField para campo de texto, Button para permitir o clique do usuário e Text para mostrar o resultado final.

No meio disto usaremos também Surface como base de todo o componente, uma Column para definir o layout vertical dos componentes e Spacer para adicionar espaçamento entre os componentes.

@Composable

fun MyApp(viewModel: MainViewModel) {

   var inputText by remember { mutableStateOf("") }

   val result by viewModel.answer


   Surface(color = MaterialTheme.colors.background) {

       Column(

           modifier = Modifier.fillMaxSize(),

           verticalArrangement = Arrangement.Center,

           horizontalAlignment = Alignment.CenterHorizontally

       ) {

           TextField(

               value = inputText,

               onValueChange = { inputText = it },

               label = { Text("Ask something") }

           )

           Spacer(modifier = Modifier.height(16.dp))

           Button(onClick = {

               if (inputText.isNotBlank()) {

                   viewModel.getMathAnswer(

                       CompletionRequest(

                           "text-davinci-003", inputText, 150,

                           0f, 1f, 0, 0, ""

                       )

                   )

               }

           }) {

               Text("Get Answer")

           }

           Spacer(modifier = Modifier.height(16.dp))

           result?.choices?.get(0)?.let { Text(text = it.text) }

       }

   }

}


Como parâmetro da função MyApp, adicionaremos o MainViewModel que será usado no atributo result para obter o State answer que uma vez atualizado, irá atualizar por consequência o atributo result da View, que por sua vez mostra o texto (text) da primeira escolha (choices?.get(0)) do resultado da requisição.

Resultado

Escreva algo no campo de texto e aperte no botão, aguarde por um breve momento e, por fim, obtenha a resposta processada por inteligência artificial!

Conclusão

Usar a OpenAI é tão simples quanto fazer consulta a qualquer API remota. Com este artigo, você aprendeu como utilizar um dos modelos mais robustos disponíveis na OpenAI, com os mais modernos recursos disponíveis para Android (Dagger Hilt e Compose).

Use essa combinação poderosa para construir aplicativos utilizando inteligência artificial e surpreender seus clientes ou gestores! Salve essa página nos seus favoritos e volte sempre que precisar consultar uma informação importante sobre este tópico.

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.