← All posts
ai python agents

Build Your First Agent Using AWS Strands and Ollama (For Free!)

April 7, 2026

Most agent tutorials start with “set up your OpenAI API key” or “configure your AWS credentials.” If you want to understand how agents work, how a model decides to call a tool, uses the result, and reasons toward a final answer, there’s a setup tax before you write a single line of code.

You don’t need a cloud account or an API key to build your first agent. Ollama runs open-source LLMs locally on your machine, and the Strands Agents SDK from AWS gives you a proper agent loop with tool calling built in. Together, you can build a fully functional agent on your laptop at zero cost. This post walks through the entire setup from scratch.

Install Ollama and Pull a Model

Ollama is a local large language model runtime that downloads open-source models and serves them over an HTTP API. Think of it as a lightweight server for running LLM on your own hardware.

Install it:

Terminal window
curl -fsSL https://ollama.com/install.sh | sh

Once installed, pull the model we’ll use throughout this post:

Terminal window
ollama pull qwen3.5:9b

This downloads qwen3.5:9b, a 9 billion parameter model from Alibaba with a nice 256K token context window. The default download is the Q4_K_M quantization at 6.6 GB, which runs comfortably on most consumer hardware with 8 GB of RAM or more. It also supports tool calling natively, which is what we need.

Verify it works by starting an interactive session:

Terminal window
ollama run qwen3.5:9b

Ask it something, confirm you get a response, then type /bye to exit. You won’t need the interactive mode again.

You’ll probably notice that the response is not quite as snappy is it is when using a frontier model like Claude or ChatGPT, but thats the tradeoff we get for running our own model locally in that we’re limited by our own hardware.

For our agent to be able to connect to our Ollama model, we’ll run the background local API service that Ollama provides which runs at http://localhost:11343. To start it, simply run:

Terminal window
ollama serve

Note: Any model tagged with tools on ollama.com/search will work with this tutorial. qwen3.5:9b is a solid default because it handles tool calling reliably at its size (my computer about died when I tried to run the 142GB qwen3:235b model).

Set Up the Python Project

Install uv if you don’t have it already:

Terminal window
curl -LsSf https://astral.sh/uv/install.sh | sh

Create the project and add the dependencies:

Terminal window
uv init my-first-agent
cd my-first-agent
uv add "strands-agents[ollama]" strands-agents-tools

uv init creates a pyproject.toml, a starter main.py, and a .python-version file. uv add installs the dependencies, creates a .venv, and generates a uv.lock lockfile, all in one step. No manual pip install or source .venv/bin/activate needed.

The two packages we’re installing:

Strands Agents is an open-source agent framework from AWS. It handles the agent loop: sending a prompt to a model, executing whatever tool calls the model generates, feeding those results back, and repeating until the model produces a final response. You define what your agent can do; Strands handles the orchestration.

Setting up Strands with Ollama

Create agent.py in the project root and add the following:

from strands import Agent
from strands.models.ollama import OllamaModel
model = OllamaModel(
host="http://localhost:11434",
model_id="qwen3.5:9b",
)
agent = Agent(model=model)
agent("What is the capital of Japan?")

Run it:

Terminal window
uv run agent.py

Walking through each piece:

This is an agent with no tools, so it can only respond with what the model already knows. Let’s now make our agent actually useful by giving it a tool that it can call.

Building your first Tool

Tools are simply functions your agent can decide to call. The model sees each tool’s name, description, and parameter schema. When it determines a tool is relevant to the user’s request, it generates a structured call. You don’t write logic to decide when tools run, that’s the model’s job based on the system prompt and session inputs you give it. The agent will reason about the inputs and determine which tools to call to fulfill the user’s request.

Let’s add a calculator tool to our agent.py:

