Build a Local Budget Analyst

Build a local CLI budget analyst with LangChain, Ollama, and Python tools.

Introduction

30 Second Summary

Personal spending often hides across many small purchases. A simple budget question becomes guesswork when your records have no way to calculate the facts.

In this project, you will build a private command-line budget analyst powered by Qwen3 4B through Ollama. LangChain routes natural-language questions to Python tools that calculate answers from sample transactions.

What You'll Build

When you finish, each spending question produces a visible tool trace followed by an exact answer from your sample transactions.

By the end of this project, you'll have:

  • A private local conversation where you can ask repeated budget questions in everyday language.
  • A visible model limitation that proves why personal spending answers need connected records.
  • Exact answers for total spending. You can also check category spending or identify the largest expense through visible tool calls.
  • Secret Mission: Add a category budget check that reports whether spending is under or over a monthly limit.

Are there any prerequisites?

You need basic Python knowledge plus confidence using a terminal. This project assumes Python 3.10+ and Visual Studio Code are already installed.

Step 1 sets up every remaining dependency.

Before We Start

Before the hands-on work begins, lock in the private budget analyst you are building. Your design uses the local qwen3:4b model through LangChain to interpret questions while deterministic Python tools calculate transaction facts.

Set Up the Local Agent Workspace

Your budget analyst can stay private when its model runs on your computer. It also needs an isolated Python environment so every package version stays predictable.

This step connects Ollama to Qwen3 4B for local responses. You will also prepare a Visual Studio Code workspace with pinned LangChain packages.

In this step, get ready to:
  • Install Ollama and confirm its version.
  • Download qwen3:4b and verify that it responds locally.
  • Create an isolated workspace with the pinned Python dependencies.
Install and verify Ollama

Ollama runs language models through a local service on your computer. The project uses release v0.33.3 as its verified baseline.

  • Check whether Ollama is already available by running this command in your terminal:
ollama -v

What does this command do?

The version check asks the Ollama command-line tool to report its installed release. Your result determines which setup path you need.

✔️ I see the required version

Your Ollama installation is ready when the output reports version 0.33.3 or higher. Continue below to confirm the local service.

ⓧ I see an older version

Update Ollama through the official path for your operating system. The verified release for this project is v0.33.3.

macOS

  • Open the official Ollama macOS guide in your browser.
  • Download ollama.dmg from the official guide.
  • Mount ollama.dmg.
  • Drag Ollama into the Applications folder.
  • Launch Ollama from your Applications folder.
  • Allow Ollama to place its command-line tool in your path when prompted.

Windows

Ollama requires Windows 10 22H2 or newer. The installer runs per user without Administrator rights.

  • Open the official Ollama Windows guide in your browser.
  • Download OllamaSetup.exe from the official guide.
  • Run OllamaSetup.exe.
  • Complete the installer using its default options.

Linux

  • Update Ollama through its official installer by running this command:
curl -fsSL https://ollama.com/install.sh | sh
What does this command do?

This downloads Ollama's official Linux installation script over HTTPS. The script installs the current stable release for your system.

ⓧ Command not found

Ollama needs to be installed before your terminal can use its command-line tool. Choose the instructions for your operating system.

macOS

  • Open the official Ollama macOS guide in your browser.
  • Download ollama.dmg from the official guide.
  • Mount ollama.dmg.
  • Drag Ollama into the Applications folder.
  • Launch Ollama from your Applications folder.
  • Allow Ollama to place its command-line tool in your path when prompted.

Windows

Ollama requires Windows 10 22H2 or newer. The installer runs per user without Administrator rights.

  • Open the official Ollama Windows guide in your browser.
  • Download OllamaSetup.exe from the official guide.
  • Run OllamaSetup.exe.
  • Complete the installer using its default options.

Linux

  • Install Ollama through its official installer by running this command:
curl -fsSL https://ollama.com/install.sh | sh
What does this command do?

This downloads Ollama's official Linux installation script over HTTPS. The script installs the current stable release for your system.

  • Confirm the completed installation by running the version check again:
ollama -v

What does this check prove?

A reported version proves that your terminal can reach the installed Ollama command-line tool. Version 0.33.3 or higher meets this project's baseline.

Ollama is now installed. Its local service is ready to manage the model that powers your analyst.

Still cannot run Ollama?

  • Close your current terminal after installation. Start a new terminal so it reloads the updated command path.
  • Launch the Ollama application on macOS or Windows before checking the version again.
  • Review the official installation guide for your operating system if the command still cannot be found.

Need a hand? Help me diagnose why Ollama is unavailable after installation.

Download and test qwen3:4b

The Ollama service needs a model before it can generate a response. This project uses qwen3:4b because the model supports tool calling for the agent you build later.

The first run downloads a 2.5GB model artifact. Connection speed controls how long this takes, so a quiet terminal during the download is expected.

  • Download the model and start its local chat by running this command:
ollama run qwen3:4b

What does this command do?

Ollama downloads qwen3:4b when the model is missing. It then starts an interactive chat backed by the local model.

Before you send the test prompt, do you expect the response to come from your computer or a cloud API?

  • Test the running model by entering this prompt:
Reply with LOCAL MODEL READY

What does this prompt test?

The prompt gives the model one precise response target. A reply containing the requested phrase proves that local generation is working.

You should see the model respond with LOCAL MODEL READY or a short sentence containing that phrase. That is your first working local AI response.

  • End the model chat cleanly by entering this command:
/bye

What does this command do?

The chat command closes the interactive model session. The downloaded model remains available for your Python application.

Model download or response failing?

  • Keep Ollama running while you retry the model command.
  • Check that your connection has enough time to finish the 2.5GB download.
  • Confirm that the model identifier is exactly qwen3:4b.

