FOSS · MIT—Open-source agent-swarm, the operating system for all your AI agents→
How-To/Build a Basic AI Agent From Scratch With Tools | desplega.ai

Build a Basic AI Agent From Scratch With Tools | desplega.ai

Learn how to build a basic AI agent from scratch with tools in Python. Set up an LLM, define callable tools, and run an autonomous reasoning loop.

AI agents are deceptively simple under the hood: a loop, an LLM, and a handful of callable tools. In this guide you'll build a working agent in pure Python — no frameworks, no LangChain abstractions — so you understand exactly how reasoning, tool-calling, and result-feeding fit together. Once the loop clicks, integrating richer protocols like MCP becomes much easier; see our MCP best practices guide for the next step after you've mastered the basics.

Install Dependencies

Create a new project folder and install the required libraries for your AI agent, including the OpenAI SDK and a small helper for environment variables. A virtual environment keeps these dependencies isolated from your system Python and makes the agent reproducible across machines.

mkdir my-ai-agent && cd my-ai-agent
python -m venv venv && source venv/bin/activate
pip install openai python-dotenv

Configure Your API Key

Create a .env file at the project root and add your OpenAI API key so the agent can authenticate against the LLM API at runtime. Never commit this file — add it to .gitignore immediately. Loading secrets from the environment keeps credentials out of your source code and makes it trivial to swap keys between dev, staging, and production.

# .env
OPENAI_API_KEY=sk-...

Define Your Tools

Tools are just Python functions paired with a JSON schema that tells the model what arguments they accept. The schema is critical: clear descriptions and parameter names dramatically improve the model's ability to pick the right tool. Start with a single tool — a mock weather lookup — and expand later.

import json

def get_weather(location: str) -> str:
    # Replace with a real weather API call
    return json.dumps({"location": location, "temperature": "72°F", "condition": "Sunny"})

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a given location.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "City and state, e.g. Austin, TX"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

Build the Agent Loop

The agent loop is the heart of the system. It sends the conversation to the LLM, inspects the response for tool calls, executes any requested functions, appends their results to the message history, and repeats. The loop terminates when the model returns plain text instead of another tool call — that's the final answer.

import os, json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

AVAILABLE_TOOLS = {"get_weather": get_weather}

def run_agent(user_message: str):
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        msg = response.choices[0].message
        messages.append(msg.model_dump(exclude_none=True))

        if msg.tool_calls:
            for tool_call in msg.tool_calls:
                fn_name = tool_call.function.name
                fn_args = json.loads(tool_call.function.arguments)
                result = AVAILABLE_TOOLS[fn_name](**fn_args)
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result
                })
        else:
            return msg.content

Run and Test the Agent

Call your agent function with a natural-language prompt and print the result. If everything is wired correctly, the model will detect that it needs weather data, invoke get_weather, receive the mocked response, and produce a clean final answer like "It's currently 72°F and sunny in Austin, TX."

if __name__ == "__main__":
    answer = run_agent("What is the weather like in Austin, TX?")
    print(answer)

Add More Tools to Extend Capabilities

Scale the agent by writing additional Python functions, registering them in the tools list, and adding them to the dispatch map. Below we add a calculator. With multiple tools available, the model autonomously decides which one (or which combination) to call based on the user's prompt — that's the magic of agentic reasoning.

import ast
import operator

OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.Pow: operator.pow,
    ast.USub: operator.neg,
}

def evaluate_math(node):
    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in OPERATORS:
        return OPERATORS[type(node.op)](evaluate_math(node.left), evaluate_math(node.right))
    if isinstance(node, ast.UnaryOp) and type(node.op) in OPERATORS:
        return OPERATORS[type(node.op)](evaluate_math(node.operand))
    raise ValueError("Only basic arithmetic expressions are allowed")

def calculator(expression: str) -> str:
    tree = ast.parse(expression, mode="eval")
    return str(evaluate_math(tree.body))

# Append to tools list
tools.append({
    "type": "function",
    "function": {
        "name": "calculator",
        "description": "Evaluate a mathematical expression and return the result.",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {"type": "string", "description": "A valid Python math expression, e.g. '2 ** 10'"}
            },
            "required": ["expression"]
        }
    }
})

# Register in dispatch map
AVAILABLE_TOOLS["calculator"] = calculator

Write End-to-End Tests With desplega.ai

Agents are notoriously tricky to test: outputs are non-deterministic, tool selection drifts as you tweak prompts, and silent regressions creep in. Use desplega.ai to write automated E2E tests that simulate real user prompts, assert that the expected tool calls fire, and verify that final answers contain the right facts. Whenever you add a tool or change the loop, your test suite catches behavioural regressions before they reach production. Pair this with the patterns in our Claude Code MCP guide to bring the same rigour to MCP-powered agents.