Annotate an existing dataset¶
Start from a dataset that already contains in-phase/quadrature (IQ) samples, structured labels, and metadata, then produce text annotations such as captions, Q&A, reasoning, scene reports, or contrastive text. In rfgen terms, this runs Phase 2 annotation over records written by the Phase 1 storage layer.
The shipped Phase 2 paths are the rfgen annotate command, a synchronous local
orchestrator, five managed batch orchestrators, and the lower-level Python
annotator API.
Prerequisites¶
Install the inference-client dependency for the provider you use. Local vLLM uses the OpenAI-compatible HTTP surface.
pip install "rfgen[openai-batch]"
pip install "rfgen[anthropic-batch]"
pip install "rfgen[gcp]"
Set only the provider keys selected by your AnnotatorConfig.
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
Use an existing store as both input and append target. The storage API keeps the Phase 1 canonical record bytes immutable and appends Phase 2 text rows through StoreHandle.append_annotation_row.
Run the bundled local command¶
The bundled annotation config is a deterministic local example. It contains
one job and one matching local_responses entry, so it runs without a provider
SDK or credentials.
rfgen annotate
To append the result to an existing dataset, set its URI and storage backend:
rfgen annotate \
store_uri=./out/phase1 \
store_backend=zarr_local
The command composes annotation.yaml through Hydra, validates every job as a
frozen AnnotationJob, and dispatches through the rfgen.commands and
rfgen.annotation_orchestrators registries. Each job requires sample_id,
annotation_type, template_id, run_id, prompt, and output_schema.
Submit a managed batch¶
Select a managed orchestrator and supply its typed resources. This Vertex example uses the selected stable Gemini model:
store_uri: gs://rfgen-datasets/phase1
store_backend: zarr_gcs
orchestrator_config:
name: vertex_batch
min_poll_interval_s: 10.0
resources:
project: ${oc.env:GCP_PROJECT}
location: us-central1
model: gemini-3.1-flash-lite
input_uri: gs://rfgen-batch/input/
output_uri: gs://rfgen-batch/output/
retain_staging: false
jobs:
- sample_id: sample-0001
annotation_type: caption
template_id: caption.dense.v3
run_id: text-v1
prompt: Describe the RF activity in this record.
output_schema:
type: object
required: [caption]
properties:
caption: {type: string}
local_responses: {}
Save the file as managed-annotation.yaml, then run:
rfgen annotate --config-dir . --config-name managed-annotation
If the provider batch is still pending, the command prints a serialized handle
and exits successfully. Store that JSON as durable run state. A later command
must provide the JSON through resume_handle_json and leave jobs empty;
supplying both is rejected.
# resume-annotation.yaml
store_uri: gs://rfgen-datasets/phase1
store_backend: zarr_gcs
orchestrator_config:
name: vertex_batch
min_poll_interval_s: 10.0
resources:
project: ${oc.env:GCP_PROJECT}
location: us-central1
model: gemini-3.1-flash-lite
input_uri: gs://rfgen-batch/input/
output_uri: gs://rfgen-batch/output/
retain_staging: false
jobs: []
local_responses: {}
resume_handle_json: '<serialized handle JSON>'
rfgen annotate --config-dir . --config-name resume-annotation
Polling uses a monotonic clock and makes at most one provider poll request per
min_poll_interval_s for each handle. Calls inside that interval return the
cached status.
Retry persistence and cleanup safely¶
The stable job key is (sample_id, annotation_type, template_id, run_id).
Submitting a key that is already durable reuses its stored result instead of
submitting and billing it again.
Managed fetch is at-least-once. The handle checkpoints normalized provider results before persistence and records each key after its append-only store write succeeds. If persistence fails partway through a batch, resume with the latest serialized handle. The retry skips durable keys and continues with the first unpersisted result without refetching checkpointed provider output.
Provider cleanup begins only after every result is durable. If cleanup fails,
the error remains visible and the handle remains incomplete. Retry with the
same handle; the orchestrator repeats cleanup without rewriting annotations or
refetching provider output. retain_staging: false deletes managed staging on
successful cleanup, while true preserves it.
Build the annotator¶
Create a shipped AnnotatorConfig
and construct a MetadataAnnotator.
The example enables all five implemented annotation types.
from rfgen.annotators import MetadataAnnotator
from rfgen.config.annotation import AnnotatorConfig, LLMConfig
from rfgen.enums import AnnotationType
from rfgen.inference.clients import OpenAIClient
config = AnnotatorConfig(
enabled=True,
types=[
AnnotationType.CAPTION,
AnnotationType.QA,
AnnotationType.REASONING,
AnnotationType.SCENE_REPORT,
AnnotationType.CONTRASTIVE,
],
bulk_llm=LLMConfig(
provider="openai",
model="gpt-4o-mini",
temperature=0.2,
max_tokens=512,
json_schema_mode=True,
),
)
client = OpenAIClient.from_llm_config(config.bulk_llm)
annotator = MetadataAnnotator.from_config(config, bulk_client=client)
For one annotation type, restrict types:
config = AnnotatorConfig(
enabled=True,
types=[AnnotationType.REASONING],
bulk_llm=LLMConfig(provider="openai", model="gpt-4o-mini"),
)
Run synchronously¶
The annotator receives a StoredRecord,
which is a metadata-only view. For existing Phase 1 stores, call
annotate_store(...): it reads the full record from an open
StoreHandle, strips IQ through
whitelist_filter, and appends
the text row under the existing sample_id without rewriting IQ, scene, or
labels.
from rfgen.annotators import MetadataAnnotator
from rfgen.enums import AnnotationType, StoreMode
from rfgen.storage import ZarrStore
dataset_uri = "./out/phase1"
run_id = "text-v1"
store = ZarrStore(uri=dataset_uri)
with store.open(dataset_uri, StoreMode.APPEND) as handle:
for sample_id in handle.iter_sample_ids():
for annotation_type in config.types:
template_id = {
AnnotationType.CAPTION: "caption.dense.v3",
AnnotationType.QA: "qa.attribute.v1",
AnnotationType.REASONING: "reasoning.physical.v1",
AnnotationType.SCENE_REPORT: "scene_report.summary.v1",
AnnotationType.CONTRASTIVE: "contrastive.hard_negative.v1",
}[annotation_type]
stored = annotator.annotate_store(
handle,
sample_id=sample_id,
annotation_type=annotation_type,
template_id=template_id,
run_id=run_id,
)
assert stored.text is not None
Re-running annotate_store(...) with the same (sample_id, annotation_type, template_id, run_id) is idempotent. The store keeps the original sample bytes
and appends only the new annotation row. Use a new run_id or template_id
when you want a distinct text run.
Add verification¶
Set verifier_subset_pct=100.0 and provide verifier_llm plus a PAES metric
registry when you want every record handed to annotate(...) verified. For
partial verification, preselect sample IDs with
select_paes_verifier_subset
and construct the annotator through from_components(...) with
verifier_subset_sample_ids.
config = AnnotatorConfig(
enabled=True,
types=[AnnotationType.CAPTION],
bulk_llm=LLMConfig(provider="openai", model="gpt-4o-mini"),
verifier_llm=LLMConfig(provider="anthropic", model="claude-sonnet-4-6"),
verifier_subset_pct=100.0,
)
PAES support depends on a registered rfgen.metrics entry named paes; if the
metric is not registered, annotate(...) raises an AnnotationError instead
of silently skipping verification.
Inspect the result¶
Open the same dataset and read back the text field from a record:
with ZarrStore(uri=dataset_uri).open(dataset_uri, StoreMode.READ) as store:
sample_id = next(store.iter_sample_ids())
annotated = store.read(sample_id)
print(annotated.text)
The text dictionary is keyed by annotation type, template id, and run id. Each row records the JSON output, model id, request id, token usage, optional PAES score, and schema version.
Tips¶
Schema failures
MetadataAnnotator validates model output against the annotation-type schema.
Invalid JSON or schema mismatches raise InferenceError with the template id,
run id, request id, and validation error in context.
Closed-vocabulary mismatches
The default closed vocabulary rejects unsupported RF claims in generated text. If you extend the emitter taxonomy, update the vocabulary before re-running the annotation loop.
Cost controls
Managed orchestrators reuse durable job keys, but they do not define a global spending budget. Limit cost by submitting fewer jobs, choosing a lower-cost provider model, and enforcing provider quotas outside rfgen. Preserve every pending handle so a retry resumes the existing provider batch instead of starting another one.
See Also¶
Concepts / Annotations: grounding contract and Phase 2 lifecycle.
Reference / API /
rfgen.annotators: exact annotator, template, and verifier contracts.Reference / Annotation Templates: built-in template ids and output schemas.