OpenAI em um ESP32

Apr 20 2023
O chatbot da OpenAI conquistou o mundo. Finalmente, uma IA que (às vezes) realmente parece inteligente.

O chatbot da OpenAI conquistou o mundo. Finalmente, uma IA que (às vezes) realmente parece inteligente. Dada a API relativamente barata, me perguntei se faria sentido executar o OpenAI em um ESP32. É possível executar um chatbot em um microcontrolador? Há vantagens em fazê-lo? Poderíamos usar o OpenAI para tornar nossos dispositivos mais inteligentes?

Talvez falar com eles como falaríamos com um humano. Por exemplo, “Desligue o umidificador se a umidade exceder 70%.”

Depois de alguns dias hackeando, posso responder a todas essas perguntas com um retumbante “sim”, contanto que você substitua “falar” por “escrever”. Embora tecnicamente viável (especialmente em ESP32s com SPIRAM extra), ainda não implementei o reconhecimento de voz em um ESP32. Talvez para um projeto futuro.

Sejamos claros: na maioria das vezes, há pouca necessidade de executar um cliente GPT em um ESP32. Ter uma chave OpenAI no dispositivo é um risco de segurança para produtos comerciais, e ter um servidor extra que executa as solicitações OpenAI não é grande coisa.

Isso muda para pequenos projetos pessoais. Não precisar executar um servidor (mesmo que seja uma máquina de desktop pessoal) tem várias vantagens:

  • A configuração é mais fácil
  • É mais barato
  • Consome apenas uma quantidade insignificante de energia

Interagir com OpenAI não é difícil. Sua API é bem documentada por meio de uma especificação OpenAPI e, portanto, acessível por meio de solicitações HTTP simples.

Inscrever-se e obter uma chave de API também é trivial.

Do lado do ESP32, está o openaipacote que implementa a funcionalidade que precisamos. É um wrapper simples em torno da API OpenAI e implementa conclusões (chat).

Bater papo

O bate-papo é o caso de uso mais óbvio para o OpenAI. Embora relativamente simples de implementar, pode-se obter uma vantagem inicial usando o chatbotpacote .

A única tarefa restante é então se comunicar com o usuário. Isso pode ser feito usando serviços de bate-papo comuns. O pacote chatbot possui exemplos de integrações com Telegram e Discord .

Um exemplo simples de HTTP também mostra que não é necessário passar por um serviço de bate-papo para usar a funcionalidade de bate-papo do OpenAI.

IU do bot de bate-papo. (HTML gerado por OpenAI).

Tornando os Dispositivos Inteligentes

Um caso de uso mais fascinante para o OpenAI é fornecer inteligência aos nossos dispositivos para que eles entendam os comandos humanos.

O device_botpacote simplifica a criação de tais dispositivos. Como usuário, é preciso especificar a funcionalidade do dispositivo. A biblioteca cuida do resto.

Em nosso exemplo, temos um dispositivo com três periféricos:

  • Dois LEDs (um verde no pino 23 e um vermelho no pino 22)
  • Um sensor de temperatura/umidade (um DHT11 no pino 32)
  • green_led(<true|false>)
  • red_led(<true|false>)
  • temperature()
  • humidity()

jag pkg init
jag pkg install toit-dhtxx
jag pkg install device_bot
jag pkg install telegram

import telegram
import device_bot show *
import gpio
import dhtxx.dht11

LED_GREEN_PIN ::= 23
LED_RED_PIN ::= 22

class Leds:
  pin_green_/gpio.Pin
  pin_red_/gpio.Pin

  constructor:
    pin_green_ = gpio.Pin LED_GREEN_PIN --output
    pin_red_ = gpio.Pin LED_RED_PIN --output

  functions -> List:
    return [
      Function
          --syntax="green_led(<true|false>)"
          --description="Turns the green LED on or off."
          --action=:: | args/List |
            pin_green_.set (args[0] ? 1 : 0),
      Function
          --syntax="red_led(<true|false>)"
          --description="Turns the red LED on or off."
          --action=:: | args/List |
            pin_red_.set (args[0] ? 1 : 0),
    ]

