Build a Private AI Notes Organizer

Build a private browser app that organizes notes with Ollama.

Introduction

30 Second Summary

Study notes, meeting updates, and personal reminders often end up in the same rough block of text. Turning that block into clear next steps can take longer than writing it.

In this project, you will build a local browser app that uses Ollama with qwen3:8b to organize messy notes. You will replace one free-form response with validated structured output that your interface can display reliably.

What You'll Build

Your finished Private AI Notes Organizer transforms pasted notes into a private dashboard with a concise summary, clear key points, prioritized actions, and follow-up questions.

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

  • A local analysis workflow that sends pasted notes to qwen3:8b on your own computer.
  • An organized dashboard built with Streamlit that separates the summary, key points, prioritized actions, and follow-up questions.
  • A validated data flow built with Pydantic that replaces one free-form response with predictable fields. Clear messages also explain empty input or an Ollama error.
  • Secret Mission: Detect whether notes are Study, Meeting, Personal, or Mixed. Adapt the analysis to match the detected category.

What do I need before starting?

Basic Python knowledge is enough. The project assumes your Windows device already has Ollama, qwen3:8b, Python, and Visual Studio Code installed.

Before We Start

A clear first example gives your private local notes organizer a real purpose before the hands-on work begins. Your choice will anchor the model tests that follow.

Set Up and Verify the Local Stack

Your organizer depends on Python to run the app. Ollama supplies the local model connection.

A clean virtual environment keeps package problems separate from model problems. A successful local reply proves the AI layer works before you start building.

In this step, get ready to:
  • Confirm that Python supports the pinned packages.
  • Confirm that qwen3:8b responds through Ollama.
  • Prepare a working local app environment.
Verify Python and the local model

Streamlit 1.66.0 supports Python 3.11 through 3.15. The version check confirms that your interpreter fits this range.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Press Ctrl+` to open the integrated terminal.
  • Check your installed Python version by running this command:
python --version

What does this command check?

The command prints the Python interpreter version used by your terminal. The result determines whether Streamlit can run in this project.

✔️ I see a supported version

Good. Your terminal is using Python 3.11 through 3.15, so it is ready for the pinned packages.

ⓧ My version is outside the supported range

Your current interpreter cannot run the pinned Streamlit release. Install Python 3.15.0 before you continue.

  • Download Python 3.15.0 from the official Python Windows downloads page.
  • Run the downloaded Windows installer.
  • Close Visual Studio Code after the installation finishes.
  • Press the Windows key to reopen Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to reopen Visual Studio Code.
  • Press Ctrl+` to open a fresh integrated terminal.
  • Recheck the active Python version by running:
python --version

Why run the check again?

A fresh terminal loads the updated Python installation. The repeated check confirms that the terminal now uses a supported interpreter.

You should now see Python 3.11 through 3.15 in the output.

Still seeing an unsupported Python version?

  • Make sure you restarted Visual Studio Code after installing the supported release.
  • Check that the new terminal shows the same supported version before continuing.

Help me fix the Python version used by my VS Code terminal. Share the version output in the NextWork community if your Windows setup needs more help.

The next check starts an interactive qwen3:8b session on your computer. A reply confirms that the model can accept a prompt through Ollama.

Before you run this, do you expect the terminal to show a model reply or a browser page? Make your prediction now.

  • Start a local qwen3:8b chat by running this command:
ollama run qwen3:8b

What does this command do?

Ollama starts the downloaded qwen3:8b model in an interactive terminal session. Your prompts stay on the local Ollama connection.

  • Type Hello from my notes organizer at the interactive prompt.
  • Press Enter to send the greeting.

You should see qwen3:8b reply beneath your greeting. The wording can vary because the response comes from the model.

  • Press Ctrl+D after the reply to leave the interactive session.

That local reply is an important milestone. Your model connection is ready before any application code enters the picture.

No local model reply?

  • Confirm that Ollama is running in the background on Windows.
  • Check that the command uses the exact model identifier qwen3:8b.
  • Wait for the interactive prompt before entering your greeting.

Help me diagnose why qwen3:8b is not replying through Ollama. Share the non-sensitive terminal output in the NextWork community if the problem continues.

Create the isolated project

Your project needs one named folder for its code. Keeping it on your Desktop makes the files easy to find during the remaining steps.

  • Create the private-ai-notes-organizer folder on your Desktop by running these commands:
  • Confirm that you trust the folder if Visual Studio Code asks because you created it yourself.
