Initial commit

This commit is contained in:
2026-05-14 15:29:03 -03:00
parent 82ac556ecc
commit 54bcf081f6
31 changed files with 3132 additions and 518 deletions

52
docs/README.md Normal file
View File

@@ -0,0 +1,52 @@
# Assistente Analítico para Banco de Dados - Documentação
Documentação do projeto **Assistente Analítico para Banco de Dados** da Inovyo, uma plataforma de IA conversacional que utiliza dados de pesquisas de experiência do cliente (CX) para fornecer insights analíticos.
## Índice
| Documento | Descrição |
|-----------|-----------|
| [Arquitetura](architecture.md) | Visão geral da arquitetura, componentes e fluxo de dados |
| [Referência da API](api-reference.md) | Endpoints, modelos de dados e exemplos de uso |
| [Modelo de Dados](data-model.md) | Estrutura das tabelas, relacionamentos e convenções |
| [Infraestrutura](infrastructure.md) | Recursos AWS provisionados com Pulumi (ECS, ALB, ECR) |
| [Deploy](deployment.md) | Processo de build, push e deploy da aplicação |
| [Desenvolvimento](development.md) | Guia para contribuição, estrutura de código e padrões |
## Visão Geral do Projeto
A Inovyo é uma plataforma especializada em gestão de experiência do cliente (CX), responsável por coletar, processar e analisar dados de pesquisas. Este projeto cria um chatbot que atua como **consultor especialista**, utilizando os dados dos dashboards da Inovyo para:
- Responder perguntas sobre resultados de pesquisas
- Identificar padrões, tendências e oportunidades
- Auxiliar gestores na interpretação de indicadores (NPS, CSAT, CES)
- Oferecer insights contextualizados e acionáveis
## Stack Tecnológico
| Camada | Tecnologia |
|--------|-----------|
| **Linguagem** | Python 3.12+ |
| **Frontend** | Streamlit |
| **API** | FastAPI + Uvicorn |
| **Agente IA** | LangGraph + LangChain |
| **LLMs** | AWS Bedrock (Claude, Llama, Nova) |
| **Banco de Dados** | DynamoDB |
| **Observabilidade** | Langfuse |
| **Infraestrutura** | AWS ECS Fargate, ALB, ECR |
| **IaC** | Pulumi (Python) |
| **Container** | Docker |
## Estrutura do Backend
O pacote `backend` é dividido em módulos com responsabilidades únicas:
```
code/app/backend/
├── __init__.py # Marca o diretório como pacote Python
├── config.py # Leitura de variáveis de ambiente
├── dynamo.py # Cliente DynamoDB, Langfuse, get_contexto
├── tools.py # Ferramentas LangChain (@tool)
├── agent_bedrock.py # LLM Bedrock, grafo LangGraph, AgentState
└── orquestrador.py # Ponto de entrada: main()
```

88
docs/api-reference.md Normal file
View File

@@ -0,0 +1,88 @@
# Referência da API
A API REST é servida pelo FastAPI na porta `8000`, implementada em [app/api.py](../code/app/api.py) e orquestrada por [app/backend/orquestrador.py](../code/app/backend/orquestrador.py).
## Endpoints
### `GET /`
Health check da aplicação.
**Resposta:**
```json
{
"status": "ok"
}
```
---
### `POST /agent`
Executa uma consulta no agente analítico.
**Request Body (`QueryRequest`):**
| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| `query` | `string` | Sim | Pergunta do usuário em português |
| `history` | `string` | Não | Histórico de mensagens anteriores (serializado) |
| `model` | `string` | Não | ID do modelo LLM (padrão: Claude Haiku) |
| `base` | `string` | Não | Nome do dashboard/base de dados |
**Response Body (`QueryResponse`):**
| Campo | Tipo | Descrição |
|-------|------|-----------|
| `response` | `string` | Resposta do agente em português |
| `input_tokens` | `int` | Total de tokens de entrada consumidos |
| `output_tokens` | `int` | Total de tokens de saída gerados |
| `total_tokens` | `int` | Total de tokens (input + output) |
**Exemplo — cURL:**
```bash
curl -X POST http://localhost:8000/agent \
-H "Content-Type: application/json" \
-d '{
"query": "Qual foi meu NPS?",
"history": "",
"model": "anthropic.claude-haiku-4-5-20251001-v1:0",
"base": "nome_do_dashboard"
}'
```
**Exemplo — Resposta:**
```json
{
"response": "O NPS foi...",
"input_tokens": 1250,
"output_tokens": 340,
"total_tokens": 1590
}
```
## Modelos Disponíveis
| ID do Modelo | Provider |
|-------------|----------|
| `anthropic.claude-haiku-4-5-20251001-v1:0` | Anthropic |
| `anthropic.claude-sonnet-4-5-20250929-v1:0` | Anthropic |
| `meta.llama4-maverick-17b-instruct-v1:0` | Meta |
| `meta.llama4-scout-17b-instruct-v1:0` | Meta |
| `amazon.nova-lite-v1:0` | Amazon |
| `amazon.nova-pro-v1:0` | Amazon |
| `amazon.nova-2-lite-v1:0` | Amazon |
## Bases Disponíveis
As bases são carregadas dinamicamente do DynamoDB (tabela `TABLE`, index `item_type_index`, `item_type = "contexto"`). O formato do nome segue o padrão `{cliente}_{dashboard}`.
## Documentação Interativa
Com a aplicação rodando:
- **Swagger UI:** `http://localhost:8000/docs`
- **ReDoc:** `http://localhost:8000/redoc`

