5 min read

Building an AI Agent from Scratch

The Digital Intern pattern: learn how to implement a ReAct agent in Python that can think, use tools, and solve multi-step problems without any framework.

Building an AI Agent from Scratch

The “Digital Intern” Pattern

Imagine you’ve just hired a new intern, Kevin. Kevin is eager, reasonably smart, but has zero access to your internal software. If you ask Kevin, “How much stock of the Turbo-Encabulator do we have, and can we ship it to Detroit by Friday?”, Kevin can’t just know the answer.

Instead, Kevin does a specific loop of behaviors:

  • Thought: “I need to check the inventory system for ‘Turbo-Encabulators’.”
  • Action: He walks to the warehouse computer and types in the query.
  • PAUSE: He waits for the spinning wheel of death on the old monitor.
  • Observation: The screen says: 50 units in stock.
  • Thought: “Okay, we have stock. Now I need to check shipping times to Detroit.”

This “Kevin Loop” is exactly how a ReAct (Reasoning + Acting) Agent works.

At EDVM, we build complex ERP systems to automate processes so humans don’t have to run around like Kevin. Today, we’re going to build a Python-based “Digital Kevin”, an AI agent built from scratch that can think, use tools, and solve multi-step problems.

What You’ll Learn

  • How to implement the ReAct pattern (Thought, Action, PAUSE, Observation).
  • How to separate the LLM’s reasoning from Python runtime.
  • How to parse LLM outputs to trigger real Python functions.

Prerequisites

  • Python 3.10+
  • An OpenAI API Key (or any LLM endpoint)
  • pip install openai python-dotenv

The “Thought-Action” Loop

An LLM on its own is like a brain in a jar. It can write poetry, but it can’t check your SQL database or calculate complex math reliably. To fix this, we give it a specific prompt that forces it to stop and ask for help.

We tell the LLM: “Don’t just guess. If you need data, state the Action you want to take, and then PAUSE. We (our Python script) will run that action and give you the Observation.”

It looks like this:

  • User: “What is the total cost of 5 Laptops for Client X?”
  • Agent (Thought): I need the price of a Laptop.
  • Agent (Action): get_price("Laptop") -> PAUSE
  • Python Runtime: Runs function get_price, returns $1000.
  • Agent (Observation): “The price is $1000.”
  • Agent (Thought): Now I need Client X’s discount…

Python Implementation Walkthrough

Let’s build a mini-ERP agent. We’ll strip away the fancy frameworks (LangChain, AutoGen) to see the raw gears turning underneath.

1. Define the “Tools” (The Environment)

First, we need the systems Kevin interacts with. In a real EDVM scenario, these would be Odoo API calls. Here, we’ll mock them.

from typing import Union, Dict, Callable

def get_product_price(item_name: str) -> str:
    """Simulates looking up an item in the database."""
    catalog: Dict[str, float] = {
        "Turbo-Encabulator": 5000.0,
        "Flux Capacitor": 2500.0,
        "Self-Sealing Stem Bolt": 10.0
    }
    price = catalog.get(item_name)
    if price:
        return str(price)
    return "Error: Product not found."

def calculate_tax(amount: Union[str, float]) -> str:
    """A specific calculator tool since LLMs struggle with precise math."""
    try:
        val = float(amount)
        return str(val * 1.21)  # 21% Tax
    except ValueError:
        return "Error: Invalid number provided."

# The Dictionary of Knowledge (The Agent's Toolbelt)
known_actions: Dict[str, Callable[[str], str]] = {
    "get_product_price": get_product_price,
    "calculate_tax": calculate_tax
}

2. The System Prompt

This is where we “program” the LLM’s behavior using English. We explicitly tell it to use the Thought -> Action -> PAUSE -> Observation loop.

system_prompt: str = """
You are a resourceful ERP Assistant. You run in a loop of Thought, Action, PAUSE, Observation.
At the end of the loop, output an Answer.

1. Thought: Describe what you need to do next.
2. Action: Run one of the available actions. Format: function_name: argument
3. PAUSE: Stop and wait for the result.
4. Observation: The result of your action will be provided here.

Your available actions are:
- get_product_price: item_name (looks up raw price)
- calculate_tax: amount (adds 21% tax to a raw amount)

Example session:
Question: How much is a Flux Capacitor with tax?
Thought: I need to find the base price first.
Action: get_product_price: Flux Capacitor
PAUSE

Observation: 2500

Thought: Now I need to add tax to 2500.
Action: calculate_tax: 2500
PAUSE

Observation: 3025.0

Answer: A Flux Capacitor costs $3025.0 including tax.
""".strip()

3. The Agent Class

The Agent handles the history. It appends the user’s question, sends it to the LLM, and waits for a response.

from openai import OpenAI
from typing import List, Dict

client = OpenAI()  # Assumes OPENAI_API_KEY is in env

class ReActAgent:
    def __init__(self, system_prompt: str) -> None:
        self.system_prompt: str = system_prompt
        self.messages: List[Dict[str, str]] = [
            {"role": "system", "content": system_prompt}
        ]

    def __call__(self, message: str) -> str:
        self.messages.append({"role": "user", "content": message})
        return self.execute()

    def execute(self) -> str:
        response = client.chat.completions.create(
            model="gpt-4o",
            temperature=0,  # Keep Kevin deterministic
            messages=self.messages
        )
        content = response.choices[0].message.content
        return content if content else ""

4. The Execution Loop (The Runtime)

This is the “Brain” of the operation. It looks at what the LLM said. If the LLM said “PAUSE”, the Python script wakes up, parses the action, runs the function, and feeds the result back.

import re

def run_agent_loop(agent: ReActAgent, question: str, max_turns: int = 5) -> None:
    next_prompt: str = question
    action_re = re.compile(r'^Action: (\w+): (.*)$')

    for step in range(max_turns):
        # 1. Ask Agent
        result: str = agent(next_prompt)
        print(f"\n[Step {step}] Agent says:\n{result}\n")

        # 2. Check if Agent is asking for an action
        actions = [
            action_re.match(a)
            for a in result.split('\n')
            if action_re.match(a)
        ]

        if actions:
            match = actions[0]
            if match:
                action_name, action_input = match.groups()

                if action_name not in known_actions:
                    raise Exception(f"Unknown tool: {action_name}")

                print(f"--- Python Running: {action_name}('{action_input}') ---")

                # 3. Execute Python function
                observation: str = known_actions[action_name](action_input)

                # 4. Feed the result back to the Agent
                next_prompt = f"Observation: {observation}"
                agent.messages.append({"role": "user", "content": next_prompt})
        else:
            print("--- Task Complete ---")
            return

Understanding the Flow

Here is what happens when you run the script. It’s a conversation between two entities:

  • The LLM (Kevin): “I need the price of the Turbo-Encabulator.” -> Writes Action -> Stops.
  • The Runtime (You/Python): Sees the action request. You look up the price (5000). You paste “Observation: 5000” into the chat history.
  • The LLM (Kevin): Sees the 5000. “Great. Now I need to calculate tax on 5000.” -> Writes Action -> Stops.
  • The Runtime: Runs the math. Pastes “Observation: 6050.0”.
  • The LLM (Kevin): “Answer: It costs $6050.0.”

This architecture allows the brain (LLM) to drive the body (Python functions) without them being hard-coded together.

Common Pitfalls

  • Regex Fragility: Using regex to parse Action: tool: input is great for learning, but in production, it’s brittle. If the LLM adds a space or a typo, the regex fails. Modern frameworks use JSON mode or Tool Calling API features to enforce structure.
  • Infinite Loops: If the agent gets confused, it might ask for the same price 50 times. Always implement a max_turns limit to prevent API bill shock.
  • Context Limit: Every “Observation” is appended to the message history. If you return a massive SQL query result, you will overflow the LLM’s context window, so keep observations concise.

Key Takeaways

  • Agents are Loops: They aren’t magic; they are just while loops that append string outputs to a list.
  • Tools are Functions: Giving an AI “tools” just means mapping a string command (like get_price) to a Python function.
  • Structured Thinking: The “Thought, Action, Pause” pattern forces the model to plan, reducing hallucinations significantly.

Need a system more robust than a digital intern?

At EDVM, we build tailored ERP & CRM systems using Odoo and Python. We own the data, automate the workflows, scale it to your needs so you can stop manually checking stock and start growing.

Visit edvm.network to learn more.

Want to build something similar?

If you are validating a product, modernizing operations, or shipping a technical MVP, we can help.

Start a Conversation