In the last article, we covered the concepts behind AI Agents and the ReAct loop. Now let's build one — under 50 lines of Python, an Agent that searches the web and executes code on its own.
You'll need: Python 3.10+, and an API that supports Function Calling. We use an OpenAI-compatible interface (any model supporting /v1/chat/completions works, including locally deployed ones).
One for searching, one for computing — these two already cover the vast majority of real-world needs.
def search_web(query: str) -> str:
"""Search the web, return top 5 results with titles and links."""
import urllib.request, urllib.parse, json
# DuckDuckGo Instant Answer API (free, no API key needed)
url = "https://api.duckduckgo.com/?" + urllib.parse.urlencode({
"q": query, "format": "json", "no_html": 1, "skip_disambig": 1
})
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())
results = []
if data.get("AbstractText"):
results.append(f"Abstract: {data['AbstractText']}")
for item in data.get("RelatedTopics", [])[:5]:
if isinstance(item, dict) and item.get("Text"):
results.append(f"- {item['Text']}")
return "\n".join(results) if results else "No results found"
def run_python(code: str) -> str:
"""Execute Python code, return stdout output."""
import subprocess
try:
result = subprocess.run(
["python3", "-c", code],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
return result.stdout or "(no output)"
return f"Error: {result.stderr}"
except subprocess.TimeoutExpired:
return "Error: code execution timed out"
run_python must execute inside a sandbox (Docker/VM) to prevent malicious code from compromising the system. Simplified here for demonstration.
The core is the ReAct loop, plus tool definitions and message management:
import json
from openai import OpenAI
# Initialize client (replace with your API endpoint and key)
client = OpenAI(
base_url="https://api.openai.com/v1", # or any compatible endpoint
api_key="your-api-key"
)
# Tool definitions (JSON Schema — the model needs this to understand tools)
TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for current information. Use when real-time data or knowledge beyond training cutoff is needed.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "run_python",
"description": "Execute Python code for calculations or data processing.",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "Python code to execute"}
},
"required": ["code"]
}
}
}
]
def run_agent(user_input: str, max_turns: int = 10) -> str:
"""Main Agent loop."""
messages = [
{"role": "system", "content": "You are a helpful assistant. You can search the web for current info and run Python for calculations. Prefer using tools for accuracy — don't guess from memory."},
{"role": "user", "content": user_input}
]
for turn in range(max_turns):
# Call the model
response = client.chat.completions.create(
model="gpt-4o", # or deepseek-chat, etc.
messages=messages,
tools=TOOLS,
tool_choice="auto"
)
msg = response.choices[0].message
# If model responds directly (no more tool calls), we're done
if not msg.tool_calls:
return msg.content or ""
# Execute each tool call
for tool_call in msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f"🔧 Calling tool: {fn_name}({fn_args})")
# Execute the tool
if fn_name == "search_web":
result = search_web(**fn_args)
elif fn_name == "run_python":
result = run_python(**fn_args)
else:
result = f"Unknown tool: {fn_name}"
# Add tool result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Also add the model's tool-call message
messages.append(msg)
return "Max turns reached. Task may be incomplete."
# Run
if __name__ == "__main__":
answer = run_agent(
"What's the time difference between Beijing and Shanghai? "
"Also compute 2^20 using Python."
)
print(f"\n✅ Final answer:\n{answer}")
The Agent will:
run_python("print(2**20)") → 1048576search_web("Beijing Shanghai time difference") → both in UTC+8You'll see something like:
🔧 Calling tool: run_python({'code': 'print(2**20)'})
🔧 Calling tool: search_web({'query': 'Beijing Shanghai time difference'})
✅ Final answer:
2^20 = 1,048,576.
Beijing and Shanghai are both in China Standard Time (UTC+8), so there is no time difference.
This isn't a toy. These 50 lines contain the core of every production Agent framework:
Understand these 50 lines, and you understand what LangChain, AutoGPT, CrewAI, and other frameworks are doing under the hood — they just add more tools, better error handling, and more complex orchestration on top of this same loop.
From here, you can go in three directions:
Future articles will expand on each of these.
📖 Next: Agent Tool Design Best Practices — how to write tool descriptions that models actually understand