Skip to content

Add per-score direction flag to eval output scores - #1622

Open
scosman wants to merge 1 commit into
mainfrom
scosman/score-direction
Open

scosman wants to merge 1 commit into
mainfrom
scosman/score-direction

Conversation

@scosman

@scosman scosman commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Eval output scores had no way to declare which way is better. This adds direction on EvalOutputScorehigher_is_better (default), lower_is_better, or informational (context only, should never drive decisions) — so API consumers comparing results across runs can interpret score deltas without guessing.

  • Rating scales (five_star, pass_fail, pass_fail_critical) are higher-is-better by definition: they accept higher_is_better and informational, and reject lower_is_better. lower_is_better is reserved for custom scores (unbounded numeric metrics), which evaluators do not support yet.
  • Backward compatible: the field defaults to higher_is_better, so existing evals load unchanged; requests from clients that predate the field get the default.
  • The field flows through the create_evaluator and read APIs automatically via EvalOutputScore; web client schema regenerated.

Related Issues

Companion to #1620 and #1621 (eval-API improvements).

Checklists

  • Tests have been run locally and passed
  • New tests have been added to any work in /lib

🤖 Generated with Claude Code

https://claude.ai/code/session_0135zWTBh8MRPwCXiWvuE597


Generated by Claude Code

Eval output scores had no way to declare which way is better. This adds
direction on EvalOutputScore - higher_is_better (default),
lower_is_better, or informational (context only, should never drive
decisions) - so API consumers comparing results across runs can
interpret score deltas without guessing.

- Rating scales (five_star, pass_fail, pass_fail_critical) are
  higher-is-better by definition: they accept higher_is_better and
  informational, and reject lower_is_better. lower_is_better is
  reserved for custom scores (unbounded numeric metrics), which
  evaluators do not support yet.
- Backward compatible: the field defaults to higher_is_better, so
  existing evals load unchanged and re-save with the field explicit;
  requests from clients that predate the field get the default.
- The field flows through the create_evaluator and read APIs
  automatically via EvalOutputScore; regenerate the web client schema
  for the new field and enum.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds ScoreDirection to eval output scores with defaulting and validation, updates the generated API schema, and expands model and evaluator API tests for explicit, omitted, and legacy directions.

Changes

Eval score direction

Layer / File(s) Summary
Score direction model and compatibility
libs/core/kiln_ai/datamodel/eval.py, libs/core/kiln_ai/datamodel/test_eval_model.py
Defines supported directions, defaults missing values to higher_is_better, validates rating compatibility, and covers legacy persisted eval migration.
API direction contract and evaluator creation
app/web_ui/src/lib/api_schema.d.ts, app/desktop/studio_server/test_eval_api.py
Exposes direction in the API schema and verifies explicit and omitted directions during evaluator creation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EvaluatorCreationAPI
  participant EvalOutputScore
  participant EvalFile
  Client->>EvaluatorCreationAPI: Submit output score direction
  EvaluatorCreationAPI->>EvalOutputScore: Parse and validate direction
  EvalOutputScore->>EvalFile: Persist default or explicit direction
  EvalFile-->>EvaluatorCreationAPI: Saved evaluator
  EvaluatorCreationAPI-->>Client: Return direction in response
Loading

Possibly related PRs

Suggested reviewers: sfierro

Poem

I’m a rabbit with scores in my burrow tonight,
Higher, lower, or info—all labeled just right.
Old files hop forward, defaults in their place,
New API fields join the evaluator race.
Test carrots are gathered with bright-eyed delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a per-score direction flag to eval output scores.
Description check ✅ Passed The description explains the change, supported values, validation rules, compatibility, API impact, related issues, and completed checklists.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scosman/score-direction

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.

@github-actions

Copy link
Copy Markdown

📊 Coverage Report

Overall Coverage: 92%

Diff: origin/main...HEAD

  • libs/core/kiln_ai/datamodel/eval.py (75.0%): Missing lines 123,125-127

