[🦀🤖][Tutorial] Assistente virtual de terminal em Rust

Why?

Temos dois motivos: O primeiro é que esse é parte dos meus estudo para implementação do bot para criação e correção de códigos maliciosos, dá uma olhada aqui

A segunda surgiu porque tenho TDAH e volta e meia acabo esquecendo alguns comandos específicos que preciso usar no terminal, além de precisar fazer algumas pesquisas, o que seria muito legal de ser feito sem precisar sair do terminal.

Um script em python seria muito fácil e eu gosto de um pouco de desafio, além de ver que algumas pessoas aqui já perguntaram e comentaram sobre Rust, que tal mostrar o poder de rust então para gerar o nosso assistente virtual?

Iniciando o projeto

Este tutorial leva em cosideração que você tenha um conhecimento mínimo de rust e do gerenciador de projetos cargo!

Precisamos iniciar um novo projeto para começarmos:

cargo new assistente_virtual --bin

Iniciamos um projeto de rust, assistente virtual é um nome qualquer, eu por exemplo escolhi o nome Robert para meu assistente virtual.

--bin

O --bin é para indicar que queremos um projeto binário, ou seja, um projeto que será executado no terminal.

Entre na pasta do projeto e abra o Cargo.toml, ele é o arquivo que contém as dependências do projeto, vamos adicionar as dependências que vamos usar:

[dependencies]
hyper = { version = "^0.14", features = ["full"]}
hyper-tls = "^0.5"
tokio = {version = "1", features = ["full"]}
serde = "^1"
serde_derive = "^1"
serde_json = "^1"

A primeira dependência é o hyper, que é um framework para construir servidores http, vamos usar ele para criar um servidor http que vai receber as requisições do nosso assistente virtual.

A segunda dependência é o hyper-tls, que é um wrapper para o hyper que vai nos permitir usar https.

A terceira dependência é o tokio, que é um framework para construir aplicações assíncronas, vamos usar ele para criar um servidor http assíncrono.

A quarta dependência é o serde, que é um framework para serialização e desserialização de dados, vamos usar ele para serializar e desserializar os dados que vamos receber e enviar.

Após adicionar todas as dependências vamos começar o desenvolvimento do nosso assistente virtual.

Abra o arquivo main.rs e vamos começar a escrever o código:

use hyper::body::Buf;
use hyper::{header, Body, Client, Request};
use hyper_tls::HttpsConnector;
use serde_derive::{Deserialize, Serialize};
use std::io::{stdin, stdout, Write};

Não entratei em muitos detalhes para não ser um tutorial maçante mas basicamente nós importamos todas as dependências que vamos usar.

O nosso projeto se baseia em usar o GPT-3 para nos gerar repostas para nossas perguntas, a API do GPT-3 é uma API REST, então vamos usar o hyper para criar um servidor http que vai receber as requisições e enviar as respostas.

Precisamos criar os structs que são responsáveis por serializar e desserializar os dados que vamos receber e enviar. A serialização é o processo de converter dados em uma forma que possa ser armazenada ou transmitida de uma maneira eficiente.

#[derive(Deserialize, Debug)]
struct OAIChoises {
 text: String,
}

O struct OAIChoises é responsável por desserializar os dados que vamos receber da API do GPT-3, ele é responsável por desserializar o campo text que é o campo que contém a resposta que o GPT-3 gerou para a nossa pergunta.

#[derive(Deserialize, Debug)]
struct OAIResponse {
 choices: Vec<OAIChoises>,
}

O struct OAIResponse é responsável por desserializar os dados que vamos receber da API do GPT-3, existem outros campos que são retornados na requisição mas não vamos usar eles, então optei por não usar pois o compilador iria ficar brigando com a gente por criar um campo que não usamos

#[derive(Serialize, Debug)]
struct OAIRequest {
 prompt: String,
 model: String,
 max_tokens: u32,
 temperature: u32,
}

O struct OAIRequest é responsável por serializar os dados que vamos enviar para a API do GPT-3, ele é responsável por serializar os campos prompt, model, max_tokens e temperature. Que representam, respectivamente, a pergunta que vamos fazer, o modelo que vamos usar, a quantidade máxima de tokens que o GPT-3 vai gerar e a temperatura que o GPT-3 vai usar para gerar a resposta. PS: Eu não cheguei a buscar mais informaçõe sobre essa tal "temperatura" mas acredito que seja algo relacionado a assertividade do texto.

As próximas partes serão um pouco mais complicadas, colocarei snippets do código e o código final para caso você se perca.

Vamos iniciar o nosso main

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {

}

Nosso método main será um método async, para isso usamos o tokio::main, que é um macro que transforma o método main em um método async.

let https = HttpsConnector::new();
 let client = Client::builder().build(https);
 let uri = "https://api.openai.com/v1/completions";
 
 let preamble = "Eu sou um robô de pesquisa";

 let oai_token: String = String::from("SEU-TOKEN-AQUI");
 let auth_header_val = format!("Bearer {}", oai_token);

Criamos uma requisição Http e criamos os parâmetros que usaremos na construção da nossa requisição.

O parâmetro preamble é o texto que vem antes da pergunta, ela será usada para que o GPT-3 entenda a melhor abordagem para responder a nossa pergunta, você pode alterar isso para criar um chatbot meio sarcástico, por exemplo.

oai_token e auth_header_val são o token da API do GPT-3 e o valor do header de autenticação que vamos usar na requisição.

