Annotations
An annotation is a person’s judgement recorded against a test result, a trace, or a test. It is its own entity, linked to what it judges by entity_type and entity_id.
A Pass or Fail annotation on a test result or a trace overrides that parent’s automated outcome, so creating one changes what the platform reports for the parent, not only what the annotation says.
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
entity_type | str | What is being judged: TestResult, Trace, or Test |
entity_id | str | Id of that parent. Fixed at creation |
status_id | str | The verdict. Required to create |
status | Status | The verdict as stored, read-only |
target_type | str | What within the parent: the entity itself, metric, or turn |
target_reference | str | Which metric or turn, when target_type names one |
comments | str | What the person wrote |
resolved | bool | Whether the thread has been closed |
attributes | dict | Extra fields per annotation kind, e.g. the verdict a tuning judgement was made against |
user | AnnotationUser | Who wrote it, read-only |
resolved_by | AnnotationUser | Who resolved it, read-only |
context | AnnotationContext | Where the parent sits, for linking back. Populated by the list and entity routes |
Leave target_type and target_reference unset for a judgement on the parent as a whole; the backend fills in the right entity-level target for the parent type.
Reading annotations
Start from whatever you already have in hand:
There is no Metric entity class: a platform metric in the SDK is the judge itself, from rhesis.sdk.metrics, and get_annotations hangs off that.
Or query the collection directly when you have an id rather than an object:
These read every page rather than the first one, so a parent with more annotations than a single page returns whole.
for_metric takes a name or an id and returns both kinds of judgement about that metric: the ones left on a metric within a test result or trace, which name the metric, and the metric tuning ones, which name its id so that renaming the metric does not orphan them. The tuning ones are the judgements about whether the metric itself is any good, so asking by name and getting only the first kind would be the wrong answer.
Recording a judgement
Annotate the thing you have in hand. The verdict is named, not looked up.
"pass" and "fail" are resolved to the organization’s status rows for you, and cached. Metric tuning uses "accepted" and "rejected". Verdict and AnnotatableEntity hold the same values as constants if you would rather not type strings.
A turn is given as its number, from test_output.conversation_summary, and stored as the label the platform groups judgements by:
Both land on the same target. Passing a bare "2" would not: judgements are grouped by that reference, so it would sit beside the turn’s other annotations instead of superseding them, which is why the number is normalised here rather than sent as given.
For a parent with no entity class of its own, such as a trace, use the collection:
Constants are available where a literal would be easy to mistype:
Annotating a trace from the application that produced it
trace_db_id above is the span’s row id, which the platform assigns on ingestion. An instrumented
application never sees it — it knows the OTEL trace id, the 32-character hex its tracer generated.
annotate_trace takes that instead, and the root span of the trace is resolved for you.
This is how feedback from outside the platform gets recorded: a thumbs-down from an end user, a QA
harness, an eval script. It lands as an ordinary annotation, so a Pass/Fail verdict overrides the
trace’s automated outcome and appears wherever annotations already do. metric and turn work the
same way as above.
annotate_current_trace reads the trace id from the tracer, so it works after the span has closed,
which is when a human verdict usually arrives. It raises if there is no trace in context rather
than filing the verdict against nothing.
Spans are ingested asynchronously, so a trace recorded moments ago may not be queryable yet; that case raises too, and retrying is the right response. Waiting for the request to finish first leaves more time for the spans to arrive but does not guarantee they have, so a call made close to the request should be ready to retry. Reading annotations back takes the same hex:
for_trace_id covers annotations on any span of the trace, not only the root, since the question is
about the trace rather than one operation inside it.
Resolving and deleting
A resolve sends only that change, so a local edit to a comment cannot ride along with it.
Building an annotation by hand
annotate and create cover the common cases. Construct an Annotation directly when you need a field they do not take, such as attributes:
The write endpoints take the target nested under target, while responses return it flat as target_type and target_reference. push handles that remap, so set the flat pair and it arrives correctly.