Spaces:
Sleeping
ResearchPath β Project Log
A chronological record of decisions, work sessions, issues, and outcomes for the ResearchPath portfolio project.
Schema for each entry:
## YYYY-MM-DD HH:MM β [type] Title
**Type:** setup | decision | implementation | issue | resolution | discussion
**Status:** in-progress | complete | blocked | reverted
**Duration:** approx time
### Context
### Decisions
### Actions taken
### Issues encountered
### Files touched
### Outcome
### Next
2026-05-06 15:00 β [discussion] Project framing & scope
Type: decision Status: complete Duration: ~30 min
Context
Chetan (~2.5 YOE Data Scientist, Kyndryl + ex-Swiss Re, targeting top product companies) needed a portfolio project. Earlier "Agentic RAG Document Search" on his resume was copied from another GitHub β needed a genuine, defensible project.
Decisions
- Rejected Underwriter Copilot (insurance) and Equity Research Analyst (SEC filings) β too generic / didn't match his preference for research-flavored work.
- Rejected generic-RAG-over-papers framing β already exists (Perplexity, Elicit, Consensus); not enough differentiation.
- Committed: ResearchPath β agentic research-onboarding companion. Killer feature: prerequisite-chain reading-path planning. Genuinely agentic (graph traversal + recursive retrieval + reasoning over user state), not RAG-with-extra-steps.
- Demo domain: Reinforcement Learning. Architecture domain-agnostic.
- Stack: Gemini 2.5 Flash Lite + Groq Llama 3.3 70B (free tiers), BAAI/bge-small-en embeddings, FAISS, LangGraph, Streamlit, RAGAS.
- Eval-first methodology: gold dataset + ablation table is the differentiator from generic portfolios.
Outcome
Project scope locked. 5-week plan in README.
Next
Day 1 setup: env, deps, API keys, smoke test.
2026-05-06 15:20 β [setup] Day 1: Environment bootstrap
Type: setup Status: complete Duration: ~45 min
Context
Stand up local dev env so the agent work in Week 2+ has a working LLM substrate.
Decisions
- Use
uvfor Python + dependency management (already installed on user's machine). - Default Gemini model:
gemini-2.5-flash-lite(chosen aftergemini-2.5-flashreturned transient 503s during smoke test).
Actions taken
- Scaffolded repo files:
.gitignore,.env.example,README.md,pyproject.toml,researchpath/__init__.py,researchpath/llm.py,scripts/smoke_test.py. uv python install 3.12β installed CPython 3.12.9.uv syncβ resolved 38 packages includinggoogle-genai,groq,python-dotenv,rich,pytest,ruff.- User created
.envfrom template and pasted Gemini + Groq API keys. uv run python scripts/smoke_test.pyβ both providers returned valid completions.
Issues encountered
- Unicode arrow
βbroke on Windows cp1252 codepage whenrichtried to write to stdout. Fix: removed the arrow fromscripts/smoke_test.py. Also setPYTHONIOENCODING=utf-8for the run. - Gemini 2.5 Flash returned 503 UNAVAILABLE (transient overload) despite a valid key. Fix: switched default model in
researchpath/llm.pyfromgemini-2.5-flashtogemini-2.5-flash-lite.
Files touched
- Created:
.gitignore,.env.example,README.md,pyproject.toml,researchpath/__init__.py,researchpath/llm.py,scripts/smoke_test.py,.claude/settings.local.json,PROJECT_LOG.md. - User-created (gitignored):
.env.
Outcome
Both Gemini and Groq APIs verified end-to-end. .claude/settings.local.json grants Bash + PowerShell auto-approval for this project (gitignored β personal preference, not committed).
Next
Day 2: git init β arxiv corpus fetch script β download ~17 canonical RL papers β first commit.
2026-05-06 16:00 β [implementation] Day 2: Corpus pipeline + first commit
Type: implementation Status: complete (with one outstanding follow-up β see "Outcome") Duration: ~75 min
Context
Build the end-to-end corpus path: fetch canonical RL papers from arXiv β parse PDFs β chunk text into a single JSONL ready for embedding. Land everything as the first git commit.
Decisions
- 17-paper canon chosen across 5 eras (value-based, policy-gradient, actor-critic, model-based, RLHF). See
researchpath/corpus.py. - Streaming downloads via
requestsinstead ofarxivlibrary's built-in downloader β the latter was truncating large PDFs at exact 1MB / 2MB boundaries on Windows. - PDF validation step (open with pymupdf, read page 1) gates every download; truncated files auto-deleted and retried.
- Chunking: 800-char windows, 100-char overlap, sentence-boundary aware, hyphenation-aware.
- Accept partial corpus (10/17) rather than grind on rate-limited retries β script is idempotent so missing papers can be picked up later.
Actions taken
- Added deps:
arxiv,pymupdf.uv syncresolved 5 new packages. researchpath/corpus.pyβ 17-paper canon witheratag.scripts/fetch_corpus.pyβ streaming downloader with per-paper validate-and-retry, 12s inter-request spacing, 30s backoff.researchpath/parsing.pyβ PDF text extraction + sentence-boundary chunking.scripts/parse_corpus.pyβ drives parsing across all valid PDFs intodata/chunks.jsonl.- Ran fetcher: 10 papers landed cleanly. Deleted 3 truncated stragglers (
1801.01290,1911.08265,2301.04104). - Ran parser: 1,093 chunks, 778,995 chars across 10 papers.
git init -b mainβ first commit270993c("Bootstrap ResearchPath: scaffold + corpus pipeline"). 13 files, 1,611 insertions.
Issues encountered
- arXiv HTTP 429 rate-limiting on PDF endpoint. Tried 3sβ8sβ12s inter-request delays; eventually too much accumulated state on this IP. Pivoted to "accept partial corpus, document the gap" per user's
feedback_dont_grind.md. arxivlibrary truncates large PDFs at 1MB/2MB even on retry β buffer issue specific to its downloader. Switched to directrequestsstreaming. (Confirmed by post-hoc validation: 3 stragglers stuck at exactly 2,097,152 / 1,048,576 bytes.)- Unicode
βin console output broke on cp1252 (already known from Day 1; reusedPYTHONIOENCODING=utf-8).
Files touched
- Added:
researchpath/corpus.py,researchpath/parsing.py,scripts/fetch_corpus.py,scripts/parse_corpus.py. - Modified:
pyproject.toml(+arxiv, +pymupdf). - Generated (gitignored):
data/papers/*.pdf(10 files),data/chunks.jsonl.
Outcome
End-to-end corpus path works: arxiv ID β validated PDF β chunked JSONL. Repo under git, first commit landed.
Outstanding: 4 papers still missing β 1511.05952 (PER), 1707.06347 (PPO), 1802.01561 (IMPALA), 2106.01345 (Decision Transformer). PPO is the painful gap (canonical paper). Re-run uv run python scripts/fetch_corpus.py later (script is idempotent + only retries missing/invalid).
Next
Day 3: embeddings + FAISS index over data/chunks.jsonl. Use BAAI/bge-small-en-v1.5 (CPU-friendly, ~133MB, strong on academic text). Then a tiny CLI: ask question β top-k chunks β grounded Gemini answer. That's the baseline RAG to measure against.
2026-05-06 17:00 β [implementation] Day 3: Embeddings, FAISS index, baseline RAG
Type: implementation Status: complete Duration: ~50 min
Context
Stand up the first measurable RAG system: chunks β embeddings β vector search β grounded LLM answer with citations. This becomes the baseline that all future improvements (hybrid retrieval, reranker, agent planning) get measured against.
Decisions
- Embedding model:
BAAI/bge-small-en-v1.5(384 dim, ~133MB, CPU-fast, strong on academic text). Documents embedded raw; queries prefixed with the BGE-recommended retrieval instruction for measurably better results. - FAISS
IndexFlatIPwith L2-normalized embeddings β exact cosine search, fast enough for ~1k chunks. Will swap to HNSW if corpus grows past ~50k. - Citation format
[arxiv_id, p<page>]β parseable by future eval code, human-readable in output. - System prompt is strict: only-from-sources, no speculation. Will tighten further once we have eval numbers.
Actions taken
- Added deps:
sentence-transformers,faiss-cpu,numpy.uv syncresolved 29 new packages (~330MB inc. torch CPU). researchpath/embeddings.pyβEmbedderwrapper with separateembed_documents/embed_query(BGE wants different prefixes).researchpath/index.pyβbuild_index,load_index,search.Hitdataclass carries score + citation metadata.researchpath/rag.pyβbuild_prompt,answer. Strict system prompt enforcing citations + grounding.scripts/build_index.pyβ drives end-to-end embed + persist.scripts/ask.pyβ CLI for question β answer with citations.--show-sourcesflag for debugging retrieval.- Built index: 1,093 chunks embedded in 148s on CPU. Persisted
data/index.faiss+data/index.chunks.json. - Smoke test query: "How does Rainbow combine multiple deep Q-learning improvements like Double DQN, Dueling networks, and prioritized replay?"
- Answer correctly cited [1710.02298, p1] for Rainbow components and [1511.06581, p4] for Dueling architecture details.
- Listed all 6 Rainbow components accurately. No hallucination observed.
- 1,106 input / 196 output tokens (β free under Gemini quota).
Issues encountered
- HF Hub symlink warning on Windows β cache-system can't use symlinks without admin/Developer Mode. Cosmetic only; cache works in degraded mode (more disk, no functional impact).
VIRTUAL_ENVmismatch warning from uv β leftover env var from a prior shell session pointing at a different Python. Harmless;.venvis used regardless.
Files touched
- Added:
researchpath/embeddings.py,researchpath/index.py,researchpath/rag.py,scripts/build_index.py,scripts/ask.py. - Modified:
pyproject.toml(+sentence-transformers, +faiss-cpu, +numpy),uv.lock. - Generated (gitignored):
data/index.faiss,data/index.chunks.json.
Outcome
Baseline RAG is end-to-end working and producing high-quality, grounded, cited answers on a real RL question. This is the system we'll measure in Day 4.
Next
Day 4: eval harness + gold dataset. Build ~30 (question, expected answer key, expected source citations) tuples sourced from OpenAI Spinning Up problem sets, Sutton & Barto exercises, and 3-5 hand-curated questions per major paper. Then implement metrics (citation faithfulness, retrieval recall@k, answer quality blind-rated 1-5). First numbers go into the README eval table.
2026-05-06 18:00 β [implementation] Day 4: Eval harness + gold dataset + baseline numbers
Type: implementation Status: complete Duration: ~90 min
Context
Build a rigorous eval harness before optimizing anything. Eval-first is the portfolio differentiator β ablation table beats vibes.
Decisions
- 30-question gold dataset across 10 indexed papers (3 per single-paper + 4 cross-paper comparison). Difficulty-stratified: 7 easy / 15 medium / 8 hard. Each example:
{id, question, expected_arxiv_ids, expected_key_claim, difficulty}. - Three metrics (no citation faithfulness per-claim β too expensive on free tier):
retrieval_recall@k: fraction of expected arxiv IDs found in top-k hitscitation_presence: answer cites at least one expected paper (string check)answer_correctness: LLM-as-judge (Groq llama-3.3-70b), YES/NO prompt
- Judge model: Groq not Gemini β Gemini free tier has 20 req/day limit (hit during initial test run). Groq handles batch eval at no cost.
- RAG model: Groq for batch eval. Gemini stays as default for
scripts/ask.py(the demo CLI). Added--groqflag torun_eval.pyto toggle. - Retry logic added to
gemini_generate()β parses retry delay from 429 error body, sleeps and retries up to 5 times.
Actions taken
data/gold_dataset.jsonβ 30 QA triples (hand-authored from Spinning Up + paper reading).researchpath/eval.pyβGoldExample,EvalResult,EvalSummarydataclasses +evaluate_example,summarize,results_to_json.scripts/run_eval.pyβ full pipeline: load index β batch retrieve + generate β judge β print table + save JSON.- Updated
researchpath/rag.pyβ addedgenerate_fninjection param +answer_groq()convenience wrapper. - Updated
researchpath/llm.pyβ retry-with-backoff on Gemini 429 (parsesretryDelayfrom error body). - Ran full 30-question eval (
--groq). Duration: ~3.5 min. Results saved todata/eval_results.json.
Issues encountered
- Gemini 20 req/day free-tier limit β hit on second run (smoke test + partial full run = 20 calls). Fix: route eval to Groq entirely.
- Retry delay detection: Gemini's 429 body contains
retryDelay: '10s'in the Details array. Regex extracts it; if missing, defaults to 15s sleep.
Files touched
- Added:
data/gold_dataset.json,researchpath/eval.py,scripts/run_eval.py. - Modified:
researchpath/rag.py(generate_fn injection, answer_groq),researchpath/llm.py(retry logic),README.md(eval table row 1). - Generated (gitignored):
data/eval_results.json.
Outcome
Baseline RAG numbers (Groq llama-3.3-70b, k=5, n=30):
| Metric | Value |
|---|---|
| Retrieval Recall@5 | 90.0% |
| Citation Presence | 86.7% |
| Answer Correctness | 36.7% |
| Avg Latency | 5.07s |
| RAG Tokens | 33,498 in / 7,000 out |
Gap analysis: Retrieval finds the right paper in 27/30 cases β the FAISS index is working well. But answer correctness is only 37%, revealing two failure modes:
- Wrong-chunk problem: Right paper retrieved, but the specific mechanistic claim is in a chunk outside top-5 (e.g., Rainbow's 6 components, distributional RL definition). Fix: hybrid retrieval (BM25 + vector) to catch exact-term hits + smaller chunks.
- Precision failure: Right chunk retrieved, but answer paraphrases around the key claim without naming the specific mechanism (e.g., "stability" instead of "identifiability" for Dueling DQN mean subtraction). Fix: reranker + more directive prompt.
Difficulty breakdown: easy=28.6%, medium=53.3%, hard=12.5% answer correctness β harder questions show compounding retrieval + precision failures.
Next
Day 5: Optimization round 1 β implement hybrid BM25+vector retrieval and measure improvement on same 30 gold examples. Target: push Answer Correctness from 37% β 55%+.
2026-05-06 19:30 β [implementation] Day 5: Hybrid BM25+FAISS retrieval (RRF fusion)
Type: implementation Status: complete Duration: ~90 min
Context
Day 4 baseline showed Retrieval Recall@5=90% but Answer Correctness=37%. Root-cause: right paper retrieved but wrong chunks β dense embeddings de-prioritize specific technical terms ("conjugate gradient", "Polyak averaging", "identifiability") relative to surrounding context. BM25 keyword search catches exact-term hits that dense misses.
Decisions
- BM25Okapi (rank-bm25 library) for sparse retrieval over the same 1,093 chunks.
- Reciprocal Rank Fusion (RRF, k=60) to combine BM25 and FAISS rankings without normalizing incompatible score spaces.
- Stop-word filter in tokenizer: query "What does GAE stand for?" without stop words =
['gae', 'stand', 'fundamental', 'tradeoff']β much better signal than including "what", "does", "for", "it". - fetch_k=20 from each retriever before fusion, then top-k=5 from combined candidate set.
Actions taken
researchpath/retrieval.pyβBM25Retriever,HybridRetrieverwith RRF fusion. Stop-word list added to tokenizer.researchpath/index.pyβ addedglobal_idxfield toHitdataclass (FAISS vector position, globally unique across papers). Fixed critical bug: original code usedchunk_idx(within-paper index, 0..N) as RRF dict key, causing collisions across papers with same within-paper index.scripts/ask.pyβ added--hybridflag.scripts/run_eval.pyβ added--hybridflag, fixedPath.relative_to()crash on relative paths.- Ran full hybrid eval (
--groq --hybrid): 29/30 questions (cross_04 errored on Groq 100K daily TPD limit).
Issues encountered
- global_idx bug:
chunk_idxis per-paper, not globally unique. Two chunks from different papers with the same within-paper index collided in the RRF dict, silently corrupting results. Fixed by addingglobal_idx(FAISS position) to Hit and using it in HybridRetriever. Pre-fix: hybrid was WORSE than dense in all spot checks. Post-fix: BETTER or TIED. - BM25 stop-word pollution: Without filtering, "What does GAE manage?" matched InstructGPT appendix QA sections (had literal "What is X? A: ...") scoring 19.9. After adding stop-word filter, query tokens =
['gae', 'stand', 'fundamental', 'tradeoff', 'manage'], GAE paper correctly ranked first. - Groq 100K daily TPD: Exhausted daily token limit on the 30th question (cross_04). n=29 eval still valid; cross_04 retry deferred.
Files touched
- Added:
researchpath/retrieval.py. - Modified:
researchpath/index.py(+global_idx field),scripts/ask.py(+--hybrid),scripts/run_eval.py(+--hybrid, path fix),README.md(eval table col 2),PROJECT_LOG.md. - Generated (gitignored): results in memory only (save failed on path bug, now fixed).
Outcome
Hybrid BM25+FAISS results (Groq llama-3.3-70b, k=5, n=29):
| Metric | Baseline | Hybrid | Delta |
|---|---|---|---|
| Retrieval Recall@5 | 90.0% | 89.7% | -0.3 pp |
| Citation Presence | 86.7% | 86.2% | -0.5 pp |
| Answer Correctness | 36.7% | 72.4% | +35.7 pp |
| Avg Latency | 5.07s | 4.58s | -0.5s |
By difficulty: easy 100%, medium 73.3%, hard 42.9%. Hard questions remain the weak spot.
Why hybrid helped so much: The global_idx bug fix + stop-word filter together fixed hybrid retrieval so it correctly fuses dense + sparse signals. Many questions where the right paper was retrieved but answer was wrong (dense recall=100%, correct=N in baseline) flipped to correct under hybrid because BM25 surfaced chunks with the specific technical term rather than contextual chunks about the same topic.
Next
Retry cross_04 when Groq TPD resets (saves the 30/30 result). Then: cross-encoder reranker as column 3 in the ablation table. Target: 75%+ correctness on hard questions.
2026-05-07 β [implementation] Day 5 cont.: Agentic planning loop (offline prerequisite-chain planner)
Type: implementation Status: complete Duration: ~60 min
Context
The killer differentiator of ResearchPath over generic RAG is the prerequisite-chain reading-path planner: given a target RL paper and what the user already knows, compute the topologically-sorted chain of papers they need to read first. This module runs fully offline β no LLM calls, no API quota burned. It's the deterministic core that an LLM annotation layer will augment later.
Decisions
- Static prerequisite graph as the seed: hand-curate ~10 edges covering canonical chains (DQNβDouble DQNβDueling DQNβRainbow, TRPOβGAE, DQNβDDPG, DQNβA3C, InstructGPTβDPO). Enough to demo the killer feature; LLM-extracted edges added in a future iteration.
- BFS backwards from target, stopping at
known_idsβ avoids pulling in papers the user already understands. - Kahn's algorithm for topological sort with
(year, arxiv_id)tie-breaking β deterministic + chronologically sensible for RL literature. - ReadingStep annotation: each step carries
bridges_to(which later papers depend on it),concepts(the bridging ideas), andwhy(user-facing rationale) aggregated from the static edge metadata.
Actions taken
researchpath/prerequisites.pyβPrerequisitedataclass +STATIC_PREREQUISITEStuple (10 hand-curated edges) +edges_into/edges_fromaccessors.researchpath/planning.pyβReadingStep,ReadingPlandataclasses +_bfs_backwards,_topo_sort(Kahn's),_build_step,plan_reading_path.ReadingPlan.render_text()produces human-readable ordered list.scripts/plan.pyβ CLI:--target(arxiv ID or tag, case-insensitive),--known(comma-separated),--list. Rich Panel output.tests/test_planning.pyβ 7 pytest tests: no-prereq paper (1 step), Rainbow full chain (4 steps in order), known set truncates, known target β empty plan, topo order respects all edges, unknown target raises ValueError, render_text contains target title.
Smoke tests (pre-pytest, verified manually):
- Rainbow (
1710.02298, no prior knowledge):1312.5602 β 1509.06461 β 1511.06581 β 1710.02298β - Rainbow knowing DQN:
1509.06461 β 1511.06581 β 1710.02298(3 steps) β - DPO (
2305.18290) knowing InstructGPT: 1 step (DPO itself) β - DDPG (
1509.02971):1312.5602 β 1509.02971(2 steps) β
Issues encountered
None. BFS + Kahn's on the static graph is deterministic and small enough that correctness is straightforward to verify.
Files touched
- Added:
researchpath/prerequisites.py,researchpath/planning.py,scripts/plan.py,tests/test_planning.py.
Outcome
7/7 pytest tests pass in 0.09s. Planning loop runs offline without any API calls. Demonstrates genuine agentic behavior (graph traversal, state-aware planning) that differentiates ResearchPath from simple RAG demos.
Sample CLI output (uv run python scripts/plan.py --target Rainbow):
1. [1312.5602] DQN (2013) β Playing Atari with Deep Reinforcement Learning
bridges: -> 1509.06461, 1511.06581, 1710.02298
concepts: Q-learning, experience replay, target network
2. [1509.06461] DoubleDQN (2015) β Deep Reinforcement Learning with Double Q-learning
bridges: -> 1511.06581, 1710.02298
concepts: overestimation bias, decoupled action selection/evaluation, Double Q-learning
3. [1511.06581] DuelingDQN (2015) β Dueling Network Architectures for Deep Reinforcement Learning
bridges: -> 1710.02298
concepts: dueling networks
4. [1710.02298] Rainbow (2017) β Rainbow: Combining Improvements in Deep Reinforcement Learning
bridges: <- target
Next
- Retry cross_04 when Groq TPD resets β update README hybrid result from n=29 to n=30
- Run reranker full eval (column 3 in ablation table):
--hybrid --rerank - LLM annotation layer: map free-text user background β
known_idsset; per-step "what to focus on" rationale - Fetch 4 missing papers: PER (1511.05952), PPO (1707.06347), IMPALA (1802.01561), Decision Transformer (2106.01345)