Summary

  • Total: 16 lines
  • Missing: 4 lines
  • Coverage: 75%

Line-by-line

View line-by-line diff coverage

libs/core/kiln_ai/datamodel/eval.py

Lines 119-131

  119                 if self.direction == ScoreDirection.lower_is_better:
  120                     raise ValueError(
  121                         f"Score '{self.name}' has type '{self.type.value}', which is higher-is-better by definition. 'lower_is_better' is only valid for custom scores."
  122                     )
! 123             case TaskOutputRatingType.custom:
  124                 # Any direction is valid for custom scores (unbounded numeric metrics).
! 125                 pass
! 126             case _:
! 127                 raise_exhaustive_enum_error(self.type)
  128         return self
  129 
  130 
  131 class EvalRun(KilnParentedModel):


@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 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 `@app/web_ui/src/lib/api_schema.d.ts`:
- Around line 6047-6051: Update the evaluator-create input schema so
EvalOutputScore.direction is optional while retaining its server-side default
and required direction in response/output schemas. Split or override the
create-specific CreatedEvaluatorRequest/EvalOutputScore schema as needed, then
regenerate api_schema.d.ts using generate_schema.sh.

In `@libs/core/kiln_ai/datamodel/eval.py`:
- Around line 90-93: Update the ScoreDirection field description and the related
validate_type validation message to state that lower_is_better is reserved for
currently unsupported custom evaluator scores, rather than presenting it as a
usable custom-score option; keep the existing rejection behavior unchanged.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c2867f8-f448-40bf-b27e-d806c3aa69ae

📥 Commits

Reviewing files that changed from the base of the PR and between bc9edc9 and 6deb120.

📒 Files selected for processing (4)
  • app/desktop/studio_server/test_eval_api.py
  • app/web_ui/src/lib/api_schema.d.ts
  • libs/core/kiln_ai/datamodel/eval.py
  • libs/core/kiln_ai/datamodel/test_eval_model.py

Comment on lines +6047 to +6051
/**
* @description The direction of improvement for this score: 'higher_is_better', 'lower_is_better', or 'informational' (context only, no preferred direction). Rating scales ('five_star', 'pass_fail', 'pass_fail_critical') are higher-is-better by definition, so they allow 'higher_is_better' and 'informational' but not 'lower_is_better'.
* @default higher_is_better
*/
direction: components["schemas"]["ScoreDirection"];

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate schema/update scripts =="
fd -a 'check_schema\.sh|generate_schema\.sh' . || true
echo

echo "== relevant schema excerpt =="
if [ -f app/web_ui/src/lib/api_schema.d.ts ]; then
  sed -n '5980,6075p' app/web_ui/src/lib/api_schema.d.ts | cat -n
fi
echo

echo "== search ScoreDirection usages =="
rg -n "ScoreDirection|ScoreUpdate|EvaluatorCreate|evaluator" app/web_ui/src/lib || true
echo

echo "== search schema scripts if present =="
if [ -f app/web_ui/check_schema.sh ]; then cat -n app/web_ui/check_schema.sh | sed -n '1,220p'; fi
if [ -f app/web_ui/generate_schema.sh ]; then cat -n app/web_ui/generate_schema.sh | sed -n '1,260p'; fi

Repository: Kiln-AI/Kiln

Length of output: 14930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generate/check script excerpts =="
sed -n '1,280p' app/web_ui/src/lib/generate_schema.sh | cat -n
echo "----"
sed -n '1,240p' app/web_ui/src/lib/check_schema.sh | cat -n
echo

echo "== OpenAPI source for create evaluator relevant schemas =="
rg -n "create_evaluator|EvalOutputScore|ScoreDirection|output_scores" . -g '!app/web_ui/src/lib/api_schema.d.ts' --glob '!node_modules' --glob '!dist' --glob '!build' || true
echo

echo "== generated endpoint request/response schema block =="
sed -n '4720,4850p' app/web_ui/src/lib/api_schema.d.ts | cat -n
echo

