rfgen.annotators¶
Phase 2 of the pipeline: turning verified per-emitter and scene metadata into
natural-language artifacts (captions, Q&A, reasoning, scene reports, contrastive
pairs). Every concrete annotator subclasses
BaseAnnotator; the inference call itself goes
through a BaseInferenceClient implementation
(text-only LLMs, vision-language, and audio-language models all register against
the same rfgen.inference_clients entry-point group).
Batch submission backends are planned future surface. The shipped annotation API uses synchronous BaseInferenceClient.complete calls behind BaseAnnotator.
Warning
Pre-implementation. Class signatures, parameter types, and class-attribute
defaults are proposals. Once code lands, this page will be regenerated from
docstrings via sphinx.ext.autodoc. The shape below matches what autodoc emits
so the swap is mechanical.
Module summary¶
from rfgen.annotators import BaseAnnotator, StoredRecord
from rfgen.enums import AnnotationType
from rfgen.inference import BaseInferenceClient, InferenceResponse
client: BaseInferenceClient = ... # any BaseInferenceClient implementation
annotator: BaseAnnotator = ...
stored_record: StoredRecord = ...
response = client.complete(
prompt="FACTS\nemitters: ...\nTASK\nWrite a caption.",
json_schema={"type": "object", "properties": {"caption": {"type": "string"}}},
max_tokens=200,
request_id="sample-001::caption::caption.dense.v3::run-2026-07-08",
template_id="caption.dense.v3",
run_id="run-2026-07-08",
)
record = annotator.annotate(
stored_record,
annotation_type=AnnotationType.CAPTION,
template_id="caption.dense.v3",
run_id="run-2026-07-08",
)
BaseInferenceClient is the provider-agnostic inference call surface. InferenceResponse is the parsed, metadata-tagged result yielded by every inference client.
Class index¶
Class |
Kind |
Notes |
|---|---|---|
datatype |
IQ-stripped metadata view of a stored record; the annotator pipeline input |
|
function |
Strips IQ and unsafe metadata fields from a Record, returning a StoredRecord |
|
datatype |
Provider-agnostic response from |
|
abc |
Inference-grounded text annotation contract; subclassed by every concrete annotator |
|
abc |
Provider-agnostic inference call surface (Gemini / Anthropic / OpenAI / vLLM, including vision- and audio-language models) |
|
class |
Pure deterministic prompt renderer; consumes only |
|
function |
Deterministically preselects partial PAES verifier subsets for |
|
enum / Literal |
The five annotation types: |
|
class |
Closed-vocabulary container injected into every prompt |
|
dataclass |
|
|
config |
Mapping from AnnotationType to its Pydantic output schema |
For the conceptual map of annotation types and the hallucination-control strategy, see Concepts / Annotations. For the normative prompt structure, output schemas, and validation rules, see Annotation Templates.
class rfgen.core_types.StoredRecord¶
@dataclass(frozen=True)
class StoredRecord:
"""IQ-stripped metadata view of a stored record.
The annotator pipeline (Templater, BaseAnnotator, and integrated PAES
scoring) reads this
view rather than the full Record so that Phase 2 workers never load IQ
tensors. Created by StoredRecord.from_record(record) and redacted by
whitelist_filter(record, sample_id=...) in the pipeline coordinator.
Lives in rfgen.core_types and is re-exported from rfgen.annotators.
"""
scene: SceneMetadata
emitters: tuple[SignalMetadata, ...]
bboxes: tuple[BBox, ...]
seg_mask: torch.Tensor | None = None
text: dict[str, object] | None
schema_version: int = 1
Kind. Frozen dataclass / value type.
Module. rfgen.core_types; re-exported from rfgen.annotators.
Field |
Type |
Description |
|---|---|---|
|
|
Per-emitter ground-truth metadata. Same ordering as the parent Record. The Templater reads |
|
Scene-level ground truth: duration, realized emitter count, receiver geometry, scene ID. |
|
|
|
Structured time-frequency boxes copied from the parent Record. |
|
|
Optional segmentation labels copied from the parent Record. |
|
|
Existing text annotations written by prior Phase 2 runs, keyed by |
|
int |
Stored-record schema version. |
Notes¶
IQ is excluded. The coordinator calls
whitelist_filter(record, sample_id=...)after reading from BaseStore to strip the IQ tensor before handing the view to annotators. This is a hard API boundary: no component in the annotator pipeline may request IQ.Construction.
whitelist_filter(...)starts fromStoredRecord.from_record(record), redacts prompt-unsafe metadata, and stores the validatedsample_idinscene.extras["sample_id"]for downstream routing. Annotator plugins receiveStoredRecord; they never construct it themselves.
whitelist_filter¶
from rfgen.annotators import whitelist_filter
def whitelist_filter(record: Record, *, sample_id: str) -> StoredRecord: ...
Module. rfgen.annotators (src/rfgen/annotators/__init__.py).
The IQ-stripping bridge between the storage layer and the annotator pipeline. Reads a full Record (which includes the IQ tensor) and returns a StoredRecord that contains only metadata fields safe to inject into inference prompts. The IQ tensor and per-device fingerprint parameters are excluded.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
Full record as returned by StoreHandle.read. Contains IQ, labels, per-emitter metadata, scene metadata, and any existing text annotations. |
|
|
str |
Content-hash identifier of this record in the store (the value returned by StoreHandle.write or passed to |
Returns¶
A StoredRecord with:
scene.extras["sample_id"]set to the suppliedsample_idargument.emitterscopied fromrecord.emitters, withextras.fingerprint_paramsandextras.channel_tapsremoved (these are per-device or per-path technical fields not useful for prose generation).scenecopied fromrecord.scene, withrealization_seed,config_hash, and operational fields removed.bboxesandseg_maskcopied from the parent record.textcopied fromrecord.text(existing annotations, orNonefor fresh records).
Notes¶
Hard boundary. This function is the only place in the annotator pipeline where
Record.iqis read; the returnedStoredRecordhas no IQ field. All annotator plugin code receivesStoredRecordand cannot access IQ by design.Deterministic. Same
recordandsample_idalways produce the sameStoredRecord. No randomness.Called by the coordinator, not plugins. Annotator plugins (
BaseAnnotator,Templater) receive aStoredRecordthat was already filtered. They do not callwhitelist_filterdirectly.
class rfgen.inference.InferenceResponse¶
@dataclass(frozen=True)
class InferenceResponse:
text: str
parsed: object | None
usage: TokenUsage
finish_reason: str
model_id: str
request_id: str
One parsed inference response returned by
BaseInferenceClient.complete.
Token counts live in the nested TokenUsage value as input_tokens,
output_tokens, and cached_input_tokens. parsed is populated only when the
caller supplied json_schema and provider output validated successfully;
otherwise callers read text and perform their own parsing.
class rfgen.annotators.Templater¶
class Templater:
"""Pure deterministic prompt renderer.
Same inputs yield byte-exact output. The templater consumes only the
output of `whitelist_filter(record, sample_id=...)`; it never reads `iq.zarr` and
never sees the raw signal.
"""
Kind. Class. Stub: full signature lands with the implementation.
The Templater builds a structured (system, user, output_schema)
Prompt from verified metadata, an
AnnotationType, a template_id
(e.g. "caption.dense.v3"), and a ClosedVocab.
It is the first stage of the shipped Phase 2 pipeline
(Templater -> inference client -> BaseAnnotator PAES scoring). See
Annotation Templates § Prompt structure for
the normative spec.
render()¶
def render(
self,
record: StoredRecord,
annotation_type: AnnotationType,
template_id: str,
) -> Prompt
Build a structured prompt from verified metadata. Pure function: same
inputs yield byte-exact output. The ALLOWED VOCABULARY block comes from
self.closed_vocab, which is set when the Templater
is constructed.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
IQ-stripped metadata view of the record to annotate. The templater reads |
|
|
Which annotation template family to render ( |
|
|
str |
Specific template variant, e.g. |
Returns¶
A Prompt carrying system, user, and output_schema ready for the inference adapter.
select_paes_verifier_subset¶
def select_paes_verifier_subset(
records: Iterable[StoredRecord],
*,
verifier_subset_pct: float,
global_seed: int | str | None = None,
config_hash: str | None = None,
) -> frozenset[str]: ...
The shipped PAES verifier path is integrated into
BaseAnnotator. There is no public
Verifier class in rfgen.annotators.
AnnotatorConfig.verifier_subset_pct accepts values from 0.0 through
100.0. 0.0 disables PAES and 100.0 scores every record handed to
annotate(...). Intermediate percentages require preselected membership from
select_paes_verifier_subset(...), then passed to
BaseAnnotator.from_components(...) as verifier_subset_sample_ids with the
same non-zero verifier_subset_pct.
global_seed and config_hash are optional deterministic tie-break inputs for
out-of-band subset planning. When omitted, the function uses uniform
scene.extras["global_seed"] and scene.extras["config_hash"] values if every
record carries the same value; otherwise it falls back to stable absent-value
sentinels. Supplying these arguments pins the same subset ordering across
workers that receive equivalent StoredRecord metadata.
The PAES metric itself is resolved at annotation time through an
EntryPointRegistry lookup for the canonical paes metric. That metric owns
its result schema. The annotator stores the returned object under the
annotation row’s paes field without exposing a separate verifier datatype.
class rfgen.annotators.AnnotationType¶
# Canonical definition in rfgen.enums; re-exported from rfgen.annotators
class AnnotationType(StrEnum):
CAPTION = "caption"
QA = "qa"
REASONING = "reasoning"
SCENE_REPORT = "scene_report"
CONTRASTIVE = "contrastive"
Kind. StrEnum (re-exported from rfgen.enums). The five normative annotation types
shipped by the framework. Used as a field on
InferenceResponse, as the routing key in
ANNOTATION_OUTPUT_SCHEMAS,
and as a parameter of Templater.render.
In Python code prefer the symbolic form (AnnotationType.CAPTION); in YAML
use the string value (caption). Pydantic coerces by value at validation
time.
See Reference / API / Enums § AnnotationType for the canonical definition and Annotation Templates § The five annotation types for per-type schemas, token budgets, and example outputs.
class rfgen.annotators.ClosedVocab¶
class ClosedVocab:
"""Closed-vocabulary container injected into every prompt.
Auto-generated from (1) the hierarchical taxonomy, (2) the channel-profile
list, (3) physical-attribute units, and (4) generic prose descriptors.
"""
Kind. Class. Stub: full signature lands with the implementation.
ClosedVocab is rendered into the ALLOWED VOCABULARY block of every
system prompt and is the basis for the post-hoc validator rule: a rewrite
that contains a token unknown to the closed vocabulary and claims a
physical attribute fails the record. Generic prose tokens that make no
physical claim are not constrained. See
Annotation Templates § Closed vocabulary.
class rfgen.annotators.Prompt¶
@dataclass(frozen=True)
class Prompt:
system: str # system instruction, including the forbid clause
user: str # user message: FACTS + TASK sections
output_schema: dict # JSON Schema for constrained decoding
Kind. Dataclass. The triple returned by
Templater.render. Each
provider adapter maps the triple to its native call shape (Gemini
response_json_schema, OpenAI response_format, Anthropic
tools[].input_schema, vLLM grammar). See
Annotation Templates § Prompt structure and
§ JSON-constrained decoding per provider.
ANNOTATION_OUTPUT_SCHEMAS¶
ANNOTATION_OUTPUT_SCHEMAS: dict[AnnotationType, type[BaseModel]] = {
AnnotationType.CAPTION: CaptionOutput,
AnnotationType.QA: QAOutput,
AnnotationType.REASONING: ReasoningOutput,
AnnotationType.SCENE_REPORT: SceneReportOutput,
AnnotationType.CONTRASTIVE: ContrastivePair,
}
Kind. Module-level config / registry. Maps each AnnotationType to the Pydantic schema that validates its raw model output. Used by the BaseAnnotator contract test:
schema_cls = ANNOTATION_OUTPUT_SCHEMAS[annotator.annotation_type]
schema_cls.model_validate(result)
The shipped schemas (CaptionOutput, QAOutput, ReasoningOutput,
SceneReportOutput, ContrastivePair) are defined in
Annotation Templates; this dict is the
single lookup table consumers use to dispatch validation.
Abstract base classes¶
class rfgen.annotators.BaseAnnotator¶
Inference-grounded text annotation. Operates on already-labeled samples (does not see IQ; only metadata).
class BaseAnnotator(ABC):
name: str
def annotate(
self,
record: StoredRecord,
*,
annotation_type: AnnotationType,
template_id: str,
run_id: str,
) -> StoredRecord:
"""Return record with the matching text annotation populated.
Implementation pattern:
1. Render a deterministic template from the StoredRecord metadata.
2. Call client.complete(...) for the requested annotation type, passing
request_id, template_id, and run_id for retry and validation context.
3. Optionally call a second client to verify the result is grounded.
4. Return a new StoredRecord with a text row keyed by
(annotation_type, template_id, run_id).
Implementations MUST NOT pass IQ or spectrograms to the model;
all hallucination control rests on metadata-only prompting plus
JSON-constrained decoding where the provider supports it.
"""
def annotate_store(
self,
handle: StoreHandle,
*,
sample_id: str,
annotation_type: AnnotationType,
template_id: str,
run_id: str,
) -> StoredRecord:
"""Read an existing stored sample and append one annotation row."""
annotate()¶
Public method on BaseAnnotator. Produces annotation text grounded only in metadata; never sees IQ or spectrograms. Re-running with an existing (annotation_type, template_id, run_id) row returns the input StoredRecord unchanged.
annotate_store()¶
Public method on BaseAnnotator.
Reads an existing sample from an open
StoreHandle, applies
whitelist_filter, runs
annotate(...), and persists the resulting row through
StoreHandle.append_annotation_row.
The sample_id does not change, and the store preserves IQ, scene metadata,
labels, canonical sample bytes, and any existing annotation rows.
class rfgen.inference.BaseInferenceClient¶
Defined in rfgen.inference.protocols and re-exported from rfgen.inference. The ABC is modality-agnostic: text-only LLMs, vision-language, and audio-language models all register against the same rfgen.inference_clients entry-point group.
class BaseInferenceClient(ABC):
"""Abstracts Gemini / Anthropic / OpenAI / vLLM (and vision- or audio-language
variants) behind a uniform interface."""
provider_name: ClassVar[str]
model_id: ClassVar[str]
@abstractmethod
def complete(
self,
*,
prompt: str | Messages,
json_schema: dict | None = None,
max_tokens: int | None = None,
request_id: str | None = None,
template_id: str = "",
run_id: str = "",
) -> InferenceResponse: ...
The shipped surface lives at src/rfgen/inference/protocols.py. Concrete
clients in rfgen.inference.clients provide Gemini, Anthropic, OpenAI, and
OpenAI-compatible vLLM request shaping. The cross-cutting ABC keeps a single
completion entry point so callers can swap providers without changing
annotator code.
complete()¶
Abstract method on BaseInferenceClient. Single synchronous inference call returning a typed InferenceResponse (parsed is populated when json_schema is supplied and the response validates). request_id, template_id, and run_id are per-call metadata used in retry logs and InferenceError context; they are not constructor fields.
Batch annotation orchestration¶
The shipped Phase 2 surface lives in rfgen.annotation_orchestrators. It owns
local and managed-provider submission, bounded polling, result validation,
durable resume handles, append-only persistence, cancellation, and provider
resource cleanup. It is separate from the synchronous inference-client surface
used by BaseAnnotator.
class rfgen.annotation_orchestrators.AnnotationJob¶
class AnnotationJob(BaseModel):
sample_id: str
annotation_type: AnnotationType
template_id: str
run_id: str
prompt: str
output_schema: dict[str, object] = {}
@property
def key(self) -> tuple[str, AnnotationType, str, str]: ...
The immutable job key is (sample_id, annotation_type, template_id, run_id).
All string fields must be non-empty. Provider outputs are checked against
output_schema before successful results are persisted.
class rfgen.annotation_orchestrators.BatchResult¶
An immutable result containing its AnnotationJob, an output, or an error
string. Provider failures, missing or duplicate provider IDs, malformed output,
and schema-validation failures become per-job errors rather than silently
changing result order.
class rfgen.annotation_orchestrators.BatchStatus¶
class BatchStatus(BaseModel):
total: int
pending: int
succeeded: int
failed: int
class rfgen.annotation_orchestrators.BatchHandle¶
An opaque handle returned by submit(). to_json() serializes the provider
identity, submitted jobs, provider-specific jobs, fetched and reused results,
adapter state, persisted job keys, and cleanup state. Pass that JSON to
resume() on a new orchestrator instance of the same provider class to resume
polling or cleanup without resubmitting completed work.
Handles are bound to the orchestrator instance that created or resumed them. Passing a handle to another instance raises an error.
class rfgen.annotation_orchestrators.BaseBatchAnnotationOrchestrator¶
class BaseBatchAnnotationOrchestrator(ABC):
@abstractmethod
def submit(self, jobs: Sequence[AnnotationJob]) -> BatchHandle: ...
def poll(self, handle: BatchHandle) -> BatchStatus: ...
@abstractmethod
def fetch(self, handle: BatchHandle) -> Iterable[BatchResult]: ...
@abstractmethod
def cancel(self, handle: BatchHandle) -> None: ...
def bind_persistence(
self,
*,
persist: Callable[[BatchResult], None],
load_completed: Callable[[AnnotationJob], BatchResult | None],
) -> None: ...
def resume(self, payload: str) -> BatchHandle: ...
poll() caches the last status until min_poll_interval_s has elapsed, which
prevents tight provider polling loops. Managed provider status normalization
counts cancelled and expired jobs as failed terminal results.
Delivery is at least once at the provider boundary and idempotent at dataset
persistence. Before submission, an orchestrator loads completed results by
AnnotationJob.key and submits only missing
jobs. During fetch, each key is persisted at most once per durable handle.
StoreAnnotationPersistence writes through the public append-only store API at
text[annotation_type][template_id][run_id], the same path it reads to detect
completed work.
Managed fetch persists validated results before attempting provider cleanup.
If cleanup fails, cleanup_complete remains false and the serialized handle
retains the fetched results plus persisted keys. Resuming and fetching that
handle retries cleanup without another provider fetch or second dataset write.
class rfgen.annotation_orchestrators.LocalLoopAnnotator¶
Synchronous sequential implementation for local and small runs. It accepts a completion callable, validates each output, records per-job errors, and reuses completed keys from its bounded cache or bound persistence.
Managed provider implementations¶
Class |
Provider lifecycle |
|---|---|
OpenAI Files and Batch APIs, including optional input-file deletion. |
|
Anthropic Message Batches submission, polling, result iteration, and cancellation. |
|
Vertex batch prediction with content-addressed Google Cloud Storage staging. |
|
SageMaker transform jobs with content-addressed Amazon S3 staging. |
|
Azure ML batch endpoints with content-addressed Azure Blob staging. |
Each managed class supports from_config() for typed provider resources and
from_sdk_client() for an injected official SDK boundary. Cloud SDK imports
are lazy. Missing optional dependencies raise BackendUnavailableError with
the required extra.
The provider adapters preserve submission ordering with provider custom_id
values and turn partial vendor failures into ordered
BatchResult entries.
Vertex, SageMaker, and Azure own their staging objects unless the corresponding
resource config sets retain_staging=true.
The legacy anchors below continue to resolve old links to the shipped classes.
Legacy class names¶
Pre-implementation drafts named the inference call surface differently. The shipped surface lives at rfgen.inference.protocols; the anchors below redirect old cross-references to the current shipped surface so other doc pages render.
Legacy: BaseLLMClient¶
Renamed to BaseInferenceClient in the shipped surface. The class wraps any inference backend (text-only LLMs, vision-language models, audio-language models), so the name was generalized.
Legacy: LLMResponse¶
Renamed to InferenceResponse in the shipped surface. The same modality-agnostic generalization applies.