164
docs/architecture.md Normal file
View File

@@ -0,0 +1,164 @@
# Arquitetura
## Visão Geral
O Assistente Analítico é composto por três camadas principais: **interface** (Streamlit e FastAPI), **agente de IA** (LangGraph com Bedrock) e **dados** (DynamoDB).
```
┌─────────────────────────────────────────────────────────┐
│ Interfaces │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ Streamlit (8501) │ │ FastAPI (8000) │ │
│ │ front.py │ │ api.py │ │
│ └────────┬───────────┘ └──────────┬─────────────┘ │
│ │ │ │
│ └──────────┬──────────────┘ │
│ ▼ │
│ orquestrador.main(query, history, model, base) │
│ │ │
│ ┌───────────────────▼──────────────────────────┐ │
│ │ agent_bedrock — LangGraph │ │
│ │ ┌────────┐ ┌───────┐ ┌────────────┐ │ │
│ │ │ Model │───▶│Router │───▶│ Tools │ │ │
│ │ │ Node │◀───│ │ │ Node │ │ │
│ │ └────────┘ └───────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Serviços AWS │ │
│ │ ┌──────────┐ ┌────────────────────────────┐ │ │
│ │ │ Bedrock │ │ DynamoDB │ │ │
│ │ │ (LLMs) │ │ (contexto + dados pré- │ │ │
│ │ └──────────┘ │ processados) │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Observabilidade │ │
│ │ ┌──────────┐ ┌──────────────────────────┐ │ │
│ │ │ Langfuse │ │ CloudWatch Logs │ │ │
│ │ └──────────┘ └──────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Módulos do Backend
O pacote `backend` é organizado por responsabilidade:
```
backend/
├── config.py ← variáveis de ambiente
├── dynamo.py ← depende de config
├── tools.py ← depende de config, dynamo
├── agent_bedrock.py ← depende de config, tools
└── orquestrador.py ← depende de config, dynamo, agent_bedrock
```
Não há dependências circulares entre os módulos.
### `config.py`
Lê todas as variáveis de ambiente na inicialização e exporta como constantes:
| Variável | Descrição |
|----------|-----------|
| `TABLE` | Tabela DynamoDB |
| `REGION` | Região AWS |
| `AWS_ACCOUNT` | ID da conta AWS |
| `SECRET_NAME` | Nome do secret no Secrets Manager |
### `dynamo.py`
- Instancia o cliente `dynamodb` (boto3) usando `REGION`
- `get_secret()` — busca credenciais do Langfuse no Secrets Manager
- Inicializa o objeto `langfuse` na carga do módulo
- `get_contexto(dashboard: str) -> dict` — carrega contexto, filtro e itens disponíveis do DynamoDB para o dashboard informado
### `tools.py`
Define a classe `ReportTools`, instanciada por requisição com o mapeamento de IDs local → real.
```
ReportTools(id_mapping: dict[str, str])
├── get_variable_value(id, variable) — busca uma variável no DynamoDB
├── get_variables_list(id) — lista as variáveis disponíveis para um ID
└── as_tools() -> list[StructuredTool] — retorna as tools prontas para o agente
```
| Tool (nome exposto ao LLM) | Método | Descrição |
|---------------------------|--------|-----------|
| `get_variable_value` | `get_variable_value(id, variable)` | Busca o valor de uma variável no DynamoDB para o ID informado |
| `get_variable_list` | `get_variables_list(id)` | Lista as variáveis disponíveis para o ID informado |
Os IDs expostos ao LLM são locais (`id_1`, `id_2`, …). Internamente cada método converte o ID local para o ID real do DynamoDB via `self.id_mapping`. Por usar estado de instância em vez de variável global, múltiplas requisições simultâneas ficam completamente isoladas.
### `agent_bedrock.py`
- `AgentState` — TypedDict com `messages` e `current_step`
- `create_bedrock_llm(model_id, region, tools)` — instancia `ChatBedrockConverse` e vincula as tools via `bind_tools`
- `call_model(state, llm)` — nó do grafo: invoca o LLM
- `call_tools(state, tools_map)` — nó do grafo: executa as tool calls usando o `tools_map` da requisição
- `should_continue(state)` — roteador: `"tools"` se há tool_calls, `"end"` se não
- `create_agent(inference_profile_arn, region, tools)` — constrói `tools_map = {t.name: t for t in tools}`, monta e compila o `StateGraph`
**Fluxo do Grafo:**
```
SystemMessage + HumanMessage
┌─────────┐
│ model │ ◄── LLM via Bedrock
└────┬────┘
should_continue?
├── tool_calls → ┌───────┐
│ │ tools │ → executa tools → volta ao model
│ └───────┘
└── (fim) → [END]
```
### `orquestrador.py`
Ponto de entrada da lógica de negócio. A função `main(user_query, history, model, base)`:
1. Chama `get_contexto(base)` para carregar o contexto do dashboard
2. Constrói `id_mapping` (`id_1`, `id_2`, … → IDs reais do DynamoDB) e `local_items` (IDs locais → descrições)
3. Instancia `ReportTools(id_mapping)` com o mapeamento isolado da requisição
4. Monta o `SYSTEM_PROMPT` dinamicamente com contexto, regras de filtro, `local_items` e histórico
5. Cria o agente via `create_agent(model, REGION, tools=report_tools.as_tools())`
6. Invoca o agente com o estado inicial
7. Agrega tokens de todos os `AIMessage` do estado final
8. Retorna `response`, `input_tokens`, `output_tokens`, `total_tokens`
## Interfaces
### `front.py` — Streamlit (porta 8501)
- Importa `from backend import orquestrador`
- Lista bases disponíveis consultando DynamoDB (`item_type_index`, `item_type = "contexto"`)
- Seleção de modelo LLM e base via `st.selectbox`
- Histórico de conversas em `session_state`
- Efeito de digitação caractere a caractere
- Exibe consumo de tokens por resposta
### `api.py` — FastAPI (porta 8000)
- Importa `from .backend import orquestrador`
- `GET /` — health check
- `POST /agent` — recebe `QueryRequest`, chama `orquestrador.main()`, retorna `QueryResponse`
## Modelos LLM Suportados
| Modelo | Provider | Prefixo de Rota |
|--------|----------|-----------------|
| `anthropic.claude-haiku-4-5-20251001-v1:0` | Anthropic | `us` |
| `anthropic.claude-sonnet-4-5-20250929-v1:0` | Anthropic | `global` |
| `meta.llama4-maverick-17b-instruct-v1:0` | Meta | `us` |
| `meta.llama4-scout-17b-instruct-v1:0` | Meta | `us` |
| `amazon.nova-lite-v1:0` | Amazon | `us` |
| `amazon.nova-pro-v1:0` | Amazon | `us` |
| `amazon.nova-2-lite-v1:0` | Amazon | `global` |
Todos acessados via AWS Bedrock inference profiles cross-region. O ARN é construído dinamicamente com `REGION` e `AWS_ACCOUNT`.

129
docs/data-model.md Normal file
View File

@@ -0,0 +1,129 @@
# Modelo de Dados
## Visão Geral
Os dados de contexto e relatórios pré-processados são armazenados e consumidos diretamente do **DynamoDB**.
## Estrutura de Tabelas
Cada combinação cliente + dashboard gera **três tabelas**:
```
{cliente}_{dashboard}_contatos
{cliente}_{dashboard}_respostas
{cliente}_{dashboard}_pesquisa
```
### Convenção de Nomenclatura
- Letras minúsculas
- Espaços e hífens substituídos por `_`
**Exemplo:**
- Cliente: `Bacio Di Latte`, Dashboard: `Transacional Loja App`
- Tabelas: `bacio_transacional_loja_app_contatos`, `bacio_transacional_loja_app_respostas`, `bacio_transacional_loja_app_pesquisa`
---
### 1. `{cliente}_{dashboard}_contatos`
Contatos recebidos para ativar as pesquisas. Cada linha = um contato único.
**Colunas Fixas:**
| Coluna | Descrição |
|--------|-----------|
| `contact_id` | Identificador único do contato |
| `surveyid` | Identificador da pesquisa associada |
| `name` | Nome do contato |
| `email` | E-mail |
| `phone` | Telefone |
| `status` | Status (enviado, inválido, abriu e-mail, etc.) |
| `url` | Link único de resposta |
| `data_recebimento` | Data de recebimento (`yyyy-mm-dd`) |
| `mes_recebimento` | Mês de recebimento (`mm/yyyy`) |
**Colunas Variáveis** (dependem do cliente): `cidade`, `estado`, `cpf`, `produto_comprado`, `canal_de_compra`, `nome_da_loja`, `valor_da_venda`, etc.
---
### 2. `{cliente}_{dashboard}_respostas`
Respostas consolidadas da pesquisa. Cada linha = um respondente único (versão mais atualizada).
**Colunas Fixas:**
| Coluna | Descrição |
|--------|-----------|
| `responseid` | Identificador único da resposta |
| `status` | Status (completou, parcial, inválida) |
| `contact_id` | Referência ao contato (FK) |
| `data_resposta` | Data da resposta (`yyyy-mm-dd`) |
| `mes_resposta` | Mês da resposta (`mm/yyyy`) |
**Colunas Variáveis (Perguntas):** Uma coluna por pergunta, nomeada pelo `shortname` da tabela `pesquisa`. Exemplos: `nps`, `atendimento_cordialidade`, `achou_produto`, `comentarios`.
---
### 3. `{cliente}_{dashboard}_pesquisa`
Catálogo de perguntas dos questionários. Dicionário das colunas dinâmicas da tabela de respostas.
| Coluna | Descrição |
|--------|-----------|
| `order_by` | Ordem da pergunta no questionário |
| `title` | Texto completo da pergunta |
| `shortname` | Alias usado como nome de coluna em `respostas` |
| `status` | Status (ativa, oculta, inativa) |
---
### Relacionamentos
```
contatos (1) ─── (N) respostas (join via contact_id)
pesquisa ──────────▶ respostas (shortname = nome das colunas dinâmicas)
```
## DynamoDB — Tabela `poc_dnx_monthly_summary`
Armazena dados pré-processados e contexto para o agente.
### Estrutura
| Campo | Tipo | Descrição |
|-------|------|-----------|
| `id` | `string` (PK) | Identificador (ex: `1`, `2`, `bacio_transacional_loja_app_contexto`) |
| `item_type` | `string` (GSI) | Tipo do item (ex: `contexto`) |
| `contexto` | `string` | Descrição contextual do dashboard |
| `filter_key` | `string` | Tipo de filtro: `period` ou `event` |
| `itens_disponiveis` | `map` | Dicionário `{id: descrição}` dos dados disponíveis |
| `chaves_consolidadas` | `string` | Lista de colunas/variáveis disponíveis por ID |
| *variáveis dinâmicas* | `string` | Dados pré-processados (resumos mensais, NPS, etc.) |
### Índice Secundário Global
- **`item_type_index`** — Permite consultar todos os itens de um tipo (ex: listar todas as bases com `item_type = "contexto"`)
### Formato do ID de Período
Quando `filter_key = "period"`, os IDs seguem o formato `year_month`:
- Ano: 4 dígitos (2025, 2024, etc.)
- Mês: 2 dígitos (01 a 12)
- Exemplo: `2025_05` = maio de 2025
### Formato do ID de Evento
Quando `filter_key = "event"`, os IDs descrevem o evento:
- Formato: `Nome - Cidade DD/MM/YYYY`
## Cálculo de NPS
Dentro dos dados existe a variável `NPS` que contém `distribuicao` com notas e quantidade de respondentes:
- Notas **0-6**: Detratores
- Notas **7-8**: Neutros
- Notas **9-10**: Promotores
**Fórmula:** `NPS = %Promotores - %Detratores`

92
docs/deployment.md Normal file
View File

@@ -0,0 +1,92 @@
# Deploy
## Fluxo
```
1. Build Docker ──▶ 2. Push ECR ──▶ 3. Atualizar SHA no Pulumi ──▶ 4. pulumi up
```
---
## 1. Build da Imagem Docker
Execute a partir da raiz do repositório:
```bash
docker build code/ --platform linux/amd64 -t <ECR_REPO_NAME>
```
---
## 2. Push para o ECR
### Autenticar no ECR
```bash
aws ecr get-login-password --region <REGION> \
| docker login --username AWS --password-stdin <AWS_ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com
```
### Tag e push
```bash
docker tag <ECR_REPO_NAME>:latest \
<AWS_ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/<ECR_REPO_NAME>:latest
docker push <AWS_ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/<ECR_REPO_NAME>:latest
```
---
## 3. Obter o SHA da imagem publicada
Após o push, obtenha o digest da imagem no ECR:
```bash
aws ecr describe-images \
--repository-name <ECR_REPO_NAME> \
--region <REGION> \
--query 'sort_by(imageDetails, &imagePushedAt)[-1].imageDigest' \
--output text
```
O retorno será no formato `sha256:xxxxxxxx...`.
---
## 4. Atualizar o SHA no Pulumi
Edite [infra/ecs_alb/Pulumi.Inovyo.yaml](../infra/ecs_alb/Pulumi.Inovyo.yaml) e atualize o campo `ecr_image_digest` com o valor obtido no passo anterior:
```yaml
ecr_image_digest: sha256:<novo-digest>
```
---
## 5. Aplicar com Pulumi
```bash
cd infra/ecs_alb
pulumi up --stack Inovyo
```
Para visualizar as mudanças antes de aplicar:
```bash
pulumi preview --diff --stack Inovyo
```
---
## Configuração do Container
O `entrypoint.sh` inicia dois processos:
1. **FastAPI** (background): `uvicorn app.api:app --host 0.0.0.0 --port 8000`
2. **Streamlit** (foreground): `streamlit run app/front.py --server.port 8501 --server.address 0.0.0.0 --server.headless true`
Após o deploy, a aplicação fica acessível pelo DNS do ALB:
- **API:** `http://<alb-dns>:8000`
- **Streamlit:** `http://<alb-dns>:8501`

