feat(evals): add Critical Thinking onto the shared evaluator contract - #188
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “Critical Thinking” durable-skills evaluator (ELA writing) to the repo’s eval contract, including prompt assets, input/output schemas, fixtures, and an example notebook, plus the Python dependency needed to run the notebook’s Anthropic LangChain integration.
Changes:
- Introduces
durable_skills.ela_writing.critical_thinkingevaluator assets (config, prompts, schemas, fixtures). - Adds an executable example notebook demonstrating structured-output invocation + fixture sniff-testing.
- Updates
evals/requirements.txtto includelangchain-anthropic.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| evals/requirements.txt | Adds langchain-anthropic dependency for the example notebook. |
| evals/durable-skills/ela-writing/critical-thinking/config.json | Registers the evaluator, model, generation, schemas, and fixtures configuration. |
| evals/durable-skills/ela-writing/critical-thinking/system.txt | Adds the rubric and decision rules as the system prompt. |
| evals/durable-skills/ela-writing/critical-thinking/user.txt | Adds the user prompt template with required placeholders. |
| evals/durable-skills/ela-writing/critical-thinking/input_schema.json | Defines the evaluator’s expected input shape. |
| evals/durable-skills/ela-writing/critical-thinking/output_schema.json | Defines the structured output contract for indicator + overall ratings. |
| evals/durable-skills/ela-writing/critical-thinking/fixtures.json | Adds two labeled fixtures for regression/sniff testing (overall rating). |
| evals/durable-skills/ela-writing/critical-thinking/example_notebook.ipynb | Demonstrates config/prompt hashing, invocation, and fixture comparison. |
Suppressed comments (1)
evals/durable-skills/ela-writing/critical-thinking/output_schema.json:81
- The schema text says indicators rated above "Not Evident" must include at least one evidence quote, but
Indicator.evidencecurrently allows an empty array for any rating. This makes the schema looser than the stated output contract and can let invalid outputs through the structured-output parser.
"evidence": {
"type": "array",
"description": "Verbatim quote/comment pairs supporting the rating. At least one for any indicator rated above Not Evident; may be empty for Not Evident.",
"items": {
"$ref": "#/$defs/Evidence"
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| " parser.kind == \"structured_output\" -> native structured-output enforcement.\n", | ||
| " \"\"\"\n", | ||
| " # max_tokens: outputs run ~2-4K tokens; the langchain default (1024) would truncate.\n", | ||
| " llm = ChatAnthropic(model=_STEP[\"model\"][\"name\"], max_tokens=16000)\n", |
| "Level": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "Not Evident", |
There was a problem hiding this comment.
This may need to be updated to snake_case for consistency. May require updates to the model prompt
| "$id": "output_schema.json", | ||
| "title": "EvaluatorOutput", | ||
| "description": "Critical Thinking ratings for a student argumentative essay. Property order is intentional: each indicator's evidence and rationale are produced before its rating, and all indicators are produced before the Overall rating, so reasoning conditions the ratings.", | ||
| "type": "object", |
There was a problem hiding this comment.
@aychi1 - Did we align with our internal experts on the output shape of this Eval?
There was a problem hiding this comment.
Reviewed with Michelle and made adjustments. Pls see the last commit b484496
aychi1
left a comment
There was a problem hiding this comment.
Overall LGTM. Several minor questions/ comments.
| "$id": "output_schema.json", | ||
| "title": "EvaluatorOutput", | ||
| "description": "Critical Thinking ratings for a student argumentative essay. Property order is intentional: each indicator's evidence and rationale are produced before its rating, and all indicators are produced before the Overall rating, so reasoning conditions the ratings.", | ||
| "type": "object", |
bb60f4f to
1554246
Compare
1554246 to
6e32882
Compare
| 2. What counts as a citation | ||
| Citations are defined broadly for grade 6–12 writers: a direct quote, "the article says", an author name, or a source number all count. |
0ff0cb8 to
c487cce
Compare
| "expected": { | ||
| "overall": { | ||
| "rationale": "Expert consensus median across 6 raters was Analyzing.", | ||
| "rating": "analyzing" | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
The fixtures should also include the scores for the indicators. Based on this spreadsheet, the indicator scores are all "Analyzing" as well.
https://docs.google.com/spreadsheets/d/1_sE_Fx2z_bx6l4iKO7PueVYD7jTJR8MR_6y9UgTcmpQ/edit?gid=2100920111#gid=2100920111
There was a problem hiding this comment.
@aychi1 - I'll have to include these fields in a followup PR. Nested object verification will require updates to the fixture tests + notebook harness. I dont want to expand the current scope of this PR, and instead do that separately and add later. Is that ok?
| "expected": { | ||
| "overall": { | ||
| "rationale": "Expert consensus median across 5 raters was Exploring.", | ||
| "rating": "exploring" |
There was a problem hiding this comment.
Similar comment-- the fixture should include the indicators. Based on the consensus_labels_round3, the indicators are all "Exploring", aside from synthesizing_sources which does not have a score.
c487cce to
bf9192f
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The output schema currently allows empty evidence arrays even when an indicator is rated above not_evident, which contradicts the prompt/contract requirements and can let invalid structured outputs validate.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Lite
| "Indicator": { | ||
| "type": "object", | ||
| "required": [ | ||
| "evidence", | ||
| "rationale", | ||
| "rating" | ||
| ], | ||
| "additionalProperties": false, | ||
| "properties": { | ||
| "evidence": { | ||
| "type": "array", | ||
| "description": "Verbatim quote/comment pairs supporting the rating. At least one for any indicator rated above not_evident; may be empty for not_evident.", | ||
| "items": { | ||
| "$ref": "#/$defs/Evidence" | ||
| } | ||
| }, | ||
| "rationale": { | ||
| "type": "string", | ||
| "description": "2–5 sentences explaining the rating against the rubric cells and decision rules. Produced before the rating." | ||
| }, | ||
| "rating": { | ||
| "$ref": "#/$defs/Level" | ||
| } | ||
| } | ||
| }, |
bf9192f to
e966bba
Compare
aychi1
left a comment
There was a problem hiding this comment.
LGTM, thank you so much Adnan for your work on this
Adds the Critical Thinking evaluator at
evals/durable-skills/ela-writing/critical-thinking/, following the same shared contract as the student-facing-text and feedback families (#159, #161, #163, #173–#177, #179–#185). First evaluator under thedurable-skillsdomain.Contract-only: the TypeScript evaluator follows in its own PR, so the rubric, schemas and fixtures can be reviewed by the people who own them first.
Scope: grades 8-10 only.
supported_gradesis["8", "9", "10"], and the rubric prompt states the same range in both places it appears. The validation evidence covers those grades and no others, so the evaluator is not offered for grades 6-7 or 11-12.Evaluator
evaluator.id:durable_skills.ela_writing.critical_thinkingstable_id:114fb24f-65d8-4736-bb23-300b0470fc4devaluate_critical_thinking) onclaude-opus-5,provider: anthropicassignment_text,sources,source_count,essay_text— no grade input, so thegrade_levelstring standardisation doesn't applysupported_gradesis8–10(see Scope above); the prompt's two grade references matchnot_evident,exploring,analyzing,integrating,extending.synthesizing_sourcesis omitted whensource_countis 1Contract
system.txt/user.txt, hash-pinned inconfig.jsonoutput_schema.jsondefinesindicators(each withevidence,rationale,rating) thenoverall. Property order is deliberate — evidence and rationale precede each rating, and all indicators precede the Overall, so the reasoning conditions the scoreindicatorsis an object keyed by indicator id, not an array.evidence_strength,counterarguments,facts_over_opinionsanddrawing_conclusionsarerequired;synthesizing_sourcesis an optional fifth, present only on multi-source prompts.additionalProperties: falsecloses the rest. Nothing in the prompts required an array, and named keys say all of this in the schema itself rather than inallOf/contains— and make duplicate ids impossible, which no array constraint couldIndicator.idis gone: the key carries the identity, so keeping both would let them disagree. The id-to-rubric mapping (2.1, 2.2, 3.1, 3.2, 4.1) moved to theindicatorsdescriptionfixtures.jsoncarries two real cases covering both branches: one multi-source with all 5 indicators, one single-source with 4. Each case'sdescriptionrecords the per-indicator expert consensus it was labelled fromexample_notebook.ipynbruns the config end-to-end, verifying both prompt hashes on load and deriving its generation settings from the configgeneration.temperatureisnullFirst use of the nullable temperature from #186. Verified against the live API on
claude-opus-5:10temperatureis deprecated for this model"0.5nullAnthropic's guidance for Opus 4.7 and later is to omit the parameter entirely, which is exactly what
nullmeans in our contract.nullis also the only correct encoding here, not merely a convenient one:config.schema.jsonrequiresgeneration.temperature, and in the SDK an absent value falls back to the evaluator config's default whilenullexplicitly omits. Removing the key would send a temperature rather than suppress one.The model is
claude-opus-5with no dated suffix. Per Anthropic's versioning docs, dateless ids from the 4.6 generation onward are themselves pinned snapshots rather than evergreen pointers, so there is no dated form to pin instead.Also fixed
$schemawas one level short for a three-deep path —eval-configcaught itlangchain-anthropicadded toevals/requirements.txt— this is the first eval to use it, and the existinganthropicentry is the raw SDK, not the LangChain integration.eval-requirementscaught itfixtures.toleranceadded — the notebook already readtolerance.allow_adjacent_levels, but config never set it, so fixtures ran on a strict gate. Now matches the sibling evaluatorstemperaturefrom the config rather than printing it and ignoring it. It previously built the model without a temperature, which was correct for this config by coincidence —nullmeans omit and it omitted — but any config that set a number would have been silently disregardedregistry-conformance.test.tsgains aUNIMPLEMENTEDentry for this id. That test asserts every contract on disk is either implemented or explicitly named, so a contract-only PR has to declare itself; the entry says to drop it when the TypeScript evaluator lands. This is the only file touched outside the eval directoryVerification
scripts/check.py— all 6 pass. TypeScript unit tests, typecheck and lint pass, which this PR sees for the first time: it predates #223, so the old path filter (evals/prompts/**,evals/student-facing-text/**) never matched a new domain and the SDK jobs never ran.The notebook was executed against the real API using the same command and timeout as
eval-notebooks.yml. Exit 0, no cell errors, both fixtures exact:5076429_cleaned(multi-source, 5 indicators)analyzinganalyzingAAAOPP13416000111182(single-source, 4 indicators)exploringexploringBoth matched exactly rather than relying on the adjacency tolerance. Re-run after each change: after the rating values moved to snake_case, after the level-token hint was removed from the prompt (confirming the schema enum alone constrains the model), and after
indicatorsbecame an object. The live payload came back keyed by id with all five indicators and noidfield.A live run exercises one branch at a time, so the schema's accept/reject behaviour is checked directly as well:
idfieldevidence: []atanalyzingOpen items, not blocking
evidencemay be empty at any rating. The prompt requires at least one quote for anything abovenot_evident, but the schema permits an empty array, so the structured-output parser will not enforce it. Expressible as anif/thenonrating; not done here.outcomeis not declared, soreadOutcomewill return no score for this evaluator once it is implemented. Every scored evaluator except math declares one; worth adding when the SDK side lands.fixtures.tolerance.allow_adjacent_levelsis read only by the notebook. Nothing in the SDK or the checks consumes it.max_tokens=16000is hardcoded in the notebook.generationonly permitstemperature(additionalProperties: false), so there is nowhere in the contract to declare it. Measured output is ~4.5K tokens, so the ceiling is generous headroom rather than a tuned value.output_schema.json.evals/README.mdrow, consistent with the other in-flight evaluator PRs.