Reference extractor¶
Reference only: bug fixes, no new features
These extractors get bug fixes only; work that would extend them is parked (DECISIONS #24). To check another extractor's output, start from Inputs.
An extractor is anything with extract(chunk, ontology) -> Iterable[Fact]. The
three in openodke.extract key an entity by its type and folded name
(Person:ada lovelace), stamp each fact with its predicate's identity keys
(DECISIONS #15), and check every span with
Span.is_faithful. documents= gives them each chunk's document; odke run
passes it for you.
PatternExtractor¶
Facts from a record, a pipe table or Key: value lines, with no model call
and confidence 1.0. Field names match predicate names, labels and aliases,
ignoring case and punctuation. A row with no nameable subject yields nothing.
mappings=[RecordMapping(...)] maps paths to predicates by hand.
from openodke import Chunk, Document, Ontology
from openodke.extract import PatternExtractor
from openodke.loaders import CsvLoader
people = Ontology.from_dict(
{
"name": "people",
"types": {"Person": {"keys": ["full_name"]}, "Company": {"keys": ["legal_name"]}},
"predicates": {
"full_name": {"domain": ["Person"], "aliases": ["name"]},
"born": {"domain": ["Person"], "range": "date"},
"employer": {"domain": ["Person"], "range": "Company"},
"legal_name": {"domain": ["Company"]},
},
}
)
(row,) = CsvLoader(tier="curated").load(
b"name,born,employer\nAda Lovelace,1815-12-10,Analytical Engines Ltd\n"
)
whole = Chunk(doc_id=row.id, start=0, end=len(row.text), text=row.text, index=0)
for fact in PatternExtractor(documents=[row]).extract(whole, people):
obj = fact.object_entity.key if fact.object_entity else fact.object_value
print(fact.subject.key, "|", fact.predicate, "|", obj)
# Person:ada lovelace | full_name | Ada Lovelace
# Person:ada lovelace | born | 1815-12-10
# Person:ada lovelace | employer | Company:analytical engines ltd
LLMExtractor¶
One model call per chunk, through the extract model role. Each
fact comes back with a quote and the offset where the model thinks it starts.
- A quote found at that offset stays there.
- A quote found elsewhere in the passage moves to the occurrence nearest the claim.
- A quote not in the passage is dropped. So is a predicate not in the snippet,
a type not in the prompt, a missing value or an unknown polarity. Each drop is
recorded in
rejections.
The quote asked for is the whole clause that states the fact, because the
default grounder judges a claim against its cited span. mention is the
narrower span inside it that tells one fact from its siblings
(DECISIONS #23).
| Option | Default |
|---|---|
client, spec, roles |
the extract role's client |
types |
every entity type gets a snippet |
snippet_limit |
25 predicates per snippet |
confidence |
0.5 |
repairs |
1 more attempt after a malformed reply |
structured |
True; False sends no response schema |
retry |
RetryPolicy(), for transient provider errors |
max_workers |
8 calls in flight, so the client must be thread-safe |
calls, rejections, empty_extractions and malformed count what happened.
A reply that no repair makes readable fails its chunk with UnreadableReply,
a ProviderError, and keeps the raw text in malformed: the pipeline leaves
that document out and names it in stats["failed"], so it is never scored as
a passage with nothing in it.
from openodke.extract import LLMExtractor
from openodke.llm import ScriptedClient
note = Document(id="note", text="Grace Hopper was born in 1906. She joined the US Navy in 1943.")
passage = Chunk(doc_id="note", start=0, end=len(note.text), text=note.text, index=0)
reply = {
"entities": [
{
"type": "Person",
"name": "Grace Hopper",
"facts": [
{"predicate": "born", "value": "1906", "quote": "born in 1906", "start": 3},
{"predicate": "employer", "value": "US Navy", "quote": "joined the US Navy"},
{"predicate": "born", "value": "1907", "quote": "born in 1907"},
],
}
]
}
extractor = LLMExtractor(client=ScriptedClient([reply]), documents=[note])
facts = extractor.extract(passage, people)
assert [(f.predicate, f.evidence[0].span.start) for f in facts] == [("born", 17), ("employer", 35)]
assert [r.reason for r in extractor.rejections] == ["quote not in the passage"]
What the model is shown¶
One OntologySnippet per entity type: its predicates, ranked by importance
then name, cut at snippet_limit, rendered as prose and as JSON Schema from one
object (DECISIONS #6). odke ontology snippet prints it.
print(people.snippet("Person").render())
# Entity type: Person
# Properties you may extract:
# - born (date, single)
# - employer (Company, single)
# - full_name (string, single)
HybridExtractor¶
HybridExtractor(llm, *, pattern=None, documents) routes each chunk by its
document's modality and merges both paths' facts by signature, keeping the more
confident one. A chunk whose document it was not given raises LookupError.
modality |
Pattern | Model |
|---|---|---|
structured |
yes | no |
semi_structured |
yes | yes |
unstructured |
no | yes |
from openodke import HybridExtractor, Pipeline
answer = {
"entities": [
{
"type": "Person",
"name": "Grace Hopper",
"facts": [{"predicate": "born", "value": "1906", "quote": "born in 1906"}],
}
]
}
documents = [row, note]
hybrid = HybridExtractor(LLMExtractor(client=ScriptedClient([answer])), documents=documents)
graph = Pipeline(people, hybrid).run(documents)
totals = hybrid.totals()
assert (totals.pattern_facts, totals.llm_facts, totals.model_calls) == (3, 1, 1)
In a run config¶
The extractors are pattern, llm and hybrid in an odke run
config.
stages:
extractor:
use: hybrid
llm: {snippet_limit: 25, repairs: 1}
pattern: {}