researchpath / PROJECT_LOG.md
Chetan110801's picture
Upload folder using huggingface_hub
2f25a40 verified
|
Raw
History Blame Contribute Delete
22.7 kB

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 uv for Python + dependency management (already installed on user's machine).
  • Default Gemini model: gemini-2.5-flash-lite (chosen after gemini-2.5-flash returned 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 including google-genai, groq, python-dotenv, rich, pytest, ruff.
  • User created .env from 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 when rich tried to write to stdout. Fix: removed the arrow from scripts/smoke_test.py. Also set PYTHONIOENCODING=utf-8 for the run.
  • Gemini 2.5 Flash returned 503 UNAVAILABLE (transient overload) despite a valid key. Fix: switched default model in researchpath/llm.py from gemini-2.5-flash to gemini-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 requests instead of arxiv library'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 sync resolved 5 new packages.
  • researchpath/corpus.py β€” 17-paper canon with era tag.
  • 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 into data/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 commit 270993c ("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.
  • arxiv library truncates large PDFs at 1MB/2MB even on retry β€” buffer issue specific to its downloader. Switched to direct requests streaming. (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; reused PYTHONIOENCODING=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 IndexFlatIP with 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 sync resolved 29 new packages (~330MB inc. torch CPU).
  • researchpath/embeddings.py β€” Embedder wrapper with separate embed_documents / embed_query (BGE wants different prefixes).
  • researchpath/index.py β€” build_index, load_index, search. Hit dataclass 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-sources flag 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_ENV mismatch warning from uv β€” leftover env var from a prior shell session pointing at a different Python. Harmless; .venv is 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 hits
    • citation_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 --groq flag to run_eval.py to 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, EvalSummary dataclasses + 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 β€” added generate_fn injection param + answer_groq() convenience wrapper.
  • Updated researchpath/llm.py β€” retry-with-backoff on Gemini 429 (parses retryDelay from error body).
  • Ran full 30-question eval (--groq). Duration: ~3.5 min. Results saved to data/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:

  1. 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.
  2. 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, HybridRetriever with RRF fusion. Stop-word list added to tokenizer.
  • researchpath/index.py β€” added global_idx field to Hit dataclass (FAISS vector position, globally unique across papers). Fixed critical bug: original code used chunk_idx (within-paper index, 0..N) as RRF dict key, causing collisions across papers with same within-paper index.
  • scripts/ask.py β€” added --hybrid flag.
  • scripts/run_eval.py β€” added --hybrid flag, fixed Path.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_idx is 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 adding global_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), and why (user-facing rationale) aggregated from the static edge metadata.

Actions taken

  • researchpath/prerequisites.py β€” Prerequisite dataclass + STATIC_PREREQUISITES tuple (10 hand-curated edges) + edges_into / edges_from accessors.
  • researchpath/planning.py β€” ReadingStep, ReadingPlan dataclasses + _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_ids set; per-step "what to focus on" rationale
  • Fetch 4 missing papers: PER (1511.05952), PPO (1707.06347), IMPALA (1802.01561), Decision Transformer (2106.01345)