# bedrock-agent-starter

Agente Bedrock com tools, memória, evals e Terraform — do clone ao chat em 30 minutos.

- URL: https://fernando.moretes.com/open-source/bedrock-agent-starter

- Markdown: https://fernando.moretes.com/open-source/bedrock-agent-starter/guide.md?lang=pt

- GitHub: https://github.com/fernando-moretes/app-bedrock-agent-starter

- Homepage: https://fernando-moretes.github.io/app-bedrock-agent-starter/

- Language: Python

- Topics: ai, ai-agents, aws, bedrock, github-actions, lambda, moretes, portfolio, solution-architecture, terraform

- Stars: 0

- Forks: 0

- Updated: 2026-09-08T20:37:15Z

---

Um template Python para agentes na Amazon Bedrock que já vem com o que o console não entrega: registro de tools, memória multi-turno, logs estruturados, harness de evals e Terraform para Lambda + API Gateway — para você forkar, apontar o `AWS_PROFILE` e ter um agente respondendo no terminal antes de decidir qualquer coisa de arquitetura.

## Por que este repositório existe

A Bedrock resolve bem um problema: chamar um modelo. O que ela não resolve é o resto — e o resto é o que consome semanas quando você tenta colocar um agente em produção. Quem já tentou sabe a lista: registro de tools com schema JSON, memória entre turnos, log que dá para consultar no CloudWatch às 2h da manhã, um conjunto de casos para provar que a mudança no prompt não regrediu nada, IaC, tratamento de erro, fallback de modelo.

Eu escrevi este starter depois de montar essa mesma base mais de uma vez em projetos diferentes. Cada vez a estrutura era 80% igual e os 20% restantes eram a decisão de negócio de verdade. O template cristaliza os 80%.

A pergunta que ele responde não é "como chamo o Claude pela Bedrock?" — é "o que preciso ter em volta da chamada para confiar nela em produção?". Por isso o loop do agente usa a **Converse API**, que é estável entre Claude, Nova, Llama e Mistral e trata tool-use como cidadão de primeira classe. Trocar de modelo é mudar `BEDROCK_MODEL_ID`, não reescrever o parser de resposta.

**Opinativo onde deve, substituível onde não deve:** o registro de tools, a observabilidade e o harness de evals são opiniões fortes. A memória (in-memory local, DynamoDB em produção) e o backend do Terraform são pontos de troca deliberados.

## O que vem na caixa

- **Loop do agente sobre a Converse API:** o mesmo código roda Claude, Nova, Llama ou Mistral; tool-use nativo, sem parser artesanal.
- **Registro de tools com três exemplos funcionais:** `calculator`, `get_time` e um stub de `web_search`; adicionar a sua é um decorator e uma função.
- **Memória plugável:** in-memory para desenvolvimento local, tabela DynamoDB de sessões em produção — mesma interface.
- **Observabilidade estruturada:** uma linha JSON por turno com `session_id`, `turn`, `model_id`, tokens e `duration_ms`; métricas EMF no namespace `BedrockAgent`.
- **Harness de evals em pytest:** replay de `tests/evals/golden.jsonl` verificando substrings e tool-calls esperados — falha em regressão.
- **Terraform + CI + docs:** Lambda Python 3.12, API Gateway HTTP API, DynamoDB, IAM, log group; ruff + mypy + pytest no CI; MkDocs Material no GitHub Pages.

## Instalar e conversar com o agente

1. **Clone e crie o ambiente** — `git clone https://github.com/fernando-moretes/app-bedrock-agent-starter.git && cd app-bedrock-agent-starter && python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"`. Python 3.11+ é obrigatório; o extra `dev` traz ruff, mypy e pytest.

2. **Aponte para a conta e o modelo** — `export AWS_REGION=us-east-1` e `export BEDROCK_MODEL_ID=anthropic.claude-3-5-sonnet-20241022-v2:0` (ou o modelo que você tem habilitado). Use `AWS_PROFILE` em vez de chave estática — confira com `aws sts get-caller-identity` que é a conta certa antes do primeiro turno.

3. **Converse no terminal** — `agent chat`. Pergunte "what time is it in Tokyo, and what is (123 * 456) - 789?" e veja as duas tools serem chamadas na sequência, com o resultado de cada uma impresso antes da resposta final.

4. **Rode os evals antes de mudar qualquer prompt** — `pytest tests/evals/`. O runner replaya `golden.jsonl` e compara substring e tool-call esperados com a saída real. Faça isso passar uma vez limpo para ter linha de base — sem linha de base, regressão de prompt é invisível.

5. **Suba para a AWS quando estiver pronto** — `cd terraform && terraform init && terraform apply -var="project=my-agent"`. Provisiona Lambda, API Gateway HTTP API, tabela DynamoDB de sessões, roles IAM e log group. Backend de state, tags, naming e permission boundaries ficam por sua conta — de propósito.

_Adicionar uma tool: um decorator, uma função. O Pydantic infere o schema JSON das anotações e o agente a registra sozinho._

```python
# src/agent/tools.py
from agent.tools import tool

@tool(description="Translate text between languages using a deterministic table.")
def translate(text: str, source_lang: str, target_lang: str) -> str:
    ...
    return translated
```

## Como um turno atravessa o sistema

O mesmo núcleo (`src/agent/`) serve o CLI local e o handler Lambda; só a memória e a entrada mudam.

### 💻 Local — dev loop

- CLI `agent chat` src/agent (frontend)
- Memória in-memory sessão por processo (data)
- pytest tests/evals golden.jsonl (ci)

### 🟧 AWS — runtime

- API Gateway HTTP API (edge)
- Lambda Python 3.12 handler + agent loop (compute)
- DynamoDB tabela de sessões (data)
- Bedrock Converse API Claude / Nova / Llama (ai)
- CloudWatch JSON logs + EMF `BedrockAgent` (security)

### 🔧 Agent core — tools

- Tool registry @tool + Pydantic schema (compute)
- calculator · get_time web_search (stub) (external)

### Fluxos

- user -> cli: prompt no terminal
- user -> apigw: POST /chat
- apigw -> lambda
- cli -> bedrock: Converse (messages + tool specs)
- lambda -> bedrock: Converse (messages + tool specs)
- bedrock -> registry: toolUse → dispatch
- registry -> tools
- tools -> bedrock: toolResult
- cli -> mem-local: histórico do turno
- lambda -> ddb: histórico por session_id
- lambda -> cw: 1 linha JSON + EMF por turno
- evals -> cli: replay do golden set

## Como funciona por dentro

**O loop:** cada turno monta a lista de mensagens a partir da memória, anexa os specs das tools registradas e chama `Converse`. Se a resposta contém um bloco `toolUse`, o registro despacha a função Python correspondente, devolve um `toolResult` e chama o modelo de novo — até vir uma resposta final sem tool-use. É o mesmo ciclo que você faria à mão, só que com um teto de iterações e tratamento de erro no lugar certo.

**As tools:** o decorator `@tool` lê as anotações de tipo da função e pede ao Pydantic o schema JSON que a Converse API espera. Isso elimina a classe de bug mais comum em agentes caseiros — schema declarado à mão que diverge da assinatura real. As três tools que vêm no repo existem para provar o caminho, não para serem usadas como estão: `web_search` é um stub e você troca por Tavily, Brave ou o AgentCore Gateway conforme o caso.

**A memória:** a interface é uma só; a implementação in-memory serve para o CLI e a DynamoDB serve para o Lambda, onde cada invocação é sem estado e o histórico precisa viver fora do processo, chaveado por `session_id`.

**A observabilidade:** cada turno vira uma linha JSON com `session_id`, `turn`, `model_id`, `input_tokens`, `output_tokens`, `tool_calls` e `duration_ms`. Em paralelo, métricas EMF (`Turns`, `InputTokens`, `OutputTokens`, `Duration`, `ToolErrors`) vão para o namespace `BedrockAgent` no CloudWatch sem chamada extra de API — o formato embutido faz o CloudWatch extrair a métrica do próprio log. É o que permite um alarme em `ToolErrors > 0` no primeiro dia, e um gráfico de custo por sessão no segundo.