Set-Location "$HOME\Desktop"
New-Item -Path "private-ai-notes-organizer" -ItemType Directory
Set-Location "private-ai-notes-organizer"
code .

What do these commands do?

  • The first command moves the terminal to your Desktop.
  • The second command creates the private-ai-notes-organizer folder.
  • The third command makes that folder the terminal's current location.
  • The final command opens the current folder as a Visual Studio Code workspace.
  • Confirm that the Explorer sidebar shows private-ai-notes-organizer as the open folder.

Your project now has a fixed home. Every file created from this point belongs inside that folder.

Project folder did not open?

  • Check that each command completed before the next command ran.
  • Confirm that the terminal location ends with private-ai-notes-organizer.
  • Use the Visual Studio Code window that shows the project folder in the Explorer sidebar.

Help me open my new project folder in Visual Studio Code.

The requirements.txt file records the exact package versions used by the project. Version pins keep your environment aligned with the code you build later.

  • Select the file-shaped plus icon beside private-ai-notes-organizer in the Explorer sidebar.

You should see a field for the new filename in the Explorer sidebar.

  • Enter requirements.txt into the filename field.
  • Press Enter to create the file.

You should see a blank requirements.txt file open in the editor.

  • Fill requirements.txt with the pinned dependencies by pasting this content:
ollama==0.6.3
pydantic==2.14.1
streamlit==1.66.0

Why pin these packages?

  • The ollama==0.6.3 package lets Python call the local Ollama service.
  • Pydantic 2.14.1 validates the structured model output you add later.
  • Streamlit 1.66.0 turns your Python script into a browser app.
  • Save requirements.txt by pressing Ctrl+S.
  • Confirm that requirements.txt remains listed inside private-ai-notes-organizer in the Explorer sidebar.

Dependency file looks different?

  • Check that the filename is exactly requirements.txt.
  • Remove any spaces around the double equals signs.
  • Compare every package version with the reference below.

Help me check my requirements file for package or version mistakes.

✔️ Awesome, I've got everything!

Great. Double check that you saved requirements.txt inside private-ai-notes-organizer.

ⓧ I'd like to double check the full code

Compare your complete requirements.txt file with this reference.

ollama==0.6.3
pydantic==2.14.1
streamlit==1.66.0

What should match?

All three lines should match exactly. This file is the package source of truth for your virtual environment.

The isolated environment stores this project's Python packages inside .venv. This keeps the pinned versions away from packages used by your other Python projects.

  • Create the virtual environment inside private-ai-notes-organizer by running:
python -m venv .venv

What does this command create?

Python creates a .venv folder containing an isolated interpreter. The folder also holds project-specific package installations.

  • Confirm that a .venv folder appears in the Explorer sidebar.

The new folder proves that Python created the isolated environment inside your project.

No .venv folder?

  • Confirm that the terminal is inside private-ai-notes-organizer.
  • Check that the Python version in this terminal is within the supported range.
  • Rerun the environment command after correcting the terminal location.

Help me diagnose why Python did not create my .venv folder.

Activation routes Python package commands into the new environment. The terminal prompt changes to show which environment is active.

  • Activate .venv in the PowerShell terminal by running:
.venv\Scripts\Activate.ps1

What does activation change?

Activation makes the .venv interpreter the default Python for this terminal. Package installation now stays inside the project environment.

  • Check the beginning of your terminal prompt.

You should see (.venv) at the start of the prompt. That prefix confirms the environment is active.

Virtual environment did not activate?

  • Confirm that the terminal shell is PowerShell.
  • Check that .venv\Scripts\Activate.ps1 exists inside your project folder.
  • Make sure the terminal is still inside private-ai-notes-organizer.

Help me activate my Windows PowerShell virtual environment. Share the non-sensitive PowerShell output in the NextWork community if activation remains blocked.

Install dependencies and launch Streamlit

The active environment is empty until you install the packages listed in requirements.txt. Installing from that file reproduces the same package set every time.

  • Install the pinned project dependencies into the active environment by running:
python -m pip install -r requirements.txt

What does this command install?

Python asks pip to read every line in requirements.txt. Pip installs those exact package versions inside the active .venv environment.

  • Keep the terminal open until the installation finishes.
  • Confirm that the terminal returns to a prompt with (.venv) still visible.

