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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pip install -e ".[dev]"
pytest
python -m notes_rag --mock "What is a git remote?"
python -m notes_rag --mock --json "What is a git remote?" # machine-readable answer + sources
python -m notes_rag --list-notes # print indexed note titles + files
```

**Expected stdout** (deterministic on the bundled `notes/` corpus + `--mock` — yours should match):
Expand Down
23 changes: 21 additions & 2 deletions src/notes_rag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ def build_parser() -> argparse.ArgumentParser:
"Examples:\n"
" python -m notes_rag --mock \"What is a git remote?\"\n"
" python -m notes_rag --notes ./notes --mock \"What is RAG?\"\n"
" python -m notes_rag --list-notes\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument("question", help="Your question")
p.add_argument("question", nargs="?", default=None, help="Your question")
p.add_argument(
"--notes",
type=Path,
Expand All @@ -40,15 +41,33 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Print a single JSON object with answer and sources",
)
p.add_argument(
"--list-notes",
action="store_true",
help="Print indexed note titles and filenames, then exit (prefers list mode if question is also passed)",
)
return p


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
parser = build_parser()
args = parser.parse_args(argv)
notes_dir = args.notes
if not notes_dir.is_dir():
print(f"error: notes folder not found: {notes_dir}", file=sys.stderr)
return 1

if args.list_notes:
notes = load_notes(notes_dir)
for note in notes:
print(f"{note.title} ({note.path.name})")
return 0

if not args.question:
parser.print_usage(file=sys.stderr)
print("notes-rag: error: the following arguments are required: question", file=sys.stderr)
return 2

notes = load_notes(notes_dir)
hits = retrieve(notes, args.question, k=args.k)
answerer = Answerer.from_env(mock=args.mock)
Expand Down
32 changes: 32 additions & 0 deletions tests/test_notes_rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,35 @@ def test_missing_key_without_mock():
assert False, "expected RuntimeError"
except RuntimeError as exc:
assert "OPENAI_API_KEY" in str(exc)


def test_list_notes(capsys):
code = main(["--list-notes", "--notes", str(NOTES)])
out = capsys.readouterr().out
assert code == 0
assert "Git remotes (git-remotes.md)" in out
assert "What RAG is (rag-idea.md)" in out


def test_list_notes_prefers_list_over_question(capsys):
code = main(["--list-notes", "--notes", str(NOTES), "What is a git remote?"])
out = capsys.readouterr().out
assert code == 0
assert "Git remotes (git-remotes.md)" in out
assert "[mock]" not in out


def test_missing_question_without_list_notes(capsys):
code = main(["--notes", str(NOTES)])
err = capsys.readouterr().err
assert code == 2
assert "the following arguments are required: question" in err


def test_list_notes_missing_dir(tmp_path, capsys):
missing = tmp_path / "nonexistent"
code = main(["--list-notes", "--notes", str(missing)])
err = capsys.readouterr().err
assert code == 1
assert "notes folder not found" in err