Skip to content

feat: write online scores idempotently and keep them past retention - #4

Open
RobertCrupa wants to merge 4 commits into
feat/online-evals-scoringfrom
feat/online-evals-score-writing
Open

RobertCrupa wants to merge 4 commits into
feat/online-evals-scoringfrom
feat/online-evals-score-writing

Conversation

@RobertCrupa

Copy link
Copy Markdown

Closes apify/ai-team#270. Part of apify/ai-team#249, stacked on the #269 PR.

  • `online-scores.ts`: each verdict is written twice, to the trace (id `--p-i<JUDGE_IMPL_VERSION>-`) and to an invented dataset run `apify-ai-online-YYYY-MM-DD` (id suffixed `-run`, no dataset, no items; reads back with the `experimentId` filter). BOOLEAN, source defaults to API.
  • Idempotency: already-judged traces (same prompt version, impl version and model in the holistic score's metadata) are filtered out BEFORE judging; `force` bypasses. The holistic trace copy is written last so a half-written trace is retried.
  • Daily rollup as dataset item `rollup-YYYY-MM-DD` in `apify-ai-online-rollups` (pass rate and n per criterion, sample rate, coverage counters), keyed on the window start day.
  • The checkpoint is now written only after scores and rollup succeed. All writes failed, or all judgements failed, holds the window and fails the run.

Every Langfuse API assumption was verified live on 2026-09-09 with throwaway objects (score create and same-day replace, the run subject without a run, exactly-one-subject, EVAL rejected, datasets and items in events_only mode, v3 read with a traceId list). One inference remains and is marked as such: metrics-API aggregation of run-attached scores. No public API deletes a dataset, so the rollup dataset name is permanent once created.

…ai-team#270)

Every verdict from the online judge is written to Langfuse twice: once on
the trace, once under an invented datasetRunId apify-ai-online-YYYY-MM-DD so
the score survives the 30-day trace sweep. Score ids are readable,
<traceId>-<scoreName>-p<promptVersion>-i<JUDGE_IMPL_VERSION>-<judgeModel>,
with a -run suffix on the archival copy; a bumped version writes beside the
old score. Sampled traces already judged under the same version are dropped
before judging, so a re-run spends nothing on the LLM.

A daily rollup (pass rate and n per score, coverage counters) is upserted as
dataset item rollup-YYYY-MM-DD in apify-ai-online-rollups, merging runs of
the same day. The checkpoint write moves out of selectTraces (writeCheckpoint
option) and happens only after the scores and the rollup are written; a
rollup failure fails the run and leaves the window to be retried.

OUTPUT gains scoresSkipped, scoresWritten, failedToWrite and rollupItemId.
…the window day (ai-team#270 review)

A batch whose writes all failed was reported as success: the rollup came from
every judged verdict, the checkpoint advanced and the run exited 0, so a
Langfuse outage silently lost the window and its LLM spend. writeOnlineScores
now returns the verdicts that were written, only those feed the rollup, the
rollup coverage gains scoresWritten and failedToWrite, and an all-failed batch
skips the rollup, keeps the checkpoint and exits non-zero.

Run ids and rollup items are keyed on the UTC date of the window start, not
the write, so a backfill lands on its own day; the daily run therefore labels
D-1 when it writes at ~06:0x on D (README says so for the #271 monitor). The
rollup is skipped when nothing was sampled or judged. README and comments now
describe the ReplacingMergeTree semantics (same-day rewrite replaces the row,
cross-day retry of a half-written trace leaves two rows) and the retry double
count of the window counters. A round-trip test pins writer and reader to
each other.
… a judge failure (ai-team#270 round 3)

A live verification against langfuse.apify.dev (2026-09-09) confirmed every
API assumption. datasets.create is idempotent by name, so ensureRollupDataset
calls it unconditionally; the events_only hedges become "verified live"; the
README records the experimentId read filter and the experiment subject kind,
that ANNOTATION needs a configId (API is the only usable source), that BOOLEAN
scores read back as JSON booleans in environment default, and that no public
API deletes a dataset (ROLLUP_DATASET_NAME is permanent; pinned by a test).

isNotFound stays on statusCode, now pinned by a test that constructs the
client's real NotFoundError (@langfuse/core added as a devDependency for it).
finishOnlineRun gains the judge-side twin guard: when every trace given to the
judge failed to judge, no rollup, no checkpoint, AllJudgementsFailedError and
a non-zero exit. Window override docs say the ISO 8601 value needs an offset.
…cs claim (ai-team#270 follow-ups)

The all-judgements-failed guard is judged === 0 && failedToJudge > 0, which
is what sampled - scoresSkipped > 0 && judged === 0 meant, so the extra
scoresSkipped option on finishOnlineRun goes. The README no longer claims the
metrics aggregation of the run copy was verified live: read-back via
experimentId and the subject kind were, aggregation under an invented run is
the one open inference, and the working endpoint in events_only mode is
GET /api/public/v2/metrics. The two wordings for the held checkpoint are one.
@RobertCrupa
RobertCrupa added this pull request to stack #6 September 9, 2026 15:14
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.

2 participants