
How to Build an Autonomous Python Agent with Function Calling in 50 Lines
Quick Start (Complete 50-Line Agent)
You do not need heavy frameworks like LangChain or AutoGen to build an autonomous agent. A production-grade agent is simply a stateful message loop that queries an LLM, inspects tool_calls, executes local Python functions, and feeds results back until completion:
import json, subprocess
from openai import OpenAI
client = OpenAI()
def execute_shell(command: str) -> str:
"""Execute a local bash command safely and return stdout/stderr."""
res = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
return res.stdout if res.returncode == 0 else f"Error: {res.stderr}"
tools = [{
"type": "function",
"function": {
"name": "execute_shell",
"description": "Run a bash command on the local machine",
"parameters": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]
}
}
}]
messages = [
{"role": "system", "content": "You are an autonomous DevOps assistant. Use execute_shell to solve tasks."},
{"role": "user", "content": "Check disk space on / and return free GB."}
]
# The Autonomous Execution Loop
while True:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools
)
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
print(f"\nFinal Answer:\n{msg.content}")
break
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
print(f"-> Executing: {call.function.name}({args})")
result = execute_shell(args["command"])
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": str(result)
})
1. How the Agentic Loop Works (Step-by-Step)
An autonomous agent operates on the ReAct (Reason + Act) pattern divided into four deterministic stages:
[User Request]
│
▼
┌──────────────┐
│ LLM Reason │ ◄─────────────────────────┐
└──────┬───────┘ │
│ │
Has tool_calls? │
├── YES ──► [Execute Local Tool] ───┘ (Append tool message)
│
└── NO ──► [Return Final Response to User]
- State Preservation: The
messageslist acts as the agent’s short-term memory buffer. Every turn appends the assistant’s planned call and the tool’s raw execution output. - Tool Dispatcher: When
msg.tool_callsis present, the agent iterates over each call, parses JSON arguments, executes the corresponding Python function, and returns a message withrole: "tool". - Termination Condition: When the LLM decides it has sufficient information to answer the user request, it omits
tool_callsand outputs natural language text, which triggersbreak.
2. Minimalist Python Loop vs Heavy Frameworks
| Metric / Dimension | Raw Python Loop (Above) | LangChain / LangGraph | CrewAI |
|---|---|---|---|
| Dependencies | openai (or httpx) |
40+ packages (langchain-core, etc.) |
crewai, chromadb, etc. |
| Token Overhead | Zero prompt bloat | ~500–1,200 hidden token wrappers | ~1,000+ persona tokens |
| Debugging Difficulty | Single breakpoint in loop | Nested call stacks & internal handlers | Complex multi-agent threads |
| Execution Latency | Direct network speed | 150ms–400ms framework overhead | 500ms+ orchestration overhead |
| Best For | Internal tools, microservices, CLI agents | Complex stateful DAGs & visual graphs | High-level persona brainstorming |
3. Essential Guardrails for Production
A raw loop can run indefinitely or execute destructive commands without safeguards. Add these three guardrails before running on remote servers:
1. Hard Maximum Turns (Recursion Prevention)
MAX_TURNS = 10
turn_count = 0
while turn_count < MAX_TURNS:
turn_count += 1
# ... execution logic ...
else:
raise TimeoutError("Agent exceeded maximum allowed execution turns.")
2. Tool Whitelisting & Sandboxing
Never pass unrestricted user inputs into shell=True without command validation:
ALLOWED_COMMANDS = {"df", "ls", "uptime", "free", "docker ps"}
def execute_shell(command: str) -> str:
binary = command.strip().split()[0]
if binary not in ALLOWED_COMMANDS:
return f"Error: Command '{binary}' is not permitted by security policy."
# execute safely...
3. Graceful Error Handling (Self-Correction)
Return exceptions as strings inside the tool output instead of crashing the Python process. LLMs can inspect error traces and automatically retry with corrected arguments:
try:
output = run_command()
except Exception as e:
return f"Tool Execution Failed: {type(e).__name__} - {str(e)}"
Summary Checklist
- Standard function calling APIs support fully autonomous loops in under 50 lines.
- Append both the
assistantmessage and thetoolreturn message to maintain context integrity. - Always enforce
MAX_TURNSand command whitelists to prevent runaway token costs and unauthorized actions.