Skip to content

feat(questions): run Inspect, OpenAI evals, skill-creator and promptfoo sets on the answers track - #214

Merged
napetrov merged 32 commits into
intel:mainfrom
napetrov:feat/run-answer-formats
Oct 27, 2026
Merged

napetrov merged 32 commits into
intel:mainfrom
napetrov:feat/run-answer-formats

Conversation

@napetrov

Copy link
Copy Markdown
Contributor

Stacked on #210 (tasks inspect). Review only the top commit until #210 merges.

Summary

This PR runs the answer-only formats that tasks inspect detects on the existing questions, answers and judge track. Each format gets an import adapter into one canonical record, so there is no parallel runner.

questions import <path>        # Inspect samples/@task (ast, never imported), OpenAI evals JSONL,
  -> questions.v2              # skill-creator evals.json, promptfoo config
     + prompt_style: verbatim
     + grading.checks[]        # match|exact|fuzzy|regex|choice|json|judge|unsupported
answers generate / arms run    # unchanged, except that verbatim questions are sent unwrapped
eval reference                 # deterministic checks: no model; judge: checklist, both polarities
  -> eval.v1 | arms
eval native-import <log>       # inspect json log / promptfoo -o json -> answers.v1 + eval.v1
 dokimi/
 ├── commands/
+│   └── answer_formats.py        # questions import, eval reference, eval native-import
 ├── eval/
+│   ├── reference_checks.py      # the check types
+│   └── reference_run.py         # answers/arms -> eval.v1/arms
 └── task_formats/
+    └── imports/                 # one adapter per format + native_logs

What does not carry over is listed in docs/answer-formats.md. The main points:

  • Only one message is sent: system and few-shot turns are folded into the prompt. No tools or sandbox are provided.
  • Inspect solvers other than multiple_choice/system_message are not reproduced.
  • Model-graded scorers (Inspect C/P/I, OpenAI fact, promptfoo llm-rubric) all become the yes/no checklist judge.
  • promptfoo js/py asserts, similar, cost and latency become unsupported, which is reported and not counted.

Evidence

Haiku 4.5 on Bedrock, at most 20 samples per set. Native tools were pinned: inspect-ai 0.3.283 and promptfoo 0.125.0. OpenAI evals graders were reproduced from upstream 8eac7a7d.

Set Grader n dokimi / native pass Agreement
born-first OpenAI Match 20 20 / 20 1.00
chinese-remainder-theorem OpenAI Includes 15 12 / 12 1.00
logic-fact OpenAI ModelBasedClassify fact 10 8 / 8 0.80
ARC-Easy, same completions Inspect choice 20 20 / 20 1.00
biology_qa, same completions Inspect model_graded_qa 20 20 / 19 0.95
riddles promptfoo icontains + llm-rubric 15 14 / 15 0.93
3 skill-creator sets grader.md, per expectation 77 0.88

The deterministic graders agree on 100% of the same completions. Every disagreement is a judge verdict. A split verdict (0.5) fails here, which is stricter than Inspect P.

Tests:

  • Before: tasks inspect born_first.jsonl gave consumers: [none-yet].
  • After: it gives [questions import].
  • New suites: tests/test_answer_imports.py (20 tests) and tests/test_reference_checks.py (28 tests). Both are offline and use a fake judge.
  • make ci-local passes 28 checks and fails 1: pip-audit flags filelock and setuptools in uv.lock, which this PR does not change.

Merge Danger

Door: two-way. The new commands and modules are additive. Two schemas are widened: questions.v2 gains grading and prompt_style, and task_inspect.v1 accepts the questions import consumer.

Blast Radius: small. The answerer changes only for questions with prompt_style: verbatim. answers generate --doc-source none is a new opt-in flag. benchmarks/spec.v1.yaml max_docs goes from 31 to 32 for the new doc.

Decisions for reviewers:

  1. Should a split judge verdict fail?
  2. Should a verbatim prompt in context arms get the docs before it?
  3. Should skill-creator imports carry expected_skill?
  4. Is --doc-source none acceptable as a flag?

🤖 Generated with Claude Code

@github-actions github-actions Bot added the cli-interface-change Changes dokimi's user-facing CLI surface label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

CLI interface change detected

This PR changes dokimi's user-facing CLI surface (commands and/or options):

+ added command `dokimi eval native-import`
    + argument `log`
    + option `--answers-out`
    + option `--eval-out`
    + option `--help/-h`
+ added command `dokimi eval reference`
    + option `--answers`
    + option `--concurrency`
    + option `--help/-h`
    + option `--judge-model`
    + option `--judge-provider`
    + option `--no-judge`
    + option `--output/-o`
    + option `--questions`