Still stuck? Help me troubleshoot my local qwen3:4b model.

Create the isolated Python workspace

A virtual environment keeps this project's packages separate from other Python projects. The requirements.txt file records the exact dependency versions needed to reproduce the workspace.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Visual Studio Code and press Enter to open it.

You should see the Visual Studio Code window. You can now create the folder that holds the entire project.

  • Click File in the top menu.
  • Click Open Folder....

You should see your operating system's folder picker. This picker lets you create the workspace in a location you can find again.

  • Select your Desktop location.
  • Click New Folder.

You should see a field for the new folder name.

  • Enter local-budget-agent as the folder name.
  • Click Open on macOS or Select Folder on Windows.
  • Select Yes, I trust the authors if Visual Studio Code displays the Workspace Trust dialog.

The Explorer sidebar should now show local-budget-agent as the open workspace.

  • Click View in the top menu.
  • Click Terminal.

The integrated terminal should open inside the local-budget-agent folder. Commands you run here now apply to this project.

  • Create the project's .venv environment by running this command:
python -m venv .venv

What does this command do?

Python creates an isolated interpreter and package directory inside .venv. Packages installed after activation stay scoped to this project.

The .venv folder should now appear in the Explorer sidebar.

Activation updates the current terminal so its python command uses the environment you just created. Choose the command for your operating system.

macOS

  • Activate .venv in the Visual Studio Code terminal by running this command:
source .venv/bin/activate

What does this command do?

The activation script places the environment's Python interpreter first in the terminal path. The terminal prompt should now include the .venv environment name.

Windows

  • Activate .venv in a PowerShell terminal by running this command:
.\.venv\Scripts\Activate.ps1

What does this command do?

The PowerShell activation script places the environment's Python interpreter first in the terminal path. The terminal prompt should now include the .venv environment name.

Linux

  • Activate .venv in a Bash or Zsh terminal by running this command:
source .venv/bin/activate

What does this command do?

The activation script places the environment's Python interpreter first in the terminal path. The terminal prompt should now include the .venv environment name.

Virtual environment not activating?

  • Confirm that the Explorer sidebar contains the .venv folder.
  • Confirm that the terminal is running inside local-budget-agent.
  • Select Command Prompt from the terminal profile menu on Windows if PowerShell blocks the activation script.

Need help? Help me activate my .venv environment.

  • Create a new file in the Explorer sidebar using the New File control.
  • Enter requirements.txt as the file name.

The empty requirements.txt file should open in the editor. This file pins the two packages used by the local agent.

  • Add the pinned dependencies to requirements.txt by pasting this content:
langchain==1.4.0
langchain-ollama==1.1.0

What do these dependencies provide?

  • LangChain 1.4.0 provides the agent harness that selects and executes Python tools.
  • langchain-ollama 1.1.0 provides the ChatOllama integration used to reach your local model.
  • Save requirements.txt.
  • Confirm that requirements.txt is listed directly inside local-budget-agent in the Explorer sidebar.

✔️ Awesome, I've got everything!

Great. Confirm that requirements.txt is saved inside local-budget-agent.

ⓧ I'd like to double check the full code

Compare your file with this complete version:

langchain==1.4.0
langchain-ollama==1.1.0

What should match?

Your file should contain exactly these two dependency pins. Each package belongs on its own line.

  • Install the pinned packages into the active environment by running this command:
python -m pip install -r requirements.txt

What does this command do?

Python runs the pip installer from the active environment. The -r option tells it to install every dependency listed in requirements.txt.

Package installation may print several dependency lines. Wait until the terminal returns to its prompt before continuing.

Before you run the final check, do you think both imports will succeed inside the active environment?

  • Verify both installed packages by running this command:
python -c "import langchain; import langchain_ollama"

What does this verification do?

Python starts briefly and imports both installed modules. A successful check exits without a traceback.

You should return to the terminal prompt without an import error. That confirms both pinned packages are available inside .venv.

Seeing an import error?

  • Confirm that your terminal prompt shows the active .venv environment.
  • Confirm that requirements.txt contains both dependency lines exactly.
  • Run the installation command again after the environment is active.

Still blocked? Help me fix my LangChain imports.

Your local agent workspace is ready. Next, you will connect Python to the model and experience the data blind spot that separates a chatbot from a grounded budget analyst.

Expose the Model's Data Blind Spot

Your local Qwen3 model can already respond through Ollama. That confirms the local model is ready.

A working chat model still has no route to your private transaction records. You'll connect the model through LangChain's ChatOllama integration. A fixed blocked state makes that missing-data boundary visible.

In this step, get ready to:
  • Configure a local ChatOllama model connection.
  • Generate a one-sentence budget assistant introduction.
  • Expose the absence of connected transaction records.
Configure the local model

ChatOllama gives Python a direct interface to the local model served by Ollama. The configuration keeps responses consistent. It also checks that qwen3:4b is available when the file starts.

  • In the Explorer sidebar in VS Code, click New File.
  • Type budget_agent.py in the file name field.
  • Press Enter to create the file inside local-budget-agent.

You'll see budget_agent.py listed beside requirements.txt in the Explorer sidebar.

  • Connect Python to the local model by pasting this code into budget_agent.py:
from langchain_ollama import ChatOllama


model = ChatOllama(
    model="qwen3:4b",
    temperature=0,
    reasoning=False,
    validate_model_on_init=True,
)

What Does This Setup Do?

  • The import makes the ChatOllama integration available to your Python file.
  • The model setting selects the local qwen3:4b model.
  • The temperature value keeps variation low. The reasoning value disables separate reasoning output.
  • The validate_model_on_init setting checks that the selected model is available during startup.
  • Save budget_agent.py.
  • Check the model connection by running this command in the activated VS Code terminal:
python budget_agent.py

What Does This Check Prove?

Python creates the ChatOllama model instance. The startup validation checks whether Ollama can provide qwen3:4b to the application.

You should see the terminal return to its prompt without a traceback. That confirms Python can initialize the local model.

Does the Model Validation Fail?

  • Make sure the local Ollama service from the previous step is still running.
  • Check that the model value in budget_agent.py matches qwen3:4b exactly.

Still stuck? Help me diagnose why ChatOllama cannot initialize my local qwen3:4b model.

Generate a local introduction

A model connection becomes useful when your application sends it a prompt. The response stores the generated text in its content property.

  • Add the model invocation below the model configuration by pasting this code:
response = model.invoke(
    "Introduce yourself as a budget assistant in one sentence."
)
print(f"Assistant: {response.content}")

What Does This Code Do?

  • The invoke call sends one instruction to the local model.
  • The response variable holds the model's reply.
  • The print statement exposes the reply in the terminal with an Assistant: label.
  • Save budget_agent.py.
  • Generate the local introduction by running this command:
python budget_agent.py

What Should You See?

You'll see Assistant: followed by a one-sentence introduction generated by your local model. The wording can vary because the answer comes from the model.

No Introduction in the Terminal?

  • Confirm that response = model.invoke( begins below the closing parenthesis of the model configuration.
  • Check that the print statement uses response.content exactly.

Need a hand? Help me find why budget_agent.py does not print the ChatOllama response.

Expose the missing transaction access

The introduction proves that the model can generate text. It provides no evidence that the model knows your private spending history. A deterministic blocked state makes this limitation impossible to miss.

  • Add the record count below the assistant print statement by pasting these lines:
print("Connected records: 0")
print("Blocked: no transaction data or tools are connected.")

Why Add a Fixed Blocked State?

The first line exposes how many application records the model can access. The second line creates a reliable guardrail when no transaction data or calculation tools are connected.

  • Save budget_agent.py.

Before you run the complete file, make a quick prediction about whether the introduction proves the model knows your spending history.

  • Reveal the model's data blind spot by running this command:
python budget_agent.py

What Should You See?

  • You'll see a model-generated introduction after Assistant:.
  • You'll see Connected records: 0.
  • You'll see Blocked: no transaction data or tools are connected..

You've surfaced the key limitation. Your local model can speak like a budget assistant while remaining completely disconnected from your personal transactions.

Missing the Blocked State?

  • Make sure both new print statements sit below print(f"Assistant: {response.content}").
  • Save budget_agent.py before running the command again.

Still not seeing all three lines? Help me compare my budget_agent.py file with the expected blocked-state version.

✔️ Awesome, I've got everything!

Your file now produces a local response. It also exposes the missing transaction connection.

ⓧ I'd like to double check the full code

  • Compare budget_agent.py with this complete version:
from langchain_ollama import ChatOllama


model = ChatOllama(
    model="qwen3:4b",
    temperature=0,
    reasoning=False,
    validate_model_on_init=True,
)

response = model.invoke(
    "Introduce yourself as a budget assistant in one sentence."
)
print(f"Assistant: {response.content}")
print("Connected records: 0")
print("Blocked: no transaction data or tools are connected.")

The model's blind spot is now visible. Next, you'll connect deterministic transaction calculations that give the analyst grounded financial facts.

Build Deterministic Budget Tools

Your local model can introduce itself as a budget assistant. It still cannot calculate from records it has never received.

This step gives the application a six-record dataset. You will expose that data through deterministic tools written in Python.

In this step, get ready to:
  • Add six sample transactions to the budget analyst.
  • Calculate total spending for the full dataset or one category.
  • Identify the largest expense through a repeatable calculation.
Add the transaction records

Each transaction uses a dictionary to keep its date, category, description, and amount together. The six dictionaries form the private dataset that your tools can calculate from.

  • In budget_agent.py, select everything from model = ChatOllama( through print("Blocked: no transaction data or tools are connected.").
  • Replace the selected code with the first half of TRANSACTIONS by pasting this snippet:
TRANSACTIONS = [
    {
        "date": "2026-08-02",
        "category": "Groceries",
        "description": "Neighborhood market",
        "amount": 54.25,
    },
    {
        "date": "2026-08-05",
        "category": "Transport",
        "description": "Train pass",
        "amount": 18.40,
    },
    {
        "date": "2026-08-09",
        "category": "Dining",
        "description": "Lunch with friends",
        "amount": 32.10,
    },

What does this first half contain?

  • Each dictionary represents one transaction with four consistent fields.
  • The amount values remain numbers so the tools can add or compare them.
  • This snippet intentionally leaves the TRANSACTIONS list open. The next snippet closes it before you save or run the file.
  • Complete the same TRANSACTIONS list by pasting this snippet immediately after the last line:
    {
        "date": "2026-08-13",
        "category": "Groceries",
        "description": "Fresh produce",
        "amount": 27.75,
    },
    {
        "date": "2026-08-18",
        "category": "Utilities",
        "description": "Electricity bill",
        "amount": 89.50,
    },
    {
        "date": "2026-08-22",
        "category": "Entertainment",
        "description": "Cinema tickets",
        "amount": 24.00,
    },
]

How is the dataset structured?

  • The closing bracket completes one list containing six transaction dictionaries.
  • Repeated categories let the analyst combine separate purchases into one category total.
  • Consistent keys let every function read the records through the same structure.
  • Save budget_agent.py.
  • Count the "date" keys inside TRANSACTIONS.

You should count six date keys. That confirms every sample transaction is present.

Does the list look incomplete?

  • Check that TRANSACTIONS begins with an opening square bracket.
  • Check that every transaction dictionary ends with a comma.
  • Check that the final line closes the list with a square bracket.

Still stuck? Help me check the structure of my transaction list.

Calculate totals and categories

A deterministic function returns the same result whenever it receives the same records. Type hints define the tool input while docstrings tell LangChain when each function is useful.

  • In budget_agent.py, place your cursor on a blank line immediately below the closing bracket for TRANSACTIONS.
  • Add the total calculation by pasting this function:
def total_spending() -> str:
    """Calculate total spending across all available transactions."""
    total = sum(transaction["amount"] for transaction in TRANSACTIONS)
    return f"Total spending: ${total:.2f}"

What does this function calculate?

  • The -> str type hint states that the function returns text.
  • The docstring describes the calculation for the future agent.
  • The sum() expression adds every transaction amount.
  • The final format keeps the result at two decimal places.
  • Add a direct test call at the bottom of budget_agent.py by pasting this line:
print(total_spending())

Why call the function directly?

The direct call checks the calculation without involving the model. This isolates the financial logic from generated text.

  • Save budget_agent.py.
  • Check the first calculation by running this command in the terminal from earlier:
python budget_agent.py

What should you see?

You should see Total spending: $246.00. The result comes from adding all six transaction amounts.

Is the total missing?

  • Check that print(total_spending()) sits below the function definition.
  • Check that every transaction uses the exact amount key.
  • Check that the terminal is still inside the local-budget-agent folder.

Need another pair of eyes? Help me debug my total spending calculation.

Category questions need one extra step. The function normalizes the requested category before filtering matching transactions.

  • Find print(total_spending()) near the bottom of budget_agent.py.
  • Insert the category function directly above that line by pasting this code:
def spending_by_category(category: str) -> str:
    """Calculate spending for one category using case-insensitive matching."""
    normalized_category = category.strip().casefold()
    matches = [
        transaction
        for transaction in TRANSACTIONS
        if transaction["category"].casefold() == normalized_category
    ]

    if not matches:
        categories = ", ".join(
            sorted({transaction["category"] for transaction in TRANSACTIONS})
        )
        return f"No transactions found for {category}. Available categories: {categories}."

    total = sum(transaction["amount"] for transaction in matches)
    display_category = matches[0]["category"]
    return f"{display_category} spending: ${total:.2f}"

How does category matching work?

  • The category: str type hint defines the tool input.
  • The normalized_category value removes surrounding spaces before applying case-insensitive matching.
  • The matches list keeps only transactions from the requested category.
  • The fallback returns the available categories when no records match.
  • Add the category test directly below print(total_spending()) by pasting this line:
print(spending_by_category("groceries"))

What does this test prove?

The lowercase input tests the case-insensitive matching. The function still displays the category name from the matching records.

  • Save budget_agent.py.
  • Check both calculations by running this command:
python budget_agent.py

What should you see now?

You should see Total spending: $246.00 followed by Groceries spending: $82.00.

Is the groceries total wrong?

  • Check that both grocery transactions use the exact Groceries category.
  • Check that matches compares each category with normalized_category.
  • Check that the test passes "groceries" into spending_by_category().

Still seeing a different value? Help me trace my category calculation.

Find the largest expense

The final tool compares transaction amounts with max(). It returns the complete record so the result can include the category, description, amount, and date.

  • Find print(total_spending()) near the bottom of budget_agent.py.
  • Insert the largest-expense function directly above that line by pasting this code:
def largest_expense() -> str:
    """Find the single transaction with the largest amount."""
    transaction = max(TRANSACTIONS, key=lambda item: item["amount"])
    return (
        f"Largest expense: {transaction['category']} - "
        f"{transaction['description']} - ${transaction['amount']:.2f} "
        f"on {transaction['date']}"
    )

How does the comparison work?

  • The key function tells max() to compare each transaction by its amount.
  • The selected transaction keeps every field from the winning record.
  • The return value formats those fields as one grounded summary.
  • Add the final direct test below print(spending_by_category("groceries")) by pasting this line:
print(largest_expense())

What does the final call check?

This call checks the largest-expense calculation beside the two totals. One run now verifies all three tools against the same dataset.

  • Save budget_agent.py.

Before you run the complete check, which transaction do you expect max() to select?

  • Verify all three calculations by running this command:
python budget_agent.py

What should the complete check show?

  • You should see Total spending: $246.00.
  • You should see Groceries spending: $82.00.
  • You should see Largest expense: Utilities - Electricity bill - $89.50 on 2026-08-18.

Does the largest expense differ?

  • Check that the electricity transaction has an amount of 89.50.
  • Check that max() uses item["amount"] as its comparison key.
  • Check that print(largest_expense()) is the final line in the file.

Need help comparing your output? Help me debug the largest expense result.

That is the grounding layer working. Your three calculations now come directly from the transaction records.

✔️ Awesome, I've got everything!

Great. Confirm that you saved budget_agent.py.

Your terminal should show the verified total, groceries total, and largest expense.

What have you proved?

Each function produces a grounded result from the same six records. The model is no longer responsible for financial arithmetic.

ⓧ I'd like to double check the full code

from langchain_ollama import ChatOllama


TRANSACTIONS = [
    {
        "date": "2026-08-02",
        "category": "Groceries",
        "description": "Neighborhood market",
        "amount": 54.25,
    },
    {
        "date": "2026-08-05",
        "category": "Transport",
        "description": "Train pass",
        "amount": 18.40,
    },
    {
        "date": "2026-08-09",
        "category": "Dining",
        "description": "Lunch with friends",
        "amount": 32.10,
    },
    {
        "date": "2026-08-13",
        "category": "Groceries",
        "description": "Fresh produce",
        "amount": 27.75,
    },
    {
        "date": "2026-08-18",
        "category": "Utilities",
        "description": "Electricity bill",
        "amount": 89.50,
    },
    {
        "date": "2026-08-22",
        "category": "Entertainment",
        "description": "Cinema tickets",
        "amount": 24.00,
    },
]


def total_spending() -> str:
    """Calculate total spending across all available transactions."""
    total = sum(transaction["amount"] for transaction in TRANSACTIONS)
    return f"Total spending: ${total:.2f}"


def spending_by_category(category: str) -> str:
    """Calculate spending for one category using case-insensitive matching."""
    normalized_category = category.strip().casefold()
    matches = [
        transaction
        for transaction in TRANSACTIONS
        if transaction["category"].casefold() == normalized_category
    ]

    if not matches:
        categories = ", ".join(
            sorted({transaction["category"] for transaction in TRANSACTIONS})
        )
        return f"No transactions found for {category}. Available categories: {categories}."

    total = sum(transaction["amount"] for transaction in matches)
    display_category = matches[0]["category"]
    return f"{display_category} spending: ${total:.2f}"


def largest_expense() -> str:
    """Find the single transaction with the largest amount."""
    transaction = max(TRANSACTIONS, key=lambda item: item["amount"])
    return (
        f"Largest expense: {transaction['category']} - "
        f"{transaction['description']} - ${transaction['amount']:.2f} "
        f"on {transaction['date']}"
    )


print(total_spending())
print(spending_by_category("groceries"))
print(largest_expense())

How should you compare the file?

Check the dataset first. Then compare the three function definitions and the three direct test calls in order.

Your budget facts are now repeatable and grounded in code. Next, LangChain can choose the right function from a natural-language question.

Let LangChain Run the Tools

Your three Python functions now calculate exact answers from six transactions. A person still needs to know which function to call.

A LangChain agent loop maps an everyday question to an available function.

The loop executes that function. The model then explains the grounded result.

In this step, get ready to:
  • Prepare the validated local model for agent use.
  • Register the three budget functions with a strict system prompt.
  • Trace the agent's answer to the selected budget tool.
Prepare the local model

The local Qwen3 model becomes the decision-maker for tool selection. Validation checks that the named model is available when your script starts.

  • In budget_agent.py, find the existing import at the top of the file.
  • Use this current import as your reference:
from langchain_ollama import ChatOllama

What are you changing?

The current import gives your script access to ChatOllama. The agent also needs the create_agent harness from LangChain.

  • Replace the current import with these two imports:
from langchain.agents import create_agent
from langchain_ollama import ChatOllama

What do these imports provide?

  • ChatOllama connects LangChain to the model served by your local Ollama service.
  • The create_agent function manages model requests and tool execution.
  • In budget_agent.py, find the closing parenthesis of largest_expense().
  • Add the validated model below the function by pasting:
model = ChatOllama(
    model="qwen3:4b",
    temperature=0,
    reasoning=False,
    validate_model_on_init=True,
)

What does this model setup do?

  • The model field selects the local qwen3:4b model.
  • A temperature of 0 keeps model behavior focused.
  • The reasoning setting disables separate reasoning output.
  • The validate_model_on_init setting checks that Ollama can find the model during initialization.
  • Save budget_agent.py.
  • Confirm that the model initializes by running:
python budget_agent.py

What does this check prove?

The script creates the validated model before reaching the existing direct function calls. Seeing the previous totals proves that model initialization completed.

You should still see Total spending: $246.00, Groceries spending: $82.00, and the $89.50 largest expense.

Does model validation stop the script?

Confirm that the local Ollama service from earlier is still running. Check that qwen3:4b remains available to Ollama.

Still stuck? Help me diagnose why ChatOllama cannot validate my local qwen3:4b model.

Create the budget agent

An agent needs a model for interpreting questions. It also needs deterministic tools for retrieving financial facts.

  • In budget_agent.py, find the closing parenthesis of the model setup.
  • Add the agent configuration below it by pasting:
agent = create_agent(
    model=model,
    tools=[total_spending, spending_by_category, largest_expense],
    system_prompt=(
        "You are a local budget analyst. Always use one or more available tools "
        "for questions about transactions, totals, categories, or largest expenses. "
        "Never invent transaction data. Keep the final answer concise."
    ),
)

Why use create_agent?

  • The model argument gives the agent access to your local Qwen3 model.
  • The tools list exposes your three typed Python functions to the model.
  • The system_prompt requires grounded tool use for transaction questions.
  • The create_agent harness handles the repeated model and tool cycle for you.
  • Save budget_agent.py.
  • Confirm that the agent can be created by running:
python budget_agent.py

What does this run confirm?

The script now constructs the model and the agent before running the old direct checks. The same three deterministic values confirm that both objects were created successfully.

That is the agent wiring in place. Your three functions are now available to LangChain as tools.

Does agent creation fail?

Check that create_agent is imported from langchain.agents. Confirm that every function in the tools list matches its definition exactly.

Need another pair of eyes? Help me find why my LangChain agent cannot register the three budget functions.

Ask a question and inspect the tool call

The direct checks prove that each function works in isolation. The next run proves that natural language can select the correct function.

  • At the bottom of budget_agent.py, find the three direct function calls.
  • Use these current lines as your reference:
print(total_spending())
print(spending_by_category("groceries"))
print(largest_expense())

Why replace these calls?

These lines require you to choose the Python function yourself. Replacing them lets the agent choose a tool from the wording of a question.

  • Replace the three direct calls with this agent invocation and trace:
result = agent.invoke(
    {"messages": [{"role": "user", "content": "How much did I spend on groceries?"}]}
)

for message in result["messages"]:
    for tool_call in getattr(message, "tool_calls", []):
        print(f"[tool] {tool_call['name']} {tool_call['args']}")

print(f"Assistant: {result['messages'][-1].content}")

How does the trace work?

  • The agent.invoke call sends the groceries question through the agent loop.
  • The outer loop inspects every message produced during that run.
  • The inner loop prints each selected tool name with its argument dictionary.
  • The final line prints the last AI message after the tool result has returned.
  • Save budget_agent.py.

Before you run it, consider this question: which tool should the agent select for a groceries total.

  • Run the completed agent by entering:
python budget_agent.py

What happens during this run?

The model interprets the groceries question. LangChain executes the selected Python function before requesting the final response.

You should see a [tool] trace containing spending_by_category and its category argument. The final assistant response should be grounded in $82.00.

Missing the category tool trace?

Confirm that spending_by_category appears in the agent's tools list. Check that the system prompt still requires tools for category questions.

Need help reading the result? Help me inspect why my local LangChain agent did not show a spending_by_category tool call.

✔️ Awesome, I've got everything!

Great work. Your agent now chooses spending_by_category from a natural-language question and grounds its answer in $82.00.

ⓧ I'd like to double check the full code

Compare your complete budget_agent.py file with this reference:

from langchain.agents import create_agent
from langchain_ollama import ChatOllama


TRANSACTIONS = [
    {
        "date": "2026-08-02",
        "category": "Groceries",
        "description": "Neighborhood market",
        "amount": 54.25,
    },
    {
        "date": "2026-08-05",
        "category": "Transport",
        "description": "Train pass",
        "amount": 18.40,
    },
    {
        "date": "2026-08-09",
        "category": "Dining",
        "description": "Lunch with friends",
        "amount": 32.10,
    },
    {
        "date": "2026-08-13",
        "category": "Groceries",
        "description": "Fresh produce",
        "amount": 27.75,
    },
    {
        "date": "2026-08-18",
        "category": "Utilities",
        "description": "Electricity bill",
        "amount": 89.50,
    },
    {
        "date": "2026-08-22",
        "category": "Entertainment",
        "description": "Cinema tickets",
        "amount": 24.00,
    },
]


def total_spending() -> str:
    """Calculate total spending across all available transactions."""
    total = sum(transaction["amount"] for transaction in TRANSACTIONS)
    return f"Total spending: ${total:.2f}"


def spending_by_category(category: str) -> str:
    """Calculate spending for one category using case-insensitive matching."""
    normalized_category = category.strip().casefold()
    matches = [
        transaction
        for transaction in TRANSACTIONS
        if transaction["category"].casefold() == normalized_category
    ]

    if not matches:
        categories = ", ".join(
            sorted({transaction["category"] for transaction in TRANSACTIONS})
        )
        return f"No transactions found for {category}. Available categories: {categories}."

    total = sum(transaction["amount"] for transaction in matches)
    display_category = matches[0]["category"]
    return f"{display_category} spending: ${total:.2f}"


def largest_expense() -> str:
    """Find the single transaction with the largest amount."""
    transaction = max(TRANSACTIONS, key=lambda item: item["amount"])
    return (
        f"Largest expense: {transaction['category']} - "
        f"{transaction['description']} - ${transaction['amount']:.2f} "
        f"on {transaction['date']}"
    )


model = ChatOllama(
    model="qwen3:4b",
    temperature=0,
    reasoning=False,
    validate_model_on_init=True,
)

agent = create_agent(
    model=model,
    tools=[total_spending, spending_by_category, largest_expense],
    system_prompt=(
        "You are a local budget analyst. Always use one or more available tools "
        "for questions about transactions, totals, categories, or largest expenses. "
        "Never invent transaction data. Keep the final answer concise."
    ),
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "How much did I spend on groceries?"}]}
)

