UNPKG

@arizeai/phoenix-client

Version:
367 lines (340 loc) 14.4 kB
import type { PhoenixClient } from "../index"; import type { AnnotatorKind } from "../types/annotations"; import type { EvaluatorParams, EvaluationResult as ExperimentEvaluationResult, } from "../types/experiments"; /** * Phoenix annotator kind, re-exported from the shared client types so the * testing module and the rest of the client agree on a single definition. */ export type { AnnotatorKind }; /** A JSON-serializable map. */ export type KVMap = Record<string, unknown>; /** * Domain language * --------------- * The unit these tests evaluate over is an **Example**: a single AI example — * an `input`, its `expected` output, and optional `metadata` / `splits` — over * which the task under test is run and then scored. This is the same notion as * the dataset `Example` (`../types/datasets`): each test case _is_ one example. * When tracked, a case is recorded to Phoenix as a dataset example and * evaluated as one experiment run. * * These field names form the shared vocabulary across this module: * - `input` — the example's input, passed to the task under evaluation. * - `expected` — the example's expected (reference / ground-truth) output. * - `metadata` — extra fields carried on the example. * - `splits` — slice labels for the example. * - `id` — stable example id, used to upsert the example across runs. */ /** * The expected output of an `Example`, accepted under any one of three * interchangeable keys. All three normalize to the same slot: when recorded to * Phoenix the value becomes the dataset example's `output`, and it is exposed * to evaluators as `expected` on `EvaluatorParams`. At most one key may be set. * * - `expected` — the canonical name (the ground-truth / reference output). * - `reference` — alias preferred by frameworks that name the slot "reference". * - `output` — alias for callers who think in terms of the example's `output`. * * Modeled as a union so supplying more than one key at a time is a type error. */ export type ReferenceOutput<Expected extends KVMap = KVMap> = | { expected?: Expected; reference?: never; output?: never } | { reference?: Expected; expected?: never; output?: never } | { output?: Expected; expected?: never; reference?: never }; /** * The `Example` fields that define a single test case, excluding its * expected output (which is supplied separately via {@link ReferenceOutput}). * * `input` is the example's input — the value fed to the task under evaluation. * When the case is tracked, this becomes the dataset example's `input`. */ export interface TestParamsBase<Input extends KVMap = KVMap> { /** Optional stable example id; used to upsert the example between runs. */ id?: string; /** The example's input — fed to the task under evaluation. Required. */ input: Input; /** Additional metadata stored on the example and its run. */ metadata?: KVMap; /** * Split assignment(s) for the example, used to slice the dataset and * experiment in the Phoenix UI (e.g. `["factual_accuracy", "correct"]`). */ splits?: string[]; /** Per-test config (tags + metadata recorded on the run). */ config?: TestConfig; /** * Number of times to run this test case. Each repetition becomes a * separate experiment run against the same dataset example (carrying a * distinct `repetition_number`). Overrides the suite-level `repetitions`. * Defaults to the suite value, then `PHOENIX_TEST_REPETITIONS`, then `1`. */ repetitions?: number; /** * When `true`, this test runs as an ordinary local test only — no dataset * example is created and no experiment run or annotations are uploaded to * Phoenix. Useful for scaffolding a case before it's ready to track. */ dryRun?: boolean; } /** * The full inline definition of a single `Example` under test. * * Combines {@link TestParamsBase} with a {@link ReferenceOutput}, so the * example's expected output may be given under `expected`, `reference`, or * `output` (at most one). All three resolve to the same canonical `expected` * slot. */ export type TestParams< Input extends KVMap = KVMap, Expected extends KVMap = KVMap, > = TestParamsBase<Input> & ReferenceOutput<Expected>; /** * Resolve an `Example`'s expected output from a value that may carry it * under any of the `expected` / `reference` / `output` aliases (see * {@link ReferenceOutput}). Returns the first one set, or `undefined` if none. */ export function resolveReference<Expected extends KVMap = KVMap>( params: ReferenceOutput<Expected> ): Expected | undefined { return params.expected ?? params.reference ?? params.output; } /** Per-test runtime configuration. */ export interface TestConfig { /** Tags recorded on the experiment run for filtering in the Phoenix UI. */ tags?: string[]; /** Extra metadata recorded on the experiment run. */ metadata?: KVMap; } /** * How a criterion aggregates an annotation's scores to gate the suite: * * - `"average"` — gate on overall quality: the **mean** score across all runs * must clear the criterion's `threshold`. A few weak runs are tolerated as * long as the mean holds. * - `"passRate"` — gate on consistency: each run **passes** when the * criterion's `passFn` predicate returns `true` for its annotation, and the * suite passes when the **fraction** of runs that pass is at least * `minPassRate` (e.g. `minPassRate: 0.9` ⇒ 90% must pass; `1` ⇒ all). */ export type AcceptanceMetric = "average" | "passRate"; /** * Optimization direction for a criterion's scores: `"maximize"` (higher is * better, the default) or `"minimize"` (lower is better). Controls every * score comparison the criterion makes. */ export type OptimizationDirection = "maximize" | "minimize"; /** Fields shared by every {@link AcceptanceCriterion} variant. */ export interface AcceptanceCriterionBase { /** Annotation name to aggregate across completed test runs. */ annotationName: string; } /** * Gate the suite on the **mean** score: the average across all runs must clear * `threshold` (compared in `direction`). */ export interface AverageAcceptanceCriterion extends AcceptanceCriterionBase { metric: "average"; /** * The bar the mean score must clear, compared in `direction`. Boolean scores * average as `1` (`true`) / `0` (`false`). */ threshold: number; /** * Optimization direction; defaults to `"maximize"`. `"maximize"` treats a * higher mean as better (clears when `>= threshold`); `"minimize"` treats a * lower mean as better (clears when `<= threshold`) — use it for cost, * latency, or error-rate annotations. */ direction?: OptimizationDirection; } /** * Gate the suite on the **pass rate**: each run passes when `passFn` returns * `true` for its annotation, and the suite passes when at least `minPassRate` * of runs do. `passFn` decides what "passing" means, so any logic works — a * score bar, a score range, a label match, a metadata check, etc. */ export interface PassRateAcceptanceCriterion extends AcceptanceCriterionBase { metric: "passRate"; /** * Predicate deciding whether a single run passes, given the run's last * {@link Annotation} for `annotationName` (its `score`, `label`, * `explanation`, `metadata`, …). Runs whose predicate returns `true` count * toward the pass rate. */ passFn: (annotation: Annotation) => boolean; /** * Minimum fraction of runs (`0`–`1`) that must pass for the suite to pass — * e.g. `0.9` requires 90% of runs to satisfy `passFn`, `1` requires all of * them. The suite passes when `passRate >= minPassRate`. */ minPassRate: number; } /** * One aggregate acceptance rule, evaluated once after every test in the suite * has run. Each criterion aggregates a single annotation's scores with one * {@link AcceptanceMetric} and fails the suite when the result misses its bar. * * Scoring notes shared by every metric: * - Boolean scores count as `1` (`true`) / `0` (`false`). * - If a run logs the same annotation more than once, the last one counts. * - Skipped tests are excluded; dry-run tests are included (they still run). * - A criterion whose annotation was never logged on any run fails (rather * than passing vacuously) — see {@link AcceptanceResultFields.failureReason}. */ export type AcceptanceCriterion = | AverageAcceptanceCriterion | PassRateAcceptanceCriterion; /** The computed fields added to an {@link AcceptanceCriterion} once evaluated. */ export interface AcceptanceResultFields { /** * The aggregate the criterion gated on, or `null` when there were no runs to * aggregate. For `"average"` this is the mean score; for `"passRate"` it is * the fraction of runs that passed (so a fully-passing `"passRate"` criterion * reports `1`). */ value: number | null; /** Number of runs included in the aggregate. */ sampleCount: number; /** Whether the aggregate cleared the criterion. */ passed: boolean; /** Human-readable failure reason for invalid or empty aggregates. */ failureReason?: string; } /** Computed result for one aggregate acceptance rule. */ export type AcceptanceResult = AcceptanceCriterion & AcceptanceResultFields; /** Suite-level configuration accepted by `describe()`. */ export interface SuiteConfig { /** Override the dataset / experiment name used for the suite. */ datasetName?: string; /** Description for the dataset and experiment. */ description?: string; /** Suite-level metadata applied to every run in this experiment. */ metadata?: KVMap; /** Override the Phoenix client used for syncing this suite. */ client?: PhoenixClient; /** * Number of times to run each test case in this suite. Individual tests * may override this via `TestParams.repetitions`. Defaults to the * `PHOENIX_TEST_REPETITIONS` env var, then `1`. */ repetitions?: number; /** * When `true`, the whole suite runs as ordinary local tests — no dataset * is uploaded and no experiment, runs, or annotations are created in * Phoenix. Equivalent to `PHOENIX_TEST_TRACKING=false` scoped to this * suite. The reporter still prints a local summary. */ dryRun?: boolean; /** * Aggregate annotation criteria that gate the suite after all tests run. * Each criterion fails the suite when its scores miss the configured bar * (see {@link AcceptanceCriterion}). */ acceptanceCriteria?: AcceptanceCriterion[]; } /** * Arguments passed to a `test()` body: the `Example` under test, exposed * as its `input`, `expected` output, and `metadata`. Read straight from the * test's {@link TestParams} — the runner does not transform them. */ export interface TestArgs< Input extends KVMap = KVMap, Expected extends KVMap = KVMap, > { /** The example's input under test. */ input: Input; /** The example's expected (reference) output, when one was supplied. */ expected?: Expected; /** Any metadata attached to the example. */ metadata?: KVMap; } /** * Object form of an evaluator result. Reuses the shared experiment * {@link ExperimentEvaluationResult} shape (label / explanation / metadata) * but widens `score` to also accept booleans, which the testing API stores as * `1` / `0`. */ export interface EvaluationResultObject extends Omit< ExperimentEvaluationResult, "score" > { /** Numeric or boolean score; booleans are stored as `1` / `0`. */ score?: number | boolean | null; } /** * One annotation recorded against a run. Extends the evaluator * {@link EvaluationResultObject} with the `name` and `annotatorKind` carried * on the evaluation body, plus an optional originating trace id. */ export interface Annotation extends EvaluationResultObject { /** Phoenix evaluation name. Required, and unique per run (last write wins). */ name: string; /** Who or what produced the annotation. Defaults to `"CODE"`. */ annotatorKind?: AnnotatorKind; /** Trace id for this evaluation, when the annotation was produced by a traced evaluator. */ traceId?: string | null; } /** Result returned by `traceEvaluator` for any evaluator-shaped value. */ export type EvaluatorResult = Annotation | (KVMap & { name: string }); /** Result shape produced by evaluator objects used in eval tests. */ export type EvaluationResult = | number | boolean | string | null | EvaluationResultObject; /** * Parameters passed to an evaluator when it runs inside a test. A relaxation of * the shared {@link EvaluatorParams}: `input` is always present, while `output` * (an evaluator may run before `logOutput()`) and the remaining fields are * optional. Deriving from `EvaluatorParams` keeps this aligned with the * experiment evaluator contract as that shape evolves. */ export type EvaluationParams = Partial<EvaluatorParams> & { /** The example's input under test. */ input: KVMap; }; /** Structural evaluator interface accepted by `evaluate()`. */ export interface Evaluator< Params extends KVMap = EvaluationParams & KVMap, Result = EvaluationResult, > { /** Annotation/evaluation name. */ name: string; /** Who or what produced the result. Defaults to `"CODE"`. */ kind?: AnnotatorKind; /** Compute the evaluation result. */ evaluate: (params: Params) => Result | Promise<Result>; } /** Test handler signature. */ export type TestFn< Input extends KVMap = KVMap, Expected extends KVMap = KVMap, > = (args: TestArgs<Input, Expected>) => unknown | Promise<unknown>; /** * Each-row shape accepted by `test.each(table)(name, fn)`; each row defines one * `Example`. * * Like {@link TestParams}, the example's expected output is supplied via * {@link ReferenceOutput} (`expected` / `reference` / `output`, at most one). * The trailing index signature still permits arbitrary extra columns on a row * (e.g. for `%j` name interpolation) without weakening that constraint. */ export type TestEachRow< Input extends KVMap = KVMap, Expected extends KVMap = KVMap, > = { id?: string; input: Input; metadata?: KVMap; /** Per-row split assignment(s); see `TestParams.splits`. */ splits?: string[]; /** Per-row repetition count; see `TestParams.repetitions`. */ repetitions?: number; /** Per-row dry-run flag; see `TestParams.dryRun`. */ dryRun?: boolean; } & ReferenceOutput<Expected> & Record<string, unknown>;