The returned prompt confirms that the pinned packages finished installing in your isolated environment.

Dependency installation failed?

  • Confirm that (.venv) appears at the start of the terminal prompt.
  • Check that your Python version remains within the supported range.
  • Confirm that requirements.txt contains the three exact package pins.

Help me diagnose my pinned package installation failure. Share the non-sensitive installation output in the NextWork community if the failure depends on your machine.

The final check launches a real Streamlit example app. The command keeps the local server active in your terminal while the browser displays the app.

Before you run this, do you expect the example app to open in your browser or stay inside the terminal? Make your prediction now.

  • Launch the visible Streamlit setup check by running:
python -m streamlit hello

What does this command prove?

Python starts Streamlit from the active virtual environment. Streamlit opens its example app in your browser, which proves the local browser-app stack is ready.

You should see the Streamlit example app open in your browser. The terminal remains occupied by the local Streamlit server.

Browser stayed closed?

  • Confirm that the terminal still shows the running Streamlit server.
  • Copy the local address printed in the terminal.
  • Paste that address into your browser to open the example app.

Help me open the Streamlit example app from my local terminal output.

You have now proved every layer of the local stack. Python is compatible, qwen3:8b replies locally, and Streamlit can serve a browser app.

Your local stack is ready. Next, you will replace the example page with the first working version of your private notes organizer.

Build the First AI Note Summarizer

Your isolated environment is ready. The browser check proved that Streamlit can serve a local app from this project.

Now you can connect that browser interface to Ollama through a local model call. This first version deliberately displays the answer as one free-form block.

You will test how dependable that format is after the complete input-to-model-to-screen loop works.

In this step, get ready to:
  • Build a browser input for pasted notes.
  • Connect the input to the local qwen3:8b model.
  • Test the organizer with notes that contain a fact plus an action.
Build the note input

A Streamlit script describes the browser interface with Python. Each widget stores the learner's input in a variable that the rest of the script can use.

  • Use the file control at the top of VS Code's left sidebar to create app.py inside the project folder.
  • Build the first browser interface by pasting this code into app.py:
import ollama
import streamlit as st

MODEL_NAME = "qwen3:8b"


st.set_page_config(page_title="Private AI Notes Organizer")
st.title("Private AI Notes Organizer")
st.write(
    "Paste study, meeting, or personal notes. "
    "Your local qwen3:8b model will organize them."
)

notes = st.text_area(
    "Notes to organize",
    height=240,
    placeholder="Paste your notes here...",
)

if st.button("Organize notes", type="primary", width="stretch"):
    if not notes.strip():
        st.error("Paste some notes before running the organizer.")

What does this interface code do?

  • The imports make the local model client plus Streamlit interface tools available to the script.
  • The MODEL_NAME constant keeps the qwen3:8b identifier in one place.
  • The page configuration plus title establish the organizer's browser view.
  • The st.text_area() widget stores the pasted text in notes.
  • The button block stops an empty submission with a clear message.
  • Save app.py.
  • Return to the activated VS Code terminal from the previous step.
  • Stop the Streamlit example app by pressing Ctrl+C.

The next command starts a local server for app.py. The terminal remains busy until you stop the server with Ctrl+C.

  • Start your notes organizer by running:
python -m streamlit run app.py

What does this command do?

Python starts Streamlit from the activated virtual environment. Streamlit serves app.py as a local browser app.

Your browser should show the Private AI Notes Organizer title. You should also see a Notes to organize text area above an Organize notes button.

Your project now has its own browser interface. The notes field is ready to capture text for the local model.

Does the organizer fail to load?

  • Confirm that the VS Code terminal still shows the activated .venv environment.
  • Confirm that app.py is saved inside the project folder shown in the VS Code sidebar.
  • Check the terminal for a Python syntax message that points to a mismatched quote or parenthesis.

Help me diagnose why my Streamlit app does not open in the browser.

Connect the local model

A system message defines the organizer's role. A user message carries the notes that the learner pasted.

Ollama receives both messages through one chat request. The response content can then flow directly back into the browser.

  • In app.py, place the cursor on a new line directly below the empty-input error inside the button block.
  • Add the model branch by pasting this code:
    else:
        system_message = (
            "You organize study, meeting, and personal notes. "
            "Summarize the notes and identify useful next actions."
        )
        user_message = f"Notes:\n{notes}"
        response = ollama.chat(
            model=MODEL_NAME,
            messages=[
                {"role": "system", "content": system_message},
                {"role": "user", "content": user_message},
            ],
        )
        st.subheader("AI Response")
        st.write(response.message.content)

