MukulRay commited on
Commit
4f24399
Β·
1 Parent(s): ca3ca45

fix: trust summary resilience, Unicode in reason strings, full GitHub README

Browse files
GITHUB_README.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RECON -- Temporally-Aware Scientific Literature Retrieval
2
+
3
+ > A multi-agent RAG system that asks not just "what is relevant?" but "what should I trust, right now?"
4
+
5
+ **Live demo:** https://huggingface.co/spaces/MukulRay/recon
6
+ **Status:** Active development -- v2 (edge reliability) deployed
7
+
8
+ ---
9
+
10
+ ## The Problem
11
+
12
+ Standard RAG retrieves the most semantically similar papers. It has no mechanism to detect when those papers have been superseded by newer work.
13
+
14
+ A 2019 paper with 800 citations scores high on cosine similarity and high on authority. If a 2023 paper explicitly refutes its central claims, retrieving the 2019 paper produces a confident but stale answer. RECON detects this -- and explains why.
15
+
16
+ ---
17
+
18
+ ## What's New in v2
19
+
20
+ v2 replaces the age-based staleness threshold with a three-signal **edge reliability formula**:
21
+
22
+ ```
23
+ edge_reliability = (citation_centrality x 0.4)
24
+ + (recency_signal x 0.3)
25
+ + (content_coherence x 0.3)
26
+ ```
27
+
28
+ A 2003 paper with 10,000 citations scores **FOUNDATIONAL** -- high centrality overrides age.
29
+ A 2020 paper with 5 citations, superseded by newer work, scores **SUPERSEDED**.
30
+ Pure age-based detection cannot make this distinction.
31
+
32
+ ---
33
+
34
+ ## Architecture
35
+
36
+ ```
37
+ session_loader -> planner -> retriever -> critic -> synthesizer -> END
38
+ |
39
+ retry_retriever (max 2)
40
+ ```
41
+
42
+ | Agent | Role |
43
+ |---|---|
44
+ | **Planner** | Decomposes query into 2-3 temporally-typed sub-questions (foundational / recent / open) |
45
+ | **Retriever** | Fetches papers from Semantic Scholar + OpenAlex, deduplicated by DOI. Hybrid scoring: semantic x 0.5 + recency x 0.3 + authority x 0.2 |
46
+ | **Critic** | Computes edge reliability scores, then issues verdict: PASS / STALE / CONTRADICTED / INSUFFICIENT / FORCED_PASS. On non-PASS: rewrites sub-questions with failure-specific strategy |
47
+ | **Synthesizer** | Four-section brief (Overview / Key Findings / Active Debates / Outlook) with per-claim citations and per-paper trust summary |
48
+
49
+ ---
50
+
51
+ ## Edge Reliability Scoring (`src/reliability.py`)
52
+
53
+ Each retrieved paper receives a `ReliabilityScore` with:
54
+
55
+ - `score` -- composite [0, 1]
56
+ - `centrality` -- `min(1.0, log1p(cited_by_count) / log1p(10000))` from OpenAlex
57
+ - `recency` -- `max(0, 1 - age/20)` linear decay
58
+ - `coherence` -- LLM batch check: does this paper's abstract still represent current consensus?
59
+ - `dominant_signal` -- `FOUNDATIONAL` / `CURRENT` / `DECLINING` / `SUPERSEDED`
60
+ - `reason` -- one-line explanation
61
+
62
+ The synthesizer appends a trust summary to every response so domain experts can verify verdict reasoning.
63
+
64
+ ---
65
+
66
+ ## Evaluation
67
+
68
+ 130-question benchmark across three categories: consensus claims (Cat A), superseded claims (Cat B), contested claims (Cat C). Ground truth sourced from real ML survey paper supersession chains.
69
+
70
+ | Architecture | Staleness Catch Rate | Position Accuracy | False Positives |
71
+ |---|---|---|---|
72
+ | Single-pass RAG (baseline) | 0% | 32.3% | -- |
73
+ | Naive multi-agent | 0% | 44.6% | -- |
74
+ | RECON v1 (age-based STALE) | 52% | 43.9% | 8% |
75
+ | **RECON v2 (edge reliability)** | **44%** | **44.6%** | **2%** |
76
+
77
+ v2 trades some staleness recall for substantially lower false-positive rate. The reliability formula correctly preserves foundational papers that v1 would incorrectly flag as stale.
78
+
79
+ **Known limitation:** Contradiction catch rate is 0% -- the retriever returns topically adjacent papers rather than opposing-camp papers. This is a retrieval problem, not a critic problem. Addressed in future work.
80
+
81
+ ---
82
+
83
+ ## Repository Structure
84
+
85
+ ```
86
+ src/
87
+ agents/
88
+ planner.py -- query decomposition, temporally-typed sub-questions
89
+ retriever.py -- S2 + OpenAlex fetch, hybrid scoring, DOI dedup
90
+ critic.py -- edge reliability scoring, verdict logic, retry
91
+ synthesizer.py -- synthesis, trust summary, claim extraction
92
+ openalex_utils.py -- OpenAlex API (search, DOI lookup, citation centrality)
93
+ reliability.py -- three-signal edge reliability scorer
94
+ retriever_utils.py -- hybrid_score, recency_score, authority_score, S2 API
95
+ state.py -- ResearchState TypedDict, Paper/Claim dataclasses
96
+ memory.py -- SQLite session persistence
97
+ graph.py -- LangGraph state machine, node wiring
98
+ app.py -- Gradio UI
99
+ eval/
100
+ run_eval.py -- 5-architecture evaluation harness, LLM-as-judge
101
+ questions.json -- 130-question benchmark
102
+ ground_truth.json -- ground truth for Cat A/B
103
+ results/ -- eval CSVs
104
+ archived/ -- patch_contradiction.py (archived, not used in reported metrics)
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Setup
110
+
111
+ ```bash
112
+ git clone https://github.com/MukulRay1603/project-recon
113
+ cd project-recon
114
+ pip install -r requirements.txt
115
+ ```
116
+
117
+ Create a `.env` file:
118
+
119
+ ```
120
+ GROQ_API_KEY=...
121
+ OPENALEX_API_KEY=... # free at openalex.org/settings/api
122
+ S2_API_KEY=... # optional but recommended
123
+ TAVILY_API_KEY=... # optional fallback web search
124
+ ```
125
+
126
+ ```bash
127
+ python app.py
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Tech Stack
133
+
134
+ | Component | Choice |
135
+ |---|---|
136
+ | Orchestration | LangGraph |
137
+ | LLM | Llama 3.3 70B via Groq |
138
+ | Embeddings | all-MiniLM-L6-v2 (sentence-transformers) |
139
+ | Paper APIs | Semantic Scholar + OpenAlex |
140
+ | Web search | DuckDuckGo (Tavily fallback) |
141
+ | Session memory | SQLite |
142
+ | UI | Gradio |
143
+ | Deployment | Hugging Face Spaces |
144
+
145
+ ---
146
+
147
+ ## Author
148
+
149
+ Mukul Ray -- MS Applied ML, University of Maryland College Park
150
+ GitHub: [@MukulRay1603](https://github.com/MukulRay1603)
README.md CHANGED
@@ -11,6 +11,8 @@ license: mit
11
  short_description: Multi-agent ML literature research with staleness detection
12
  ---
13
 
 
 
14
  # RECON β€” Temporally-Aware Scientific Retrieval
15
 
16
  A multi-agent RAG system that detects when retrieved scientific evidence has been superseded by newer work.
 
11
  short_description: Multi-agent ML literature research with staleness detection
12
  ---
13
 
14
+ > For full documentation, architecture details, and eval results: [GITHUB_README.md](./GITHUB_README.md)
15
+
16
  # RECON β€” Temporally-Aware Scientific Retrieval
17
 
18
  A multi-agent RAG system that detects when retrieved scientific evidence has been superseded by newer work.
src/agents/critic.py CHANGED
@@ -163,6 +163,7 @@ def critic_node(state: ResearchState) -> ResearchState:
163
  "rewritten_questions": [],
164
  "retry_count": retry_count,
165
  "calibration_bin": Verdict.FORCED_PASS,
 
166
  }
