ArchitSharma commited on
Commit
3f1f29b
·
1 Parent(s): 8cfc5e2

Polish RAGForge UI and fix evaluation matching

Browse files
CHANGELOG.md CHANGED
@@ -1,84 +1,59 @@
1
  # Changelog
2
 
3
- RAGForge used iterative numbered releases during development. v2.0.0 is the final planned portfolio release.
4
 
5
- ## v2.0.0 - Final
 
 
 
 
 
6
 
7
- - polished product UI and final README/docs hierarchy
8
- - aligned the multi-hop Hard Mode case with the recommended Auto + Balanced semantic route
9
- - added final release verification tooling and CI consistency checks
10
- - fixed duplicate runtime-header presentation and cleaned architecture documentation
11
- - preserved the v1.9 adaptive retrieval, context-budget, compression, scale-stress and release-readiness architecture
12
 
13
- ## v1.9 - Scale and release readiness
 
 
14
 
15
- - adaptive retrieval depth by corpus size
16
- - dynamic 2-5 chunk focused context budgets
17
- - focused sentence compression
18
- - 1x/5x/20x deterministic distractor-scale stress harness
19
- - prompt/context token economics
20
- - workspace diagnostics and release-readiness checklist
21
 
22
- ## v1.8 - Context budget optimization
 
 
23
 
24
- - focused post-retrieval pruning
25
- - context/prompt telemetry
26
- - grounded-absence hardening
27
- - zero-Gemini context-budget ablation
28
 
29
- ## v1.7 - Evaluation correctness and adaptive efficiency
30
 
31
- - Markdown-safe source cards and graphical node latency waterfall
32
- - grounded absence and improved citation scoring/repair
33
- - evaluation run provenance
34
- - measured small-corpus reranker skip policy
35
 
36
- ## v1.6 - Hard Mode and analytical synthesis
 
37
 
38
- - `insight_synthesis` / `analytical` path
39
- - `[T#]` table citations
40
- - harder robustness benchmark
41
- - chunk-level reranker labels
42
- - optional profile benchmark and node latency summaries
43
 
44
- ## v1.5 - Evaluation history and adaptive retrieval
45
 
46
- - saved Quick/Standard/Deep reports
47
- - incremental Deep evaluation
48
- - typed Text2SQL scoring
49
- - zero-call citation repair
50
- - adaptive reranking
51
 
52
- ## v1.4 - Evaluation and API observability
53
 
54
- - proper evaluation score cards and quality gates
55
- - fixed retrieval AP metrics
56
- - API/architecture runtime inspection
57
- - cache-bypassed evaluation timing
58
 
59
- ## v1.3 - Component evaluation
60
 
61
- - retrieval, citation, planner, SQL and latency metrics
62
- - reranker ablation
63
- - optional Deep judge
64
 
65
- ## v1.2 - UX/lifecycle hardening
66
 
67
- - browser session continuity
68
- - lazy demo recovery
69
- - explicit abstention and workspace preflight
70
- - indexing/query progress improvements
71
 
72
- ## v1.1 - Semantic routing redesign
73
 
74
- - schema-constrained semantic query analysis
75
- - source-profile index
76
- - global/hierarchical retrieval
77
- - correction before conditional web fallback
78
 
79
- ## v1.0 - Initial production-style RAGForge
80
 
81
- - FastAPI + Gradio + LangGraph
82
- - hybrid dense/BM25 retrieval
83
- - reranking, HyDE, multi-query, CRAG, Self-RAG
84
- - Text2SQL, Ask-the-Web, citations, guardrails and Docker/Hugging Face deployment
 
1
  # Changelog
2
 
3
+ ## v2.0.1
4
 
5
+ - simplified the product UI and removed benchmark/grade marketing from the landing and evaluation screens
6
+ - rewrote the README and evaluation notes around system behavior, methodology, and limitations
7
+ - fixed numeric answer-key matching so labels such as `5 min` cannot match `15 minutes`
8
+ - changed Quick evaluation so skipped scale/context checks are reported as not run rather than as release readiness
9
+ - allowed semantic or hierarchical retrieval for the source-localization planner case, since both are valid strategies
10
+ - renamed version-specific context-ablation labels to neutral experimental names
11
 
12
+ ## v2.0.0
 
 
 
 
13
 
14
+ - consolidated the UI, documentation, architecture diagram, and release tooling
15
+ - aligned the multi-source Hard Mode comparison with the recommended Auto + Balanced path
16
+ - added release consistency checks and CI verification
17
 
18
+ ## v1.9
 
 
 
 
 
19
 
20
+ - added scale-aware retrieval depth and adaptive focused context budgets
21
+ - added focused sentence selection and synthetic distractor scale stress
22
+ - added prompt/context telemetry and workspace diagnostics
23
 
24
+ ## v1.8
 
 
 
25
 
26
+ - added focused context pruning and context-budget ablation
27
 
28
+ ## v1.7
 
 
 
29
 
30
+ - improved evaluator correctness, citation handling, source rendering, and runtime provenance
31
+ - adopted the measured small-corpus reranker skip policy
32
 
33
+ ## v1.6
 
 
 
 
34
 
35
+ - added analytical corpus synthesis, structured table evidence, harder robustness cases, and chunk-level reranker evaluation
36
 
37
+ ## v1.5
 
 
 
 
38
 
39
+ - added reusable Quick/Standard/Deep reports, incremental Deep evaluation, typed Text2SQL checks, and adaptive reranking
40
 
41
+ ## v1.4
 
 
 
42
 
43
+ - expanded evaluation metrics, cache-bypassed timing, Architecture/API inspection, and quota-aware evaluation
44
 
45
+ ## v1.3
 
 
46
 
47
+ - introduced component-level evaluation and reranker ablation
48
 
49
+ ## v1.2
 
 
 
50
 
51
+ - hardened session lifecycle, lazy corpus recovery, progress UI, and explicit abstention
52
 
53
+ ## v1.1
 
 
 
54
 
55
+ - introduced semantic query planning, source profiles, global/hierarchical retrieval, and correction-before-web policy
56
 
57
+ ## v1.0
58
+
59
+ - initial FastAPI + Gradio + LangGraph application with hybrid retrieval, Text2SQL, web search, citations, and Docker deployment
 
README.md CHANGED
@@ -10,178 +10,176 @@ pinned: false
10
 
11
  # RAGForge
12
 
13
- **RAGForge v2.0.0 - final portfolio release.**
14
 
15
- RAGForge is an evidence-first RAG system for **documents, structured tables, and the web**. It combines hybrid retrieval, semantic query planning, source-balanced and hierarchical search, analytical synthesis, read-only Text2SQL, corrective retrieval, conditional web research, explicit citations, adaptive context budgeting, and a built-in evaluation harness in one Dockerized FastAPI + Gradio application.
16
 
17
- The project is designed as a production-style portfolio system rather than a claim of production SaaS readiness. It makes retrieval decisions, evidence quality, citation provenance, evaluation tradeoffs, and runtime limitations visible instead of hiding them behind a chat box.
18
 
19
- ## Verified pre-final benchmark
20
 
21
- The final release preserves the runtime architecture measured in the user-verified **v1.9 Standard** run. v2.0 changes benchmark alignment, UI, documentation, and release tooling; it does not claim a new provider-backed benchmark result before deployment.
 
 
 
 
22
 
23
- | Metric | Verified result |
24
- |---|---:|
25
- | Deterministic quality | **0.994 / Grade A** |
26
- | Answer accuracy | **100%** |
27
- | Source Recall@5 | **100%** |
28
- | Citation validity / coverage | **100% / 100%** |
29
- | Planner route / task / strategy | **100% / 100% / 100%** |
30
- | Text2SQL pass | **100%** |
31
- | Corpus overview pass | **100%** |
32
- | Adaptive context Recall@5 | **100%** |
33
- | Median adaptive context reduction | **41%** |
34
- | Evidence compression signal retention | **100%** |
35
- | Additional evidence-token reduction | **58%** |
36
- | Synthetic scale stress | **1,724 chunks / 24 sources** |
37
- | Scale-stress Recall@5 | **100%** |
38
- | Release readiness | **READY** |
39
- | Pipeline latency p50 / p95 | **1.79 s / 5.07 s** |
40
 
41
- See [`docs/FINAL_RESULTS.md`](docs/FINAL_RESULTS.md) for the full measured baseline, methodology caveats, and optimization findings.
42
 
43
- ## Why RAGForge
44
 
45
- Most RAG demos collapse several different questions into one similarity search. RAGForge separates them:
 
 
 
 
 
 
 
46
 
47
- 1. **What does the user mean?** A schema-constrained semantic planner identifies knowledge scope, task type, freshness, and retrieval strategy.
48
- 2. **Where should the evidence come from?** Session-local documents, structured tables, the external web, or a mixed path.
49
- 3. **How much evidence should reach generation?** Retrieval depth and generation context are adapted to corpus scale, retrieval confidence, source ambiguity, and task breadth.
50
- 4. **Is the answer actually grounded?** Citations, evidence grading, grounded absence handling, optional Self-RAG verification, and transparent traces make the decision inspectable.
51
- 5. **Did an optimization help?** The bundled benchmark separately measures retrieval, routing, SQL, citations, robustness, latency, context economics, and scale stress.
52
 
53
- That distinction is why a query such as **“What is the current corpus about?”** stays local, while **“Compare our NIST document with the latest guidance online”** can legitimately become a hybrid document + web task.
54
 
55
- ## Core capabilities
56
 
57
- ### Semantic planning and routing
58
 
59
- - `corpus`, `external`, `mixed`, and `structured_data` knowledge scopes
60
- - task types for fact lookup, overview, comparison, cross-document synthesis, aggregation, insight synthesis, and follow-up
61
- - retrieval strategies: `semantic`, `global`, `hierarchical`, `analytical`, `table`, and `none`
62
- - independent document and web queries
63
- - explicit `required | useful | irrelevant` web relevance
64
- - conversation-aware query rewriting
65
- - manual Auto / Documents / Web / Hybrid / Data(SQL) overrides
66
 
67
- ### Retrieval
 
 
 
 
 
68
 
69
- - local FastEmbed dense embeddings with embedded Qdrant
70
- - BM25 lexical retrieval
71
- - reciprocal-rank fusion
72
- - source-profile index for source-first hierarchical retrieval
73
- - source-balanced global retrieval for corpus overviews
74
- - analytical document + DuckDB table evidence
75
- - adaptive cross-encoder reranker policy
76
- - corpus-scale adaptive retrieval depth
77
- - dynamic 2-5 chunk focused context budgets
78
- - deterministic focused sentence compression after retrieval
79
 
80
- ### Grounded generation
81
 
82
- - document citations as `[D#]`
83
- - structured table citations as `[T#]`
84
- - web citations as `[W#]`
85
- - deterministic citation normalization and conservative repair
86
- - grounded-absence handling for genuinely missing information
87
- - optional CRAG correction loop
88
- - optional bounded Self-RAG-style verification and revision
89
- - confidence and complete pipeline trace
90
 
91
- ### Structured data
 
 
 
 
 
92
 
93
- CSV/XLS/XLSX content is both indexed as text and loaded into an isolated in-memory DuckDB database. The Text2SQL path:
94
 
95
- - generates a single read-only query
96
- - validates `SELECT` / CTE-only SQL
97
- - blocks mutation/admin statements
98
- - enforces row limits
99
- - returns typed scalar/table results
100
- - is independently evaluated against expected computed values
101
 
102
- ### Ask-the-Web
103
 
104
- - DuckDuckGo keyless fallback
105
- - optional Tavily
106
- - optional Gemini Search grounding path
107
- - independently planned web queries
108
- - parallel fetch + main-text extraction
109
- - SSRF-oriented URL checks
110
- - local reranking and cited synthesis
111
- - web permission is separate from web relevance, so a weak local retrieval does not automatically leak a private corpus query to the internet
112
 
113
- ### File support
114
 
115
- PDF, TXT, Markdown, DOCX, PPTX, CSV, XLS/XLSX, JSON, HTML, XML/YAML, code/text formats, common images, and ZIP archives.
116
 
117
- ZIP ingestion includes path-traversal, file-count, size, and extension controls. Optional Gemini OCR can transcribe scanned PDFs/images.
118
 
119
  ## Architecture
120
 
121
  ```mermaid
122
  flowchart TD
123
- U[User / API] --> G[Input and upload guardrails]
124
- G --> P{Semantic QueryPlan}
125
- P -->|structured| SQL[Read-only DuckDB Text2SQL]
126
- P -->|external| W[Ask-the-Web]
127
- P -->|corpus or mixed| R{Retrieval strategy}
128
 
129
- R -->|semantic| C[Chunk index]
 
 
 
 
130
  R -->|global| GB[Source-balanced retrieval]
131
- R -->|hierarchical| SP[Source-profile index]
132
- R -->|analytical| AN[Documents + deterministic table evidence]
133
  SP --> C
134
 
135
- C --> D[Dense FastEmbed / Qdrant]
136
  C --> B[BM25]
137
  D --> F[RRF]
138
  B --> F
139
- F --> RR{Adaptive reranker policy}
140
  RR --> RD[Scale-aware retrieval depth]
141
- RD --> CB{Adaptive context budget}
142
- CB --> EC[Focused evidence compression]
143
 
144
- GB --> EG{Evidence grade}
145
  AN --> EG
146
  EC --> EG
147
- EG -->|sufficient| A[Grounded Gemini generation]
148
- EG -->|weak first attempt| CR[Correct retrieval]
149
  CR --> R
150
- EG -->|external relevant| W
151
- EG -->|unsupported local| AB[Abstain]
152
  W --> A
153
 
154
- A --> V{Optional Self-RAG audit}
155
- V -->|revise once| RV[Bounded revision]
156
- RV --> V
157
- V --> O[Cited answer + sources + trace]
158
  SQL --> O
159
  AB --> O
160
-
161
- O -. cache-bypassed benchmark .-> EV[Evaluation harness]
162
- EV --> Q[Quality + robustness]
163
- EV --> EFF[Context / compression / latency]
164
- EV --> ST[1x / 5x / 20x scale stress]
165
- Q --> READY[Release readiness]
166
- EFF --> READY
167
- ST --> READY
168
  ```
169
 
170
- The detailed Mermaid source lives in [`docs/architecture.mmd`](docs/architecture.mmd).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- ## Quick start
 
 
 
 
173
 
174
  ### Hugging Face Spaces
175
 
176
- 1. Create a **Docker** Space.
177
- 2. Copy this repository into the Space root.
178
- 3. Add `GEMINI_API_KEY` in **Settings -> Secrets**.
179
  4. Optionally add `TAVILY_API_KEY` and `APP_API_TOKEN`.
180
  5. Push the repository.
181
- 6. Open the Space with **Use bundled demo files** enabled.
182
- 7. Use the recommended **Auto + Balanced** profile.
183
 
184
- The bundled demo corpus can be indexed explicitly or initialized lazily on the first non-Web question.
185
 
186
  ### Local Docker
187
 
@@ -195,7 +193,7 @@ docker run --rm -p 7860:7860 --env-file .env ragforge
195
 
196
  Open `http://localhost:7860`.
197
 
198
- ### Python development
199
 
200
  ```bash
201
  python -m pip install -r requirements-dev.txt
@@ -203,77 +201,45 @@ make verify
203
  make run
204
  ```
205
 
206
- `make verify` runs linting, tests, Python compilation, and the final release-consistency check.
207
 
208
- ## Recommended demo flow
209
-
210
- Start with the bundled corpus and **Auto + Balanced**:
211
 
212
  ```text
213
  What is the Sev-1 acknowledgement target?
214
  ```
215
 
216
- This demonstrates focused retrieval, adaptive context budgeting, sentence compression, a citation, and the latency waterfall.
217
 
218
  ```text
219
  What is the collection about?
220
  ```
221
 
222
- This demonstrates source-balanced global retrieval and document + table citations.
223
 
224
  ```text
225
  What exactly does this collection reveal? Identify important trends and caveats.
226
  ```
227
 
228
- This demonstrates `insight_synthesis -> analytical` retrieval with `[D#]` and `[T#]` evidence.
229
 
230
  ```text
231
  Which support tier has the shortest first-response SLA?
232
  ```
233
 
234
- This demonstrates semantic routing into read-only Text2SQL.
235
-
236
- A short portfolio/demo script is available in [`docs/PORTFOLIO_GUIDE.md`](docs/PORTFOLIO_GUIDE.md).
237
-
238
- ## Evaluation
239
-
240
- RAGForge treats evaluation as part of the application rather than a notebook afterthought.
241
 
242
- ### Quick
243
-
244
- Small smoke test for deployment sanity.
245
-
246
- ### Standard
247
-
248
- Full deterministic suite covering:
249
-
250
- - focused QA
251
- - route/task/strategy planning
252
- - web-use precision and recall
253
- - corpus overview
254
- - typed Text2SQL
255
- - abstention
256
- - Hard Mode robustness
257
- - retrieval + reranker ablation
258
- - adaptive context-budget ablation
259
- - evidence-compression ablation
260
- - node latency
261
- - synthetic 1x / 5x / 20x scale stress
262
- - release-readiness checklist
263
-
264
- ### Deep
265
-
266
- Reuses a compatible saved Standard baseline when possible and adds a small calibrated Gemini judge sample. This keeps free-tier request pressure bounded.
267
-
268
- Evaluation uses rolling request pacing and honors provider 429 retry guidance. Saved Quick/Standard/Deep runs can be compared without rerunning them.
269
 
270
- See [`docs/EVALUATION.md`](docs/EVALUATION.md).
271
 
272
  ## REST API
273
 
274
  FastAPI exposes Swagger at `/docs` and OpenAPI at `/openapi.json`.
275
 
276
- Key endpoints:
277
 
278
  ```text
279
  GET /api/health
@@ -290,7 +256,7 @@ GET /api/v1/evaluation/history/{session_id}
290
  GET /metrics
291
  ```
292
 
293
- Example:
294
 
295
  ```bash
296
  curl -X POST "$BASE_URL/api/v1/query" \
@@ -302,69 +268,56 @@ curl -X POST "$BASE_URL/api/v1/query" \
302
  }'
303
  ```
304
 
305
- See [`docs/ARCHITECTURE_API.md`](docs/ARCHITECTURE_API.md).
306
 
307
- ## Security model and honest limitations
308
 
309
- RAGForge demonstrates defense-in-depth controls for archive ingestion, retrieved prompt injection, SQL safety, SSRF-oriented web fetching, session isolation, secrets, and rate limiting. It is **not** a compliance-certified multi-tenant service.
310
 
311
  Important limitations:
312
 
313
- - workspace/vector/SQL state is process-local and ephemeral on a standard Space
314
- - browser persistence stores only an opaque workspace ID and does not make server data durable
315
- - large production deployments need durable tenant state, storage-level tenant filters, worker isolation, external vector/database infrastructure, and stronger egress controls
316
- - prompt-injection detection remains heuristic
317
- - token counts in traces are estimates for engineering comparison, not provider billing records
318
- - the 1,724-chunk scale result is a deterministic synthetic distractor stress test, not a substitute for a real enterprise corpus benchmark
319
- - free-tier Gemini quotas and data-use terms depend on the active provider project/tier
320
 
321
  See [`SECURITY.md`](SECURITY.md).
322
 
323
- ## Repository map
324
 