144
docs/development.md Normal file
View File

@@ -0,0 +1,144 @@
# Desenvolvimento
## Estrutura do Projeto
```
agente-bd/
├── code/ # Código da aplicação
│ ├── app/
│ │ ├── api.py # FastAPI — endpoints REST
│ │ ├── front.py # Streamlit — interface web
│ │ └── backend/
│ │ ├── __init__.py # Marca o diretório como pacote Python
│ │ ├── config.py # Variáveis de ambiente
│ │ ├── dynamo.py # DynamoDB, Langfuse, get_contexto
│ │ ├── tools.py # ReportTools: get_variable_value, get_variables_list
│ │ ├── agent_bedrock.py # LLM Bedrock, grafo LangGraph
│ │ └── orquestrador.py # main() — ponto de entrada
│ ├── utils/
│ │ └── dynamodb_read_table.py # Utilitários DynamoDB
│ ├── main.py # Entry point
│ ├── Dockerfile # Imagem Docker
│ ├── entrypoint.sh # Script de inicialização
│ └── requirements.txt # Dependências Python
├── infra/ # Infraestrutura como Código
│ ├── ecr/ # Stack ECR
│ ├── ecs_alb/ # Stack ECS + ALB
│ └── langfuse/ # Stack Langfuse
├── docs/ # Esta documentação
└── Makefile # Automação de build e deploy
```
## Responsabilidades dos Módulos do Backend
### `config.py`
Lê todas as variáveis de ambiente obrigatórias na carga do módulo e as exporta como constantes. Qualquer módulo que precise de configuração importa daqui.
### `dynamo.py`
- Instancia o cliente `dynamodb` e o objeto `langfuse` na carga do módulo
- `get_secret()` — busca as credenciais do Langfuse no AWS Secrets Manager
- `get_contexto(dashboard: str) -> dict` — retorna `contexto`, `filter` e `items_disponiveis` para o dashboard informado
### `tools.py`
Define a classe `ReportTools` com as ferramentas do agente:
- `ReportTools(id_mapping)` — instanciada por requisição; `id_mapping` converte IDs locais (`id_1`, `id_2`, …) para os IDs reais do DynamoDB
- `get_variable_value(id, variable)` — busca o valor de uma variável no DynamoDB
- `get_variables_list(id)` — lista as variáveis disponíveis para um ID
- `as_tools()` — retorna a lista de `StructuredTool` com nomes `get_variable_value` e `get_variable_list`
### `agent_bedrock.py`
- `AgentState` — TypedDict com `messages` e `current_step`
- `create_bedrock_llm(model_id, region, tools)` — instancia `ChatBedrockConverse` e vincula as tools
- `call_model(state, llm)`, `call_tools(state, tools_map)`, `should_continue(state)` — nós e roteador do grafo LangGraph
- `create_agent(inference_profile_arn, region, tools)` — monta o `tools_map` e compila o `StateGraph`
### `orquestrador.py`
Função `main(user_query, history, model, base)`:
- Carrega contexto via `get_contexto(base)`
- Constrói `id_mapping` e `local_items` para isolar IDs reais do LLM
- Instancia `ReportTools(id_mapping)` e passa as tools ao agente
- Monta o system prompt com `local_items` no lugar dos IDs reais
- Cria e invoca o agente
- Retorna resposta e contagem de tokens
## Cadeia de Imports
```
config.py
dynamo.py ←── config
tools.py ←── config, dynamo
agent_bedrock.py ←── config
orquestrador.py ←── config, dynamo, agent_bedrock, tools
api.py / front.py ←── orquestrador (via pacote backend)
```
## Dependências Principais
| Pacote | Uso |
|--------|-----|
| `boto3` | SDK AWS (DynamoDB, Secrets Manager) |
| `langchain` | Framework de orquestração LLM |
| `langchain-aws` | Integração LangChain + AWS Bedrock |
| `langgraph` | Framework de agentes baseado em grafos |
| `streamlit` | Interface web interativa |
| `fastapi` | Framework de API REST |
| `uvicorn` | Servidor ASGI |
| `langfuse` | Observabilidade e tracing de LLMs |
## Fluxo de Desenvolvimento
1. **Fazer alterações** — Editar os arquivos em `code/app/`
3. **Testar localmente** — Rodar FastAPI + Streamlit ou via Docker
4. **Validar** — Executar scripts de teste em `scripts/`
5. **Build e deploy** — Seguir o [guia de deploy](deployment.md)
## Adicionando uma Nova Ferramenta ao Agente
Todas as ferramentas vivem na classe `ReportTools` em `tools.py`. Basta:
1. Adicionar o método à classe:
```python
def minha_nova_tool(self, param: str) -> str:
"""Descrição da tool para o LLM."""
real_id = self.id_mapping.get(param, param) # se precisar resolver ID
# implementação
return resultado
```
2. Registrá-lo em `as_tools()`:
```python
def as_tools(self) -> list:
return [
...,
StructuredTool.from_function(
self.minha_nova_tool,
name="minha_nova_tool",
description="Descrição da tool para o LLM.",
),
]
```
Não é necessário alterar `agent_bedrock.py` — o `tools_map` é construído dinamicamente a partir da lista retornada por `as_tools()`.
## Adicionando um Novo Modelo LLM
Em `agent_bedrock.py`, adicionar o novo modelo aos três dicionários em `create_bedrock_llm()`:
- `MODEL_ARNS` — ARN do inference profile
- `PROVIDER` — provider (`anthropic`, `meta`, `amazon`)
- `prefix` — prefixo de rota (`us` ou `global`)
E adicionar o model ID à lista `MODELS` em `front.py`.

