Spaces:
Running
Running
Commit ·
3f1f29b
1
Parent(s): 8cfc5e2
Polish RAGForge UI and fix evaluation matching
Browse files- CHANGELOG.md +35 -60
- README.md +157 -204
- docs/EVALUATION.md +65 -320
- docs/FEATURE_MATRIX.md +1 -1
- docs/FINAL_RESULTS.md +45 -70
- docs/MIGRATION_2.0.1.md +21 -0
- docs/PORTFOLIO_GUIDE.md +39 -69
- docs/RESUME_BULLETS.md +2 -2
- docs/architecture.mmd +1 -1
- evals/README.md +4 -4
- evals/demo_benchmark.json +7 -3
- pyproject.toml +1 -1
- scripts/release_check.py +17 -12
- src/ragforge/__init__.py +1 -1
- src/ragforge/api.py +1 -1
- src/ragforge/eval_metrics.py +24 -5
- src/ragforge/evaluation.py +18 -13
- src/ragforge/ui.py +111 -147
- tests/test_eval_metrics.py +4 -0
- tests/test_evaluation_assets.py +2 -2
- tests/test_ui_copy.py +4 -4
- tests/test_v16_features.py +1 -1
- tests/test_v19_features.py +2 -2
- tests/test_v20_final.py +27 -19
CHANGELOG.md
CHANGED
|
@@ -1,84 +1,59 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
- context/prompt telemetry
|
| 26 |
-
- grounded-absence hardening
|
| 27 |
-
- zero-Gemini context-budget ablation
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
-
- grounded absence and improved citation scoring/repair
|
| 33 |
-
- evaluation run provenance
|
| 34 |
-
- measured small-corpus reranker skip policy
|
| 35 |
|
| 36 |
-
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
- `[T#]` table citations
|
| 40 |
-
- harder robustness benchmark
|
| 41 |
-
- chunk-level reranker labels
|
| 42 |
-
- optional profile benchmark and node latency summaries
|
| 43 |
|
| 44 |
-
|
| 45 |
|
| 46 |
-
|
| 47 |
-
- incremental Deep evaluation
|
| 48 |
-
- typed Text2SQL scoring
|
| 49 |
-
- zero-call citation repair
|
| 50 |
-
- adaptive reranking
|
| 51 |
|
| 52 |
-
|
| 53 |
|
| 54 |
-
|
| 55 |
-
- fixed retrieval AP metrics
|
| 56 |
-
- API/architecture runtime inspection
|
| 57 |
-
- cache-bypassed evaluation timing
|
| 58 |
|
| 59 |
-
|
| 60 |
|
| 61 |
-
|
| 62 |
-
- reranker ablation
|
| 63 |
-
- optional Deep judge
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
-
|
| 68 |
-
- lazy demo recovery
|
| 69 |
-
- explicit abstention and workspace preflight
|
| 70 |
-
- indexing/query progress improvements
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
-
- source-profile index
|
| 76 |
-
- global/hierarchical retrieval
|
| 77 |
-
- correction before conditional web fallback
|
| 78 |
|
| 79 |
-
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
-
|
| 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 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 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 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 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 |
-
|
| 54 |
|
| 55 |
-
##
|
| 56 |
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
|
| 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 |
-
|
| 81 |
|
| 82 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
|
| 95 |
-
|
| 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 |
-
|
| 103 |
|
| 104 |
-
|
| 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 |
-
##
|
| 114 |
|
| 115 |
-
PDF, TXT, Markdown, DOCX, PPTX, CSV, XLS/XLSX, JSON, HTML, XML/YAML, code/text formats,
|
| 116 |
|
| 117 |
-
ZIP ingestion includes path-traversal, file-count, size, and extension
|
| 118 |
|
| 119 |
## Architecture
|
| 120 |
|
| 121 |
```mermaid
|
| 122 |
flowchart TD
|
| 123 |
-
U[User
|
| 124 |
-
G --> P{
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
R -->|global| GB[Source-balanced retrieval]
|
| 131 |
-
R -->|hierarchical| SP[Source-profile
|
| 132 |
-
R -->|analytical| AN[Documents +
|
| 133 |
SP --> C
|
| 134 |
|
| 135 |
-
C --> D[Dense
|
| 136 |
C --> B[BM25]
|
| 137 |
D --> F[RRF]
|
| 138 |
B --> F
|
| 139 |
-
F --> RR
|
| 140 |
RR --> RD[Scale-aware retrieval depth]
|
| 141 |
-
RD --> CB
|
| 142 |
-
CB --> EC[Focused
|
| 143 |
|
| 144 |
-
GB --> EG{Evidence
|
| 145 |
AN --> EG
|
| 146 |
EC --> EG
|
| 147 |
-
EG -->|
|
| 148 |
-
EG -->|
|
| 149 |
CR --> R
|
| 150 |
-
EG -->|
|
| 151 |
-
EG -->|unsupported
|
| 152 |
W --> A
|
| 153 |
|
| 154 |
-
A --> V
|
| 155 |
-
V -->
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
### Hugging Face Spaces
|
| 175 |
|
| 176 |
-
1. Create a
|
| 177 |
-
2. Copy
|
| 178 |
-
3. Add `GEMINI_API_KEY`
|
| 179 |
4. Optionally add `TAVILY_API_KEY` and `APP_API_TOKEN`.
|
| 180 |
5. Push the repository.
|
| 181 |
-
6. Open the Space
|
| 182 |
-
7.
|
| 183 |
|
| 184 |
-
The
|
| 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 |
-
###
|
| 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
|
| 207 |
|
| 208 |
-
##
|
| 209 |
-
|
| 210 |
-
Start with the bundled corpus and **Auto + Balanced**:
|
| 211 |
|
| 212 |
```text
|
| 213 |
What is the Sev-1 acknowledgement target?
|
| 214 |
```
|
| 215 |
|
| 216 |
-
|
| 217 |
|
| 218 |
```text
|
| 219 |
What is the collection about?
|
| 220 |
```
|
| 221 |
|
| 222 |
-
|
| 223 |
|
| 224 |
```text
|
| 225 |
What exactly does this collection reveal? Identify important trends and caveats.
|
| 226 |
```
|
| 227 |
|
| 228 |
-
|
| 229 |
|
| 230 |
```text
|
| 231 |
Which support tier has the shortest first-response SLA?
|
| 232 |
```
|
| 233 |
|
| 234 |
-
|
| 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 |
-
|
| 243 |
-
|
| 244 |
-
|
| 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 |
-
|
| 271 |
|
| 272 |
## REST API
|
| 273 |
|
| 274 |
FastAPI exposes Swagger at `/docs` and OpenAPI at `/openapi.json`.
|
| 275 |
|
| 276 |
-
|
| 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
|
| 308 |
|
| 309 |
-
RAGForge
|
| 310 |
|
| 311 |
Important limitations:
|
| 312 |
|
| 313 |
-
- workspace
|
| 314 |
-
- browser persistence stores
|
| 315 |
-
-
|
| 316 |
-
-
|
| 317 |
-
-
|
| 318 |
-
- the
|
| 319 |
-
-
|
| 320 |
|
| 321 |
See [`SECURITY.md`](SECURITY.md).
|
| 322 |
|
| 323 |
-
## Repository
|
| 324 |
|
| 325 |
```text
|
| 326 |
src/ragforge/
|
| 327 |
-
pipeline.py
|
| 328 |
-
llm.py Gemini planning
|
| 329 |
retrieval.py dense + BM25 + RRF + reranking
|
| 330 |
context_budget.py adaptive retrieval/context policy
|
| 331 |
-
evidence_compression.py focused
|
| 332 |
-
workspace.py
|
| 333 |
-
evaluation.py benchmark
|
| 334 |
-
stress_eval.py
|
| 335 |
sql_agent.py read-only Text2SQL
|
| 336 |
-
web_search.py external
|
| 337 |
-
security.py ingestion/query/web
|
| 338 |
ui.py Gradio application
|
| 339 |
-
api.py FastAPI
|
| 340 |
|
| 341 |
evals/
|
| 342 |
-
demo_benchmark.json
|
| 343 |
|
| 344 |
docs/
|
| 345 |
-
FINAL_RESULTS.md
|
| 346 |
-
PORTFOLIO_GUIDE.md
|
| 347 |
-
EVALUATION.md
|
| 348 |
-
ARCHITECTURE_API.md
|
| 349 |
-
FEATURE_MATRIX.md
|
| 350 |
-
QUERY_PLANNING.md
|
| 351 |
-
UX_LIFECYCLE.md
|
| 352 |
-
RESUME_BULLETS.md concise project bullets
|
| 353 |
```
|
| 354 |
|
| 355 |
-
|
| 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 |
-
#
|
| 2 |
|
| 3 |
-
|
| 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 |
-
|
| 119 |
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
|
| 124 |
-
|
| 125 |
-
- Hybrid RRF + local cross-encoder reranker
|
| 126 |
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
### Deep
|
| 130 |
|
| 131 |
-
|
| 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 |
-
##
|
| 276 |
|
| 277 |
-
|
| 278 |
|
| 279 |
-
|
| 280 |
|
| 281 |
-
|
| 282 |
|
| 283 |
-
|
| 284 |
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
-
|
| 288 |
|
| 289 |
-
|
| 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 |
-
|
| 296 |
|
| 297 |
-
|
| 298 |
|
| 299 |
-
|
| 300 |
|
| 301 |
-
|
| 302 |
-
- task-taxonomy misses,
|
| 303 |
-
- failed Text2SQL cases,
|
| 304 |
-
- reranker latency with no measurable source-ranking gain.
|
| 305 |
|
| 306 |
-
|
| 307 |
|
| 308 |
-
|
|
|
|
|
|
|
| 309 |
|
| 310 |
-
|
| 311 |
|
| 312 |
-
|
| 313 |
-
POST /api/v1/evaluate/demo
|
| 314 |
-
```
|
| 315 |
|
| 316 |
-
|
|
|
|
|
|
|
| 317 |
|
| 318 |
-
|
| 319 |
|
| 320 |
-
|
| 321 |
-
GET /api/v1/evaluation/benchmark
|
| 322 |
-
```
|
| 323 |
|
| 324 |
-
|
| 325 |
|
| 326 |
-
|
| 327 |
-
GET /api/v1/evaluation/saved/{session_id}
|
| 328 |
-
GET /api/v1/evaluation/saved/{session_id}/{level}
|
| 329 |
-
```
|
| 330 |
|
| 331 |
-
The
|
| 332 |
|
| 333 |
-
|
| 334 |
|
| 335 |
-
|
| 336 |
|
| 337 |
-
|
| 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 |
-
##
|
| 348 |
|
| 349 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
|
| 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 |
-
#
|
| 2 |
-
|
| 3 |
-
This
|
| 4 |
-
|
| 5 |
-
##
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 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 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|---|---:|---:|---:|
|
| 42 |
-
|
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
|
| 47 |
-
|
| 48 |
|
| 49 |
-
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
| 54 |
-
|
|
| 55 |
-
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
|
| 59 |
-
## Reranker
|
| 60 |
|
| 61 |
-
|
| 62 |
|
| 63 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
-
|
| 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 |
-
|
| 79 |
|
| 80 |
-
|
| 81 |
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
| 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 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
| 10 |
|
| 11 |
```text
|
| 12 |
What is the Sev-1 acknowledgement target?
|
| 13 |
```
|
| 14 |
|
| 15 |
-
Show
|
| 16 |
|
| 17 |
-
|
| 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 |
-
###
|
| 44 |
-
|
| 45 |
-
Ask:
|
| 46 |
|
| 47 |
```text
|
| 48 |
What exactly does this collection reveal? Identify important trends and caveats.
|
| 49 |
```
|
| 50 |
|
| 51 |
-
|
| 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 |
-
###
|
| 60 |
-
|
| 61 |
-
Ask:
|
| 62 |
|
| 63 |
```text
|
| 64 |
Which support tier has the shortest first-response SLA?
|
| 65 |
```
|
| 66 |
|
| 67 |
-
Show the SQL
|
| 68 |
-
|
| 69 |
-
Talking point: routing and SQL execution are independently evaluated. The SQL path is restricted to validated read-only queries.
|
| 70 |
|
| 71 |
-
###
|
| 72 |
|
| 73 |
-
Open Standard
|
| 74 |
|
| 75 |
-
-
|
| 76 |
-
-
|
| 77 |
-
-
|
| 78 |
-
- node latency
|
| 79 |
-
-
|
| 80 |
|
| 81 |
-
|
| 82 |
|
| 83 |
-
##
|
| 84 |
|
| 85 |
-
|
| 86 |
|
| 87 |
-
##
|
| 88 |
|
| 89 |
### Reranking
|
| 90 |
|
| 91 |
-
The
|
| 92 |
|
| 93 |
-
### Context
|
| 94 |
|
| 95 |
-
|
| 96 |
|
| 97 |
-
###
|
| 98 |
|
| 99 |
-
A full benchmark can
|
| 100 |
|
| 101 |
-
###
|
| 102 |
|
| 103 |
-
|
| 104 |
|
| 105 |
-
##
|
| 106 |
|
| 107 |
-
-
|
| 108 |
-
- no durable multi-tenant storage
|
| 109 |
-
- prompt-injection filtering is heuristic
|
| 110 |
-
-
|
| 111 |
-
- token
|
| 112 |
-
-
|
| 113 |
|
| 114 |
-
## Resume
|
| 115 |
|
| 116 |
-
> Built
|
|
|
|
| 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 |
-
-
|
| 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
|
| 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 +
|
| 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
|
| 2 |
|
| 3 |
-
The
|
| 4 |
|
| 5 |
-
##
|
| 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
|
| 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.
|
| 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
|
| 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.
|
| 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'
|
| 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, "
|
| 40 |
-
require("
|
| 41 |
-
require("
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
| 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.
|
| 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 |
-
|
| 46 |
-
|
| 47 |
-
|
| 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
|
| 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,
|
| 215 |
-
labels = ("Full top-k", "
|
| 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 |
-
"
|
| 239 |
-
"
|
| 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 == "
|
| 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
|
| 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") == "
|
| 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") == "
|
| 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 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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:
|
| 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-
|
| 49 |
-
.eval-
|
| 50 |
-
.eval-
|
| 51 |
-
.eval-
|
|
|
|
|
|
|
|
|
|
| 52 |
.footer-note {text-align: center; opacity: .68; font-size: .82rem; padding: 15px 0 4px 0;}
|
| 53 |
-
@media (max-width: 900px) {.
|
| 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 "
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
)
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
f"
|
| 227 |
-
|
| 228 |
-
"",
|
| 229 |
-
|
| 230 |
-
f"
|
| 231 |
-
|
| 232 |
-
|
| 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 |
-
|
| 274 |
-
if
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
f"
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
lines.extend([
|
| 289 |
-
"",
|
| 290 |
-
f"**Profile benchmark:** `{int(summary.get('profile_benchmark_cases', 0) or 0)}` profile/case runs included.",
|
| 291 |
])
|
| 292 |
-
|
| 293 |
-
|
| 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.
|
| 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.
|
| 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 |
-
"
|
| 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
|
| 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 |
-
"
|
|
|
|
|
|
|
| 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']}`
|
| 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-
|
| 536 |
-
<div class="hero-title">Evidence-first RAG for documents, tables and the web</div>
|
| 537 |
<div class="hero-subtitle">
|
| 538 |
-
|
| 539 |
-
|
| 540 |
</div>
|
| 541 |
<div class="hero-badges">
|
| 542 |
-
<span class="hero-badge">
|
| 543 |
-
<span class="hero-badge">
|
| 544 |
-
<span class="hero-badge">
|
| 545 |
-
<span class="hero-badge">
|
| 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
|
| 866 |
-
"
|
| 867 |
-
"
|
| 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.
|
| 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("
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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.
|
| 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 "
|
| 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 "
|
| 52 |
-
assert "Evaluation
|
| 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.
|
| 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 "
|
| 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("
|
| 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
|
| 8 |
-
assert 'version = "2.0.
|
| 9 |
-
assert '__version__ = "2.0.
|
| 10 |
-
assert 'version="2.0.
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 39 |
text = (ROOT / "src/ragforge/ui.py").read_text(encoding="utf-8")
|
| 40 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 51 |
text = (ROOT / "README.md").read_text(encoding="utf-8")
|
| 52 |
-
assert "
|
| 53 |
-
assert "
|
| 54 |
-
assert "
|
| 55 |
-
assert "
|
| 56 |
-
assert "
|
| 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
|