How does the model call work?

  • The else branch runs when the notes contain visible text.
  • The system_message tells the model to organize supported note categories.
  • The user_message labels the pasted text as notes for the model.
  • The ollama.chat() call sends both message roles to qwen3:8b.
  • The response.message.content value holds the completed answer that Streamlit displays.
  • Save app.py.
  • Return to the organizer in your browser.
  • Refresh the browser page.
  • Confirm that the notes field plus Organize notes button render without a Python error.

Does the interface show a Python error?

  • Check that else: aligns with if not notes.strip():.
  • Check that every line inside the else branch has one additional indentation level.
  • Compare the closing parentheses plus brackets with the complete file below.

Help me fix the indentation or syntax in my Ollama chat branch.

✔️ Awesome, I've got everything!

Great. Double check that app.py is saved before testing the model call.

ⓧ I'd like to double check the full code

Compare your current app.py with this complete version:

import ollama
import streamlit as st

MODEL_NAME = "qwen3:8b"


st.set_page_config(page_title="Private AI Notes Organizer")
st.title("Private AI Notes Organizer")
st.write(
    "Paste study, meeting, or personal notes. "
    "Your local qwen3:8b model will organize them."
)

notes = st.text_area(
    "Notes to organize",
    height=240,
    placeholder="Paste your notes here...",
)

if st.button("Organize notes", type="primary", width="stretch"):
    if not notes.strip():
        st.error("Paste some notes before running the organizer.")
    else:
        system_message = (
            "You organize study, meeting, and personal notes. "
            "Summarize the notes and identify useful next actions."
        )
        user_message = f"Notes:\n{notes}"
        response = ollama.chat(
            model=MODEL_NAME,
            messages=[
                {"role": "system", "content": system_message},
                {"role": "user", "content": user_message},
            ],
        )
        st.subheader("AI Response")
        st.write(response.message.content)

How the pieces fit

The top half builds the browser input. The button branch sends valid notes to the local model.

The final two lines place the returned text beneath the AI Response heading.

Test the free-form response

Free-form generation lets the model choose the answer's wording plus internal presentation. This test reveals how that choice affects the interface.

  • Paste non-sensitive mixed notes containing at least one fact plus one action into the Notes to organize field.

The local model uses your computer's hardware. A pause after the click is normal.

Before you click, do you expect the app to produce dependable dashboard sections or one presentation-dependent response? Keep that prediction in mind.

  • Click Organize notes.

You should see one AI Response heading with the model's complete answer beneath it. The wording plus internal formatting can vary between inputs.

This is the intended shortfall. The answer contains useful organization, while its structure remains locked inside one presentation-dependent string.

Does the model fail to respond?

  • Confirm that Ollama is still running locally on your computer.
  • Confirm that the notes field contains visible text before clicking the button.
  • Confirm that the terminal running Streamlit remains open.

Help me diagnose why my local qwen3:8b request does not return a response.

That is the full local loop working: your browser sends notes to qwen3:8b and displays the returned answer. Next, you will give that answer a schema so the interface receives predictable fields.

Make the Output Predictable

Your Streamlit app already sends notes to qwen3:8b and displays the reply in one free-form block.

A free-form reply cannot be reliably split into interface sections. In this step, a Pydantic schema will constrain the response before your app uses it.

In this step, get ready to:
  • Define models for the complete analysis and each prioritized action.
  • Generate a schema that guides the local model's response.
  • Validate the response before displaying it as a structured dictionary.
Define the validation models

A data model describes the fields your app expects. It can also restrict a field to a small set of permitted values.

  • In app.py, replace the two import lines at the top with this import group:
import json
from typing import Literal

import ollama
import streamlit as st
from pydantic import BaseModel

What do these imports provide?

  • The json module converts the generated schema into text for the prompt.
  • Literal restricts a field to specific permitted values.
  • BaseModel provides the foundation for models that validate incoming data.
  • Save app.py.
  • Return to the running app in your browser.

You should still see the Private AI Notes Organizer title and its notes input. This confirms the new imports load successfully.

Does the app stop loading?

Check that each import matches the code above. Confirm that the terminal still shows the activated .venv environment.