167
 
168
  # INSUFFICIENT β€” not enough papers
@@ -175,6 +176,7 @@ def critic_node(state: ResearchState) -> ResearchState:
175
  "rewritten_questions": rewritten,
176
  "retry_count": retry_count + 1,
177
  "calibration_bin": Verdict.INSUFFICIENT,
 
178
  }
179
 
180
  # INSUFFICIENT β€” scores too low
@@ -188,11 +190,18 @@ def critic_node(state: ResearchState) -> ResearchState:
188
  "rewritten_questions": rewritten,
189
  "retry_count": retry_count + 1,
190
  "calibration_bin": Verdict.INSUFFICIENT,
 
191
  }
192
 
193
  # --- Phase 2.4: Compute edge reliability scores for all papers ---
194
  original_query = state.get("original_query", "")
195
- reliability_scores = score_papers(papers, query=original_query, use_llm=True)
 
 
 
 
 
 
196
 
197
  # --- Run STALE and CONTRADICTED checks in parallel (both always run) ---
198
  mean_age = _mean_age_months(papers)
 
163
  "rewritten_questions": [],
164
  "retry_count": retry_count,
165
  "calibration_bin": Verdict.FORCED_PASS,
166
+ "paper_reliability_scores": {},
167
  }
168
 
169
  # INSUFFICIENT β€” not enough papers
 
