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 ProductControllerouProductCLI.
- 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:
- 
ProductControllereProductCLIfornecem dados de entrada que são processados pelos casos de usoProductUseCaseeProductScraperUseCase.
 
- 
- 
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.
 





 
    
Top comments (1)