If you search for tutorials on building AI agents today, you will be hit with a wall of complex abstractions: chains, agents, memory buffers, and runnables. Frameworks like LangChain and LangGraph are helpful for complex systems, but for 90% of local scripting projects, they introduce unnecessary bloat, steep learning curves, and unexpected runtime overhead.
In this post, we'll build a fully functional local AI agent that can run commands, search files, and calculate values. We will build it with **zero external dependencies** — just standard HTTP calls to a local Ollama server and standard libraries. It uses less than 1MB of memory and finishes execution in milliseconds.
The Core of an AI Agent
At its heart, an AI agent is a loop consisting of three phases:
- Reasoning: The LLM looks at a prompt and decides what action to take.
- Execution: The program runs the tool requested by the model (e.g. running a script or reading a file).
- Observation: The program returns the output of the tool back to the LLM to decide if the task is complete.
This loop (often called the ReAct framework) is easy to write in vanilla code. It only requires a structured prompt and a simple parser.
Designing a Native Tool Calling Interface
To let the LLM call functions, we give it a clear format to output. We can instruct the model to think out loud and then output a specific trigger text, such as: CALL: tool_name(argument). Alternatively, modern small models support JSON mode or structured outputs natively.
Let's use a simple JSON instruction. We will ask the model to reply in this exact JSON schema:
{
"thought": "Why I need this action...",
"tool": "tool_name",
"arg": "arguments_here",
"final_answer": "Only populated if the task is fully complete"
}
The Implementation (Python Standard Library Only)
Below is the complete code to set up a local agent that has access to two tools: calculating expressions and fetching details from local files. Make sure Ollama is running and has the qwen2.5-coder:1.5b or llama3.2:3b model downloaded.
import json
import urllib.request
import math
# Define the tools our agent can use
def calculator(expr):
try:
# A simple calculator allowing safe math functions
allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
return str(eval(expr, {"__builtins__": None}, allowed_names))
except Exception as e:
return f"Error: {str(e)}"
def read_local_system_info(key):
# A dummy tool simulating reading a local config file
mock_data = {
"status": "system online",
"cpu_usage": "18%",
"database_size": "14.2 MB",
"disk_free": "182 GB"
}
return mock_data.get(key.strip().lower(), "Key not found in system logs.")
TOOLS = {
"calculate": calculator,
"get_system_info": read_local_system_info
}
SYSTEM_PROMPT = """You are a helpful local assistant. You have access to the following tools:
- calculate(expr): Evaluates mathematical expressions. Example arg: "12 * 45 + 10"
- get_system_info(key): Returns local status info. Allowed keys: status, cpu_usage, database_size, disk_free
You MUST respond in this JSON format and nothing else:
{
"thought": "Explain your reasoning step by step.",
"tool": "calculate" or "get_system_info" or null,
"arg": "arguments for the tool",
"final_answer": "The final result after tools have completed, or null if you need to run a tool first."
}
If you have the answer, set tool and arg to null, and write your reply in final_answer.
"""
def query_ollama(messages):
data = {
"model": "qwen2.5-coder:1.5b",
"messages": messages,
"format": "json",
"stream": False
}
req = urllib.request.Request(
"http://localhost:11434/api/chat",
data=json.dumps(data).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
with urllib.request.urlopen(req) as res:
response = json.loads(res.read().decode('utf-8'))
return response['message']['content']
def run_agent(task):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Task: {task}"}
]
print(f"Starting Task: {task}\n" + "="*40)
# Loop up to 5 times to prevent infinite runs
for step in range(5):
raw_response = query_ollama(messages)
try:
action = json.loads(raw_response)
except Exception:
print("Failed to parse JSON response:", raw_response)
break
print(f"[{step+1}] Thought: {action.get('thought')}")
if action.get('final_answer'):
print(f"Final Answer: {action['final_answer']}")
return action['final_answer']
tool_name = action.get('tool')
tool_arg = action.get('arg')
if tool_name in TOOLS:
print(f" -> Calling Tool: {tool_name}('{tool_arg}')")
tool_result = TOOLS[tool_name](tool_arg)
print(f" -> Observation: {tool_result}")
# Append the cycle to chat history
messages.append({"role": "assistant", "content": raw_response})
messages.append({"role": "user", "content": f"Tool result: {tool_result}"})
else:
print(f"Error: Model requested unknown tool '{tool_name}'")
break
print("Agent failed to complete the task within limits.")
return None
# Test the agent
run_agent("Calculate the square root of the local disk free space value.")
Why This Matters
This agent runs locally, has no external library requirements, and uses a single script. By parsing standard JSON structures and feeding the results back into a standard chat format, you can implement robust tools without bloating your codebase. This guarantees that your tools are secure, maintainable, and load instantaneously in any resource-constrained environment.