126
docs/infrastructure.md Normal file
View File

@@ -0,0 +1,126 @@
# Infraestrutura
A infraestrutura é gerenciada via **Pulumi** (IaC em Python) e provisionada na AWS.
## Visão Geral dos Recursos
```
┌─────────────────────────────────────────────────┐
│ AWS Account 305427701314 │
│ (us-east-1) │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Application Load Balancer │ │
│ │ Portas: 8501 (Streamlit), 8000 (API) │ │
│ │ Subnets: públicas (2) │ │
│ └─────────────────┬────────────────────────┘ │
│ │ │
│ ┌─────────────────▼────────────────────────┐ │
│ │ ECS Fargate Cluster │ │
│ │ Task: assistente-analitico-db-dev │ │
│ │ CPU: 256 | Memória: 512 MB │ │
│ │ Auto-scaling: 13 instâncias (60% CPU) │ │
│ │ Subnets: privadas (2) │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ ECR │ │ KMS │ │ Secrets │ │
│ │ Repo │ │ Key │ │ Manager │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ CloudWatch Logs │ │
│ │ Log Group: assistente-analitico-db-dev │ │
│ │ Retenção: 7 dias │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
## Stacks Pulumi
O diretório `infra/` contém três stacks independentes:
### 1. `infra/ecr/` — Elastic Container Registry
Cria o repositório ECR para armazenar as imagens Docker.
| Configuração | Valor |
|-------------|-------|
| Stack | `inovyo` |
| Repositório | `assistente-analitico-db-dev` |
### 2. `infra/ecs_alb/` — ECS + Application Load Balancer
Stack principal que provisiona o cluster ECS Fargate com ALB.
**Configurações principais:**
| Configuração | Valor |
|-------------|-------|
| Stack | `Inovyo` |
| Projeto | `assistente-analitico` |
| Ambiente | `dev` |
| Conta AWS | `305427701314` |
| Região | `us-east-1` |
**Rede:**
| Recurso | Valor |
|---------|-------|
| VPC | `vpc-17ceb96c` |
| Subnets ALB | 2 subnets públicas |
| Subnets ECS | 2 subnets privadas |
| Ingress CIDR | `3.14.44.224/32` (IP restrito) |
| ALB interno | Não (externamente acessível) |
**ECS Task:**
| Configuração | Valor |
|-------------|-------|
| Task Name | `assisnte-analitico-db-dev` |
| CPU | 256 |
| Memória | 512 MB |
| Desired Count | 1 |
| Auto-scaling | 13 instâncias, target 60% CPU |
| Portas | 8000 (API), 8501 (Streamlit) |
**Variáveis de ambiente do container:**
| Variável | Descrição |
|----------|-----------|
| `LANGFUSE_HOST` | Endpoint do Langfuse |
**Módulos inclusos:**
- `iam.py` — Roles de execução e task com políticas para Bedrock, DynamoDB, Secrets Manager, CloudWatch
- `ecs.py` — Definição de task, service, target groups, listeners e auto-scaling
- `ecr.py` — Referência ao repositório ECR
- `kms.py` — Chave KMS para criptografia
- `conf.py` — Carregamento de configuração do Pulumi
- `autotag/` — Auto-tagging de recursos AWS
### 3. `infra/langfuse/` — Langfuse
Provisionamento da infraestrutura para o serviço de observabilidade Langfuse.
| Configuração | Valor |
|-------------|-------|
| Stack | `inovyo` |
| Host | `http://172.31.252.176:3000` |
## Serviços AWS Utilizados
| Serviço | Uso |
|---------|-----|
| **ECS Fargate** | Orquestração de containers |
| **ALB** | Balanceamento de carga |
| **ECR** | Registry de imagens Docker |
| **Bedrock** | Inferência de modelos LLM |
| **DynamoDB** | Contexto e dados pré-processados do agente |
| **Secrets Manager** | Credenciais do Langfuse |
| **KMS** | Criptografia |
| **CloudWatch** | Logs |
| **IAM** | Controle de acesso |
## Como Deployar Infraestrutura
Consulte o [Guia de Deploy](deployment.md) para instruções detalhadas.

