Build a Local File-Organizing AI Agent
Build a safe local AI agent that organizes files after your approval.
Introduction
30 Second Summary
Unsorted folders fill with unrelated files faster than most people can keep up. Handing that cleanup to AI feels risky when one wrong guess can move the wrong file.
In this project, you will build a local file-organizing AI agent with Python. The agent uses Ollama plus read-only tools to inspect a sandbox inbox before your approval unlocks any file move.
What You'll Build
Picture watching every tool call appear in your terminal while the agent organizes a real inbox only after you approve its complete plan.
By the end of this project, you'll have:
- Private local inference that answers prompts without a cloud account or API key.
- A transparent agent loop that displays every request to list or read the sandbox files.
- An approval-gated workflow that waits for the exact YES confirmation before moving any file. A JSON audit log records every approved move.
- Secret Mission: Add a rollback command that restores the last approved batch.
Are there any prerequisites?
You need basic Python familiarity plus a Windows computer running Windows 10 22H2 or newer.
At least 4.6 GB of free disk space plus internet access are required for the initial downloads.
Before We Start
Before any hands-on work, lock in the safe role of your local file-organizing AI agent: inspecting text files inside a sandbox inbox to propose categories. Only ordinary Python code may move those files after you explicitly type YES.
Set Up the Local Model
A safe local agent needs a local inference service before Python can request decisions. This keeps the model on your computer.
In this step, you will install Ollama as the private service that receives those requests. Ollama runs in the background on Windows.
You will verify the Qwen3 model with an interactive greeting. You will also open an empty project folder in Visual Studio Code.
In this step, get ready to:
- Confirm your Windows version meets Ollama's requirement.
- Install Ollama as a local background service.
- Verify the local model with an interactive greeting.
Check Windows and storage requirements
Ollama's native Windows build requires Windows 10 with version 22H2 or a newer Windows release. Your drive also needs at least 4.6 GB of free space for Ollama plus the selected model.
- Right-click Start.
- Select Windows Settings.
- Select System.
- Select About.
- Under Windows specifications, confirm your device runs Windows 10 with version 22H2 or a newer Windows release.
- Return to System.
- Select Storage.
- Confirm the drive that will hold Ollama has at least 4.6 GB of available space.
Install Ollama
PowerShell can run the official Windows installer directly. The installed Ollama app serves its local API at http://localhost:11434.
The installer can take a few minutes while it downloads the current build. It does not create a cloud account.
- Press the Windows key to open the search bar.
- Type PowerShell and press Enter to open it.
- Install the current official Ollama build by running this command:
irm https://ollama.com/install.ps1 | iex
What does this command do?
- The irm section retrieves Ollama's official Windows installer script.
- The pipe passes that script to iex for execution.
- The completed installation starts Ollama in the background.
✔️ The installation completes
Good. Ollama is now installed on Windows and running as your local inference service.
- Keep PowerShell open for the model download.
ⓧ The installation does not complete
The installer needs an active internet connection to retrieve the current Windows build.
- Confirm your computer is connected to the internet.
- Return to PowerShell.
- Run the installer command shown above again.
Still having trouble installing Ollama?
Copy the installer command exactly as shown. A missing pipe or changed URL prevents the script from running.
Use the official Ollama Windows guide to compare the current installation requirements.
help me troubleshoot the Ollama Windows installation
Run the model and open your project
The model identifier qwen3:0.6b tells Ollama which local model to run. The first run downloads its 523MB model data.
Allow several minutes for that first download if your connection is slow. The terminal may stay busy while Ollama retrieves the model.
Before you run it, ask yourself whether PowerShell will request a cloud API key.
- Start the local model by running this command:
ollama run qwen3:0.6b
What does this command do?
- Ollama downloads qwen3:0.6b when the model is missing from your computer.
- Ollama then starts an interactive chat with the local model.
- The conversation stays on your computer and requires no local API authentication.
- Wait for the model download to finish.
- Type Hello and press Enter.
You'll see the model answer in PowerShell without an API key prompt. That response confirms local inference is working.
- Press Ctrl+C to end the interactive chat after the response appears.
No model response?
Confirm Ollama is still running in Windows. Restart Ollama if the background service has stopped.
Check your internet connection if the model download stalls. Run the model command again after connectivity returns.
help me troubleshoot why qwen3:0.6b is not responding through Ollama
That is the core local-inference check complete. The final setup task gives your Python code a dedicated home.
- Press the Windows key to open the search bar.
- Type Visual Studio Code and press Enter to open it.
You'll see Visual Studio Code open without a project folder selected.
- Click File in the top menu bar.
- Click Open Folder.
- Select Desktop in the folder dialog.
- Select New Folder.
- Type local-file-agent as the folder name.
- Press Enter to create the folder.
You'll see the empty local-file-agent folder on your Desktop.
- Select the local-file-agent folder.
- Click Select Folder.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
You'll see local-file-agent in the Explorer sidebar with no files beneath it. Your local model and empty project workspace are ready.
✔️ Awesome, I've got everything!
Your local model setup matches the required end state.
ⓧ I'd like to double check the full setup
Compare your environment with these completed artifacts:
- Installed on Windows, running locally, and serving the chat API at http://localhost:11434.
- Created as an empty local project folder and opened in Visual Studio Code.
- Downloaded through Ollama and verified with an interactive greeting.
Your model and workspace are ready. Next up, you'll connect Python to Ollama's local chat endpoint.
Call the Local Model from Python
Your model already answers through Ollama. Python still needs its own path to request decisions locally.
A small Python client sends JSON over HTTP to the local chat endpoint. The visible reply proves your program can reach the model before agent tools add more moving parts.
In this step, get ready to:
- Create a Python client for the local Ollama chat endpoint.
- Send one message to the local model.
- Print the model's reply in the terminal.
Create the local API client
Ollama exposes its chat service at http://localhost:11434/api/chat. Your client packages the conversation into a request that this endpoint can understand.
- Create a new file inside the open local-file-agent folder using Visual Studio Code's file controls.
- Name the file agent.py.
- Build the local API client by pasting this code into agent.py:
import json
from urllib.request import Request, urlopen
API_URL = "http://localhost:11434/api/chat"
MODEL = "qwen3:0.6b"
def call_ollama(messages):
# Build the JSON body expected by the local chat endpoint.
payload = {
"model": MODEL,
"messages": messages,
"stream": False,
"think": False,
}
# Encode the payload for an HTTP POST request.
request = Request(
API_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
# Decode the JSON response to return the assistant text.
with urlopen(request, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))["message"]["content"]
What Does This Code Do?
- The API_URL value points to Ollama's local chat endpoint.
- The MODEL value selects qwen3:0.6b for each request.
- The payload contains the selected model plus the conversation messages.
- Setting stream to False requests one complete response.
- Setting think to False keeps the output focused on the final answer.
- The call_ollama() function decodes the response before returning the assistant's message content.
- Save agent.py.
- Confirm agent.py appears directly inside the local-file-agent folder in Visual Studio Code.
Don't See agent.py?
- Check that the file name is exactly agent.py.
- Confirm the file sits directly inside local-file-agent.
Ask for help if the file is missing from your project: help me find agent.py in my local-file-agent folder
Add the interactive entry point
The API client needs one user message before it can make a request. The interactive entry point collects that message before printing the returned text.
- Add the following code at the bottom of agent.py.
def main():
# Collect one goal from the learner.
task = input("Goal: ")
# Send the goal as a user message.
messages = [{"role": "user", "content": task}]
print(call_ollama(messages))
if __name__ == "__main__":
main()
How Does the Chatbot Start?
- The input() call pauses the program until you enter a goal.
- The messages list labels your text with the user role.
- The main() call starts the interactive chatbot when you run agent.py.
- Save agent.py.
Before you run the chatbot, do you expect Python to receive the same kind of local response you saw through Ollama?
- Run the interactive chatbot from the Visual Studio Code terminal by running this command:
python agent.py
- Enter Say hello in one short sentence at the Goal: prompt.
You'll see a short greeting printed in the terminal. That connection is working now: Python can request a decision from your local model without a cloud account or API key.
No Model Reply?
- Confirm Ollama is still running in the Windows background.
- Compare API_URL against http://localhost:11434/api/chat.
- Check that qwen3:0.6b is spelled exactly as shown in MODEL.
- Save agent.py before running the command again.
If the local request still fails, help me troubleshoot why agent.py cannot reach my local Ollama chat endpoint
✔️ Awesome, I've got everything!
Great. Double check that you saved agent.py after seeing the local model reply.
ⓧ I'd like to double check the full code
Compare your complete agent.py file against this version.
import json
from urllib.request import Request, urlopen
API_URL = "http://localhost:11434/api/chat"
MODEL = "qwen3:0.6b"
def call_ollama(messages):
# Build the JSON body expected by the local chat endpoint.
payload = {
"model": MODEL,
"messages": messages,
"stream": False,
"think": False,
}
# Encode the payload for an HTTP POST request.
request = Request(
API_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
# Decode the JSON response to return the assistant text.
with urlopen(request, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))["message"]["content"]
def main():
# Collect one goal from the learner.
task = input("Goal: ")
# Send the goal as a user message.
messages = [{"role": "user", "content": task}]
print(call_ollama(messages))
if __name__ == "__main__":
main()
Your local chatbot can now turn a terminal message into a model response. Next, you'll test whether that conversation alone can inspect real files on your computer.
Seed the Sandbox Inbox
Your local chatbot now returns a visible response from the model. That proves the connection between Python and Ollama works.
A known sandbox inbox lets you test a harder question. Can the chatbot report local files that were never included in its messages?
In this step, get ready to:
- Create a script that seeds three known files.
- Inspect each file's actual contents.
- Test whether the chatbot can report those contents accurately.
Define the sandbox samples
A repeatable seed script gives every test the same starting files. This known state makes the chatbot's claims easy to check.
- In the file sidebar from earlier, use the file icon beside the local-file-agent folder to create seed_workspace.py.
You should see a blank seed_workspace.py file in the editor.
- Add the sandbox paths and sample contents by pasting this into seed_workspace.py:
from pathlib import Path
ROOT_DIR = Path(__file__).parent.resolve()
INBOX_DIR = ROOT_DIR / "workspace" / "inbox"
SAMPLES = {
"project-alpha.txt": (
"Project Alpha\n"
"Prepare the demo slides and finish the API checklist before Friday.\n"
),
"grocery-list.txt": (
"Personal shopping list\n"
"Milk, rice, apples, coffee, and dish soap.\n"
),
"laptop-receipt.txt": (
"Purchase receipt\n"
"Laptop stand, total 39.99, purchased for the home office.\n"
),
}
What Does This Code Define?
- The Path import gives the script a platform-safe way to construct file locations.
- ROOT_DIR anchors every location to the folder containing seed_workspace.py.
- INBOX_DIR points to the future workspace/inbox directory.
- SAMPLES stores the exact filename and text for each sample.
- Save seed_workspace.py.
- Check that the definitions are valid by running this command in the terminal panel:
python seed_workspace.py
What Did This Check Prove?
Python reads the file and validates the syntax. A fresh terminal prompt without a traceback confirms that the paths and sample dictionary are valid.
Seeing a Python Traceback?
A traceback at this point usually means a quote or bracket differs from the sample. Compare the SAMPLES dictionary character for character.
Check that every filename has a matching closing parenthesis.
Help me fix the syntax in my sample dictionary.
- Return to seed_workspace.py.
- Add the file-generation logic below the SAMPLES dictionary by pasting this code:
def main():
INBOX_DIR.mkdir(parents=True, exist_ok=True)
for filename, content in SAMPLES.items():
path = INBOX_DIR / filename
path.write_text(content, encoding="utf-8")
print(f"Created {path.relative_to(ROOT_DIR)}")
if __name__ == "__main__":
main()
What Does This Code Do?
- main() creates the inbox directory when it does not already exist.
- The loop builds one path for each entry in SAMPLES.
- write_text() saves each sample with UTF-8 encoding.
- The entry point calls main() when you run the script directly.
- Save seed_workspace.py.
Before you run the completed script, predict how many creation messages the terminal prints.
- Create the sandbox inbox by running this command:
python seed_workspace.py
What Should I See?
You should see three lines beginning with Created.
- workspace/inbox/project-alpha.txt.
- workspace/inbox/grocery-list.txt.
- workspace/inbox/laptop-receipt.txt.
Good progress. Your seed script now creates the same three-file inbox whenever you run it.
Do Not See Three Created Files?
Confirm that the terminal is inside the local-file-agent folder. The script must run beside seed_workspace.py.
Save the file before trying the command again.
Help me find why the seed script did not create all three inbox files.
✔️ Awesome, I've got everything!
Your complete seed_workspace.py file is saved. The three sandbox files now exist.
ⓧ I'd like to double check the full code
Compare your complete seed_workspace.py file against this reference.
from pathlib import Path
ROOT_DIR = Path(__file__).parent.resolve()
INBOX_DIR = ROOT_DIR / "workspace" / "inbox"
SAMPLES = {
"project-alpha.txt": (
"Project Alpha\n"
"Prepare the demo slides and finish the API checklist before Friday.\n"
),
"grocery-list.txt": (
"Personal shopping list\n"
"Milk, rice, apples, coffee, and dish soap.\n"
),
"laptop-receipt.txt": (
"Purchase receipt\n"
"Laptop stand, total 39.99, purchased for the home office.\n"
),
}
def main():
INBOX_DIR.mkdir(parents=True, exist_ok=True)
for filename, content in SAMPLES.items():
path = INBOX_DIR / filename
path.write_text(content, encoding="utf-8")
print(f"Created {path.relative_to(ROOT_DIR)}")
if __name__ == "__main__":
main()
How This File Fits Together
The first section defines a fixed inbox dataset. The main() function turns that dataset into real files inside the sandbox.
Create and inspect the inbox
The terminal output confirms that the script wrote three paths. Inspecting the files gives you direct evidence of what each one contains.
- Expand the workspace folder in the file sidebar.
- Expand the inbox folder.
- Select project-alpha.txt.
- Select grocery-list.txt.
- Select laptop-receipt.txt.
What Should the Inbox Contain?
project-alpha.txt contains a reminder about demo slides and an API checklist.
grocery-list.txt contains a personal shopping list.
laptop-receipt.txt contains a purchase receipt for a laptop stand.
Test the chatbot's filesystem access
The seeded inbox gives you a known answer to compare against. The next request tests whether a convincing response is evidence of real file access.
- Switch back to the terminal panel from earlier.
- Start the current chatbot by running this command:
python agent.py
What Does This Command Start?
This starts the same interactive chatbot that answered your greeting. Its messages still contain only the text entered in the terminal.
Before you send the request, predict whether the unchanged chatbot can name all three files from the folder path alone.
- Enter this request when the chatbot asks for your message:
Tell me exactly what files are in workspace/inbox and summarize each one
Why Ask for Exact Details?
The request demands filenames and summaries that you can compare with the known inbox. Accuracy alone cannot prove that the chatbot observed the files.
- Compare the chatbot's response with the three files in the file sidebar.
The response cannot provide verifiable evidence that it read those files. It may guess, add caveats, or admit that it cannot inspect the folder.
This shortfall is the intended result. The model has no file-changing authority, so your sandbox remains unchanged.
Chatbot Does Not Respond?
Confirm that Ollama is still running in the background. Also confirm that the qwen3:0.6b model remains available.
Help me restore the connection between agent.py and my local Ollama model.
You now have a repeatable inbox and direct evidence of the chatbot's blind spot. Next, you will give the agent bounded read access to the sandbox.
Add Read-Only Agent Tools
Your seeded inbox proved the chatbot's blind spot. Its answer came from language patterns because it had no route to the real files.
Read-only tool calling gives the model a controlled way to request observations. In this step, you will let Ollama request two bounded Python functions.
In this step, get ready to:
- Restrict every file read to workspace/inbox.
- Describe the read-only functions with JSON tool schemas.
- Process tool requests inside a bounded agent loop.
Build bounded inbox readers
A model-supplied filename is untrusted input. Path validation keeps every read inside workspace/inbox.
- In agent.py, replace the imports and constants at the top with this code:
import json
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen
API_URL = "http://localhost:11434/api/chat"
MODEL = "qwen3:0.6b"
MAX_ROUNDS = 8
ROOT_DIR = Path(__file__).parent.resolve()
WORKSPACE_DIR = (ROOT_DIR / "workspace").resolve()
INBOX_DIR = (WORKSPACE_DIR / "inbox").resolve()
What does this code set up?
- The Path import gives the agent platform-safe file paths.
- MAX_ROUNDS limits the model to eight requests.
- ROOT_DIR anchors every project path beside agent.py.
- INBOX_DIR resolves the only directory that the tools may read.
- Place these file functions below the constants in agent.py by copying the code:
def inbox_file(filename):
if Path(filename).name != filename:
raise ValueError("Only a plain filename is allowed.")
candidate = (INBOX_DIR / filename).resolve()
if candidate.parent != INBOX_DIR:
raise ValueError("The requested path is outside the inbox.")
if not candidate.is_file():
raise ValueError(f"Inbox file not found: {filename}")
return candidate
def list_inbox():
files = sorted(
path.name for path in INBOX_DIR.iterdir() if path.is_file()
)
return json.dumps(files)
def read_file(filename):
path = inbox_file(filename)
return path.read_text(encoding="utf-8")[:2000]
How do the file boundaries work?
- inbox_file() rejects names containing folder segments.
- candidate.parent confirms the resolved path remains directly inside the inbox.
- list_inbox() returns the real filenames in a predictable order.
- read_file() reads at most 2,000 characters from one validated file.
- Save agent.py.
- Prepare to enter Say hello in one short sentence when the terminal asks for a prompt.
- Confirm the existing chatbot remains runnable by running:
python agent.py
What does this check prove?
This command runs the current chatbot after the path code is added. A model response confirms that the new imports and helper definitions did not break its local connection.
Good. The chatbot still answers while the new readers remain bounded to the sandbox.
Seeing a Python error?
Check that the new functions sit outside every existing function. Their def lines must begin at the far-left edge of agent.py.
Compare the parentheses around ROOT_DIR, WORKSPACE_DIR, and INBOX_DIR.
Help me fix the new path and inbox functions in agent.py.
Describe and dispatch the tools
The model needs a machine-readable description of each available function. Each schema names one tool plus the arguments it accepts.
- Add the system prompt and first tool schema below the INBOX_DIR line:
SYSTEM_PROMPT = """You are a file-organizing agent working in a sandbox.
You must call list_inbox before making claims about the files.
You must call read_file for every file you find.
"""
TOOLS = [
{
"type": "function",
"function": {
"name": "list_inbox",
"description": "List the text files currently inside the sandbox inbox.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
]
What does the first schema describe?
SYSTEM_PROMPT requires the agent to observe the inbox before making claims. The list_inbox schema takes no arguments because it always reads the same bounded directory.
- Inside TOOLS, insert this second schema immediately before the closing square bracket:
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read one text file from the sandbox inbox.",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "A filename returned by list_inbox.",
}
},
"required": ["filename"],
},
},
},
Why require a filename?
The read_file schema tells the model to supply one filename returned by list_inbox. The Python validator still makes the final decision about whether that filename is safe.
- Save agent.py.
- Prepare to enter Say hello in one short sentence when the terminal asks for a prompt.
- Confirm the new schemas parse successfully by running:
python agent.py
What does this run check?
Python reads the new nested dictionaries before reaching the existing chatbot entry point. A normal model answer confirms that every brace and bracket is balanced.
Your tool descriptions now load successfully. The next change lets the chat request include them.
Seeing a schema syntax error?
Check that the second schema sits between the opening and closing brackets of TOOLS.
Confirm that both tool objects end with a comma.
Help me find the syntax error in my TOOLS schemas.
- In agent.py, replace the complete call_ollama function with this version:
def call_ollama(messages, tools=None):
payload = {
"model": MODEL,
"messages": messages,
"stream": False,
"think": False,
}
if tools:
payload["tools"] = tools
request = Request(
API_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))
except URLError as error:
raise RuntimeError(
"Cannot reach Ollama. Confirm that Ollama is running on Windows."
) from error
How does the request change?
- tools=None keeps the function usable for ordinary chat requests.
- payload["tools"] adds the schemas when the agent loop supplies them.
- json.loads() preserves the complete response so the loop can inspect message.tool_calls.
- URLError becomes a local error that explains which service to check.
- Place the tool dispatcher below read_file by copying this code:
def run_tool(name, arguments):
try:
if name == "list_inbox":
return list_inbox()
if name == "read_file":
return read_file(arguments.get("filename", ""))
return f"Unknown tool: {name}"
except (OSError, TypeError, ValueError) as error:
return f"Tool error: {error}"
Why use a dispatcher?
run_tool() maps a model-requested name to an approved Python function. Unknown tools return text instead of gaining access to another operation.
File and argument errors also return as tool results. This gives the model an observation without weakening the sandbox.
- Save agent.py.
- Prepare to enter Say hello in one short sentence when the terminal asks for a prompt.
- Confirm that the complete Ollama response is preserved by running:
python agent.py
Why does the output look different?
The updated function returns the complete response object instead of extracting only the answer text. That object contains the message data needed for tool requests.
You should see a response object containing the model's message. The final loop will format that data into agent and tool entries.
Cannot reach the local model?
Confirm that Ollama is still running in the Windows taskbar. The Python request depends on its local service.
Check that API_URL remains http://localhost:11434/api/chat.
Help me troubleshoot why agent.py cannot reach Ollama.
Run the bounded agent loop
One tool call gives the model one observation. An agent loop sends each result back so the model can choose its next read.
- Place the first working version of run_agent below run_tool by copying this code:
def run_agent(task):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": task},
]
for round_number in range(1, MAX_ROUNDS + 1):
print(f"\n[Agent round {round_number}]")
response = call_ollama(messages, TOOLS)
message = response["message"]
messages.append(message)
content = message.get("content", "").strip()
if content:
print(f"[Agent] {content}")
tool_calls = message.get("tool_calls") or []
if not tool_calls:
return
for tool_call in tool_calls:
function = tool_call.get("function", {})
name = function.get("name", "")
arguments = function.get("arguments") or {}
print(f"[Tool] {name} {arguments}")
result = run_tool(name, arguments)
print(f"[Result] {result}")
messages.append(
{
"role": "tool",
"tool_name": name,
"content": str(result),
}
)
How does the loop use results?
- messages holds the system instruction plus the task.
- call_ollama(messages, TOOLS) lets the model choose from the two schemas.
- message.tool_calls contains each requested function name plus its arguments.
- role: tool sends the real Python result back into the next round.
- Replace the existing main function and entry point with this code:
def main():
if not INBOX_DIR.is_dir() or not any(INBOX_DIR.iterdir()):
print("The inbox is empty. Run: python seed_workspace.py")
return
task = (
"Organize every file in the inbox. "
"Inspect each file before proposing one category for it."
)
run_agent(task)
if __name__ == "__main__":
main()
What starts the agent?
main() checks that the seeded inbox exists before asking the agent to inspect every file. The exposed tool list contains only list_inbox and read_file.
The model may describe categories in its final answer. It has no Python function that can change the files.
- Save agent.py.
Before you run the agent, which tool do you expect it to request first?
- Run the first complete agent loop with this command:
python agent.py
What does this command run?
The command starts main() with the fixed inbox task. Each model request can now produce a tool call or a final answer.
You should see [Tool] list_inbox before several [Tool] read_file entries. The final agent message summarizes the real contents of all three files.
No tool entries?
Confirm that run_agent calls call_ollama(messages, TOOLS).
Check that SYSTEM_PROMPT requires list_inbox before file claims.
Help me debug why my Ollama agent is answering without calling list_inbox or read_file.
The first run proves the tool cycle works. Two final checks make its safety boundary explicit.
- Inside run_agent, expand the tool argument section to match this code:
arguments = function.get("arguments") or {}
if not isinstance(arguments, dict):
arguments = {}
Why check the argument type?
The dispatcher expects a dictionary with named arguments. Any other shape becomes an empty dictionary before reaching Python.
- Add this line after the complete for loop in run_agent.
raise RuntimeError("The agent reached the eight-round safety limit.")
Why raise after eight rounds?
The loop normally returns when a response has no tool calls. Reaching this line means the model kept requesting tools through every allowed round.
- Save agent.py.
Before the final run, do you expect any file to leave the inbox?
- Verify the finished read-only agent by running:
python agent.py
What does the final run prove?
The terminal exposes each observation through [Tool] and [Result] entries. The loop stops after the model returns a message without another tool call.
You should see one inbox listing plus reads for project-alpha.txt, grocery-list.txt, and laptop-receipt.txt.
That is the chatbot's blind spot closed. It can now support its summary with real sandbox observations.
Missing a file summary?
Confirm that all three text files remain directly inside workspace/inbox.
Check that the system prompt says to call read_file for every discovered file.
Help me diagnose why my read-only agent skipped an inbox file.
✔️ Awesome, I've got everything!
Great. Double check that you saved agent.py after the final run.
ⓧ I'd like to double check the full code
Compare your complete agent.py file with this read-only version.
import json
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen
API_URL = "http://localhost:11434/api/chat"
MODEL = "qwen3:0.6b"
MAX_ROUNDS = 8
ROOT_DIR = Path(__file__).parent.resolve()
WORKSPACE_DIR = (ROOT_DIR / "workspace").resolve()
INBOX_DIR = (WORKSPACE_DIR / "inbox").resolve()
SYSTEM_PROMPT = """You are a file-organizing agent working in a sandbox.
You must call list_inbox before making claims about the files.
You must call read_file for every file you find.
"""
TOOLS = [
{
"type": "function",
"function": {
"name": "list_inbox",
"description": "List the text files currently inside the sandbox inbox.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read one text file from the sandbox inbox.",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "A filename returned by list_inbox.",
}
},
"required": ["filename"],
},
},
},
]
def call_ollama(messages, tools=None):
payload = {
"model": MODEL,
"messages": messages,
"stream": False,
"think": False,
}
if tools:
payload["tools"] = tools
request = Request(
API_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))
except URLError as error:
raise RuntimeError(
"Cannot reach Ollama. Confirm that Ollama is running on Windows."
) from error
def inbox_file(filename):
if Path(filename).name != filename:
raise ValueError("Only a plain filename is allowed.")
candidate = (INBOX_DIR / filename).resolve()
if candidate.parent != INBOX_DIR:
raise ValueError("The requested path is outside the inbox.")
if not candidate.is_file():
raise ValueError(f"Inbox file not found: {filename}")
return candidate
def list_inbox():
files = sorted(
path.name for path in INBOX_DIR.iterdir() if path.is_file()
)
return json.dumps(files)
def read_file(filename):
path = inbox_file(filename)
return path.read_text(encoding="utf-8")[:2000]
def run_tool(name, arguments):
try:
if name == "list_inbox":
return list_inbox()
if name == "read_file":
return read_file(arguments.get("filename", ""))
return f"Unknown tool: {name}"
except (OSError, TypeError, ValueError) as error:
return f"Tool error: {error}"
def run_agent(task):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": task},
]
for round_number in range(1, MAX_ROUNDS + 1):
print(f"\n[Agent round {round_number}]")
response = call_ollama(messages, TOOLS)
message = response["message"]
messages.append(message)
content = message.get("content", "").strip()
if content:
print(f"[Agent] {content}")
tool_calls = message.get("tool_calls") or []
if not tool_calls:
return
for tool_call in tool_calls:
function = tool_call.get("function", {})
name = function.get("name", "")
arguments = function.get("arguments") or {}
if not isinstance(arguments, dict):
arguments = {}
print(f"[Tool] {name} {arguments}")
result = run_tool(name, arguments)
print(f"[Result] {result}")
messages.append(
{
"role": "tool",
"tool_name": name,
"content": str(result),
}
)
raise RuntimeError("The agent reached the eight-round safety limit.")
def main():
if not INBOX_DIR.is_dir() or not any(INBOX_DIR.iterdir()):
print("The inbox is empty. Run: python seed_workspace.py")
return
task = (
"Organize every file in the inbox. "
"Inspect each file before proposing one category for it."
)
run_agent(task)
if __name__ == "__main__":
main()
What should the complete file contain?
This version exposes only the two read-only tools. It contains no move function, destination directory, approval prompt, or audit log.
Your agent can now inspect real files without changing them. Next, you will put every proposed file move behind explicit human approval.
Require Approval for File Moves
Your read-only AI agent can inspect the real inbox without escaping its sandbox. It still lacks a safe way to organize what it finds.
This step separates the model's proposals from real file moves. Ordinary Python code performs each move only after you approve the complete plan with the exact confirmation.
In this step, get ready to:
- Give the model a proposal-only tool with three allowed categories.
- Put every real file move behind an explicit approval gate.
- Verify rejection leaves the inbox unchanged before approving the plan.
Let the model record proposals
A proposal is structured data describing a possible move. The new JSON tool schema lets the model choose a filename, category, and reason without giving it access to the filesystem operation.
- Switch back to agent.py in Visual Studio Code.
- Select the file's import and constant section from import json through the current INBOX_DIR assignment.
- Replace the selected section by pasting this code:
import json
import shutil
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen
API_URL = "http://localhost:11434/api/chat"
MODEL = "qwen3:0.6b"
MAX_ROUNDS = 8
ROOT_DIR = Path(__file__).parent.resolve()
WORKSPACE_DIR = (ROOT_DIR / "workspace").resolve()
INBOX_DIR = (WORKSPACE_DIR / "inbox").resolve()
SORTED_DIR = (WORKSPACE_DIR / "sorted").resolve()
AUDIT_FILE = WORKSPACE_DIR / "audit_log.json"
CATEGORIES = {"projects", "personal", "receipts"}
PROPOSALS = []
What does this section add?
- shutil provides the file move operation that ordinary Python calls after approval.
- SORTED_DIR and AUDIT_FILE keep every side effect inside workspace.
- CATEGORIES limits destinations to three known folder names.
- PROPOSALS stores the model's plan in memory until you review it.
- Find the existing read_file() function.
- Add the proposal recorder directly below read_file() by pasting this code:
def propose_move(filename, category, reason):
inbox_file(filename)
category = str(category).strip().lower()
if category not in CATEGORIES:
raise ValueError(f"Unsupported category: {category}")
if any(item["filename"] == filename for item in PROPOSALS):
return f"A proposal already exists for {filename}."
PROPOSALS.append(
{
"filename": filename,
"category": category,
"reason": str(reason).strip(),
}
)
return f"Recorded proposal: {filename} to {category}."
How are proposals kept safe?
propose_move() reuses inbox_file() to reject unsafe filenames. It also rejects every category outside the fixed allowlist.
The function only appends data to PROPOSALS. No directory is created and no file is moved.
- Save agent.py.
- Switch back to the integrated terminal.
- Confirm the existing read-only agent still runs by executing this command:
python agent.py
What does this check prove?
The terminal still displays the existing [Tool] list_inbox and [Tool] read_file activity. Adding proposal storage has not broken the read-only loop.
The agent still summarizes the three inbox files. The newly defined proposal function is waiting for a tool schema and dispatcher route.
Did the regression check fail?
Check that CATEGORIES appears before propose_move(). Confirm that every opening brace or parenthesis in the new function has a matching closing character.
Confirm that Ollama is still running if the terminal reports that the local service cannot be reached.
Help me debug the proposal storage changes in agent.py.
- Find the final closing bracket of TOOLS.
- Add the proposal schema immediately before that bracket by pasting this object:
{
"type": "function",
"function": {
"name": "propose_move",
"description": "Record a proposed category for one inbox file without moving it.",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "A filename returned by list_inbox.",
},
"category": {
"type": "string",
"enum": ["projects", "personal", "receipts"],
"description": "The destination category.",
},
"reason": {
"type": "string",
"description": "A short reason based on the file contents.",
},
},
"required": ["filename", "category", "reason"],
},
},
},
What does the schema control?
The schema tells the model that propose_move requires a filename, category, and reason. Its category enum gives the model only the three accepted destination names.
The Python function repeats these checks at runtime. The model's output never becomes trusted input by assumption.
- Find the existing run_tool() function.
- Replace the complete function by pasting this version:
def run_tool(name, arguments):
try:
if name == "list_inbox":
return list_inbox()
if name == "read_file":
return read_file(arguments.get("filename", ""))
if name == "propose_move":
return propose_move(
arguments.get("filename", ""),
arguments.get("category", ""),
arguments.get("reason", ""),
)
return f"Unknown tool: {name}"
except (OSError, TypeError, ValueError) as error:
return f"Tool error: {error}"
How does dispatch stay bounded?
run_tool() recognizes only the three named tools. An unknown tool becomes a text result instead of an arbitrary function call.
Every expected filesystem or validation failure also becomes a tool result. The agent can observe the failure without gaining extra authority.
- Save agent.py.
- Return to the integrated terminal.
- Run the agent again to confirm its existing tool activity still completes:
python agent.py
What should this run confirm?
The agent completes another bounded run and prints its tool activity. The proposal route now exists even though the current system instructions do not yet require a plan.
Your read-only workflow still completes. Next, the system prompt makes proposal recording part of every organization run.
- Find the existing SYSTEM_PROMPT assignment near the top of agent.py.
- Replace the complete assignment by pasting this prompt:
SYSTEM_PROMPT = """You are a file-organizing agent working in a sandbox.
You must call list_inbox before making claims about the files.
You must call read_file for every file you find.
After reading every file, call propose_move exactly once for each file.
The only categories are projects, personal, and receipts.
propose_move records a plan only. It does not move files.
When all proposals are recorded, briefly summarize the plan and stop calling tools.
"""
Why state the boundary twice?
The prompt tells the model to inspect every file before proposing a destination. It also states that proposals cannot move files.
Python still enforces the actual boundary. Prompt instructions guide behavior while deterministic checks control authority.
Before you run the updated agent, predict which new tool name appears after the read calls.
- Save agent.py.
- Return to the integrated terminal.
- Run the proposal-enabled agent with this command:
python agent.py
What should the agent reveal?
After the read calls, the terminal displays [Tool] propose_move for each inbox file. Each matching result begins with Recorded proposal:.
The three files remain inside workspace/inbox. This run proves the model can build a plan without applying it.
No proposal calls?
Confirm that the propose_move schema sits inside the TOOLS list. Check that the system prompt requires one proposal after every file has been read.
A small local model can occasionally stop early. Start a fresh run after confirming the schema and prompt match the code above.
Help me find why propose_move is not being called.
Build the approval gate
The approval gate displays every proposal before any side effect begins. Exact input keeps accidental keystrokes from being interpreted as consent.
- Find the end of run_agent().
- Add the first part of show_and_apply_proposals() directly below it by pasting this code:
def show_and_apply_proposals():
if not PROPOSALS:
print("\nNo file moves were proposed.")
return
print("\nProposed plan:")
for item in PROPOSALS:
print(
f"- {item['filename']} to {item['category']}: "
f"{item['reason']}"
)
approval = input("\nType YES to apply every move: ").strip()
if approval != "YES":
print("No files were moved.")
return
How does approval work?
The function prints the complete contents of PROPOSALS before requesting consent. The comparison accepts only the exact uppercase value YES.
Every other response returns from the function immediately. That return happens before the move loop exists on the execution path.
- Place the cursor directly after the rejection branch's return line.
- Continue the same function by pasting this move loop with four spaces of indentation:
completed = []
for item in PROPOSALS:
try:
source = inbox_file(item["filename"])
destination_dir = (SORTED_DIR / item["category"]).resolve()
if destination_dir.parent != SORTED_DIR:
raise ValueError("Destination escaped the sorted directory.")
destination_dir.mkdir(parents=True, exist_ok=True)
destination = (destination_dir / source.name).resolve()
if destination.parent != destination_dir:
raise ValueError("Destination filename is unsafe.")
if destination.exists():
raise FileExistsError(f"Destination exists: {destination}")
shutil.move(str(source), str(destination))
completed.append(
{
"filename": item["filename"],
"category": item["category"],
}
)
print(f"Moved {item['filename']} to {item['category']}")
except (OSError, ValueError) as error:
print(f"Skipped {item['filename']}: {error}")
What protects each move?
- inbox_file() validates the source again immediately before the move.
- destination_dir.parent confirms that the category stays directly inside workspace/sorted.
- destination.parent confirms that the filename cannot escape its category folder.
- destination.exists() prevents an approved move from overwriting an existing file.
- Save agent.py.
- Return to the integrated terminal.
- Run the agent to confirm proposal generation still completes:
python agent.py
Why does this run remain harmless?
The current main() still calls only run_agent(). You can see proposals recorded, but the new approval function has not joined the program flow yet.
The inbox remains unchanged after the process ends. This keeps the test loop small before the approval gate becomes active.
- Return to the end of show_and_apply_proposals().
- Add audit-log writing directly below the move loop by pasting this code:
if completed:
AUDIT_FILE.write_text(
json.dumps(completed, indent=2),
encoding="utf-8",
)
print(f"Audit log written to {AUDIT_FILE.relative_to(ROOT_DIR)}")
What enters the audit log?
Only successfully completed moves enter workspace/audit_log.json. Each entry contains the validated filename and category.
The model's free-form reason stays in the displayed plan. The audit file records the deterministic side effects that actually happened.
- Find the existing main() function near the bottom of agent.py.
- Replace the complete function by pasting this final entry point:
def main():
if not INBOX_DIR.is_dir() or not any(INBOX_DIR.iterdir()):
print("The inbox is empty. Run: python seed_workspace.py")
return
task = (
"Organize every file in the inbox. "
"Inspect each file before proposing one category for it."
)
run_agent(task)
show_and_apply_proposals()
How is the workflow connected?
main() first asks the model to inspect every file and build the plan. It calls show_and_apply_proposals() only after the bounded agent loop finishes.
The model never receives shutil.move() as a tool. The approval-gated Python function owns every real side effect.
Before you test rejection, predict whether entering any value except exact uppercase YES can move a file.
- Save agent.py.
- Return to the integrated terminal.
- Start the approval-gated agent by running this command:
python agent.py
What happens before consent?
The agent inspects the inbox and prints its complete proposed plan. The terminal then waits at Type YES to apply every move:.
No move has happened at this point. The program is paused at the approval boundary.
- Enter NO at the approval prompt.
- Press Enter to submit the rejection.
- Expand workspace/inbox in the Explorer sidebar.
The terminal prints No files were moved. The three original files remain visible inside workspace/inbox.
Did a rejected run change a file?
Confirm that the rejection check appears before completed = [] inside show_and_apply_proposals(). Check that its return line has eight spaces of indentation.
Confirm that main() calls the approval function only after run_agent(task) finishes.
Help me debug why rejection did not preserve the inbox.
Approve and verify the organized files
Approval applies the whole reviewed plan as one deliberate decision. The audit log then gives you a machine-readable record of every completed move.
Before the final run, predict what appears inside workspace/sorted after exact approval.
- Return to the integrated terminal.
- Start a fresh organization run with this command:
python agent.py
Review the complete plan
The proposal should contain these three mappings before you approve it:
- project-alpha.txt maps to projects.
- grocery-list.txt maps to personal.
- laptop-receipt.txt maps to receipts.
Exact approval gives ordinary Python permission to apply these proposed destinations. The model still has no direct move tool.
- Enter NO if any proposed category differs from the reviewed mappings.
- Start a fresh run with the same command after rejecting an incorrect plan.
- Enter YES when all three mappings match.
- Press Enter to apply the approved plan.
The terminal lists three Moved messages. It then confirms that the audit log was written inside workspace.
- Expand the workspace folder in the Explorer sidebar.
- Expand the workspace/inbox folder.
- Expand the workspace/sorted/projects folder.
- Expand the workspace/sorted/personal folder.
- Expand the workspace/sorted/receipts folder.
- Open workspace/audit_log.json from the Explorer sidebar.
What should you see?
- workspace/inbox is empty.
- workspace/sorted/projects contains project-alpha.txt.
- workspace/sorted/personal contains grocery-list.txt.
- workspace/sorted/receipts contains laptop-receipt.txt.
- workspace/audit_log.json lists the filename and category for all three completed moves.
You have closed the agent's biggest safety gap. The model can recommend real actions while your approval remains the only path to a filesystem change.
✔️ Awesome, I've got everything!
Great. Double check that you saved agent.py after the approved run.
ⓧ I'd like to double check the full code
Compare your complete agent.py file with this approval-gated version.
import json
import shutil
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen
API_URL = "http://localhost:11434/api/chat"
MODEL = "qwen3:0.6b"
MAX_ROUNDS = 8
ROOT_DIR = Path(__file__).parent.resolve()
WORKSPACE_DIR = (ROOT_DIR / "workspace").resolve()
INBOX_DIR = (WORKSPACE_DIR / "inbox").resolve()
SORTED_DIR = (WORKSPACE_DIR / "sorted").resolve()
AUDIT_FILE = WORKSPACE_DIR / "audit_log.json"
CATEGORIES = {"projects", "personal", "receipts"}
PROPOSALS = []
SYSTEM_PROMPT = """You are a file-organizing agent working in a sandbox.
You must call list_inbox before making claims about the files.
You must call read_file for every file you find.
After reading every file, call propose_move exactly once for each file.
The only categories are projects, personal, and receipts.
propose_move records a plan only. It does not move files.
When all proposals are recorded, briefly summarize the plan and stop calling tools.
"""
TOOLS = [
{
"type": "function",
"function": {
"name": "list_inbox",
"description": "List the text files currently inside the sandbox inbox.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read one text file from the sandbox inbox.",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "A filename returned by list_inbox.",
}
},
"required": ["filename"],
},
},
},
{
"type": "function",
"function": {
"name": "propose_move",
"description": "Record a proposed category for one inbox file without moving it.",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "A filename returned by list_inbox.",
},
"category": {
"type": "string",
"enum": ["projects", "personal", "receipts"],
"description": "The destination category.",
},
"reason": {
"type": "string",
"description": "A short reason based on the file contents.",
},
},
"required": ["filename", "category", "reason"],
},
},
},
]
def call_ollama(messages, tools=None):
payload = {
"model": MODEL,
"messages": messages,
"stream": False,
"think": False,
}
if tools:
payload["tools"] = tools
request = Request(
API_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))
except URLError as error:
raise RuntimeError(
"Cannot reach Ollama. Confirm that Ollama is running on Windows."
) from error
def inbox_file(filename):
if Path(filename).name != filename:
raise ValueError("Only a plain filename is allowed.")
candidate = (INBOX_DIR / filename).resolve()
if candidate.parent != INBOX_DIR:
raise ValueError("The requested path is outside the inbox.")
if not candidate.is_file():
raise ValueError(f"Inbox file not found: {filename}")
return candidate
def list_inbox():
files = sorted(
path.name for path in INBOX_DIR.iterdir() if path.is_file()
)
return json.dumps(files)
def read_file(filename):
path = inbox_file(filename)
return path.read_text(encoding="utf-8")[:2000]
def propose_move(filename, category, reason):
inbox_file(filename)
category = str(category).strip().lower()
if category not in CATEGORIES:
raise ValueError(f"Unsupported category: {category}")
if any(item["filename"] == filename for item in PROPOSALS):
return f"A proposal already exists for {filename}."
PROPOSALS.append(
{
"filename": filename,
"category": category,
"reason": str(reason).strip(),
}
)
return f"Recorded proposal: {filename} to {category}."
def run_tool(name, arguments):
try:
if name == "list_inbox":
return list_inbox()
if name == "read_file":
return read_file(arguments.get("filename", ""))
if name == "propose_move":
return propose_move(
arguments.get("filename", ""),
arguments.get("category", ""),
arguments.get("reason", ""),
)
return f"Unknown tool: {name}"
except (OSError, TypeError, ValueError) as error:
return f"Tool error: {error}"
def run_agent(task):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": task},
]
for round_number in range(1, MAX_ROUNDS + 1):
print(f"\n[Agent round {round_number}]")
response = call_ollama(messages, TOOLS)
message = response["message"]
messages.append(message)
content = message.get("content", "").strip()
if content:
print(f"[Agent] {content}")
tool_calls = message.get("tool_calls") or []
if not tool_calls:
return
for tool_call in tool_calls:
function = tool_call.get("function", {})
name = function.get("name", "")
arguments = function.get("arguments") or {}
if not isinstance(arguments, dict):
arguments = {}
print(f"[Tool] {name} {arguments}")
result = run_tool(name, arguments)
print(f"[Result] {result}")
messages.append(
{
"role": "tool",
"tool_name": name,
"content": str(result),
}
)
raise RuntimeError("The agent reached the eight-round safety limit.")
def show_and_apply_proposals():
if not PROPOSALS:
print("\nNo file moves were proposed.")
return
print("\nProposed plan:")
for item in PROPOSALS:
print(
f"- {item['filename']} to {item['category']}: "
f"{item['reason']}"
)
approval = input("\nType YES to apply every move: ").strip()
if approval != "YES":
print("No files were moved.")
return
completed = []
for item in PROPOSALS:
try:
source = inbox_file(item["filename"])
destination_dir = (SORTED_DIR / item["category"]).resolve()
if destination_dir.parent != SORTED_DIR:
raise ValueError("Destination escaped the sorted directory.")
destination_dir.mkdir(parents=True, exist_ok=True)
destination = (destination_dir / source.name).resolve()
if destination.parent != destination_dir:
raise ValueError("Destination filename is unsafe.")
if destination.exists():
raise FileExistsError(f"Destination exists: {destination}")
shutil.move(str(source), str(destination))
completed.append(
{
"filename": item["filename"],
"category": item["category"],
}
)
print(f"Moved {item['filename']} to {item['category']}")
except (OSError, ValueError) as error:
print(f"Skipped {item['filename']}: {error}")
if completed:
AUDIT_FILE.write_text(
json.dumps(completed, indent=2),
encoding="utf-8",
)
print(f"Audit log written to {AUDIT_FILE.relative_to(ROOT_DIR)}")
def main():
if not INBOX_DIR.is_dir() or not any(INBOX_DIR.iterdir()):
print("The inbox is empty. Run: python seed_workspace.py")
return
task = (
"Organize every file in the inbox. "
"Inspect each file before proposing one category for it."
)
run_agent(task)
show_and_apply_proposals()
if __name__ == "__main__":
main()
How to use this reference
This is the complete cumulative file for comparison. Keep your existing agent.py when it matches this reference.
Secret mission
Undo the Last Organization Run
Your audit log already records the files moved during an approved organization run. Build a rollback utility that validates the complete batch before restoring those files to the inbox.
Clean Up Your Resources
Clean Up Your Resources
Everything you built stays on your Windows computer, so local inference creates no ongoing cloud bill. Choose whether to keep the setup, pause Ollama, or remove the local resources.
Resources you used:
- The qwen3:0.6b model, which occupies 523 MB of local storage.
- The Ollama Windows installation, which requires at least 4 GB of local storage.
- The local-file-agent folder containing agent.py, seed_workspace.py, undo.py, and the workspace sandbox.
Keep everything running
No cleanup is required. Choose this option if you want to keep testing the agent or extend its safety controls.
- Leave Ollama running with its local API available at http://localhost:11434.
- Retain the qwen3:0.6b model for future local AI projects.
- Keep the local-file-agent folder with your agent, rollback utility, and sandbox files.
Pause - I'll come back to this later
Pause the background service to stop local model activity while keeping every file ready for later. The installed model continues to occupy 523 MB of disk space.
- Quit Ollama from the Windows taskbar.
- Confirm Ollama is no longer running in the taskbar.
- Keep the qwen3:0.6b model installed.
- Retain the local-file-agent folder with its restored inbox files.
- Reopen Ollama from the Windows Start menu when you want to run the agent again.
Delete - I don't want to use this again
Deleting these local resources is final. Save a copy of local-file-agent first if you want to keep your code for reference.
- Remove the local model by replacing <model-name> with qwen3:0.6b in this documented pattern before you run it in PowerShell:
ollama rm <model-name>
What Does This Remove?
This command deletes the selected model files from local storage. Ollama remains installed for other models.
- Confirm PowerShell returns without an error after removing the model.
Model Command Not Working?
Check that you replaced the placeholder with the exact model identifier. Help me remove the local Ollama model.
- Select Command Prompt from the terminal profile menu in Visual Studio Code.
- Confirm the terminal path ends with local-file-agent.
- Delete local-file-agent from its parent folder by running these commands:
cd ..
rmdir /s /q local-file-agent
What Do These Commands Do?
- The first line moves Command Prompt to the folder that contains local-file-agent.
- The second line permanently removes the project folder and everything inside it.
- Confirm the local-file-agent folder no longer appears in its parent folder.
Folder Still Present?
Close Visual Studio Code if Windows reports that the folder is in use. Return to Command Prompt from the folder that contained local-file-agent before retrying the deletion. Help me delete the local project folder.
- Quit Ollama from the Windows taskbar.
- Press the Windows key to open search.
- Enter Add or remove programs in the search field.
- Select Ollama from the installed apps.
- Choose Uninstall for Ollama.
- Follow the Windows confirmation prompts to complete the removal.
- Confirm Ollama no longer appears in the installed apps.
Nice Work!
Nice Work!
You did it! You built a local file-organizing AI agent powered by Ollama. Every file move stays behind your explicit approval.
You learned how to:
- Built local inference into agent.py through Ollama's chat endpoint. You proved the model could answer without a cloud account or API key.
- Experienced why a chatbot cannot inspect local files without tools. You added JSON tool schemas for read-only access. A bounded agent loop returned each real observation to the model.
- Protected the sandbox with path validation. You kept model proposals separate from deterministic Python file moves. You required YES before any move. An audit log recorded each completed move.
- Secret Mission: Added a rollback utility in undo.py. It validates the last approved batch before restoring files in reverse order. The audit log disappears only after every restoration succeeds.
Ready to quiz yourself?