py-agent-ner
NER training-data generation built on py-agent-lib.
Architecture: one LLM call per (text, label). Each call uses a single-value
Literal["<that_label>"] enum on its response schema; the model returns the
verbatim substrings it found for that label. You fan out N calls per text
(asyncio or DagExecutor) and assemble a TrainingRecord from the N
responses. The package gives you the shapes, the prompt template, the schema
builder, and the BIO converter — nothing else. The loop, the fan-out, and
the merge are in your code where you can see them.
Install
pip install py-agent-ner
# or for local dev within this monorepo:
uv sync
py-agent-ner pulls in py-agent-lib[llm,fuzzy] (Pydantic + httpx +
instructor + rapidfuzz) plus Jinja2 — everything needed to run.
What you write yourself
The loop, the fan-out, the assembly. The package never hides any of these.
The canonical pattern (from examples/quickstart/run.py):
import asyncio
from py_agent_lib.adapters.llm import from_ollama
from py_agent_lib.adapters.spans import find_spans
from py_agent_ner import (
EntitySpan, LabelExtraction, TaggedEntity, TrainingRecord,
build_system_prompt, load_label_prompts, labels_of, single_label_schema,
)
label_prompts = load_label_prompts("./labels")
all_labels = labels_of(label_prompts)
base_template = open("./prompts/base_system.jinja").read()
llm = from_ollama("gemma4", think=False)
records = []
for text in your_texts:
# Fan out: N LLM calls, one per label, in parallel
extractions = await asyncio.gather(*[
llm.extract(
system=build_system_prompt(lp, all_labels=all_labels, base_template=base_template),
user=text,
response_model=single_label_schema(lp.label),
)
for lp in label_prompts
])
# Merge: assemble the TrainingRecord
entities = {
lp.label: TaggedEntity(
label=lp.label,
confidence=ext.confidence,
spans=[EntitySpan(text=s.text, start=s.start, end=s.end)
for s in find_spans(ext.matches, text, fuzzy=True)],
)
for lp, ext in zip(label_prompts, extractions, strict=True)
}
records.append(TrainingRecord(input_text=text, entities=entities))
That's the whole story. Add JSONL output and BIO conversion:
from py_agent_ner import write_records_jsonl, jsonl_to_bio
write_records_jsonl(records, "training_data.jsonl")
jsonl_to_bio("training_data.jsonl", "training_data.bio.tsv")
Public API
from py_agent_ner import (
# Pydantic v2 shapes (every field carries a description)
LabelPrompt, # input: label + instructions
LabelExtraction, # output of ONE call: label + matches + confidence
EntitySpan, # one located occurrence with character offsets
TaggedEntity, # one label's spans + confidence
TrainingRecord, # input text + per-label TaggedEntities (JSONL row)
# Prompts — single-label rendering, matching the per-label fan-out
DEFAULT_BASE_TEMPLATE,
build_system_prompt, # render ONE label's system prompt
label_prompts_from_dict, # {label: instructions} → [LabelPrompt]
load_label_prompts, # dir of .jinja → [LabelPrompt]
labels_of, # convenience
# Schema — 5-line helper around create_model + Literal[label]
single_label_schema, # name → type[LabelExtraction] with Literal[name]
# BIO conversion (JSONL → CoNLL TSV for HuggingFace / spaCy / Flair)
simple_tokenize,
record_to_bio,
jsonl_to_bio,
records_to_bio,
write_records_jsonl,
)
What py-agent-ner intentionally does NOT provide
- No
extract_training_data-style runner. You write the per-text loop. - No "merge" helper. Building the
dict[label, TaggedEntity]from NLabelExtractions is five lines you write in your code. - No multi-label wrapper class. The LLM returns
LabelExtractionper call. There is no "Extraction containing entities for all labels" type. - No DAG plumbing inside the package. Use
asyncio.gather(cheapest), or wire your fan-out as steps onpy-agent-lib'sDagExecutorif you want its observers / retries / cancellation / snapshots.
Architecture
your run.py
│ loops over texts
│ fans out N LLM calls per text via asyncio.gather
│ assembles TrainingRecord from N LabelExtractions
│
▼
py-agent-ner
models.py LabelPrompt, LabelExtraction, EntitySpan, TaggedEntity, TrainingRecord
prompts.py build_system_prompt + DEFAULT_BASE_TEMPLATE + loaders
schema.py single_label_schema (Literal[label_name] subclass of LabelExtraction)
bio.py JSONL → CoNLL BIO TSV
│
▼
py-agent-lib (domain-agnostic — knows nothing about NER)
adapters/llm from_ollama, from_openai, from_anthropic, from_litellm
adapters/spans find_spans (strict + opt-in rapidfuzz)
adapters/training write_records_jsonl, read_records_jsonl (generic Pydantic JSONL)
(also: DagExecutor, observers, snapshots, etc. — for users who want them)
Training a model from the output
jsonl_to_bio converts the JSONL to BIO TSV — the canonical input for:
- HuggingFace — follow the official token-classification tutorial verbatim.
- spaCy —
python -m spacy convert training_data.bio.tsv ./spacy-data/ --converter ner - GLiNER, Flair, custom — all consume BIO.
Tests
uv run pytest packages/py-agent-ner/tests/
Tests don't need a live model — they use a FakeLlm that returns canned
LabelExtraction responses for the per-label calls.
Metadata
Release files for py-agent-ner 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| py_agent_ner-0.1.1.tar.gz | 10.5 kB | With provenance | Details for py_agent_ner-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| py_agent_ner-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for py_agent_ner-0.1.1-py3-none-any.whl |
Total release size: 23.4 kB
Release files / py_agent_ner-0.1.1.tar.gz
| Download URL | py_agent_ner-0.1.1.tar.gz |
|---|---|
| Size | 10.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
70ec5df489088f170e3b482d2e03bf74e6493178978ca953e7e9b06acc7550bb
|
|
BLAKE2b-256 checksum How to use checksums |
4f4fb4a9a25c8d8ea987e18a2160a64af28fea88b51518980443a9b6850c57c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 26, 2026.
Transparency logRelease files / py_agent_ner-0.1.1-py3-none-any.whl
| Download URL | py_agent_ner-0.1.1-py3-none-any.whl |
|---|---|
| Size | 12.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
124d1108d44fdf50a0c591d8b9bc3b95e4a5420fa0d7e1a5eeaccb2f1aa392a2
|
|
BLAKE2b-256 checksum How to use checksums |
c617a5ffb249939156f3eb5e34e2d766f97e34dd9635abcca5022292d23f660d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 26, 2026.
Transparency logSecurity for py-agent-ner 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for py_agent_ner-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.