325
  ```text
326
  src/ragforge/
327
- pipeline.py LangGraph RAG orchestration
328
- llm.py Gemini planning/generation/judging
329
  retrieval.py dense + BM25 + RRF + reranking
330
  context_budget.py adaptive retrieval/context policy
331
- evidence_compression.py focused local sentence compression
332
- workspace.py session corpus, indexes, SQL, eval history
333
- evaluation.py benchmark + diagnostics + readiness
334
- stress_eval.py zero-Gemini scale-stress harness
335
  sql_agent.py read-only Text2SQL
336
- web_search.py external research path
337
- security.py ingestion/query/web guardrails
338
  ui.py Gradio application
339
- api.py FastAPI surface
340
 
341
  evals/
342
- demo_benchmark.json transparent benchmark labels
343
 
344
  docs/
345
- FINAL_RESULTS.md verified pre-final benchmark
346
- PORTFOLIO_GUIDE.md demo and interview walkthrough
347
- EVALUATION.md evaluation methodology
348
- ARCHITECTURE_API.md architecture/API reference
349
- FEATURE_MATRIX.md feature inventory
350
- QUERY_PLANNING.md planner semantics
351
- UX_LIFECYCLE.md session/UI lifecycle
352
- RESUME_BULLETS.md concise project bullets
353
  ```
354
 
355
- ## Final project status
356
-
357
- **v2.0.0 is the final planned feature release.** Future work should be driven by a genuinely larger real-world corpus or deployment requirement, not by adding more RAG acronyms.
358
-
359
- The measured engineering conclusions are already useful:
360
-
361
- - source recall is saturated on the demo benchmark, so context economics mattered more than another retriever
362
- - the cross-encoder reranker was hundreds of times slower on the small demo with no measured source/chunk ranking gain, so runtime policy skips it there
363
- - adaptive context budgeting and sentence compression reduced generation input while preserving benchmark recall/signals
364
- - generation dominates latency, making prompt/context reduction more valuable than micro-optimizing millisecond retrieval
365
- - evaluation exposed multiple evaluator/UI bugs during development, demonstrating why the benchmark itself needed to be audited as carefully as the RAG pipeline
366
-
367
- For project history, see [`CHANGELOG.md`](CHANGELOG.md).
368
 
369
  ## License
370
 
 
10
 
11
  # RAGForge
12
 
13
+ RAGForge is a retrieval application for asking questions across documents, structured tables, and web sources. It combines local hybrid search with query routing, read-only Text2SQL, optional web research, source-linked answers, and an evaluation suite that exposes how the system reached a result.
14
 
15
+ The project is designed to make RAG behavior inspectable. The UI shows the selected route, retrieved evidence, context-budget decisions, model calls, and per-node latency instead of presenting only a chat response.
16
 
17
+ ## What it does
18
 
19
+ A question can be routed to one of several paths:
20
 
21
+ - **Documents** - dense + BM25 retrieval with reciprocal-rank fusion
22
+ - **Data (SQL)** - read-only SQL over uploaded CSV/XLS/XLSX tables in DuckDB
23
+ - **Web** - external search when the question genuinely requires current or external information
24
+ - **Hybrid** - local evidence plus web evidence when both are needed
25
+ - **Overview / analysis** - source-balanced retrieval for corpus summaries and analytical synthesis
26
 
27
+ The default `Auto` route uses a structured planner to decide the knowledge scope, task type, retrieval strategy, and whether web access is relevant.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ ## Retrieval and context handling
30
 
31
+ The document path uses:
32
 
33
+ - FastEmbed dense embeddings with embedded Qdrant
34
+ - BM25 lexical retrieval
35
+ - reciprocal-rank fusion
36
+ - source-profile retrieval for source-first and corpus-wide queries
37
+ - an adaptive cross-encoder policy rather than always paying reranking cost
38
+ - retrieval depth that grows with corpus size
39
+ - a focused context budget that retains more evidence when retrieval is ambiguous
40
+ - local sentence selection for focused lookups before the evidence is sent to the generation model
41
 
42
+ Broad overview, comparison, and insight queries keep wider context. Focused lookups can use a smaller evidence set when the retrieval signal is clear.
 
 
 
 
43
 
44
+ Source cards always keep the original retrieved text. Context compression only changes the copy passed to generation.
45
 
46
+ ## Grounding and citations
47
 
48
+ Generated answers refer back to the source cards returned with the response. Document, structured-table, and web evidence use separate source identifiers so the UI can distinguish where a claim came from.
49
 
50
+ The pipeline also includes:
 
 
 
 
 
 
51
 
52
+ - citation normalization and conservative local repair
53
+ - evidence sufficiency checks
54
+ - calibrated handling of information that is absent from the corpus
55
+ - optional corrective retrieval
56
+ - optional bounded faithfulness revision
57
+ - a full execution trace for each answer
58
 
59
+ ## Structured data
 
 
 
 
 
 
 
 
 
60
 
61
+ CSV, XLS, and XLSX files are indexed for retrieval and also loaded into an isolated in-memory DuckDB database.
62
 
63
+ The Text2SQL path:
 
 
 
 
 
 
 
64
 
65
+ 1. generates a single query,
66
+ 2. validates that it is read-only,
67
+ 3. blocks mutation and administrative statements,
68
+ 4. applies row limits,
69
+ 5. executes the query in DuckDB, and
70
+ 6. returns the resulting scalar or table.
71
 
72
+ The benchmark evaluates the computed values directly instead of relying on how a table happens to be rendered as text.
73
 
74
+ ## Web research
 
 
 
 
 
75
 
76
+ RAGForge can use DuckDuckGo, Tavily, or Gemini Search. Web search is planned separately from local retrieval, and allowing web fallback is not the same thing as deciding that the web is relevant.
77
 
78
+ This matters for private/local questions: weak document retrieval by itself does not cause the query to be sent to the internet.
 
 
 
 
 
 
 
79
 
80
+ ## Supported files
81
 
82
+ PDF, TXT, Markdown, DOCX, PPTX, CSV, XLS/XLSX, JSON, HTML, XML/YAML, common code/text formats, images, and ZIP archives are supported.
83
 
84
+ ZIP ingestion includes path-traversal, file-count, size, and extension checks. Optional Gemini OCR can be enabled for scanned PDFs and images.
85
 
86
  ## Architecture
87
 
88
  ```mermaid
89
  flowchart TD
90
+ U[User or API] --> G[Input and upload checks]
91
+ G --> P{Query planner}
 
 
 
92
 
93
+ P -->|structured data| SQL[Read-only DuckDB Text2SQL]
94
+ P -->|external| W[Web search]
95
+ P -->|documents or mixed| R{Retrieval strategy}
96
+
97
+ R -->|semantic| C[Chunk retrieval]
98
  R -->|global| GB[Source-balanced retrieval]
99
+ R -->|hierarchical| SP[Source-profile retrieval]
100
+ R -->|analytical| AN[Documents + table evidence]
101
  SP --> C
102
 
103
+ C --> D[Dense search]
104
  C --> B[BM25]
105
  D --> F[RRF]
106
  B --> F
107
+ F --> RR[Adaptive reranker policy]
108
  RR --> RD[Scale-aware retrieval depth]
109
+ RD --> CB[Adaptive context budget]
110
+ CB --> EC[Focused sentence selection]
111
 
112
+ GB --> EG{Evidence sufficient?}
113
  AN --> EG
114
  EC --> EG
115
+ EG -->|yes| A[Grounded generation]
116
+ EG -->|retry local| CR[Correct retrieval]
117
  CR --> R
118
+ EG -->|web relevant| W
119
+ EG -->|unsupported| AB[Abstain]
120
  W --> A
121
 
122
+ A --> V[Optional faithfulness check]
123
+ V --> O[Answer + sources + trace]
 
 
124
  SQL --> O
125
  AB --> O
 
 
 
 
 
 
 
 
126
  ```
127
 
128
+ The full architecture diagram is in [`docs/architecture.mmd`](docs/architecture.mmd).
129
+
130
+ ## Evaluation
131
+
132
+ The Evaluation tab runs a transparent benchmark stored in [`evals/demo_benchmark.json`](evals/demo_benchmark.json). The goal is to diagnose specific parts of the system rather than to claim a general RAG accuracy score.
133
+
134
+ ### Quick
135
+
136
+ A small deployment smoke test covering a subset of QA, routing, overview, SQL, robustness, and abstention cases. It intentionally does **not** run the larger context, compression, or scale-stress ablations.
137
+
138
+ ### Standard
139
+
140
+ Runs the full deterministic suite:
141
+
142
+ - focused QA
143
+ - route / task / retrieval-strategy planning
144
+ - corpus overview
145
+ - Text2SQL
146
+ - abstention
147
+ - robustness cases
148
+ - retrieval and reranker ablation
149
+ - context-budget comparison
150
+ - sentence-compression comparison
151
+ - node latency
152
+ - synthetic distractor scale stress
153
+
154
+ ### Deep
155
+
156
+ Reuses a compatible Standard result when possible and adds a small LLM-judge sample for answer faithfulness, relevance, completeness, and citation support.
157
+
158
+ ### Interpreting the benchmark
159
+
160
+ The bundled benchmark is intentionally small: five source files, one structured table, and a 90-chunk base corpus. It is useful for regression testing and for comparing design choices, not for making claims about general enterprise performance.
161
+
162
+ The scale-stress harness expands the retrieval index with deterministic distractor copies so retrieval behavior can be checked at a larger index size without spending additional model calls. That is still a synthetic stress test, not a substitute for a real large-corpus evaluation.
163
+
164
+ During the final v2.0 acceptance run, the evaluation suite exposed a useful benchmark bug: one generated Sev-1 answer said `15 minutes` even though the source says `5 minutes`, and the old answer matcher incorrectly accepted it because `5 min` appeared inside `15 minutes`. v2.0.1 fixes answer matching at token/phrase boundaries and changes the benchmark version so older saved results are not silently reused.
165
 
166
+ Context-budget and compression tables should likewise be read as engineering comparisons: they show how much evidence reached generation and whether labeled answer evidence was retained on this benchmark. They are not general claims that a fixed percentage of tokens can always be removed safely.
167
+
168
+ See [`docs/FINAL_RESULTS.md`](docs/FINAL_RESULTS.md) for the detailed validation notes and caveats.
169
+
170
+ ## Running the project
171
 
172
  ### Hugging Face Spaces
173
 
174
+ 1. Create a Docker Space.
175
+ 2. Copy the repository into the Space root.
176
+ 3. Add `GEMINI_API_KEY` under **Settings -> Secrets**.
177
  4. Optionally add `TAVILY_API_KEY` and `APP_API_TOKEN`.
178
  5. Push the repository.
179
+ 6. Open the Space and leave **Use bundled demo files** enabled for the demo corpus.
180
+ 7. Start with `Auto` + `Balanced`.
181
 
182
+ The demo corpus can be indexed explicitly or initialized lazily on the first local question.
183
 
184
  ### Local Docker
185
 
 
193
 
194
  Open `http://localhost:7860`.
195
 
196
+ ### Development
197
 
198
  ```bash
199
  python -m pip install -r requirements-dev.txt
 
201
  make run
202
  ```
203
 
204
+ `make verify` runs linting, tests, Python compilation, and release-consistency checks.
205
 
206
+ ## Suggested demo questions
 
 
207
 
208
  ```text
209
  What is the Sev-1 acknowledgement target?
210
  ```
211
 
212
+ Focused document retrieval with a short cited answer.
213
 
214
  ```text
215
  What is the collection about?
216
  ```
217
 
218
+ Corpus-wide source-balanced retrieval, including the structured support table.
219
 
220
  ```text
221
  What exactly does this collection reveal? Identify important trends and caveats.
222
  ```
223
 
224
+ Analytical synthesis across documents and structured data.
225
 
226
  ```text
227
  Which support tier has the shortest first-response SLA?
228
  ```
229
 
230
+ Routes to the read-only Text2SQL path.
 
 
 
 
 
 
231
 
232
+ ```text
233
+ Compare our NIST document with the latest NIST AI risk guidance online.
234
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
+ Demonstrates the mixed local + web route.
237
 
238
  ## REST API
239
 
240
  FastAPI exposes Swagger at `/docs` and OpenAPI at `/openapi.json`.
241
 
242
+ Common endpoints:
243
 
244
  ```text
245
  GET /api/health
 
256
  GET /metrics
257
  ```
258
 
259
+ Example query:
260
 
261
  ```bash
262
  curl -X POST "$BASE_URL/api/v1/query" \
 
268
  }'
269
  ```
270
 
271
+ See [`docs/ARCHITECTURE_API.md`](docs/ARCHITECTURE_API.md) for the complete API notes.
272
 
273
+ ## Security and limitations
274
 
275
+ RAGForge includes controls for archive ingestion, retrieved prompt injection, SQL safety, session isolation, secrets, rate limiting, and web-fetch URL checks. It is not a compliance-certified multi-tenant service.
276
 
277
  Important limitations:
278
 
279
+ - workspace, vector, and SQL state are process-local and ephemeral on a standard Hugging Face Space
280
+ - browser persistence stores an opaque workspace ID, not durable server state
281
+ - prompt-injection detection is heuristic
282
+ - trace token counts are engineering estimates, not provider billing records
283
+ - the bundled benchmark is a regression suite over a small demo corpus
284
+ - the larger scale result uses synthetic distractors rather than a real enterprise dataset
285
+ - production multi-tenant deployment would require durable storage, stronger tenant isolation, external database/vector infrastructure, and stricter network controls
286
 
287
  See [`SECURITY.md`](SECURITY.md).
288
 
289
+ ## Repository layout
290
 
291
  ```text
292
  src/ragforge/
293
+ pipeline.py pipeline orchestration
294
+ llm.py Gemini planning and generation
295
  retrieval.py dense + BM25 + RRF + reranking
296
  context_budget.py adaptive retrieval/context policy
297
+ evidence_compression.py focused sentence selection
298
+ workspace.py sessions, indexes, SQL, evaluation history
299
+ evaluation.py benchmark and diagnostics
300
+ stress_eval.py local synthetic scale-stress harness
301
  sql_agent.py read-only Text2SQL
302
+ web_search.py external search path
303
+ security.py ingestion/query/web checks
304
  ui.py Gradio application
305
+ api.py FastAPI API
306
 
307
  evals/
308
+ demo_benchmark.json
309
 
310
  docs/
311
+ FINAL_RESULTS.md
312
+ PORTFOLIO_GUIDE.md
313
+ EVALUATION.md
314
+ ARCHITECTURE_API.md
315
+ FEATURE_MATRIX.md
316
+ QUERY_PLANNING.md
317
+ UX_LIFECYCLE.md
 