Help me troubleshoot the new imports in app.py.

  • Add the validation models below MODEL_NAME = "qwen3:8b" by copying this code:
class ActionItem(BaseModel):
    task: str
    priority: Literal["High", "Medium", "Low"]


class NoteAnalysis(BaseModel):
    summary: str
    key_points: list[str]
    action_items: list[ActionItem]
    follow_up_questions: list[str]

How do the models shape the result?

  • ActionItem requires a task plus a priority of High, Medium, or Low.
  • NoteAnalysis requires a summary plus three lists for the rest of the analysis.
  • action_items uses ActionItem so every generated action follows the same structure.
  • Save app.py.

Before you test the app, do you think defining the models alone changes the model's reply?

  • Return to the running app in your browser.
  • Paste the same mixed notes from the previous step into Notes to organize.
  • Click Organize notes.

You will still see one free-form response under AI Response. The models now describe the required structure, but the model call does not use them yet.

Does the app show a model definition error?

Compare both class names and their indentation with the code above. Make sure NoteAnalysis starts after the complete ActionItem class.

Help me fix the Pydantic model definitions.

Build the structured analysis function

A JSON Schema turns your models into a machine-readable contract. The same schema can guide Ollama and validate the returned text.

  • Add the analysis function immediately below the NoteAnalysis class by copying this code:
def analyze_notes(notes: str) -> NoteAnalysis:
    schema = NoteAnalysis.model_json_schema()
    system_message = (
        "You organize study, meeting, and personal notes. "
        "Extract only information supported by the notes. "
        "Use empty lists when a section has no supported items."
    )
    user_message = (
        "Return only JSON that matches this schema:\n"
        f"{json.dumps(schema)}\n\n"
        "Notes:\n"
        f"{notes}"
    )

    response = ollama.chat(
        model=MODEL_NAME,
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": user_message},
        ],
        format=schema,
        options={"temperature": 0},
    )

    return NoteAnalysis.model_validate_json(response.message.content)

What does this function do?

  • NoteAnalysis.model_json_schema() generates the contract from your Pydantic models.
  • json.dumps(schema) includes that contract in the user message.
  • format=schema asks the local model to return data that follows the contract.
  • options={"temperature": 0} makes the structured response more deterministic.
  • NoteAnalysis.model_validate_json() rejects returned text that does not satisfy the model.
  • Save app.py.
  • Return to the running app in your browser.

You should still see the title and notes input. This confirms the new function loads without interrupting the app.

Does the function stop the app?

Check the indentation inside analyze_notes(). Confirm that both message definitions and the model call remain inside the function.

Help me debug the analyze_notes function.

Connect the validated result

The button still runs the old inline model call. Replacing that block routes every non-empty note through analyze_notes() before anything reaches the page.

  • Select the existing button block from if st.button("Organize notes", type="primary", width="stretch"): through st.write(response.message.content).
  • Replace the selected block with this code:
if st.button("Organize notes", type="primary", width="stretch"):
    if not notes.strip():
        st.error("Paste some notes before running the organizer.")
    else:
        analysis = analyze_notes(notes)
        st.subheader("Validated analysis")
        st.write(analysis.model_dump())

How does the button use validated data?

  • analysis = analyze_notes(notes) sends the pasted text through the schema-constrained pipeline.
  • analysis.model_dump() converts the validated model into a dictionary that Streamlit can display.
  • Save app.py.

Before you test it, do you expect one free-form paragraph or a dictionary split into named fields?

  • Refresh the running app in your browser.
  • Paste the same mixed notes into Notes to organize.
  • Click Organize notes.

You should see a dictionary under Validated analysis with summary, key_points, action_items, and follow_up_questions fields.

That reliability gap is closed. Your app now turns the local model's reply into data with a predictable shape.

Does the validated dictionary fail to appear?

Confirm that the button block calls analyze_notes(notes). Check that the display line uses analysis.model_dump().

Help me troubleshoot the structured response.

✔️ Awesome, I've got everything!

Your saved app.py now validates the model response and displays a structured dictionary.

ⓧ I'd like to double check the full code

Compare your saved app.py with the complete file below.

import json
from typing import Literal

import ollama
import streamlit as st
from pydantic import BaseModel

MODEL_NAME = "qwen3:8b"


class ActionItem(BaseModel):
    task: str
    priority: Literal["High", "Medium", "Low"]


