UNPKG

@arizeai/phoenix-client

Version:
191 lines (161 loc) 6.05 kB
--- title: "CI Eval Test Annotations" description: "Record annotations and evaluator results on Phoenix experiment runs" --- CI eval test annotations are how the `@arizeai/phoenix-client/vitest` and `@arizeai/phoenix-client/jest` submodules record scored, labeled, or explained results on an experiment run. They map directly to Phoenix's `experiment_evaluations` REST surface — each annotation becomes one `ExperimentEvaluation` with an `ExperimentEvaluationResult` body. ## The `Annotation` Shape ```ts interface Annotation { /** Phoenix evaluation name. Required. */ name: string; /** Numeric or boolean score. Booleans are stored as 0 / 1. */ score?: number | boolean | null; /** Categorical label. */ label?: string | null; /** Free-form explanation, shown in the Phoenix UI. */ explanation?: string | null; /** Custom metadata. */ metadata?: Record<string, unknown>; /** Source of the annotation. Defaults to "CODE". */ annotatorKind?: "LLM" | "CODE" | "HUMAN"; } ``` The fields line up with Phoenix's `ExperimentEvaluationResult` plus the `name` and `annotator_kind` carried on the surrounding evaluation body. ## `logAnnotation(annotation)` Records a single annotation against the current run. Must be called inside a `test()` body. ```ts import * as px from "@arizeai/phoenix-client/vitest"; px.describe("demo", () => { px.test( "manual annotation", { input: { x: 1 }, expected: { y: 2 } }, async ({ input, expected }) => { const result = myApp(input.x); px.logOutput({ y: result }); px.logAnnotation({ name: "harmfulness", score: 0.2, explanation: "no PII detected", annotatorKind: "CODE", }); }, ); }); ``` ## `evaluate(evaluator, params?)` Runs an evaluator object and records its result as an annotation on the current run. An evaluator is any object with a `name` and an `evaluate` function, including evaluators created with `@arizeai/phoenix-evals.createEvaluator()` and `@arizeai/phoenix-client/experiments.asExperimentEvaluator()`. The evaluator call is traced as an OpenInference `EVALUATOR` span, and the annotation is linked back to that evaluator trace. If `params` is omitted, Phoenix supplies the current test's `input`, recorded `output`, `expected`, `metadata`, and task `traceId`. If `params` is supplied, it is merged on top of those defaults. ```ts import * as px from "@arizeai/phoenix-client/vitest"; import { createEvaluator } from "@arizeai/phoenix-evals"; const correctness = createEvaluator( async ({ output, expected }: { output: { sql: string }; expected: { sql: string }; }) => { const grade = await llmAsJudge(output.sql, expected.sql); return { score: grade.score, label: grade.passed ? "correct" : "incorrect", explanation: grade.rationale, }; }, { name: "correctness", kind: "LLM" }, ); px.describe("generate sql", () => { px.test( "select all", { input: { userQuery: "Get all users from the customers table" }, expected: { sql: "SELECT * FROM customers;" }, }, async ({ input, expected }) => { const sql = await myApp(input.userQuery); px.logOutput({ sql }); await px.evaluate(correctness, { output: { sql }, expected: expected ?? { sql: "" }, }); }, ); }); ``` The annotation name comes from `evaluator.name`. The evaluator result can be a number, boolean, string label, `null`, or an object with `score`, `label`, `explanation`, and `metadata`. ## Using `@arizeai/phoenix-evals` `createEvaluator()` gives you a reusable evaluator object. `px.evaluate()` runs that evaluator in the test context, traces the call, and records its result on the experiment run. If the evaluator itself uses OpenInference telemetry, those implementation spans appear under the evaluator trace: ```ts import * as px from "@arizeai/phoenix-client/vitest"; import { createEvaluator } from "@arizeai/phoenix-evals"; async function judgeHallucination(answer: string) { return llmAsJudge({ answer }); } const hallucination = createEvaluator( async ({ output }: { output: { answer: string } }) => { const result = await judgeHallucination(output.answer); return { score: result.score, label: result.label, explanation: result.explanation, }; }, { name: "hallucination", kind: "LLM" }, ); ``` If you prefer to hand-write a plain evaluator, use the same shape: ```ts const exactMatch = { name: "exact_match", kind: "CODE" as const, evaluate: ({ output, expected }: { output: { sql: string }; expected: { sql: string }; }) => ({ score: output.sql === expected.sql, }), }; ``` Use `createEvaluator()` or OpenInference decorators from `@arizeai/phoenix-otel` when the evaluator implementation should emit child spans of its own. The older `traceEvaluator(fn)` helper remains available for raw function wrapping, but evaluator objects are the preferred interface. ## Built-In `pass` Annotation Every test automatically records a `pass` boolean annotation based on whether the test body threw. You don't have to log it yourself; it's included in the reporter summary and on the run in Phoenix. ## Aggregating Annotations In CI Suite `acceptanceCriteria` aggregate annotation scores after all cases run. Use them when a metric should clear a threshold across the dataset — either an `average` bar (e.g. mean `correctness >= 0.8`) or a `passRate` rule requiring a minimum fraction of runs to satisfy a per-run `passFn` predicate (e.g. 100% of runs must have `valid_sql === true`). See [CI Eval Tests: Vitest](./ci-evals-vitest#acceptance-criteria) for the full configuration shape. <section className="hidden" data-agent-context="source-map" aria-label="Source map"> <h2>Source Map</h2> <ul> <li><code>src/testing/helpers.ts</code></li> <li><code>src/testing/acceptance.ts</code></li> <li><code>src/testing/phoenix-test-tracking.ts</code></li> <li><code>src/testing/types.ts</code></li> </ul> </section>