176
  "rewritten_questions": rewritten,
177
  "retry_count": retry_count + 1,
178
  "calibration_bin": Verdict.INSUFFICIENT,
179
+ "paper_reliability_scores": {},
180
  }
181
 
182
  # INSUFFICIENT β€” scores too low
 
190
  "rewritten_questions": rewritten,
191
  "retry_count": retry_count + 1,
192
  "calibration_bin": Verdict.INSUFFICIENT,
193
+ "paper_reliability_scores": {},
194
  }
195
 
196
  # --- Phase 2.4: Compute edge reliability scores for all papers ---
197
  original_query = state.get("original_query", "")
198
+ try:
199
+ reliability_scores = score_papers(papers, query=original_query, use_llm=True)
200
+ if not reliability_scores:
201
+ logger.warning("score_papers() returned empty dict β€” falling back to no reliability scores")
202
+ except Exception as e:
203
+ logger.warning(f"score_papers() failed entirely: {e} β€” trust summary will be skipped")
204
+ reliability_scores = {}
205
 
206
  # --- Run STALE and CONTRADICTED checks in parallel (both always run) ---
207
  mean_age = _mean_age_months(papers)
src/agents/synthesizer.py CHANGED
@@ -286,6 +286,8 @@ Synthesize a research position on this query using the evidence above."""
286
 
287
  # --- Phase 2.6: Trust summary block ---
288
  reliability_scores = state.get("paper_reliability_scores", {})
 
 
289
  if reliability_scores and papers:
290
  trust_lines = ["\n\n---\n## Evidence Trust Summary\n"]
291
  for p in papers[:8]: # same top-8 window synthesizer already uses
 
286
 
287
  # --- Phase 2.6: Trust summary block ---
288
  reliability_scores = state.get("paper_reliability_scores", {})
289
+ if not reliability_scores:
290
+ logger.warning("paper_reliability_scores is empty β€” trust summary skipped. Check if score_papers() ran in critic.")
291
  if reliability_scores and papers:
292
  trust_lines = ["\n\n---\n## Evidence Trust Summary\n"]
293
  for p in papers[:8]: # same top-8 window synthesizer already uses
src/reliability.py CHANGED
@@ -185,13 +185,13 @@ def _build_reason(dominant: str, centrality: float, recency: float,
185
  age_str = f"{age}yr old" if age is not None else "unknown age"
186
 
187
  if dominant == "FOUNDATIONAL":
188
- return f"High citation centrality ({centrality:.2f}), {age_str} β€” foundational work still current"
189
  elif dominant == "CURRENT":
190
- return f"Recent ({age_str}), coherence={coherence:.2f} β€” aligns with current consensus"
191
  elif dominant == "DECLINING":
192
  return f"Mixed signals: centrality={centrality:.2f}, recency={recency:.2f}, coherence={coherence:.2f}"
193
  else:
194
- return f"Low reliability: {age_str}, centrality={centrality:.2f}, coherence={coherence:.2f} β€” likely superseded"
195
 
196
 
197
  def score_papers(papers: list, query: str, use_llm: bool = True) -> dict[str, ReliabilityScore]:
 
185
  age_str = f"{age}yr old" if age is not None else "unknown age"
186
 
187
  if dominant == "FOUNDATIONAL":
188
+ return f"High citation centrality ({centrality:.2f}), {age_str} - foundational work still current"
189
  elif dominant == "CURRENT":
190
+ return f"Recent ({age_str}), coherence={coherence:.2f} - aligns with current consensus"
191
  elif dominant == "DECLINING":
192
  return f"Mixed signals: centrality={centrality:.2f}, recency={recency:.2f}, coherence={coherence:.2f}"
193
  else:
194
+ return f"Low reliability: {age_str}, centrality={centrality:.2f}, coherence={coherence:.2f} - likely superseded"
195
 
196
 
197
  def score_papers(papers: list, query: str, use_llm: bool = True) -> dict[str, ReliabilityScore]: