Skip to content

feat(evaluation): admin System tab for indexing speed and RAG quality - #811

Draft
EnjoyBacon7 wants to merge 14 commits into
developfrom
feat/evaluation-page
Draft

feat(evaluation): admin System tab for indexing speed and RAG quality#811
EnjoyBacon7 wants to merge 14 commits into
developfrom
feat/evaluation-page

Conversation

@EnjoyBacon7

@EnjoyBacon7 EnjoyBacon7 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Important

Superseded — this PR has been split into a stacked chain of 15 reviewable PRs.
It stays open as a draft tracker because the design discussion and review history live here. Do not merge it.

The chain

Merge in order. Each PR targets the previous one's branch, so its diff shows only its own change; GitHub retargets automatically as each lands. The tree at the tip of the chain is byte-identical to this branch (verified with git diff), and every part leaves ruff, the layer-import guard, the unit suite, tsc -b, eslint and the UI tests in the same state as develop.

# PR LOC What
1 #812 +362 config surface (EvaluationConfig, server.internal_url, resolved_rdb()) and domain models
2 #813 +271 test-set CSV parsing and file_id normalisation
3 #814 +138 indexing throughput aggregation
4 #815 +441 fold promptfoo output into retrieval and answer metrics
5 #816 +273 generate the two promptfoo configs
6 #817 +467 eval_datasets / eval_runs persistence + migration
7 #818 +268 dataset upload, storage and deletion
8 #819 +67 the EvaluationRunner port
9 #820 +476 run start, dispatch and cancellation
10 #821 +479 the EvalRunner Ray actor and its dispatcher
11 #822 +275 admin API routes; promptfoo in both images
12 #823 +147 UI API client
13 #824 +462 dataset card and run detail components
14 #825 +351 Evaluation tab — the feature becomes reachable here
15 #826 +140 feature guide and sample dataset

Layering. Parts 1–5 are pure logic with no I/O. 6–9 are storage and orchestration. 10–11 are the worker and the HTTP surface — the backend is complete and usable at 11. 12–14 are the UI, ordered data layer → components → page. 15 is documentation.

Functional between parts. Nothing is reachable until part 14 registers the tab, so every intermediate state is inert-but-valid rather than half-wired. The one place this cost something is metrics.py and EvaluationService, each of which is introduced across two parts (3/4 and 7/9) because a single part would have exceeded 500 LOC.


Original description

What

Adds an admin-only Evaluation tab to the System page. A run indexes an uploaded corpus into a throwaway partition, replays a CSV test set through promptfoo, and reports three metric families:

Measures Source
Indexing speed files/min, MB/s, p50 / p95 per file, per-extension breakdown wall-clock around each upload
Retrieval quality hit rate, MRR, recall, context relevance GET /search/partition/{p} responses
Answer quality pass rate, factuality, rubric score POST /v1/chat/completions responses

How it works

POST /evaluation/runs
  → EvalRunner (Ray actor, one run at a time)
      1. create partition __eval_<run_id>
      2. upload + time each corpus file   → indexing metrics
      3. promptfoo eval × 2               → retrieval + answer metrics
      4. persist, drop the partition

The runner drives OpenRAG through its own HTTP API rather than in-process calls: that is the path real uploads take, so the timings mean something, and it is the same surface promptfoo talks to.

Two promptfoo configs, not one. Retrieval targets /search, whose documents carry the chunk under content (context-relevance grades it) and metadata.file_id (feeds the ranking metrics). Answers target /v1/chat/completions, reduced to the assistant message. Splitting them means every assertion applies to a single provider, so none can run against an output shape it cannot read.

transformResponse must be a single JavaScript expression — statements and IIFEs are rejected, and every row errors before grading.

Notable decisions

  • Ranking definitions follow the write-up already in tests/load/automatic-evaluation-pipeline/README.md, so these numbers mean the same thing as the offline pipeline's.
  • Rows without expected_file_ids are skipped, not failed. Scoring them as misses would make a sparsely-annotated test set look like a broken retriever; the run reports the skipped count.
  • The grader is OpenRAG's own configured LLM (llm.base_url / llm.model), so model-graded assertions need no third-party credentials.
  • Auth (changed from the original sketch). Rather than an ephemeral user per run, runs authenticate as one long-lived non-admin service user __openrag_eval__ whose token is regenerated at the start of every run. That keeps a single service account in the DB while ensuring no usable plaintext token is ever stored at rest.
  • __eval_* partitions are filtered out of the partition listing so a throwaway never shows up as a user-facing collection.
  • file_id sanitisation. The indexing API accepts only [A-Za-z0-9._:-], so a corpus file cannot be uploaded under a raw filename. Both the uploader and the ground-truth comparison go through core/evaluation/identity.py, so an author keeps writing real filenames.
  • One run at a time — a second POST /evaluation/runs returns 409, so indexing timings stay comparable. Enforced by the partial unique index ux_eval_runs_single_active: the run row is created before the token is regenerated, so racing starts cannot revoke each other's credentials.
  • promptfoo is pinned in both api.Dockerfile and ray.Dockerfile (compose runs Ray inside the API container), not run via npx promptfoo@latest: a run should not depend on npm reachability or on CLI behaviour changing under a deployment that was not rebuilt. Node 22 comes from NodeSource — distro packages predate promptfoo's floor.

Testing

  • 50 backend unit tests covering CSV parsing/validation, promptfoo config generation, the metric math (zero-division, percentile ranks, tolerance of promptfoo's varying output envelope), file_id sanitisation on both sides of the match, and the run-lifecycle service paths.
  • 9 UI tests covering dataset listing, run start/cancel gating, the metric panels, and error display.
  • ruff, the layer-import guard, tsc -b and eslint all pass.

Reviewer notes

  • Now executed end to end on a real deployment and corpus. That run is what surfaced the file_id rejection, the invalid answer transform, the unwritable promptfoo config dir and the unresolved DB name — all fixed in the commits above. promptfoo still does not parse the generated configs in CI; they are asserted structurally.
  • ui has 31 pre-existing test failures on develop (localStorage undefined under jsdom); this branch neither adds to nor fixes them.
  • New env vars, all documented in env_vars.md with working defaults: OPENRAG_INTERNAL_URL, PROMPTFOO_BIN, and the EVAL_* tunables (EVAL_MAX_CORPUS_MB, EVAL_MAX_TESTSET_MB, EVAL_MAX_TESTSET_ROWS, EVAL_TOP_K, EVAL_TASK_TIMEOUT, EVAL_TASK_POLL_SECONDS, EVAL_HTTP_TIMEOUT, EVAL_PROMPTFOO_TIMEOUT). They resolve through the existing Pydantic + env-override mechanism (evaluation: in conf/config.yaml).
  • Migration a7c9e1f2b3d4 is idempotent per the repo's alembic rules.

Update — audit pass

A review pass over the whole feature. Four defects fixed, plus configuration and simplification work.

Correctness

  • p50/p95 reported the wrong file. round() breaks ties to even, so the nearest-rank index was off by one whenever fraction * n landed on an odd integer — p50 of two files returned the slower one. Now ceil, per the definition.
  • Concurrent starts could revoke each other's credentials. start_run read active_run() and then regenerated the shared eval user's token; two requests could pass that check, and the loser invalidated the token the winner was still indexing with. The run row is now created before anything is provisioned, and a partial unique index makes that insert the lock. Composes with the pre-dispatch runner ping, which still runs first.
  • A failed provision wedged the feature. The row stayed QUEUED, which now holds the lock, blocking every later run. Failure releases the row and drops the partition.
  • The DI wiring test was failing on this branchget_evaluation_service was never registered in the provider registry.

Configuration — limits and timeouts were hard-coded. They now resolve through the project's Pydantic + env-override mechanism. Domain contracts stay constants on purpose: the reserved partition prefix, the CSV column names and the file_id alphabet would invalidate stored datasets if retuned.

Cost and simplification

  • Corpus uploads stream to disk instead of buffering up to 512 MB in memory.
  • Duplicate corpus basenames are rejected; previously they overwrote silently while corpus_file_count overstated.
  • Removed dead code (active_run(), an unread promptfoo var) and deduplicated the httpx client and the UI's repeated poll constant.
  • Comments rewritten to be install-agnostic — several encoded one deployment's absolute paths, distro versions and incident symptoms.

Not changed, flagged for a decision

  • Indexing is sequential, so files_per_minute measures serialised throughput, not the system's real concurrency.
  • /search defaults similarity_threshold to 0.75 and the eval does not override it, so retrieval can return fewer than top_k and depress recall for reasons unrelated to ranking.
  • A separate Ray cluster needs <data_dir> on shared storage — the runner reads dataset files by path.
  • Ordinary users can still create __eval_-prefixed partitions, which are now hidden from listings.

Verification: ruff, the layer-import guard and 2262 unit tests pass. The one failure, test_content_deduplication_can_be_disabled_by_env, reproduces on develop and is unrelated. Frontend checks (tsc -b, eslint, UI tests) were not re-run — dependencies are not installed in this environment; three UI files changed, all import-only edits for the shared poll constant.

Summary by CodeRabbit

  • New Features
    • Added an admin Evaluation tab to upload datasets, start/cancel evaluation runs, and review run details (including per-question outcomes and indexing/retrieval/answer metrics).
    • Added admin API endpoints and schemas for evaluation datasets and run lifecycle management.
  • Documentation
    • Documented evaluation environment variables and required runtime/prerequisites; added a CLAUDE.md evaluation flow guide and sample dataset README.
  • Bug Fixes
    • Updated status display to include the new Evaluating state.
  • Tests
    • Added/expanded unit and UI tests for evaluation test-set parsing, metrics computation, promptfoo config generation, and run behavior.

Adds an on-demand evaluation harness driven from the admin System page.
A run indexes an admin-uploaded corpus into a throwaway partition, replays
a CSV test set through promptfoo, and reports three metric families:
indexing throughput, retrieval ranking quality, and answer quality.

Backend
- EvalRunner Ray actor (detached, one run at a time) drives OpenRAG over
  its own HTTP API, so the timings measure the path real uploads take.
- Two promptfoo configs rather than one: retrieval targets /search (whose
  documents carry chunk text and metadata.file_id), answers target
  /v1/chat/completions. Each config has a single provider, so no assertion
  runs against an output shape it cannot read.
- Hit rate / MRR / recall follow the definitions already documented in
  tests/load/automatic-evaluation-pipeline. Rows lacking expected_file_ids
  are reported as skipped, never scored as misses.
- Model-graded assertions use OpenRAG's own configured LLM, so an eval
  needs no third-party credentials.
- Runs authenticate as the non-admin service user __openrag_eval__ whose
  token is regenerated per run, so no usable plaintext token is at rest.
- __eval_* partitions are filtered out of the partition listing.

UI
- Evaluation tab on the System page: dataset upload, run history, and a
  run detail view with the three metric panels and per-question results.

Infra
- Node + pinned promptfoo in ray.Dockerfile; OPENRAG_INTERNAL_URL and
  PROMPTFOO_BIN documented in env_vars.md.

Tests: 37 backend unit tests (CSV parsing, config generation, metric
math) and 9 UI tests.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an admin evaluation system with dataset uploads, persisted run history, Ray-based indexing and Promptfoo benchmarking, metric aggregation, API endpoints, and a System-page UI for launching, monitoring, canceling, and inspecting runs.

Changes

Evaluation workflow

Layer / File(s) Summary
Evaluation contracts and metric computation
openrag/core/models/evaluation.py, openrag/core/evaluation/*, tests/unit/core/evaluation/*
Defines evaluation models, CSV validation, Promptfoo configurations, metric aggregation, and unit tests.
Evaluation persistence and service wiring
openrag/core/ports/*, openrag/services/persistence/*, openrag/di/*, openrag/core/config/*
Adds repository contracts, database tables, PostgreSQL persistence, configuration resolution, partition filtering, and dependency injection for evaluation services.
Run orchestration and Ray execution
openrag/services/orchestrators/evaluation_service.py, openrag/services/workers/eval_runner.py, infra/docker/*
Creates datasets, dispatches runs, indexes corpus files, executes Promptfoo, persists results, supports cancellation, and removes temporary partitions.
Admin evaluation API
openrag/api/routers/admin/evaluation.py, openrag/api/schemas/admin/evaluation_schemas.py, openrag/api/main.py
Exposes admin-protected dataset and run endpoints with structured metric and case responses.
Admin evaluation interface
ui/src/lib/api/evaluation.ts, ui/src/pages/admin/evaluation/*, ui/src/pages/admin/system.tsx, ui/src/components/shared/status-badge.tsx
Adds dataset management, run controls, polling, cancellation, metrics, case details, status styling, and UI tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: admin-ui, documentation

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the new admin System evaluation tab and its focus on indexing speed and RAG quality.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/evaluation-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added admin-ui Admin UI documentation Improvements or additions to documentation labels Jul 27, 2026
The compose stack runs Ray inside the openrag container, which is built
from api.Dockerfile — ray.Dockerfile is only used by deployments with a
separate Ray cluster. Without this, EvalRunner would fail every run with
'promptfoo executable not found' on the standard docker compose install.

Found while deploying the branch to a test host.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (5)
ui/src/pages/admin/evaluation/index.test.tsx (2)

148-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

First renderTab() in this test is never unmounted.

The initial renderTab() call (line 149) isn't captured/unmounted before the second renderTab() (line 157) mounts a second tree in the same test — two EvaluationTab instances coexist until the end of the test.

♻️ Proposed fix
-    renderTab();
+    const { unmount: unmountFirst } = renderTab();
     await screen.findByText("Support docs");
     expect(screen.queryByRole("button", { name: /cancel run/i })).toBeNull();
+    unmountFirst();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/admin/evaluation/index.test.tsx` around lines 148 - 162, Update
the test case around renderTab so the initial rendered EvaluationTab instance is
captured and unmounted before the second renderTab call. Preserve the existing
assertions and active-run cancellation flow while ensuring only one mounted tree
exists at a time.

1-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for dataset creation/deletion flows.

Tests cover listing, starting, cancelling runs and metric rendering well, but the "New dataset" upload dialog and delete flow (createEvalDataset/deleteEvalDataset, mocked but never exercised) have no tests. A test reopening the upload dialog after Cancel would have caught the stale-state bug flagged in dataset-card.tsx.

Want me to draft tests for the upload dialog (including a reopen-after-cancel case) and the delete confirmation flow?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/admin/evaluation/index.test.tsx` around lines 1 - 213, The
EvaluationTab tests omit dataset creation, upload-dialog cancellation/reopen
behavior, and deletion coverage. Extend the EvaluationTab test suite to exercise
createEvalDataset with the New dataset dialog, verify Cancel clears the selected
upload state so reopening starts clean, and cover the delete confirmation flow
including deleteEvalDataset invocation; add the necessary mocked symbols and
fixtures while preserving existing tests.
ui/src/pages/admin/evaluation/run-detail.tsx (2)

81-125: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Failed-file samples aren't surfaced anywhere.

IndexingMetrics.samples (with filename, duration_seconds, failed) is fetched but never rendered — only the aggregate files_failed count shows. An admin debugging a bad corpus can't tell which file(s) failed.

Consider rendering the failed entries from metrics.samples (filename + reason if available) when files_failed > 0, so failures are actionable rather than just a number.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/admin/evaluation/run-detail.tsx` around lines 81 - 125, Update
IndexingPanel to render failed entries from metrics.samples when
metrics.files_failed is greater than zero, showing each filename and its failure
reason when available. Keep the existing aggregate statistics and only display
the failure details when failed samples exist.

11-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated, inconsistent metric-formatting helpers across the two views. run-detail.tsx and index.tsx each define their own percent/inline formatting for the same metrics (hit_rate, mrr), with different precision, so the identical run can display "75%" in the runs table and "75.0%" in the detail panel, and MRR as toFixed(2) vs toFixed(3).

  • ui/src/pages/admin/evaluation/run-detail.tsx#L11-L17: move percent/score into a shared module (e.g. ui/src/lib/format.ts) and import it here.
  • ui/src/pages/admin/evaluation/index.tsx#L16-L18: drop the local percent and inline toFixed(2) for mrr; import the same shared percent/score helpers so both views render identical precision for the same metric.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/admin/evaluation/run-detail.tsx` around lines 11 - 17, Move the
percent and score helpers from run-detail.tsx into a shared formatting module,
then import and use them in both evaluation views. In
ui/src/pages/admin/evaluation/run-detail.tsx lines 11-17, remove the local
definitions and import the shared helpers; in
ui/src/pages/admin/evaluation/index.tsx lines 16-18, remove the local percent
helper and inline MRR formatting, replacing both with the shared percent/score
functions so hit_rate and mrr use identical precision.
ui/src/lib/api/evaluation.ts (1)

42-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type by_extension bucket shape explicitly.

Record<string, number> for each extension bucket type-checks bucket.files/bucket.mean_seconds access in run-detail.tsx only because of the loose index signature — it doesn't document the actual shape or catch typos in field names.

♻️ Proposed type refinement
-  by_extension: Record<string, Record<string, number>>;
+  by_extension: Record<string, { files: number; mean_seconds: number }>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/api/evaluation.ts` around lines 42 - 53, Update the
IndexingMetrics interface’s by_extension property to use an explicit bucket type
defining the actual fields consumed by run-detail.tsx, including files and
mean_seconds, instead of nested Record<string, number>. Preserve the existing
extension-keyed structure while enabling type checking for bucket field names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/docker/ray.Dockerfile`:
- Around line 24-32: Update the Dockerfile’s Node installation in the promptfoo
setup to install a supported Node.js version, such as Node 20.20.0 or newer,
from an official or NodeSource source instead of Debian bookworm’s default
nodejs package. Preserve the pinned promptfoo version and ensure npm remains
available for the existing global installation.

In `@openrag/core/evaluation/metrics.py`:
- Around line 41-52: Update _percentile to calculate the nearest-rank index with
a ceiling-based rank rather than round, ensuring exact fractional ties select
the intended rank while retaining the existing bounds clamping and
observed-value behavior.

In `@openrag/core/evaluation/testset.py`:
- Around line 95-136: Update the CSV iteration around the cases and errors
collections to enforce limits while reading: stop processing once valid cases
exceed MAX_ROWS, and cap stored validation errors at _MAX_REPORTED_ERRORS while
still tracking the total error count needed for the summary suffix. Preserve the
existing validation messages and final ValidationError codes, but avoid
materializing unbounded cases or errors before rejection.

In `@openrag/services/orchestrators/evaluation_service.py`:
- Around line 147-204: Enforce the single-active-run invariant at both layers:
in evaluation_service.py lines 147-204, protect the active_run() check through
create_run() and dispatch with an atomic lock or database-level guard; in
eval_runner.py lines 59-60 and 87-108, reject run() when _active_run_id is
already set before overwriting state; and in eval_runner.py lines 168-169,
ensure finally cleanup only clears state belonging to the completing run so
overlapping calls cannot corrupt cancellation or process tracking.
- Around line 147-204: Serialize the active-run check and run creation in
EvaluationService.start_run so concurrent requests cannot both pass active_run()
before create_run(). Use the service’s existing locking or synchronization
mechanism to guard the check-and-create sequence, while preserving the current
conflict behavior and single-run dispatch flow.
- Around line 131-134: Update delete_dataset to detect and reject deletion when
an evaluation run is actively using the dataset, before deleting the repository
record or dataset directory. Reuse the existing run-tracking or active-run
mechanism associated with _index_corpus, and raise a clear dataset-in-use error;
retain the current NotFoundError behavior for missing datasets and only call
shutil.rmtree after the guard passes.

In `@openrag/services/orchestrators/partition_service.py`:
- Around line 245-247: Update list_partition_summaries() to filter out eval
partitions with the same is_eval_partition() predicate used by
list_partitions(), including when expanding the admin all sentinel. Ensure
orphaned __eval_<run_id> partitions are excluded from the GET /partition/
response while preserving non-eval summaries.

In `@openrag/services/persistence/evaluation_repo.py`:
- Around line 110-120: Enforce the one-active-run invariant at the database
layer rather than relying on the check-then-act flow around active_run(). Add a
partial unique index or equivalent constraint on eval_runs covering rows whose
status is QUEUED, INDEXING, or EVALUATING, and update create_run() to surface
the atomic constraint failure as the existing active-run conflict. Keep
active_run() for lookup, but do not rely on it for exclusivity.

In `@ui/src/pages/admin/evaluation/dataset-card.tsx`:
- Around line 137-158: Introduce a shared close handler near reset that resets
name, testset, and corpus before invoking onOpenChange(false). Use this handler
both in the Dialog onOpenChange wrapper for closing and in the Cancel button
instead of calling the raw onOpenChange prop, while preserving the existing open
behavior.

In `@ui/src/pages/admin/evaluation/index.tsx`:
- Around line 93-116: Update the run rows rendered in the runs.map callback to
be keyboard accessible: add an appropriate row role, make each row focusable
with tabIndex, and invoke setSelectedRunId(run.id) from an onKeyDown handler for
Enter and Space while preserving the existing onClick behavior.

---

Nitpick comments:
In `@ui/src/lib/api/evaluation.ts`:
- Around line 42-53: Update the IndexingMetrics interface’s by_extension
property to use an explicit bucket type defining the actual fields consumed by
run-detail.tsx, including files and mean_seconds, instead of nested
Record<string, number>. Preserve the existing extension-keyed structure while
enabling type checking for bucket field names.

In `@ui/src/pages/admin/evaluation/index.test.tsx`:
- Around line 148-162: Update the test case around renderTab so the initial
rendered EvaluationTab instance is captured and unmounted before the second
renderTab call. Preserve the existing assertions and active-run cancellation
flow while ensuring only one mounted tree exists at a time.
- Around line 1-213: The EvaluationTab tests omit dataset creation,
upload-dialog cancellation/reopen behavior, and deletion coverage. Extend the
EvaluationTab test suite to exercise createEvalDataset with the New dataset
dialog, verify Cancel clears the selected upload state so reopening starts
clean, and cover the delete confirmation flow including deleteEvalDataset
invocation; add the necessary mocked symbols and fixtures while preserving
existing tests.

In `@ui/src/pages/admin/evaluation/run-detail.tsx`:
- Around line 81-125: Update IndexingPanel to render failed entries from
metrics.samples when metrics.files_failed is greater than zero, showing each
filename and its failure reason when available. Keep the existing aggregate
statistics and only display the failure details when failed samples exist.
- Around line 11-17: Move the percent and score helpers from run-detail.tsx into
a shared formatting module, then import and use them in both evaluation views.
In ui/src/pages/admin/evaluation/run-detail.tsx lines 11-17, remove the local
definitions and import the shared helpers; in
ui/src/pages/admin/evaluation/index.tsx lines 16-18, remove the local percent
helper and inline MRR formatting, replacing both with the shared percent/score
functions so hit_rate and mrr use identical precision.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cbd4ffa-bc56-4da0-a201-a7a86269aa65

📥 Commits

Reviewing files that changed from the base of the PR and between b2ab6b1 and 17fbfb3.

📒 Files selected for processing (32)
  • CLAUDE.md
  • docs/content/docs/documentation/env_vars.md
  • infra/docker/ray.Dockerfile
  • openrag/api/main.py
  • openrag/api/routers/admin/evaluation.py
  • openrag/api/schemas/admin/evaluation_schemas.py
  • openrag/core/evaluation/__init__.py
  • openrag/core/evaluation/metrics.py
  • openrag/core/evaluation/promptfoo_config.py
  • openrag/core/evaluation/testset.py
  • openrag/core/models/evaluation.py
  • openrag/core/ports/catalog_store.py
  • openrag/core/ports/evaluation_repo.py
  • openrag/di/container.py
  • openrag/di/providers.py
  • openrag/services/orchestrators/evaluation_service.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/persistence/evaluation_repo.py
  • openrag/services/persistence/migrations/alembic/versions/a7c9e1f2b3d4_add_evaluation_tables.py
  • openrag/services/persistence/schema.py
  • openrag/services/storage/postgres_store.py
  • openrag/services/workers/eval_runner.py
  • tests/unit/core/evaluation/test_metrics.py
  • tests/unit/core/evaluation/test_promptfoo_config.py
  • tests/unit/core/evaluation/test_testset.py
  • ui/src/components/shared/status-badge.tsx
  • ui/src/lib/api/evaluation.ts
  • ui/src/pages/admin/evaluation/dataset-card.tsx
  • ui/src/pages/admin/evaluation/index.test.tsx
  • ui/src/pages/admin/evaluation/index.tsx
  • ui/src/pages/admin/evaluation/run-detail.tsx
  • ui/src/pages/admin/system.tsx

Comment thread infra/docker/ray.Dockerfile Outdated
Comment thread openrag/core/evaluation/metrics.py Outdated
Comment thread openrag/core/evaluation/testset.py Outdated
Comment thread openrag/services/orchestrators/evaluation_service.py Outdated
Comment thread openrag/services/orchestrators/evaluation_service.py Outdated
Comment thread openrag/services/orchestrators/partition_service.py
Comment thread openrag/services/persistence/evaluation_repo.py
Comment thread ui/src/pages/admin/evaluation/dataset-card.tsx Outdated
Comment thread ui/src/pages/admin/evaluation/index.tsx
@coderabbitai coderabbitai Bot removed the documentation Improvements or additions to documentation label Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/docker/api.Dockerfile`:
- Around line 29-31: Update the Node.js installation in both Dockerfiles so it
uses a controlled source providing Node.js 20.20+ or 22.22+, rather than relying
on the distribution apt version. In the setup that installs promptfoo, add
build-time assertions for node --version, npm --version, and promptfoo
--version, ensuring promptfoo@${PROMPTFOO_VERSION} is available before the image
build completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e20f881-14c0-4973-9d7a-028a6be608df

📥 Commits

Reviewing files that changed from the base of the PR and between 17fbfb3 and 271538a.

📒 Files selected for processing (2)
  • CLAUDE.md
  • infra/docker/api.Dockerfile

Comment thread infra/docker/api.Dockerfile
expected_file_ids in the test-set CSV is matched against metadata.file_id,
but the runner uploaded each file as 'eval-{index}-{name}'. No CSV author
can predict that prefix, so every ranking metric scored zero. The file_id
is now the filename, percent-encoded for the request path since corpus
filenames routinely contain spaces and accents.

Found while building a default dataset from a real corpus.
… dead actor

Two defects found by running the feature on a real deployment:

1. EvalRunner built ConnectionManager from settings.rdb, whose database is
   None unless derived from the Milvus collection name. The actor died in
   __init__ on every run. The derivation moves to Settings.resolved_rdb()
   so the API's catalog store and any Ray worker resolve it identically —
   services cannot import di, where the logic previously lived.

2. Dispatch is fire-and-forget, so the dead actor left the run in QUEUED
   forever with no error, plus an orphaned partition and token. start_run
   now pings the runner before provisioning anything and returns 503 when
   it cannot be reached.
… Node 22

Three defects from the first end-to-end run on a real corpus:

- The indexing API rejects a file_id containing spaces, so 8 of 24 corpus
  files failed to upload. The file_id is now sanitized to the allowed
  character set.
- That sanitization would have broken expected_file_ids matching, so the
  ranking metrics now match a test set's ground truth against the chunk's
  metadata.source filename as well as its file_id. Authors keep writing
  real filenames.
- Debian's nodejs package is 20.19.x, below promptfoo's floor of
  ^20.20.0 || >=22.22.0, so 'promptfoo eval' refused to start. Both images
  now install Node 22 from NodeSource.
11 questions over 6 documents, answers verified against the source PDFs,
plus 18 topically-adjacent distractors so retrieval has to discriminate —
a corpus with one document per topic flatters any retriever.

The PDFs are third-party and stay out of the repo; corpus.txt lists them
so the dataset can be reassembled. Lives under tests/evaluation/ rather
than tests/data/ because .gitignore's 'data/' rule silently swallows the
latter.
.gitignore's blanket '*.csv' silently dropped the sample test set from
the previous commit, leaving its README pointing at a missing file.
promptfoo keeps a SQLite eval history under $HOME/.promptfoo. In the API
image that path is not writable by the non-root app user, so the database
migration failed and the CLI exited 1 with *no output on either stream* —
even 'promptfoo --version' failed. Each run now points PROMPTFOO_CONFIG_DIR
at its own temp directory.

The failure was invisible because the error path only captured stderr;
it now reports stdout too, where promptfoo prints config errors.

Also dumps YAML with allow_unicode so accented questions stay readable.
@coderabbitai coderabbitai Bot added the documentation Improvements or additions to documentation label Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/services/orchestrators/test_evaluation_service.py`:
- Around line 116-119: Run Ruff formatting on
tests/unit/services/orchestrators/test_evaluation_service.py and apply all
resulting formatting changes, including the settings model_copy block, so ruff
format --check passes without altering test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 32c17c78-7012-4a47-9ea2-9295c859faac

📥 Commits

Reviewing files that changed from the base of the PR and between 271538a and 20364b6.

⛔ Files ignored due to path filters (1)
  • tests/evaluation/rag_dataset_sample.csv is excluded by !**/*.csv
📒 Files selected for processing (13)
  • .gitignore
  • infra/docker/api.Dockerfile
  • infra/docker/ray.Dockerfile
  • openrag/core/config/root.py
  • openrag/core/evaluation/metrics.py
  • openrag/di/repositories.py
  • openrag/services/orchestrators/evaluation_service.py
  • openrag/services/workers/eval_runner.py
  • tests/evaluation/README.md
  • tests/evaluation/corpus.txt
  • tests/unit/core/config/test_resolved_rdb.py
  • tests/unit/core/evaluation/test_metrics.py
  • tests/unit/services/orchestrators/test_evaluation_service.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • infra/docker/api.Dockerfile
  • infra/docker/ray.Dockerfile
  • openrag/core/evaluation/metrics.py
  • tests/unit/core/evaluation/test_metrics.py
  • openrag/services/workers/eval_runner.py
  • openrag/services/orchestrators/evaluation_service.py

Comment thread tests/unit/services/orchestrators/test_evaluation_service.py Outdated
EnjoyBacon7 and others added 4 commits July 27, 2026 13:17
…ize ids

The first successful end-to-end run scored 0 on every answer and missed
every document whose filename contains a space. Two causes:

- promptfoo evaluates transformResponse as a single JavaScript expression.
  The IIFE that parsed 'extra' for sources failed with 'is not a function',
  so every answer row errored before grading and factuality/llm-rubric
  produced no scores at all. The answer pass only needs the message text,
  so the transform is now json.choices[0].message.content. Verified in the
  container: both graders return real scores and reasoning.
- Ground-truth matching relied on metadata.source holding the original
  filename; it holds the sanitized one. Both sides of the comparison now
  run through the same sanitizer (core.evaluation.identity), so a test set
  naming 'A B.pdf' matches the stored 'A_B.pdf' either way.
… path

metadata.source is the server's temp path
('/app/data/1785151308957_c270_report.pdf'), so the per-question table was
showing that instead of the document's name. Verified against a live
search response.

Corrects a test that encoded the earlier wrong assumption that source
holds the original filename.
Rebased onto the latest branch. The upstream commits independently fixed
the file_id rejection, the promptfoo transform and orphaned-run reaping —
their versions are kept, since they were verified against a live run. This
carries the findings they did not cover.

Correctness:
- p50/p95 used round(), which breaks ties to even and returned the wrong
  observation whenever fraction * n was an odd integer (p50 of two files
  reported the slower one). Now ceil, per the nearest-rank definition.
- start_run checked for an active run, then regenerated the shared eval
  user's token. Two concurrent starts could both pass that check, and the
  loser revoked the credentials the winner was still indexing with. The run
  row is now created before anything is provisioned, and the partial unique
  index ux_eval_runs_single_active makes that insert the lock. This composes
  with the pre-dispatch runner ping, which still runs first.
- A failed provision left the row QUEUED, which now holds the lock, so it
  would block every later run. Failure releases the row and drops the
  partition.

Configuration — limits and timeouts were hard-coded:
- New evaluation config block (EVAL_*, OPENRAG_INTERNAL_URL, PROMPTFOO_BIN)
  wired through the existing Pydantic + env-override mechanism.
- Contracts stay constants on purpose: partition prefix, CSV columns, the
  file_id alphabet. Making those configurable would invalidate stored
  datasets.

Simplification and cost:
- Corpus uploads stream to disk instead of buffering up to 512 MB in RAM.
- Rejected duplicate corpus basenames, which previously overwrote silently
  and left corpus_file_count overstated.
- Removed dead code: active_run(), now superseded by the index, and the
  expected_file_ids var no assertion reads. is_busy() is deliberately kept —
  it is the liveness probe start_run pings.
- Deduped the httpx client construction and the UI's duplicated POLL_MS.
- Fresh assertion copies per test so the promptfoo YAML has no aliases.

Also registers get_evaluation_service in the DI wiring test, which the
feature commits missed — it was failing on the branch.
Comments described one deployment's incidents rather than the code: a
container's absolute storage path, a distro's Node version, the symptoms of
a specific failed run. Rewritten to state what the code does and why, in
terms true of any install. Also genericised a test fixture path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
openrag/services/workers/eval_runner.py (1)

191-193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind partition/file_id context on these warning logs instead of raw self._logger.

run() uses a context-bound log = self._logger.bind(run_id=..., partition=...), but the per-file indexing warning and the partition-drop warnings use unbound self._logger with values only interpolated into the message string. As per coding guidelines, "Use structured Loguru logging through get_logger(), binding relevant context such as file_id and partition."

♻️ Bind context instead of string-interpolating it
-                self._logger.warning(f"Eval corpus file '{path.name}' failed to index: {exc}")
+                self._logger.bind(partition=partition, file_id=sanitize_file_id(path.name)).warning(
+                    f"Eval corpus file '{path.name}' failed to index: {exc}"
+                )
-                if response.status_code >= 400:
-                    self._logger.warning(f"Could not drop eval partition '{partition}': {response.status_code}")
-        except Exception as exc:  # noqa: BLE001 — teardown must not mask the run's outcome
-            self._logger.warning(f"Could not drop eval partition '{partition}': {exc}")
+                if response.status_code >= 400:
+                    self._logger.bind(partition=partition).warning(
+                        f"Could not drop eval partition: {response.status_code}"
+                    )
+        except Exception as exc:  # noqa: BLE001 — teardown must not mask the run's outcome
+            self._logger.bind(partition=partition).warning(f"Could not drop eval partition: {exc}")

Also applies to: 349-353

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/services/workers/eval_runner.py` around lines 191 - 193, Update the
per-file indexing warning in run() and the partition-drop warnings to use
context-bound Loguru loggers instead of raw self._logger; bind the relevant
partition and file_id/path context as structured fields, while retaining the
existing warning messages and run-level context.

Source: Coding guidelines

openrag/core/evaluation/identity.py (1)

15-17: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Sanitized file_id collisions aren't detected at upload time.

Two distinct corpus filenames can normalize to the same file_id (e.g. "invoice#1.pdf" and "invoice_1.pdf" both become invoice_1.pdf). The upload-time duplicate check in evaluation_service.py only compares raw basenames (Path(filename).name), so this collision passes validation. During indexing (eval_runner.py::_index_corpus), the second upload then silently collides with the first file's file_id, corrupting corpus coverage and skewing indexing/retrieval metrics without any visible error.

Consider rejecting corpus uploads when two files sanitize to the same file_id, using this same function, rather than only comparing raw basenames.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/core/evaluation/identity.py` around lines 15 - 17, Update the
upload-time duplicate validation in evaluation_service.py to derive each
filename’s file_id through sanitize_file_id and reject uploads when two
sanitized IDs match, including collisions from distinct raw basenames. Reuse
sanitize_file_id from openrag/core/evaluation/identity.py and preserve the
existing duplicate-error behavior for conflicting IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@conf/config.yaml`:
- Around line 365-368: Remove the duplicate max_download_bytes entry from the
mcp configuration mapping, leaving a single 104857600-byte setting before the
Evaluation section.

In `@openrag/api/routers/admin/evaluation.py`:
- Around line 80-85: Update the admin evaluation upload flow before
EvaluationService.create_dataset to enforce max_testset_bytes while reading
testset: avoid unbounded await testset.read(), read at most the configured limit
plus one byte, and reject oversized uploads before passing data to dataset
creation or CSV parsing. Preserve the existing bounded corpus stream handling
and normal behavior for uploads within the limit.
- Around line 82-85: Move the synchronous corpus-copying work in
EvaluationService.create_dataset, including _copy_within_budget and its
stream.read/target.open/write operations, off the event-loop thread by running
it through a worker-thread mechanism such as asyncio.to_thread. Preserve the
existing corpus filenames, upload contents, and budget behavior while keeping
the request coroutine non-blocking.

---

Nitpick comments:
In `@openrag/core/evaluation/identity.py`:
- Around line 15-17: Update the upload-time duplicate validation in
evaluation_service.py to derive each filename’s file_id through sanitize_file_id
and reject uploads when two sanitized IDs match, including collisions from
distinct raw basenames. Reuse sanitize_file_id from
openrag/core/evaluation/identity.py and preserve the existing duplicate-error
behavior for conflicting IDs.

In `@openrag/services/workers/eval_runner.py`:
- Around line 191-193: Update the per-file indexing warning in run() and the
partition-drop warnings to use context-bound Loguru loggers instead of raw
self._logger; bind the relevant partition and file_id/path context as structured
fields, while retaining the existing warning messages and run-level context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ebd50231-aaac-48b8-9cbc-1ecf5072335f

📥 Commits

Reviewing files that changed from the base of the PR and between 20364b6 and 6f84250.

📒 Files selected for processing (28)
  • CLAUDE.md
  • conf/config.yaml
  • docs/content/docs/documentation/env_vars.md
  • infra/docker/api.Dockerfile
  • infra/docker/ray.Dockerfile
  • openrag/api/routers/admin/evaluation.py
  • openrag/core/config/evaluation.py
  • openrag/core/config/loader.py
  • openrag/core/config/root.py
  • openrag/core/evaluation/__init__.py
  • openrag/core/evaluation/identity.py
  • openrag/core/evaluation/metrics.py
  • openrag/core/evaluation/promptfoo_config.py
  • openrag/core/evaluation/testset.py
  • openrag/core/ports/evaluation_repo.py
  • openrag/services/orchestrators/evaluation_service.py
  • openrag/services/persistence/evaluation_repo.py
  • openrag/services/persistence/migrations/alembic/versions/a7c9e1f2b3d4_add_evaluation_tables.py
  • openrag/services/persistence/schema.py
  • openrag/services/workers/eval_runner.py
  • tests/unit/core/evaluation/test_metrics.py
  • tests/unit/core/evaluation/test_promptfoo_config.py
  • tests/unit/core/evaluation/test_testset.py
  • tests/unit/di/test_container.py
  • tests/unit/services/orchestrators/test_evaluation_service.py
  • ui/src/lib/api/evaluation.ts
  • ui/src/pages/admin/evaluation/index.tsx
  • ui/src/pages/admin/evaluation/run-detail.tsx
🚧 Files skipped from review as they are similar to previous changes (14)
  • openrag/core/evaluation/init.py
  • ui/src/lib/api/evaluation.ts
  • openrag/services/persistence/migrations/alembic/versions/a7c9e1f2b3d4_add_evaluation_tables.py
  • infra/docker/api.Dockerfile
  • openrag/services/persistence/schema.py
  • openrag/core/config/root.py
  • openrag/core/ports/evaluation_repo.py
  • infra/docker/ray.Dockerfile
  • CLAUDE.md
  • ui/src/pages/admin/evaluation/index.tsx
  • openrag/core/evaluation/metrics.py
  • tests/unit/core/evaluation/test_testset.py
  • ui/src/pages/admin/evaluation/run-detail.tsx
  • openrag/services/orchestrators/evaluation_service.py

Comment thread conf/config.yaml
Comment thread openrag/api/routers/admin/evaluation.py Outdated
Comment thread openrag/api/routers/admin/evaluation.py Outdated
- conf/config.yaml: drop a duplicated `max_download_bytes` key in the `mcp:`
  block, introduced when the `evaluation:` section was appended. Invalid YAML
  that stricter loaders reject.
- Partition listings: `list_partition_summaries` is what `GET /partition/`
  responds from, so a throwaway `__eval_<run_id>` could still surface there
  even though `list_partitions` hid it. Both filter now.
- Dataset upload: the test set was read unbounded before its size was checked.
  It is now read to one byte past the cap and rejected there, and the corpus
  copy runs on a worker thread instead of blocking the event loop.
- Test-set parsing: the row cap is enforced on the row that would exceed it,
  and only the reported errors are retained rather than every one.
- `delete_dataset` is refused while a run is using the dataset — the runner
  reads the corpus off disk for the whole indexing phase, so removing it
  mid-run surfaced as a FileNotFoundError instead of a conflict.
- UI: the dialog's Cancel button bypassed the form reset that backdrop and
  Escape go through, so a reopened dialog silently kept the previous
  selection. Run rows are now keyboard-selectable.

Verified locally: ruff, layer guard, 2267 unit tests, tsc, eslint on the
files this PR changes, and the 9 evaluation UI tests.
EvaluationService reached for `build_eval_runner()` and drove the actor
handle itself, so an orchestrator held Ray imports and `.remote()` calls —
the one place Phase 9 reserves for `services/workers/`. It also meant the
unit tests had to stub `services.workers.ray_utils` into `sys.modules` and
fake an actor handle whose attributes return objects with `.remote()`.

Introduce the `EvaluationRunner` port (is_busy / dispatch / cancel) and the
`RayEvaluationRunner` adapter, injected by the container — the same shape as
IndexingDispatcher. The adapter resolves the detached actor lazily, so
building the service (e.g. to list datasets) still does not spawn a worker.
The service now has no Ray import at all and its tests bind a plain
in-memory fake.

Also move `internal_url` from `evaluation` to `server` config (env var
`OPENRAG_INTERNAL_URL` is unchanged): now that the base URL crosses the port
as a dispatch argument, it is plainly a property of the server every
out-of-process worker calls back on, not of the evaluation feature.

While here, resolve the eval service user via the `UserRepository` port's
`get_user_by_external_id` instead of the `_dict` variant the port does not
declare, and mark `get_evaluation_service` a required container provider now
that it wires cleanly.
@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator

Split into a stacked chain of 15 PRs — #812#826 — each under 500 LOC. See the table at the top of the description for the order.

Verification, so reviewers do not have to take the split on trust:

  • The tip of the chain is byte-identical to this branch. git diff eval/15-docs-and-sample-dataset feat/evaluation-page is empty, and the cumulative diff against develop is +4596 / -12 — exactly this PR's numbers.
  • Every part builds and is green on its own, not just at the end: ruff check / ruff format --check, the layer-import guard, and the unit suite (2270 tests at the backend tip). The only failure anywhere is test_content_deduplication_can_be_disabled_by_env, which reproduces on develop and is unrelated.
  • The UI parts were checked with dependencies actually installed this time, which the last update to this PR could not do: tsc -b clean, eslint reporting only the one warning already on develop, and the full vitest suite passing — 23 files / 167 tests, 0 failures. The 31 pre-existing jsdom failures reported earlier do not reproduce on a clean npm ci.

Two files had to be introduced across two PRs each, because either would have blown the 500-line budget in one: metrics.py (parts 3 and 4, split at indexing vs. promptfoo folding) and EvaluationService (parts 7 and 9, split at datasets vs. runs). Both splits fall on a seam the code already had.

The EvaluationRunner port ended up as its own part (#819, 67 lines) rather than riding along with the service — the service PR came to 543 lines with it attached. It reads fine alone: it is the boundary that parts 9 and 10 are shaped by.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

admin-ui Admin UI documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants