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.
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: inputis 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_turnslimit 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.
Want to build something similar?
If you are validating a product, modernizing operations, or shipping a technical MVP, we can help.