echo "== all generated EvalOutputScore occurrences with line context =="
rg -n -C 5 'EvalOutputScore:|direction\?: components|direction: components' app/web_ui/src/lib/api_schema.d.ts || true

Repository: Kiln-AI/Kiln

Length of output: 42500


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant datamodel/API schema definitions =="
sed -n '50,95p' libs/core/kiln_ai/datamodel/eval.py | cat -n
echo "---"
sed -n '150,175p' app/desktop/studio_server/eval_api.py | cat -n
echo "---"
sed -n '600,626p' app/desktop/studio_server/eval_api.py | cat -n
echo "---"
sed -n '280,310p' app/desktop/studio_server/test_eval_api.py | cat -n

echo "== schema file source metadata if available =="
fd -a 'openapi_schema\.sh|openapi.*schema|schema\.py|fastapi.*openapi|oas' . \
  --exclude node_modules --exclude dist --exclude build \
  | sed -n '1,120p'
echo

echo "== OpenAPI EvalOutputScore definition if generated source exists =="
sed -n '4290,4325p' app/web_ui/api_schema.d.ts 2>/dev/null | cat -n || true

Repository: Kiln-AI/Kiln

Length of output: 8085


Make direction optional for evaluator-create input schemas.

EvalOutputScore.direction has a server-side default, and requests without it are accepted, but the generated CreatedEvaluatorRequest/EvalOutputScore type has EvalOutputScore.direction as required, blocking legacy clients that omit it. Split or override the create-input schema and regenerate api_schema.d.ts with generate_schema.sh.

🤖 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 `@app/web_ui/src/lib/api_schema.d.ts` around lines 6047 - 6051, Update the
evaluator-create input schema so EvalOutputScore.direction is optional while
retaining its server-side default and required direction in response/output
schemas. Split or override the create-specific
CreatedEvaluatorRequest/EvalOutputScore schema as needed, then regenerate
api_schema.d.ts using generate_schema.sh.

Source: Coding guidelines

Comment on lines +90 to +93
direction: ScoreDirection = Field(
default=ScoreDirection.higher_is_better,
description="The direction of improvement for this score: 'higher_is_better', 'lower_is_better', or 'informational' (context only, no preferred direction). Rating scales ('five_star', 'pass_fail', 'pass_fail_critical') are higher-is-better by definition, so they allow 'higher_is_better' and 'informational' but not 'lower_is_better'.",
)

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clarify that lower_is_better is currently reserved, not usable.

The field description and validation error say it is valid for custom scores, but validate_type rejects every custom evaluator score first. State that it is reserved for unsupported custom scores to avoid advertising an API combination that always fails validation.

Proposed clarification
-... 'lower_is_better'. Rating scales ...
+... 'lower_is_better' (reserved for custom scores, which evaluators do not currently support). Rating scales ...

Also applies to: 111-125

🤖 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 `@libs/core/kiln_ai/datamodel/eval.py` around lines 90 - 93, Update the
ScoreDirection field description and the related validate_type validation
message to state that lower_is_better is reserved for currently unsupported
custom evaluator scores, rather than presenting it as a usable custom-score
option; keep the existing rejection behavior unchanged.

@chiang-daniel
chiang-daniel self-requested a review August 19, 2026 00:25
@model_validator(mode="after")
def validate_direction(self) -> Self:
match self.type:
case (

@chiang-daniel chiang-daniel Aug 19, 2026

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.

This validator allows a score to be both pass_fail_critical and informational at the same time. Those two make opposite promises: critical means "this failure should dominate the outcome", informational means "never use this score to decide anything". A score carrying both is a contradiction, and any consumer reading it has no idea which promise to believe.

Suggest rejecting the combination in this PR rather than later. If it's forbidden now and a real use ever appears, removing the restriction is free and breaks nothing. If it ships allowed and turns out to be a mistake, it can't be tightened later without breaking loads of any file that used the combination in the meantime.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants