Phase 2: UI reliability badges, changelog, eval runner — Phase 2 complete
Browse files- CHANGELOG.md +43 -0
- app.py +27 -6
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
| 2 |
|
| 3 |
## [Unreleased]
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
### Phase 1 — Integrity + Critic Fix
|
| 6 |
- 1.1: Archived patch_contradiction.py to eval/archived/ with README
|
| 7 |
- 1.2: Fixed critic_node — STALE and CONTRADICTED checks now run in parallel
|
|
|
|
| 2 |
|
| 3 |
## [Unreleased]
|
| 4 |
|
| 5 |
+
### Phase 2 — Edge Reliability Scoring + OpenAlex Augmentation
|
| 6 |
+
- 2.1: Created src/openalex_utils.py
|
| 7 |
+
- `search_openalex(query, max_results=5)` — keyword search, returns Paper-compatible dicts
|
| 8 |
+
- `get_openalex_by_doi(doi)` — singleton DOI lookup for citation enrichment
|
| 9 |
+
- `get_citation_centrality(doi, citation_count)` — `min(1.0, log1p(count) / log1p(10000))`
|
| 10 |
+
- Abstract reconstruction from OpenAlex inverted index format
|
| 11 |
+
- DOI URL stripping (`https://doi.org/` prefix removed)
|
| 12 |
+
- API key auth via `OPENALEX_API_KEY` env var; graceful degradation without key
|
| 13 |
+
- 2.2: Added DOI extraction to retriever_utils.py
|
| 14 |
+
- Added `"externalIds"` to Semantic Scholar API field list
|
| 15 |
+
- Extracts `doi = (r.get("externalIds") or {}).get("DOI", "") or ""`
|
| 16 |
+
- Passes `doi=doi` to Paper constructor
|
| 17 |
+
- Added `doi: str = ""` field to Paper dataclass in state.py (after references, before hybrid_score)
|
| 18 |
+
- 2.3: OpenAlex augmentation in retriever.py
|
| 19 |
+
- Triggered when S2 retrieval returns < 12 papers
|
| 20 |
+
- Searches top 2 sub-questions, max 3 results each
|
| 21 |
+
- Deduplicates by DOI and paper_id before merging
|
| 22 |
+
- OpenAlex papers get hybrid_score from semantic sim (0.3) + authority + recency
|
| 23 |
+
- 2.4: Created src/reliability.py — three-signal paper reliability scorer
|
| 24 |
+
- Signals: centrality (40%), recency (30%), coherence (30%)
|
| 25 |
+
- Centrality: `min(1.0, log1p(citation_count) / log1p(10000))`
|
| 26 |
+
- Recency: `max(0, 1 - (age_years / 10))`
|
| 27 |
+
- Coherence: batched LLM call (one call for all papers) — scores 0–1 relevance to query
|
| 28 |
+
- Dominant signal labels: FOUNDATIONAL / CURRENT / DECLINING / SUPERSEDED
|
| 29 |
+
- FOUNDATIONAL: `centrality >= 0.60 AND (coherence >= 0.65 OR coherence == 0.0)` — coherence passthrough for LLM-off case
|
| 30 |
+
- Returns `{paper_id: ReliabilityScore}` dict
|
| 31 |
+
- 2.5: Wired reliability scorer into critic.py
|
| 32 |
+
- `score_papers()` called after high-score paper check
|
| 33 |
+
- STALE threshold changed from `mean_age > 24` to `mean_reliability < 0.40`
|
| 34 |
+
- Falls back to age-based threshold if scorer returns empty
|
| 35 |
+
- `paper_reliability_scores` added to ResearchState TypedDict
|
| 36 |
+
- Scores serialised to `{pid: rs.__dict__}` in all non-INSUFFICIENT critic return paths
|
| 37 |
+
- 2.6: Added per-paper trust summary to synthesizer output
|
| 38 |
+
- Reads `paper_reliability_scores` from state after synthesis
|
| 39 |
+
- Appends `## Evidence Trust Summary` block to `synthesized_position`
|
| 40 |
+
- Color-coded signal labels: HIGH (FOUNDATIONAL/CURRENT), MEDIUM (DECLINING), LOW (SUPERSEDED)
|
| 41 |
+
- Trust summary included in `export_md` via `_build_export_md`
|
| 42 |
+
- Warning fix: `_headers()` in openalex_utils.py downgraded from WARNING to DEBUG when API key absent (was spamming 14+ warnings per query during centrality scoring)
|
| 43 |
+
- Eval results (recon_linear_v2_full, 130Q):
|
| 44 |
+
- Staleness (STALE verdict): 32.3% (42/130) — down from 52% v1; reliability-based threshold is more precise, fewer false-positive STALE calls
|
| 45 |
+
- Contradiction catch rate: 0% (retriever still returns adjacent-topic papers, not opposing-camp pairs — known gap, deferred)
|
| 46 |
+
- Position accuracy (MATCH): 44.6% (58/130) — unchanged vs v1 43.9%
|
| 47 |
+
|
| 48 |
### Phase 1 — Integrity + Critic Fix
|
| 49 |
- 1.1: Archived patch_contradiction.py to eval/archived/ with README
|
| 50 |
- 1.2: Fixed critic_node — STALE and CONTRADICTED checks now run in parallel
|
app.py
CHANGED
|
@@ -43,7 +43,14 @@ def _highlight_citations(text: str) -> str:
|
|
| 43 |
text
|
| 44 |
)
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"""Render retrieved papers as styled cards."""
|
| 48 |
if not papers:
|
| 49 |
return "<p style='color:#6b7280;font-style:italic'>No papers retrieved.</p>"
|
|
@@ -54,15 +61,29 @@ def _paper_cards_html(papers) -> str:
|
|
| 54 |
authors = ", ".join(p.authors[:2]) + (" et al." if len(p.authors) > 2 else "") if p.authors else "Unknown"
|
| 55 |
abstract_preview = (p.abstract[:180] + "...") if p.abstract and len(p.abstract) > 180 else (p.abstract or "")
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
cards.append(f"""
|
| 58 |
<div style="border:1px solid #2d3748;border-radius:8px;padding:12px 14px;
|
| 59 |
margin-bottom:8px;background:#1a1f2e;">
|
| 60 |
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px">
|
| 61 |
<span style="font-weight:600;font-size:0.92em;color:#e2e8f0;flex:1;margin-right:8px">{p.title}</span>
|
| 62 |
-
<
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
</div>
|
| 67 |
<div style="color:#94a3b8;font-size:0.8em;margin-bottom:6px">
|
| 68 |
{authors} · {p.year} · {p.citation_count:,} citations · <span style="color:#64748b">{p.source}</span>
|
|
@@ -205,7 +226,7 @@ def run_query(query, session_id, decay_config, history):
|
|
| 205 |
verdict_html = _verdict_badge_html(verdict, critic_notes, retry_count,
|
| 206 |
papers_used, latency, decay_config, rewritten)
|
| 207 |
claims_html = _claims_html(result.get("claim_confidences") or [])
|
| 208 |
-
papers_html = _paper_cards_html(result.get("retrieved_papers") or [])
|
| 209 |
session_ctx = load_session(session_id)
|
| 210 |
session_html = _session_html(session_ctx, session_id)
|
| 211 |
export_md = result.get("export_md", "")
|
|
|
|
| 43 |
text
|
| 44 |
)
|
| 45 |
|
| 46 |
+
SIGNAL_COLORS = {
|
| 47 |
+
"FOUNDATIONAL": "#22c55e",
|
| 48 |
+
"CURRENT": "#3b82f6",
|
| 49 |
+
"DECLINING": "#f59e0b",
|
| 50 |
+
"SUPERSEDED": "#ef4444",
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def _paper_cards_html(papers, reliability_scores: dict = {}) -> str:
|
| 54 |
"""Render retrieved papers as styled cards."""
|
| 55 |
if not papers:
|
| 56 |
return "<p style='color:#6b7280;font-style:italic'>No papers retrieved.</p>"
|
|
|
|
| 61 |
authors = ", ".join(p.authors[:2]) + (" et al." if len(p.authors) > 2 else "") if p.authors else "Unknown"
|
| 62 |
abstract_preview = (p.abstract[:180] + "...") if p.abstract and len(p.abstract) > 180 else (p.abstract or "")
|
| 63 |
|
| 64 |
+
rs = reliability_scores.get(p.paper_id)
|
| 65 |
+
if rs:
|
| 66 |
+
dominant = rs.get("dominant_signal", "DECLINING") if isinstance(rs, dict) else rs.dominant_signal
|
| 67 |
+
sig_color = SIGNAL_COLORS.get(dominant, "#6b7280")
|
| 68 |
+
signal_badge = (
|
| 69 |
+
f'<span style="background:{sig_color}22;color:{sig_color};border:1px solid {sig_color}44;'
|
| 70 |
+
f'padding:2px 7px;border-radius:12px;font-size:0.75em;white-space:nowrap;font-weight:600;margin-left:6px">'
|
| 71 |
+
f'{dominant}</span>'
|
| 72 |
+
)
|
| 73 |
+
else:
|
| 74 |
+
signal_badge = ""
|
| 75 |
+
|
| 76 |
cards.append(f"""
|
| 77 |
<div style="border:1px solid #2d3748;border-radius:8px;padding:12px 14px;
|
| 78 |
margin-bottom:8px;background:#1a1f2e;">
|
| 79 |
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px">
|
| 80 |
<span style="font-weight:600;font-size:0.92em;color:#e2e8f0;flex:1;margin-right:8px">{p.title}</span>
|
| 81 |
+
<div style="display:flex;align-items:center;flex-shrink:0">
|
| 82 |
+
<span style="background:{score_color}22;color:{score_color};border:1px solid {score_color}44;
|
| 83 |
+
padding:2px 8px;border-radius:12px;font-size:0.78em;white-space:nowrap;font-weight:600">
|
| 84 |
+
{p.hybrid_score:.3f}
|
| 85 |
+
</span>{signal_badge}
|
| 86 |
+
</div>
|
| 87 |
</div>
|
| 88 |
<div style="color:#94a3b8;font-size:0.8em;margin-bottom:6px">
|
| 89 |
{authors} · {p.year} · {p.citation_count:,} citations · <span style="color:#64748b">{p.source}</span>
|
|
|
|
| 226 |
verdict_html = _verdict_badge_html(verdict, critic_notes, retry_count,
|
| 227 |
papers_used, latency, decay_config, rewritten)
|
| 228 |
claims_html = _claims_html(result.get("claim_confidences") or [])
|
| 229 |
+
papers_html = _paper_cards_html(result.get("retrieved_papers") or [], result.get("paper_reliability_scores") or {})
|
| 230 |
session_ctx = load_session(session_id)
|
| 231 |
session_html = _session_html(session_ctx, session_id)
|
| 232 |
export_md = result.get("export_md", "")
|