+ added command `dokimi questions import`
    + argument `path`
    + option `--format`
    + option `--grader`
    + option `--help/-h`
    + option `--output/-o`
    + option `--scorer`
    + option `--task`

Updated on every push; the cli-interface-change label is on while the diff is non-empty.

@codecov-commenter

codecov-commenter commented Oct 24, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 86.36722% with 173 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.68%. Comparing base (59de48a) to head (f814c85).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
dokimi/dokimi/task_formats/imports/inspect_ai.py 81.12% 18 Missing and 19 partials ⚠️
dokimi/dokimi/task_formats/imports/promptfoo.py 80.92% 19 Missing and 14 partials ⚠️
dokimi/dokimi/commands/answer_formats.py 65.90% 27 Missing and 3 partials ⚠️
dokimi/dokimi/eval/reference_checks.py 89.85% 16 Missing and 12 partials ⚠️
dokimi/dokimi/task_formats/imports/common.py 85.43% 9 Missing and 6 partials ⚠️
dokimi/dokimi/task_formats/imports/openai_evals.py 87.34% 5 Missing and 5 partials ⚠️
dokimi/dokimi/task_formats/imports/native_logs.py 89.33% 4 Missing and 4 partials ⚠️
...okimi/dokimi/task_formats/imports/skill_creator.py 86.27% 4 Missing and 3 partials ⚠️
dokimi/dokimi/commands/answers.py 70.00% 3 Missing ⚠️
dokimi/dokimi/task_formats/imports/__init__.py 91.30% 1 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #214      +/-   ##
==========================================
- Coverage   92.94%   92.68%   -0.26%     
==========================================
  Files         241      252      +11     
  Lines       30647    31899    +1252     
  Branches     5058     5321     +263     
==========================================
+ Hits        28485    29567    +1082     
- Misses       1460     1563     +103     
- Partials      702      769      +67     
Files with missing lines Coverage Δ
dokimi/dokimi/commands/evaluate.py 89.87% <100.00%> (+0.06%) ⬆️
dokimi/dokimi/commands/questions.py 78.38% <100.00%> (+0.15%) ⬆️
dokimi/dokimi/eval/answerer.py 94.27% <100.00%> (+0.09%) ⬆️
dokimi/dokimi/eval/arm_runner.py 94.46% <100.00%> (ø)
dokimi/dokimi/eval/reference_run.py 100.00% <100.00%> (ø)
dokimi/dokimi/task_formats/detect.py 98.29% <100.00%> (ø)
...okimi/dokimi/task_formats/imports/promptfoo_csv.py 100.00% <100.00%> (ø)
dokimi/dokimi/task_formats/links.py 100.00% <100.00%> (ø)
dokimi/dokimi/task_formats/model.py 97.01% <100.00%> (+0.29%) ⬆️
dokimi/dokimi/task_formats/imports/__init__.py 91.30% <91.30%> (ø)
... and 9 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@xaleryb

xaleryb commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Ran the full test suite, ruff, mypy and bandit on the head plus the commits below. Re-ran each finding's input through the CLI.