318
  ```
319
 
320
+ Development history is kept in [`CHANGELOG.md`](CHANGELOG.md); it is intentionally separate from the main product documentation.
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
  ## License
323
 
docs/EVALUATION.md CHANGED
@@ -1,363 +1,108 @@
1
- # RAGForge evaluation - v2.0 final
2
 
3
- ## Final benchmark alignment
4
-
5
- v2.0 preserves the v1.9 deterministic evaluation architecture and bumps the benchmark to `2.0` because the multi-hop comparison Hard Mode case is now executed through the recommended `Auto + Balanced` semantic route. The case must resolve to `documents -> comparison -> hierarchical` and still satisfy the answer/retrieval labels. This prevents Fast mode's intentionally cheap fallback from being treated as the reference behavior for a multi-source reasoning test.
6
-
7
- The last provider-backed user-verified Standard baseline is documented in [`FINAL_RESULTS.md`](FINAL_RESULTS.md).
8
-
9
- ## v1.9 scale, compression and readiness evaluation
10
-
11
- v1.9 keeps the normal Standard Gemini request count unchanged while adding local ablations around the expensive generation context. The context-budget table now compares **Full top-k**, **v1.8 fixed top-3**, and **v1.9 adaptive budget**. Adaptive selection uses corpus scale, retrieval confidence, score separation and source ambiguity, with focused budgets between two and five chunks.
12
-
13
- The **Evidence compression** table compares adaptive context with and without query-focused sentence compression. It reports additional token reduction and a deterministic answer-signal-retention rate; source records themselves are never rewritten.
14
-
15
- The **Scale stress** table expands the demo corpus with deterministic long-document distractor clones at roughly 1x, 5x and 20x scale, reuses the already-computed embedding vectors, and runs the real Qdrant/BM25/RRF path. It measures source ranking, adaptive-pruned recall, retrieval latency, budget size and index-build time without model or embedding API calls. A stress-harness failure is isolated from the primary benchmark and marks release readiness incomplete rather than crashing Standard.
16
-
17
- The **Release readiness** table is a transparent checklist, not a replacement metric. Standard/Deep require configured thresholds for answer/source quality, citations, planning/web policy, Text2SQL, Hard Mode, overview behavior, adaptive-budget recall, compression retention and largest-scale stress recall. Quick intentionally omits local ablation/stress gates instead of treating skipped work as a pass.
18
-
19
- Node-latency summaries are now pacing-corrected: deliberate evaluation sleep is subtracted proportionally from nodes that issued Gemini calls. Raw traces retain wall time, and evaluation continues to report wall latency and pacing wait separately.
20
-
21
-
22
- ## v1.8 context-budget evaluation
23
-
24
- v1.8 adds a zero-Gemini **Context budget** ablation because the v1.7 Standard run showed 100% source Recall@5/Hit@1/MRR but only about 47% source Precision@5 on focused QA. That pattern means retrieval was finding the right source reliably while still sending avoidable distractors into generation.
25
-
26
- The ablation compares:
27
-
28
- - **Full top-k** - the original six-chunk focused retrieval context.
29
- - **Focused pruning** - the same ranking with a three-chunk safety floor, enabled only for local `fact_lookup`/`followup` work using semantic or hierarchical retrieval.
30
-
31
- It reports source Precision@5, Recall@5, Hit@1, MRR, median context chunks, distinct sources, characters, estimated tokens and reduction percentage. A diagnostic only endorses the pruning policy when recall is preserved while precision improves. Broad overview, insight, comparison and cross-document tasks are excluded from runtime pruning and keep their breadth.
32
-
33
- Focused benchmark rows also expose whether pruning actually ran, the pre/post context token estimate, whether the corpus manifest was included and the estimated generation-prompt tokens. This lets latency changes be interpreted alongside model-input changes instead of provider noise alone.
34
-
35
- ### Grounded absence robustness
36
-
37
- The missing-answer Hard Mode case now accepts either a successful natural-language absence match or an explicit pipeline `grounded_absence=true` trace signal. This makes evaluator correctness independent of one exact surface form and allows the Hard Mode table to show both mechanisms.
38
-
39
-
40
-
41
- ## v1.7 evaluator correctness and provenance
42
-
43
- v1.7 changes benchmark semantics where the v1.6 report exposed evaluator artifacts rather than RAG failures. Citation coverage is Markdown-aware: short numbered/bulleted factual items count, headings and generic list introductions do not. Missing-answer cases use a grounded-absence matcher that accepts natural uncertainty language instead of a single canned phrase. Global overviews include table evidence so `[T#]` citations are part of the actual returned source set.
44
-
45
- Saved reports now carry a `run_id` and `server_boot_id`. A fresh run is reported as fresh, while loading/reusing a saved run explicitly reports zero new Gemini requests. Programmatic updates to the saved-run selector no longer trigger a second UI callback that can overwrite fresh-run status.
46
-
47
- Profile benchmarking also emits `profile_summary` rows (accuracy, citation quality, median latency, LLM-call count and reranker rate) plus an evidence-scoped profile recommendation. Evaluation diagnostics additionally surface context-efficiency when recall is high but source precision is low, and identify the dominant node-latency bottleneck.
48
-
49
- ## v1.6 hard-mode and analytical evaluation
50
-
51
- The original demo benchmark reached near-saturation on the bundled corpus. v1.6 therefore makes the benchmark harder instead of treating a 0.99 score as proof of general RAG quality. Standard/Deep now add a **Hard Mode** suite covering:
52
-
53
- - paraphrases that change surface wording without changing the answer;
54
- - distractor mentions of a second source;
55
- - missing-answer questions where the system should state that the corpus does not specify a value;
56
- - multi-hop comparisons requiring two sources;
57
- - `insight_synthesis` questions that require broad source coverage plus `[T#]` table evidence;
58
- - a filtered/min structured-data case;
59
- - local “current” wording that must not trigger the web;
60
- - prompt-injection detection.
61
-
62
- The deterministic score now assigns 5% weight to hard-mode pass rate. A pass rate below 75% caps the grade at B, and below 50% caps it at C.
63
-
64
- ### Chunk-level reranker metrics
65
-
66
- The RRF vs cross-encoder ablation still reports source-level metrics, but selected QA cases now carry auditable `chunk_must_contain` labels. Only those labeled cases contribute to `chunk_hit@1` and `chunk_mrr`, avoiding artificial inflation from unlabeled cases. This directly tests whether reranking improves the evidence chunk *within* the right file.
67
-
68
- ### Optional profile benchmark
69
-
70
- A separate opt-in evaluation compares Fast, Balanced and Agentic on a focused case plus a multi-source case. It reports answer-key accuracy, citation validity/coverage, service latency, estimated graph LLM calls, reranker use and corrective-retrieval use. The switch is off by default because profile benchmarking intentionally spends extra Gemini calls.
71
-
72
- ### Node latency summary
73
-
74
- Evaluation aggregates per-node timings from QA, overview and hard-mode graph traces into mean/p50/p95 rows. This complements the per-query inspector waterfall and helps identify whether latency is dominated by planning, retrieval, generation, grading or verification.
75
-
76
- ### Evaluation history
77
-
78
- The latest Quick/Standard/Deep result remains directly reusable. v1.6 also archives timestamped reports inside the ephemeral workspace and exposes history rows with grade, deterministic score, citation coverage, hard-mode pass rate, p50 latency, Gemini requests and deltas versus the previous run at the same depth.
79
-
80
- # Evaluation architecture - retained v1.5 foundations
81
-
82
- RAGForge evaluates retrieval, orchestration, generation, structured-data behavior and runtime efficiency separately. The benchmark is intentionally small and transparent; it is a regression suite for the bundled demo corpus, not a claim about general RAG performance.
83
-
84
- ## Why v1.5 changed the evaluator
85
-
86
- The v1.3 benchmark surfaced four evaluator/system issues during real Hugging Face runs:
87
-
88
- 1. source AP@5 could exceed 1.0 because repeated chunks from the same relevant PDF were counted as separate relevant sources;
89
- 2. running Standard followed by Deep could report near-zero pipeline latency because the response cache returned benchmark answers immediately;
90
- 3. a high weighted score could still show grade A while Text2SQL passed only half of its cases;
91
- 4. the auxiliary LLM judge could award perfect citation-support scores to answers with weak or missing citations.
92
-
93
- v1.4 fixed those issues, and v1.4.1 hardened the benchmark for free-tier API quotas with rolling RPM pacing, provider-aware 429 backoff, request telemetry, lower-call Text2SQL evaluation and sampled Deep judging.
94
-
95
- v1.5 uses the next deployed benchmark run to tighten three remaining areas:
96
-
97
- 1. **evaluation reuse** - Quick/Standard/Deep reports are cached per workspace and can be switched/compared without rerunning the benchmark;
98
- 2. **incremental Deep** - a compatible saved Standard report becomes the deterministic Deep baseline, so Deep normally adds only the representative judge calls instead of repeating the whole Standard suite;
99
- 3. **typed Text2SQL checks** - scalar DuckDB outputs are compared as booleans/numerics/text rather than through Markdown rendering, preventing correct SQL from failing a fragile string matcher.
100
-
101
- The same run also showed a repeatable reranker tradeoff: identical source-level metrics with multi-second reranker latency on the tiny demo corpus. v1.5 therefore keeps the explicit ablation while the runtime adopts an adaptive reranking policy.
102
-
103
- ## Benchmark data
104
-
105
- The labels live in `evals/demo_benchmark.json` and include:
106
-
107
- - focused QA with expected answer terms and relevant source files,
108
- - one multi-source retrieval case,
109
- - corpus-overview cases with route/task/strategy and minimum source coverage,
110
- - semantic-planner cases with expected route, task, strategy and web policy,
111
- - Text2SQL lookup/aggregation cases,
112
- - empty-workspace abstention checks.
113
 
114
  ## Evaluation levels
115
 
116
  ### Quick
117
 
118
- A deployment smoke test. It runs a small subset of focused QA, planner, overview and SQL cases plus lifecycle abstention. Quick intentionally skips the reranker ablation and Deep judge.
119
 
120
- ### Standard
 
 
 
 
 
121
 
122
- Runs the full deterministic benchmark and a retrieval ablation comparing:
123
 
124
- - Hybrid RRF
125
- - Hybrid RRF + local cross-encoder reranker
126
 
127
- This is the recommended default for portfolio demonstrations because the core metrics are deterministic and auditable.
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  ### Deep
130
 
131
- Adds Gemini evaluation of a representative labeled subset of generated answers for:
132
-
133
- - faithfulness,
134
- - answer relevance,
135
- - completeness,
136
- - citation support,
137
- - overall quality and pass/fail.
138
-
139
- If a compatible Standard report is already saved for the same workspace corpus, benchmark version and model, RAGForge reuses that deterministic baseline and issues only the sampled judge calls. From scratch Deep can still run the full benchmark. The judge remains auxiliary: its citation score is conservatively bounded by deterministic citation validity and coverage, so an answer with no citations cannot receive perfect citation-support credit.
140
-
141
- ## Saved evaluation history
142
-
143
- Each workspace stores the latest Quick, Standard and Deep report with:
144
-
145
- - evaluation depth,
146
- - model,
147
- - benchmark version,
148
- - workspace/corpus version,
149
- - UTC save time.
150
-
151
- The Evaluation tab can switch among saved runs and renders a side-by-side comparison table. Saved reports are separate from the RAG response cache. They remain available across a normal browser refresh while the current Space container/workspace lives. If the corpus changes, older reports remain viewable but are marked stale and are not reused for a fresh run.
152
-
153
- `Reuse saved evaluation` is enabled by default. Running an already-compatible depth can therefore consume zero Gemini requests. When Deep has no saved Deep result but does have a compatible Standard result, it performs an incremental judge-only upgrade.
154
-
155
- ## Cache and history policy
156
-
157
- Benchmark queries call the pipeline with:
158
-
159
- ```python
160
- use_cache=False
161
- record_history=False
162
- ```
163
-
164
- This has two effects:
165
-
166
- 1. reported pipeline latency reflects actual execution rather than a prior cached answer;
167
- 2. benchmark questions do not alter the session's conversational history and therefore do not change later planner behavior.
168
-
169
- The report exposes `cache_bypassed: true` to make this methodology explicit.
170
-
171
- ## Source-level retrieval metrics
172
-
173
- RAG retrieval can return many chunks from one file. v1.4 deduplicates source names before source-level ranking metrics are computed.
174
-
175
- ### Source Hit@1
176
-
177
- Whether the first distinct returned source is relevant.
178
-
179
- ### Source Precision@5
180
-
181
- Among the distinct sources represented in the top-five chunk window, what fraction are relevant?
182
-
183
- ### Source Recall@5
184
-
185
- What fraction of labeled relevant files appear among those sources?
186
-
187
- ### Source MRR
188
-
189
- Reciprocal rank of the first relevant distinct source.
190
-
191
- ### Source AP@5
192
-
193
- Average precision over distinct relevant files within the top-five window. Each relevant file can contribute at most once, so AP is mathematically bounded to `[0, 1]`.
194
-
195
- ### Source nDCG@5
196
-
197
- Binary source relevance with logarithmic rank discounting, normalized against the ideal source ordering.
198
-
199
- ### Duplicate-source rate@5
200
-
201
- The fraction of top-five retrieved chunks that repeat a source already present in that window. This is not inherently bad for focused questions, but it makes long-document domination visible.
202
-
203
- ## Retrieval ablation
204
-
205
- Standard and Deep retrieve the same QA set twice:
206
-
207
- 1. Hybrid RRF without the cross-encoder;
208
- 2. Hybrid RRF with the cross-encoder.
209
-
210
- The table compares source Recall@5, Hit@1, MRR, AP@5, nDCG@5, duplicate-source rate and median retrieval latency.
211
-
212
- The diagnostics layer flags a reranker when it adds a large latency multiplier without improving source MRR. That is a benchmark-specific recommendation, not a universal claim that reranking is unnecessary.
213
-
214
- ## Generation and citation metrics
215
-
216
- ### Answer-key accuracy
217
-
218
- Transparent expected-term checks for focused demo questions.
219
-
220
- ### Citation validity
221
-
222
- Fraction of citation labels such as `[D2]` or `[W3]` that correspond to sources actually returned by the pipeline.
223
-
224
- ### Citation coverage
225
-
226
- Sentence-level proxy for how many substantive factual statements include at least one citation.
227
-
228
- The generation prompt also requires every substantive factual paragraph or list item to include a valid citation when evidence is present. v1.5 adds a conservative deterministic repair pass: uncited substantive units receive a citation only when one returned evidence item has clear lexical support. Ambiguous units are left unchanged. This improves citation completeness without adding another model request.
229
-
230
- ## Planner and web-policy metrics
231
-
232
- The planner is measured independently on:
233
-
234
- - route accuracy,
235
- - task-type accuracy,
236
- - retrieval-strategy accuracy,
237
- - web-use precision,
238
- - web-use recall,
239
- - unnecessary-web rate.
240
-
241
- Task labels are intentionally separate from route. For example, a direct value lookup from a CSV can be `task_type=fact_lookup` while still using `route=sql` and `retrieval_strategy=table`. A min/max computation across rows is `aggregation`.
242
-
243
- ## Text2SQL evaluation
244
-
245
- The planner suite already evaluates whether Auto mode chooses `route=sql` and `retrieval_strategy=table`. The Text2SQL component suite therefore avoids duplicating that model call. For each SQL benchmark case it:
246
-
247
- 1. gives the DuckDB table schema and user question to Gemini;
248
- 2. validates the generated statement as a single read-only `SELECT`/CTE;
249
- 3. executes it in DuckDB;
250
- 4. checks the computed result against a transparent typed scalar when one is labeled, otherwise falls back to answer-key terms.
251
-
252
- For example, `weekend_support` is evaluated as the boolean `true`, not by searching a rendered Markdown table for a particular spelling. This keeps SQL-generation correctness separate from formatting quirks. The component test still uses one model call per case while routing is measured independently in the planner suite.
253
-
254
- ## Lifecycle abstention
255
-
256
- Explicit empty `Documents` and `Data (SQL)` requests must terminate through the `abstain` node with zero estimated LLM calls. Lifecycle failures are not treated as weak retrieval.
257
-
258
- ## Latency and efficiency
259
-
260
- The report includes:
261
-
262
- - pipeline service latency p50/p95,
263
- - planner service latency p50/p95,
264
- - per-case wall latency and deliberate pacing wait,
265
- - total evaluation wall time,
266
- - actual Gemini requests issued through RAGForge's gateway,
267
- - configured evaluation RPM target,
268
- - surfaced 429 retry count and provider-directed retry wait,
269
- - mean estimated pipeline LLM calls,
270
- - correction rate and runtime web-use rate,
271
- - Deep judge latency when applicable.
272
-
273
- Service latency subtracts deliberate quota-pacing sleep so the benchmark does not make the normal interactive pipeline look slower merely because the evaluation is being rate-limited. `llm_calls_estimate` remains graph-derived and is useful for pipeline comparisons; `gemini_requests` is the gateway-level count for the evaluation run and can be higher when a provider retry occurs.
274
 
275
- ## Quota-safe evaluation
276
 
277
- Standard and Deep default to a conservative target of **12 Gemini requests per minute**. The UI exposes this target because active rate limits vary by project/model. The process-local ledger also records recent interactive requests, so starting an evaluation immediately after manual testing does not pretend the previous minute was empty.
278
 
279
- When quota-safe pacing is enabled, requests are smoothed across the minute rather than sent in a burst. If Gemini still surfaces a 429, RAGForge parses provider retry guidance when available, waits for that interval (plus small jitter) and performs only bounded retries. Structured-output code does not immediately fall back to a second plain-JSON request on transient 429/5xx errors.
280
 
281
- Set the API/GUI target below the active RPM shown for your project. `target_rpm=0` disables deliberate pacing, but bounded provider-aware retries remain.
282
 
283
- ## Quality-gated grade
284
 
285
- The deterministic weighted score is still shown for orientation, but the letter grade is additionally subject to subsystem gates.
 
 
 
 
 
286
 
287
- Examples:
288
 
289
- - route/web-policy accuracy below 90% caps the grade at C;
290
- - Text2SQL below 75% caps the grade at B;
291
- - citation validity below 90% caps the grade at B;
292
- - citation coverage below 80% caps the grade at B;
293
- - planner task accuracy below 75% caps the grade at B.
294
 
295
- This prevents a strong retrieval score from hiding a serious production weakness elsewhere.
296
 
297
- ## Diagnostic findings
298
 
299
- Each Standard/Deep report contains structured diagnostics. Current checks can highlight:
300
 
301
- - incomplete citation coverage,
302
- - task-taxonomy misses,
303
- - failed Text2SQL cases,
304
- - reranker latency with no measurable source-ranking gain.
305
 
306
- The UI renders these findings directly above the detailed tables, while the raw JSON keeps them machine-readable for CI/API use.
307
 
308
- ## API
 
 
309
 
310
- The benchmark can be run programmatically:
311
 
312
- ```text
313
- POST /api/v1/evaluate/demo
314
- ```
315
 
316
- The request body accepts `target_rpm` (default `12`; use `0` to disable deliberate pacing) and `reuse_saved` (default `true`).
 
 
317
 
318
- Benchmark metadata is available without running the benchmark:
319
 
320
- ```text
321
- GET /api/v1/evaluation/benchmark
322
- ```
323
 
324
- Saved run inventory and reports are available through:
325
 
326
- ```text
327
- GET /api/v1/evaluation/saved/{session_id}
328
- GET /api/v1/evaluation/saved/{session_id}/{level}
329
- ```
330
 
331
- The API rejects the demo benchmark when the required bundled demo sources are not present in the workspace.
332
 
333
- ## Extending evaluation
334
 
335
- Useful next steps for a larger corpus include:
336
 
337
- - chunk-level relevance labels,
338
- - frozen web snapshots for reproducible hybrid evaluation,
339
- - contradiction and multi-hop cases,
340
- - unanswerable questions with semantic sufficiency labels,
341
- - adversarial retrieved prompt-injection documents,
342
- - repeated judge runs with variance/confidence intervals,
343
- - Fast vs Balanced vs Agentic profile comparison,
344
- - latency/cost budgets as explicit pass/fail constraints.
345
 
 
346
 
347
- ## v1.5.1 saved-report and export behavior
348
 
349
- Evaluation reports are normalized to plain JSON at the workspace boundary. This prevents
350
- Pydantic/Gradio root wrappers from leaking into the UI or saved-report API when a user switches
351
- between Quick, Standard and Deep. The Raw evaluation report is rendered as formatted JSON in a
352
- copy-ready code view.
353
 
354
- Every evaluation table can be prepared as CSV, TSV or Markdown from the currently selected saved
355
- run. The export panel supports Focused QA, Semantic planner, Corpus overview, Text2SQL, Retrieval
356
- ablation, Abstention and Compare saved runs. Export files live under the ephemeral workspace
357
- evaluation directory and disappear with the Hugging Face Space runtime.
358
 
359
- Citation evaluation accepts both one-ID-per-bracket syntax such as `[D1] [D2]` and grouped model
360
- syntax such as `[D1, D2]`. Runtime answer rendering normalizes grouped citations and removes
361
- duplicate trailing citation sets. The deterministic zero-call citation repair skips list-introduction
362
- or preamble lines ending in `:` so it does not attach a source merely because a broad introductory
363
- sentence overlaps many evidence snippets.
 
1
+ # Evaluation
2
 
3
+ RAGForge includes a small, transparent regression suite in `evals/demo_benchmark.json`. It is designed to diagnose parts of the system separately rather than reduce everything to one headline score.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  ## Evaluation levels
6
 
7
  ### Quick
8
 
9
+ Quick is a deployment smoke test. It runs a subset of:
10
 
11
+ - focused QA,
12
+ - query planning,
13
+ - corpus overview,
14
+ - Text2SQL,
15
+ - robustness cases, and
16
+ - empty-workspace abstention.
17
 
18
+ Quick intentionally skips the larger context-budget, sentence-compression, reranker, and scale-stress ablations. The UI reports those sections as **not run** rather than turning skipped work into a readiness result.
19
 
20
+ ### Standard
 
21
 
22
+ Standard runs the full deterministic suite:
23
+
24
+ - focused QA answer checks,
25
+ - source-level retrieval metrics,
26
+ - planner route / task / strategy labels,
27
+ - corpus overview behavior,
28
+ - Text2SQL computed-value checks,
29
+ - abstention,
30
+ - hard-mode robustness,
31
+ - RRF vs reranker ablation,
32
+ - full vs fixed vs adaptive context budgets,
33
+ - focused sentence-compression checks,
34
+ - node latency, and
35
+ - synthetic distractor scale stress.
36
 
37
  ### Deep
38
 
39
+ Deep reuses a compatible Standard result when possible and adds a small LLM-judge sample. The judge covers faithfulness, relevance, completeness, and citation support, but deterministic citation failures cannot be overridden by the judge.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ ## Answer matching
42
 
43
+ Focused QA uses labeled answer phrases from the benchmark file. Matching is boundary-aware: numeric or alphanumeric labels cannot pass by appearing inside a different token. For example, a `5 min` answer key does not match `15 minutes`.
44
 
45
+ This rule was added after a real v2.0 evaluation exposed exactly that false positive.
46
 
47
+ ## Retrieval metrics
48
 
49
+ Source-level retrieval deduplicates repeated chunks from the same file before computing:
50
 
51
+ - Precision@5,
52
+ - Recall@5,
53
+ - Hit@1,
54
+ - MRR,
55
+ - AP@5, and
56
+ - nDCG@5.
57
 
58
+ Selected cases also include chunk-content labels for chunk Hit@1 and chunk MRR in the reranker ablation.
59
 
60
+ A source-localization question may legitimately use either direct semantic retrieval or source-first hierarchical retrieval; benchmark cases can declare more than one accepted strategy when both are valid.
 
 
 
 
61
 
62
+ ## Citation checks
63
 
64
+ Citation validity asks whether cited source IDs were actually returned with the answer. Citation coverage checks whether substantive factual units have citations. The parser is Markdown-aware so headings and list introductions are not counted as unsupported factual claims, while factual list items are.
65
 
66
+ Structured table evidence and web evidence are validated against their returned source records in the same way as document evidence.
67
 
68
+ ## Text2SQL
 
 
 
69
 
70
+ SQL generation is evaluated independently from natural-language answer generation. The benchmark:
71
 
72
+ 1. generates validated read-only SQL,
73
+ 2. executes it in DuckDB, and
74
+ 3. compares the computed scalar result using typed boolean, numeric, or text matching.
75
 
76
+ ## Context-budget ablation
77
 
78
+ Standard compares:
 
 
79
 
80
+ - **Full top-k** - all retrieved candidates used as generation context;
81
+ - **Fixed 3-chunk budget** - a deliberately tight focused baseline; and
82
+ - **Adaptive budget** - a 2-5 chunk focused budget chosen from retrieval confidence, score separation, source ambiguity, and corpus scale.
83
 
84
+ The purpose is to check whether model input can be reduced without losing the labeled source.
85
 
86
+ ## Sentence-compression ablation
 
 
87
 
88
+ Focused evidence can be reduced to query-relevant sentences after context budgeting. The ablation checks whether labeled answer evidence remains present and records the resulting context-size change. Original source cards are never rewritten.
89
 
90
+ ## Synthetic scale stress
 
 
 
91
 
92
+ The stress harness clones long-document distractor chunks and reuses their existing vectors to exercise the real Qdrant + BM25 + RRF path at larger index sizes without extra Gemini or embedding calls.
93
 
94
+ It is a retrieval regression test, not a replacement for evaluation on a real large corpus.
95
 
96
+ ## Latency and request pacing
97
 
98
+ Evaluation bypasses the response cache. Service latency excludes deliberate quota-pacing sleep, while wall time and pacing wait are reported separately.
 
 
 
 
 
 
 
99
 
100
+ All Gemini calls in a run can share a rolling request pacer. Saved Quick, Standard, and Deep reports can be reopened without spending new requests, and Deep can reuse Standard as its deterministic baseline.
101
 
102
+ ## Acceptance checks
103
 
104
+ Standard and Deep include a threshold checklist for the major benchmark subsystems. It is an engineering convenience, not a product-quality certification and not a substitute for the underlying tables.
 
 
 
105
 
106
+ ## Benchmark scope
 
 
 
107
 
108
+ The bundled corpus is deliberately small. High percentages on this suite mean the current implementation passes these labeled regression cases. They should not be read as general accuracy estimates for arbitrary documents, industries, or enterprise-scale deployments.
 
 
 
 
docs/FEATURE_MATRIX.md CHANGED
@@ -46,7 +46,7 @@ The table below describes the final planned portfolio release. Future changes sh
46
  | Focused evidence compression | deterministic query-relevant sentence selection for generator context only | reduces prompt load without mutating source cards or spending another LLM call |
47
  | Scale stress | 1x/5x/20x long-document distractor cloning with precomputed vector reuse | tests retrieval/budget robustness around ~1000 chunks without Gemini or re-embedding cost |
48
  | Workspace diagnostics | corpus scale/capacity, index readiness, TTL/idle, estimated vector memory | makes demo operational limits explicit before they become silent failures |
49
- | Release readiness | transparent threshold checklist over quality, safety, efficiency and scale robustness | summarizes ship/no-ship evidence without hiding component metrics |
50
  | Evaluation | transparent v1.9 benchmark + Markdown-aware citation claims + grounded-absence robustness + source/chunk ranking metrics + hard-mode + adaptive-context/compression/scale ablations + planner/web policy + typed Text2SQL + pacing-corrected node latency + calibrated Deep judge | separates retrieval, orchestration, context economics, scale robustness, analysis and generation failures |
51
  | Saved evaluation history | latest Quick/Standard/Deep cache plus timestamped archived runs and deltas | allows instant run switching and lightweight within-workspace regression tracking without consuming Gemini quota again |
52
  | Evaluation report serialization | JSON-safe normalization at save/load/UI/API boundaries + formatted raw JSON view | prevents `root={...}` wrapper leakage and keeps fresh/restored reports identical |
 
46
  | Focused evidence compression | deterministic query-relevant sentence selection for generator context only | reduces prompt load without mutating source cards or spending another LLM call |
47
  | Scale stress | 1x/5x/20x long-document distractor cloning with precomputed vector reuse | tests retrieval/budget robustness around ~1000 chunks without Gemini or re-embedding cost |
48
  | Workspace diagnostics | corpus scale/capacity, index readiness, TTL/idle, estimated vector memory | makes demo operational limits explicit before they become silent failures |
49
+ | Acceptance checks | transparent threshold checklist over quality, safety, efficiency and scale robustness | summarizes ship/no-ship evidence without hiding component metrics |
50
  | Evaluation | transparent v1.9 benchmark + Markdown-aware citation claims + grounded-absence robustness + source/chunk ranking metrics + hard-mode + adaptive-context/compression/scale ablations + planner/web policy + typed Text2SQL + pacing-corrected node latency + calibrated Deep judge | separates retrieval, orchestration, context economics, scale robustness, analysis and generation failures |
51
  | Saved evaluation history | latest Quick/Standard/Deep cache plus timestamped archived runs and deltas | allows instant run switching and lightweight within-workspace regression tracking without consuming Gemini quota again |
52
  | Evaluation report serialization | JSON-safe normalization at save/load/UI/API boundaries + formatted raw JSON view | prevents `root={...}` wrapper leakage and keeps fresh/restored reports identical |
docs/FINAL_RESULTS.md CHANGED
@@ -1,88 +1,63 @@
1
- # Final measured results
2
-
3
- This page records the **user-verified v1.9 Standard benchmark** immediately before the v2.0 final productization release. v2.0 preserves the measured runtime architecture but changes benchmark alignment for one multi-hop Hard Mode case, plus UI/docs/release tooling. Therefore these numbers are presented as the verified pre-final baseline rather than silently relabeled as a v2.0 provider-backed run.
4
-
5
- ## Quality
6
-
7
- | Metric | Result |
8
- |---|---:|
9
- | Deterministic quality | 0.994 (Grade A) |
10
- | Answer accuracy | 100% |
11
- | Source Precision@5 | 48% |
12
- | Source Recall@5 | 100% |
13
- | Hit@1 | 100% |
14
- | MRR | 1.000 |
15
- | AP@5 | 1.000 |
16
- | nDCG@5 | 1.000 |
17
- | Citation validity | 100% |
18
- | Citation coverage | 100% |
19
- | Corpus overview pass | 100% |
20
- | Text2SQL pass | 100% |
21
- | Planner route/task/strategy | 100% / 100% / 100% |
22
- | Web precision/recall | 100% / 100% |
23
- | Hard Mode | 88% |
24
- | Release readiness | READY (100% critical gates) |
25
-
26
- The single v1.9 Hard Mode miss was the multi-hop Acme-vs-OrbitPay timing comparison. That case was being executed through Fast mode's cheap fallback even though the application recommendation is Auto + Balanced for semantic multi-source reasoning. v2.0 aligns that case with the recommended route and explicitly requires `comparison -> hierarchical` planning.
27
-
28
- ## Context economics
29
-
30
- | Configuration | Precision@5 | Recall@5 | Median chunks | Median context tokens | Reduction |
31
- |---|---:|---:|---:|---:|---:|
32
- | Full top-k | 47.2% | 100% | 6 | 2,101 | 0% |
33
- | v1.8 fixed top-3 | 50.0% | 100% | 3 | 977 | 54.9% |
34
- | v1.9 adaptive budget | 48.1% | 100% | 4 | 1,099 | 41.0% |
35
 
36
- The final runtime keeps adaptive budgeting rather than the globally tighter fixed top-3 policy because it retains extra safety margin for ambiguous and larger-corpus cases.
37
 
38
- ## Evidence compression
39
 
40
- | Configuration | Answer-signal retention | Median evidence tokens | Additional reduction |
 
 
41
  |---|---:|---:|---:|
42
- | Adaptive context only | 100% | 1,056 | 0% |
43
- | Adaptive + sentence compression | 100% | 503 | 58% |
 
 
 
 
 
44
 
45
- Compression modifies only the generation copy of retrieved evidence. Source cards and citation provenance retain the original retrieved chunks.
46
 
47
- ## Scale stress
48
 
49
- The zero-Gemini synthetic stress harness reused existing long-document vectors and exercised the real hybrid retriever with distractor clones.
50
 
51
- | Scale | Chunks | Sources | Recall@5 | Hit@1 | MRR | Adaptive-pruned Recall@5 | Retrieval median |
52
- |---|---:|---:|---:|---:|---:|---:|---:|
53
- | Base | 90 | 5 | 100% | 100% | 1.000 | 100% | 5.5 ms |
54
- | +4x long-doc distractors | 434 | 9 | 100% | 100% | 1.000 | 100% | 7.5 ms |
55
- | +19x long-doc distractors | 1,724 | 24 | 100% | 87.5% | 0.938 | 100% | 48.2 ms |
 
 
56
 
57
- This is evidence that Recall@5 remained robust under synthetic distractor scale. It is not a substitute for a real enterprise corpus benchmark.
58
 
59
- ## Reranker ablation
60
 
61
- The demo benchmark repeatedly found no source- or chunk-level ranking gain from the cross-encoder on the small corpus while retrieval latency increased by orders of magnitude. v1.9 measured roughly a **290x** retrieval-time multiplier in the explicit ablation.
62
 
63
- RAGForge therefore keeps the reranker available but skips it under the measured small-corpus policy. Larger corpora remain eligible because the demo does not prove the reranker is universally unnecessary.
64
 
65
- ## Runtime
66
 
67
- | Metric | Result |
68
- |---|---:|
69
- | Pipeline p50 | 1.79 s |
70
- | Pipeline p95 | 5.07 s |
71
- | Planner p50 | 1.71 s |
72
- | Planner p95 | 1.84 s |
73
- | Mean estimated LLM calls | 0.76 |
74
- | Standard Gemini requests | 36 |
75
- | Evaluation target | 12 RPM |
76
- | 429 retries | 0 |
77
 
78
- Generation remained the dominant node by a wide margin, which motivated context/prompt optimization rather than further millisecond-scale retrieval tuning.
79
 
80
- ## Interpretation
81
 
82
- The final engineering conclusions are more important than the headline grade:
 
 
83
 
84
- - retrieval recall is strong enough that **context selection** is now the more useful optimization surface
85
- - deterministic evaluation caught several bugs in the evaluator itself, including invalid AP calculations, missing table-citation sources, false citation-coverage failures, and missing-answer false negatives
86
- - measured reranker cost changed runtime policy rather than remaining a decorative architecture component
87
- - benchmark request pacing made evaluation repeatable under a 15-RPM free-tier project without 429 failures
88
- - the project exposes limitations rather than treating a synthetic stress run as proof of production scale
 
1
+ # Benchmark notes
2
+
3
+ This document records the main observations from the bundled RAGForge regression suite. The suite uses a five-source demo corpus with one structured table and a 90-chunk base index. It is intended for regression testing and design comparisons, not as a claim about general RAG accuracy.
4
+
5
+ ## Final acceptance note
6
+
7
+ The v2.0 deployment run exposed a bug in the benchmark itself. A focused QA generation answered the Sev-1 acknowledgement question with `15 minutes`, while the source states `5 minutes`. The old matcher accepted that answer because one accepted phrase was `5 min`, which appeared as a substring of `15 minutes`.
8
+
9
+ v2.0.1 changes answer matching to respect token/phrase boundaries and bumps the benchmark version. Saved v2.0 results should therefore be treated as historical rather than authoritative v2.0.1 results.
10
+
11
+ The same run confirmed that the multi-source Acme-vs-OrbitPay timing comparison now follows the intended `Auto + Balanced` comparison path and returns both required time windows correctly.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ ## Retrieval and context observations
14
 
15
+ The last full Standard run before the matcher fix showed strong source recall on the demo cases, but lower source precision. That pattern motivated context selection rather than another retrieval layer: retrieve enough evidence to keep the relevant source, then reduce what reaches generation.
16
 
17
+ The context ablation compared three policies:
18
+
19
+ | Policy | Median chunks sent to generation | Source Recall@5 | Median estimated context tokens |
20
  |---|---:|---:|---:|
21
+ | Full top-k | 6 | 100% | 2,101 |
22
+ | Fixed 3-chunk budget | 3 | 100% | 977 |
23
+ | Adaptive budget | 4 | 100% | 1,099 |
24
+
25
+ The adaptive policy remains the runtime default because it keeps more safety margin for ambiguous queries than a global fixed-three rule.
26
+
27
+ ## Sentence compression
28
 
29
+ Focused sentence selection is applied after the context budget. In the latest ablation, labeled answer evidence was retained in all nine focused cases while the selected evidence was reduced further before generation.
30
 
31
+ This result is specific to the bundled benchmark. It should be read as evidence that the local compression policy is safe for these cases, not as a universal token-reduction guarantee.
32
 
33
+ ## Synthetic scale stress
34
 
35
+ The scale harness reuses existing vectors and adds deterministic distractor copies so the real dense + BM25 + RRF retrieval path can be tested at larger index sizes without additional model calls.
36
+
37
+ | Scale | Chunks | Sources | Source Recall@5 | Hit@1 | MRR |
38
+ |---|---:|---:|---:|---:|---:|
39
+ | Base | 90 | 5 | 100% | 100% | 1.000 |
40
+ | +4x long-document distractors | 434 | 9 | 100% | 100% | 1.000 |
41
+ | +19x long-document distractors | 1,724 | 24 | 100% | 87.5% | 0.938 |
42
 
43
+ The largest synthetic run retained Recall@5, while ranking quality at the very top weakened slightly. This is useful as a regression signal, but a real large-corpus upload is still needed before making stronger scale claims.
44
 
45
+ ## Reranker tradeoff
46
 
47
+ On the bundled corpus, the cross-encoder reranker repeatedly added multi-second retrieval cost without improving labeled source- or chunk-level ranking metrics. The runtime therefore skips it for the measured small-corpus case and leaves it available for larger or harder workloads.
48
 
49
+ ## Latency
50
 
51
+ Generation remains the dominant part of service latency. That is why recent optimization work focused on context size and prompt inputs rather than shaving milliseconds from retrieval.
52
 
53
+ The evaluation UI separates service latency from deliberate request pacing, so a quota-safe Standard run can take several minutes wall-clock without making the pipeline itself appear artificially slow.
 
 
 
 
 
 
 
 
 
54
 
55
+ ## How to use these results
56
 
57
+ The benchmark is most useful for three things:
58
 
59
+ 1. catching regressions in routing, retrieval, grounding, SQL, and missing-information behavior;
60
+ 2. comparing runtime policies such as reranking, context budgets, and sentence compression; and
61
+ 3. exposing measurement bugs in the evaluator itself.
62
 
63
+ For a final deployment result, rerun Standard after installing v2.0.1 so the stricter numeric answer matcher is used.
 
 
 
 
docs/MIGRATION_2.0.1.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Upgrade to RAGForge v2.0.1
2
+
3
+ v2.0.1 is a small correctness and presentation patch over v2.0.0.
4
+
5
+ ## What changed
6
+
7
+ - landing and evaluation UI copy is simplified and historical benchmark badges are removed
8
+ - evaluation summaries are scoped to the bundled benchmark instead of presenting grade/readiness cards
9
+ - Quick no longer reports acceptance/readiness results for ablations it does not run
10
+ - answer-key matching is boundary-aware, fixing numeric substring false positives such as `5 min` matching `15 minutes`
11
+ - the source-localization planner case accepts both semantic and hierarchical retrieval
12
+ - context-budget experiment names no longer contain development-version labels
13
+ - README and evaluation documentation are rewritten around behavior, methodology, and limitations
14
+
15
+ ## Benchmark compatibility
16
+
17
+ The benchmark version is `2.0.1`. Existing `2.0` saved evaluation reports remain historical but are not reused as current results because answer-matching semantics changed.
18
+
19
+ ## Upgrade
20
+
21
+ Apply the patch over v2.0.0, then run Quick. Run Standard once if you want a new authoritative benchmark result under the corrected matcher.
docs/PORTFOLIO_GUIDE.md CHANGED
@@ -1,116 +1,86 @@
1
- # Portfolio and interview guide
2
 
3
- ## 2-minute demo
4
 
5
- Use the bundled corpus with **Auto + Balanced**.
6
 
7
- ### 1. Focused retrieval
8
 
9
- Ask:
10
 
11
  ```text
12
  What is the Sev-1 acknowledgement target?
13
  ```
14
 
15
- Show:
16
 
17
- - one-sentence grounded answer with `[D1]`
18
- - semantic route / fact lookup
19
- - adaptive context budget
20
- - evidence sentence compression
21
- - reranker skipped with an explicit policy reason
22
- - node latency waterfall
23
-
24
- Talking point: RAGForge separates retrieval depth from generation context. It can retrieve broadly enough for safety, then reduce what reaches the model.
25
-
26
- ### 2. Global corpus overview
27
-
28
- Ask:
29
 
30
  ```text
31
  What is the collection about?
32
  ```
33
 
34
- Show:
35
-
36
- - source-balanced global retrieval
37
- - all five source families represented
38
- - `[D#]` document citations and `[T#]` table evidence
39
- - broad tasks bypass focused pruning/compression
40
-
41
- Talking point: long PDFs do not dominate simply because they create more chunks.
42
 
43
- ### 3. Analytical synthesis
44
-
45
- Ask:
46
 
47
  ```text
48
  What exactly does this collection reveal? Identify important trends and caveats.
49
  ```
50
 
51
- Show:
52
-
53
- - `insight_synthesis -> analytical`
54
- - deterministic DuckDB table evidence combined with document evidence
55
- - quantitative table claims cited as `[T#]`
56
-
57
- Talking point: the analytical route is different from a generic document overview and can reason across text + structured data.
58
 
59
- ### 4. Text2SQL
60
-
61
- Ask:
62
 
63
  ```text
64
  Which support tier has the shortest first-response SLA?
65
  ```
66
 
67
- Show the SQL route and typed result.
68
-
69
- Talking point: routing and SQL execution are independently evaluated. The SQL path is restricted to validated read-only queries.
70
 
71
- ### 5. Evaluation
72
 
73
- Open Standard evaluation results and show:
74
 
75
- - component metrics instead of one opaque score
76
- - context-budget and compression ablations
77
- - scale stress
78
- - node latency
79
- - release readiness
80
 
81
- Talking point: evaluation findings changed runtime policy. For example, the small-corpus reranker is skipped because the ablation showed large latency cost without measured ranking gain.
82
 
83
- ## 30-second architecture explanation
84
 
85
- > RAGForge first uses a schema-constrained planner to decide whether the question needs documents, SQL, the web, or a mixed path. Document retrieval can be semantic, source-balanced global, hierarchical source-to-chunk, or analytical document-plus-table retrieval. Dense and BM25 results are fused with RRF, then an adaptive policy decides retrieval depth, reranking, context budget, and focused sentence compression. Evidence is graded before generation, CRAG can retry retrieval, web search is conditional rather than automatic, and answers return explicit document/table/web citation IDs plus a full execution trace.
86
 
87
- ## Strong engineering tradeoffs to discuss
88
 
89
  ### Reranking
90
 
91
- The implementation contains a cross-encoder, but the runtime does not blindly pay for it. The demo ablation showed no source/chunk gain at small scale while adding multi-second retrieval latency, so the policy skips it there and preserves it for larger/harder workloads.
92
 
93
- ### Context precision vs recall
94
 
95
- The demo reached 100% Recall@5 with modest Precision@5. Instead of lowering top-k globally, RAGForge retrieves enough candidates, then applies a generation-specific adaptive context budget. That preserves recall while reducing model input.
96
 
97
- ### Free-tier evaluation
98
 
99
- A full benchmark can exceed provider RPM limits if every component fires immediately. RAGForge uses a rolling request pacer, honors retry guidance, stores reports, and lets Deep reuse Standard so evaluation itself is operationally realistic.
100
 
101
- ### Evaluation bugs are real bugs
102
 
103
- During development, the evaluator itself produced invalid AP values, missed grouped citations, penalized list citations, and misclassified correct missing-information answers. Fixing the measurement system was part of the engineering work.
104
 
105
- ## Honest limitations to mention
106
 
107
- - standard Hugging Face runtime state is ephemeral
108
- - no durable multi-tenant storage or distributed vector infrastructure
109
- - prompt-injection filtering is heuristic defense-in-depth
110
- - synthetic 1,724-chunk stress is not an enterprise-scale proof
111
- - token telemetry is estimated, not provider billing data
112
- - provider model quotas and privacy terms depend on the active tier/project
113
 
114
- ## Resume-ready one-liner
115
 
116
- > Built RAGForge, a Dockerized FastAPI/Gradio agentic RAG system with semantic routing, hybrid + hierarchical retrieval, analytical document/table synthesis, read-only Text2SQL, conditional web research, adaptive context compression, cited answers, and an evaluation suite spanning robustness, citation quality, retrieval ablations, scale stress, latency, and release-readiness gates.
 
1
+ # Demo guide
2
 
3
+ This page is a short walkthrough for showing RAGForge without turning the demo into a feature checklist.
4
 
5
+ ## Suggested demo
6
 
7
+ Use the bundled corpus with `Auto` + `Balanced`.
8
 
9
+ ### Focused document question
10
 
11
  ```text
12
  What is the Sev-1 acknowledgement target?
13
  ```
14
 
15
+ Show the answer, the returned source, and the Pipeline Inspector. The useful point is that retrieval can start broad enough for safety while the generation context is reduced when the evidence is clear.
16
 
17
+ ### Corpus overview
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  ```text
20
  What is the collection about?
21
  ```
22
 
23
+ Show that the result covers the different source files rather than being dominated by the long PDF. The support table should appear as structured evidence rather than being treated only as raw CSV text.
 
 
 
 
 
 
 
24
 
25
+ ### Cross-source analysis
 
 
26
 
27
  ```text
28
  What exactly does this collection reveal? Identify important trends and caveats.
29
  ```
30
 
31
+ This demonstrates the analytical path, which combines document evidence with deterministic table summaries before generation.
 
 
 
 
 
 
32
 
33
+ ### Structured data
 
 
34
 
35
  ```text
36
  Which support tier has the shortest first-response SLA?
37
  ```
38
 
39
+ Show that the planner sends the question to the read-only SQL path and that the query/result are inspectable.
 
 
40
 
41
+ ### Evaluation
42
 
43
+ Open a Standard result and show one or two tables rather than the whole report. The most useful examples are:
44
 
45
+ - retrieval vs reranking,
46
+ - full vs fixed vs adaptive context budgets,
47
+ - sentence-compression signal retention,
48
+ - node latency, and
49
+ - synthetic scale stress.
50
 
51
+ The main point is that evaluation changed runtime policy. Components are not enabled just because they are common RAG techniques.
52
 
53
+ ## Short architecture explanation
54
 
55
+ RAGForge first decides whether a question belongs to documents, structured data, the web, or a mixed path. Document retrieval can be direct semantic search, source-balanced global search, source-first hierarchical search, or analytical document-plus-table retrieval. Dense and lexical results are fused, then runtime policies decide retrieval depth, reranking, and how much evidence reaches generation. The answer returns its sources and an execution trace so those choices can be inspected.
56
 
57
+ ## Engineering tradeoffs worth discussing
58
 
59
  ### Reranking
60
 
61
+ The project includes a cross-encoder, but the small demo benchmark did not show a ranking improvement that justified its multi-second cost. The runtime therefore skips it for the measured small-corpus case and keeps it available for larger/harder workloads.
62
 
63
+ ### Context size
64
 
65
+ Source recall was strong while precision was modest. Instead of globally lowering retrieval depth, RAGForge separates retrieval from generation context and uses an adaptive evidence budget for focused questions.
66
 
67
+ ### Evaluation cost
68
 
69
+ A full benchmark can hit free-tier RPM limits. Evaluation uses a rolling request pacer, stores completed reports, and lets Deep reuse a compatible Standard baseline.
70
 
71
+ ### Evaluator correctness
72
 
73
+ The benchmark itself is tested and versioned. During development it exposed bugs in AP calculation, citation parsing, missing-information scoring, and numeric answer matching. A benchmark result is only useful if the measurement code is also trustworthy.
74
 
75
+ ## Limitations to mention
76
 
77
+ - state is ephemeral on a standard Hugging Face Space
78
+ - there is no durable multi-tenant storage layer
79
+ - prompt-injection filtering is heuristic
80
+ - the larger retrieval test uses synthetic distractors
81
+ - trace token counts are estimates rather than billing records
82
+ - production deployment would need stronger tenant isolation, durable storage, and external infrastructure
83
 
84
+ ## Resume description
85
 
86
+ > Built a FastAPI/Gradio RAG system that routes questions across documents, structured data, and web search, combines dense and lexical retrieval, returns source-linked answers, and includes regression tests for retrieval, routing, SQL, robustness, latency, and context-selection policies.
docs/RESUME_BULLETS.md CHANGED
@@ -6,7 +6,7 @@
6
 
7
  ## Measured supporting result
8
 
9
- - On the user-verified v1.9 Standard baseline, achieved 100% answer accuracy, Recall@5, citation validity/coverage, planner route/task/strategy accuracy and Text2SQL pass; adaptive context budgeting preserved 100% Recall@5 while reducing median context by 41%, with sentence compression retaining 100% labeled answer signals and cutting selected evidence tokens by a further 58%.
10
 
11
  - Extended RAGForge with an **Insight Synthesis** path that semantically distinguishes collection overviews from trend/pattern analysis, combines source-balanced document evidence with deterministic DuckDB descriptive context, and grounds structured claims with `[T#]` citations.
12
  - Evolved the evaluation suite into a **hard-mode robustness benchmark** covering paraphrase, distractors, missing answers, multi-hop reasoning, local freshness semantics, analytical synthesis, structured filtering and prompt-injection checks; added hard-mode quality gates.
@@ -46,5 +46,5 @@
46
  ## v1.9 scale / efficiency bullets
47
 
48
  - Generalized evaluation-driven context pruning into a **corpus-aware retrieval budget**: dynamically expanded candidate depth with corpus scale, selected 2-5 evidence chunks from ranking confidence/ambiguity, and applied zero-LLM sentence compression while preserving broad multi-source synthesis paths.
49
- - Built **zero-Gemini scale and context ablations** that stress the real hybrid retriever at roughly 1x/5x/20x long-document distractor scale, compare full/fixed/adaptive context policies, validate answer-signal retention, and gate release readiness on quality/robustness thresholds.
50
  - Added **prompt-economics and operational observability** including estimated generation input/output tokens, cited-evidence utilization, pace-corrected node latency, workspace capacity/index-memory diagnostics and a release-readiness checklist exposed in Gradio and FastAPI.
 
6
 
7
  ## Measured supporting result
8
 
9
+ - Built a transparent regression suite for retrieval, routing, citation grounding, Text2SQL, robustness, latency, context selection, and synthetic scale stress; used its ablations to drive runtime policies such as small-corpus reranker skipping and adaptive generation context.
10
 
11
  - Extended RAGForge with an **Insight Synthesis** path that semantically distinguishes collection overviews from trend/pattern analysis, combines source-balanced document evidence with deterministic DuckDB descriptive context, and grounds structured claims with `[T#]` citations.
12
  - Evolved the evaluation suite into a **hard-mode robustness benchmark** covering paraphrase, distractors, missing answers, multi-hop reasoning, local freshness semantics, analytical synthesis, structured filtering and prompt-injection checks; added hard-mode quality gates.
 
46
  ## v1.9 scale / efficiency bullets
47
 
48
  - Generalized evaluation-driven context pruning into a **corpus-aware retrieval budget**: dynamically expanded candidate depth with corpus scale, selected 2-5 evidence chunks from ranking confidence/ambiguity, and applied zero-LLM sentence compression while preserving broad multi-source synthesis paths.
49
+ - Built **zero-Gemini scale and context ablations** that stress the real hybrid retriever at roughly 1x/5x/20x long-document distractor scale, compare full/fixed/adaptive context policies, validate answer-signal retention, and gate acceptance checks on quality/robustness thresholds.
50
  - Added **prompt-economics and operational observability** including estimated generation input/output tokens, cited-evidence utilization, pace-corrected node latency, workspace capacity/index-memory diagnostics and a release-readiness checklist exposed in Gradio and FastAPI.
docs/architecture.mmd CHANGED
@@ -55,7 +55,7 @@ flowchart TD
55
  EV --> ST[1x / 5x / 20x scale stress]
56
  EV --> LAT[Service node latency]
57
  EV --> DJ[Optional calibrated Deep judge]
58
- RM --> READY[Quality gates + release readiness]
59
  CTX --> READY
60
  PL --> READY
61
  CIT --> READY
 
55
  EV --> ST[1x / 5x / 20x scale stress]
56
  EV --> LAT[Service node latency]
57
  EV --> DJ[Optional calibrated Deep judge]
58
+ RM --> READY[Quality gates + acceptance checks]
59
  CTX --> READY
60
  PL --> READY
61
  CIT --> READY
evals/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # RAGForge benchmark - v2.0 final
2
 
3
- The final benchmark keeps the v1.9 retrieval/context/compression/scale methodology and changes one Hard Mode reference path: `hard_multihop_time_compare` now runs under `Auto + Balanced` and explicitly expects `documents -> comparison -> hierarchical`. This matches the recommended semantic runtime instead of using Fast mode as the reference for a multi-source comparison.
4
 
5
- ## v1.9 local ablations and scale stress
6
 
7
  Standard/Deep add three zero-Gemini evaluation surfaces: a three-way full-vs-fixed-vs-adaptive context budget, focused sentence-compression signal retention, and a 1x/5x/20x synthetic long-document distractor stress harness. These are regression/engineering tests for the bundled demo, not claims of universal large-corpus performance. The release-readiness table applies explicit thresholds to the underlying metrics and marks missing Standard/Deep stress evidence as incomplete rather than silently passing it.
8
 
@@ -53,4 +53,4 @@ Citation coverage is Markdown-aware, missing-answer cases accept natural grounde
53
 
54
  Standard/Deep now add a deterministic `context_budget_ablation` table. It compares the original six-chunk focused context with the adaptive three-chunk safety floor and reports source Precision@5, Recall@5, Hit@1, MRR, median chunks/sources/chars, estimated tokens and reduction percentage. The ablation issues no Gemini requests.
55
 
56
- The missing-answer Hard Mode row also exposes `missing_answer_match` and `grounded_absence`. Either calibrated signal can satisfy the missing-information decision, avoiding false failures caused by one exact wording.
 
1
+ # RAGForge demo benchmark
2
 
3
+ The benchmark covers retrieval, routing, context policies, structured data, robustness, and scale stress. The multi-source Hard Mode comparison runs through the recommended Auto + Balanced path: `hard_multihop_time_compare` now runs under `Auto + Balanced` and explicitly expects `documents -> comparison -> hierarchical`. This matches the recommended semantic runtime instead of using Fast mode as the reference for a multi-source comparison.
4
 
5
+ ## Local ablations and scale stress
6
 
7
  Standard/Deep add three zero-Gemini evaluation surfaces: a three-way full-vs-fixed-vs-adaptive context budget, focused sentence-compression signal retention, and a 1x/5x/20x synthetic long-document distractor stress harness. These are regression/engineering tests for the bundled demo, not claims of universal large-corpus performance. The release-readiness table applies explicit thresholds to the underlying metrics and marks missing Standard/Deep stress evidence as incomplete rather than silently passing it.
8
 
 
53
 
54
  Standard/Deep now add a deterministic `context_budget_ablation` table. It compares the original six-chunk focused context with the adaptive three-chunk safety floor and reports source Precision@5, Recall@5, Hit@1, MRR, median chunks/sources/chars, estimated tokens and reduction percentage. The ablation issues no Gemini requests.
55
 
56
+ The missing-answer Hard Mode row also exposes `missing_answer_match` and `grounded_absence`. Either calibrated signal can satisfy the missing-information decision, avoiding false failures caused by one exact wording.
evals/demo_benchmark.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
- "version": "2.0",
3
- "description": "RAGForge v2.0 final benchmark with adaptive context budgets, evidence compression, scale stress, grounded absence, hard-mode robustness, release readiness, and a default-profile multi-hop comparison case.",
4
  "qa_cases": [
5
  {
6
  "id": "qa_acme_sev1_ack",
@@ -214,7 +214,11 @@
214
  "route": "documents",
215
  "task": "fact_lookup",
216
  "strategy": "hierarchical",
217
- "web_expected": false
 
 
 
 
218
  },
219
  {
220
  "id": "plan_internal_comparison",
 
1
  {
2
+ "version": "2.0.1",
3
+ "description": "RAGForge bundled benchmark for retrieval, routing, grounded answers, Text2SQL, robustness, context policies, and synthetic scale stress.",
4
  "qa_cases": [
5
  {
6
  "id": "qa_acme_sev1_ack",
 
214
  "route": "documents",
215
  "task": "fact_lookup",
216
  "strategy": "hierarchical",
217
+ "web_expected": false,
218
+ "strategy_any": [
219
+ "hierarchical",
220
+ "semantic"
221
+ ]
222
  },
223
  {
224
  "id": "plan_internal_comparison",
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "ragforge"
7
- version = "2.0.0"
8
  description = "Production-style agentic RAG demo for Hugging Face Spaces"
9
  requires-python = ">=3.11"
10
  dependencies = []
 
4
 
5
  [project]
6
  name = "ragforge"
7
+ version = "2.0.1"
8
  description = "Production-style agentic RAG demo for Hugging Face Spaces"
9
  requires-python = ">=3.11"
10
  dependencies = []
scripts/release_check.py CHANGED
@@ -1,5 +1,5 @@
1
  #!/usr/bin/env python3
2
- """Dependency-free consistency checks for the final RAGForge release."""
3
 
4
  from __future__ import annotations
5
 
@@ -8,8 +8,8 @@ import re
8
  from pathlib import Path
9
 
10
  ROOT = Path(__file__).resolve().parents[1]
11
- APP_VERSION = "2.0.0"
12
- BENCHMARK_VERSION = "2.0"
13
 
14
 
15
  def read(path: str) -> str:
@@ -27,18 +27,23 @@ def main() -> None:
27
  api_py = read("src/ragforge/api.py")
28
  ui_py = read("src/ragforge/ui.py")
29
  readme = read("README.md")
 
30
  benchmark = json.loads(read("evals/demo_benchmark.json"))
31
 
32
  require(f'version = "{APP_VERSION}"' in pyproject, "pyproject version mismatch")
33
  require(f'__version__ = "{APP_VERSION}"' in init_py, "package version mismatch")
34
  require(f'version="{APP_VERSION}"' in api_py, "FastAPI version mismatch")
35
- require(f'RAGForge v{APP_VERSION}' in ui_py, "UI version mismatch")
36
  require(benchmark.get("version") == BENCHMARK_VERSION, "benchmark version mismatch")
37
 
38
  require("—" not in ui_py and "–" not in ui_py, "UI contains long dash glyphs")
39
- require("Final portfolio release" in ui_py, "final UI hero missing")
40
- require("Verified pre-final benchmark" in readme, "final benchmark section missing from README")
41
- require("v2.0.0 is the final planned feature release" in readme, "final project status missing")
 
 
 
 
42
 
43
  required_docs = [
44
  "CHANGELOG.md",
@@ -68,7 +73,10 @@ def main() -> None:
68
  require(multihop.get("expected_task") == "comparison", "multi-hop Hard Mode expected task mismatch")
69
  require(multihop.get("expected_strategy") == "hierarchical", "multi-hop Hard Mode expected strategy mismatch")
70
 
71
- # Catch an earlier UI regression where the live runtime header was duplicated.
 
 
 
72
  architecture_fn = re.search(
73
  r"def _architecture_snapshot\(.*?\n\s*return sid, runtime, curl, runtime_json",
74
  ui_py,
@@ -77,10 +85,7 @@ def main() -> None:
77
  require(architecture_fn is not None, "architecture snapshot function missing")
78
  require(architecture_fn.group(0).count("**RAGForge:**") == 1, "runtime header is duplicated")
79
 
80
- print(
81
- "RAGForge release check PASS - "
82
- f"app {APP_VERSION}, benchmark {BENCHMARK_VERSION}, final docs/UI/benchmark aligned."
83
- )
84
 
85
 
86
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
+ """Dependency-free consistency checks for the RAGForge release."""
3
 
4
  from __future__ import annotations
5
 
 
8
  from pathlib import Path
9
 
10
  ROOT = Path(__file__).resolve().parents[1]
11
+ APP_VERSION = "2.0.1"
12
+ BENCHMARK_VERSION = "2.0.1"
13
 
14
 
15
  def read(path: str) -> str:
 
27
  api_py = read("src/ragforge/api.py")
28
  ui_py = read("src/ragforge/ui.py")
29
  readme = read("README.md")
30
+ metrics_py = read("src/ragforge/eval_metrics.py")
31
  benchmark = json.loads(read("evals/demo_benchmark.json"))
32
 
33
  require(f'version = "{APP_VERSION}"' in pyproject, "pyproject version mismatch")
34
  require(f'__version__ = "{APP_VERSION}"' in init_py, "package version mismatch")
35
  require(f'version="{APP_VERSION}"' in api_py, "FastAPI version mismatch")
36
+ require(f'`v{APP_VERSION}`' in ui_py, "runtime UI version mismatch")
37
  require(benchmark.get("version") == BENCHMARK_VERSION, "benchmark version mismatch")
38
 
39
  require("—" not in ui_py and "–" not in ui_py, "UI contains long dash glyphs")
40
+ require("Final portfolio release" not in ui_py, "release-marketing copy remains in UI")
41
+ require("Pre-final readiness" not in ui_py, "readiness billboard remains in UI")
42
+ require("Verified v1.9 grade" not in ui_py, "historical grade billboard remains in UI")
43
+ require("[D#] [T#] [W#] citations" not in ui_py, "internal citation syntax remains in hero copy")
44
+ require("The results describe this demo benchmark only; they are not general accuracy claims." in ui_py, "evaluation scope note missing")
45
+ require("final portfolio release" not in readme.lower(), "README still reads like portfolio-release marketing")
46
+ require("_contains_expected_term" in metrics_py, "boundary-aware answer matcher missing")
47
 
48
  required_docs = [
49
  "CHANGELOG.md",
 
73
  require(multihop.get("expected_task") == "comparison", "multi-hop Hard Mode expected task mismatch")
74
  require(multihop.get("expected_strategy") == "hierarchical", "multi-hop Hard Mode expected strategy mismatch")
75
 
76
+ planner_cases = {case.get("id"): case for case in benchmark.get("planner_cases", [])}
77
+ source_lookup = planner_cases.get("plan_find_refund_file", {})
78
+ require(set(source_lookup.get("strategy_any", [])) == {"semantic", "hierarchical"}, "source-localization strategy alternatives missing")
79
+
80
  architecture_fn = re.search(
81
  r"def _architecture_snapshot\(.*?\n\s*return sid, runtime, curl, runtime_json",
82
  ui_py,
 
85
  require(architecture_fn is not None, "architecture snapshot function missing")
86
  require(architecture_fn.group(0).count("**RAGForge:**") == 1, "runtime header is duplicated")
87
 
88
+ print(f"RAGForge release check PASS - app {APP_VERSION}, benchmark {BENCHMARK_VERSION}.")
 
 
 
89
 
90
 
91
  if __name__ == "__main__":
src/ragforge/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
  """RAGForge: production-style agentic retrieval augmented generation demo."""
2
 
3
- __version__ = "2.0.0"
 
1
  """RAGForge: production-style agentic retrieval augmented generation demo."""
2
 
3
+ __version__ = "2.0.1"
src/ragforge/api.py CHANGED
@@ -29,7 +29,7 @@ def _auth(authorization: Annotated[str | None, Header()] = None) -> None:
29
 
30
 
31
  def create_api() -> FastAPI:
32
- app = FastAPI(title="RAGForge API", version="2.0.0")
33
 
34
  @app.get("/api/health")
35
  def health():
 
29
 
30
 
31
  def create_api() -> FastAPI:
32
+ app = FastAPI(title="RAGForge API", version="2.0.1")
33
 
34
  @app.get("/api/health")
35
  def health():
src/ragforge/eval_metrics.py CHANGED
@@ -41,13 +41,32 @@ def percentile(values: list[float], q: float) -> float:
41
  return ordered[lo] + (ordered[hi] - ordered[lo]) * (pos - lo)
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def answer_key_match(answer: str, case: dict[str, Any]) -> bool:
45
- answer_l = answer.lower()
46
- expected_all = [str(x).lower() for x in case.get("expected_all", [])]
47
- expected_any = [str(x).lower() for x in case.get("expected_any", [])]
48
- if expected_all and not all(term in answer_l for term in expected_all):
49
  return False
50
- if expected_any and not any(term in answer_l for term in expected_any):
51
  return False
52
  return bool(expected_all or expected_any)
53
 
 
41
  return ordered[lo] + (ordered[hi] - ordered[lo]) * (pos - lo)
42
 
43
 
44
+ def _contains_expected_term(answer: str, term: str) -> bool:
45
+ """Match a labeled answer term without accepting alphanumeric substrings.
46
+
47
+ Plain ``term in answer`` makes numeric labels unsafe: for example ``5 min``
48
+ is a substring of ``15 minutes``. Benchmark matching should recognize the
49
+ expected phrase as its own token/phrase while remaining tolerant of ordinary
50
+ whitespace differences.
51
+ """
52
+ text = re.sub(r"\s+", " ", (answer or "").casefold()).strip()
53
+ expected = re.sub(r"\s+", " ", (term or "").casefold()).strip()
54
+ if not expected:
55
+ return False
56
+ pattern = re.escape(expected).replace(r"\ ", r"\s+")
57
+ if expected[0].isalnum():
58
+ pattern = r"(?<![0-9A-Za-z])" + pattern
59
+ if expected[-1].isalnum():
60
+ pattern = pattern + r"(?![0-9A-Za-z])"
61
+ return re.search(pattern, text) is not None
62
+
63
+
64
  def answer_key_match(answer: str, case: dict[str, Any]) -> bool:
65
+ expected_all = [str(x) for x in case.get("expected_all", [])]
66
+ expected_any = [str(x) for x in case.get("expected_any", [])]
67
+ if expected_all and not all(_contains_expected_term(answer, term) for term in expected_all):
 
68
  return False
69
+ if expected_any and not any(_contains_expected_term(answer, term) for term in expected_any):
70
  return False
71
  return bool(expected_all or expected_any)
72
 
src/ragforge/evaluation.py CHANGED
@@ -211,8 +211,8 @@ def _retrieval_ablation(workspace: Workspace, qa_cases: list[dict[str, Any]]) ->
211
 
212
 
213
  def _context_budget_ablation(workspace: Workspace, qa_cases: list[dict[str, Any]]) -> list[dict[str, Any]]:
214
- """Zero-Gemini comparison of full, v1.8 fixed and v1.9 adaptive budgets."""
215
- labels = ("Full top-k", "v1.8 fixed top-3", "v1.9 adaptive budget")
216
  buckets: dict[str, list[dict[str, float]]] = {label: [] for label in labels}
217
  for case in qa_cases:
218
  plan = QueryPlan(
@@ -235,8 +235,8 @@ def _context_budget_ablation(workspace: Workspace, qa_cases: list[dict[str, Any]
235
  )
236
  variants = {
237
  "Full top-k": (hits, len(hits), "full"),
238
- "v1.8 fixed top-3": (fixed.hits, fixed.target_chunks, fixed.reason),
239
- "v1.9 adaptive budget": (adaptive.hits, adaptive.target_chunks, adaptive.reason),
240
  }
241
  full_chars = max(1, sum(len(hit.chunk.text or "") + len(hit.chunk.source or "") + 24 for hit in hits))
242
  for label, (variant, target, reason) in variants.items():
@@ -254,7 +254,7 @@ def _context_budget_ablation(workspace: Workspace, qa_cases: list[dict[str, Any]
254
  "context_chars": float(chars),
255
  "context_tokens_est": float((chars + 3) // 4),
256
  "context_reduction_pct": float(max(0.0, 1.0 - chars / full_chars) * 100.0),
257
- "adaptive_used": float(label == "v1.9 adaptive budget" and len(variant) < len(hits)),
258
  })
259
 
260
  rows: list[dict[str, Any]] = []
@@ -413,7 +413,7 @@ def _planner_eval(
413
  "task_correct": plan.task_type == case["task"],
414
  "expected_strategy": case["strategy"],
415
  "strategy": plan.retrieval_strategy,
416
- "strategy_correct": plan.retrieval_strategy == case["strategy"],
417
  "expected_web": bool(case["web_expected"]),
418
  "planned_web": planned_web,
419
  "latency_ms": round(latency, 1),
@@ -985,7 +985,7 @@ def _diagnostics(
985
 
986
  context_budget_rows = context_budget_rows or []
987
  adaptive_row = next(
988
- (row for row in context_budget_rows if row.get("configuration") == "v1.9 adaptive budget"), {}
989
  )
990
  full_row = next((row for row in context_budget_rows if row.get("configuration") == "Full top-k"), {})
991
  if summary.get("source_recall@5", 0.0) >= 0.95 and summary.get("source_precision@5", 1.0) < 0.60:
@@ -1389,7 +1389,7 @@ def run_demo_eval(
1389
  grade, quality_gates = _grade_with_gates(deterministic_score, metrics_for_gate)
1390
 
1391
  adaptive_budget_row = next(
1392
- (row for row in context_budget_rows if row.get("configuration") == "v1.9 adaptive budget"), {}
1393
  )
1394
  compression_row = next(
1395
  (row for row in compression_rows if row.get("configuration") == "Adaptive + sentence compression"), {}
@@ -1462,10 +1462,15 @@ def run_demo_eval(
1462
  profile_recommendation = _profile_recommendation(profile_summary_rows)
1463
  if profile_recommendation:
1464
  summary["profile_recommendation"] = profile_recommendation
1465
- readiness_rows = _readiness_rows(summary, scale_stress_rows, compression_rows)
1466
- readiness_status, readiness_score = _readiness_summary(readiness_rows)
1467
- summary["release_readiness"] = readiness_status
1468
- summary["release_readiness_score"] = round(readiness_score, 3)
 
 
 
 
 
1469
  diagnostics = _diagnostics(
1470
  summary, ablation_rows, planner_rows, sql_rows, hard_rows, profile_summary_rows, node_latency_rows,
1471
  context_budget_rows, compression_rows, scale_stress_rows
@@ -1528,7 +1533,7 @@ def run_demo_eval(
1528
  "hard_mode": "Hard-mode cases cover paraphrase, distractors, missing answers, multi-hop comparison, analytical synthesis, structured filtering, local freshness semantics and prompt-injection detection.",
1529
  "profile_benchmark": "Optional Fast/Balanced/Agentic comparison uses a small labeled subset because it intentionally spends additional Gemini requests.",
1530
  "chunk_ablation": "Retrieval ablation reports source-level metrics plus chunk Hit@1/MRR for cases with explicit chunk-content labels.",
1531
- "context_budget": "Focused-query pruning is evaluated as a zero-Gemini ablation across full top-k, the v1.8 fixed top-3 policy and the v1.9 adaptive budget chosen from retrieval confidence, score separation and corpus scale.",
1532
  "evidence_compression": "Focused evidence compression selects query-relevant sentences after context budgeting and is evaluated by deterministic answer-signal retention plus token reduction; it never spends a Gemini request.",
1533
  "scale_stress": "Scale stress reuses existing embedding vectors and clones long-document distractor chunks to exercise the real Qdrant + BM25 path at roughly 1x, 5x and 20x distractor scale without additional Gemini calls.",
1534
  "release_readiness": "A transparent readiness checklist applies explicit thresholds to answer quality, grounding, routing, robustness, adaptive-budget recall, compression retention and largest-scale retrieval recall. It does not replace the underlying metrics.",
 
211
 
212
 
213
  def _context_budget_ablation(workspace: Workspace, qa_cases: list[dict[str, Any]]) -> list[dict[str, Any]]:
214
+ """Zero-Gemini comparison of full, fixed, and adaptive context budgets."""
215
+ labels = ("Full top-k", "Fixed 3-chunk budget", "Adaptive budget")
216
  buckets: dict[str, list[dict[str, float]]] = {label: [] for label in labels}
217
  for case in qa_cases:
218
  plan = QueryPlan(
 
235
  )
236
  variants = {
237
  "Full top-k": (hits, len(hits), "full"),
238
+ "Fixed 3-chunk budget": (fixed.hits, fixed.target_chunks, fixed.reason),
239
+ "Adaptive budget": (adaptive.hits, adaptive.target_chunks, adaptive.reason),
240
  }
241
  full_chars = max(1, sum(len(hit.chunk.text or "") + len(hit.chunk.source or "") + 24 for hit in hits))
242
  for label, (variant, target, reason) in variants.items():
 
254
  "context_chars": float(chars),
255
  "context_tokens_est": float((chars + 3) // 4),
256
  "context_reduction_pct": float(max(0.0, 1.0 - chars / full_chars) * 100.0),
257
+ "adaptive_used": float(label == "Adaptive budget" and len(variant) < len(hits)),
258
  })
259
 
260
  rows: list[dict[str, Any]] = []
 
413
  "task_correct": plan.task_type == case["task"],
414
  "expected_strategy": case["strategy"],
415
  "strategy": plan.retrieval_strategy,
416
+ "strategy_correct": plan.retrieval_strategy in set(case.get("strategy_any", [case["strategy"]])),
417
  "expected_web": bool(case["web_expected"]),
418
  "planned_web": planned_web,
419
  "latency_ms": round(latency, 1),
 
985
 
986
  context_budget_rows = context_budget_rows or []
987
  adaptive_row = next(
988
+ (row for row in context_budget_rows if row.get("configuration") == "Adaptive budget"), {}
989
  )
990
  full_row = next((row for row in context_budget_rows if row.get("configuration") == "Full top-k"), {})
991
  if summary.get("source_recall@5", 0.0) >= 0.95 and summary.get("source_precision@5", 1.0) < 0.60:
 
1389
  grade, quality_gates = _grade_with_gates(deterministic_score, metrics_for_gate)
1390
 
1391
  adaptive_budget_row = next(
1392
+ (row for row in context_budget_rows if row.get("configuration") == "Adaptive budget"), {}
1393
  )
1394
  compression_row = next(
1395
  (row for row in compression_rows if row.get("configuration") == "Adaptive + sentence compression"), {}
 
1462
  profile_recommendation = _profile_recommendation(profile_summary_rows)
1463
  if profile_recommendation:
1464
  summary["profile_recommendation"] = profile_recommendation
1465
+ if level == "Quick":
1466
+ readiness_rows = []
1467
+ summary["release_readiness"] = "NOT RUN"
1468
+ summary["release_readiness_score"] = None
1469
+ else:
1470
+ readiness_rows = _readiness_rows(summary, scale_stress_rows, compression_rows)
1471
+ readiness_status, readiness_score = _readiness_summary(readiness_rows)
1472
+ summary["release_readiness"] = readiness_status
1473
+ summary["release_readiness_score"] = round(readiness_score, 3)
1474
  diagnostics = _diagnostics(
1475
  summary, ablation_rows, planner_rows, sql_rows, hard_rows, profile_summary_rows, node_latency_rows,
1476
  context_budget_rows, compression_rows, scale_stress_rows
 
1533
  "hard_mode": "Hard-mode cases cover paraphrase, distractors, missing answers, multi-hop comparison, analytical synthesis, structured filtering, local freshness semantics and prompt-injection detection.",
1534
  "profile_benchmark": "Optional Fast/Balanced/Agentic comparison uses a small labeled subset because it intentionally spends additional Gemini requests.",
1535
  "chunk_ablation": "Retrieval ablation reports source-level metrics plus chunk Hit@1/MRR for cases with explicit chunk-content labels.",
1536
+ "context_budget": "Focused-query pruning is evaluated as a zero-Gemini ablation across full top-k, a fixed 3-chunk budget, and the adaptive budget chosen from retrieval confidence, score separation and corpus scale.",
1537
  "evidence_compression": "Focused evidence compression selects query-relevant sentences after context budgeting and is evaluated by deterministic answer-signal retention plus token reduction; it never spends a Gemini request.",
1538
  "scale_stress": "Scale stress reuses existing embedding vectors and clones long-document distractor chunks to exercise the real Qdrant + BM25 path at roughly 1x, 5x and 20x distractor scale without additional Gemini calls.",
1539
  "release_readiness": "A transparent readiness checklist applies explicit thresholds to answer quality, grounding, routing, robustness, adaptive-budget recall, compression retention and largest-scale retrieval recall. It does not replace the underlying metrics.",
src/ragforge/ui.py CHANGED
@@ -21,7 +21,7 @@ DEMO_DIR = ROOT / "demo_documents"
21
 
22
  CSS = """
23
  #hero {max-width: 1220px; margin: 0 auto 14px auto;}
24
- .hero-shell {padding: 20px 22px; border: 1px solid rgba(128,128,128,.22); border-radius: 16px; background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(128,128,128,.03));}
25
  .hero-kicker {font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; opacity: .72;}
26
  .hero-title {font-size: 2.3rem; line-height: 1.05; font-weight: 780; margin: 5px 0 8px 0;}
27
  .hero-subtitle {font-size: 1rem; line-height: 1.55; max-width: 900px; opacity: .84;}
@@ -45,12 +45,15 @@ CSS = """
45
  .latency-track {height: 9px; border-radius: 999px; background: rgba(128,128,128,.18); overflow: hidden;}
46
  .latency-fill {height: 100%; min-width: 2px; border-radius: 999px; background: var(--primary-500, currentColor);}
47
  .latency-time {font-size: .84rem; text-align: right; opacity: .8;}
48
- .eval-card-grid {display: grid; grid-template-columns: repeat(5, minmax(105px, 1fr)); gap: 8px; margin: 8px 0 14px 0;}
49
- .eval-card {padding: 10px 12px; border: 1px solid rgba(128,128,128,.2); border-radius: 10px; background: rgba(128,128,128,.035);}
50
- .eval-card-label {font-size: .70rem; text-transform: uppercase; letter-spacing: .045em; opacity: .65;}
51
- .eval-card-value {font-size: 1.05rem; font-weight: 720; margin-top: 2px;}
 
 
 
52
  .footer-note {text-align: center; opacity: .68; font-size: .82rem; padding: 15px 0 4px 0;}
53
- @media (max-width: 900px) {.baseline-grid,.eval-card-grid {grid-template-columns: repeat(2, minmax(120px, 1fr));}}
54
  """
55
 
56
 
@@ -202,103 +205,72 @@ def _inspector_markdown(trace: dict[str, Any]) -> str:
202
 
203
 
204
  def _eval_summary_markdown(report: dict[str, Any]) -> str:
 
205
  summary = report.get("summary", {}) if report else {}
206
  if not summary:
207
- return "*Run an evaluation to see the score card.*"
208
- cards = (
209
- '<div class="eval-card-grid">'
210
- f'<div class="eval-card"><div class="eval-card-label">Grade</div><div class="eval-card-value">{html.escape(str(summary.get("quality_grade", "-")))}</div></div>'
211
- f'<div class="eval-card"><div class="eval-card-label">Answer accuracy</div><div class="eval-card-value">{float(summary.get("answer_accuracy", 0.0)):.0%}</div></div>'
212
- f'<div class="eval-card"><div class="eval-card-label">Recall@5</div><div class="eval-card-value">{float(summary.get("source_recall@5", 0.0)):.0%}</div></div>'
213
- f'<div class="eval-card"><div class="eval-card-label">Citation coverage</div><div class="eval-card-value">{float(summary.get("citation_coverage", 0.0)):.0%}</div></div>'
214
- f'<div class="eval-card"><div class="eval-card-label">Release readiness</div><div class="eval-card-value">{html.escape(str(summary.get("release_readiness", "-")))}</div></div>'
215
- '</div>'
216
- )
217
- lines = [
218
- f"### Evaluation score card - {summary.get('evaluation_level', '-')} - grade {summary.get('quality_grade', '-')}",
219
- cards,
220
- f"**Deterministic quality:** `{float(summary.get('deterministic_quality_score', 0.0)):.3f}` - "
221
- f"**answer accuracy:** `{float(summary.get('answer_accuracy', 0.0)):.0%}` - "
222
- f"**source Precision@5:** `{float(summary.get('source_precision@5', 0.0)):.0%}` - "
223
- f"**source Recall@5:** `{float(summary.get('source_recall@5', 0.0)):.0%}` - "
224
- f"**Hit@1:** `{float(summary.get('source_hit@1', 0.0)):.0%}` - "
225
- f"**MRR:** `{float(summary.get('source_mrr', 0.0)):.3f}` - "
226
- f"**AP@5:** `{float(summary.get('source_ap@5', 0.0)):.3f}` - "
227
- f"**nDCG@5:** `{float(summary.get('source_ndcg@5', 0.0)):.3f}`",
228
- "",
229
- f"**Citation validity:** `{float(summary.get('citation_validity', 0.0)):.0%}` - "
230
- f"**citation coverage:** `{float(summary.get('citation_coverage', 0.0)):.0%}` - "
231
- f"**overview pass:** `{float(summary.get('overview_pass_rate', 0.0)):.0%}` - "
232
- f"**Text2SQL pass:** `{float(summary.get('text2sql_pass_rate', 0.0)):.0%}` - "
233
- f"**hard-mode pass:** `{float(summary.get('hard_mode_pass_rate', 0.0)):.0%}`",
234
- "",
235
- f"**Planner route/task/strategy:** `{float(summary.get('planner_route_accuracy', 0.0)):.0%}` / "
236
- f"`{float(summary.get('planner_task_accuracy', 0.0)):.0%}` / "
237
- f"`{float(summary.get('planner_strategy_accuracy', 0.0)):.0%}` - "
238
- f"**web precision/recall:** `{float(summary.get('web_use_precision', 0.0)):.0%}` / "
239
- f"`{float(summary.get('web_use_recall', 0.0)):.0%}`",
240
- "",
241
- f"**Adaptive context policy:** Precision@5 `{float(summary.get('context_pruning_precision@5', 0.0)):.0%}` - "
242
- f"Recall@5 `{float(summary.get('context_pruning_recall@5', 0.0)):.0%}` - "
243
- f"target chunks p50 `{float(summary.get('adaptive_context_target_p50', 0.0)):.1f}` - "
244
- f"median context reduction `{float(summary.get('context_pruning_token_reduction_pct', 0.0)):.0f}%`",
245
- f"**Evidence compression:** signal retention `{float(summary.get('compression_signal_retention', 0.0)):.0%}` - "
246
- f"additional reduction `{float(summary.get('compression_additional_reduction_pct', 0.0)):.0f}%` - "
247
- f"runtime compression rate `{float(summary.get('focused_evidence_compression_rate', 0.0)):.0%}`",
248
- f"**Focused context tokens p50:** `{float(summary.get('focused_context_tokens_before_p50', 0.0)):.0f} -> {float(summary.get('focused_context_tokens_after_p50', 0.0)):.0f}` - "
249
- f"**generation prompt/total tokens p50:** `{float(summary.get('focused_generation_prompt_tokens_p50', 0.0)):.0f}` / "
250
- f"`{float(summary.get('focused_generation_total_tokens_p50', 0.0)):.0f}` - "
251
- f"**evidence utilization p50:** `{float(summary.get('focused_evidence_utilization_p50', 0.0)):.0%}`",
252
- f"**Scale stress:** max `{int(summary.get('scale_stress_max_chunks', 0) or 0)}` chunks - "
253
- f"Recall@5 `{float(summary.get('scale_stress_recall@5', 0.0)):.0%}` - "
254
- f"adaptive-pruned recall `{float(summary.get('scale_stress_pruned_recall@5', 0.0)):.0%}`",
255
- f"**Release readiness:** `{summary.get('release_readiness', '-')}` - "
256
- f"`{float(summary.get('release_readiness_score', 0.0)):.0%}` critical checks passed",
257
- "",
258
- f"**Pipeline latency p50/p95:** `{float(summary.get('latency_p50_ms', 0.0)):.0f} ms` / "
259
- f"`{float(summary.get('latency_p95_ms', 0.0)):.0f} ms` - "
260
- f"**planner p50/p95:** `{float(summary.get('planner_latency_p50_ms', 0.0)):.0f} ms` / "
261
- f"`{float(summary.get('planner_latency_p95_ms', 0.0)):.0f} ms` - "
262
- f"**mean estimated LLM calls:** `{float(summary.get('mean_llm_calls_estimate', 0.0)):.2f}`",
263
- "",
264
- f"**Evaluation wall time:** `{float(summary.get('evaluation_wall_ms', 0.0)) / 1000:.1f} s` - "
265
- f"**response cache bypassed:** `{bool(summary.get('cache_bypassed', False))}`",
266
- "",
267
- f"**Gemini request budget:** "
268
- f"`{int(summary.get('evaluation_target_rpm', 0) or 0)} RPM` - "
269
- f"**requests issued:** `{int(summary.get('gemini_requests', 0) or 0)}` - "
270
- f"**deliberate pacing wait:** `{float(summary.get('pacing_sleep_ms', 0.0) or 0.0) / 1000:.1f} s` - "
271
- f"**429 retries:** `{int(summary.get('rate_limit_retries', 0) or 0)}`",
272
  ]
273
- gates = summary.get("quality_gate_notes") or []
274
- if gates:
275
- lines += ["", "**Quality gates affecting the grade:**"] + [f"- {item}" for item in gates]
276
- if "judge_overall" in summary:
277
- lines += [
278
- "",
279
- f"**Deep judge:** faithfulness `{float(summary.get('judge_faithfulness', 0.0)):.3f}` - "
280
- f"answer relevance `{float(summary.get('judge_answer_relevance', 0.0)):.3f}` - "
281
- f"completeness `{float(summary.get('judge_completeness', 0.0)):.3f}` - "
282
- f"citation support `{float(summary.get('judge_citation_support', 0.0)):.3f}` - "
283
- f"overall `{float(summary.get('judge_overall', 0.0)):.3f}` - "
284
- f"judge pass `{float(summary.get('judge_pass_rate', 0.0)):.0%}` - "
285
- f"sampled cases `{int(summary.get('deep_judge_cases', 0) or 0)}`",
286
- ]
287
- if summary.get("profile_benchmark_enabled"):
288
- lines.extend([
289
- "",
290
- f"**Profile benchmark:** `{int(summary.get('profile_benchmark_cases', 0) or 0)}` profile/case runs included.",
291
  ])
292
- if summary.get("profile_recommendation"):
293
- lines.extend(["", f"**Profile policy:** {summary.get('profile_recommendation')}"])
294
- if summary.get("reused_standard_baseline"):
295
- lines += [
296
- "",
297
- "**Evaluation reuse:** Deep reused the saved Standard deterministic baseline and ran only the sampled judge layer. "
298
- f"Baseline Standard wall time: `{float(summary.get('deterministic_baseline_wall_ms', 0.0) or 0.0) / 1000:.1f} s`.",
299
- ]
300
- return "\n".join(lines)
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
  def _eval_diagnostics_markdown(report: dict[str, Any]) -> str:
304
  diagnostics = report.get("diagnostics", []) if report else []
@@ -361,7 +333,7 @@ def _architecture_snapshot(session_id: str | None) -> tuple[str, str, str, dict[
361
  settings = get_settings()
362
  stats = ws.health_snapshot()
363
  runtime_json = {
364
- "ragforge_version": "2.0.0",
365
  "workspace": stats,
366
  "models": {
367
  "generation": settings.default_model,
@@ -382,7 +354,7 @@ def _architecture_snapshot(session_id: str | None) -> tuple[str, str, str, dict[
382
  }
383
  runtime = (
384
  "### Live runtime\n"
385
- f"**RAGForge:** `v2.0.0` - **workspace:** `{sid[:12]}...` - **status:** `{stats['status']}`\n\n"
386
  f"**Corpus:** `{stats['sources']}` sources - `{stats['chunks']}` chunks - "
387
  f"`{stats['source_profiles']}` source profiles - `{stats['tables']}` tables - "
388
  f"corpus version `{stats['version']}`\n\n"
@@ -438,7 +410,7 @@ EVAL_TABLE_KEYS = {
438
  "Context budget ablation": "context_budget_ablation",
439
  "Evidence compression": "evidence_compression_ablation",
440
  "Scale stress": "scale_stress",
441
- "Release readiness": "release_readiness",
442
  "Hard mode": "hard_mode",
443
  "Profile benchmark": "profile_benchmark",
444
  "Profile summary": "profile_summary",
@@ -454,7 +426,7 @@ def _eval_table_frame(ws, report: dict[str, Any], label: str) -> pd.DataFrame:
454
  if key == "__compare__":
455
  return _eval_comparison_frame(ws)
456
  if key == "__history__":
457
- return pd.DataFrame(ws.evaluation_history_inventory())
458
  if not key:
459
  return pd.DataFrame()
460
  return _eval_frame(report, key)
@@ -479,16 +451,13 @@ def _eval_comparison_frame(ws) -> pd.DataFrame:
479
  rows.append(
480
  {
481
  "depth": item["level"],
482
- "grade": summary.get("quality_grade", "-"),
483
- "deterministic_score": summary.get("deterministic_quality_score"),
484
  "answer_accuracy": summary.get("answer_accuracy"),
485
- "citation_coverage": summary.get("citation_coverage"),
 
 
486
  "hard_mode_pass": summary.get("hard_mode_pass_rate"),
487
  "text2sql_pass": summary.get("text2sql_pass_rate"),
488
- "release_readiness": summary.get("release_readiness", ""),
489
  "scale_stress_recall@5": summary.get("scale_stress_recall@5"),
490
- "adaptive_context_reduction": summary.get("context_pruning_token_reduction_pct"),
491
- "planner_task_accuracy": summary.get("planner_task_accuracy"),
492
  "pipeline_p50_ms": summary.get("latency_p50_ms"),
493
  "gemini_requests": summary.get("gemini_requests"),
494
  "pacing_wait_s": round(float(summary.get("pacing_sleep_ms", 0.0) or 0.0) / 1000, 1),
@@ -501,6 +470,24 @@ def _eval_comparison_frame(ws) -> pd.DataFrame:
501
  return pd.DataFrame(rows)
502
 
503
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  def _saved_eval_status(ws) -> str:
505
  inventory = ws.evaluation_inventory()
506
  if not inventory:
@@ -509,7 +496,7 @@ def _saved_eval_status(ws) -> str:
509
  for item in inventory:
510
  stale = "" if item.get("current_corpus") else " (stale corpus)"
511
  run_id = item.get("run_id") or "legacy"
512
- parts.append(f"`{item['level']}` grade {item.get('grade', '-')} - run `{run_id}`{stale}")
513
  return "**Saved runs:** " + " - ".join(parts)
514
 
515
 
@@ -532,33 +519,20 @@ def build_ui() -> gr.Blocks:
532
  gr.HTML(
533
  """
534
  <div id="hero" class="hero-shell">
535
- <div class="hero-kicker">RAGForge v2.0.0 - Final portfolio release</div>
536
- <div class="hero-title">Evidence-first RAG for documents, tables and the web</div>
537
  <div class="hero-subtitle">
538
- Hybrid retrieval, semantic planning, analytical synthesis, read-only Text2SQL, conditional web research,
539
- evidence citations, adaptive context budgets and a built-in evaluation harness in one Hugging Face Space.
540
  </div>
541
  <div class="hero-badges">
542
- <span class="hero-badge">Auto + Balanced recommended</span>
543
- <span class="hero-badge">Adaptive 2-5 chunk budgets</span>
544
- <span class="hero-badge">[D#] [T#] [W#] citations</span>
545
- <span class="hero-badge">Quick / Standard / Deep evals</span>
546
- <span class="hero-badge">FastAPI + Gradio + LangGraph</span>
547
  </div>
548
  </div>
549
  """
550
  )
551
- gr.HTML(
552
- """
553
- <div class="baseline-grid">
554
- <div class="baseline-card"><div class="baseline-label">Verified answer accuracy</div><div class="baseline-value">100%</div></div>
555
- <div class="baseline-card"><div class="baseline-label">Verified Recall@5</div><div class="baseline-value">100%</div></div>
556
- <div class="baseline-card"><div class="baseline-label">Synthetic scale stress</div><div class="baseline-value">1,724 chunks</div></div>
557
- <div class="baseline-card"><div class="baseline-label">Pre-final readiness</div><div class="baseline-value">READY</div></div>
558
- </div>
559
- <div class="section-note">Verified on the bundled v1.9 Standard benchmark. v2.0 keeps the measured retrieval/runtime architecture and finalizes benchmark alignment, UI, documentation and release tooling.</div>
560
- """
561
- )
562
 
563
  with gr.Tabs():
564
  with gr.Tab("Chat"):
@@ -862,19 +836,9 @@ def build_ui() -> gr.Blocks:
862
 
863
  with gr.Tab("Evaluation"):
864
  gr.Markdown(
865
- "### Evaluation lab\n"
866
- "RAGForge evaluates retrieval, routing, analytical synthesis, citations, abstention, Text2SQL, hard-mode robustness, "
867
- "context economics and scale stress against the transparent benchmark in `evals/demo_benchmark.json`."
868
- )
869
- gr.HTML(
870
- """
871
- <div class="baseline-grid">
872
- <div class="baseline-card"><div class="baseline-label">Verified v1.9 grade</div><div class="baseline-value">A / 0.994</div></div>
873
- <div class="baseline-card"><div class="baseline-label">Citations</div><div class="baseline-value">100% / 100%</div></div>
874
- <div class="baseline-card"><div class="baseline-label">Context reduction</div><div class="baseline-value">41% + 58%</div></div>
875
- <div class="baseline-card"><div class="baseline-label">20x stress recall</div><div class="baseline-value">100%</div></div>
876
- </div>
877
- """
878
  )
879
  eval_level = gr.Radio(
880
  ["Quick", "Standard", "Deep"],
@@ -923,7 +887,7 @@ def build_ui() -> gr.Blocks:
923
  )
924
  eval_btn = gr.Button("Run evaluation", variant="primary")
925
  eval_status = gr.Markdown("Ready to evaluate.", elem_classes=["status-line"])
926
- eval_scorecard = gr.Markdown("*Run an evaluation to see the score card.*")
927
  eval_diagnostics = gr.Markdown("*Diagnostics appear after an evaluation run.*")
928
  with gr.Accordion("Saved evaluation runs", open=True):
929
  eval_saved_status = gr.Markdown("*No saved evaluation runs for this workspace yet.*")
@@ -951,7 +915,7 @@ def build_ui() -> gr.Blocks:
951
  eval_compression = gr.Dataframe(interactive=False, wrap=True)
952
  with gr.Tab("Scale stress"):
953
  eval_scale_stress = gr.Dataframe(interactive=False, wrap=True)
954
- with gr.Tab("Release readiness"):
955
  eval_readiness = gr.Dataframe(interactive=False, wrap=True)
956
  with gr.Tab("Hard mode"):
957
  eval_hard = gr.Dataframe(interactive=False, wrap=True)
@@ -1046,7 +1010,7 @@ def build_ui() -> gr.Blocks:
1046
  _eval_frame(report, "node_latency"),
1047
  _eval_frame(report, "abstention"),
1048
  _eval_comparison_frame(ws),
1049
- pd.DataFrame(ws.evaluation_history_inventory()),
1050
  pretty_json(report),
1051
  _saved_eval_status(ws),
1052
  status_text,
@@ -1312,7 +1276,7 @@ The bundled benchmark evaluates separate failure surfaces rather than relying on
1312
  - saved runs - Quick/Standard/Deep reports are kept per workspace with corpus/model/benchmark metadata
1313
  - incremental Deep - a compatible Standard baseline can be reused so Deep adds only the sampled judge layer
1314
 
1315
- The letter grade uses quality gates so one weak subsystem cannot be hidden by a high weighted average elsewhere.
1316
  """
1317
  )
1318
 
@@ -1325,6 +1289,6 @@ The letter grade uses quality gates so one weak subsystem cannot be hidden by a
1325
  )
1326
 
1327
  gr.HTML(
1328
- '<div class="footer-note">RAGForge v2.0.0 - final portfolio release - evidence-first retrieval, transparent evaluation, explicit limitations.</div>'
1329
  )
1330
  return demo
 
21
 
22
  CSS = """
23
  #hero {max-width: 1220px; margin: 0 auto 14px auto;}
24
+ .hero-shell {padding: 20px 22px; border: 1px solid rgba(128,128,128,.22); border-radius: 14px; background: rgba(128,128,128,.025);}
25
  .hero-kicker {font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; opacity: .72;}
26
  .hero-title {font-size: 2.3rem; line-height: 1.05; font-weight: 780; margin: 5px 0 8px 0;}
27
  .hero-subtitle {font-size: 1rem; line-height: 1.55; max-width: 900px; opacity: .84;}
 
45
  .latency-track {height: 9px; border-radius: 999px; background: rgba(128,128,128,.18); overflow: hidden;}
46
  .latency-fill {height: 100%; min-width: 2px; border-radius: 999px; background: var(--primary-500, currentColor);}
47
  .latency-time {font-size: .84rem; text-align: right; opacity: .8;}
48
+ .eval-summary {margin: 8px 0 14px 0; padding: 14px 16px; border: 1px solid rgba(128,128,128,.20); border-radius: 12px; background: rgba(128,128,128,.025);}
49
+ .eval-summary-title {font-size: 1.08rem; font-weight: 680; margin-bottom: 3px;}
50
+ .eval-summary-scope {font-size: .86rem; opacity: .70; line-height: 1.45; margin-bottom: 10px;}
51
+ .eval-summary-table {display: grid; grid-template-columns: minmax(140px, 190px) 1fr; row-gap: 7px; column-gap: 14px;}
52
+ .eval-summary-row {display: contents;}
53
+ .eval-summary-label {font-size: .86rem; font-weight: 620; opacity: .76;}
54
+ .eval-summary-value {font-size: .90rem; line-height: 1.4;}
55
  .footer-note {text-align: center; opacity: .68; font-size: .82rem; padding: 15px 0 4px 0;}
56
+ @media (max-width: 900px) {.eval-summary-table {grid-template-columns: 1fr;} .eval-summary-row {display: block; margin-bottom: 8px;}}
57
  """
58
 
59
 
 
205
 
206
 
207
  def _eval_summary_markdown(report: dict[str, Any]) -> str:
208
+ """Render a scoped benchmark summary without marketing-style grades or badges."""
209
  summary = report.get("summary", {}) if report else {}
210
  if not summary:
211
+ return '<div class="eval-summary">Run an evaluation to see a summary.</div>'
212
+
213
+ level = str(summary.get("evaluation_level", "Evaluation"))
214
+ qa = report.get("focused_qa", []) or []
215
+ planner = report.get("semantic_planner", []) or []
216
+ overviews = report.get("corpus_overviews", []) or []
217
+ sql = report.get("text2sql", []) or []
218
+ hard = report.get("hard_mode", []) or []
219
+
220
+ qa_pass = sum(bool(row.get("answer_key_match")) for row in qa)
221
+ route_pass = sum(bool(row.get("route_correct")) for row in planner)
222
+ task_pass = sum(bool(row.get("task_correct")) for row in planner)
223
+ strategy_pass = sum(bool(row.get("strategy_correct")) for row in planner)
224
+ overview_pass = sum(bool(row.get("pass")) for row in overviews)
225
+ sql_pass = sum(bool(row.get("answer_key_match")) for row in sql)
226
+ hard_pass = sum(bool(row.get("pass")) for row in hard)
227
+
228
+ rows = [
229
+ ("Focused QA", f"{qa_pass}/{len(qa)} passed" if qa else "not run"),
230
+ ("Planner", (f"route {route_pass}/{len(planner)}, task {task_pass}/{len(planner)}, strategy {strategy_pass}/{len(planner)}" if planner else "not run")),
231
+ ("Corpus overview", f"{overview_pass}/{len(overviews)} passed" if overviews else "not run"),
232
+ ("Text2SQL", f"{sql_pass}/{len(sql)} passed" if sql else "not run"),
233
+ ("Hard mode", f"{hard_pass}/{len(hard)} passed" if hard else "not run"),
234
+ ("Retrieval", f"Recall@5 {float(summary.get('source_recall@5', 0.0)):.0%}; Precision@5 {float(summary.get('source_precision@5', 0.0)):.0%}"),
235
+ ("Service latency", f"p50 {float(summary.get('latency_p50_ms', 0.0)) / 1000:.2f}s; p95 {float(summary.get('latency_p95_ms', 0.0)) / 1000:.2f}s"),
236
+ ("Gemini requests", f"{int(summary.get('gemini_requests', 0) or 0)} at {int(summary.get('evaluation_target_rpm', 0) or 0)} RPM pacing"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  ]
238
+
239
+ if level != "Quick":
240
+ adaptive_target = float(summary.get("adaptive_context_target_p50", 0.0) or 0.0)
241
+ before = float(summary.get("focused_context_tokens_before_p50", 0.0) or 0.0)
242
+ after = float(summary.get("focused_context_tokens_after_p50", 0.0) or 0.0)
243
+ compression_cases = report.get("evidence_compression_ablation", []) or []
244
+ compressed = next((r for r in compression_cases if r.get("configuration") == "Adaptive + sentence compression"), {})
245
+ retention = float(compressed.get("answer_signal_retention", 0.0) or 0.0)
246
+ compression_n = int(compressed.get("cases", 0) or 0)
247
+ scale_chunks = int(summary.get("scale_stress_max_chunks", 0) or 0)
248
+ scale_recall = float(summary.get("scale_stress_recall@5", 0.0) or 0.0)
249
+ rows.extend([
250
+ ("Context policy", f"median target {adaptive_target:.0f} chunks; estimated focused context {before:.0f} -> {after:.0f} tokens"),
251
+ ("Sentence compression", f"labeled answer signal retained in {int(round(retention * compression_n))}/{compression_n} cases" if compression_n else "not run"),
252
+ ("Synthetic scale stress", f"{scale_chunks:,} chunks; Recall@5 {scale_recall:.0%}" if scale_chunks else "not run"),
 
 
 
253
  ])
254
+ else:
255
+ rows.append(("Extended ablations", "not run in Quick; use Standard for context, compression, and scale-stress checks"))
 
 
 
 
 
 
 
256
 
257
+ body = ''.join(
258
+ f'<div class="eval-summary-row"><div class="eval-summary-label">{html.escape(label)}</div>'
259
+ f'<div class="eval-summary-value">{html.escape(value)}</div></div>'
260
+ for label, value in rows
261
+ )
262
+ scope = (
263
+ "Smoke-test subset of the bundled demo benchmark."
264
+ if level == "Quick"
265
+ else "Bundled demo benchmark; scale stress uses synthetic distractor copies and is not a claim of enterprise-scale accuracy."
266
+ )
267
+ return (
268
+ '<div class="eval-summary">'
269
+ f'<div class="eval-summary-title">{html.escape(level)} evaluation</div>'
270
+ f'<div class="eval-summary-scope">{html.escape(scope)}</div>'
271
+ f'<div class="eval-summary-table">{body}</div>'
272
+ '</div>'
273
+ )
274
 
275
  def _eval_diagnostics_markdown(report: dict[str, Any]) -> str:
276
  diagnostics = report.get("diagnostics", []) if report else []
 
333
  settings = get_settings()
334
  stats = ws.health_snapshot()
335
  runtime_json = {
336
+ "ragforge_version": "2.0.1",
337
  "workspace": stats,
338
  "models": {
339
  "generation": settings.default_model,
 
354
  }
355
  runtime = (
356
  "### Live runtime\n"
357
+ f"**RAGForge:** `v2.0.1` - **workspace:** `{sid[:12]}...` - **status:** `{stats['status']}`\n\n"
358
  f"**Corpus:** `{stats['sources']}` sources - `{stats['chunks']}` chunks - "
359
  f"`{stats['source_profiles']}` source profiles - `{stats['tables']}` tables - "
360
  f"corpus version `{stats['version']}`\n\n"
 
410
  "Context budget ablation": "context_budget_ablation",
411
  "Evidence compression": "evidence_compression_ablation",
412
  "Scale stress": "scale_stress",
413
+ "Acceptance checks": "release_readiness",
414
  "Hard mode": "hard_mode",
415
  "Profile benchmark": "profile_benchmark",
416
  "Profile summary": "profile_summary",
 
426
  if key == "__compare__":
427
  return _eval_comparison_frame(ws)
428
  if key == "__history__":
429
+ return _eval_history_frame(ws)
430
  if not key:
431
  return pd.DataFrame()
432
  return _eval_frame(report, key)
 
451
  rows.append(
452
  {
453
  "depth": item["level"],
 
 
454
  "answer_accuracy": summary.get("answer_accuracy"),
455
+ "source_recall@5": summary.get("source_recall@5"),
456
+ "source_precision@5": summary.get("source_precision@5"),
457
+ "planner_strategy_accuracy": summary.get("planner_strategy_accuracy"),
458
  "hard_mode_pass": summary.get("hard_mode_pass_rate"),
459
  "text2sql_pass": summary.get("text2sql_pass_rate"),
 
460
  "scale_stress_recall@5": summary.get("scale_stress_recall@5"),
 
 
461
  "pipeline_p50_ms": summary.get("latency_p50_ms"),
462
  "gemini_requests": summary.get("gemini_requests"),
463
  "pacing_wait_s": round(float(summary.get("pacing_sleep_ms", 0.0) or 0.0) / 1000, 1),
 
470
  return pd.DataFrame(rows)
471
 
472
 
473
+
474
+ def _eval_history_frame(ws) -> pd.DataFrame:
475
+ rows = []
476
+ for row in ws.evaluation_history_inventory():
477
+ rows.append({
478
+ "saved_at": row.get("saved_at"),
479
+ "level": row.get("level"),
480
+ "benchmark": row.get("benchmark"),
481
+ "model": row.get("model"),
482
+ "workspace_version": row.get("workspace_version"),
483
+ "citation_coverage": row.get("citation_coverage"),
484
+ "hard_mode_pass": row.get("hard_mode_pass"),
485
+ "p50_ms": row.get("p50_ms"),
486
+ "gemini_requests": row.get("gemini_requests"),
487
+ "run_id": row.get("run_id"),
488
+ })
489
+ return pd.DataFrame(rows)
490
+
491
  def _saved_eval_status(ws) -> str:
492
  inventory = ws.evaluation_inventory()
493
  if not inventory:
 
496
  for item in inventory:
497
  stale = "" if item.get("current_corpus") else " (stale corpus)"
498
  run_id = item.get("run_id") or "legacy"
499
+ parts.append(f"`{item['level']}` - run `{run_id}`{stale}")
500
  return "**Saved runs:** " + " - ".join(parts)
501
 
502
 
 
519
  gr.HTML(
520
  """
521
  <div id="hero" class="hero-shell">
522
+ <div class="hero-title">RAGForge</div>
 
523
  <div class="hero-subtitle">
524
+ Search and analyze documents, structured tables, and web sources in one workspace.
525
+ RAGForge routes each question to the appropriate retrieval path and keeps the supporting sources and execution trace visible.
526
  </div>
527
  <div class="hero-badges">
528
+ <span class="hero-badge">Hybrid document search</span>
529
+ <span class="hero-badge">Read-only Text2SQL</span>
530
+ <span class="hero-badge">Conditional web search</span>
531
+ <span class="hero-badge">Built-in evaluation</span>
 
532
  </div>
533
  </div>
534
  """
535
  )
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  with gr.Tabs():
538
  with gr.Tab("Chat"):
 
836
 
837
  with gr.Tab("Evaluation"):
838
  gr.Markdown(
839
+ "### Evaluation\n"
840
+ "Run the bundled benchmark to inspect retrieval, routing, grounding, SQL behavior, robustness, latency, and context policies. "
841
+ "The results describe this demo benchmark only; they are not general accuracy claims."
 
 
 
 
 
 
 
 
 
 
842
  )
843
  eval_level = gr.Radio(
844
  ["Quick", "Standard", "Deep"],
 
887
  )
888
  eval_btn = gr.Button("Run evaluation", variant="primary")
889
  eval_status = gr.Markdown("Ready to evaluate.", elem_classes=["status-line"])
890
+ eval_scorecard = gr.HTML('<div class="eval-summary">Run an evaluation to see a summary.</div>')
891
  eval_diagnostics = gr.Markdown("*Diagnostics appear after an evaluation run.*")
892
  with gr.Accordion("Saved evaluation runs", open=True):
893
  eval_saved_status = gr.Markdown("*No saved evaluation runs for this workspace yet.*")
 
915
  eval_compression = gr.Dataframe(interactive=False, wrap=True)
916
  with gr.Tab("Scale stress"):
917
  eval_scale_stress = gr.Dataframe(interactive=False, wrap=True)
918
+ with gr.Tab("Acceptance checks"):
919
  eval_readiness = gr.Dataframe(interactive=False, wrap=True)
920
  with gr.Tab("Hard mode"):
921
  eval_hard = gr.Dataframe(interactive=False, wrap=True)
 
1010
  _eval_frame(report, "node_latency"),
1011
  _eval_frame(report, "abstention"),
1012
  _eval_comparison_frame(ws),
1013
+ _eval_history_frame(ws),
1014
  pretty_json(report),
1015
  _saved_eval_status(ws),
1016
  status_text,
 
1276
  - saved runs - Quick/Standard/Deep reports are kept per workspace with corpus/model/benchmark metadata
1277
  - incremental Deep - a compatible Standard baseline can be reused so Deep adds only the sampled judge layer
1278
 
1279
+ Detailed metrics and acceptance checks are shown separately so a single aggregate score does not hide subsystem behavior.
1280
  """
1281
  )
1282
 
 
1289
  )
1290
 
1291
  gr.HTML(
1292
+ '<div class="footer-note">RAGForge - document, table, and web retrieval with inspectable sources and evaluation.</div>'
1293
  )
1294
  return demo
tests/test_eval_metrics.py CHANGED
@@ -37,6 +37,10 @@ def test_answer_key_supports_all_and_any():
37
  assert answer_key_match("Govern, Map, Measure, Manage", {"expected_all": ["govern", "map", "measure", "manage"]})
38
  assert answer_key_match("Enterprise is fastest", {"expected_any": ["enterprise", "business"]})
39
  assert not answer_key_match("Team is fastest", {"expected_any": ["enterprise"]})
 
 
 
 
40
 
41
 
42
  def test_percentile_interpolates():
 
37
  assert answer_key_match("Govern, Map, Measure, Manage", {"expected_all": ["govern", "map", "measure", "manage"]})
38
  assert answer_key_match("Enterprise is fastest", {"expected_any": ["enterprise", "business"]})
39
  assert not answer_key_match("Team is fastest", {"expected_any": ["enterprise"]})
40
+ # Numeric answer labels must not match inside a different number. This caught
41
+ # a real regression where "15 minutes" passed a "5 min" answer key.
42
+ assert answer_key_match("The target is 5 minutes.", {"expected_any": ["5 minutes", "5 min"]})
43
+ assert not answer_key_match("The target is 15 minutes.", {"expected_any": ["5 minutes", "5 min"]})
44
 
45
 
46
  def test_percentile_interpolates():
tests/test_evaluation_assets.py CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
5
  def test_demo_benchmark_is_multilayer_and_auditable():
6
  path = Path("evals/demo_benchmark.json")
7
  data = json.loads(path.read_text(encoding="utf-8"))
8
- assert data["version"] == "2.0"
9
  assert len(data["qa_cases"]) >= 9
10
  assert len(data["planner_cases"]) >= 10
11
  assert len(data["overview_cases"]) >= 2
@@ -49,7 +49,7 @@ def test_demo_evaluation_and_introspection_are_available_through_api():
49
  assert "/api/v1/session/{session_id}" in text
50
  assert "/api/v1/evaluation/saved/{session_id}" in text
51
  assert "/api/v1/evaluation/saved/{session_id}/{level}" in text
52
- assert 'version="2.0.0"' in text
53
 
54
 
55
  def test_v15_evaluation_cache_and_incremental_deep_are_present():
 
5
  def test_demo_benchmark_is_multilayer_and_auditable():
6
  path = Path("evals/demo_benchmark.json")
7
  data = json.loads(path.read_text(encoding="utf-8"))
8
+ assert data["version"] == "2.0.1"
9
  assert len(data["qa_cases"]) >= 9
10
  assert len(data["planner_cases"]) >= 10
11
  assert len(data["overview_cases"]) >= 2
 
49
  assert "/api/v1/session/{session_id}" in text
50
  assert "/api/v1/evaluation/saved/{session_id}" in text
51
  assert "/api/v1/evaluation/saved/{session_id}/{level}" in text
52
+ assert 'version="2.0.1"' in text
53
 
54
 
55
  def test_v15_evaluation_cache_and_incremental_deep_are_present():
tests/test_ui_copy.py CHANGED
@@ -31,7 +31,7 @@ def test_ui_exposes_layered_evaluation_and_diagnostics():
31
  assert label in text
32
  assert '["Quick", "Standard", "Deep"]' in text
33
  assert "Diagnostic findings" in text
34
- assert "Quality gates affecting the grade" in text
35
 
36
 
37
  def test_architecture_api_tab_is_interactive_and_live():
@@ -48,13 +48,13 @@ def test_ui_has_quota_safe_evaluation_controls_and_score_card_spacing():
48
  text = Path("src/ragforge/ui.py").read_text(encoding="utf-8")
49
  assert "Quota-safe pacing" in text
50
  assert "Target Gemini requests per minute" in text
51
- assert "Evaluation score card" in text
52
- assert "Evaluation scorecard" not in text
53
 
54
 
55
  def test_architecture_snapshot_returns_complete_runtime_payload():
56
  text = Path("src/ragforge/ui.py").read_text(encoding="utf-8")
57
- assert '"ragforge_version": "2.0.0"' in text
58
  assert "return sid, runtime, curl, runtime_json" in text
59
  assert "curl = f\ndef _eval_frame" not in text
60
 
 
31
  assert label in text
32
  assert '["Quick", "Standard", "Deep"]' in text
33
  assert "Diagnostic findings" in text
34
+ assert "Diagnostic findings" in text
35
 
36
 
37
  def test_architecture_api_tab_is_interactive_and_live():
 
48
  text = Path("src/ragforge/ui.py").read_text(encoding="utf-8")
49
  assert "Quota-safe pacing" in text
50
  assert "Target Gemini requests per minute" in text
51
+ assert "Run an evaluation to see a summary" in text
52
+ assert "Evaluation score card" not in text
53
 
54
 
55
  def test_architecture_snapshot_returns_complete_runtime_payload():
56
  text = Path("src/ragforge/ui.py").read_text(encoding="utf-8")
57
+ assert '"ragforge_version": "2.0.1"' in text
58
  assert "return sid, runtime, curl, runtime_json" in text
59
  assert "curl = f\ndef _eval_frame" not in text
60
 
tests/test_v16_features.py CHANGED
@@ -4,7 +4,7 @@ from pathlib import Path
4
 
5
  def test_v16_benchmark_has_hard_mode_and_insight_plans():
6
  data = json.loads(Path("evals/demo_benchmark.json").read_text(encoding="utf-8"))
7
- assert data["version"] == "2.0"
8
  assert len(data.get("hard_mode_cases", [])) >= 8
9
  insight = [c for c in data["planner_cases"] if c.get("task") == "insight_synthesis"]
10
  assert insight
 
4
 
5
  def test_v16_benchmark_has_hard_mode_and_insight_plans():
6
  data = json.loads(Path("evals/demo_benchmark.json").read_text(encoding="utf-8"))
7
+ assert data["version"] == "2.0.1"
8
  assert len(data.get("hard_mode_cases", [])) >= 8
9
  insight = [c for c in data["planner_cases"] if c.get("task") == "insight_synthesis"]
10
  assert insight
tests/test_v19_features.py CHANGED
@@ -112,7 +112,7 @@ def test_v19_evaluation_contains_scale_stress_compression_and_readiness():
112
  assert '"evidence_compression_ablation": compression_rows' in text
113
  assert '"release_readiness": readiness_rows' in text
114
  assert '"scale_stress": scale_stress_rows' in text
115
- assert "v1.9 adaptive budget" in text
116
 
117
 
118
  def test_v19_api_and_ui_surface_operational_diagnostics_and_new_eval_tabs():
@@ -121,7 +121,7 @@ def test_v19_api_and_ui_surface_operational_diagnostics_and_new_eval_tabs():
121
  assert "/api/v1/session/{session_id}/diagnostics" in api
122
  assert 'with gr.Tab("Evidence compression")' in ui
123
  assert 'with gr.Tab("Scale stress")' in ui
124
- assert 'with gr.Tab("Release readiness")' in ui
125
  assert "Adaptive retrieval depth" in ui
126
  assert "Focused evidence sentence compression" in ui
127
 
 
112
  assert '"evidence_compression_ablation": compression_rows' in text
113
  assert '"release_readiness": readiness_rows' in text
114
  assert '"scale_stress": scale_stress_rows' in text
115
+ assert "Adaptive budget" in text
116
 
117
 
118
  def test_v19_api_and_ui_surface_operational_diagnostics_and_new_eval_tabs():
 
121
  assert "/api/v1/session/{session_id}/diagnostics" in api
122
  assert 'with gr.Tab("Evidence compression")' in ui
123
  assert 'with gr.Tab("Scale stress")' in ui
124
+ assert 'with gr.Tab("Acceptance checks")' in ui
125
  assert "Adaptive retrieval depth" in ui
126
  assert "Focused evidence sentence compression" in ui
127
 
tests/test_v20_final.py CHANGED
@@ -4,19 +4,19 @@ from pathlib import Path
4
  ROOT = Path(__file__).resolve().parents[1]
5
 
6
 
7
- def test_v20_versions_and_final_release_assets_are_aligned():
8
- assert 'version = "2.0.0"' in (ROOT / "pyproject.toml").read_text(encoding="utf-8")
9
- assert '__version__ = "2.0.0"' in (ROOT / "src/ragforge/__init__.py").read_text(encoding="utf-8")
10
- assert 'version="2.0.0"' in (ROOT / "src/ragforge/api.py").read_text(encoding="utf-8")
11
  benchmark = json.loads((ROOT / "evals/demo_benchmark.json").read_text(encoding="utf-8"))
12
- assert benchmark["version"] == "2.0"
13
  assert (ROOT / "CHANGELOG.md").is_file()
14
  assert (ROOT / "docs/FINAL_RESULTS.md").is_file()
15
  assert (ROOT / "docs/PORTFOLIO_GUIDE.md").is_file()
16
  assert (ROOT / "scripts/release_check.py").is_file()
17
 
18
 
19
- def test_v20_multihop_hard_case_uses_recommended_semantic_path():
20
  benchmark = json.loads((ROOT / "evals/demo_benchmark.json").read_text(encoding="utf-8"))
21
  case = next(c for c in benchmark["hard_mode_cases"] if c["id"] == "hard_multihop_time_compare")
22
  assert case["mode"] == "Auto"
@@ -26,7 +26,13 @@ def test_v20_multihop_hard_case_uses_recommended_semantic_path():
26
  assert case["expected_strategy"] == "hierarchical"
27
 
28
 
29
- def test_v20_hard_mode_evaluator_honors_case_profile_and_plan_expectations():
 
 
 
 
 
 
30
  text = (ROOT / "src/ragforge/evaluation.py").read_text(encoding="utf-8")
31
  assert 'case_mode = str(case.get("mode") or "Documents")' in text
32
  assert 'case_profile = str(case.get("profile") or "Fast")' in text
@@ -35,23 +41,25 @@ def test_v20_hard_mode_evaluator_honors_case_profile_and_plan_expectations():
35
  assert '"evaluation_profile": cfg.profile' in text
36
 
37
 
38
- def test_v20_ui_is_productized_and_keeps_short_dash_constraint():
39
  text = (ROOT / "src/ragforge/ui.py").read_text(encoding="utf-8")
40
- assert "Final portfolio release" in text
 
 
 
 
 
 
41
  assert 'with gr.Tab("Chat")' in text
42
- assert "Evaluation lab" in text
43
- assert "Verified answer accuracy" in text
44
- assert "Release readiness" in text
45
- assert "footer-note" in text
46
  assert "—" not in text
47
  assert "–" not in text
48
 
49
 
50
- def test_v20_readme_leads_with_product_and_measured_results_not_release_diary():
51
  text = (ROOT / "README.md").read_text(encoding="utf-8")
52
- assert "Verified pre-final benchmark" in text
53
- assert "Core capabilities" in text
54
- assert "Recommended demo flow" in text
55
- assert "Final project status" in text
56
- assert "What is new in v1.9" not in text
57
  assert "CHANGELOG.md" in text
 
4
  ROOT = Path(__file__).resolve().parents[1]
5
 
6
 
7
+ def test_versions_and_release_assets_are_aligned():
8
+ assert 'version = "2.0.1"' in (ROOT / "pyproject.toml").read_text(encoding="utf-8")
9
+ assert '__version__ = "2.0.1"' in (ROOT / "src/ragforge/__init__.py").read_text(encoding="utf-8")
10
+ assert 'version="2.0.1"' in (ROOT / "src/ragforge/api.py").read_text(encoding="utf-8")
11
  benchmark = json.loads((ROOT / "evals/demo_benchmark.json").read_text(encoding="utf-8"))
12
+ assert benchmark["version"] == "2.0.1"
13
  assert (ROOT / "CHANGELOG.md").is_file()
14
  assert (ROOT / "docs/FINAL_RESULTS.md").is_file()
15
  assert (ROOT / "docs/PORTFOLIO_GUIDE.md").is_file()
16
  assert (ROOT / "scripts/release_check.py").is_file()
17
 
18
 
19
+ def test_multihop_hard_case_uses_recommended_semantic_path():
20
  benchmark = json.loads((ROOT / "evals/demo_benchmark.json").read_text(encoding="utf-8"))
21
  case = next(c for c in benchmark["hard_mode_cases"] if c["id"] == "hard_multihop_time_compare")
22
  assert case["mode"] == "Auto"
 
26
  assert case["expected_strategy"] == "hierarchical"
27
 
28
 
29
+ def test_source_localization_planner_case_accepts_two_reasonable_strategies():
30
+ benchmark = json.loads((ROOT / "evals/demo_benchmark.json").read_text(encoding="utf-8"))
31
+ case = next(c for c in benchmark["planner_cases"] if c["id"] == "plan_find_refund_file")
32
+ assert set(case["strategy_any"]) == {"semantic", "hierarchical"}
33
+
34
+
35
+ def test_hard_mode_evaluator_honors_case_profile_and_plan_expectations():
36
  text = (ROOT / "src/ragforge/evaluation.py").read_text(encoding="utf-8")
37
  assert 'case_mode = str(case.get("mode") or "Documents")' in text
38
  assert 'case_profile = str(case.get("profile") or "Fast")' in text
 
41
  assert '"evaluation_profile": cfg.profile' in text
42
 
43
 
44
+ def test_ui_reads_like_a_product_not_a_release_dashboard():
45
  text = (ROOT / "src/ragforge/ui.py").read_text(encoding="utf-8")
46
+ assert '<div class="hero-title">RAGForge</div>' in text
47
+ assert "Final portfolio release" not in text
48
+ assert "Pre-final readiness" not in text
49
+ assert "Verified v1.9 grade" not in text
50
+ assert "[D#] [T#] [W#] citations" not in text
51
+ assert "Acceptance checks" in text
52
+ assert "The results describe this demo benchmark only; they are not general accuracy claims." in text
53
  assert 'with gr.Tab("Chat")' in text
 
 
 
 
54
  assert "—" not in text
55
  assert "–" not in text
56
 
57
 
58
+ def test_readme_leads_with_product_and_scoped_evaluation():
59
  text = (ROOT / "README.md").read_text(encoding="utf-8")
60
+ assert text.count("# RAGForge") == 1
61
+ assert "final portfolio release" not in text.lower()
62
+ assert "Interpreting the benchmark" in text
63
+ assert "not for making claims about general enterprise performance" in text
64
+ assert "15 minutes" in text and "5 minutes" in text
65
  assert "CHANGELOG.md" in text