class NoteAnalysis(BaseModel):
    summary: str
    key_points: list[str]
    action_items: list[ActionItem]
    follow_up_questions: list[str]


def analyze_notes(notes: str) -> NoteAnalysis:
    schema = NoteAnalysis.model_json_schema()
    system_message = (
        "You organize study, meeting, and personal notes. "
        "Extract only information supported by the notes. "
        "Use empty lists when a section has no supported items."
    )
    user_message = (
        "Return only JSON that matches this schema:\n"
        f"{json.dumps(schema)}\n\n"
        "Notes:\n"
        f"{notes}"
    )

    response = ollama.chat(
        model=MODEL_NAME,
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": user_message},
        ],
        format=schema,
        options={"temperature": 0},
    )

    return NoteAnalysis.model_validate_json(response.message.content)


st.set_page_config(page_title="Private AI Notes Organizer")
st.title("Private AI Notes Organizer")
st.write(
    "Paste study, meeting, or personal notes. "
    "Your local qwen3:8b model will organize them."
)

notes = st.text_area(
    "Notes to organize",
    height=240,
    placeholder="Paste your notes here...",
)

if st.button("Organize notes", type="primary", width="stretch"):
    if not notes.strip():
        st.error("Paste some notes before running the organizer.")
    else:
        analysis = analyze_notes(notes)
        st.subheader("Validated analysis")
        st.write(analysis.model_dump())

Your model output now has a dependable structure. Next, you will turn those validated fields into an organized dashboard.

Turn the Result into an Organized Dashboard

Your Pydantic schema now turns every model response into validated fields. The browser still shows those fields as one dictionary, so the organizer feels like a developer test.

This final step gives each field a clear section in the Streamlit interface. Safeguards keep local model problems from crashing the experience.

In this step, get ready to:
  • Display each validated field in its own dashboard section.
  • Protect local analysis with progress feedback plus clear error messages.
  • Test the dependable interface with three kinds of notes.
Render the dashboard sections

The validated fields already separate the model's ideas in Python. Rendering each field under its own heading gives people the same structure in the browser.

  • In app.py, find the output block directly beneath analysis = analyze_notes(notes).
  • Remove the old Validated analysis output block.
  • Keep analysis = analyze_notes(notes) in place.
  • Add the dashboard sections directly beneath that line by pasting this code:
        st.subheader("Summary")
        st.write(analysis.summary)

        st.subheader("Key points")
        if analysis.key_points:
            for point in analysis.key_points:
                st.write(f"- {point}")
        else:
            st.write("No key points found.")

        st.subheader("Action items")
        if analysis.action_items:
            for item in analysis.action_items:
                st.write(f"- {item.priority}: {item.task}")
        else:
            st.write("No action items found.")

        st.subheader("Follow-up questions")
        if analysis.follow_up_questions:
            for question in analysis.follow_up_questions:
                st.write(f"- {question}")
        else:
            st.write("No follow-up questions found.")

How does the dashboard use the schema?

  • The summary appears as one readable text block.
  • Each list field uses a loop to display its items on separate lines.
  • Every action item shows its validated priority beside its task.
  • Each empty list receives a clear fallback message.
  • Save app.py.
  • Return to the browser tab running your organizer.
  • Paste the mixed notes from the previous step into Notes to organize.
  • Click Organize notes.

You will see four separate headings. Each list appears beneath its matching heading.

Good progress. Your validated data now looks like an organizer that someone can use.

Dashboard sections missing?

Check that the new lines align with analysis = analyze_notes(notes). Extra indentation can place a section inside the wrong loop.

Confirm that you removed both lines from the old validated dictionary output. Help me fix the indentation in my Streamlit dashboard code.

Add safeguards around local analysis

A local model request can fail when Ollama cannot complete the call. The interface should explain the problem instead of exposing a Python crash.

Error handling changes the indentation of the whole button block. Replacing that block avoids hidden spacing mistakes.

  • In app.py, select the entire block that begins with if st.button("Organize notes", type="primary", width="stretch"):.
  • Replace the selected block with this first connected part:
