Go, Gemini e Alexa: Como criar automações para o seu dia a dia
Tags: go, gemini, alexa, scraping
Published: 2024-07-01 14:20:53

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w5o6xeallal5hc8ui35.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wv629f4k69aep24w36hy.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6xrcrhgbdboxydserj7.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yldmz6n1skzgmtzz4de.png)


#### 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.

```go
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

```go
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

```go
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.

```go
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.

```go
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.