Vinícius Boscardin
Posted on July 1, 2024
Vamos explorar como utilizar as tecnologias Go, Gemini e o uso da Alexa, para desenvolver automações úteis para o cotidiano. Utilizando um projeto prático como exemplo, mergulhamos nas possibilidades que essas ferramentas oferecem para automatizar tarefas e processos.
Cenário Hipotético:
Recomendações de Janta para a Alexa Integradas com o Gemini
Contexto
"Bido" deseja usar seu assistente virtual Alexa para receber recomendações de janta personalizadas. Para tornar isso possível, é necessário integrar a Alexa com o sistema de banco de dados, que armazena os cardápios de diferentes restaurantes. O banco de dados é alimentado por serviços independentes que atualizam constantemente as opções de menu.
Descrição do Problema
"Bido" quer que a Alexa forneça recomendações de jantar com base nas opções disponíveis nos restaurantes. Para isso, a Alexa precisa consultar um banco de dados PostgreSQL, onde o sistema armazena os cardápios de diversos restaurantes.
Requisitos
-
Integração com a Alexa:
- Desenvolver uma skill para a Alexa que permita a "Bido" pedir recomendações de jantar.
-
Banco de Dados PostgreSQL:
- Configurar um banco de dados PostgreSQL que armazene os cardápios dos restaurantes.
- Cada entrada no banco de dados deve incluir informações como nome do prato, preço, e restaurante.
-
Serviços de Alimentação de Dados:
- Criar serviços independentes que coletam e inserem dados de cardápios de restaurantes no banco de dados.
- Esses serviços podem são web scrapers.
-
Processo de Recomendação:
- Implementar um algoritmo de recomendação com os dados disponíveis no banco de dados.
Fluxo de Trabalho
-
Coleta de Dados:
- Serviços independentes coletam os cardápios dos restaurantes e os inserem no banco de dados PostgreSQL.
- O banco de dados é atualizado regularmente para garantir a precisão das informações.
-
Solicitação de Recomendação:
- "Bido" pede à Alexa uma recomendação de jantar.
- A Alexa coleta os dados e faz uma solicitação ao sistema.
-
Consulta ao Banco de Dados:
- O sistema consulta o banco de dados PostgreSQL para obter opções de cardápio.
- As informações são enviadas ao Gemini que por sua vez retorna o output da recomendação.
-
Resposta da Alexa:
- A Alexa apresenta a "Bido" uma lista de recomendações de jantar personalizadas, incluindo detalhes como nome do prato, preço e restaurante.
Desafios e Considerações
Garantir que os dados dos cardápios estejam sempre atualizados para fornecer recomendações precisas.
Diagrama
Vamos montar um diagrama de sequência, que contemple o nosso cenário. Temos que considerar que:
-
Ator (Usuário)
- Ativa a Alexa usando um comando de voz.
-
Alexa
- Recebe o comando para ativar uma skill específica, aqui rotulada como "prato rápido".
-
Skill
- Consulta o endpoint associado à skill ativada.
-
Webhook
- Ativado pela skill para realizar uma ação específica, como consultar dados.
-
Postgres
- Banco de dados que é consultado pelo webhook para buscar dados de produtos.
-
Lista de Produtos
- A lista de produtos recuperados do banco de dados Postgres é enviada de volta ao webhook.
-
Pedido de Análise
- Os dados dos produtos recuperados podem opcionalmente ser enviados para análise adicional por um LLM (Modelo de Linguagem de Grande Escala).
-
LLM
- Processa os dados de texto, para gerar uma nova saída com base na análise.
-
Saída Final
- A resposta processada é enviada de volta à skill, que então a comunica ao usuário através da Alexa.
Desenvolvimento
Sabendo do cenário e do fluxo de interação com as tecnologias, vamos começar desenvolver a solução. Precisamos de 2 serviços para tornar nossa aplicação mais rápida. Um serviço responsável pelo webhook e outro serviço para o scraper, garantindo que a base de dados se mantenha atualizada com os sites de pedidos.
Service 1
- Atualiza produtos a cada 30 minutos.
- Interage diretamente com os catálogos nos sites didios.menudino.com.br e potatos.menudino.com.br.
Service 2
- Interage com Gemini.
- Utiliza o Gemini para criar a resposta.
- Banco de dados central que armazena informações usadas por outros serviços.
Software
ProductController
- Descrição: Controlador principal que recebe solicitações HTTP relacionadas a solicitação da skill da alexa.
- Entradas: Solicitações HTTP com dados de produto (ex: POST com detalhes que a skill fornece no corpo da requisição).
- Saídas: Respostas HTTP baseadas no resultado das operações.
ProductCLI
- Descrição: Interface de linha de comando para interação direta com operações de produtos.
- Entradas: Comandos via terminal (ex: atualizar os produdos na base de dados).
- Saídas: Resultados das operações exibidos no terminal.
Casos de Uso
ProductUseCase
- Descrição: Caso de uso principal para operações com produtos, como consulta e atualização de informações de produto.
-
Entradas: Dados de produtos fornecidos pelo
ProductController
ouProductCLI
. - Saídas: Resultados das operações (sucesso, falha, dados do produto), que são enviados de volta para os respectivos iniciadores de solicitações.
ProductScraperUseCase
- Descrição: Caso de uso específico para o scraping de dados de produtos de diferentes fontes externas.
- Entradas: Comandos ou solicitações para iniciar o scraping.
- Saídas: Dados de produtos que são armazenados nos repositórios ou retornados para atualizações em tempo real.
Repositórios
ProductDatabaseRepository
- Descrição: Repositório responsável por gerenciar as operações de banco de dados relacionadas aos produtos.
- Entradas: Requisições de armazenamento, atualização ou recuperação de dados de produtos.
- Saídas: Dados de produtos recuperados ou status das operações de banco de dados.
ProductScraperRepository
- Descrição: Repositório que lida com a integração e armazenamento dos dados obtidos através do scraping de produtos.
-
Entradas: Dados de produtos do
ProductScraperUseCase
. - Saídas: Status do armazenamento dos dados e dados integrados prontos para uso.
ProductLLMRepository
- Descrição: Repositório para gerenciar operações específicas de aprendizado de máquina relacionadas a produtos, como recomendações.
- Entradas: Dados de produtos para a análise.
- Saídas: Resultados das análises de aprendizado de máquina, como recomendações.
Fluxos de Informação
-
De interfaces para casos de uso:
-
ProductController
eProductCLI
fornecem dados de entrada que são processados pelos casos de usoProductUseCase
eProductScraperUseCase
.
-
-
De casos de uso para repositórios:
- Dados são enviados dos casos de uso para os repositórios para armazenamento, recuperação ou processamento adicional.
Implementação
Primeiro passo é definir os contratos (interfaces) que serão implementados no sistema.
package contract
import (
"context"
"net/http"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/domain"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/dto"
)
type ProductScraperRepository interface {
FindProducts(context.Context) ([]domain.Product, error)
}
type ProductDataBaseRepository interface {
Fetch(context.Context) ([]domain.Product, error)
Create(context.Context, domain.Product) (*domain.Product, error)
}
type ProductScraperUseCase interface {
SeedProducts(context.Context) ([]domain.Product, error)
}
type ProductUseCase interface {
GetMenu(context.Context) (*string, error)
SearchForTips(context.Context) (*dto.AlexaResponse, error)
}
type ProductLLMRepository interface {
GetMenu(context.Context, []domain.Product) (*string, error)
}
type ProductController interface {
SearchForTips(http.ResponseWriter, *http.Request)
}
type ProductCLI interface {
SeedProducts(context.Context)
}
Com as definições em mãos, precisamos realizar a implementação de cada interface com seus métodos.
Um exemplo da implementação do scraper
package repository
import (
"context"
"strconv"
"strings"
"time"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/contract"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/domain"
"github.com/playwright-community/playwright-go"
)
const (
DIDIOS_SITE = "https://didios.menudino.com/"
)
type didiosRepository struct {
scrapper *playwright.Playwright
}
// FindProducts implements contract.ProductScraperRepository.
func (repository *didiosRepository) FindProducts(context.Context) ([]domain.Product, error) {
products := []domain.Product{}
browser, err := repository.scrapper.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(true),
})
if err != nil {
return nil, err
}
context, err := browser.NewContext()
if err != nil {
return nil, err
}
page, err := context.NewPage()
if err != nil {
return nil, err
}
_, err = page.Goto(DIDIOS_SITE)
if err != nil {
return nil, err
}
categories, err := page.Locator("#cardapio > section.cardapio-body > div > div.categories > div").All()
if err != nil {
return nil, err
}
for _, category := range categories {
category.ScrollIntoViewIfNeeded()
time.Sleep(time.Millisecond * 500)
cards, err := category.Locator("div:nth-child(2) > div > div").All()
if err != nil {
return nil, err
}
for _, card := range cards {
card.ScrollIntoViewIfNeeded()
productName, err := card.Locator("a > div > div.media-body > div.name > span").TextContent()
if err != nil {
return nil, err
}
productPrice, err := card.Locator("a > div > div.media-body > div.priceDescription > div").TextContent()
if err != nil {
return nil, err
}
productPrice = strings.ReplaceAll(productPrice, "R$ ", "")
productPrice = strings.ReplaceAll(productPrice, ",", ".")
product := domain.Product{
Name: productName,
Company: "DIDIOS",
}
if price, err := strconv.ParseFloat(productPrice, 64); err == nil {
product.Price = price
}
products = append(products, product)
}
}
return products, nil
}
func NewDidiosRepository(scraper *playwright.Playwright) contract.ProductScraperRepository {
return &didiosRepository{
scrapper: scraper,
}
}
Exemplo da implementação da integração com o Gemini
package repository
import (
"bytes"
"context"
"fmt"
"log"
"text/tabwriter"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/contract"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/domain"
"github.com/google/generative-ai-go/genai"
)
type geminiRepository struct {
client *genai.Client
}
// GetMenu implements contract.ProductLLMRepository.
func (repository *geminiRepository) GetMenu(ctx context.Context, products []domain.Product) (*string, error) {
model := repository.client.GenerativeModel("gemini-1.5-pro")
chatSession := model.StartChat()
var b bytes.Buffer
writer := tabwriter.NewWriter(&b, 0, 8, 1, '\t', tabwriter.AlignRight)
for _, product := range products {
fmt.Fprintf(writer, "ID: %v\tNome: %s\tPreço: %v\tEmpresa: %s\n", product.ID, product.Name, product.Price, product.Company)
}
writer.Flush()
prompt := fmt.Sprintf(`
Você é um atendente de restaurante.
Sempre monte um cardápio para pedir em qualquer hora segundo essa lista de produtos: %s.
Informe o que seria ideal pedir para comer e o preço total da compra.
Detalhe de qual empresa é cada produto, bem como seu nome e preço.
Mostre três opções de pedidos.
Não dê mais informações do que o necessário.
Sempre seja gentil.
Seja sucinto!
Você pode misturar os produtos de todas as empresas, se quiser.
Diversifique sempre os pedidos para não ser toda vez a mesma coisa.
Coloque uma pequena frase legal no final.
Escreva os números por extenso sempre.
Escreva tudo por extenso para leitura da Alexa.
Escreva tudo em uma frase, respeitando o portugues.
Não coloque caracteres especiais.
Fale sempre em primeira pessoa.
`, b.String())
res, err := chatSession.SendMessage(ctx, genai.Text(prompt))
if err != nil {
log.Fatal(err)
}
output := ""
for _, cand := range res.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
output = fmt.Sprintf("%s %s", output, part)
}
}
}
fmt.Println(output)
return &output, nil
}
func NewGeminiRepository(client *genai.Client) contract.ProductLLMRepository {
return &geminiRepository{
client: client,
}
}
A persistência dos dados no banco.
package database
import (
"context"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/contract"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/domain"
"github.com/jmoiron/sqlx"
)
type productDatabaseRepository struct {
database *sqlx.DB
}
// Create implements contract.ProductDataBaseRepository.
func (repository *productDatabaseRepository) Create(ctx context.Context, input domain.Product) (*domain.Product, error) {
output := domain.Product{}
query := "INSERT INTO product (name, price, company, inserted_at) VALUES ($1, $2, $3, $4) RETURNING *;"
err := repository.database.QueryRowxContext(
ctx,
query,
input.Name,
input.Price,
input.Company,
input.InsertedAt,
).StructScan(&output)
if err != nil {
return nil, err
}
return &output, nil
}
// Fetch implements contract.ProductDataBaseRepository.
func (repository *productDatabaseRepository) Fetch(ctx context.Context) ([]domain.Product, error) {
output := []domain.Product{}
query := `
SELECT p.* FROM product p
INNER JOIN (
SELECT MAX(inserted_at) AS MAXDATE
FROM product
) p2
ON p.inserted_at = p2.MAXDATE
`
err := repository.database.SelectContext(ctx, &output, query)
if err != nil {
return nil, err
}
return output, nil
}
func NewProductDatabase(database *sqlx.DB) contract.ProductDataBaseRepository {
return &productDatabaseRepository{database: database}
}
E o nosso caso de uso do scraper.
package usecase
import (
"context"
"slices"
"time"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/contract"
"github.com/booscaaa/go-gemini-gdg/api/internals/core/domain"
)
type scraperUsecase struct {
didiosRepository contract.ProductScraperRepository
potatosRepository contract.ProductScraperRepository
productDatabaseRepository contract.ProductDataBaseRepository
}
// SeedProducts implements contract.ProductScraperUseCase.
func (usecase *scraperUsecase) SeedProducts(ctx context.Context) ([]domain.Product, error) {
currenteDate := time.Now()
productsCreated := []domain.Product{}
potatosProducts, err := usecase.potatosRepository.FindProducts(ctx)
if err != nil {
return nil, err
}
didiosProducts, err := usecase.didiosRepository.FindProducts(ctx)
if err != nil {
return nil, err
}
products := slices.Concat(potatosProducts, didiosProducts)
for _, product := range products {
product.InsertedAt = currenteDate
productCreated, err := usecase.productDatabaseRepository.Create(ctx, product)
if err != nil {
return nil, err
}
productsCreated = append(productsCreated, *productCreated)
}
return productsCreated, nil
}
func NewProductScraperUsecase(
didiosRepository contract.ProductScraperRepository,
potatosRepository contract.ProductScraperRepository,
productDatabaseRepository contract.ProductDataBaseRepository,
) contract.ProductScraperUseCase {
return &scraperUsecase{
didiosRepository: didiosRepository,
potatosRepository: potatosRepository,
productDatabaseRepository: productDatabaseRepository,
}
}
O código completo da implementação está aqui:
https://github.com/booscaaa/go-gemini-gdg
Conclusão
Integrar a Alexa com o Gemini e um banco de dados PostgreSQL permitirá que o usuário receba recomendações de jantar personalizadas de maneira eficiente. Com serviços independentes alimentando o banco de dados, a solução garante atualizações constantes e precisas, proporcionando uma experiência de usuário satisfatória e personalizada. E, seguindo uma arquitetura limpa, sem acoplamentos, podemos incrementar funções na skill sem afetar o funcionamento atual.
Posted on July 1, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.