if st.button("Organize notes", type="primary", width="stretch"):
    if not notes.strip():
        st.error("Paste some notes before running the organizer.")
    else:
        try:
            with st.spinner("Analyzing notes locally..."):
                analysis = analyze_notes(notes)

            st.subheader("Summary")
            st.write(analysis.summary)

            st.subheader("Key points")
            if analysis.key_points:
                for point in analysis.key_points:
                    st.write(f"- {point}")
            else:
                st.write("No key points found.")

            st.subheader("Action items")
            if analysis.action_items:
                for item in analysis.action_items:
                    st.write(f"- {item.priority}: {item.task}")
            else:
                st.write("No action items found.")

What does this first part do?

  • The empty-input check stops before any model request begins.
  • The try block creates a protected path for local analysis.
  • The spinner gives you visible feedback while the local model works.
  • The dashboard keeps displaying validated fields from NoteAnalysis.
  • Complete the replacement by pasting this second connected part directly beneath the first part:
            st.subheader("Follow-up questions")
            if analysis.follow_up_questions:
                for question in analysis.follow_up_questions:
                    st.write(f"- {question}")
            else:
                st.write("No follow-up questions found.")

        except ollama.ResponseError as error:
            st.error(f"Ollama returned an error: {error.error}")
        except Exception as error:
            st.error(f"Could not analyze the notes: {error}")

How do the error handlers help?

  • The follow-up section completes the four-part dashboard.
  • The first exception handler displays the message supplied by Ollama.
  • The general exception handler gives other failures a readable place in the interface.
  • Save app.py.
  • Return to the running browser app.
  • Remove all text from Notes to organize.
  • Click Organize notes.

You will see Paste some notes before running the organizer. The model call never starts.

  • Paste the notes you tested earlier back into Notes to organize.

Before you click again, do you expect the input check or the local analysis path to run? Take a moment to decide.

  • Click Organize notes.

You will see the local-analysis spinner while the request runs. The four dashboard sections appear when the validated response returns.

Seeing an analysis error?

Confirm that Ollama remains available with the local qwen3:8b model. Check that both exception handlers align with try.

Read the message shown in the app before changing your code. Help me diagnose the error shown by my local notes organizer.

✔️ Awesome, I've got everything!

Your saved app.py now renders validated sections with input checks, progress feedback, plus readable errors.

ⓧ I'd like to double check the full code

Compare your saved app.py with this complete version.

import json
from typing import Literal

import ollama
import streamlit as st
from pydantic import BaseModel

MODEL_NAME = "qwen3:8b"


class ActionItem(BaseModel):
    task: str
    priority: Literal["High", "Medium", "Low"]


class NoteAnalysis(BaseModel):
    summary: str
    key_points: list[str]
    action_items: list[ActionItem]
    follow_up_questions: list[str]


def analyze_notes(notes: str) -> NoteAnalysis:
    schema = NoteAnalysis.model_json_schema()
    system_message = (
        "You organize study, meeting, and personal notes. "
        "Extract only information supported by the notes. "
        "Use empty lists when a section has no supported items."
    )
    user_message = (
        "Return only JSON that matches this schema:\n"
        f"{json.dumps(schema)}\n\n"
        "Notes:\n"
        f"{notes}"
    )

    response = ollama.chat(
        model=MODEL_NAME,
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": user_message},
        ],
        format=schema,
        options={"temperature": 0},
    )

    return NoteAnalysis.model_validate_json(response.message.content)


st.set_page_config(page_title="Private AI Notes Organizer")
st.title("Private AI Notes Organizer")
st.write(
    "Paste study, meeting, or personal notes. "
    "Your local qwen3:8b model will organize them."
)

notes = st.text_area(
    "Notes to organize",
    height=240,
    placeholder="Paste your notes here...",
)

if st.button("Organize notes", type="primary", width="stretch"):
    if not notes.strip():
        st.error("Paste some notes before running the organizer.")
    else:
        try:
            with st.spinner("Analyzing notes locally..."):
                analysis = analyze_notes(notes)

            st.subheader("Summary")
            st.write(analysis.summary)

            st.subheader("Key points")
            if analysis.key_points:
                for point in analysis.key_points:
                    st.write(f"- {point}")
            else:
                st.write("No key points found.")

            st.subheader("Action items")
            if analysis.action_items:
                for item in analysis.action_items:
                    st.write(f"- {item.priority}: {item.task}")
            else:
                st.write("No action items found.")

            st.subheader("Follow-up questions")
            if analysis.follow_up_questions:
                for question in analysis.follow_up_questions:
                    st.write(f"- {question}")
            else:
                st.write("No follow-up questions found.")

        except ollama.ResponseError as error:
            st.error(f"Ollama returned an error: {error.error}")
        except Exception as error:
            st.error(f"Could not analyze the notes: {error}")

