Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
notes/
84 changes: 84 additions & 0 deletions smart-notes/rag_mvp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Smart Notes – Local Q&A (RAG MVP)

This is a minimal, local-first MVP that allows users to ask natural-language questions over their markdown notes.

## Features (Current MVP)

- Loads markdown files from a local `notes/` directory
- Supports natural-language questions (e.g., "what is AI", "where is AI used")
- Returns sentence-level answers from notes
- Shows the source note filename
- Interactive CLI loop (type `exit` to quit)

This is a starter implementation intended to be extended with embeddings and vector search in future iterations.

---

## How it works

1. Notes are loaded from the local `notes/` directory.
2. Question words (what, where, who, when, etc.) are filtered.
3. Notes are split into sentences.
4. Relevant sentences are returned based on keyword matching.

---

## How to run

```bash
python smart-notes/rag_mvp/qa_cli.py



>> what is AI

[1] From test.md:
Artificial Intelligence (AI) is the simulation of human intelligence in machines.


>> what is machine learning
how is machine learning used
difference between AI and ML



Comment on lines +28 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Malformed Markdown β€” unclosed code block bleeds into the rest of the document.

The code block opened at line 28 is never properly closed. The example CLI output (lines 33–43) and everything after it gets swallowed into the code fence, making the second half of the README render as a raw code block rather than formatted documentation.

Close the code block after the CLI example and before the second section heading.

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/README.md` around lines 28 - 44, The README.md has an
unclosed code fence around the CLI example (the block starting with the ```bash
before "python smart-notes/rag_mvp/qa_cli.py"), so close that code block
immediately after the shown CLI output (after the lines that show the prompts
and responses like ">> what is AI" and the subsequent output) to prevent the
rest of the document from rendering as code; locate the code fence in the
section containing the qa_cli.py example and add the closing ``` on its own line
before the next section heading or normal text.



# Smart Notes – RAG MVP (Embeddings & FAISS)

This project is a simple **Retrieval-Augmented Generation (RAG)** pipeline for Smart Notes.
It allows users to store notes, convert them into embeddings, and search relevant notes using vector similarity.

---

## πŸš€ Features

- Convert notes into embeddings using Sentence Transformers
- Store and search embeddings using FAISS (CPU)
- CLI tool to ask questions about your notes
- Simple chunking for text files
- Works fully offline after model download

---

## 🧠 Tech Stack

- Python 3.10+
- sentence-transformers
- FAISS (faiss-cpu)
- HuggingFace Transformers

---

## πŸ“ Project Structure

```bash
smart-notes/
β”œβ”€β”€ rag_mvp/
β”‚ β”œβ”€β”€ embed.py # Embedding logic
β”‚ β”œβ”€β”€ index.py # FAISS index creation
β”‚ β”œβ”€β”€ qa_cli.py # CLI for asking questions
β”‚ └── utils.py # Helper functions
β”œβ”€β”€ notes/ # Put your .txt notes here
β”œβ”€β”€ requirements.txt
└── README.md
Comment on lines +75 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Project structure in README doesn't match actual file layout.

The documented structure references embed.py, index.py, and utils.py, but the actual PR introduces embeddings/chunker.py, embeddings/embedder.py, embeddings/indexer.py, and pipelines/embedding_pipeline.py. Also, the trailing code block is never closed.

Please update the project structure to reflect the real file layout and close the code fence.

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/README.md` around lines 75 - 84, Update the README
project structure to match the actual files introduced in the PR: replace
references to embed.py, index.py, and utils.py with the new modules
embeddings/chunker.py, embeddings/embedder.py, embeddings/indexer.py and include
pipelines/embedding_pipeline.py under the rag_mvp/ tree; also close the open
Markdown code fence at the end of the example block. Ensure the README lists the
correct filenames and paths exactly as in the diff (embeddings/chunker.py,
embeddings/embedder.py, embeddings/indexer.py, pipelines/embedding_pipeline.py)
and that the triple backtick that starts the code block is properly terminated.

Binary file not shown.
Empty file.
31 changes: 31 additions & 0 deletions smart-notes/rag_mvp/embeddings/chunker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Chunking utilities for splitting long notes into overlapping chunks.
This helps embeddings capture local context.
"""

from typing import List


def chunk_text(text: str, max_length: int = 500, overlap: int = 50) -> List[str]:
if not text:
return []

chunks = []
start = 0
text = text.strip()

while start < len(text):
end = start + max_length
chunk = text[start:end].strip()

if chunk:
chunks.append(chunk)

if end >= len(text):
break

start = end - overlap
if start < 0:
start = 0
Comment on lines +9 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Infinite loop when overlap >= max_length.

If overlap >= max_length, start never advances (it gets clamped to 0 or stays the same), causing an infinite loop. Add input validation.

Proposed fix
 def chunk_text(text: str, max_length: int = 500, overlap: int = 50) -> List[str]:
     if not text:
         return []
+    if overlap >= max_length:
+        raise ValueError("overlap must be less than max_length")
 
     chunks = []
     start = 0
πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/embeddings/chunker.py` around lines 9 - 29, The
chunk_text function can enter an infinite loop when overlap >= max_length; add
input validation at the start of chunk_text to ensure max_length is a positive
integer and 0 <= overlap < max_length (or raise a ValueError with a clear
message), and reject or normalize invalid inputs before the while loop;
reference the chunk_text function and the start/end/overlap logic so the check
runs immediately after parameters are received and before any slicing logic.


return chunks
30 changes: 30 additions & 0 deletions smart-notes/rag_mvp/embeddings/embedder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Embedding wrapper for converting text chunks into vectors.
Supports pluggable embedding backends later (Ollama, OpenAI, SentenceTransformers).
"""

from typing import List
import numpy as np

try:
from sentence_transformers import SentenceTransformer
except ImportError:
SentenceTransformer = None


class Embedder:
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
if SentenceTransformer is None:
raise ImportError(
"sentence-transformers not installed. Run: pip install sentence-transformers"
)

self.model_name = model_name
self.model = SentenceTransformer(model_name)

def embed(self, texts: List[str]) -> np.ndarray:
if not texts:
return np.array([])

embeddings = self.model.encode(texts, convert_to_numpy=True)
return embeddings
41 changes: 41 additions & 0 deletions smart-notes/rag_mvp/embeddings/indexer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Simple vector indexer using FAISS for similarity search.
"""

from typing import List
import numpy as np

try:
import faiss
except ImportError:
faiss = None


class VectorIndexer:
def __init__(self, dim: int):
if faiss is None:
raise ImportError("faiss not installed. Run: pip install faiss-cpu")

self.dim = dim
self.index = faiss.IndexFlatL2(dim)
self.texts: List[str] = []

def add(self, embeddings: np.ndarray, chunks: List[str]):
if len(embeddings) == 0:
return

self.index.add(embeddings)
self.texts.extend(chunks)

def search(self, query_embedding: np.ndarray, k: int = 3):
if self.index.ntotal == 0:
return []

distances, indices = self.index.search(query_embedding.reshape(1, -1), k)
results = []

for idx in indices[0]:
if idx < len(self.texts):
results.append(self.texts[idx])
Comment on lines +34 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Bug: FAISS returns -1 for unfilled result slots, which passes the bounds check.

When the index has fewer vectors than k, FAISS pads results with index -1. Since -1 < len(self.texts) is always True in Python, this silently returns self.texts[-1] (the last chunk) instead of being filtered out.

Proposed fix
-        distances, indices = self.index.search(query_embedding.reshape(1, -1), k)
+        _distances, indices = self.index.search(query_embedding.reshape(1, -1), k)
         results = []
 
         for idx in indices[0]:
-            if idx < len(self.texts):
+            if 0 <= idx < len(self.texts):
                 results.append(self.texts[idx])
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
distances, indices = self.index.search(query_embedding.reshape(1, -1), k)
results = []
for idx in indices[0]:
if idx < len(self.texts):
results.append(self.texts[idx])
_distances, indices = self.index.search(query_embedding.reshape(1, -1), k)
results = []
for idx in indices[0]:
if 0 <= idx < len(self.texts):
results.append(self.texts[idx])
🧰 Tools
πŸͺ› Ruff (0.15.0)

[warning] 34-34: Unpacked variable distances is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/embeddings/indexer.py` around lines 34 - 39, The loop
over FAISS search results in indexer.py incorrectly treats FAISS sentinel -1 as
a valid index; update the post-search filtering (after self.index.search(...)
that assigns distances, indices) to ignore any idx values that are negative
(e.g., check idx >= 0) and also ensure idx < len(self.texts) before appending
self.texts[idx], so negative padded results are not used; reference the
variables indices, distances, and self.texts in the change.


return results
Empty file.
Binary file not shown.
Binary file not shown.
47 changes: 47 additions & 0 deletions smart-notes/rag_mvp/pipelines/embedding_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# rag_mvp/pipelines/embedding_pipeline.py

from sentence_transformers import SentenceTransformer
import faiss
Comment on lines +3 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion | 🟠 Major

Direct imports without graceful error handling, unlike sibling modules.

embedder.py and indexer.py use try/except ImportError to provide helpful messages when dependencies are missing. This file imports SentenceTransformer and faiss directly, which will produce a raw ModuleNotFoundError instead.

More broadly, this pipeline class reimplements functionality already provided by Embedder, VectorIndexer, and chunk_text from the embeddings/ package. Consider reusing those modules instead of duplicating logic.

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/pipelines/embedding_pipeline.py` around lines 3 - 4, This
file directly imports SentenceTransformer and faiss which will raise raw
ModuleNotFoundError; wrap those imports in try/except ImportError and raise a
helpful message matching sibling modules (e.g., instructing to pip install
sentence-transformers/faiss) or fallback gracefully. Also refactor the pipeline
to reuse existing embeddings package code instead of duplicating logic: import
and call Embedder, VectorIndexer, and chunk_text (instead of reimplementing
embedding/indexing/chunking) so embedding_pipeline.py delegates to those
classes/functions for model loading, embedding generation, and FAISS indexing.

import numpy as np


class EmbeddingPipeline:
def __init__(self, model_name="all-MiniLM-L6-v2"):
self.model = SentenceTransformer(model_name, cache_folder="D:/models_cache")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Hardcoded Windows-specific cache path β€” breaks portability.

cache_folder="D:/models_cache" will fail on non-Windows systems and on other developers' machines. Remove this or use a platform-agnostic default (e.g., a project-relative directory or respect HF_HOME/SENTENCE_TRANSFORMERS_HOME env vars).

Proposed fix
-        self.model = SentenceTransformer(model_name, cache_folder="D:/models_cache")
+        self.model = SentenceTransformer(model_name)
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.model = SentenceTransformer(model_name, cache_folder="D:/models_cache")
self.model = SentenceTransformer(model_name)
πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/pipelines/embedding_pipeline.py` at line 10, The
SentenceTransformer instantiation uses a hardcoded Windows-only cache folder;
update the constructor call in embedding_pipeline (the place where self.model =
SentenceTransformer(...)) to remove the fixed "D:/models_cache" and instead
compute a platform-agnostic cache directory: prefer honoring environment vars
like HF_HOME or SENTENCE_TRANSFORMERS_HOME (via os.getenv) and fall back to a
project-relative cache (e.g., os.path.join(project_root, ".cache",
"sentence_transformers")) or the default by omitting cache_folder; ensure you
import os and construct the path using os.path.join so the code is portable
across OSes.

self.index = None
self.chunks = []

def chunk_text(self, text, max_length=300, overlap=50):
chunks = []
start = 0

while start < len(text):
end = start + max_length
chunk = text[start:end]
chunks.append(chunk)
start = end - overlap

return chunks
Comment on lines +14 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion | 🟠 Major

Duplicate chunk_text β€” also susceptible to infinite loop.

This is a copy of the logic in embeddings/chunker.py with a different default max_length (300 vs 500) and without the end >= len(text) break guard. If overlap >= max_length, this version loops forever.

Reuse chunk_text from embeddings/chunker.py instead.

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/pipelines/embedding_pipeline.py` around lines 14 - 24,
The chunk_text implementation in embedding_pipeline.py duplicates the logic from
embeddings/chunker.py and lacks the end>=len(text) guard (causing an infinite
loop when overlap >= max_length); replace the local method with a call to the
canonical chunk_text from embeddings.chunker (import chunk_text and delegate to
it) so you reuse the tested implementation and its break guard; if for some
reason you cannot import, modify the existing chunk_text to include the same
guard (check if end >= len(text) then append final chunk and break) and keep the
same signature (chunk_text(self, text, max_length=300, overlap=50)) so callers
remain compatible.


def build_index(self, chunks):
embeddings = self.model.encode(chunks)
embeddings = np.array(embeddings).astype("float32")

dim = embeddings.shape[1]
self.index = faiss.IndexFlatL2(dim)
self.index.add(embeddings)

return embeddings

def process_notes(self, text):
self.chunks = self.chunk_text(text)
embeddings = self.build_index(self.chunks)
return self.chunks, embeddings

def semantic_search(self, query, top_k=3):
query_vec = self.model.encode([query])
query_vec = np.array(query_vec).astype("float32")

distances, indices = self.index.search(query_vec, top_k)
results = [self.chunks[i] for i in indices[0]]
return results
Comment on lines +41 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

semantic_search crashes if FAISS returns -1 indices.

self.chunks[i] on line 46 will return the wrong chunk (last element) for -1 indices and raises IndexError for any out-of-range positive index. Unlike VectorIndexer.search, there is no bounds check here at all.

Proposed fix
     def semantic_search(self, query, top_k=3):
         query_vec = self.model.encode([query])
         query_vec = np.array(query_vec).astype("float32")
 
-        distances, indices = self.index.search(query_vec, top_k)
-        results = [self.chunks[i] for i in indices[0]]
+        _distances, indices = self.index.search(query_vec, top_k)
+        results = [self.chunks[i] for i in indices[0] if 0 <= i < len(self.chunks)]
         return results
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def semantic_search(self, query, top_k=3):
query_vec = self.model.encode([query])
query_vec = np.array(query_vec).astype("float32")
distances, indices = self.index.search(query_vec, top_k)
results = [self.chunks[i] for i in indices[0]]
return results
def semantic_search(self, query, top_k=3):
query_vec = self.model.encode([query])
query_vec = np.array(query_vec).astype("float32")
_distances, indices = self.index.search(query_vec, top_k)
results = [self.chunks[i] for i in indices[0] if 0 <= i < len(self.chunks)]
return results
🧰 Tools
πŸͺ› Ruff (0.15.0)

[warning] 45-45: Unpacked variable distances is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/pipelines/embedding_pipeline.py` around lines 41 - 47,
The semantic_search method should guard against invalid indices returned by
self.index.search (e.g., -1 or out-of-range) before indexing into self.chunks;
update semantic_search (and the handling of distances, indices) to iterate over
indices[0] and distances[0], filter out any index < 0 or >= len(self.chunks),
and build results and corresponding distances only from valid entries (returning
fewer than top_k if needed) so attempts to access self.chunks[i] never use
invalid positions.

109 changes: 109 additions & 0 deletions smart-notes/rag_mvp/qa_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import os
import re

#-------------------emedding-pipeline-chunking concept
from rag_mvp.pipelines.embedding_pipeline import EmbeddingPipeline
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check package configuration and structure
fd -t f "__init__.py" smart-notes/
fd -t f "setup.py|pyproject.toml|setup.cfg" smart-notes/

Repository: AOSSIE-Org/Info

Length of output: 143


🏁 Script executed:

#!/bin/bash
# Check for __init__.py at rag_mvp level
ls -la smart-notes/rag_mvp/__init__.py 2>&1

# Find README
fd -t f "README|readme" . --max-depth 3

# Check for config files at repo root
fd -t f "setup.py|pyproject.toml|setup.cfg|poetry.lock" . --max-depth 1

Repository: AOSSIE-Org/Info

Length of output: 181


🏁 Script executed:

#!/bin/bash
# Check the rag_mvp README which is most relevant
cat smart-notes/rag_mvp/README.md

# Also check root README for context
echo "=== ROOT README ===" 
head -100 ./README.md

Repository: AOSSIE-Org/Info

Length of output: 5181


Absolute import will fail when script is run as documented.

from rag_mvp.pipelines.embedding_pipeline import EmbeddingPipeline requires rag_mvp to be a package discoverable from sys.path. Running python smart-notes/rag_mvp/qa_cli.py from the repo root (as the README suggests) adds only smart-notes/rag_mvp to sys.path, not smart-notes. Additionally, there is no __init__.py at smart-notes/rag_mvp/, so rag_mvp is not recognized as a package. This will fail with ModuleNotFoundError.

Use relative imports (from .pipelines.embedding_pipeline import ...), add an __init__.py at smart-notes/rag_mvp/, or update the execution instructions to use a method that properly configures the module path.

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/qa_cli.py` at line 5, The import in qa_cli.py uses an
absolute path that will fail when run directly; update the import to a relative
import (e.g., replace "from rag_mvp.pipelines.embedding_pipeline import
EmbeddingPipeline" with a relative import like "from
.pipelines.embedding_pipeline import EmbeddingPipeline") so EmbeddingPipeline is
resolved when running python smart-notes/rag_mvp/qa_cli.py, or alternatively add
an __init__.py to smart-notes/rag_mvp and adjust run instructions to execute the
package form; ensure the change targets the import line in qa_cli.py referencing
EmbeddingPipeline.


def demo_embeddings_pipeline():
pipeline = EmbeddingPipeline()

note_text = """
Python is a programming language.
It is widely used in AI and machine learning projects.
Smart Notes helps users organize knowledge using embeddings.
"""

chunks, embeddings = pipeline.process_notes(note_text)

print("\n--- Chunks Created ---")
for i, c in enumerate(chunks):
print(f"[{i}] {c}")

query = "What is Python used for?"
results = pipeline.semantic_search(query)

print("\n--- Search Results ---")
for r in results:
print("-", r)
#-------------------------------------------------




QUESTION_WORDS = {
"what", "where", "who", "when", "which",
"is", "are", "was", "were", "the", "a", "an",
"of", "to", "in", "on", "for"
}

NOTES_DIR = "notes"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

NOTES_DIR is a relative path β€” behavior depends on the working directory.

"notes" resolves relative to the CWD, not relative to the script or project root. This will silently find no notes if the user runs the CLI from a different directory.

Proposed fix β€” resolve relative to the script location
-NOTES_DIR = "notes"
+NOTES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "notes")
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NOTES_DIR = "notes"
NOTES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "notes")
πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/qa_cli.py` at line 39, NOTES_DIR is a relative path that
depends on the current working directory; change it to be resolved relative to
the script location by constructing NOTES_DIR from the module file path (e.g.,
using Path(__file__).parent / "notes") so functions that reference NOTES_DIR
always point to the project's notes directory regardless of CWD; update any
imports to use pathlib.Path if necessary and ensure downstream code expects a
Path or str consistently (reference: NOTES_DIR in qa_cli.py).



def load_notes():
notes = []
if not os.path.exists(NOTES_DIR):
print(f"Notes directory '{NOTES_DIR}' not found.")
return notes

for file in os.listdir(NOTES_DIR):
if file.endswith(".md"):
path = os.path.join(NOTES_DIR, file)
with open(path, "r", encoding="utf-8") as f:
notes.append({
"filename": file,
"content": f.read()
})
return notes


def split_sentences(text):
return re.split(r'(?<=[.!?])\s+', text)


def search_notes(query, notes):
results = []

query_words = [
word.lower()
for word in query.split()
if word.lower() not in QUESTION_WORDS
]

for note in notes:
sentences = split_sentences(note["content"])
for sentence in sentences:
sentence_lower = sentence.lower()
if any(word in sentence_lower for word in query_words):
results.append({
"filename": note["filename"],
"sentence": sentence.strip()
})

return results


if __name__ == "__main__":

demo_embeddings_pipeline() # Temporary demo for embeddings pipeline
Comment on lines +85 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

demo_embeddings_pipeline() runs unconditionally on every CLI start.

This forces the SentenceTransformer model to load (and potentially download) every time a user launches the CLI, even if they only want the keyword-based Q&A. This adds significant startup latency. Consider making the embedding demo opt-in (e.g., via a CLI flag) or removing it from the default flow.

πŸ€– Prompt for AI Agents
In `@smart-notes/rag_mvp/qa_cli.py` around lines 85 - 87, The
demo_embeddings_pipeline() is invoked unconditionally in the __main__ block
causing the SentenceTransformer to load on every CLI start; make the demo opt-in
by adding a CLI flag (e.g., --embeddings-demo or --demo-embeddings) via
argparse/typer and only call demo_embeddings_pipeline() when that flag is set
(leave existing keyword-based Q&A flow unchanged), or remove the call entirely
if you prefer no demo; update the __main__ block to check the new flag before
invoking demo_embeddings_pipeline() so startup latency is avoided unless the
user explicitly requests the demo.


notes = load_notes()

print("Ask questions about your notes (type 'exit' to quit)\n")

while True:
query = input(">> ").strip()

if query.lower() == "exit":
print("Goodbye πŸ‘‹")
break

matches = search_notes(query, notes)

if not matches:
print("No relevant notes found.\n")
else:
print("\n--- Answers ---\n")
for i, m in enumerate(matches, 1):
print(f"[{i}] From {m['filename']}:")
print(m["sentence"])
print()