> **O que o Terraform deixa de fora — de propósito:** O `terraform apply` funciona com state local e sem tags, o que é ótimo para experimentar e péssimo para produção. Antes de subir em conta compartilhada: configure um backend remoto (S3 + lock), defina a convenção de nomes, aplique tags de custo e coloque um permission boundary na role do Lambda. Também confira o `BEDROCK_MODEL_ID` — o default do README é um id de Sonnet 3.5 de 2024; use o id que está habilitado na sua região e revise o custo por token antes de abrir o endpoint para tráfego real.

## Evals e CI: o que separa um demo de um agente

O custo de um agente não está na primeira versão que funciona — está em cada mudança de prompt, de modelo ou de tool feita depois, sem saber o que quebrou. Por isso o harness de evals é parte do template e não um apêndice.

`tests/evals/golden.jsonl` guarda prompts com a saída esperada: substrings que devem aparecer na resposta e tools que devem ter sido chamadas. `pytest tests/evals/` replaya cada linha contra o agente real e falha se algum caso regrediu. Não é benchmark de qualidade absoluta; é detector de regressão, e detector de regressão é o que você precisa para trocar `claude-3-5-sonnet` por Nova com alguma confiança.

O CI roda ruff, mypy e pytest a cada push. O workflow de docs publica o MkDocs Material no GitHub Pages. A landing em `frontend/` é HTML estático sem dependência — sobe na Vercel sem build.

O repositório também carrega automação de higiene DevSecOps (checks de dependência, Conventional Commits) que vem do meu portfólio como um todo; ela é útil, mas não é o que faz o agente funcionar — pode remover sem perder nada do núcleo.

## Perguntas frequentes

### Preciso usar Claude?

Não. O loop usa a Converse API, então qualquer modelo da Bedrock com suporte a tool-use funciona — Claude, Nova, Llama, Mistral. Mude `BEDROCK_MODEL_ID` e rode os evals para ver o que mudou.

### Por que não Bedrock Agents (o serviço gerenciado) ou AgentCore?

Porque aqui o objetivo é você ser dono do loop. Bedrock Agents e AgentCore são boas escolhas quando o runtime gerenciado, memória e guardrails num único plano de controle valem mais que o controle fino — este starter é para quando você quer ver e testar cada iteração, e migrar depois com o registro de tools já pronto.

### Quanto custa rodar?

Localmente, só tokens da Bedrock. Na AWS, Lambda e API Gateway HTTP API e DynamoDB on-demand ficam próximos de zero em baixo volume; o custo dominante é sempre token de modelo. As métricas `InputTokens`/`OutputTokens` existem justamente para você medir antes de escalar.

### A tool `web_search` funciona?

É um stub. Está lá para mostrar como uma tool com efeito externo se encaixa no registro. Troque a implementação pela API de busca que você já paga.

## Referências

- [fernando-moretes/app-bedrock-agent-starter (GitHub)](https://github.com/fernando-moretes/app-bedrock-agent-starter)
- [Project docs (MkDocs, GitHub Pages)](https://fernando-moretes.github.io/app-bedrock-agent-starter/)
- [Amazon Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html)
- [CloudWatch Embedded Metric Format (EMF)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html)

## Quando usar

Use este starter quando: você quer ser dono do loop do agente e testar cada mudança com evals, precisa de um agente com tools próprias em Lambda sem depender de runtime gerenciado, e o time já opera Terraform e CloudWatch. Não use quando: a equipe prefere não manter código de orquestração — Bedrock Agents ou AgentCore Runtime vão custar menos em manutenção ao longo de anos — ou quando o caso é uma chamada única sem tool-use, onde um `converse()` direto basta. Em qualquer dos casos, o registro de tools com schema inferido e o golden set de evals são as duas peças que vale levar junto.

## Links

- [GitHub repository](https://github.com/fernando-moretes/app-bedrock-agent-starter)
- [Homepage](https://fernando-moretes.github.io/app-bedrock-agent-starter/)
