Annotate Datasets with Human-in-the-Loop Workflows
Create annotation views with categorical, numeric, and text labels, assign annotators, and log annotations programmatically with fi.annotations.
Create an annotation view with categorical, numeric, and text labels, assign annotators to review rows, and push annotations in bulk with Annotation.log_annotations().
| Time | Difficulty | Package |
|---|---|---|
| 15 min | Intermediate | futureagi + pandas |
- Future AGI account → app.futureagi.com
- API keys:
FI_API_KEYandFI_SECRET_KEY(see Get your API keys) - Python 3.11+
- A dataset with at least a few rows (see Dataset Management to create one)
- A tracing project with spans already logged, so the
context.span_idvalues used in Steps 5 and 6 resolve to something real (see Manual Tracing to create one)
Install
pip install futureagi pandas
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"
Tutorial
Open a trace and start an annotation view
Create the annotation view from inside the tracing project whose spans you want to annotate, not from a standalone dataset. A view created off a project-scoped trace carries that project with it; a view created outside a project doesn’t, and get_labels(project_id=...) (which log_annotations() calls under the hood) only returns labels attached to a project, so labels created without one are invisible to the SDK.
- Go to app.futureagi.com → Tracer (left sidebar under OBSERVE) and open the project with the spans you want to annotate
- Open any trace, then open its annotation drawer
- From the drawer, click Create New View and give it a descriptive name, e.g. “Response Quality Review”
You should see the view editor open with Static Fields, Response Fields, and Labels sections ready to configure.
Choose static and response fields
Static fields give annotators read-only context. Response fields hold the output they’ll judge.
- Under Static Fields, select the columns that provide reference context (e.g.
user_query,context). Annotators see these but can’t edit them - Under Response Fields, select the column with the model output to judge (e.g.
response)
You should see both columns previewed in the view layout before you add labels.
Create labels
Click Create New Label for each judgment you want annotators to make. Each label needs a name, an annotation type, and (for non-categorical types) a min/max range and a step size.
| Field | Description |
|---|---|
| Name | The label’s name, shown to annotators and used to reference it from the SDK |
| Annotation Type | Categorical (predefined options), Numeric (a score on a scale), or Text (free-form feedback) |
| Description | Required guidance shown to annotators on how to apply the label |
| Display Options | Numeric labels only: currently offers a single option, Slider |
| Min / Max Value | Required for Numeric and Text labels: the lower and upper bounds of the score (for Text, these map to minimum/maximum character length) |
| Step Size | Numeric labels only, required: the increment the slider moves by |
For this guide, create three labels:
| Label name | Annotation Type | Description | Min | Max | Step Size |
|---|---|---|---|---|---|
| Sentiment | Categorical | Overall tone of the response | N/A | N/A | N/A |
| Relevance Score | Numeric | How well the response addresses the query | 1 | 5 | 0.5 |
| Reviewer Notes | Text | Free-form feedback or corrections | 0 | 500 | N/A |
For the Sentiment label, define categories: “Positive”, “Negative”, “Neutral”.
Tip
For categorical labels, enable Auto Annotate during label creation. Annotate a handful of rows manually first, then review, accept, or override the labels it suggests for the remaining rows.
Click Save to store the label.
You should see the three labels listed under Labels in the view editor.
Assign annotators and label rows
- In the view’s Annotators section, add the workspace members who should contribute annotations
- Click Create to create the view
- Each annotator opens the view, sees the static fields as read-only context and the response field alongside the label inputs, and applies labels row by row
You should see each labeled row’s status update as annotators work through the dataset. Changes save automatically.
List labels and projects with the SDK
Before pushing annotations programmatically, confirm the exact label names and the project you’re targeting. Annotation.log_annotations() matches label names by exact string, so a typo or case mismatch raises a ValueError and aborts the call.
import os
from fi.annotations import Annotation
client = Annotation(
fi_api_key=os.environ["FI_API_KEY"],
fi_secret_key=os.environ["FI_SECRET_KEY"],
)
projects = client.list_projects()
for p in projects:
print(f" {p.name} (id: {p.id}, type: {p.project_type})")
labels = client.get_labels(project_id=projects[0].id)
for label in labels:
print(f" {label.name}: type {label.type} (id: {label.id})")Expected output:
My Tracing Project (id: proj_abc123, type: observe)
Sentiment: type categorical (id: lbl_001)
Relevance Score: type numeric (id: lbl_002)
Reviewer Notes: type text (id: lbl_003) Log annotations programmatically
For bulk annotation or CI pipelines, push a pandas DataFrame with Annotation.log_annotations(). Each row references a traced span by its context.span_id, and each annotation column follows annotation.{label_name}.{type}. The {label_name} segment must match a label name from the previous step exactly, including case and spaces.
import os
import pandas as pd
from fi.annotations import Annotation
client = Annotation(
fi_api_key=os.environ["FI_API_KEY"],
fi_secret_key=os.environ["FI_SECRET_KEY"],
)
# Column format: annotation.{label_name}.{type}
# Types: label (categorical), score (numeric), text, rating (1-5 stars), thumbs (True/False)
df = pd.DataFrame({
"context.span_id": ["span_abc123", "span_def456", "span_ghi789"],
"annotation.Sentiment.label": ["Positive", "Negative", "Neutral"],
"annotation.Relevance Score.score": [4.5, 2.0, 3.5],
"annotation.Reviewer Notes.text": [
"Accurate and well-structured response",
"Hallucinated a date that wasn't in the context",
"Correct but could be more concise",
],
"annotation.notes": [
"Reviewed by QA team",
"Flagged for retraining",
None,
],
})
result = client.log_annotations(df, project_name="My Tracing Project")
print(f"Annotations created: {result.annotationsCreated}")
print(f"Annotations updated: {result.annotationsUpdated}")
print(f"Notes created: {result.notesCreated}")
print(f"Errors: {result.errorsCount}")Expected output:
Annotations created: 9
Annotations updated: 0
Notes created: 2
Errors: 0Try it with a mismatched label name to see the guardrail in action. Rename annotation.Sentiment.label to annotation.sentiment.label (lowercase) and rerun:
df_bad = df.rename(columns={"annotation.Sentiment.label": "annotation.sentiment.label"})
client.log_annotations(df_bad, project_name="My Tracing Project")ValueError: No annotation label found for name 'sentiment' and type 'label' in project 'My Tracing Project'Fix it by copying the exact name from the get_labels() output in Step 5 (Sentiment, not sentiment) and rerun with the original DataFrame.
Note
The context.span_id values must correspond to spans already recorded in a tracing project. If a {label_name} segment doesn’t exactly match a label name in that project, log_annotations() raises ValueError: No annotation label found for name '{name}' and type '{value_type}' in project '{project_name}'.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
ValueError: No annotation label found for name '{name}' and type '{value_type}' in project '{project_name}' | The DataFrame column’s {label_name} segment doesn’t exactly match a label name in the project (matching is case-sensitive) | Run get_labels(project_id=...) and copy the label name verbatim into the column, including case and spaces |
log_annotations() succeeds but annotationsCreated is lower than expected | Some context.span_id values don’t correspond to a span in the target project | Verify span IDs against the tracing project before building the DataFrame, and check errorsCount in the response |
| An annotation column is ignored with no error | The column name has more or fewer than three dot-separated segments (a label name containing a dot does this) | Keep the column as annotation.{label_name}.{type} with no extra dots in the label name |
| Auto Annotate never suggests labels | No manual annotations exist yet for the platform to learn from, or the label isn’t Categorical | Annotate a handful of rows manually first; Auto Annotate only applies to Categorical labels |
| Annotator can’t see the view | The workspace member wasn’t added to the view’s Annotators section | Open the view, add the member under Annotators, and click Save |
client.list_projects() doesn’t include the expected project | project_name passed to log_annotations() doesn’t match a project’s exact name | Print p.name for each project returned by list_projects() and use that literal string |
Next
Use the annotated rows as a gold-standard set in Dataset SDK: Upload, Evaluate, and Download Results.
Questions & Discussion