bedrock-agent-starter
Bedrock agent with tools, memory, evals and Terraform — clone to chat in 30 minutes.
git clone https://github.com/fernando-moretes/app-bedrock-agent-starter.gitListen to guide
generated on playGenerated only on first play
Powered by Amazon Polly + OmniVoice
A Python template for agents on Amazon Bedrock that ships what the console doesn't: a tool registry, multi-turn memory, structured logs, an eval harness and Terraform for Lambda + API Gateway — fork it, point AWS_PROFILE at your account and have an agent answering in the terminal before you make a single architecture decision.
Why this repository exists
Bedrock solves one problem well: calling a model. What it doesn't solve is everything else — and everything else is what eats weeks when you try to put an agent in production. Anyone who has tried knows the list: a tool registry with JSON schemas, memory across turns, logs you can actually query in CloudWatch at 2 AM, a set of cases proving your prompt change didn't regress anything, IaC, error handling, model fallbacks.
I wrote this starter after building that same foundation more than once across different projects. Each time the structure was 80% identical and the remaining 20% was the actual business decision. The template crystallises the 80%.
The question it answers isn't "how do I call Claude through Bedrock?" — it's "what do I need around that call to trust it in production?". That's why the agent loop uses the Converse API, which is stable across Claude, Nova, Llama and Mistral and treats tool-use as a first-class citizen. Switching models means changing BEDROCK_MODEL_ID, not rewriting the response parser.
Opinionated where it should be, replaceable where it shouldn't: the tool registry, observability and eval harness are strong opinions. Memory (in-memory locally, DynamoDB in production) and the Terraform backend are deliberate swap points.
What's in the box
calculator, get_time and a web_search stub; adding yours is one decorator and one function.session_id, turn, model_id, tokens and duration_ms; EMF metrics in the BedrockAgent namespace.tests/evals/golden.jsonl checking expected substrings and tool-calls — fails on regression.Install and talk to the agent
- 1
Clone and create the environment
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+ is required; thedevextra pulls in ruff, mypy and pytest. - 2
Point at the account and the model
export AWS_REGION=us-east-1andexport BEDROCK_MODEL_ID=anthropic.claude-3-5-sonnet-20241022-v2:0(or whichever model you have enabled). UseAWS_PROFILEinstead of a static key — check withaws sts get-caller-identitythat it's the right account before the first turn. - 3
Chat in the terminal
agent chat. Ask "what time is it in Tokyo, and what is (123 * 456) - 789?" and watch both tools get called in sequence, each result printed before the final answer. - 4
Run the evals before touching any prompt
pytest tests/evals/. The runner replaysgolden.jsonland compares expected substrings and tool-calls against the actual output. Get it passing clean once to have a baseline — without one, prompt regressions are invisible. - 5
Deploy to AWS when you're ready
cd terraform && terraform init && terraform apply -var="project=my-agent". Provisions Lambda, API Gateway HTTP API, a DynamoDB sessions table, IAM roles and a log group. State backend, tags, naming and permission boundaries are yours — on purpose.
# 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 translatedHow a turn moves through the system
The same core (src/agent/) serves the local CLI and the Lambda handler; only memory and the entry point change.
- CLI `agent chat` · src/agent
- Memória in-memory · sessão por processo
- pytest tests/evals · golden.jsonl
- API Gateway · HTTP API
- Lambda Python 3.12 · handler + agent loop
- DynamoDB · tabela de sessões
- Bedrock Converse API · Claude / Nova / Llama
- CloudWatch · JSON logs + EMF `BedrockAgent`
- Tool registry · @tool + Pydantic schema
- calculator · get_time · web_search (stub)
How it works under the hood
The loop: each turn builds the message list from memory, attaches the specs of the registered tools and calls Converse. If the response contains a toolUse block, the registry dispatches the matching Python function, returns a toolResult and calls the model again — until a final answer arrives with no tool-use. It's the same cycle you'd write by hand, but with an iteration cap and error handling in the right place.
The tools: the @tool decorator reads the function's type annotations and asks Pydantic for the JSON schema the Converse API expects. That removes the most common bug class in home-grown agents — a hand-declared schema drifting from the real signature. The three tools that ship in the repo exist to prove the path, not to be used as-is: web_search is a stub and you swap it for Tavily, Brave or the AgentCore Gateway depending on the case.
Memory: one interface; the in-memory implementation serves the CLI and DynamoDB serves Lambda, where every invocation is stateless and history has to live outside the process, keyed by session_id.
Observability: every turn becomes a JSON line with session_id, turn, model_id, input_tokens, output_tokens, tool_calls and duration_ms. In parallel, EMF metrics (Turns, InputTokens, OutputTokens, Duration, ToolErrors) land in the BedrockAgent CloudWatch namespace with no extra API call — the embedded format has CloudWatch extract the metric from the log itself. That's what lets you put an alarm on ToolErrors > 0 on day one, and a cost-per-session graph on day two.
What the Terraform leaves out — on purpose
terraform apply works with local state and no tags, which is great for experimenting and terrible for production. Before deploying into a shared account: configure a remote backend (S3 + lock), settle the naming convention, apply cost tags and put a permission boundary on the Lambda role. Also check BEDROCK_MODEL_ID — the README default is a 2024 Sonnet 3.5 id; use the id enabled in your region and review the per-token cost before opening the endpoint to real traffic.
Evals and CI: what separates a demo from an agent
The cost of an agent isn't in the first version that works — it's in every prompt, model or tool change made afterwards without knowing what broke. That's why the eval harness is part of the template and not an appendix.
tests/evals/golden.jsonl holds prompts with expected output: substrings that must appear in the answer and tools that must have been called. pytest tests/evals/ replays each line against the real agent and fails if any case regressed. It's not an absolute quality benchmark; it's a regression detector, and a regression detector is what you need to swap claude-3-5-sonnet for Nova with any confidence.
CI runs ruff, mypy and pytest on every push. The docs workflow publishes MkDocs Material to GitHub Pages. The landing in frontend/ is dependency-free static HTML — it deploys to Vercel with no build step.
The repository also carries DevSecOps hygiene automation (dependency checks, Conventional Commits) inherited from my portfolio as a whole; it's useful, but it isn't what makes the agent work — you can strip it without losing anything from the core.
Frequently asked questions
Do I have to use Claude?
No. The loop uses the Converse API, so any Bedrock model with tool-use support works — Claude, Nova, Llama, Mistral. Change BEDROCK_MODEL_ID and run the evals to see what shifted.
Why not Bedrock Agents (the managed service) or AgentCore?
Because the goal here is for you to own the loop. Bedrock Agents and AgentCore are good choices when a managed runtime, memory and guardrails in a single control plane are worth more than fine-grained control — this starter is for when you want to see and test every iteration, and migrate later with the tool registry already in place.
What does it cost to run?
Locally, Bedrock tokens only. On AWS, Lambda, API Gateway HTTP API and on-demand DynamoDB stay near zero at low volume; the dominant cost is always model tokens. The InputTokens/OutputTokens metrics exist precisely so you measure before you scale.
Does the `web_search` tool work?
It's a stub. It's there to show how a tool with an external side effect fits the registry. Swap the implementation for whichever search API you already pay for.
References
When to use it
Use this starter when: you want to own the agent loop and test every change with evals, you need an agent with your own tools on Lambda without depending on a managed runtime, and the team already operates Terraform and CloudWatch. Don't use it when: the team would rather not maintain orchestration code — Bedrock Agents or AgentCore Runtime will cost less in maintenance over years — or when the case is a single call with no tool-use, where a direct converse() is enough. Either way, the tool registry with inferred schemas and the golden eval set are the two pieces worth taking with you.
Architecture, AWS, AI and market deep dives — straight to your inbox. Free.
No spam · unsubscribe anytime