As the software development landscape continues to shift rapidly toward artificial intelligence, Large Language Models (LLMs) have transformed from conversational novelties into active components of modern architecture. While most developers interact with LLMs through simple prompt-and-response interfaces, the true frontier of AI engineering lies in AI agents.
Moving beyond basic text generation, this comprehensive tutorial explores the foundational mechanics of agentic systems. By building a functional, locally hosted customer support agent using Python, Ollama, and SQLite, we will strip away the abstractions of heavy cloud frameworks and reveal the raw, transparent mechanics powering modern autonomous software.
1. Main Facts: Defining the Modern AI Agent
Before writing code, it is critical to establish a precise taxonomy. In an ecosystem where marketing buzzwords often obscure technical reality, what is an AI agent?
An AI agent is a software system that uses an LLM to call one or more tools in a loop to reach a goal.
The "goal" can range widely in complexity: answering a factual customer inquiry, utilizing a browser to book an airline ticket, or autonomously architecting and debugging complex software codebases.
In agentic systems, the "loop" describes the continuous cycle between the model, the application runtime, and its designated tools. Crucially, this does not always require a literal while or for statement. In our foundational implementation, we execute a single pass through that cycle: the model requests a tool, Python executes it, and the model translates the raw result into a natural language response. Because a single turn is sufficient for this scope, the steps are explicitly written out to maintain transparency.
Operating entirely locally via Ollama ensures that this architecture requires no cloud accounts, external API keys, or subscription fees, keeping your data secure and your development environment entirely self-contained.
2. Chronology: Setting Up Your Development Environment
To construct our local agent, we will establish a structured workflow using modern Python tooling and local model runners. The setup process is streamlined for reliability across Windows, macOS, and Linux platforms.
Phase 1: Installing Modern Tooling with uv
We use uv, an extremely fast Python package and project manager written in Rust, to handle our runtime environment. Open your terminal (PowerShell on Windows) and run the installation command:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
After closing and reopening your terminal to refresh your system paths, verify the installation.
Phase 2: Deploying Ollama for Local Inference
Next, install Ollama, a versatile engine designed to run large language models locally on consumer and enterprise hardware. Download and execute the installer from the official Ollama download page.
Once installed, open a fresh terminal window and pull a tool-capable model (ensure your machine has sufficient RAM and disk space—the download is roughly 3.4 GB):
ollama run llama3.2
Phase 3: Initializing the Project Directory
Create a dedicated project workspace and initialize your environment with the Ollama Python library:
uv init shop_agent
cd shop_agent
uv add ollama
uv will automatically manage virtual environments and download a compatible Python version (such as Python 3.14) without requiring manual activation steps.
3. Supporting Data: Constructing the Test Database
To make our agent useful, it needs access to domain-specific data that an LLM cannot possibly know a priori—such as internal e-commerce records. We will build a lightweight SQLite database to simulate a small online shop’s order management system.
The database consists of two primary tables:
orders: Stores the order number, customer name, and purchase date.order_items: Stores individual products, quantities, and pricing data associated with each order.
Create a file named build_shop_database.py and populate it with the following initialization code:
import sqlite3
from contextlib import closing
DATABASE_PATH = "shop_agent.db"
def build_database():
with closing(sqlite3.connect(DATABASE_PATH)) as conn:
with closing(conn.cursor()) as cursor:
# Create tables
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
order_id INTEGER PRIMARY KEY,
customer_name TEXT NOT NULL,
order_date TEXT NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
product_name TEXT NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders (order_id)
)
""")
# Insert sample data
cursor.execute("INSERT OR IGNORE INTO orders (order_id, customer_name, order_date) VALUES (1001, 'Sarah Jones', '2026-03-01')")
cursor.execute("INSERT OR IGNORE INTO order_items (order_id, product_name, quantity, price) VALUES (1001, 'Mechanical Keyboard', 1, 79.95)")
cursor.execute("INSERT OR IGNORE INTO order_items (order_id, product_name, quantity, price) VALUES (1001, 'USB-C Cable', 2, 8.50)")
conn.commit()
print("Database built and seeded successfully.")
if __name__ == "__main__":
build_database()
Execute the script via uv run:
uv run build_shop_database.py
Note on Architecture: SQLite is part of Python’s standard library, requiring no external database server. The explicit closing(...) wrapper is utilized because standard with connection blocks do not automatically close the underlying database file on Windows, preventing file-locking issues.
4. Official Implementation: Building the Single-Tool AI Agent
With our database primed, we can now construct the core agent script. Create a file named single_tool.py to handle the model inference, tool dispatch, and query execution loop.
import os
import sqlite3
import json
from contextlib import closing
from ollama import chat
MODEL = os.getenv("OLLAMA_MODEL", "llama3.2")
DATABASE_PATH = "shop_agent.db"
def get_order_total(order_id: int) -> str:
"""
Calculate the total value of a specific order ID by querying the shop database.
Args:
order_id (int): The unique numeric identifier of the order.
"""
with closing(sqlite3.connect(f"file:DATABASE_PATH?mode=ro", uri=True)) as conn:
with closing(conn.cursor()) as cursor:
cursor.execute(
"SELECT product_name, quantity, price FROM order_items WHERE order_id = ?",
(order_id,)
)
rows = cursor.fetchall()
if not rows:
return json.dumps("error": f"Order order_id not found.")
total = sum(q * p for _, q, p in rows)
items = ["product": p, "quantity": q, "price": pr for p, q, pr in rows]
return json.dumps("order_id": order_id, "items": items, "total_value": round(total, 2))
def run_agent():
messages = [
"role": "system", "content": "You are a helpful customer support assistant for an online shop. Use the provided tools to answer customer order queries accurately.",
"role": "user", "content": "What is the total value of order 1001?"
]
print(f"Sending prompt to local model (MODEL)...n")
# First turn: Send question and available tools to the model
response = chat(
model=MODEL,
messages=messages,
tools=[get_order_total],
)
# Append model's response (which may include a tool call request) to conversation history
messages.append(response.message)
# Check if the model requested a tool execution
if response.message.tool_calls:
for tool in response.message.tool_calls:
if tool.function.name == "get_order_total":
print(f"-> Model requested tool: tool.function.name")
print(f"-> Arguments provided: tool.function.arguments")
# Execute the local Python function
result = get_order_total(**tool.function.arguments)
print(f"-> Database execution result: resultn")
# Append tool result back into the conversation history
messages.append(
"role": "tool",
"content": result,
)
# Second turn: Send complete history back to the model to formulate final answer
final_response = chat(model=MODEL, messages=messages)
print(f"Agent Final Answer:nfinal_response.message.content")
else:
print(f"Agent Direct Answer:nresponse.message.content")
if __name__ == "__main__":
run_agent()
Execute your agent:
uv run single_tool.py
How the Mechanics Operate Under the Hood
- Tool Schema Generation: The Ollama Python SDK automatically inspects
get_order_total()‘s name, type annotations, and docstring, converting them into a JSON schema sent to the model. The model never reads raw Python source code; it receives a structured interface definition. - Intent Recognition: When asked about order 1001, the model evaluates the prompt, determines it lacks the native data, and emits a structured tool request (
response.message.tool_calls). - Python-Gated Execution: Python validates the function name and executes the SQLite query securely in read-only mode. The model has no direct connection to the database.
- Final Synthesis: The structured JSON output from the database is fed back into the conversation history, allowing the model to draft a coherent, natural-language response for the end-user.
5. Implications: Failure Paths, Schemas, and Production Realities
While our basic agent successfully resolves valid queries, real-world deployment requires understanding edge cases, failure boundaries, and security implications.
Testing Failure Paths
Modify the user prompt in the script to test non-existent records:
"role": "user", "content": "What is the total value of order 9999?"
Because the function returns structured JSON containing an error message ("Order 9999 not found"), the model appropriately informs the user rather than hallucinating arbitrary financial figures.
Conversely, testing ambiguous or irrelevant questions reveals the limits of LLM guardrails:
"role": "user", "content": "Can you recommend a good movie to watch tonight?"
While system prompts help steer behavior, they are not hard security boundaries. If an answer must strictly rely on verified backend data, your application logic must explicitly verify whether an authorized tool was called and handle unverified responses accordingly.
Decoding the Schema
It is vital to remember that passing Python functions is merely a developer convenience. Ollama translates your function into a schema resembling the following:
"type": "function",
"function":
"name": "get_order_total",
"description": "Calculate the total value of a specific order ID by querying the shop database.",
"parameters":
"type": "object",
"properties":
"order_id":
"type": "integer",
"description": "The unique numeric identifier of the order."
,
"required": ["order_id"]
This underscores why writing descriptive docstrings and precise type hints is paramount; the model relies entirely on this text representation to decide when and how to utilize your application code.
Model Interoperability
Because our script reads the OLLAMA_MODEL environment variable, switching underlying intelligence engines is trivial. You can test alternative local models (such as mistral or qwen2.5) by pulling them via Ollama and setting your session variable:
ollama pull mistral
$env:OLLAMA_MODEL="mistral"
uv run single_tool.py
Summary
By combining Python, Ollama, and SQLite, we have constructed a fully autonomous local AI agent without relying on proprietary cloud infrastructure or paid APIs. We examined the foundational lifecycle of agentic loops—how models request tools, how application runtimes safely execute them, and how results are translated back into actionable intelligence.
This transparent, single-tool implementation provides the essential building blocks for scaling toward multi-step, multi-tool agents in future software engineering projects.

