CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
# Run the app (http://127.0.0.1:6001)
python main.py
# Ingest documents from data/uploads/
python ingest_docs.py
# Run all tests
../venv/bin/python -m pytest tests/
# Run a single test file
../venv/bin/python -m pytest tests/test_phase2_rag_graph.py
# Run a single test case
../venv/bin/python -m pytest tests/test_phase2_rag_graph.py::Phase2RagGraphTests::test_build_initial_graph_state_sets_expected_defaults
# Evaluate retrieval and answer quality
../venv/bin/python evaluate_retrieval.py
Environment
Copy .env.example to .env. Key variables:
| Variable | Default | Purpose |
|---|---|---|
GROQ_API_KEY |
β | LLM inference (Groq) |
GEMINI_KEY |
β | Alternative LLM (Google Gemini) |
RABBOOK_LLM_PROVIDER |
groq |
Provider select: groq, gemini, or ollama |
RABBOOK_LLM_MODEL |
llama-3.1-8b-instant |
Model name |
RABBOOK_OLLAMA_BASE_URL |
http://localhost:11434 |
Ollama endpoint (local provider) |
RABBOOK_ENABLE_LANGGRAPH_AGENT |
true |
Route queries through the LangGraph RAG graph (the only maintained path) |
RABBOOK_ENABLE_RESEARCH_FALLBACK |
false |
Allow the RAG graph to fall back to the web research agent |
RABBOOK_MIN_GROUNDED_RERANK_SCORE |
1.0 |
Grounding gate threshold |
All retrieval parameters (RETRIEVAL_K, CONTEXT_WINDOW, MIN_GROUNDED_CHUNKS, etc.) are in core/config.py and can be overridden via env vars.
Architecture
Layer separation
rag/ β retrieval primitives (no app state, no web concerns)
agents/ β LangGraph graphs and orchestration
app/ β FastAPI routes, runtime state, view helpers
learning/ β flashcard and review logic (future)
core/ β config and shared constants
agents/services.py is the public API between app/ and the agent/retrieval layers. All query routing goes through answer_query() there.
answer_query() has two branches keyed on use_langgraph. In practice queries always take the LangGraph path (run_rag_graph_answer); the else branch (the inline "Direct RAG" pipeline) is legacy and no longer used β kept for reference, not maintained. Don't extend it or worry about keeping it in sync. The research fallback (enable_research) is wired only through the LangGraph path.
Retrieval pipeline (rag/retrieve.py)
The multi-stage pipeline runs in this order:
- Query transform β LLM generates 2β4 sub-queries (
generate_sub_queries) - Candidate collection β dense (Chroma) + BM25 results per query, deduplicated
- RRF fusion β
fuse_ranked_documentsmerges ranked lists - Reranking β
CrossEncoderreorders the fused set against the original query - Context window expansion β
expand_with_context_windowadds neighboring chunks from the same document using the chunk registry - Grounding check β
check_grounding_evidencegates on rerank score and chunk count - Answer generation β
generate_answertries structured output first, falls back to plain invoke, then runs citation repair if needed
LangGraph RAG graph (agents/rag_graph.py)
Activated when RABBOOK_ENABLE_LANGGRAPH_AGENT=true. The graph wraps the same retrieval pipeline as nodes:
prepare_input β retrieve β expand_context β check_grounding
β decide_next_action
β generate_answer β finalize_response β END
β fallback_answer β finalize_response β END
RagGraphState is a TypedDict carrying all intermediate state. decide_next_action_node sets next_action to "answer", "retry_retrieval", or "fallback" β but "retry_retrieval" routing is not yet wired (known gap, addressed in Phase 3 of PROJECT_UPDATE_PLAN.md).
Research graph (agents/research_graph.py)
A separate LangGraph agent for open-web research, distinct from the RAG graph (which answers from ingested documents). Reached via the enable_research flag through answer_query(); off by default (RABBOOK_ENABLE_RESEARCH_FALLBACK). Nodes:
plan_search β execute_search β finalize β END
β save_note (when save_to_notes)
plan_searchreusesgenerate_sub_queriesto turn the topic into search queries.execute_searchrunsweb_search(DuckDuckGo viaddgs) per query, dedupes by URL, thenfetch_url_content(crawl4ai) on the top 3 for full text, falling back to the snippet on fetch failure.finalizesynthesizes;save_noteoptionally persists viarag/notes.py.
Entry point is run_research_agent(topic, llm=..., save_to_notes=..., debug_mode=...), returning a ResearchResult (defined in agents/services.py alongside AnswerResult).
Chunk registry (chunk_registry.json)
A flat JSON index of every ingested chunk, structured as:
{
"by_document": { "<document_id>": { "<chunk_index>": { "page_content": "...", "metadata": {} } } },
"by_chunk_id": { "<chunk_id>": { "page_content": "...", "metadata": {} } }
}
The registry enables O(1) neighbor lookup for context window expansion and powers the BM25 index. It is rebuilt by ingest_docs.py or via the UI maintenance actions.
App runtime (app/runtime.py)
Vectorstore, chunk registry, and BM25 index are lazy-loaded and cached on app.state. Call refresh_runtime_state after ingestion to invalidate the cache.
LLM selection (app/web.py)
The app instantiates ChatGroq, ChatGoogleGenerativeAI, or ChatOllama based on RABBOOK_LLM_PROVIDER and available API keys. All support .with_structured_output(), which the retrieval layer uses for structured query rewriting and answer drafting. Ollama-specific tuning (RABBOOK_OLLAMA_NUM_GPU, RABBOOK_OLLAMA_THINKING) lives in core/config.py.
Ingestion flow
ingest_docs.py β rag/ingest.py (file loading, metadata) β rag/chunking.py (semantic chunking) β Chroma + chunk_registry.json
URL imports: rag/web_ingest.py fetches and stores pages under data/uploads/urls/ so they survive re-ingestion as regular documents. It also exposes web_search (ddgs) and fetch_url_content (crawl4ai), shared with the research graph.
Tests
Tests use unittest with mocks β no real LLM or vectorstore calls. Phase-named files track the build-out order:
test_phase1_architecture.pyβ module boundaries and import structuretest_phase2_rag_graph.pyβ LangGraph RAG-graph node behavior and routingtest_phase3_rag_agent.pyβ RAG agent integrationtest_phase4_research_agent.py/test_research_graph.pyβ research graph nodes andrun_research_agenttest_query_transform.pyβ sub-query generation and parsingtest_answer_structured_output.pyβ structured answer extraction and citation repairtest_web_ingest.pyβweb_search/fetch_url_contenttest_actions.py,test_gemma_prompt_wrapper.pyβ app actions and prompt wrapping
Working style
This project is a learning environment. The user reviews every change to understand it, so:
- Keep each implementation step small to medium. One focused change per session: one new node, one new function, one new test file. Do not bundle multiple features into a single change.
- Explain the why before writing code. Before implementing, briefly state what the change does and why it fits the architecture. One or two sentences is enough.
- Prefer readable over clever. Step-by-step control flow, explicit variable names (
retrieved_documentsnotdocs), no unnecessary abstractions. Code should be easy to read on first review. - Do not add helpers, error handling, or abstractions beyond what the immediate task requires. The user will ask for the next step when ready.
- After each change, point out what to look at. Name the specific function or file the user should read to understand what changed and why.