Agora que conhecemos o projeto e exploramos o código-fonte, partimos para o trabalho.
Integrando com a API
Na página do navegador no endereço localhost:8080, a página do nosso chatbot.
Conforme evidenciado no vídeo anterior, esse chatbot não está funcionando corretamente, pois responde essencialmente com a mesma pergunta enviada.
É neste ponto que começaremos a fazer as modificações. A proposta é que, ao receber uma pergunta, pegaremos o texto digitado e enviaremos uma requisição à API da OpenAI para obter uma resposta via inteligência artificial. Essa etapa de integração assemelha-se ao que foi abordado no curso anterior.
Como mencionamos, é uma aplicação web, e essa parte do front-end já está implementada, não vamos modificar, não é o foco deste curso. Mas, basicamente, quando enviamos uma mensagem nesse formulário e clicamos no botão ou teclamos um "Enter", uma requisição do tipo post (postagem) é disparada para a URL /chat.
No código, estamos usando o Spring, seguindo o padrão MVC, e terá uma classe Controller que está recebendo essa requisição. É lá que vamos começar a analisar e verificar o que está acontecendo para fazer as adaptações.
No IntelliJ, vamos analisar o projeto à esquerda, no "src > main > java", no pacote br.com.alura.ecomart.chatbot, tem um pacote chamado web e dentro dele um subpacote chamado controller, e lá dentro está a classe ChatController.
- web
- br.com.alura.ecomart.chatbot
Todos esses arquivos já foram preparados para você no ambiente, é o nosso projeto inicial.
Numa classe Controller do Spring, que recebe requisições do navegador, faz o processamento e devolve uma resposta.
Quando abrimos a página no navegador, localhost:8080, / ou /chat, ela cai no método CarregarPaginaChatBot(), que basicamente navega para a página chat.html, que está usando o Thymeleaf (tecnologia para fazer o desenho da página HTML do front-end).
ChatController.java
private static final String PAGINA_CHAT = "chat";
@GetMapping
public String carregarPaginaChatbot() {
return PAGINA_CHAT;
}
// código omitido
Mas o que interessa para nós é o segundo método chamado ResponderPergunta(). Esse é o método que é chamado quando a pessoa usuária envia uma pergunta na página.
ChatController.java
@PostMapping
@ResponseBody
public String responderPergunta(@RequestBody PerguntaDto dto) {
return dto.pergunta();
}
// código omitido
A pergunta chega como parâmetro nesse objeto do tipo PerguntaDTO, que é só um record do Java, está seguindo aquele padrão DTO (Data Transfer Object), para encapsular as informações que estão chegando da página, que no nosso caso é só uma informação que é a pergunta: o texto que a pessoa digitou.
E observem que esse método está fazendo no Controller no momento. A única coisa que ele faz é return dto.pergunta. Por isso que ele não está funcionando como esperado. Ele está simplesmente devolvendo a própria pergunta que foi enviada.
Neste ponto, precisamos efetuar uma mudança. Não buscamos apenas devolver a pergunta; desejamos extrair o texto dessa pergunta, enviar uma requisição à API da OpenAI, solicitar o chat completion (um recurso), gerar uma resposta, capturar essa resposta e exibi-la na página.
No entanto, a chamada da API não será incluída na classe Controller. Criaremos uma classe Service, seguindo o padrão de projeto Service, na qual isolaremos a lógica de disparar a requisição.
Utilizar uma classe Service é uma prática recomendada para evitar a inserção de lógicas, validações ou algoritmos dentro de uma classe Controller.
Dentro do pacote Domain, já tem um pacote chamado Service, que possui uma classe chamada CalculadoraDeFrete, que analisaremos mais adiante no curso. No pacote Service, criamos uma nova classe.
Com o pacote selecionado, teclamos "Alt + Insert" e escolhemos a opção Class, e chamamos de ChatbotService.
ChatbotService
package br.com.alura.ecomart.chatbot.domain.service;
public class ChatbotService {
}
Essa será a classe Service que vai encapsular essas lógicas relacionadas com o chatbot. Acima da classe, deve ter a anotação @Service, porque desejamos injetar essa classe no Controller e o Spring precisa conhecer essa classe. Portanto, tem que ter alguma anotação, neste caso, será a anotação Service.
Na classe ChatbotService, criamos o método que chama a classe que faz a requisição para API da OpenAI. Criamos um método public que devolve uma String.
Essa String que vai ser devolvida é a resposta que a API nos devolveu. O nome do método será responderPergunta(), que receberá como parâmetro a String com a pergunta. O Controller vai chamar essa classe e ela é que chama quem sabe disparar a requisição.
ChatbotService
package br.com.alura.ecomart.chatbot.domain.service;
import org.sparingframework.stereotype.Service;
@Service
public class ChatbotService {
public String responderPergunta(String pergunta) {
}
}
No entanto, não será na classe Service que terá o código de infraestrutura, todo aquele código que usa aquela biblioteca Java para disparar a requisição. No vídeo anterior, mostramos que já tem uma classe no projeto que dispara a requisição.
No pacote infra.openai, tem uma classe chamada OpenAIClient. Nela, temos o código que dispara a requisição. E o método é esse, chamado enviarRequisicaoChatCompletion().
Para chamarmos a API da OpenAI, temos que pegar essa classe OpenAIClient e chamar esse método. Isso é o que desejamos fazer na classe Service.
Na classe ChatBotService, declaramos um atributo private do tipo OpenAIClient denominado client. É necessário informar ao Spring que ele injetará esse atributo na classe. Existem várias maneiras de fazer isso, uma delas é através da declaração de um construtor.
Assim, abaixo do atributo, implementamos um construtor. Para isso, teclamos "Alt + Insert" escolhemos a opção "construtor" e teclamos "Enter".
ChatbotService
package br.com.alura.ecomart.chatbot.domain.service;
import br.com.alura.ecomart.chatbot.infra.openai.OpenAIClient;
import org.sparingframework.stereotype.Service;
@Service
public class ChatbotService {
private OpenAIClient client;
public ChatbotService(OpenAIClient client) {
this.client = client;
}
public String responderPergunta(String pergunta) {
}
}
Dessa forma, implementamos um construtor que recebe esse client, e o Spring reconhece que, ao executar a service, é necessário fornecer o client, pois ele saberá instanciar o client devido à classe client possuir a anotação @Component.
O que o método responderPergunta() fará? Delegará, return client.enviarRequisicaoChatCompletion();. Assim, a nossa Service está invocando o método da classe client. Contudo, esse método requer um parâmetro, mas não podemos passar a pergunta diretamente, pois esse método não aceita a String com a pergunta.
ChatbotService
package br.com.alura.ecomart.chatbot.domain.service;
import br.com.alura.ecomart.chatbot.infra.openai.OpenAIClient;
import org.sparingframework.stereotype.Service;
@Service
public class ChatbotService {
private OpenAIClient client;
public ChatbotService(OpenAIClient client) {
this.client = client;
}
public String responderPergunta(String pergunta) {
return client.enviarRequisicaoChatCompletion(pergunta);
}
}
Ao analisarmos a classe OpenAIClient, observamos que o método enviarRequisicaoChatCompletion() recebe um objeto do tipo DadosRequisicaoChatCompletion.
Este é outro record presente no Java, contendo o prompt do sistema e o prompt do usuário. É uma classe que encapsula esses dois prompts.
Portanto, na classe Service, não podemos passar a pergunta como parâmetro, temos que passar os dados. No entanto, essa variável não existe, vamos criá-la. Teclamos "Alt + Enter" e escolhemos a opção "Create local variable 'dados'".
Trocaremos a variável DadosRequisicaoChatCompletion para utilizar a palavra var. Assim, na linha acima, temos var dados, e então definimos dados como new DadosRequisicaoChatCompletion.
ChatbotService
public String responderPergunta(String pergunta) {
var dados = new DadosRequisicaoChatCompletion();
return client.enviarRequisicaoChatCompletion(dados);
}
}
Ao instanciar esse objeto, DadosRequisicaoChatCompletion(), são necessários dois parâmetros, ambos strings. O primeiro é a string contendo o prompt do sistema, e o segundo é a string contendo o prompt da pessoa usuária.
O prompt do sistema será atribuído a uma variável chamada promptSistema, que ainda não foi criada. Quanto ao prompt da pessoa usuária, este será a própria pergunta digitada na tela, utilizada como o prompt da pessoa usuária.
Como está ocorrendo um erro devido à ausência da variável promptSistema, na linha anterior, declaramos var promptSistemae o igualamos aoprompt já copiado, apenas colando com "Ctrl + V".
ChatbotService
public String responderPergunta(String pergunta) {
var promptSistema = "Você é um chatbot de atendimento a clientes de um ecommerce e deve responder apenas perguntas relacionadas com o ecommerce";
var dados = new DadosRequisicaoChatCompletion(promptSistema, pergunta);
return client.enviarRequisicaoChatCompletion(dados);
}
}
Esse é o prompt do sistema. Salvamos, e agora está pronta a nossa service, ela já sabe chamar a classe Client que dispara a requisição.
Agora, na classe ChatController.java , no método responderPergunta(), é necessário devolver o retorno do método responderPergunta() da classe Service. No ChatController, utilizaremos a classe Service.
Desta forma, declaramos um atributo private chamado ChatbotService do tipo service na classe Controller. Para isso, é necessário que o Spring faça a injeção, então declaramos um construtor no Controller que recebe a Service como parâmetro, permitindo que o Spring realize a injeção automaticamente. Para isso, usamos o "Alt + Insert" e optamos por "construtor".
ChatController.java
private ChatbotService service;
public ChatController(ChatbotService service) {
this.service = service;
}
// código omitido
No método responderPergunta(), a resposta agora não será do tipo dto.Pergunta; será do tipo service.responderPergunta(). Passamos como parâmetro o dto.pergunta(), que contém a pergunta digitada pela pessoa usuária na página.
ChatController.java
@PostMapping
@ResponseBody
public String responderPergunta(@RequestBody PerguntaDto dto) {
return service.responderPergunta(dto.pergunta());
}
// código omitido
Assim, o Controller chama a Service, e está encapsula a chamada para a classe Client, responsável por disparar a requisição para a API.
A princípio, está tudo certo e podemos testar.
Entraremos novamente no localhost:8080/chat, ou apenas localhost:8080 também carregará a página. Digitaremos uma pergunta, por exemplo, "Bom dia, tudo bem?". Observaremos a resposta ao pressionar "Enter".
Nesse processo, inicialmente, o Controller invoca a Service, que por sua vez aciona o Client. O Client então envia a requisição para a API, a qual retorna uma resposta que o Client recebe, repassa para a Service, esta retorna ao Controller, e o Controller finalmente entrega à página.
Obtemos como resposta:
Bom dia! Estou bem, obrigado por perguntar. Como posso ajudá-lo hoje com relação ao nosso ecommerce? Precisa de informações sobre produtos, entregas, pagamentos ou tem alguma outra questão?
Há todo esse fluxo, e ele ocorreu perfeitamente. Observem que ele capturou o prompt do sistema corretamente, reconhecendo que se trata de um assistente de um e-commerce.
Entretanto, talvez tenham percebido que a resposta foi exibida de uma vez, sem ser apresentada palavra por palavra ou token por token. Se já tiverem utilizado o chat GPT, notaram que, ao fazer uma pergunta, a resposta não é exibida integralmente de imediato; ao contrário, o texto é gerado gradualmente, com os tokens aparecendo progressivamente na tela.
Conclusão
Podemos implementar essa abordagem neste projeto também, tornando-o mais semelhante a um chat onde as respostas são geradas conforme a pessoa digita, assemelhando-se ao chat GPT. A seguir, aprenderemos como fazer isso.