162
docs/langfuse-guide.md Normal file
View File

@@ -0,0 +1,162 @@
# Guia de Integração Langfuse
Este guia descreve como o Langfuse está integrado ao assistente e como utilizar suas funcionalidades de observabilidade e rastreamento.
## Índice
- [O que é Langfuse](#o-que-e-langfuse)
- [Como está integrado](#como-esta-integrado)
- [Credenciais](#credenciais)
- [Rastreamento automático via LangChain](#rastreamento-automatico-via-langchain)
- [Tags por dashboard](#tags-por-dashboard)
- [Adicionando scores customizados](#adicionando-scores-customizados)
- [Visualizando traces](#visualizando-traces)
---
## O que é Langfuse
Langfuse é uma plataforma de observabilidade para aplicações LLM que permite:
- Rastrear automaticamente todas as chamadas ao modelo (tokens, latência, erros)
- Inspecionar o histórico de mensagens e chamadas de tools
- Adicionar scores e métricas customizadas por trace
- Analisar performance e uso ao longo do tempo
---
## Como está integrado
A integração é feita em dois módulos:
### `dynamo.py` — inicialização do cliente
O cliente `Langfuse` é instanciado na carga do módulo, usando credenciais obtidas do AWS Secrets Manager:
```python
from langfuse import Langfuse
secrets = json.loads(get_secret())
langfuse = Langfuse(
public_key=secrets["LANGFUSE-PUBLIC-KEY"],
secret_key=secrets["LANGFUSE-SECRET-KEY"],
host=os.environ["LANGFUSE_HOST"],
)
```
O objeto `langfuse` é exportado e reutilizado pelo `orquestrador.py`.
### `orquestrador.py` — rastreamento por execução
A cada chamada ao agente, um `CallbackHandler` do LangChain é criado e passado na configuração do grafo LangGraph. Isso registra automaticamente no Langfuse todas as etapas da execução — chamadas ao modelo, chamadas de tools e mensagens trocadas.
Ao final da execução, `langfuse.flush()` garante o envio dos dados pendentes.
```python
from langfuse.langchain import CallbackHandler
from .dynamo import langfuse
langfuse_handler = CallbackHandler()
config = {"callbacks": [langfuse_handler], "tags": [base]}
final_state = agent.invoke(initial_state, config=config)
langfuse.flush()
```
---
## Credenciais
As chaves do Langfuse são armazenadas no AWS Secrets Manager, no secret definido pela variável de ambiente `SECRET_NAME`. O secret deve conter as seguintes chaves:
| Chave no secret | Descrição |
|-----------------|-----------|
| `LANGFUSE-PUBLIC-KEY` | Chave pública do projeto Langfuse |
| `LANGFUSE-SECRET-KEY` | Chave secreta do projeto Langfuse |
O endpoint do servidor é configurado pela variável de ambiente `LANGFUSE_HOST`.
---
## Rastreamento automático via LangChain
O `CallbackHandler` captura automaticamente, sem código adicional:
- Cada chamada ao modelo Bedrock (input, output, tokens)
- Chamadas às tools `get_monthly_report` e `get_consolidated_keys`
- Sequência de passos do grafo LangGraph
- Erros e exceções durante a execução
Cada invocação de `orquestrador.main()` gera um trace independente no Langfuse.
---
## Tags por dashboard
A tag `base` (nome do dashboard, ex: `bacio_transacional_loja_app`) é passada em cada execução:
```python
config = {"callbacks": [langfuse_handler], "tags": [base]}
```
Isso permite filtrar traces no Langfuse por cliente/dashboard.
---
## Adicionando scores customizados
Para registrar métricas adicionais em um trace, use a API do cliente `langfuse` após a execução do agente. O trace ID pode ser obtido via `langfuse_handler.get_trace_id()`.
### Tipos de score
| Tipo | Uso |
|------|-----|
| `NUMERIC` | Valores numéricos (ex: satisfação 15, tempo de resposta) |
| `CATEGORICAL` | Valores de categoria (ex: canal de origem, qualidade) |
| `BOOLEAN` | Verdadeiro/falso |
### Exemplo: score após execução
```python
from langfuse.langchain import CallbackHandler
from .dynamo import langfuse
langfuse_handler = CallbackHandler()
config = {"callbacks": [langfuse_handler], "tags": [base]}
final_state = agent.invoke(initial_state, config=config)
trace_id = langfuse_handler.get_trace_id()
if trace_id:
langfuse.score(
trace_id=trace_id,
name="canal_origem",
value="api",
data_type="CATEGORICAL",
)
langfuse.flush()
```
> Sempre chame `langfuse.flush()` depois de registrar scores para garantir o envio.
---
## Visualizando traces
Acesse a interface web do Langfuse no endereço configurado em `LANGFUSE_HOST`.
### Navegação útil
| O que ver | Onde ir |
|-----------|---------|
| Todas as execuções | Traces |
| Execuções por dashboard | Traces → filtrar por tag |
| Tokens por modelo | Dashboard → Usage |
| Erros e falhas | Traces → filtrar por status `ERROR` |
| Scores registrados | Trace individual → aba Scores |
### Referências
- [Documentação Oficial Langfuse](https://langfuse.com/docs)
- [Integração LangChain/LangGraph](https://langfuse.com/docs/integrations/langchain)
- [API de Scores](https://langfuse.com/docs/scores/custom)

256
docs/pulumi-guide.md Normal file
View File

@@ -0,0 +1,256 @@
# Guia Pulumi
## O que é Pulumi?
Pulumi é uma ferramenta de **Infrastructure as Code (IaC)**: em vez de clicar no console da AWS, você descreve os recursos em código Python (ou outras linguagens) e o Pulumi cria, atualiza e destrói esses recursos de forma controlada e reproduzível.
### Conceitos básicos
| Conceito | O que é |
|----------|---------|
| **Projeto** | O diretório com `Pulumi.yaml` — define o nome e a linguagem do projeto |
| **Stack** | Um ambiente independente do mesmo projeto (ex: `dev`, `prod`, `Inovyo`). Cada stack tem seu próprio estado e configuração |
| **Estado** | Arquivo que o Pulumi mantém mapeando cada recurso do código a um recurso real na AWS. É a "memória" do Pulumi |
| **`pulumi preview`** | Mostra o que **vai** mudar, sem aplicar nada — equivalente a um "dry run" |
| **`pulumi up`** | Aplica as mudanças: cria, atualiza ou remove recursos para corresponder ao código |
| **Output** | Valores exportados pelo código após o deploy (ex: a URL do ALB) |
### Como o Pulumi sabe o que mudar?
1. Você edita o código ou o arquivo de configuração
2. `pulumi preview` compara o código com o estado salvo e mostra o diff
3. `pulumi up` aplica o diff na AWS e atualiza o estado
---
## Pré-requisitos
- [Pulumi CLI](https://www.pulumi.com/docs/install/) instalado
- AWS CLI configurado com credenciais válidas para a conta
- Python 3.12+ e `venv`
---
## Estrutura do projeto
```
infra/ecs_alb/
├── Pulumi.yaml # Metadados do projeto (nome, runtime)
├── Pulumi.Inovyo.yaml # Configuração da stack "Inovyo" (região, VPC, ECS, env vars...)
├── __main__.py # Ponto de entrada — define ALB, cluster ECS e chama ecs.deploy_app()
├── conf.py # Lê as configurações do Pulumi e as expõe como variáveis Python
├── ecs.py # Cria task definition, service, target groups, listeners e auto-scaling
├── ecr.py # Referencia a imagem no ECR pelo digest ou tag
├── iam.py # Cria execution role e task role com políticas (Bedrock, DynamoDB, Secrets Manager...)
└── kms.py # Chave KMS (usada opcionalmente para criptografia de segredos)
```
---
## Setup inicial
```bash
cd infra/ecs_alb
# Criar e ativar virtualenv
python -m venv .venv
source .venv/bin/activate
# Instalar dependências
pip install -r requirements.txt
# Selecionar a stack
pulumi stack select <stack-name>
```
---
## Comandos principais
| Comando | O que faz |
|---------|-----------|
| `pulumi preview` | Mostra o que será criado/alterado/destruído **sem aplicar** |
| `pulumi preview --diff` | Igual ao anterior, com diff detalhado de cada recurso |
| `pulumi up` | Aplica as mudanças na AWS |
| `pulumi up --yes` | Aplica sem pedir confirmação interativa |
| `pulumi destroy` | Remove **todos** os recursos da stack da AWS |
| `pulumi stack ls` | Lista todas as stacks do projeto |
| `pulumi stack output` | Exibe os outputs exportados (ex: URL do ALB) |
| `pulumi refresh` | Sincroniza o estado Pulumi com o estado real da AWS |
Sempre prefira `pulumi preview` antes de `pulumi up` para revisar o impacto das mudanças.
---
## Arquivo de configuração: `Pulumi.Inovyo.yaml`
Toda a configuração da stack fica nesse arquivo. As seções principais:
### Rede
```yaml
app-ecs:network:
vpc_id: <vpc-id>
alb_internal: false
alb_subnet_ids: # Subnets públicas do ALB (mínimo 2, mesma região)
- <subnet-publica-1>
- <subnet-publica-2>
alb_allow_ingress_cidr: # IPs que podem acessar o ALB
- <ip-permitido>/32
ecs_subnet_ids: # Subnets privadas dos containers
- <subnet-privada-1>
- <subnet-privada-2>
```
### ECS / Container
```yaml
app-ecs:ecs:
- task_name: assisnte-analitico-db-dev
ecr_repo_name: assistente-analitico-db-dev
# Use ecr_image_digest OU ecr_image_tag (nunca os dois)
ecr_image_digest: sha256:<digest>
cpu: 256
memory: 512
desired_count: 1
use_load_balancer: true
auto_scaling:
min_capacity: 1
max_capacity: 3
target_value: 60.0 # % de CPU alvo para escalar
lb_configs:
- name: api
listener_port: 8000
target_port: 8000
container_port: 8000
- name: streamlit
listener_port: 8501
target_port: 8501
container_port: 8501
env_variables:
TABLE: poc_dnx_monthly_summary
REGION: us-east-1
```
---
## Fluxo de deploy de nova imagem
Após buildar e publicar uma nova imagem Docker (veja [deployment.md](deployment.md)):
**1. Obter o digest da nova imagem:**
```bash
aws ecr describe-images \
--repository-name assistente-analitico-db-dev \
--region us-east-1 \
--query 'sort_by(imageDetails, &imagePushedAt)[-1].imageDigest' \
--output text
```
**2. Atualizar o `Pulumi.Inovyo.yaml`:**
```yaml
ecr_image_digest: sha256:<novo-digest>
```
**3. Revisar e aplicar:**
```bash
cd infra/ecs_alb
pulumi preview --diff --stack <stack-name>
pulumi up --stack <stack-name>
```
---
## Adicionar ou alterar variáveis de ambiente do container
Edite a seção `env_variables` em `Pulumi.Inovyo.yaml` e rode `pulumi up`. O Pulumi atualizará a task definition e forçará o re-deploy do serviço ECS automaticamente.
```yaml
env_variables:
NOVA_VARIAVEL: valor
```
---
## Verificar o output após o deploy
```bash
pulumi stack output --stack <stack-name>
```
Retorna a URL do ALB, por exemplo:
```
url: http://alb-assistente-analitico-xxxxxxxx.us-east-1.elb.amazonaws.com
```
---
## Alterar capacidade de auto-scaling
Edite `auto_scaling` em `Pulumi.Inovyo.yaml`:
```yaml
auto_scaling:
min_capacity: 1 # mínimo de tasks rodando
max_capacity: 5 # máximo de tasks
target_value: 70.0 # escala quando CPU média ultrapassar 70%
```
Depois rode `pulumi up`.
---
## Permitir acesso ao ALB por Security Group
Por padrão, o acesso ao ALB é restrito por CIDR (`alb_allow_ingress_cidr`). Para permitir um security group no lugar de um IP, são necessárias duas alterações:
**1. Edite `__main__.py`** — troque `cidr_blocks` por `security_groups` na regra de ingress do ALB:
```python
ingress=[aws.ec2.SecurityGroupIngressArgs(
protocol="-1",
from_port=0,
to_port=0,
security_groups=config.network["alb_allow_ingress_sgs"],
)],
```
**2. Edite `Pulumi.Inovyo.yaml`** — substitua `alb_allow_ingress_cidr` por:
```yaml
app-ecs:network:
alb_allow_ingress_sgs:
- <security-group-id>
```
> Para manter ambos (IP e SG ao mesmo tempo), adicione os dois campos à mesma `SecurityGroupIngressArgs`, ou crie entradas separadas em `ingress`.
---
## Solução de problemas
**`pulumi up` falha com erro de estado inconsistente:**
```bash
pulumi refresh --stack <stack-name> # sincroniza estado com AWS
pulumi up --stack <stack-name>
```
**Recurso preso em estado de update:**
```bash
pulumi stack export --stack <stack-name> > state.json
# Editar state.json para remover o recurso problemático
pulumi stack import --stack <stack-name> < state.json
```
**Ver logs do container após o deploy:**
```bash
aws logs tail <nome-do-log-group> --follow --region us-east-1
```
**Verificar tasks rodando no ECS:**
```bash
aws ecs list-tasks --cluster <nome-do-cluster> --region us-east-1
```