{"slug": "build-your-first-ai-agent-on-amazon-bedrock-step-by-step", "title": "Build your first AI agent on Amazon Bedrock, step by step", "summary": "A developer walkthrough shows how to build a minimal AI agent in Python on Amazon Bedrock by hand, wiring two ordinary functions — one listing S3 buckets, one counting objects in a bucket — to a foundation model through boto3 and a manual tool-calling loop. The guide deliberately skips Bedrock knowledge bases and the managed Agents for Amazon Bedrock service so readers see the underlying request-and-respond loop, noting the script leaves no vector store, index, endpoint or server running after it exits.", "body_md": "Your first AI agent on AWS: two tools and a loop you wrote yourself.\n\nBy the end of this walkthrough you'll have a Python script that answers a question about your own AWS account by deciding, on its own, which of two functions to call. That's an AI agent: a model that can ask your code to do something, and a loop that keeps handing it the results until it has an answer.\n\nTwo things we're deliberately not using. A [Bedrock knowledge base](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html), which is the piece that lets a model search your own documents. And [Agents for Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html), the managed service that builds an agent for you. Both are useful later. Neither shows you the loop, and the loop is the part worth understanding.\n\n[Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) is the AWS service that gives you one way to call several foundation models, which are the large pretrained models you send text to and get text back from.\n\n`boto3`, the AWS library for Python.\nAmazon Bedrock is pay per use, so this walkthrough spends money every time it calls a model. What it costs depends on the model and on how much text goes in and comes back. Prices change, so we won't put a number in this article: read the [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) before you run anything.\n\nThe good news is what this doesn't create. No vector store, no index, no endpoint, no server. Nothing is left running when the script exits, and there's nothing to remember to delete. That's the main reason we're writing the loop by hand instead of standing up a knowledge base.\n\nAnd if you'd rather spend nothing: the two tool functions in step 3 are ordinary Python and run fine with no model anywhere near them. Read the loop, run the tools by hand, and you'll still see what an agent does.\n\nThat's it. The model never runs anything itself. It asks, and your code decides whether to oblige.\n\nOpen Amazon Bedrock in the AWS console and pick your Region first, top right. A Region is the part of the world where AWS runs the service, and [which models you can use depends on it](https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html).\n\nNow find model access and [request access to one model](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). Access is per model and per Region, and you ask for it before your first call.\n\nThen open the model list and copy the model ID of the model you got access to. A model ID is the string you name in your code to say which model you want. We're not printing one here, because your console is the truth about what you have.\n\nWhat you should see: one model with access granted, and a model ID on your clipboard.\n\n```\npip install boto3\n```\n\n`boto3` is the AWS SDK for Python: the library that turns a Python function call into an AWS request. It needs credentials, and if you've never set those up, the [boto3 credentials guide](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) lists every place it looks. No separate check needed, because the first tool you write does that job for free.\n\nA tool is just a function. Nothing about it is special, and the model can't see inside it.\n\nMake a file called `agent.py`:\n\n``` python\nimport boto3\n\nREGION = \"paste-your-region-here\"\nMODEL_ID = \"paste-your-model-id-here\"\n\ns3 = boto3.client(\"s3\", region_name=REGION)\nbedrock = boto3.client(\"bedrock-runtime\", region_name=REGION)\n\ndef list_buckets():\n    \"\"\"Names of the S3 buckets in this account.\"\"\"\n    response = s3.list_buckets()\n    return {\"buckets\": [bucket[\"Name\"] for bucket in response[\"Buckets\"]]}\n\ndef count_objects(bucket_name):\n    \"\"\"How many objects one listing call finds in a bucket.\"\"\"\n    response = s3.list_objects_v2(Bucket=bucket_name)\n    return {\"bucket\": bucket_name, \"objects\": len(response.get(\"Contents\", []))}\n```\n\nA bucket is the container your files live in on Amazon S3, and each file you put in one is called an object.\n\nTry the first tool now, before any model is involved. Add `print(list_buckets())` at the bottom and run `python agent.py`. Names coming back means your credentials work, and it cost nothing, because listing S3 is not a Bedrock call. Take the print line out again.\n\nTwo honest notes on `count_objects`. It counts what one listing call returns, and a bucket with a lot of objects comes back in pages, which the [boto3 reference for list_objects_v2](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_objects_v2.html) shows how to walk through. One call is enough today. And if you have no buckets, make one empty bucket in the S3 console so the second tool has something to count, then delete it after.\n\nThe model can't read your Python. It only knows what you tell it, so every tool gets a name, a description, and a schema: a small description of the arguments it takes, written in JSON, a plain text format for structured data.\n\nHere's the catch. That description is the only thing the model has to go on when it decides whether this is the right tool, so a vague one gets ignored.\n\n```\nTOOL_CONFIG = {\n    \"tools\": [\n        {\n            \"toolSpec\": {\n                \"name\": \"list_buckets\",\n                \"description\": \"List the names of the S3 buckets in this AWS account.\",\n                \"inputSchema\": {\n                    \"json\": {\"type\": \"object\", \"properties\": {}, \"required\": []}\n                },\n            }\n        },\n        {\n            \"toolSpec\": {\n                \"name\": \"count_objects\",\n                \"description\": \"Count the objects in one S3 bucket. Needs the bucket name.\",\n                \"inputSchema\": {\n                    \"json\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"bucket_name\": {\n                                \"type\": \"string\",\n                                \"description\": \"The name of the bucket to count.\",\n                            }\n                        },\n                        \"required\": [\"bucket_name\"],\n                    }\n                },\n            }\n        },\n    ]\n}\n\nTOOLS = {\"list_buckets\": list_buckets, \"count_objects\": count_objects}\n\ndef run_tool(name, arguments):\n    if name not in TOOLS:\n        return {\"error\": f\"There is no tool called {name}.\"}\n    try:\n        return TOOLS[name](**arguments)\n    except Exception as problem:\n        return {\"error\": str(problem)}\n```\n\n`run_tool` hands errors back to the model instead of crashing, and that's deliberate. An agent told \"that bucket doesn't exist\" can try another. An agent whose script has died can't.\n\nThis is the whole agent, and it's shorter than the tool descriptions.\n\n``` python\nMAX_TURNS = 5\n\ndef ask(question):\n    messages = [{\"role\": \"user\", \"content\": [{\"text\": question}]}]\n\n    for turn in range(MAX_TURNS):\n        response = bedrock.converse(\n            modelId=MODEL_ID,\n            messages=messages,\n            toolConfig=TOOL_CONFIG,\n        )\n        reply = response[\"output\"][\"message\"]\n        messages.append(reply)\n\n        if response[\"stopReason\"] != \"tool_use\":\n            return \"\\n\".join(b[\"text\"] for b in reply[\"content\"] if \"text\" in b)\n\n        results = []\n        for block in reply[\"content\"]:\n            if \"toolUse\" not in block:\n                continue\n            use = block[\"toolUse\"]\n            print(f\"  calling {use['name']} with {use['input']}\")\n            results.append(\n                {\n                    \"toolResult\": {\n                        \"toolUseId\": use[\"toolUseId\"],\n                        \"content\": [{\"json\": run_tool(use[\"name\"], use[\"input\"])}],\n                    }\n                }\n            )\n        messages.append({\"role\": \"user\", \"content\": results})\n\n    return \"Hit the turn limit without finishing.\"\n\nif __name__ == \"__main__\":\n    print(ask(\"What S3 buckets do I have, and how many objects are in each one?\"))\n```\n\nThree things in there are worth reading twice. [`converse`](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html) is the Bedrock call for a back-and-forth conversation with a model, and `toolConfig` is how the tools come along for the ride.\n\n`messages` is the whole conversation, and you keep appending to it. The model has no memory between calls. That list is the memory.\n\n`stopReason` is the model telling you why it stopped talking. [`tool_use` means it wants something run](https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use-client-side.html). Anything else means it's answering you. The `toolUseId` matches a result to the request it answers, so send it back exactly as it arrived. The [boto3 reference for converse](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html) has the full shape.\n\n`MAX_TURNS` is a rail, not a detail. A loop that can call a paid service forever is worth a hard stop.\n\n```\npython agent.py\n```\n\nWhat you should see: a `calling list_buckets with {}` line, then a `calling count_objects` line for each bucket you own, then a sentence listing your buckets with their counts.\n\nRead that order again, because it's the point of the whole article. The model asked for the names first, because it couldn't count a bucket without knowing what it was called. You didn't write that order anywhere. It came out of the loop.\n\nIf you own a lot of buckets, the script may run out of turns and say so. That's the rail doing its job. Raise `MAX_TURNS` once you've read the pricing page and decided you're happy.\n\nIt works because of a split worth keeping for life: the model decides, and your code acts. The model never touched your AWS account. It asked for `count_objects` with a bucket name, and your Python chose to run it. Every permission that mattered is still an IAM permission, short for Identity and Access Management, the AWS service that decides who can do what. Your agent can only do what the credentials running the script may do. So a tool that reads is a small decision, and a tool that deletes is a much bigger one.\n\nSo where does a Bedrock knowledge base come in? Different job. A knowledge base looks answers up in your own documents and hands the text to the model as context. That's retrieval. What you built is action. Real systems use both, and the loop is the better one to learn first, because a knowledge base is mostly something you configure.\n\nAnd Agents for Amazon Bedrock is the managed version of the loop you just wrote by hand. Now it reads like a form instead of a mystery.\n\n`REGION` matches the Region where you asked for it, then check your credentials are allowed to call Bedrock.`messages` is the wrong shape. The usual cause is appending the tool results without the model's own reply in front of them. The conversation has to alternate, so `reply` goes in before the results do.\nAdd a third tool tonight. Make it read-only, in any AWS service you're curious about, with a description that says plainly what it returns. Then ask a question that needs two of your three tools together, and watch which ones it reaches for.\n\nWhich tool did you add, and did it pick the one you expected? Tell us in the comments.\n\nEvery statement about how AWS behaves is linked to the AWS or boto3 documentation page it comes from, right where we make it.\n\nAWS changes things. If a step doesn't match your screen, the documentation is right and we're out of date. Tell us and we'll fix the article.", "url": "https://wpnews.pro/news/build-your-first-ai-agent-on-amazon-bedrock-step-by-step", "canonical_source": "https://dev.to/cloudandai/build-your-first-ai-agent-on-amazon-bedrock-step-by-step-3o67", "published_at": "2026-11-14 14:17:22+00:00", "updated_at": "2026-11-14 14:21:14.660508+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "artificial-intelligence", "large-language-models", "ai-infrastructure"], "entities": ["Amazon Bedrock", "AWS", "boto3", "Amazon S3", "Agents for Amazon Bedrock"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/build-your-first-ai-agent-on-amazon-bedrock-step-by-step", "markdown": "https://wpnews.pro/news/build-your-first-ai-agent-on-amazon-bedrock-step-by-step.md", "text": "https://wpnews.pro/news/build-your-first-ai-agent-on-amazon-bedrock-step-by-step.txt", "jsonld": "https://wpnews.pro/news/build-your-first-ai-agent-on-amazon-bedrock-step-by-step.jsonld"}}