A Functionclasse é usada para declarar a funcionalidade do dispositivo. Leva três argumentos nomeados, syntax, descriptione action. Os dois primeiros fornecem mais informações sobre a função, enquanto o action argumento é o lambda chamado quando a função é invocada.

Ambos syntaxe descriptionsão enviados para OpenAI. Quanto mais intuitivos forem, maior a probabilidade de o OpenAI entender a função.

O syntaxé adicionalmente usado pelo device_botpacote para inferir o número de argumentos.

O actionargumento é o lambda que é chamado quando a função é invocada. Ele recebe uma lista de argumentos e retorna seu resultado. Não há nada para retornar para as funções do LED, então não nos preocupamos com isso.

No caso do DHT11, temos que retornar a temperatura e a umidade. Aqui está o que parece:

DHT11_PIN ::= 32

class Dht11Sensor:
  data_/gpio.Pin
  sensor_/dht11.Dht11

  constructor:
    data_ = gpio.Pin DHT11_PIN
    sensor_ = dht11.Dht11 data_

  close:
    data_.close

  functions -> List:
    return [
      Function
          --syntax="temperature()"
          --description="Reads the temperature in C as a float"
          --action=:: sensor_.read_temperature,
      Function
          --syntax="humidity()"
          --description="Reads the humidity in % as a float"
          --action=:: sensor_.read_humidity,
    ]

main --openai_key/string --telegram_token/string:
  leds := Leds
  dht11_sensor := Dht11Sensor

  // Connect to Telegram
  telegram_client := telegram.Client --token=telegram_token

  // Keep track of the last chat-id we've seen.
  // A more sophisticated bot would need to make sure that
  // only authenticated users can manipulate the device.
  chat_id/int? := null

  // Give the device a way to send messages to us.
  functions := [
    Function
      --syntax="print(<message>)"
      --description="Print a message"
      --action=:: | args/List |
        message := args[0]
        telegram_client.send_message --chat_id=chat_id "$message"
  ]
  functions.add_all leds.functions
  functions.add_all dht11_sensor.functions

  // Create a device bot.
  device_bot := DeviceBot --openai_key=openai_key functions

  // Start listening to new messages and interpret them.
  telegram_client.listen: | update/telegram.Update |
    if update is telegram.UpdateMessage:
      print "Got message: $update"
      message/telegram.Message? := (update as telegram.UpdateMessage).message
      if message.text == "/start":
        continue.listen

      chat_id = message.chat.id
      device_bot.handle_message message.text --when_started=::
        telegram_client.send_message --chat_id=chat_id "Running"

sob o capô

Arquitetura

O dispositivo que estamos usando é uma placa ESP32 padrão. O chip em si é muito barato e pode ser comprado por cerca de 1 USD. Uma prancha completa, como visto no vídeo, não é muito mais cara.

No entanto, ao contrário dos computadores reais, os ESP32s têm algumas restrições. Embora ainda sejam bastante rápidos (dois núcleos rodando a 240 MHz), eles têm muito pouca memória. O ESP32 que estamos utilizando possui apenas 520 kB de RAM, dos quais 320 kB estão disponíveis para o aplicativo. Isso não é muito. O computador em que estou escrevendo tem 64 GB de RAM, um fator de mais de 100.000. A memória é, portanto, a principal limitação do ESP32 e deste projeto.

Anexado ao ESP32 está um sensor de temperatura/umidade DHT11 e dois LEDs. O DHT11 é um sensor que pode medir temperatura e umidade. Não é muito bom nisso, mas barato e difundido. Os LEDs são LEDs padrão conectados em série com resistores limitadores de corrente. (Geralmente, um resistor de 330 Ohm é uma boa escolha para LEDs conectados a 3,3V.)

A comunicação com o usuário é feita através do Telegram. Sua
API de bot é uma delícia de usar e é fácil de começar. O bot se comunica com o ESP32 por meio de solicitações HTTP (as mesmas que os navegadores usam para buscar páginas da web). O maior desafio aqui é que a comunicação deve ser feita através de HTTPS (a versão criptografada do HTTP). Uma conexão HTTPS ocupa muita memória, que, como mencionado anteriormente, é escassa no ESP32.