for message in result["messages"]:
    for tool_call in getattr(message, "tool_calls", []):
        print(f"[tool] {tool_call['name']} {tool_call['args']}")

print(f"Assistant: {result['messages'][-1].content}")

Use this full-file reference to check the import order, blank lines, tool list, system prompt, invocation, and trace loop.

Your local model can now turn a natural-language question into a deterministic tool call. Next, you will replace the hardcoded question with an interactive terminal conversation.

Build an Interactive Analyst

Your LangChain agent already turns a groceries question into a grounded answer. The hardcoded question only proves that path once.

A reusable analyst needs a terminal loop that accepts new questions. You will also display each selected tool so the model's choices stay visible.

In this step, get ready to:
  • Wrap the existing agent invocation in ask_agent(question).
  • Add a terminal loop for repeated questions.
  • Verify three natural-language questions against three deterministic tools.
Wrap one question in a reusable function

The bottom of budget_agent.py currently handles one fixed question. A reusable function lets the terminal loop send any question through the same agent path.

  • In the budget_agent.py file from earlier, locate the line that starts with result = agent.invoke(.
  • Select that line through the final print(f"Assistant: {result['messages'][-1].content}") line.
  • Replace the selected block by pasting this function:
def ask_agent(question: str) -> None:
    """Ask the budget agent a question and display its tool calls and answer."""
    result = agent.invoke(
        {"messages": [{"role": "user", "content": question}]}
    )

    for message in result["messages"]:
        for tool_call in getattr(message, "tool_calls", []):
            print(f"[tool] {tool_call['name']} {tool_call['args']}")

    print(f"Assistant: {result['messages'][-1].content}")

What Does This Function Do?

  • The question parameter holds one natural-language request.
  • The agent.invoke(...) call sends that request through the existing agent loop.
  • The nested loops inspect every returned message for tool calls.
  • The final print displays the answer after the selected tools finish.
  • Save budget_agent.py.
  • In the VS Code editor from earlier, confirm that ask_agent appears below agent without a red syntax underline.

Seeing a Syntax Warning?

  • Check that every line inside ask_agent uses four spaces of indentation.
  • Confirm that the old hardcoded invocation no longer appears below the new function.

Still stuck? Help me check the ask_agent function for indentation or replacement errors.

Add the terminal conversation loop

The main() function owns the command-line interface. Its loop keeps accepting questions until the learner enters one of the exit words.

  • Place your cursor below the ask_agent function in budget_agent.py.
  • Add the interactive entry point by pasting this code:
def main() -> None:
    """Run an interactive local budget-analysis session."""
    print("Local Budget Analyst")
    print("Ask about total spending, a category, or the largest expense.")
    print("Enter 'quit' or 'exit' to stop.")

    while True:
        question = input("\nYou: ").strip()
        if question.casefold() in {"quit", "exit"}:
            print("Session ended.")
            break
        if not question:
            continue
        ask_agent(question)


if __name__ == "__main__":
    main()

How Does the Loop Work?

  • The opening print calls explain what the analyst can answer.
  • The while True loop keeps the session available for repeated questions.
  • The casefold() check recognizes either exit word regardless of capitalization.
  • The final guard starts main() when you run the file directly.
  • Save budget_agent.py.

Before you run the analyst, do you expect the terminal to answer immediately or wait for a question? Make your prediction now.

  • Return to the activated terminal from earlier.
  • Start the interactive analyst by running:
python budget_agent.py

What Should I See?

You will see Local Budget Analyst followed by usage guidance. The You: prompt proves that main() is waiting for your first question.

Does the Analyst Exit Immediately?

  • Check that main() contains the entire while True block.
  • Check that the final main() call is indented beneath the if __name__ == "__main__": guard.

Need another pair of eyes? Help me debug why the interactive analyst does not stay open for questions.

✔️ Awesome, I've got everything!

Your file now contains the complete interactive entry point. The terminal is ready for repeated budget questions.

ⓧ I'd like to double check the full code

from langchain.agents import create_agent
from langchain_ollama import ChatOllama


TRANSACTIONS = [
    {
        "date": "2026-08-02",
        "category": "Groceries",
        "description": "Neighborhood market",
        "amount": 54.25,
    },
    {
        "date": "2026-08-05",
        "category": "Transport",
        "description": "Train pass",
        "amount": 18.40,
    },
    {
        "date": "2026-08-09",
        "category": "Dining",
        "description": "Lunch with friends",
        "amount": 32.10,
    },
    {
        "date": "2026-08-13",
        "category": "Groceries",
        "description": "Fresh produce",
        "amount": 27.75,
    },
    {
        "date": "2026-08-18",
        "category": "Utilities",
        "description": "Electricity bill",
        "amount": 89.50,
    },
    {
        "date": "2026-08-22",
        "category": "Entertainment",
        "description": "Cinema tickets",
        "amount": 24.00,
    },
]


def total_spending() -> str:
    """Calculate total spending across all available transactions."""
    total = sum(transaction["amount"] for transaction in TRANSACTIONS)
    return f"Total spending: ${total:.2f}"


def spending_by_category(category: str) -> str:
    """Calculate spending for one category using case-insensitive matching."""
    normalized_category = category.strip().casefold()
    matches = [
        transaction
        for transaction in TRANSACTIONS
        if transaction["category"].casefold() == normalized_category
    ]

    if not matches:
        categories = ", ".join(
            sorted({transaction["category"] for transaction in TRANSACTIONS})
        )
        return f"No transactions found for {category}. Available categories: {categories}."

    total = sum(transaction["amount"] for transaction in matches)
    display_category = matches[0]["category"]
    return f"{display_category} spending: ${total:.2f}"


def largest_expense() -> str:
    """Find the single transaction with the largest amount."""
    transaction = max(TRANSACTIONS, key=lambda item: item["amount"])
    return (
        f"Largest expense: {transaction['category']} - "
        f"{transaction['description']} - ${transaction['amount']:.2f} "
        f"on {transaction['date']}"
    )


model = ChatOllama(
    model="qwen3:4b",
    temperature=0,
    reasoning=False,
    validate_model_on_init=True,
)

agent = create_agent(
    model=model,
    tools=[total_spending, spending_by_category, largest_expense],
    system_prompt=(
        "You are a local budget analyst. Always use one or more available tools "
        "for questions about transactions, totals, categories, or largest expenses. "
        "Never invent transaction data. Keep the final answer concise."
    ),
)


def ask_agent(question: str) -> None:
    """Ask the budget agent a question and display its tool calls and answer."""
    result = agent.invoke(
        {"messages": [{"role": "user", "content": question}]}
    )

    for message in result["messages"]:
        for tool_call in getattr(message, "tool_calls", []):
            print(f"[tool] {tool_call['name']} {tool_call['args']}")

    print(f"Assistant: {result['messages'][-1].content}")


def main() -> None:
    """Run an interactive local budget-analysis session."""
    print("Local Budget Analyst")
    print("Ask about total spending, a category, or the largest expense.")
    print("Enter 'quit' or 'exit' to stop.")

    while True:
        question = input("\nYou: ").strip()
        if question.casefold() in {"quit", "exit"}:
            print("Session ended.")
            break
        if not question:
            continue
        ask_agent(question)


if __name__ == "__main__":
    main()

What Should Match?

The completed file keeps the existing dataset and deterministic tools. Its new bottom section contains ask_agent(question) followed by main() and the direct-run guard.

Test all three tool paths

Each demonstration question targets a different deterministic calculation. The trace reveals whether the model routes each request to the correct Python function.

Before you enter the questions, which tool do you expect the agent to choose for each one? Make your predictions now.

  • Enter What is my total spending? at the You: prompt.

You will see a trace naming total_spending. The final answer is grounded in $246.00.

  • Enter How much did I spend on dining? at the next prompt.

You will see a trace naming spending_by_category with a dining argument. The final answer is grounded in $32.10.

  • Enter What was my largest expense? at the next prompt.

You will see a trace naming largest_expense. The answer identifies the electricity bill as $89.50.

Why Might the Wording Differ?

The local model can phrase its final responses differently between runs. The selected tool and deterministic dollar value are the reliable checks.

  • Enter quit at the next prompt.

You will see Session ended. before the terminal returns to its normal command prompt.

Did the Wrong Tool Run?

  • Confirm that the existing agent still registers all three budget functions.
  • Confirm that the existing system prompt still requires tools for transaction questions.
  • Run the same question again if the local model produced an unexpected route.

Need help reading the trace? Help me compare my selected tools and dollar values with the expected interactive analyst results.

You now have a reusable local analyst that maps natural-language questions to different Python tools. The visible traces prove which calculation grounded each answer.

Secret mission

Add Category Budget Checks

Your analyst can explain where your money went. Extend it with monthly category limits so it can show whether spending is under budget or over budget.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your local setup ready, pause its running service, or delete it. There are no cloud or paid API charges.

Resources you used:

  • The local-budget-agent folder containing budget_agent.py, requirements.txt, plus the .venv environment with pinned LangChain packages.
  • The downloaded qwen3:4b model.
  • The Ollama runtime hosting the local model service.

Keep everything running

No action is needed. Choose this option if you are still experimenting with your local budget analyst.

  • Leave the local Ollama service running for immediate access.
  • Keep qwen3:4b downloaded for future sessions.
  • Keep the local-budget-agent folder unchanged.

Your analyst remains ready for more transaction questions or budget experiments.

Pause - I'll come back to this later

Shut down the running processes to free up memory. Your downloaded model plus project files remain available for later.

macOS

  • Enter quit in the interactive analyst if the session is still running.
  • Quit the Ollama application to stop the local model service.

Your qwen3:4b model plus the local-budget-agent folder remain stored on your computer.

Windows

  • Enter quit in the interactive analyst if the session is still running.
  • Quit the Ollama application to stop the local model service.

Your qwen3:4b model plus the local-budget-agent folder remain stored on your computer.

Linux

Your qwen3:4b model plus the local-budget-agent folder remain stored on your computer.

Delete - I don't want to use this again

Removing your local setup is permanent. Your cloud accounts remain unaffected because this project created no cloud resources.

Delete the local project folder:

macOS

  • Enter quit in the interactive analyst if the session is still running.
  • Remove the activated environment plus the local-budget-agent folder by running these commands in the terminal from earlier:
deactivate
cd ..
rm -rf local-budget-agent
What Does This Cleanup Do?
  • The deactivate command leaves the active virtual environment.
  • The cd command moves the terminal into the parent folder.
  • The rm command permanently removes the project folder plus everything inside it.
Folder Still Present?

Close any editor or terminal process using the project folder before trying again. Help me remove my local project folder safely.

Windows

  • Enter quit in the interactive analyst if the session is still running.
  • Remove the activated environment plus the local-budget-agent folder by running these commands in PowerShell:
deactivate
Set-Location ..
Remove-Item -Recurse -Force local-budget-agent
What Does This Cleanup Do?
  • The deactivate command leaves the active virtual environment.
  • The Set-Location command moves PowerShell into the parent folder.
  • The Remove-Item command permanently removes the project folder plus everything inside it.
Folder Still Present?

Close any editor or terminal process using the project folder before trying again. Help me remove my local project folder safely.

Linux

  • Enter quit in the interactive analyst if the session is still running.
  • Remove the activated environment plus the local-budget-agent folder by running these commands in the terminal from earlier:
deactivate
cd ..
rm -rf local-budget-agent
What Does This Cleanup Do?
  • The deactivate command leaves the active virtual environment.
  • The cd command moves the terminal into the parent folder.
  • The rm command permanently removes the project folder plus everything inside it.
Folder Still Present?

Close any editor or terminal process using the project folder before trying again. Help me remove my local project folder safely.

Remove the downloaded model:

Remove the Ollama runtime:

macOS

  • Quit Ollama if it is still running.
  • Follow Ollama's official macOS instructions to remove the application.
  • Complete the documented local-data removal steps to clear any remaining Ollama files.

Windows

  • Quit Ollama if it is still running.
  • Follow Ollama's official Windows instructions to remove the application.
  • Complete the documented local-data removal steps to clear any remaining Ollama files.

Linux

  • Stop the local Ollama service.
  • Follow Ollama's official Linux instructions to remove the runtime.
  • Complete the documented local-data removal steps to clear any remaining Ollama files.

Nice Work!

Nice Work!

You did it! Your private budget analyst now turns natural-language spending questions into calculations grounded in local transaction data. It runs through Ollama on your computer without a cloud API key.

You've learned how to:

  • Connected the qwen3:4b model to LangChain through ChatOllama to generate local responses.
  • Turned the model's data blind spot into grounded calculations by building deterministic Python tools over six transaction records.
  • Built an interactive agent loop that maps natural-language questions to the correct function while displaying visible tool-call traces.
  • Completed the optional Secret Mission by adding check_category_budget() for category budget comparisons with remaining or over-budget amounts.

Ready to quiz yourself?