DS DevShelfHub Projects · AI tools
Articles / Build a Python AI Agent in 10 Minutes With LangGraph, LangChain, and OpenAI

AI Engineering

Build a Python AI Agent in 10 Minutes With LangGraph

By DevShelfHub

The shortest path from zero to a working tool-calling AI agent in Python — five dependencies, three custom tools (write_json, read_json, generate_sample_users), one create_react_agent call, and a small driver loop for conversational use. Plus why docstrings and type hints decide tool quality, the temperature-0 rule, and the path from this 50-line demo to a real production agent.

Build a Python AI Agent in 10 Minutes With LangGraph

Introduction

The barrier to writing your first AI agent in Python has collapsed. Ten minutes, five dependencies, a single Python file — and you have an agent that can read inputs, decide when to call tools, execute those tools (real Python functions you wrote), and respond intelligently.

This is the absolute-shortest path from zero to a working tool-calling agent in 2026, using LangChain + LangGraph + OpenAI. The example agent generates and persists fake user data — trivial in itself, but it shows every concept you’ll use to build agents that handle real work later.

📚 Table of contents

  • What “agent” actually means here
  • Setup with uv (60 seconds)
  • The .env file and your OpenAI key
  • Defining your first tools
  • Why docstrings and type hints decide tool quality
  • Wiring up the LangGraph agent
  • The driver loop that makes it conversational
  • Running it
  • Common mistakes
  • FAQs

What “agent” actually means here

Distinguishing an AI agent from a chatbot: an agent has tools. It can decide, mid-conversation, that the right answer requires calling a function — reading a file, querying a database, hitting an API, sending an email — and it can execute that call and incorporate the result into its response. A chatbot only talks; an agent acts.

The pattern is called ReAct (Reason + Act). The model alternates between reasoning steps (“I need user data; the generate_sample_users tool exists; I’ll call it”) and action steps (actual tool calls). LangGraph’s create_react_agent wraps the whole loop into one function call.

Setup with uv (60 seconds)

Bash
mkdir 10-min-agent && cd 10-min-agent
uv init .
uv add langchain langgraph langchain-openai python-dotenv

Five dependencies. uv installs all of them in seconds. Open main.py in your editor (PyCharm, VS Code, Cursor — doesn’t matter).

The .env file and your OpenAI key

Create a .env file at the project root:

.env
OPENAI_API_KEY=sk-...

Get the key from platform.openai.com/api-keys. Inference for this demo costs pennies. Add .env to .gitignore immediately so it doesn’t land in a repo.

Defining your first tools

A tool is just a Python function decorated with @tool. The example agent will need three: generate fake users, write JSON to disk, read JSON from disk.

main.py
from dotenv import load_dotenv
load_dotenv()

import json, random
from langchain_core.tools import tool

@tool
def write_json(file_path: str, data: dict) -> str:
    """Write a Python dictionary as JSON to a file with pretty formatting.
    Use this when the user wants to save generated data persistently."""
    try:
        with open(file_path, "w") as f:
            json.dump(data, f, indent=2)
        return f"Successfully wrote data to {file_path}"
    except Exception as e:
        return f"Error writing file: {e}"

@tool
def read_json(file_path: str) -> str:
    """Read a JSON file and return its contents as a string.
    Use this when the user asks about data stored in a JSON file."""
    try:
        with open(file_path, "r") as f:
            return json.dumps(json.load(f), indent=2)
    except FileNotFoundError:
        return f"File {file_path} does not exist."
    except Exception as e:
        return f"Error reading file: {e}"

@tool
def generate_sample_users(
    first_names: list[str], last_names: list[str],
    domains: list[str], min_age: int = 18, max_age: int = 80,
) -> dict:
    """Generate sample user data with random names, emails, and ages.
    Use when the user asks for fake or sample users for testing."""
    users = []
    for first, last in zip(first_names, last_names):
        users.append({
            "name": f"{first} {last}",
            "email": f"{first.lower()}.{last.lower()}@{random.choice(domains)}",
            "age": random.randint(min_age, max_age),
        })
    return {"users": users}

Why docstrings and type hints decide tool quality

The LLM never sees your function’s code. It only sees:

  • The function name
  • The parameter names and their type annotations
  • The return type annotation
  • The docstring

All of those become part of the tool spec sent to the model. Vague docstrings produce vague tool use. Missing type hints mean the model guesses at parameter shapes. Misleading names mean the wrong tool gets called.

The fix: write docstrings as if they’re instructions to a junior developer. Lead with what the tool does and when to use it. Specify parameter types explicitly. Keep return types accurate. Two extra minutes per tool, dramatically better agent behaviour.

Wiring up the LangGraph agent

Three ingredients: a list of tools, an LLM, and a system prompt. LangGraph’s create_react_agent binds them together:

main.py (continued)
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

tools = [write_json, read_json, generate_sample_users]

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

system_message = (
    "You are a data generator agent. You can create sample user data and save "
    "it to JSON files. When the user asks for users, generate realistic names "
    "and ages. When asked to save, write to the file path they provide. When "
    "asked about saved data, read the file and answer from it."
)

agent = create_react_agent(llm, tools=tools, prompt=system_message)

Why temperature=0? Tool-calling agents benefit from deterministic tool selection. Temperature above 0 makes the model occasionally call the wrong tool for randomness reasons. Keep it at 0 unless you have a specific reason.

The driver loop that makes it conversational

The agent only does single-turn work in one invoke call. To make it conversational, keep a message history and append on each turn:

main.py (continued)
def run_agent(user_input: str, history: list) -> tuple[str, list]:
    messages = history + [{"role": "user", "content": user_input}]
    try:
        result = agent.invoke({"messages": messages}, {"recursion_limit": 50})
        reply = result["messages"][-1].content
        return reply, result["messages"]
    except Exception as e:
        return f"Agent error: {e}", history

def main():
    history = []
    print("Type 'quit' to exit.\n")
    while True:
        user_input = input("you > ").strip()
        if user_input.lower() in {"quit", "exit"}:
            break
        if not user_input:
            continue
        reply, history = run_agent(user_input, history)
        print(f"\nagent > {reply}\n")

if __name__ == "__main__":
    main()

The recursion_limit bounds how many tool-calling steps the agent can do per turn. 50 is generous; bump higher for more complex tasks, lower if you want to catch runaway loops.

Running it

Bash
uv run main.py

Sample interactions:

  • “Generate 5 random users with company.com emails.” — agent calls generate_sample_users, returns the list inline.
  • “Save those users to users.json.” — agent calls write_json, confirms.
  • “What’s the oldest user in users.json?” — agent calls read_json, parses, answers.

The agent reasons about which tool to use without you specifying. That’s the entire point.

❌ Common mistakes

  • Skipping type hints on tool functions. The agent can’t generate correct calls without them.
  • Vague docstrings. “Helps with files” is useless; “Read a JSON file and return its contents as a string” is correct.
  • Temperature above 0 on tool-calling models. Random tool choice is rarely what you want.
  • Committing .env to Git. Add it to .gitignore on commit one.
  • Hardcoding the OpenAI key in code. Read from env vars, even for personal projects.
  • Not handling errors inside tools. A tool that raises crashes the whole agent run; return error strings instead.
  • Forgetting recursion limits. Without one, a buggy agent can loop indefinitely on tool errors.

💡 Pro tips

  • Start with 2–3 tools. Adding more later is trivial; getting the first few right is what matters.
  • Use Pydantic models for complex tool inputs — LangChain accepts them and the agent gets richer parameter validation.
  • Switch to Claude (ChatAnthropic) or Gemini (ChatGoogleGenerativeAI) by changing one import. Pricing and tool-calling quality vary; try a few for your use case.
  • For production, swap the in-memory history list for LangGraph’s checkpointer with a Postgres backend. Multi-user sessions, durable state.
  • Wrap the agent in FastAPI when you want a real HTTP service. See the DevShelf deploy-FastAPI-AI-agent guide for the next step.
  • Use verbose=True on the agent to see every reasoning + tool-call step in the logs during development. Invaluable for debugging.

Conclusion

The minimum-viable AI agent is genuinely tiny. Five dependencies, three tool definitions, one create_react_agent call, a small driver loop. The pattern scales unchanged to real agents — you just write more interesting tools.

Next steps: swap the toy tools for something useful (database queries, API calls, file system operations), add persistent conversation memory, deploy as an API. Each one is a small extension of what you already have.

Build a Python AI Agent in 10 Minutes With LangGraph, LangChain, and OpenAI FAQ

Why LangGraph and not raw LangChain?

LangGraph is the modern way to compose agent loops — cleaner state management, better durability, easier to extend with checkpoints. LangChain still provides the underlying tool/model abstractions; LangGraph orchestrates them.

Can I use this with a local LLM (Ollama)?

Yes — swap ChatOpenAI for ChatOllama from langchain-ollama, point at localhost:11434, pick a tool-calling-capable model (Llama 3.1+, Qwen 2.5, Gemma 2). Smaller models have worse tool-calling reliability; benchmark before committing.

How does this differ from OpenAI’s Agent Builder?

Different shape. OpenAI Agent Builder is a visual flow editor; this is code. The code version is more flexible (any LLM provider, real tool execution), harder to share with non-developers. For most production agents, code wins on long-term maintainability.

How much does this cost to run?

With gpt-4o-mini at temperature 0, a typical interaction with 2–3 tool calls runs sub-cent. Even a heavy day of development costs $1–$5. For real production traffic with thousands of users, budget accordingly — the OpenAI Usage dashboard is your friend.

Can the agent call external APIs?

Absolutely. Define a tool that wraps requests.get or any other HTTP client. The pattern is identical to file I/O — the agent doesn’t care what the tool does internally, only what it claims to do via the docstring.

How do I make the agent multi-step?

LangGraph handles this automatically — if a tool call returns information that requires another tool call, the model decides and loops until it has the answer. Within one invoke call the recursion limit bounds the depth.