As requisições ao OpenAI também são feitas por meio de requisições HTTPS, muito parecidas com a forma como o ESP32 se comunica com o Telegram.

O OpenAI é usado para converter as solicitações do usuário em código. No entanto, ainda precisamos entender o código retornado. Um simples interpretador faz isso. Ele primeiro analisa o código e constrói uma AST (árvore de sintaxe abstrata). Cada nó do AST tem uma evalfunção e o programa é avaliado recursivamente chamando evalo nó raiz do AST. Essa abordagem é ineficiente, mas funciona bem para programas pequenos (é com isso que estamos lidando). É trivial de implementar e não ocupa muito espaço.

A vida de um pedido

Como primeiro passo, o ESP32 se conecta ao Telegram e escuta novas mensagens. Quando uma nova mensagem chega, o texto da mensagem é extraído e passado para a DeviceBotclasse.

A DeviceBotclasse então constrói uma conversa falsa com esta mensagem. Ele prefixa a mensagem de solicitação com algum contexto, possibilitando que o chatbot OpenAI responda razoavelmente. No momento da escrita, as mensagens de contexto são as seguintes:

mensagem de descrição

Given a simplified C-like programming language with the following builtin functions:
- print(<message>).
- sleep(<ms>).
- to_int(<number or string>). Converts the given number or string to an integer.
- random(<min>, <max>).
- now(): Returns the ms since the epoch.
- List functions: 'list_create()', 'list_add(<list>, <value>)', 'list_get', 'list_set', 'list_size'.
- Map function: 'map_create()', 'map_set(<map>, <key>, <value>)', 'map_get', 'map_contains', 'map_keys', 'map_values', 'map_size'.
    'map_get' throws an error if the key is not in the map.
    Keys can be integers or strings.

Example:
```
// Create a map from numbers to their squares.
let map = map_create();
let i = 0;
let sum = 0;
while (i < 10) {
    map_set(map, i, i * i);
    sum = sum + i * i;
    i = i + 1;
}
// Print the map.
print(map);
// Print the square of 5.
print(map_get(map, 5));

let keys = map_keys(map);
// Print the element in the middle of the key list.
print(list_get(keys, to_int(list_size(keys) / 2)));
// Print the average of the squares.
print(sum / list_size(keys));
```

This language is *not* Javascript. It has no objects (not even 'Math') or self-defined functions. No 'const'.

The language furthermore has the following functions:
<FUNCTIONS>
Under no circumstances use any function that is not on the builtin list or this list!

Write a program that implements the functionality below (after '===').
Only respond with the program. Don't add any instructions or explanations.
====
<REQUEST>

Essas mensagens são enviadas ao chatbot da OpenAI, que responde com um programa. Por exemplo, a solicitação “Pisque o LED verde a 1 Hz por 5 segundos” pode gerar a seguinte resposta:

// Blink the green LED at 1Hz for 5 seconds.
let start = now();
while (now() - start < 5000) {
  green_led(true);
  sleep(500);
  green_led(false);
  sleep(500);
}

Cada nó neste AST possui um evalmétodo que executa a operação correspondente. Por exemplo, o Programnó avalia a statementslista e o Whilenó avalia os nós conditione body.

Como exemplo, aqui está o código para o Ifnó:

eval scope/List [brek] [cont]:
  if condition.eval scope:
    return then.eval scope brek cont
  else if els:
    return els.eval scope brek cont
  return null

O programa é avaliado até que seja encerrado, ocorra um erro ou seja recebida uma nova solicitação.

Conclusão

Este artigo deu uma boa ideia do que é possível com o OpenAI em um ESP32. Se quiser experimentar por conta própria, você pode encontrar o código no GitHub .

Apesar do seu tamanho relativamente pequeno, este projeto combinou muita diversão e áreas interessantes: compiladores/intérpretes, hardware, chatbots e IA.

Espero que tenham gostado de ler sobre isso tanto quanto eu gostei de escrevê-lo.