println!("{esc}c", esc = 27 as char);
loop {
  print!("> "); // Será usado para representar nosso input
  stdout().flush().unwrap(); // Limpa o buffer do stdout
  let mut user_text = String::new();

  stdin().read_line(&mut user_text).expect("Failed to read line"); // Pega o input do usuário e coloca na variável user_text
  println!("");

  let oai_request = OAIRequest {
   prompt: format!("{}{}", preamble, user_text),
   model: String::from("text-curie-001"),
   max_tokens: 60,
   temperature: 0,
  };

O parâmetro oai_request é o objeto que vamos usar para serializar os dados que vamos enviar para a API do GPT-3, observe as opções que foram selecionadas, o text-curie não é tão poderoo quanto o DaVinci mas é mais rápido e mais barato, então é uma boa opção para testes.

Caso queira brincar um pouco com o DaVinci altera o model para text-davinci-002, ele é realmente mais divertido porém consome bastante dos tokens, tome cuidado!

PS: Usando bastante por 1 dia eu obtive o uso total de 0.4 cents!

let body = Body::from(serde_json::to_vec(&oai_request)?);
  
  let req = Request::post(uri)
   .header(header::CONTENT_TYPE, "application/json")
   .header("Authorization", &auth_header_val)
   .body(body)
   .unwrap();
  
  let res = client.request(req).await?;

  let body = hyper::body::aggregate(res).await?;

  let json: OAIResponse = serde_json::from_reader(body.reader())?;

  // Print the json
  println!("🤖: {}", json.choices[0].text);

Essa parte é a mais complicada, mas vamos por partes. O req é a requisição em si, ele é construído a partir do objeto body e dos parâmetros que criamos anteriormente. Res é a resposta da requisição, ela é construída a partir do objeto req. body é o corpo da resposta, ele é construído a partir do objeto res. json é o objeto que vamos usar para desserializar os dados que recebemos da API do GPT-3, ele é construído a partir do objeto body. E a partir do json nós imprimimos na tela a reposta do bot!

Código inteiro:

use hyper::body::Buf;
use hyper::{header, Body, Client, Request};
use hyper_tls::HttpsConnector;
use serde_derive::{Deserialize, Serialize};
use std::io::{stdin, stdout, Write};

#[derive(Deserialize, Debug)]
struct OAIChoises {
 text: String,
}

#[derive(Deserialize, Debug)]
struct OAIResponse {

 choices: Vec<OAIChoises>,
}

#[derive(Serialize, Debug)]
struct OAIRequest {
 prompt: String,
 model: String,
 max_tokens: u32,
 temperature: u32,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let https = HttpsConnector::new();
 let client = Client::builder().build(https);
 let uri = "https://api.openai.com/v1/completions";
 
 let preamble = "Eu sou um robô de pesquisa";

 let oai_token: String = String::from("SEU-TOKEN-AQUI");
 let auth_header_val = format!("Bearer {}", oai_token);

 println!("{esc}c", esc = 27 as char);
 loop {
  print!("> ");
  stdout().flush().unwrap();
  let mut user_text = String::new();

  stdin().read_line(&mut user_text).expect("Failed to read line");
  println!("");

  let oai_request = OAIRequest {
   prompt: format!("{}{}", preamble, user_text),
   model: String::from("text-curie-001"),
   max_tokens: 60,
   temperature: 0,
  };

  let body = Body::from(serde_json::to_vec(&oai_request)?);
  
  let req = Request::post(uri)
   .header(header::CONTENT_TYPE, "application/json")
   .header("Authorization", &auth_header_val)
   .body(body)
   .unwrap();
  
  let res = client.request(req).await?;

  let body = hyper::body::aggregate(res).await?;

  let json: OAIResponse = serde_json::from_reader(body.reader())?;

  // Print the json
  println!("🤖: {}", json.choices[0].text);
  
 }
}

Buildando e rodando

Para vermos o resultado no terminal basta rodar cargo run no terminal.

> como faço para mudar de diretório no linux?

🤖: Para mudar de diretório no Linux, você precisa utilizar o comando cd (comando de diretório) para ir ao diretório que deseja mudar

> poderia me mostar um exemplo do comando ssh para me conectar a um servidor remoto?

🤖: ssh -L 8888@localhost remotepassword

Show de bola! Agora vamos gerar um binário para usarmos o bot quando precisarmos sem compilar o código toda vez.

cargo build --release

Após compilar observe que foi criada uma pasta chamada target e dentro dela uma pasta chamada release e dentro dela um arquivo chamado *robert_ai.*Nesse caso vai ser o nome do seu projetinho.

Precisamos agora adicionar o arquivo ao PATH!

sudo mv target/release/robert_ai /usr/local/bin
source ~/.bashrc *
ource ~/.zshrc *

O comando source é para atualizar o PATH do seu terminal, caso você esteja usando o bash ou o zsh, use de acordo com o seu terminal.

Após isso você pode rodar o comando robert_ai e ver o bot funcionar! Maravillha o/

Conclusão

Esse foi um tutorial bem simples, mas que mostra como é fácil criar um bot de pesquisa usando a API do GPT-3. O código fonte do projeto não etá disponível no Github ainda porque sou preguiçoso.

O tutorial está propositalmente incompleto, pois a ideia é que vocês pesquisem e aprendam mais sobre o assunto. Alguns desafios para vocês:

  • Colocar a pergunta como parâmetro para o programa Ex: robert_ai "como faço para mudar de diretório no linux?"
  • Adicionar cores no terminal
  • Testar outros modelos disponíveis
  • Criar um bot que responde perguntas de forma mais inteligente
  • Criar um bot que não seja de pesquisa

PS: Fui brincar um pouco de madrugada e se liga nessa resposta:

./robert_ai

> oi?

🤖: Não, sou um humano.

Eu gelei e fui dormir 😂

No código inteiro você sem querer vazou seu token de API da OpenAI!

A chave foi deletada depois que postei o tutorial! Mas obrigado pelo toque, mais um exemplo de por quê devemos usar variáveis de ambiente 😂