from strands import Agent, tool
from strands.models.ollama import OllamaModel
@tool
def calculator(operation: str, a: float, b: float) -> str:
"""Perform a math operation on two numbers.
Args:
operation: The operation to perform. One of: add, subtract, multiply, divide.
a: The first number.
b: The second number.
"""
match operation:
case "add":
return f"{a} + {b} = {a + b}"
case "subtract":
return f"{a} - {b} = {a - b}"
case "multiply":
return f"{a} * {b} = {a * b}"
case "divide":
if b == 0:
return "Cannot divide by zero"
return f"{a} / {b} = {a / b}"
case _:
return f"Unknown operation: {operation}"
model = OllamaModel(
host="http://localhost:11434",
model_id="qwen3.5:9b",
)
agent = Agent(model=model, tools=[calculator])
agent("What is 42 multiplied by 17, and then subtract 100 from that result?")

A few things worth noting about the @tool decorator:

Run it again with uv run agent.py. You should see the agent call the calculator twice: once to multiply 42 by 17, and again to subtract 100 from the result. Then it produces a natural language answer incorporating both.

Adding a Second Tool

The real value of agents comes from combining multiple tools, and letting the model determine which ones to call and in what order.

strands-agents-tools includes a set of pre-built tools you can drop straight into an agent. Pull in current_time and add it alongside the calculator:

from strands import Agent, tool
from strands.models.ollama import OllamaModel
from strands_tools import current_time
@tool
def calculator(operation: str, a: float, b: float) -> str:
"""Perform a math operation on two numbers.
Args:
operation: The operation to perform. One of: add, subtract, multiply, divide.
a: The first number.
b: The second number.
"""
match operation:
case "add":
return f"{a} + {b} = {a + b}"
case "subtract":
return f"{a} - {b} = {a - b}"
case "multiply":
return f"{a} * {b} = {a * b}"
case "divide":
if b == 0:
return "Cannot divide by zero"
return f"{a} / {b} = {a / b}"
case _:
return f"Unknown operation: {operation}"
model = OllamaModel(
host="http://localhost:11434",
model_id="qwen3.5:9b",
)
agent = Agent(model=model, tools=[calculator, current_time])
agent("What time is it right now, and what is 365 multiplied by 24?")

The agent will call current_time to get the real time from your system and calculator to do the multiplication. Try rephrasing the prompt in different ways. The model should figure out which tools apply regardless of how you ask.

What’s Happening Under the Hood

The agent loop that Strands runs looks like this:

  1. Your prompt goes to the model along with descriptions of all available tools.
  2. The model either generates a direct response or produces one or more structured tool calls.
  3. Strands executes those tool calls and appends the results to the conversation as new messages.
  4. The model gets another turn. It can call more tools or produce a final response.
  5. This repeats until the model produces a response with no tool calls.

This is what distinguishes an agent from a plain chat completion, it is the Agentic Loop. The model isn’t just generating text in one pass. It’s deciding which capabilities to use, executing actions against real data, and building its final answer from those results.

One thing that matters here is model choice. Tool calling requires the model to output structured JSON in a specific format at the right moment in a conversation. Not every model does this reliably which is why we’re using qwen3.5:9b, a model that was explicitly trained to use tools.

Where to Go from Here

Now you’ve got a local agent development environment that costs nothing to run and doesn’t require any cloud setup. From here, the interesting work is building tools. Try writing a tool that reads a file, fetches a URL, or queries a local database. Anything your agent should be able to act on is a candidate for a tool. The @tool decorator keeps the surface area small.

Strands goes deeper than what this post covers. The documentation has good guides on conversation memory, hooks for validating or intercepting tool calls, multi-agent patterns, and MCP tool servers, which let you connect to any MCP-compatible tool provider without writing the tool yourself. The community tools package also has a growing set of ready-made tools beyond what we used here.

When you’re ready to move beyond local models, swapping providers is a one-line change. Strands supports Amazon Bedrock, OpenAI, Google, and others. The same agent code works across all of them, which makes local development with Ollama a natural first step.