Repository navigation
fix(cli): eval, report, answers, benchmark and evaluate exit with a message on bad input or output - #236
Conversation
…le --questions file Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…eval file Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…-UTF-8 runtime config Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…eadable run file Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…questions-from Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…file or unusable --output-dir Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…output cannot be written Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…written Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open issues
What changed in this PR
Improves CLI robustness by converting file/path OS and decoding failures into concise, user-facing error messages with appropriate exit codes across eval, report, answers, benchmark, and evaluate.
Changes:
- Add/centralize error handling for reading user-supplied JSON/YAML inputs and writing
--output/ output dirs, exiting with one-line messages (no tracebacks). - Refactor some repeated I/O logic into helpers (
_load_eval,_write_json_or_exit,_check_questions_from,_load_points). - Add targeted tests for unreadable/invalid inputs and unwritable outputs; update growth baseline.
| File | Description |
|---|---|
| dokimi/tests/test_report_eval_input_errors.py | New coverage for report eval failing cleanly on bad --eval. |
| dokimi/tests/test_report_cli_inputs.py | Adds coverage for unwritable report generate --output. |
| dokimi/tests/test_model_compare.py | Adds coverage for load_run() non-UTF8 and directory-path handling. |
| dokimi/tests/test_evaluate_cli_inputs.py | New coverage for evaluate bad --custom-questions and --output-dir. |
| dokimi/tests/test_eval_questions_file_errors.py | New coverage for eval checklist/cross-judge --grounded bad --questions handling. |
| dokimi/tests/test_eval_grounding_output_errors.py | New coverage for eval grounding/judge-validity unwritable --output. |
| dokimi/tests/test_benchmark_command_question_set.py | Adds coverage ensuring benchmark run rejects unusable --questions-from before pipeline starts. |
| dokimi/tests/test_answers_command.py | Adds coverage for unreadable/non-UTF8 runtime config handling. |
| dokimi/scripts/growth_baseline.json | Updates complexity/line baselines impacted by refactors. |
| dokimi/dokimi/report/model_compare.py | Makes load_run() emit clearer errors on read/decode failures. |
| dokimi/dokimi/report/eval_report.py | Adds _load_eval() to validate/normalize eval JSON input errors. |
| dokimi/dokimi/commands/report.py | Wraps report write path creation/write with OSError handling and exit 1. |
| dokimi/dokimi/commands/orchestrate.py | Wraps EvaluationPipeline creation to catch input/output-dir OS failures. |
| dokimi/dokimi/commands/judge_reliability.py | Adds _load_points() to ensure bad --questions exits 2 with a message. |
| dokimi/dokimi/commands/evaluate.py | Adds _write_json_or_exit() and uses it for command outputs. |
| dokimi/dokimi/commands/benchmark.py | Adds _check_questions_from() for preflight/run validation of --questions-from. |
| dokimi/dokimi/commands/answers.py | Handles unreadable/non-UTF8 YAML config with clean exit 1 + message. |
| dokimi/changelog.d/20261025_120000_vchamarthi_cli-input-eval-report.md | Changelog entry for the improved CLI error reporting. |
Review effort: Lite
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #236 +/- ##
==========================================
+ Coverage 92.29% 92.92% +0.62%
==========================================
Files 239 241 +2
Lines 29969 30582 +613
Branches 4929 5043 +114
==========================================
+ Hits 27661 28417 +756
+ Misses 1627 1462 -165
- Partials 681 703 +22
🚀 New features to boost your workflow:
|
antonwolfy
left a comment
There was a problem hiding this comment.
The fixes that are in place hold up, but the PR leaves sibling paths in the same commands with the traceback it sets out to remove, and one new exit code contradicts the docs. Four points inline.
What I ran, on a Linux review host at cbb1fc1:
make ci-local: 29 passed, 0 failed, 7 skipped; 8241 tests passed, 2 skipped.- Reproductions for the code findings, with the LLM transport mocked.
- Mutation run on the new tests: 29 mutants, 5 of which pass the full suite. Restoring any changed module to the base fails at least 2 new tests.
Copilot's point on except Exception in _check_questions_from does not apply: it is the same catch that cmd_benchmark_preflight had at the base, and SystemExit is not an Exception. Copilot's point on the unused load_questions_payload import does not apply: cmd_benchmark_preflight still calls it for the golden set at benchmark.py:181.
Checked, no issues
- Exit codes:
evaluate,report,benchmarkand the grounding write exit 1, the questions loaders exit 2; each is pinned by a test. - Behaviour:
_load_evalrejects no input that worked before;cmd_evaluate'sexcept OSErrorcovers only the constructor. - Repo rules: changelog fragment, growth baseline, ruff and mypy are clean.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to 0644d19, which fix the nit from my review:
0644d19:dokimi/tests/test_evaluate_cli_inputs.py:35: the assertion now checks that stderr starts withError:and names the blocker path, not the OS message.
make ci-local at 0644d19: 29 passed, 0 failed, 7 skipped.
Not approving yet. Still open: the three risks from my review.
|
@exai-bot /review |
exai-bot
left a comment
There was a problem hiding this comment.
Note
AI-generated review of 0644d19d8bc5, requested by @vchamarthi, using the default review skill. It can be wrong: verify a finding before acting on it. React with a thumbs up or down on a finding to tell the bot whether it helped.
Model us.anthropic.claude-sonnet-5-5, effort default, 13 of 94 turns, 1.0 of 48 minutes, 463k in / 6k out tokens.
The change wraps the read and write paths of eval, report, answers, benchmark and evaluate so OS and decode errors exit with an Error: … line naming the path. The new tests cover each path. I read the diff and the code around it but could not run the tests, because pytest is not installed in the container. The input-side handling looks sound, but two output paths that the changelog's "unwritable --output" wording suggests are covered still end in a traceback.
1 finding is on the changed lines, as comments below.
Findings outside the diff
1. Minor (confirmed, correctness): report eval --out and report model-compare --out still traceback when unwritable
dokimi/dokimi/report/eval_report.py:220
- The PR and the changelog say a command exits with a message on "an unwritable
--output". That holds forreport generate,eval groundingandeval judge-validity, but not for two otherreportsubcommands. generate_reportineval_report.pystill callsos.makedirs(dir_path),open(out_path, "w")andopen(qa_json_out, "w")unguarded.cmd_report_evalonly catchesRecordError, soreport eval --out <dir or path under a file>raises a rawOSErrortraceback. The new_load_evalcovers only the read side.report model-compare --outwrites throughPath(out_path).parent.mkdirandwrite_textinmodel_compare.py(lines 736-737), also unguarded.- The standalone
main()ineval_report.py(python -m dokimi.report.eval_report) does not catch theRecordErrorthat_load_evalnow raises, so a bad--evalthere still shows a traceback.
Suggested fix:
Wrap the output writes in generate_report and generate_combined_report so they raise RecordError or SystemExit with cannot write <path>: <reason>. Alternatively, catch OSError in cmd_report_eval and cmd_report_model_compare. Add a test for each, like the existing report generate one.
For a human reviewer
- Is exit code 1 right for
evaluateandbenchmark runinput errors, wheneval checklistandcross-judgeuse 2 for bad input?
…ritable report output report generate, report eval and report model-compare catch OSError on the questions read and the report write. eval judge-validity exits 2 when --output cannot be written, matching its documented malformed-input code. Pin the benchmark run questions-from pre-check on the multi-run path and for a directory source, and the report eval directory input. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
exai-bot
left a comment
There was a problem hiding this comment.
Note
AI-generated review of 8137a3c95996, requested by @vchamarthi, using the default review skill. It can be wrong: verify a finding before acting on it. React with a thumbs up or down on a finding to tell the bot whether it helped.
Model us.anthropic.claude-sonnet-5-5, effort default, 13 of 96 turns, 1.4 of 48 minutes, 725k in / 8k out tokens.
Re-review of 8137a3c95996: 4 new findings.
4 findings are on the changed lines, as comments below.
| questions_from = getattr(args, "questions_from", None) | ||
| questions_root = getattr(args, "questions_root", None) | ||
|
|
||
| if questions_from: |
There was a problem hiding this comment.
Nit (plausible, maintainability): Questions file is parsed twice per benchmark run
cmd_benchmark_run now loads and validates the --questions-from set via _check_questions_from, and the pipeline reads it again in _read_question_inputs (and again for every run in a multi-run). It is cheap for normal sets and gives a clean error, so this is acceptable. The pipeline's own FileNotFoundError and ValueError for the same input are now partly redundant.
There was a problem hiding this comment.
Acceptable as is: the pre-flight check gives a clean exit-2 message before any run starts, and the pipeline reads stay the authoritative path for other callers. Leaving the double parse; no change.
🤖 Addressed by Claude Code
Keeps cmd_report_model_compare within its complexity baseline. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
napetrov
left a comment
There was a problem hiding this comment.
Review at 11f9e77. Earlier rounds covered the code; this note is only about landing this PR next to the others in the set.
What I ran: the eight touched test files on 11f9e77: 143 passed. ruff check: clean. git merge-tree against current main: no conflict.
Conflict with #237 in dokimi/commands/answers.py. Both PRs change the same except line in _load_runtime_config: this PR has except (OSError, UnicodeDecodeError), #237 has except (OSError, ValueError). git merge-tree refs/pull/236/head refs/pull/237/head reports a content conflict on that one line. The two versions act the same here, because UnicodeDecodeError is a subclass of ValueError. Whichever PR lands second should keep #237's ValueError form, which also covers the shape checks that follow. tests/test_answers_command.py merges cleanly.
scripts/growth_baseline.json is also changed by #233, #235 and #239. Regenerate it on the final base when this lands. Do not merge the committed copy as it is.
Still open from the bot review at 8137a3c: four findings: other eval subcommands still traceback on an unwritable --output, report eval blames --out for a sidecar failure, exit codes differ between input and output failures, and the questions file is parsed twice. None of them has a reply yet. Either fix each one or say why it can wait.
…report sidecar Route the judge-reliability, score, panel-score and ragas writers through one exit-with-message helper, exit 2 for an unwritable grounding output like judge-validity, name the Q&A sidecar when it cannot be written, and stop the sidecar from overwriting an --out that has no .md suffix. Catch ValueError in the answers runtime-config read to match the sibling change. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 3331949. The new guards are right and every open point from earlier rounds is fixed. One test gap is left before merge: most of the write guards added in the last commits are not pinned by a behavioural test.
What I ran, on a Linux review host at 3331949:
make ci-local: 29 passed, 0 failed, 7 skipped; 8255 tests passed, 2 skipped.- Mutation run on the new code: 27 mutants, 14 of which pass the new test files. Those 14 also pass the nine existing test files that exercise these commands (231 tests).
- Reproductions for the code paths, with the LLM transport mocked.
Checked, no issues
- Judge errors vs. the new
except OSError:panel.evaluate_answersandjudge.evaluate_answerscatch provider failures per answer, so the new catch ineval panel-scoreandeval scoresees only write errors. - Exit codes:
evalwriters exit 2, matching the documented malformed-input code;reportandbenchmarkexit 1. Each is pinned for grounding, judge-validity, score and the threereportpaths. - Sidecar name:
report eval --outwithout.mdno longer overwrites the report with its own sidecar; the test fails when the oldreplaceis restored.
Cover eval pairwise, checklist, judge-wording and cross-judge (JSON and .md sidecar), the ragas results write and the panel-score OSError handler, each asserting exit 2 and the failing path. Replaces the source-text check. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
…so the two PRs merge cleanly Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 5ebaeca. The tests added since 3331949 pin every write guard I mutated, and the comment change in answers.py is right. One gap is left in the command the PR names: answers generate --output still ends in a traceback.
What I ran, on a Linux review host at 5ebaeca:
make ci-local: 29 passed, 0 failed, 7 skipped; 8263 tests passed, 2 skipped.- Mutation run on the new write-guard tests: 22 mutants (exit code 2 to 1,
except OSErrorremoved, guarded write replaced by a barewrite_text,.mdsidecar guard removed) acrosspairwise,checklist,judge-wording,cross-judge,ragas,panel-score,score,groundingandjudge-validity. 21 fail a test. The survivor changesexc.filename or output_pathtooutput_pathincmd_eval_panel_score, which only changes which path the message names. - A reproduction for the finding, with the answerer and the doc client stubbed.
Checked, no issues
except OSErrorineval scoreandeval panel-score:Judge.evaluate_answersand the panel catch provider errors per question, so the new catch sees only the incremental and final saves.- Sidecar name:
os.path.splitext(out_path)[0] + ".qa.json"no longer collides with an--outthat lacks.md. - Repo rules: growth ratchet tests pass, changelog fragment and SPDX headers are present, schema-compat is OK.
…te its output generate_answers saves incrementally and save_answers saves at the end; an unwritable --output now exits with 'Error: cannot write <path>' like eval score instead of a traceback. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
napetrov
left a comment
There was a problem hiding this comment.
Delta review of 11f9e77..34f8e41, plus one push of mine (5ebaeca).
Resolved at 3331949/34f8e41: the four open findings from the 8137a3c bot review.
- Every
evaloutput write now goes throughwrite_text_or_exit(pairwise, checklist, judge-wording, cross-judge, ragas, grounding), andscore/panel-scorecatchOSErrorfrom the save. report evalnow names the.qa.jsonpath when that sidecar fails, not--out.splitextalso fixes the.md-less--outcase.- Output failures now all exit 2.
- The double parse of the questions file is not touched in this delta. Fine to leave it for later.
Pushed 5ebaeca: the change is one comment in answers.py, now # ValueError covers bad UTF-8, the same text as #237. Your switch to ValueError left that comment as the only difference on the line. With it aligned, git merge-tree --write-tree of this PR and #237 (b42ded3) is clean.
Ran: ruff clean. 159 passed across the touched test files. CI 24/24 green on 5ebaeca.
Still to do: regenerate scripts/growth_baseline.json on the final base, because it is shared with #233/#235/#239.
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. The answers generate write guard is right: the incremental save and the final save both exit 2 with Error: cannot write ..., matching eval score, and both are pinned by a test. One nit inline.
What I ran, on a Linux review host at 33bb01e:
make ci-local: 29 passed, 0 failed, 7 skipped.- Mutation run on the new guard: 8 mutants (exit 2 to 1,
except OSErrorremoved or narrowed toFileNotFoundError, either save moved outside thetry,strerrorformatting changed, message path changed). 6 fail a test. The 2 survivors only change which path the message names. - Reproductions with the answerer and doc client stubbed:
--output <regular file>/a.jsonand--output <existing directory>both exit 2 with anError:line and no traceback.
Checked, no issues
- Provider errors:
Answerer.generate_answerscatches per-question failures (answerer.py:164), so aConnectionErrorfrom retrieval is recorded as an error record and never reaches the newexcept OSError. - Save failure in the pool: the incremental save sets
stopand re-raises (answerer.py:190-193); theOSErrorleavesf.result()and the queued questions return at once. Exit 2 at concurrency 1 and 2. --ragasstep: its sidecar write already goes through_write_json_or_exit(..., code=2).- Repo rules: changelog fragment and SPDX headers are present, growth ratchet tests pass, no docs state an exit code for
answers generate.
… generate cannot write Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at fabe4be. The answers generate message now names the --output path as passed, and the write guard holds. One risk is left in the same command, and two nits are inline.
What I ran, on a Linux review host at fabe4be:
make ci-local: 29 passed, 0 failed, 7 skipped; 8265 tests passed, 2 skipped.- Reproductions with the answerer and doc client stubbed:
--output <existing directory>exits 2 withError: cannot write <dir>: Is a directory;--output <regular file>/a.jsonexits 2 with anError:line and no traceback. - Mutation check on the new
answers generatetests: changing{output_path}back to{exc.filename or output_path}passes all 14 tests intest_answers_command.py.
Still open in answers generate:
answers generate --questions <missing file or directory>still ends in a traceback._load_questions(answers.py:46-53, outside this diff) catches onlyValueError.--questions nope.jsongivesFileNotFoundError: [Errno 2] No such file or directory: '.../nope.json', and--questions <dir>givesIsADirectoryError: [Errno 21]. A non-UTF-8 file already exits 1 with a message. This predates the PR, but the PR text namesanswersand its questions input, andeval checklistandeval cross-judgeget the same fix here. Suggestion: catchOSErrornext toValueErrorin_load_questionsand exit 1 withError: cannot read {path}: {exc.strerror or exc}, as_load_runtime_configdoes, and add a test for a missing path.
Checked, no issues
- Provider errors:
Answerer.generate_answersrecords per-question failures, so the newexcept OSErrorsees only saves. - Repo rules: changelog fragment, SPDX headers, growth ratchet and schema-compat are clean.
…put path in write-error tests _load_questions now catches OSError (missing file, directory) and exits 1 naming the path. eval score and panel-score report the --output path, not the temp sibling. Tests use temp-named PermissionError filenames and a real directory --output. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. The three points from my last review are fixed: eval score and eval panel-score now name the --output path as passed, the tests now fail on a message that names the temp file, and answers generate --questions <missing file or directory> exits 1 with Error: cannot read <path>: <reason> instead of a traceback.
What I ran, on a Linux review host at eeebd1b:
make ci-local: 29 passed, 0 failed, 7 skipped.- Mutation run on the delta: 16 mutants (
{output_path}reverted to{exc.filename or output_path}at each of the three sites,OSErrorhandler in_load_questionsremoved or narrowed toFileNotFoundError, exit codes changed,strerrordropped, either save moved out of thetry). 15 fail a test. answers generate --questionson a missing path and on a directory through the CLI entry point: both printError: cannot read <path>: ...and exit 1, no traceback.
Checked, no issues
- Handler order:
ValueError(which coversUnicodeDecodeErrorandJSONDecodeError) is caught beforeOSError, and the two do not overlap. - Temp-file names: the updated tests raise
PermissionErroron a.123.0.tmpsibling, so a message that names the temp file fails them. - Provider errors:
Answerer.generate_answersrecords per-question failures, so the newexcept OSErrorsees only saves.
test_answers_command.py: keeps main's _stub_generate and write-error tests (intel#236) and this branch's --doc-source none test. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

Summary
eval,report,answers,benchmarkandevaluateread user-supplied files (questions, eval and run files, runtime config) and write--outputwithout handling OS or decode errors, so a bad file or path ends in a traceback.Changes
Each affected read or write now reports the path and reason in a one-line message and exits. One commit per command.
Risk
Error paths only.
Testing
make ci-local(offline), Python 3.13: everything passes exceptmypy(1 error) and the 10 tests that also fail onmain(process, sandbox, container probes; macOS). Load-flaky tests pass alone.Checklist
pytestpasses (apart from the 10 pre-existing failures)ruff,mypy, and schema checks pass (mypy: the one pre-existing error).github/.licenserc.yaml)