Open:

  • promptfoo CSV __expected javascript:..., not-contains: bye, grade: is funny, contains-any: apple,banana -> each becomes a literal exact match counted as a failure -> should follow assertionFromString: code asserts unsupported, grade: a judge check, not-* negated, *-any/*-all values split on commas.
  • arms run on a prompt_style: verbatim question -> the prompt is wrapped in "Answer the following user question. Be specific..." -> the same bare prompt answers generate sends.
  • A match check plus a judge check, where the judge call fails or --no-judge is set -> passed, and counted as scored in the tally -> at least a count of unjudged questions in the summary.

Pushed:

  • 96170cd: eval reference --no-judge on an arms file failed arms.v1 validation and wrote nothing.
  • 369a9d4: an errored Inspect sample was graded as a wrong answer; it is now unscored.
  • cc99eb0: a partial FieldSpec dropped id/choices; a CSV choices string was not split; a target matching no choice made eval reference refuse the whole set.
  • 00b6731: a .txt prompt file was not split on --- lines.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 00b6731. The adapters and the check types are a sound design, and the verbatim baseline path works. Several graders are imported wrongly, though, and each one reports a wrong pass or drops the set. I'd fix the inline bugs before merge.

What I ran, on a Linux review host at 00b6731:

  • make ci-local: 26 passed, 3 failed, 7 skipped. 7,998 tests passed, changed-line coverage 91%. The three FAILs are the gitleaks install, the gitleaks scan and pip-audit, which fail on the lock files and the tool download, not on this PR's code. Running gitleaks directly over the PR's 10 commits finds no leaks.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the new importers and checks: 41 mutants of reference_checks.py, reference_run.py and the answerer, 18 of which pass both new test files; 45 mutants of the importers, 26 of which pass test_answer_imports.py.

I agree with @xaleryb that arms run ignores prompt_style: verbatim. New evidence: grep verbatim dokimi/eval/arm_runner.py returns nothing, and arm_runner.py:322 formats ANSWER_PROMPT_PLAIN, so docs/answer-formats.md:19 and :37 (arms run, and "a context arm puts the docs first and the prompt after them") describe behaviour that does not exist.

Findings that cannot be anchored inline:

  1. The PR is stacked on #210, which AGENTS.md forbids. git log origin/main..HEAD lists 10 commits. Three are #210's (f4ce883, b9dd1a8, e4b4e80). They account for 76 of the 102 changed files and 4,854 of the 6,423 added lines. The description says "Review only the top commit". Rebase onto the base once #210 merges, or land #210 first. The task-format-detect fragment also announces tasks inspect again, and the answer-formats fragment lists "task_inspect.v1 accepts the questions import consumer" as a change to a schema that is new in this diff.
  2. validate_grading accepts blocks that later abort the whole scoring run. reference_checks.py:316-330 checks the type and the values only. A regex that does not compile, location: middle, mode: ALL, weight: "high" and a judge check with no criterion all pass it, then raise inside the thread pool (:163, :242, :269, :292, :297). eval reference exits 2 with no output file, after judge calls for other questions were paid for, and the message names no question id. Validate regex compilation, location, mode, numeric weight and judge criterion in validate_grading.
  3. A regex check has no backtracking guard. reference_checks.py:267 runs re.search(str(v), answer) with a pattern from an imported config. (a+)+$ against "a" * 26 + "b" takes 3.05 s, and the cost doubles per added character. A promptfoo regex assert with a nested quantifier hangs eval reference in a worker thread.
  4. The new tests let real regressions through. Mutation check: switching the with-docs verbatim prompt to the wrapped template, disabling the --doc-source none branch (answers.py:83), > instead of >= on the threshold (reference_checks.py:108), dropping judge negate, regex ignore_case, making choice a subset test, one-way fuzzy, and swapping numeric begin/end all pass the full new suites. On the importer side, the OpenAI includes ignore_case default, contains-all mode, icontains, starts-with, the Inspect match defaults and the choice-target mapping do too. Add a test per mapping that asserts the check dict for one real row of each format, and a grading test at the exact threshold.

Checked, no issues

  • Schemas: questions.v2.json only adds optional prompt_style and grading. task_inspect.v1 is new and registered in SCHEMA_VERSIONS. eval reference on an arms file keeps the with_docs and without_docs keys and validates against eval.v1.
  • Docs and CLI: the flags in answer-formats.md and the changelog match --help for questions import, eval reference, eval native-import and answers generate --doc-source none. mkdocs build --strict passes with the nav entry.
  • Path escape: an Inspect json_dataset symlink or ../ path and a promptfoo file:// prompt outside the config directory are refused. Tree scans skip symlinked children.
  • Judge failures: a failing judge call gives a None score, which stays unscored. Negation of a 0.5 split stays not passed.

Comment thread dokimi/dokimi/task_formats/imports/openai_evals.py Outdated
Comment thread dokimi/dokimi/task_formats/imports/openai_evals.py
Comment thread dokimi/dokimi/task_formats/imports/inspect_ai.py Outdated
Comment thread dokimi/dokimi/task_formats/imports/inspect_ai.py
Comment thread dokimi/dokimi/eval/reference_run.py Outdated
Comment thread dokimi/dokimi/commands/answer_formats.py
@xaleryb

xaleryb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Merged main after #210 landed (e6d30d1): conflicts merged three-way against e4b4e80, the commit this branch was built on. The PR's own diff against main is the same 30 files, line for line; the consumers enum and the docs table now carry both questions import and main's catalog trigger-set.

On top, besides the inline fixes:

  • 9fe3a58: questions import refuses a file whose detection lists alternatives and names the candidates; --format picks one. joke_fruits.jsonl alone was imported as inspect-samples with empty match values; now it exits 2 with ambiguous format (inspect-samples, openai-evals), and with --format openai-evals it imports.
  • a0d16ff: validate_grading reports everything eval reference would fail on (a regex that does not compile, a bad match location/mode, a non-numeric weight/threshold, a judge check with no criterion or reference) before any judge call.
  • 2b936f3: arms run sends a prompt_style: verbatim question unwrapped, after the context block on a context arm; a multi-turn session arm still wraps its opening turn, and the docs say so.
  • e17a773: tests for the 13 surviving mutants from the review; each fails under its mutant.

Full suite, ruff, mypy and bandit pass on Linux at e17a773.

Still for the author: regex backtracking (Python re has no timeout, so this needs a choice: the regex package, a subprocess, or refusing nested quantifiers); promptfoo __expected following assertionFromString; what the summary reports for a match+judge question whose judge failed or was skipped with --no-judge.

@xaleryb

xaleryb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

A proposal for the regex backtracking point, the one open item here with a security angle (a set's regex check can stall eval reference on a crafted pattern or answer).

Python re has no timeout. regex is already in requirements.lock.txt (2026.9.29, pulled in transitively), and its search/fullmatch take timeout=. Proposal: compile and run the regex check through regex with a per-check timeout (one second, say), and on TimeoutError mark that check unscored with the reason, as an unsupported check is, instead of failing or passing it. That means listing regex as a direct dependency (the deptry gate requires it) and one test with a catastrophic pattern that must return within the timeout.

If that works for you, I can push it here.

@xaleryb

xaleryb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Two commits on top of your latest push:

6598cd9: regex checks run with a timeout, as proposed above. The regex check goes through regex.search(..., timeout=) with one 1 s deadline per check; a check that runs past it is unscored with not scored: regex ran past 1s, the same shape as an unsupported check, and validate_grading compiles with the same engine. regex is now a direct dependency (pyproject.toml, requirements.txt); the lock already pinned 2026.9.29 and is unchanged, and deptry is clean. (a|aa)+$ on 40 a plus !: before, still running when killed after 30 s; now unscored after 1 s. One trade-off: on heavy backtracking regex is about 2.7x slower than re when it does finish, so a pattern near the limit now comes back unscored instead of with a verdict.

224aa36: a CSV __expected cell parses as promptfoo does. Reviewing a08fbb2 against promptfoo's src/csv.ts (assertionFromString) and src/validation/parseCommaSeparatedValues.ts at b4c2404, several cells differed, e.g.:

  • contains-all: apple, banana failed on "banana and apple" (the values were not trimmed);
  • contains-any: apple,,banana became unsupported (promptfoo skips empty fields);
  • contains-any: "a, b", c split inside the quotes (promptfoo's escape is a quoted field, not \,);
  • is-json, contains-json, factuality: ..., model-graded-closedqa: ..., llm-rubric(0.5): ... became an exact match on the whole cell, failing every answer;
  • fn: ..., file://check.js, file://check.py became scored equals instead of javascript/python;
  • " foo " was not trimmed, and contains : foo was read as contains.

The new imports/promptfoo_csv.py ports both functions with promptfoo's assertion type list (copied from src/types/index.ts, source and commit named), and the resulting {type, value, threshold} goes through _check, the mapper the YAML path already uses, so _CSV_TYPES is gone and a CSV cell grades exactly like the same YAML assert. Checked against a node harness built from promptfoo's own TypeScript: identical on 90 named cells, the 24 parseCommaSeparatedValues cases from promptfoo's tests and 20,000 random cells. One pinned case changed: grade: is funny keeps promptfoo's untrimmed criterion " is funny".

Full suite, ruff, mypy, bandit and deptry pass on Linux at 224aa36.

Not changed, both on the YAML path too: a bare regex or equals assert with no value becomes the string "None" in _check, so {type: regex} matches any answer that contains "None"; and a CSV header __expected (with a space) is read as a var, where promptfoo trims column names.

@xaleryb

xaleryb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Pushed b7d7bfd for the first "not changed" item above: a text assert (regex, equals, starts-with, contains*, and their not- forms) with no value is now unsupported on both the YAML and the CSV path, instead of a regex for "None" or a contains that fails every answer. 6 cases; they fail without the fix. Full suite passes on Linux.

@xaleryb xaleryb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at b7d7bfd: every review point is fixed at this head (a CSV header with a leading space is the one minor item left, named in the thread), the full suite, ruff, mypy, bandit and deptry pass on Linux, and CI is green.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at b7d7bfd. All six of my earlier threads are fixed. The new checks and importers hold up, but two import paths still grade or crash wrongly, and the new tests let several mutants through. Details are inline.

What I ran, on a Linux review host at b7d7bfd:

  • make ci-local: 28 passed, 1 failed, 7 skipped. 8,423 tests, changed-line coverage 91%. The one FAIL is test_a_regex_that_backtracks_past_the_timeout_is_unscored[True], which asserts the timed-out check returns in under 2 s; it took 2.9 s with other jobs running and passes alone (40 passed for the file). See the nit inline.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the new checks, importers and answerer: 88 mutants, 10 of which pass the new test files.

Findings that cannot be anchored inline:

  1. Nothing pins that a question without prompt_style: verbatim keeps the expert wrapper, or that answers generate --doc-source none works. Mutation check: verbatim = True for every question in answerer.py (around line 240) and removing the none branch in answers.py (around line 83) both pass the 167 answerer, arm-runner and answers tests. The first would strip the wrapper from every existing question set. Add one Answerer test with a plain question that asserts the wrapped prompt, and one answers generate --doc-source none test.
  2. Four Inspect and promptfoo mappings that the docs describe are not pinned. These mutants pass the suite: multiple_correct not passed through the json_dataset path, csv_dataset accepted, Inspect includes dropping punctuation handling, a negated judge assert (not-llm-rubric) let through as a judge check without negation, and a defaultTest threshold no longer inherited. The not-llm-rubric to unsupported mapping is also missing from answer-formats.md.

Still open from my earlier review: none.

Checked, no issues

  • Earlier threads: joke_fruits.jsonl with a model-graded spec imports; a non-JSON json_match ideal becomes unsupported; Inspect pattern() is refused; the multiple_correct prompt uses the multiple-answer template; a fallback_none answer is unscored and delta is None; malformed expectations, files and vars end as Error with exit 2.
  • Regex timeout: it holds inside the worker pool (8 timing-out grades on 4 threads returned in 1.8 s) and negate on a timed-out check stays unscored.
  • Docs and CLI: the flags, example commands and mapping rows in answer-formats.md match the code; mkdocs build passes with the nav entry; max_docs 32 matches the file count; questions.v2 and task_inspect.v1 are only widened.
  • Deps: regex is a direct dependency in pyproject.toml and requirements.txt, and the lock already pins a satisfying version; deptry is clean.

Comment thread dokimi/dokimi/task_formats/imports/promptfoo.py Outdated
Comment thread dokimi/dokimi/task_formats/imports/openai_evals.py
Comment thread dokimi/dokimi/eval/reference_checks.py
Comment thread dokimi/dokimi/eval/reference_checks.py
Comment thread dokimi/dokimi/eval/reference_run.py
Comment thread dokimi/tests/test_reference_checks.py Outdated
Comment thread dokimi/docs/answer-formats.md Outdated
Comment thread dokimi/docs/answer-formats.md Outdated

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I pushed a commit per nit, up to 56b1981, which fix the three nits from my review:

  • 7c2abfe: dokimi/tests/test_reference_checks.py:100: the timing assert now allows 5 * REGEX_TIMEOUT.
  • b966e5e: dokimi/docs/answer-formats.md:61: the checklist link now points to benchmarking-and-comparison.md.
  • 56b1981: dokimi/docs/answer-formats.md:60: with contains, the answer may be any JSON value, or else it is the first JSON object or array anywhere in it.

make ci-local at 56b1981: 29 passed, 0 failed, 7 skipped.

Not approving yet. Still open: the two bugs and the five risks from my review.

napetrov and others added 10 commits October 26, 2026 08:59
…oo sets on the answers track

questions import translates the answer-only formats that tasks inspect
detects into questions.v2 records with a format-neutral grading block.
eval reference scores answers or arms output against those checks:
deterministic checks need no model, judge checks use the checklist judge
in both polarities. eval native-import reads Inspect json logs and
promptfoo outputs so native verdicts sit next to dokimi's.

Inspect @task files are parsed with ast, never imported or run.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The skill an evals.json belongs to (skill_name, else the <skill>/evals/
directory) is each question's expected_skill, so the imported set also
scores skill selection on a skill-catalog: arm. Without either, the
import report notes it.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…o-judge

arms.v1 requires judge_model and judge_provider strings next to evaluations,
so eval reference --no-judge on an arms run file failed validation and wrote
nothing. Stamp both with the scorer name, as each score block already does.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
An Inspect sample that errored has an empty completion and no score; it was
stored as a plain empty answer, so eval reference graded it as wrong while
Inspect left it unscored. The error now goes on the answer, which grade()
already treats as unscored.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A partial FieldSpec dropped the id and choices columns Inspect reads by
default, a CSV choices string stayed one choice instead of read_choices'
comma split, and a sample whose target matched no choice was written with
an empty choice check that made eval reference refuse the whole set; it is
now skipped with a lossy note.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
promptfoo reads a .txt prompt file as several prompts separated by lines
that are ---; the importer sent the whole file as one prompt, so the
questions joined both prompts and lost promptfoo's <test>.p<prompt> ids.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
ModelBasedClassify samples often carry only input (registry/data/test_modelgraded/joke_fruits.jsonl), and every such row was skipped, so the set imported as zero questions. Only the deterministic templates need an ideal now; a model-graded row with neither an ideal nor spec criteria gets an unsupported check, since the spec's own prompt is not read.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
detect() reports a second format within its margin in report.alternatives, and import_path took the first one anyway: joke_fruits.jsonl alone imported as Inspect samples, with match checks that had no values. The import now stops and names the candidates; --format picks one.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…eference

With no retrieved docs the answerer writes "[FALLBACK: No relevant documentation found]" (doc_source fallback_none or none) as the context-arm answer, and eval reference graded it: a negated check passed it and the delta was corrupted. It now counts as unscored, like an errored answer.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
xaleryb and others added 15 commits October 26, 2026 08:59
…gex search

Inspect's pattern scorer compares the extracted group with the target; the import emitted a regex check that dropped the target, so ANSWER: 5 passed for target 4, and --scorer pattern on a samples file had no pattern at all and matched every answer. pattern() now imports as unsupported, and --scorer pattern on a samples file is refused.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…etters

Only cot was read from multiple_choice(), so multiple_correct=True samples were asked with the single-answer template ('ANSWER: $LETTER') while the choice check needs the whole target set. They now get Inspect's multiple-answer template.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
questions import catches ValueError only, so an inspect-task file that does not parse, skill-creator expectations: null, a promptfoo weight: null or vars: file://... gave a traceback; expectations or files given as a string were split into characters, and a truncated file:// JSON prompt became the question "None". Each is now a ValueError naming the file (null expectations read as none).

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
answer-formats.md says arms run answers an imported set, but the arm runner always formatted ANSWER_PROMPT_PLAIN or the context suffix around the question, so an exact-match or ANSWER: X grader saw a reply to a different prompt than answers generate sends. A verbatim question is now the whole prompt, after the context block on a context arm.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
… through

13 mutants from the review passed the suites: the with-docs verbatim prompt, the threshold at its exact value, judge negate, regex ignore_case, choice as a subset, one-way fuzzy, swapped numeric begin/end, and the OpenAI includes, promptfoo contains-all/icontains/starts-with, Inspect match default and choice-text mappings. One test per side now asserts each, and each mutant fails it.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…ring

A __expected cell was split on its first colon only for a fixed list of
types. javascript: became a literal equals check, not-contains: and
grade: did the same, and contains-any / contains-all kept a comma list
as one value. Cells now read as promptfoo's assertionFromString does:
code is unsupported (not run), grade: is an llm-rubric judge, a not-
prefix negates, and a *-any / *-all value splits on unescaped commas.
Red test: the promptfoo CSV test failed on the previous code.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
With no judge model, or a failed judge call, a judge check has no
verdict. A match check next to it still scored the record, so the
record was counted as a pass with no judged check behind it. The
summary now has judge_unscored: the number of counted records with a
judged check that has no verdict. The verdict itself is unchanged.
Red test: the tally test failed on the previous code (no such key).

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ed, not a pass

Every answer contains the empty string, so match [""] passed whatever the
answer said. A CSV cell "contains-any:" or a YAML value '' now imports as
unsupported and is not counted.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… unscored

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…es and map it like a YAML assert

The cell parser split on the first colon for a short list of types and
fell back to equals for the rest. is-json, factuality, llm-rubric(0.5):,
similar and fn: became literal equals checks, a *-any / *-all list kept
the spaces after its commas and its empty fields, quoted fields were not
read, and a blank cell was an equals check on whitespace.

promptfoo_csv.py ports assertionFromString (src/csv.ts) and
parseCommaSeparatedValues at promptfoo b4c2404a: the trimmed cell, its
legacy forms, the [not-]type(threshold):value regex over promptfoo's
full assertion type list, quoted list fields, equals otherwise. The
assert it returns goes through the same _check as a YAML assert.
A malformed quoted list field is an import error, as in promptfoo.

Tests mirror promptfoo's test/csv.test.ts and pin, per cell, the
promptfoo assert and that its CSV and YAML forms give the same check.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
… not graded

_check turned a missing value into the string "None": a bare regex assert passed any answer containing None, and a bare contains failed every answer. YAML and CSV now both report it as unsupported.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
@napetrov
napetrov force-pushed the feat/run-answer-formats branch from 56b1981 to 804c83b Compare October 26, 2026 16:28
- A blank ideal or target imports as unsupported, not as a check that
  passes every answer. A promptfoo assert with a blank value does too.
- eval reference refuses a match value that is blank once stripped, an
  empty regex pattern and a negative weight, before any run starts.
  Whitespace-only regex and unstripped match values stay valid.
- A regex check stops at the first pattern that settles its mode, so a
  later pattern past the limit no longer unscores a decided check.
- promptfoo threshold rejects booleans and non-finite numbers.
- OpenAI json_match serialises a structured ideal as JSON, not repr.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The five threads are fixed: the blank values, thresholds, weights and the timed-out regex are all handled. The blank-value guard has a gap that still passes every answer, so I would not merge yet. Three points are worth fixing before merge.

What I ran, on a Linux review host at 804c83b:

  • make ci-local: 29 passed, 0 failed, 7 skipped. Test suite and changed-line coverage pass; overall coverage 91%.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over reference_checks.py and reference_run.py: 24 mutants, 5 of which pass the three new test files. The five earlier mutants (any for all in the pass decision, the weight > 0 filter, unjudged on any unscored check, judge_unscored on the all-unscored path, a per-pattern regex timeout) now fail the suite.

Checked, no issues

  • Threshold and weight at import: promptfoo threshold: [1], true, .nan, .inf and weight: [1] end as Error: with exit 2.
  • Empty ideal / target: "", " ", [] and null give unsupported for the OpenAI match, includes, fuzzy_match and json_match templates and for the Inspect match, includes, exact and model-graded scorers.
  • validate_grading: weight: null and type: ["match"] return problem strings. Over 1,700 accepted check blocks, grade_check raised on none.
  • Timed-out regex: (a|aa)+$ on 40 a plus ! next to a passing match still grades passed: true, but the check shows passed: null and check_unscored: 1 appears in the summary. The artifact validates against eval.v1 and arms.v1.

Comment thread dokimi/dokimi/eval/reference_checks.py Outdated
Comment thread dokimi/dokimi/task_formats/imports/promptfoo.py Outdated
Comment thread dokimi/dokimi/eval/reference_checks.py
With the default ignore_punctuation, a match value of "..." or "!!" is
"" once normalised, and every answer contains "". blank_value now
applies the check's own punctuation normalisation, so validate_grading
reports the value and eval reference refuses the block. The Inspect
importer drops such a target for the match scorer, leaving the sample
unsupported when none is left. OpenAI evals and promptfoo keep
punctuation in every match they build, so a punctuation-only value
there is a real check; tests pin both.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
float() accepted weight: -1, .nan and .inf, so the file imported and
only eval reference refused it later. The weight now goes through the
same finite-number check as the test threshold, and a negative weight
is refused at import too.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…olds

An all-mode regex whose first pattern matches must still try the next
one, and validate_grading must refuse a nan or inf weight or threshold.
Neither was covered: replacing the stop condition with "True in hits",
or dropping math.isfinite from _is_number, passed the suite.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 1a9d30a. The three open threads are fixed: blank_value now applies the check's own normalisation, the promptfoo weight goes through the same finite-number helper as the threshold, and the all-mode and finite-number mutants fail the new tests. Two gaps of the same kind as the blank-value threads are left, one in the promptfoo importer and one in the Inspect importer. Both are inline.

What I ran, on a Linux review host at 1a9d30a:

  • make ci-local: 29 passed, 0 failed, 7 skipped. 8,621 tests, changed-line coverage check passes.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the delta: 20 mutants of blank_value, the Inspect norm and the promptfoo _finite/weight check, 3 of which pass test_answer_imports.py and test_reference_checks.py (the kind == "match" guard on the punctuation strip, weight < 0 to <= 0, and the Inspect exact case below). Your two earlier mutants (if (True in hits): and no math.isfinite) now fail 2 and 5 tests.

Checked, no issues

  • blank_value vs match_text: over every code point plus punctuation and whitespace combinations, under all combinations of ignore_punctuation, strip and ignore_case, the two agree on whether a value is found in every answer (location any).
  • Importers: ..., !!, ?, . and ["!!", "??"] give unsupported for Inspect match; ["...", "x"] keeps x; OpenAI match/includes and promptfoo contains/equals/starts-with keep ... as a check that only an answer containing it passes.
  • Weights: promptfoo weight of -1, .nan, .inf, true, null and [1] end as Error: with exit 2.

if base in _CONTAINS or base in ("equals", "starts-with", "regex"):
# No value to compare (`regex` would search for "None"), or a blank one, which
# eval reference refuses: left unsupported instead.
if value is None or any(not str(v).strip() for v in as_list(value)):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A promptfoo assert with a blank judge value, or an empty contains-any list, imports cleanly and then eval reference refuses the whole file.

The guard at :48 covers the text asserts only, and any([]) is False for an empty list. _other_check (:76) builds the judge check from str(value or ""), so a blank value leaves it with no criterion or reference.

  • {type: llm-rubric, value: ""} imports as {'type': 'judge', 'native': 'llm-rubric'}; validate_grading returns ['check 0: a judge check needs a criterion or a reference'].
  • {type: factuality, value: " "} imports as {'type': 'judge', 'reference': ' ', ...}, same problem.
  • {type: contains-any, value: []} imports as {'type': 'match', 'values': [], ...}; validate_grading returns ['check 0: match has no values'].

One such assert aborts eval reference for every other question in the file, which is the failure the blank-value threads were about. docs/answer-formats.md says "A promptfoo assert whose value is blank (whitespace included) imports as unsupported too", which does not hold for these.

Suggestion: in _text_check, also return None when not as_list(value); in _other_check, return None for a judged assert when not str(value or "").strip(). Add the three asserts above next to test_promptfoo_blank_values_are_unsupported_not_refused_later.

def _checks(scorer: str, kwargs: Mapping[str, Any], targets: list[str], imp: Imported) -> list[dict[str, Any]]:
# A target the scorer normalises to nothing ("", or "..." for `match`) is in every answer, so it
# is dropped; with none left nothing is graded.
norm = {"ignore_punctuation": scorer == "match"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

An Inspect exact target such as ... imports as a check that no answer can pass.

norm strips punctuation only for match, so for exact the target ... is kept. The grader's exact squad-normalises it to "", which never equals a non-empty value, so {'type': 'exact', 'values': ['...']} validates and grades False for zzz, ... and ?. It fails safe, but it silently scores 0 and is not marked unsupported.

Suggestion: norm = {"ignore_punctuation": scorer in ("match", "exact")}, so the target is dropped and the sample becomes unsupported. Add a ... row with scorer="exact" to test_inspect_punctuation_only_target_is_unsupported_for_match_only.

@xaleryb xaleryb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at 1a9d30a: the three points of the review at 804c83b are fixed here, the full suite, ruff, mypy, bandit and deptry pass on Linux, and CI is green.

@vchamarthi vchamarthi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blockers. 1 non-blocking comment.

No blockers; the punctuation-only match value and promptfoo weight/threshold fixes from earlier review are in and tested. One non-blocking question on JSON equality remains.

CI at send time: 0 failing, 0 pending.

ok, parsed = parse_json(answer, contains=bool(check.get("contains")))
if not ok:
return False, "not JSON"
return (not values or any(parsed == v for v in values)), ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: Is the json check meant to treat true and 1 (and false and 0) as equal? parsed == v accepts both, which differs from promptfoo equals on an object value.

dokimi/dokimi/eval/reference_checks.py:309: return (not values or any(parsed == v for v in values)), "". In Python bool is a subclass of int, so {"ok": true} == {"ok": 1} and [true] == [1] are both True, and 1.0 == 1 is True. A target of {"ok": 1} therefore passes the answer {"ok": true}. promptfoo compares in JavaScript, where true and 1 are different values. If strictness matters here, compare after a recursive type-aware walk, or document that bool and number are interchangeable in json checks.

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>
@xaleryb

xaleryb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Merged main (f814c85): the one conflict was tests/test_answers_command.py, where #236 and this branch each added tests at the same place; both are kept (main's _stub_generate and write-error tests, this branch's --doc-source none test). Full suite, ruff, mypy, bandit and deptry pass on Linux.

@xaleryb xaleryb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at f814c85: merged with main after #236 (both sides' tests kept), the review points are fixed, the full suite, ruff, mypy, bandit and deptry pass on Linux, and CI is green.

@napetrov
napetrov merged commit a2b787a into intel:main Oct 27, 2026
25 checks passed
napetrov pushed a commit that referenced this pull request Oct 29, 2026
…ted (#266)

Settles two review points of #214 (task-format importers).

promptfoo: a judged assert (llm-rubric, factuality, g-eval, ...) whose
value is missing or blank after strip() imported as a judge check with no
criterion or reference, and a contains*/icontains* assert with an empty
list imported as a match check with no values. validate_grading refuses
both, so one such assert made `eval reference` refuse the whole file.
_other_check now returns None for a blank judged value and _text_check
returns None for an empty contains* list, so both import as `unsupported`
with the "not translated" note, like a blank text value already did.

Inspect: the grader's `exact` check SQuAD-normalises the target, so a
target such as "..." or "the" becomes "" and equals no answer; it was
kept as a check that always fails. _checks now drops such a target for
the `exact` scorer, so a sample left with no target is `unsupported`.
`includes` and the model-graded scorers are unchanged.

docs/answer-formats.md states the new rules; changelog fragment (Fixed).

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli-interface-change Changes dokimi's user-facing CLI surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants