Go, Gemini e Alexa: Como criar automações para o seu dia a dia

booscaaa

Vinícius Boscardin

Posted on July 1, 2024

Go, Gemini e Alexa: Como criar automações para o seu dia a dia

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

Image description

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

  1. Integração com a Alexa:

    • Desenvolver uma skill para a Alexa que permita a "Bido" pedir recomendações de jantar.
  2. 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.
  3. 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.
  4. Processo de Recomendação:

    • Implementar um algoritmo de recomendação com os dados disponíveis no banco de dados.

Fluxo de Trabalho

  1. 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.
  2. 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.
  3. 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.
  4. 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:

Image description

  1. Ator (Usuário)

    • Ativa a Alexa usando um comando de voz.
  2. Alexa

    • Recebe o comando para ativar uma skill específica, aqui rotulada como "prato rápido".
  3. Skill

    • Consulta o endpoint associado à skill ativada.
  4. Webhook

    • Ativado pela skill para realizar uma ação específica, como consultar dados.
  5. Postgres

    • Banco de dados que é consultado pelo webhook para buscar dados de produtos.
  6. Lista de Produtos

    • A lista de produtos recuperados do banco de dados Postgres é enviada de volta ao webhook.
  7. 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).
  8. LLM

    • Processa os dados de texto, para gerar uma nova saída com base na análise.
  9. 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.

Image description

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

Image description

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 ou ProductCLI.
  • 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

  1. De interfaces para casos de uso:
    • ProductController e ProductCLI fornecem dados de entrada que são processados pelos casos de uso ProductUseCase e ProductScraperUseCase.
  2. 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)
}


Enter fullscreen mode Exit fullscreen mode

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,
    }
}


Enter fullscreen mode Exit fullscreen mode

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,
    }
}



Enter fullscreen mode Exit fullscreen mode

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}
}


Enter fullscreen mode Exit fullscreen mode

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,
    }
}


Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
booscaaa
Vinícius Boscardin

Posted on July 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related