What should you compare?

Check the indentation beneath the button, empty-input condition, protected analysis block, plus both exception handlers. Python uses that spacing to determine which lines belong together.

Test three kinds of notes

A dependable organizer keeps the same interface when the subject changes. These final tests check study notes, meeting notes, plus personal tasks without changing the schema.

Local response speed varies

The model runs on your computer, so response speed depends on its available resources. The spinner confirms that the request is still in progress.

  • Enter Study: The biology exam is Friday. Mitochondria make ATP. Review chapters 4 and 5 tonight. in Notes to organize.
  • Click Organize notes.

You will see the study content organized beneath the same four headings. Any extracted task includes a validated priority.

  • Replace the study note with Meeting: The launch review is Tuesday. Priya owns the final slides. Send the deck by Monday afternoon..
  • Click Organize notes.

You will see the meeting details organized into the same dependable layout. The action list separates tasks from the summary.

  • Replace the meeting note with Personal: The dentist appointment is Thursday at 3 PM. Buy toothpaste. Call the clinic tomorrow to confirm..

Before you run this final check, do you expect the interface structure to change for a personal note? Take a moment to decide.

  • Click Organize notes.

You will see Summary, Key points, Action items, plus Follow-up questions again. The content changes while the validated interface stays consistent.

That is the full organizer working. Your local model now turns three kinds of messy notes into one predictable browser experience.

Secret mission

Detect and Adapt to the Note Type

Give your local organizer enough context to recognize Study, Meeting, Personal, and Mixed notes. It will adapt its emphasis for each category while preserving the same validated dashboard.

Clean Up Your Resources

Clean Up Your Resources

Your organizer creates no cloud resources or ongoing API fees. Choose whether to keep the local project, pause the running app, or delete the project files.

Resources you used:

  • The app.py file containing your adaptive notes organizer.
  • The requirements.txt file containing the pinned Python packages.
  • The .venv folder containing the isolated environment.
  • The local Streamlit process serving your browser app.

Keep everything running

No action needed. This option keeps your organizer ready for more notes or further improvements.

  • Your app.py file stays available with the adaptive note-type logic.
  • Your requirements.txt file preserves the package versions used by the project.
  • Your .venv folder keeps the installed packages isolated.
  • Your existing qwen3:8b model remains available in Ollama.

Pause - I'll come back to this later

Pausing stops the local app while preserving every project file. You can return to the same code later.

  • Switch back to the VS Code terminal running Streamlit.
  • Stop the Streamlit server by pressing Ctrl+C.
  • Exit the active virtual environment by running this command:
deactivate

What does this command do?

The deactivate command exits the current virtual environment. It preserves the environment and every installed package.

  • Confirm that the terminal prompt returns without the active environment marker.

Your organizer is now paused. Its code and isolated environment are ready for your next session.

Delete - I don't want to use this again

This option removes the project code and its isolated environment. Your pre-existing local model remains untouched.

  • Switch back to the VS Code terminal running Streamlit.
  • Stop the Streamlit server by pressing Ctrl+C.
  • Exit the active virtual environment by running this command:
deactivate

What does this command do?

The deactivate command disconnects the terminal from .venv. This prepares the entire project folder for deletion.

  • Close Visual Studio Code.
  • Use Windows File Explorer to delete the project folder that contained app.py, requirements.txt, and .venv.
  • Confirm that the project folder is no longer available at its previous location.

That clears the project code and virtual environment from your computer. Your existing qwen3:8b model remains available in Ollama.

Nice Work!

Nice Work!

You did it! You built a private browser app that turns messy notes into a validated dashboard. Ollama runs qwen3:8b locally for every analysis.

You've learned how to:

  • Build a browser-based notes organizer with Streamlit. It sends private text to a local model. It displays the analysis in one app.
  • Control model behavior through prompt design. Constrain each response through a Pydantic model. Generate a JSON Schema for Ollama. Validate the returned data before the interface uses it.
  • Render validated fields as an organized dashboard with prioritized actions. Handle empty input through clear feedback. Surface local model failures through error handling.
  • Secret Mission: Extend the schema with adaptive note classification. The app now detects Study, Meeting, Personal, or Mixed notes. Its prompt changes the emphasis for each category.

Ready to quiz yourself?