Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Conversational Note-Taking AI Agent

A production-quality, conversational note-taking assistant. Manage notes — create, search, edit, delete — entirely through natural language, with conversational memory, human-in-the-loop confirmation before destructive actions, and intent disambiguation when a request is ambiguous.


Overview

The agent understands free-text requests like:

  • "Create a note about Docker deployment"
  • "Actually, add Kubernetes too" (resolves "too" to the note just created)
  • "What did I write about APIs last week?"
  • "Delete my standup note" (asks for confirmation before deleting)

It never guesses when multiple notes match, never invents information that isn't in the database, and always asks for explicit confirmation before a destructive or content-replacing action.

How intent understanding works

Natural language is parsed into structured intent (add_note, search_notes, update_note, delete_note, etc.) in one of three ways, resolved automatically at runtime by agent/intent.py::_resolve_provider:

  1. Anthropic (Claude) — used if ANTHROPIC_API_KEY is set (and LLM_PROVIDER isn't forcing something else). The message plus recent conversation history is sent to the Claude API via the anthropic SDK.
  2. Groq — used if GROQ_API_KEY is set and Anthropic isn't. Uses the groq SDK's OpenAI-compatible chat completions endpoint, with the same system prompt.
  3. Rule-based fallback (no API key required) — a set of conservative regex/keyword heuristics (agent/intent.py::parse_intent_rules) handles the same intents, so the whole app is fully runnable and testable with zero external API calls or cost.

Both LLM paths use the exact same JSON-only system prompt (agent/prompts.py) and schema, so switching providers doesn't change any downstream logic in agent/chatbot.py — only how the intent JSON gets produced. Confirmation detection (yes/no) always uses simple rule matching regardless of mode, since it must be fast and unambiguous.

Provider selection is controlled via environment variables:

Variable Purpose
ANTHROPIC_API_KEY Enables the Anthropic path
GROQ_API_KEY Enables the Groq path
LLM_PROVIDER Force "anthropic" or "groq" explicitly. If unset, auto-detects: Anthropic preferred, then Groq, then rule-based.
NOTE_AGENT_MODEL Override the model name for whichever provider is active (defaults: claude-sonnet-4-6 / llama-3.3-70b-versatile)

If a call to either provider fails for any reason (bad key, network error, malformed response), the app transparently falls back to the rule-based parser for that message rather than crashing. The sidebar always shows which mode is currently active.


Architecture

User (Streamlit chat)
        │
        ▼
  NoteChatbot (agent/chatbot.py)   ── orchestrates the conversation
        │
        ├── agent/intent.py         ── NL → structured intent (LLM or rules)
        ├── agent/memory.py         ── in-memory dialogue/session state
        ├── agent/confirmation.py   ── yes/no confirmation detection
        │
        ▼
  services/notes.py   ── business logic (create/update/delete, formatting)
  services/search.py  ── keyword / tag / category / date / NL search
  services/parser.py  ── date & title parsing helpers
        │
        ▼
  database/crud.py    ── CRUD functions over SQLAlchemy sessions
  database/models.py  ── Note ORM model
  database/database.py── SQLite engine/session setup
        │
        ▼
     data/notes.db   (persisted SQLite file)

Key design decision: conversational/session state (what note "that" or "the second one" refers to, pending confirmations, chat transcript) is kept entirely in memory (agent/memory.ConversationMemory), separate from the persistent notes database. This keeps the SQLite schema clean and means restarting the app resets dialogue context without touching your notes.


Folder structure

note_agent/
│
├── app.py                  # Application entry point (run with `streamlit run app.py`)
├── requirements.txt
├── README.md
│
├── database/
│   ├── database.py         # SQLAlchemy engine / session / init_db()
│   ├── models.py            # Note ORM model
│   └── crud.py               # create/read/update/delete helpers
│
├── agent/
│   ├── chatbot.py            # Conversation orchestrator (the "brain")
│   ├── intent.py              # NL → structured intent (Claude API + fallback)
│   ├── memory.py               # ConversationMemory / pending confirmation & disambiguation
│   ├── confirmation.py          # Yes/No confirmation detection
│   └── prompts.py                # System prompt for the LLM intent parser
│
├── services/
│   ├── search.py              # Keyword / tag / category / date / NL search
│   ├── notes.py                 # Note business logic + display formatting
│   └── parser.py                 # Relative-date parsing, hashtag extraction, auto-titles
│
├── ui/
│   └── app.py                  # Streamlit chat UI
│
└── data/
    └── notes.db                # SQLite database file (created automatically)

Dependencies

  • Python 3.12+
  • streamlit — chat UI
  • sqlalchemy — ORM over SQLite
  • anthropic — Claude API client (optional; only used if the Anthropic provider is active)
  • groq — Groq API client (optional; only used if the Groq provider is active)
  • python-dateutil — flexible date parsing for search
  • python-dotenv — loads variables from a .env file automatically at startup

Installation

# 1. Clone the project
git clone <your-repo-url>
cd note_agent

# 2. Create and activate a virtual environment (on Windows)
python -m venv .venv
.venv\Scripts\activate

# 3. Install dependencies
pip install -r requirements.txt

# 4. (Optional) Enable LLM-powered natural language understanding
cp .env.example .env
# Edit .env and set EITHER:
#   ANTHROPIC_API_KEY=sk-ant-...     (uses Claude)
# OR
#   GROQ_API_KEY=gsk_...             (uses Groq, e.g. Llama 3.3 70B)
#
# If you set both, ANTHROPIC_API_KEY wins unless you also set
# LLM_PROVIDER=groq to force Groq. If neither key is set, the app
# automatically uses the built-in rule-based parser, so you can run and
# test everything with zero API keys and zero cost.

app.py loads .env automatically at startup via python-dotenv, so you don't need to export anything manually in your shell — just edit .env and restart the app. (You can still use export ANTHROPIC_API_KEY=... / export GROQ_API_KEY=... directly in your shell if you prefer; both work.)

Running the app

streamlit run app.py

This opens the chat UI in your browser (typically http://localhost:8501). The SQLite database is created automatically on first run at data/notes.db — no manual setup needed.

How SQLite storage works

  • On startup, database/database.init_db() creates the notes table if it doesn't already exist, using SQLAlchemy's declarative models (database/models.py).
  • Every note operation runs inside a transactional session (database.database.get_session()), which commits on success and rolls back automatically on any error.
  • The database file lives at data/notes.db by default; override the location with the NOTE_AGENT_DB_PATH environment variable (useful for tests — see below).
  • Tags are stored as a comma-separated string column for simplicity (SQLite has no native array type) and exposed to the rest of the app as a Python list[str] via a model property.

Behavior notes (recent fixes)

A few things worth knowing about how notes are actually stored and searched:

  • Timestamps: created_at is set once, at creation. updated_at stays null until the note is genuinely edited for the first time -- the UI only shows "Updated: ..." once that's happened; otherwise it shows "Created: ... (never edited)".
  • Content formatting: the assistant is instructed to store clean, well-formed note content -- not the user's raw command sentence -- and to format comma/and-separated lists as Markdown bullet points rather than one run-on sentence. A regex-based safety net applies the same cleanup when running in rule-based (no-API-key) mode.
  • Auto-tagging: if you don't give explicit tags/category, the LLM (when configured) infers 1-3 relevant tags and a category from the note's content itself, rather than leaving them empty. This only happens in LLM-powered mode -- the rule-based fallback doesn't attempt to invent tags.
  • Date-based search ("what did I create today", "notes from last week") filters by the note's most recent activity (updated_at if edited, else created_at) and is no longer confused by generic query words like "create" or "today" being treated as literal content keywords.
  • Semantic search: when an LLM provider is configured, a query that doesn't literally share words with a note's content (e.g. "login security notes" vs. a note titled "OAuth Authentication") is re-ranked by the LLM for topical relevance, not just keyword overlap. See services/search.py::_llm_semantic_search. Falls back to keyword-only search with no provider configured.
  • Targeted edits: "replace X with Y" performs a precise, in-place text substitution on the note body, preserving everything else -- it does not overwrite the whole note (that's what full-content confirmation prompts are for). If X isn't found in the note, nothing is changed and you're shown the note's current content instead.
  • Undo: every create/update/delete pushes an entry onto an in-memory undo stack (ConversationMemory.action_history). Saying "undo" (or "revert") pops the most recent entry and reverses it: a create is undone by deleting the note, an update is undone by restoring the prior content, and a delete is undone by recreating the note (as a new row, since SQLite auto-increment ids aren't reused). Saying "undo" while a confirmation prompt is pending cancels that pending action instead.

Example conversations

Creating notes

You: Create a note about Docker deployment
Bot: Done. I created **Docker deployment**.

You: Add a note titled "Grocery List" with milk, eggs, and bread, tag it #errands
Bot: Done. I created **Grocery List**.

Searching

You: Show my meeting notes
Bot: I found 2 matching note(s):
     1. **Team Standup** (meetings) [weekly]
     2. **Client Kickoff** (meetings)

You: What did I write about APIs last week?
Bot: I found 1 matching note(s):
     1. **API Rate Limiting Notes**

Editing

You: Rename "Docker deployment" to "Docker Production Deployment"
Bot: Updated **Docker Production Deployment**.

You: Actually, add Kubernetes too
Bot: Updated **Docker Production Deployment**.

(The assistant resolves "too" and "it" to the note just discussed via conversational memory — no need to repeat the note's name.)

Deleting with confirmation

You: Delete my standup note
Bot: You're about to permanently delete **Team Standup**.
     Type **Yes** to confirm or **No** to cancel.

You: Yes
Bot: Deleted **Team Standup**.

Ambiguous searches (intent disambiguation)

You: Delete my API note
Bot: I found 2 matching notes. Which one would you like to delete?
     1. **API Design Notes**
     2. **API Testing Checklist**

You: 2
Bot: You're about to permanently delete **API Testing Checklist**.
     Type **Yes** to confirm or **No** to cancel.

You: Yes
Bot: Deleted **API Testing Checklist**.

Follow-up conversations

You: Create a note about Docker deployment
Bot: Done. I created **Docker deployment**.

You: Actually, add Kubernetes too
Bot: Updated **Docker deployment**.

You: Summarize it
Bot: **Docker deployment**
     Create a note about Docker deployment
     Kubernetes too
     _Category: none | Tags: none | Updated: 2026-08-01 10:15_

You: Delete it
Bot: You're about to permanently delete **Docker deployment**.
     Type **Yes** to confirm or **No** to cancel.

You: No
Bot: Okay, I cancelled that action. Nothing was changed.

Notes on safety behaviors

  • No hallucination: if a search or reference resolves to zero notes, the assistant says so explicitly instead of inventing a note or result.
  • Human-in-the-loop confirmation: deletions and any update that would replace the bulk of a note's content require an explicit "Yes"/"No" before anything happens. Appending content ("add X too") or simple renames/tag edits are treated as low-risk and applied directly.
  • Disambiguation over guessing: whenever a request could refer to more than one note, the assistant lists the candidates and asks the user to pick, instead of assuming.

Running without Streamlit (quick smoke test)

You can exercise the chatbot logic directly in Python without the UI:

from database.database import init_db
from agent.chatbot import NoteChatbot
from agent.memory import ConversationMemory

init_db()
bot = NoteChatbot(ConversationMemory())
print(bot.handle_message("Create a note about Docker deployment"))
print(bot.handle_message("Actually, add Kubernetes too"))
print(bot.handle_message("Show my notes about docker"))

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages