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/
32 changes: 32 additions & 0 deletions smart-notes-design/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Smart Notes – Landing Page UI Design

## Overview
This folder contains the UI/UX design exploration for the Smart Notes
landing page. The goal is to visually communicate the app’s privacy-first,
offline-by-default philosophy through a clean and focused interface.

## Scope
- Landing page UI design
- No functional implementation included
- Design-first contribution

## Screens Included
- Landing Page (Desktop)

## Design Goals
- Clear value proposition
- Calm, distraction-free layout
- Emphasis on privacy and offline usage
- Developer-friendly design for easy implementation

## Design Decisions
- Minimal color palette
- Bento-style feature cards
- Strong visual hierarchy
- Simple and focused navigation

## Assets
- `design/landing-page.png` – Landing page UI mockup

## Status
Initial design mock submitted for feedback and iteration.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 +45
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

Unclosed code block causes the rest of the README to render as a code literal.

The fenced code block opened at line 28 is never closed. Everything after line 29 (including the "How to run" examples and the second project section) will render as preformatted text. Add the closing ``` after the example output.

🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/README.md` around lines 28 - 45, The README's fenced code
block that starts with "```bash" before the example output is never closed,
causing the remainder of the document to render as a code literal; fix by adding
the closing triple-backtick fence (```) immediately after the shown example
output where the qa_cli.py example ends so subsequent sections (How to run,
second project) render normally.


# 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 | 🟠 Major

Project structure doesn't match actual file names and the code block is unclosed.

  • embed.py → actual: embeddings/embedder.py
  • index.py → actual: embeddings/indexer.py
  • utils.py → not present; actual utilities are in embeddings/chunker.py
  • The pipelines/ directory is missing from the structure
  • Line 82 says .txt notes but qa_cli.py loads .md files
  • The code fence is never closed (file ends without ```)
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/README.md` around lines 75 - 84, Update the README
project tree to match actual filenames and dirs: replace embed.py with
embeddings/embedder.py, index.py with embeddings/indexer.py, utils.py with
embeddings/chunker.py, add the missing pipelines/ entry, and change the notes
bullet to indicate .md files since qa_cli.py loads Markdown; finally close the
unclosed code fence (add the trailing ```). Reference embeddings/embedder.py,
embeddings/indexer.py, embeddings/chunker.py, pipelines/, and qa_cli.py when
making the edits.

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, then start = end - overlap never advances past the current position (and the start < 0 guard resets it to 0), causing an infinite loop on any text longer than max_length. Add a validation guard at the top.

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 = []
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/embeddings/chunker.py` around lines 9 - 29, The
chunk_text function can infinite-loop when overlap >= max_length; add an upfront
validation in chunk_text (using parameters max_length and overlap) that either
raises a ValueError or adjusts overlap (e.g., require 0 <= overlap < max_length)
and return an error if the inputs are invalid; ensure this guard runs before
trimming text or entering the while loop so start always progresses.


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([])
Comment on lines +25 to +27
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

Empty-input return shape is 1-D, but callers likely expect 2-D.

np.array([]) returns shape (0,), while successful calls return shape (n, dim). Downstream code (e.g., VectorIndexer.add which calls self.index.add(embeddings)) may fail or behave unexpectedly with a 1-D array. Consider returning a properly shaped empty array.

Proposed fix
     def embed(self, texts: List[str]) -> np.ndarray:
         if not texts:
-            return np.array([])
+            return np.empty((0, self.model.get_sentence_embedding_dimension()), dtype=np.float32)
 
         embeddings = self.model.encode(texts, convert_to_numpy=True)
📝 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 embed(self, texts: List[str]) -> np.ndarray:
if not texts:
return np.array([])
def embed(self, texts: List[str]) -> np.ndarray:
if not texts:
return np.empty((0, self.model.get_sentence_embedding_dimension()), dtype=np.float32)
embeddings = self.model.encode(texts, convert_to_numpy=True)
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/embeddings/embedder.py` around lines 25 - 27, The embed
method returns a 1-D empty array for empty input; change it to return a 2-D
empty array with zero rows and the embedding dimensionality so downstream code
(e.g., VectorIndexer.add -> self.index.add) receives shape (0, dim). Update
embed to return np.empty((0, self.embedding_dim)) (or np.empty((0,
<detected_dim>)) if the class exposes a model/embedding size) when texts is
empty, or compute the dim from an existing weight/embedding shape and use that
to form the (0, dim) 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 +37 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 neighbor slots, which is a valid Python negative index.

When fewer than k vectors are in the index, FAISS sets missing indices to -1. Since -1 < len(self.texts) is always True in Python, self.texts[-1] silently returns the last stored chunk instead of being skipped.

Proposed fix
         for idx in indices[0]:
-            if idx < len(self.texts):
+            if 0 <= idx < len(self.texts):
                 results.append(self.texts[idx])
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/embeddings/indexer.py` around lines 37 - 39, FAISS can
return -1 for empty neighbor slots which becomes a valid Python negative index;
in the loop in indexer.py that iterates "for idx in indices[0]:" (inside
whatever method populating results), change the guard to explicitly skip
negative indices (e.g., require idx >= 0 and idx < len(self.texts)) instead of
only checking "idx < len(self.texts)"; update the condition so -1 is not used to
index self.texts and only valid non-negative indices are appended to results.


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
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 will break on all other environments.

"D:/models_cache" is a local developer path. This will fail on Linux/macOS and on any other developer's machine. Remove it or use a platform-agnostic default.

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 in embedding_pipeline.py hardcodes a
Windows-only cache path ("D:/models_cache"); change the self.model =
SentenceTransformer(...) call to use a platform-agnostic cache location (or no
cache_folder so the library's default is used). Replace the literal with a
cross-platform value obtained from configuration or an environment variable
(e.g., MODEL_CACHE_DIR) or construct one via pathlib/expanduser (e.g.,
Path.home()/".cache"/"models") and pass that variable as cache_folder to
SentenceTransformer to avoid OS-specific paths.

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

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]]
Comment on lines +8 to +46
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

EmbeddingPipeline duplicates the modular components instead of composing them.

This class re-implements chunking (vs chunker.py), embedding (vs embedder.py), and indexing (vs indexer.py) with diverging defaults (max_length=300 here vs 500 in chunker.py) and missing safeguards (no empty-input checks, no import guards). Consider composing Embedder, VectorIndexer, and chunk_text instead of duplicating their logic.

Sketch of a composed pipeline
-from sentence_transformers import SentenceTransformer
-import faiss
-import numpy as np
+from rag_mvp.embeddings.chunker import chunk_text
+from rag_mvp.embeddings.embedder import Embedder
+from rag_mvp.embeddings.indexer import VectorIndexer
 
 
 class EmbeddingPipeline:
     def __init__(self, model_name="all-MiniLM-L6-v2"):
-        self.model = SentenceTransformer(model_name, cache_folder="D:/models_cache")
-        self.index = None
+        self.embedder = Embedder(model_name)
+        self.indexer = None
         self.chunks = []
 
-    def chunk_text(self, text, max_length=300, overlap=50):
-        ...
-
     def build_index(self, chunks):
-        embeddings = self.model.encode(chunks)
-        ...
+        embeddings = self.embedder.embed(chunks)
+        self.indexer = VectorIndexer(embeddings.shape[1])
+        self.indexer.add(embeddings, chunks)
+        return embeddings
 
     def process_notes(self, text):
-        self.chunks = self.chunk_text(text)
+        self.chunks = 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 = self.embedder.embed([query])
+        return self.indexer.search(query_vec[0], k=top_k)
🧰 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 8 - 46,
EmbeddingPipeline currently duplicates chunking, embedding, and indexing logic
(see methods chunk_text, build_index, process_notes, semantic_search) with
diverging defaults and missing safeguards; refactor to compose existing
components by injecting/using the shared chunk_text function (align max_length
with chunker.py), the Embedder class for model loading/encode calls, and the
VectorIndexer (or Indexer) for faiss index creation/search, and remove local
model/index implementation; also add input validation (empty text/query checks)
and import guards when instantiating Embedder/VectorIndexer to avoid reloading
models or failing on missing imports.

Comment on lines +44 to +46
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

IndexError when FAISS returns -1 for unfilled neighbor slots.

indices[0] can contain -1 when fewer than top_k results exist. Using that directly as a list index (self.chunks[i]) will silently return the wrong chunk (last element) or crash. Add a bounds check.

Proposed fix
-        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
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
🧰 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 44 - 46,
FAISS can return -1 for empty neighbor slots so iterating indices[0] and doing
self.chunks[i] may index out-of-bounds or return the wrong item; in the method
where you call self.index.search(query_vec, top_k) and build results from
indices (variables distances, indices), filter or clamp indices[0] to only
non-negative values and within range(len(self.chunks)) before using them, e.g.,
map valid_idx = [i for i in indices[0] if 0 <= i < len(self.chunks)] and then
construct results = [self.chunks[i] for i in valid_idx], preserving distances
alignment if needed.

return results
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
Comment on lines +4 to +5
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

Typo: "emedding" → "embedding".

-#-------------------emedding-pipeline-chunking concept
+#-------------------embedding-pipeline-chunking concept
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/qa_cli.py` around lines 4 - 5, Fix the typo in the inline
comment above the import: change "emedding-pipeline-chunking concept" to
"embedding-pipeline-chunking concept" so the comment correctly references the
EmbeddingPipeline import (EmbeddingPipeline) and related embedding pipeline
code.


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"


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
Comment on lines +63 to +82
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

Substring matching produces false positives on partial words.

word in sentence_lower (line 76) matches substrings, so a query word like "art" matches "start", "smart", etc. Use word-boundary matching for better precision.

Proposed fix using word boundaries
+import re
+
 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):
+            if any(re.search(r'\b' + re.escape(word) + r'\b', sentence_lower) for word in query_words):
                 results.append({
                     "filename": note["filename"],
                     "sentence": sentence.strip()
                 })
 
     return results
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/qa_cli.py` around lines 63 - 82, In search_notes, avoid
substring matches by replacing the current "any(word in sentence_lower for word
in query_words)" logic with word-boundary matching: for each sentence in
split_sentences(note["content"]), normalize and either use a regex search with
\b{word}\b (case-insensitive) or tokenize sentence_lower into words and check
membership of each query_word in that set; update the check inside the
search_notes function so results only append when whole words match (refer to
search_notes, query_words, sentence_lower, and split_sentences).



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 and requires heavy ML dependencies.

If sentence-transformers or faiss aren't installed, this crashes the entire CLI before the keyword-based search (which has no such dependencies) can be used. Guard it or make it opt-in.

Proposed fix
 if __name__ == "__main__":
-
-    demo_embeddings_pipeline()      # Temporary demo for embeddings pipeline
+    try:
+        demo_embeddings_pipeline()      # Temporary demo for embeddings pipeline
+    except (ImportError, Exception) as e:
+        print(f"Embedding demo skipped: {e}")
 
     notes = load_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
if __name__ == "__main__":
demo_embeddings_pipeline() # Temporary demo for embeddings pipeline
if __name__ == "__main__":
try:
demo_embeddings_pipeline() # Temporary demo for embeddings pipeline
except (ImportError, Exception) as e:
print(f"Embedding demo skipped: {e}")
notes = load_notes()
🤖 Prompt for AI Agents
In `@smart-notes/rag_mvp/qa_cli.py` around lines 85 - 87, The
demo_embeddings_pipeline() call runs unconditionally and pulls heavy ML deps
(sentence-transformers/faiss); make it opt-in or fail-safe: change the __main__
block to only invoke demo_embeddings_pipeline() when an explicit flag or env var
(e.g., --demo-embeddings or DEMO_EMBEDDINGS) is present, and/or wrap the call in
a try/except ImportError that catches missing sentence-transformers/faiss, logs
a clear warning, and continues so the rest of the CLI (keyword-based search) can
run; refer to demo_embeddings_pipeline() and the if __name__ == "__main__":
block when making the change.


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()