Tagging Tokens
INIF’s tagging API is a small set of methods on Sample (single sample) and InifDocument (whole-document fan-out). The two surfaces share names so the receiver disambiguates the scope — sample.tag_by_regex(...) runs on one sample; doc.tag_by_regex(...) runs the same matching across every sample in the document.
All taggers add TokenAnnotation entries unless otherwise noted; per-name auto-merge applies, so calling the same method twice with the same name extends the existing record rather than creating a duplicate.
Regex over token strings
Tag every token whose decoded string matches a regex.
sample.tag_by_regex(r"^\d+$", "number") # one sample
doc.tag_by_regex(r"^\d+$", "number") # all samples in a docFor multiple patterns in a single token pass — much faster than calling the single-pattern method N times:
doc.tag_by_regexes([
(r"^\d+$", "number"),
(r"^[A-Z]+$", "uppercase"),
(r"\.$", "sentence_end"),
])Sequence refs
Token-string regex tagging is sequence-ref aware: the Sample methods accept a sequences= argument, and the InifDocument methods forward doc.sequences automatically. When a sample has refs, the tagger inspects the expanded view; if a match falls inside a ref, only that one ref is materialized in place — other refs in the sample, and refs in other samples that don’t have a match, are left untouched.
sample.tag_by_regex(r"\bParis\b", "city", sequences=doc.sequences)The Sequence itself is never modified — the materialization replaces the ref token in sample.tokens with copies of the sequence’s tokens, leaving doc.sequences intact. Other samples that pointed at the same sequence keep pointing at it.
Regex over concatenated text
Token-string regex only sees one token at a time, so it can’t match patterns that span token boundaries (e.g. "Eiffel" split by BPE into [" E", "iff", "el"]). For those, use tag_by_text_regex, which joins all token strings, runs the regex on the joined text, then maps character spans back to token indices.
from inif import TextTagMode
sample.tag_by_text_regex(r"Paris", "city") # default: ALL
sample.tag_by_text_regex(r"\d+\.\d+", "decimal", mode=TextTagMode.FIRST)
doc.tag_by_text_regex(r"\d{4}", "year")mode controls which tokens of a multi-token match get tagged:
| Mode | Behaviour |
|---|---|
ALL (default) |
Tag every token overlapping the match. |
FIRST |
Tag only the first token of each match. |
LAST |
Tag only the last token of each match. |
You can also pass the bare strings "all" / "first" / "last" instead of importing TextTagMode.
Predicate-based tagging
When you need full Python expressivity:
sample.tag_by_predicate(lambda t: t.id is not None and t.id > 50_000, "rare")
doc.tag_by_predicates([
(lambda t: t.id is not None and t.id > 50_000, "rare"),
(lambda t: t.token is not None and t.token.startswith(" "), "leading_space"),
])Predicates receive the TokenOrSeqRef object (not just the string), so you can filter by id, presence of extras, etc. Sequence-ref handling matches the regex methods.
Chat roles
tag_chat_roles annotates each token with the role of its enclosing chat message — system, user, assistant, tool — using character-span matching against tokenizer.apply_chat_template. Delimiters and template-generated content get tagged template. The Sample form takes one messages list; the InifDocument form takes a list-of-lists, one per sample, in the same order as doc.samples.
messages_per_sample = [
[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}],
...
]
doc.tag_chat_roles(messages_per_sample, tokenizer)For a single sample, call
sample.tag_chat_roles(messages, tokenizer, sequences=doc.sequences or None)sequences is needed when the document has been deduplicated so the tagger can expand refs whose expanded positions straddle a role boundary.
Generated-output tagging
The Inspect AI and evaleval converters add a generated annotation (metadata={"source": "converter"}) covering the last assistant message — the model’s actual response. You normally don’t need to call this yourself; it’s controlled via tag_generated=True (the default) on the converter entry points. See from_eval_log and from_instance_records for the option.
Reasoning and tool-call tagging
tag_reasoning=True on the eval converters runs the shared tag_template_field_renderings helper, which detects each per-message structured field (reasoning, tool_calls) by rendering the chat template twice — once with the field, once without — and using the diff to locate the chars the template emitted for that field. Tokens in the diff window get the field’s annotation (reasoning / tool_call) AND the message’s role (assistant), moved out of template if tag_chat_roles had labelled them there.
This is fully model-agnostic: the chat template itself decides where each field renders. You don’t normally call the helper directly; flip tag_reasoning=True (the default) on the converter entry points.
Special tokens
Tag every special token (BOS, EOS, etc.) discovered via the tokenizer’s all_special_ids:
sample.tag_special_tokens(tokenizer) # tag = "special"
sample.tag_special_tokens(tokenizer, "delimiter") # custom namePosition tagging
If you already know the positions, skip the matching helpers and use the annotation primitives directly:
sample.annotate_positions("manual", [3, 5, 7])
sample.annotate("manual", [(3, 8)]) # half-open range formannotate and annotate_positions are the underlying primitives all the tag methods build on; both auto-coalesce overlapping ranges and merge into the (single) annotation entry under that name.
Pattern compilation
Precompile patterns when you re-use them many times:
import re
NUMBER = re.compile(r"^\d+$")
INITIAL = re.compile(r"^[A-Z]\.")
doc.tag_by_regexes([(NUMBER, "number"), (INITIAL, "initial")])Pre-compiled re.Pattern and bare strings can be mixed freely.