lorebase-scribe 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lorebase_scribe/__init__.py +3 -0
- lorebase_scribe/__main__.py +156 -0
- lorebase_scribe/checkpoints.py +324 -0
- lorebase_scribe/cron.py +117 -0
- lorebase_scribe/embeddings.py +570 -0
- lorebase_scribe/extraction/__init__.py +17 -0
- lorebase_scribe/extraction/assemble.py +339 -0
- lorebase_scribe/extraction/candidates.py +136 -0
- lorebase_scribe/extraction/discovery.py +567 -0
- lorebase_scribe/extraction/llm_discovery.py +271 -0
- lorebase_scribe/extraction/matching.py +285 -0
- lorebase_scribe/extraction/ontology.py +432 -0
- lorebase_scribe/extraction/parsing.py +114 -0
- lorebase_scribe/extraction/pipeline.py +1091 -0
- lorebase_scribe/extraction/relations.py +194 -0
- lorebase_scribe/extraction/sandbox.py +269 -0
- lorebase_scribe/extraction/settings.py +293 -0
- lorebase_scribe/extraction/text.py +179 -0
- lorebase_scribe/extraction/tier2.py +573 -0
- lorebase_scribe/extraction/values.py +282 -0
- lorebase_scribe/jobs.py +418 -0
- lorebase_scribe/scheduler.py +318 -0
- lorebase_scribe/service.py +146 -0
- lorebase_scribe/settings.py +521 -0
- lorebase_scribe/sources/__init__.py +60 -0
- lorebase_scribe/sources/access.py +241 -0
- lorebase_scribe/sources/connectors.py +320 -0
- lorebase_scribe/sources/delta.py +175 -0
- lorebase_scribe/sources/http_json.py +283 -0
- lorebase_scribe/sources/mapping.py +429 -0
- lorebase_scribe/sources/profile.py +36 -0
- lorebase_scribe/sources/records.py +298 -0
- lorebase_scribe/sources/sql.py +332 -0
- lorebase_scribe/sources/sync.py +730 -0
- lorebase_scribe/telemetry.py +113 -0
- lorebase_scribe/webhooks.py +99 -0
- lorebase_scribe/worker.py +254 -0
- lorebase_scribe-0.2.3.dist-info/METADATA +23 -0
- lorebase_scribe-0.2.3.dist-info/RECORD +41 -0
- lorebase_scribe-0.2.3.dist-info/WHEEL +4 -0
- lorebase_scribe-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Candidates to Proposal items (design 9.2 step 11).
|
|
2
|
+
|
|
3
|
+
- A mention resolved as `new` becomes a `record` item holding its key and the best value per
|
|
4
|
+
Attribute found for it (defaults filled in). Its confidence is the lowest of the mention's and
|
|
5
|
+
its values'; it records the required Attributes and required Relations still missing.
|
|
6
|
+
- Values of existing Records become `value` items, one per Record and Attribute (the most
|
|
7
|
+
confident); values equal to the current one are dropped as unchanged.
|
|
8
|
+
- Links become `link` items, one per Relation and endpoints (the most confident); Links that
|
|
9
|
+
exist already are dropped. A Link of a foreign-key backed Relation becomes a value of its
|
|
10
|
+
Attribute.
|
|
11
|
+
- Items below `min_confidence` are dropped. An item is accepted at once when its confidence
|
|
12
|
+
reaches `auto_accept_threshold`, its match is not ambiguous, and, for a new Record, nothing
|
|
13
|
+
required is missing; a Link is accepted at once only when its endpoints exist or are accepted
|
|
14
|
+
at once too. Items that set a value of a vault Attribute are never accepted at once: the
|
|
15
|
+
extraction commit checks vault values against erased ones, so a reviewer decides (design
|
|
16
|
+
13.5).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
22
|
+
from dataclasses import dataclass, field, replace
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from lorebase_core.model import RelationBacking, new_ulid
|
|
27
|
+
from lorebase_core.types import EncodingError, encode_value
|
|
28
|
+
from lorebase_scribe.extraction.candidates import LinkCandidate, Resolved, ValueCandidate
|
|
29
|
+
from lorebase_scribe.extraction.ontology import Ontology
|
|
30
|
+
from lorebase_scribe.extraction.settings import ExtractionSettings
|
|
31
|
+
from lorebase_store.extraction import link_element, record_element
|
|
32
|
+
from lorebase_store.models.enums import ItemDecision
|
|
33
|
+
from lorebase_store.repositories.proposal_items import (
|
|
34
|
+
LINK_ITEM,
|
|
35
|
+
RECORD_ITEM,
|
|
36
|
+
VALUE_ITEM,
|
|
37
|
+
ItemRow,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
ITEM_PREFIX = "pit"
|
|
41
|
+
|
|
42
|
+
RecordKey = tuple[str, str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class CurrentState:
|
|
47
|
+
"""What the target branch holds for the Records and Links the candidates name."""
|
|
48
|
+
|
|
49
|
+
records: Mapping[RecordKey, Mapping[str, Any]]
|
|
50
|
+
links: frozenset[tuple[str, str, str]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Assembled:
|
|
55
|
+
items: list[ItemRow] = field(default_factory=list)
|
|
56
|
+
stats: dict[str, int] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def count(self, name: str, n: int = 1) -> None:
|
|
59
|
+
self.stats[name] = self.stats.get(name, 0) + n
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _best_values(
|
|
63
|
+
values: Iterable[ValueCandidate],
|
|
64
|
+
) -> dict[RecordKey, dict[str, ValueCandidate]]:
|
|
65
|
+
best: dict[RecordKey, dict[str, ValueCandidate]] = {}
|
|
66
|
+
for value in values:
|
|
67
|
+
if not value.subject.usable:
|
|
68
|
+
continue
|
|
69
|
+
element = (value.subject.kind_id, str(value.subject.key))
|
|
70
|
+
per = best.setdefault(element, {})
|
|
71
|
+
current = per.get(value.attribute_id)
|
|
72
|
+
if current is None or value.confidence > current.confidence:
|
|
73
|
+
per[value.attribute_id] = value
|
|
74
|
+
return best
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _best_links(links: Iterable[LinkCandidate]) -> dict[tuple[str, str, str], LinkCandidate]:
|
|
78
|
+
best: dict[tuple[str, str, str], LinkCandidate] = {}
|
|
79
|
+
for link in links:
|
|
80
|
+
if not (link.source.usable and link.target.usable):
|
|
81
|
+
continue
|
|
82
|
+
element = (link.relation_id, str(link.source.key), str(link.target.key))
|
|
83
|
+
if element not in best or link.confidence > best[element].confidence:
|
|
84
|
+
best[element] = link
|
|
85
|
+
return best
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def assemble_items(
|
|
89
|
+
ontology: Ontology,
|
|
90
|
+
mentions: Sequence[Resolved],
|
|
91
|
+
values: Sequence[ValueCandidate],
|
|
92
|
+
links: Sequence[LinkCandidate],
|
|
93
|
+
current: CurrentState,
|
|
94
|
+
*,
|
|
95
|
+
job_id: str,
|
|
96
|
+
principal: str,
|
|
97
|
+
now: datetime,
|
|
98
|
+
settings: ExtractionSettings,
|
|
99
|
+
) -> Assembled:
|
|
100
|
+
"""Items of an extraction Proposal, in order: Records, values, Links."""
|
|
101
|
+
result = Assembled()
|
|
102
|
+
threshold = settings.auto_accept_threshold
|
|
103
|
+
schema = ontology.schema
|
|
104
|
+
new_records: dict[RecordKey, Resolved] = {}
|
|
105
|
+
for mention in sorted(mentions, key=lambda m: (m.mention.start, m.kind_id)):
|
|
106
|
+
if mention.resolution.status == "new" and mention.key is not None:
|
|
107
|
+
new_records.setdefault((mention.kind_id, mention.key), mention)
|
|
108
|
+
|
|
109
|
+
pending_values = list(values)
|
|
110
|
+
best_links = _best_links(links)
|
|
111
|
+
for (relation_id, from_key, to_key), link in list(best_links.items()):
|
|
112
|
+
relation = schema.relations[relation_id]
|
|
113
|
+
if relation.backing is RelationBacking.FOREIGN_KEY and relation.foreign_key_attribute:
|
|
114
|
+
del best_links[(relation_id, from_key, to_key)]
|
|
115
|
+
pending_values.append(
|
|
116
|
+
ValueCandidate(
|
|
117
|
+
subject=link.source,
|
|
118
|
+
attribute_id=relation.foreign_key_attribute,
|
|
119
|
+
value=to_key,
|
|
120
|
+
evidence=link.evidence,
|
|
121
|
+
confidence=link.confidence,
|
|
122
|
+
tier=link.tier,
|
|
123
|
+
model=link.model,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
best_values = _best_values(pending_values)
|
|
127
|
+
|
|
128
|
+
drafts: list[tuple[int, ItemRow]] = []
|
|
129
|
+
auto_records: set[RecordKey] = set()
|
|
130
|
+
|
|
131
|
+
def decide(confident: bool) -> tuple[ItemDecision, str | None, datetime | None]:
|
|
132
|
+
if confident:
|
|
133
|
+
return ItemDecision.ACCEPTED, principal, now
|
|
134
|
+
return ItemDecision.PENDING, None, None
|
|
135
|
+
|
|
136
|
+
for element, mention in new_records.items():
|
|
137
|
+
kind_id, key = element
|
|
138
|
+
view = ontology.kinds[kind_id]
|
|
139
|
+
record_values: dict[str, Any] = {}
|
|
140
|
+
evidence: dict[str, Any] = {}
|
|
141
|
+
confidences = [mention.confidence]
|
|
142
|
+
if view.key_attribute is not None:
|
|
143
|
+
try:
|
|
144
|
+
record_values[view.key_attribute.id] = encode_value(view.key_attribute.type, key)
|
|
145
|
+
except EncodingError:
|
|
146
|
+
result.count("invalid_key")
|
|
147
|
+
continue
|
|
148
|
+
evidence[view.key_attribute.id] = mention.mention.evidence.as_dict()
|
|
149
|
+
for attribute_id, value in best_values.get(element, {}).items():
|
|
150
|
+
record_values[attribute_id] = value.value
|
|
151
|
+
evidence[attribute_id] = {**value.evidence.as_dict(), "model": value.model}
|
|
152
|
+
confidences.append(value.confidence)
|
|
153
|
+
for attribute in view.kind.attributes.values():
|
|
154
|
+
if attribute.id not in record_values and attribute.default is not None:
|
|
155
|
+
record_values[attribute.id] = attribute.default
|
|
156
|
+
missing = sorted(
|
|
157
|
+
a.api_name
|
|
158
|
+
for a in view.kind.attributes.values()
|
|
159
|
+
if a.required and not a.is_derived and a.id not in record_values
|
|
160
|
+
)
|
|
161
|
+
needed = [
|
|
162
|
+
r
|
|
163
|
+
for r in ontology.relations.values()
|
|
164
|
+
if r.required_from and r.from_kind == kind_id and r.backing is RelationBacking.EXPLICIT
|
|
165
|
+
]
|
|
166
|
+
provided = {
|
|
167
|
+
relation_id
|
|
168
|
+
for (relation_id, from_key, _to), link in best_links.items()
|
|
169
|
+
if from_key == key
|
|
170
|
+
and schema.relations[relation_id].from_kind == kind_id
|
|
171
|
+
and link.confidence >= threshold
|
|
172
|
+
and link.target.resolution.status == "matched"
|
|
173
|
+
}
|
|
174
|
+
missing_links = sorted(r.api_name for r in needed if r.id not in provided)
|
|
175
|
+
confidence = min(confidences)
|
|
176
|
+
if confidence < settings.min_confidence:
|
|
177
|
+
result.count("below_min_confidence")
|
|
178
|
+
continue
|
|
179
|
+
vaulted = any(
|
|
180
|
+
view.kind.attributes[a].in_vault
|
|
181
|
+
for a, v in record_values.items()
|
|
182
|
+
if v is not None and a in view.kind.attributes
|
|
183
|
+
)
|
|
184
|
+
confident = confidence >= threshold and not missing and not missing_links and not vaulted
|
|
185
|
+
if confident:
|
|
186
|
+
auto_records.add(element)
|
|
187
|
+
decision, by, at = decide(confident)
|
|
188
|
+
partition, element_key = record_element(kind_id, key)
|
|
189
|
+
drafts.append(
|
|
190
|
+
(
|
|
191
|
+
mention.mention.start,
|
|
192
|
+
ItemRow(
|
|
193
|
+
item_id=f"{ITEM_PREFIX}_{new_ulid()}",
|
|
194
|
+
item_type=RECORD_ITEM,
|
|
195
|
+
seq=0,
|
|
196
|
+
subject={
|
|
197
|
+
"kind": kind_id,
|
|
198
|
+
"key": key,
|
|
199
|
+
"values": record_values,
|
|
200
|
+
"evidence": evidence,
|
|
201
|
+
},
|
|
202
|
+
partition=partition,
|
|
203
|
+
element_key=element_key,
|
|
204
|
+
evidence=mention.mention.evidence.as_dict(),
|
|
205
|
+
confidence=round(confidence, 4),
|
|
206
|
+
decision=decision,
|
|
207
|
+
decided_by=by,
|
|
208
|
+
decided_at=at,
|
|
209
|
+
job_id=job_id,
|
|
210
|
+
match={
|
|
211
|
+
**mention.resolution.as_dict(),
|
|
212
|
+
"complete": not missing and not missing_links,
|
|
213
|
+
"missing": missing,
|
|
214
|
+
"missing_links": missing_links,
|
|
215
|
+
},
|
|
216
|
+
tier=mention.mention.tier,
|
|
217
|
+
model=mention.mention.extractor,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
for element, per_attribute in best_values.items():
|
|
223
|
+
if element in new_records:
|
|
224
|
+
continue
|
|
225
|
+
stored = current.records.get(element)
|
|
226
|
+
if stored is None:
|
|
227
|
+
result.count("unknown_record", len(per_attribute))
|
|
228
|
+
continue
|
|
229
|
+
for attribute_id, value in per_attribute.items():
|
|
230
|
+
if stored.get(attribute_id) == value.value:
|
|
231
|
+
result.count("unchanged")
|
|
232
|
+
continue
|
|
233
|
+
if value.confidence < settings.min_confidence:
|
|
234
|
+
result.count("below_min_confidence")
|
|
235
|
+
continue
|
|
236
|
+
resolution = value.subject.resolution
|
|
237
|
+
target = schema.kinds[element[0]].attributes.get(attribute_id)
|
|
238
|
+
confident = (
|
|
239
|
+
value.confidence >= threshold
|
|
240
|
+
and resolution.status == "matched"
|
|
241
|
+
and not (target is not None and target.in_vault)
|
|
242
|
+
)
|
|
243
|
+
decision, by, at = decide(confident)
|
|
244
|
+
partition, element_key = record_element(*element)
|
|
245
|
+
drafts.append(
|
|
246
|
+
(
|
|
247
|
+
value.evidence.start,
|
|
248
|
+
ItemRow(
|
|
249
|
+
item_id=f"{ITEM_PREFIX}_{new_ulid()}",
|
|
250
|
+
item_type=VALUE_ITEM,
|
|
251
|
+
seq=0,
|
|
252
|
+
subject={
|
|
253
|
+
"kind": element[0],
|
|
254
|
+
"key": element[1],
|
|
255
|
+
"attribute": attribute_id,
|
|
256
|
+
"value": value.value,
|
|
257
|
+
"current": stored.get(attribute_id),
|
|
258
|
+
},
|
|
259
|
+
partition=partition,
|
|
260
|
+
element_key=element_key,
|
|
261
|
+
attribute_id=attribute_id,
|
|
262
|
+
evidence=value.evidence.as_dict(),
|
|
263
|
+
confidence=round(value.confidence, 4),
|
|
264
|
+
decision=decision,
|
|
265
|
+
decided_by=by,
|
|
266
|
+
decided_at=at,
|
|
267
|
+
job_id=job_id,
|
|
268
|
+
match=resolution.as_dict(),
|
|
269
|
+
tier=value.tier,
|
|
270
|
+
model=value.model,
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
for (relation_id, from_key, to_key), link in best_links.items():
|
|
276
|
+
if (relation_id, from_key, to_key) in current.links:
|
|
277
|
+
result.count("unchanged")
|
|
278
|
+
continue
|
|
279
|
+
if link.confidence < settings.min_confidence:
|
|
280
|
+
result.count("below_min_confidence")
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
def ready(side: Resolved) -> bool:
|
|
284
|
+
if side.resolution.status == "matched":
|
|
285
|
+
return True
|
|
286
|
+
return (side.kind_id, str(side.key)) in auto_records
|
|
287
|
+
|
|
288
|
+
missing_attributes = sorted(
|
|
289
|
+
a.api_name
|
|
290
|
+
for a in schema.relations[relation_id].attributes.values()
|
|
291
|
+
if a.required and not a.is_derived and a.id not in link.values
|
|
292
|
+
)
|
|
293
|
+
confident = (
|
|
294
|
+
link.confidence >= threshold
|
|
295
|
+
and ready(link.source)
|
|
296
|
+
and ready(link.target)
|
|
297
|
+
and not missing_attributes
|
|
298
|
+
)
|
|
299
|
+
decision, by, at = decide(confident)
|
|
300
|
+
partition, element_key = link_element(relation_id, from_key, to_key)
|
|
301
|
+
drafts.append(
|
|
302
|
+
(
|
|
303
|
+
link.evidence.start,
|
|
304
|
+
ItemRow(
|
|
305
|
+
item_id=f"{ITEM_PREFIX}_{new_ulid()}",
|
|
306
|
+
item_type=LINK_ITEM,
|
|
307
|
+
seq=0,
|
|
308
|
+
subject={
|
|
309
|
+
"relation": relation_id,
|
|
310
|
+
"from": from_key,
|
|
311
|
+
"to": to_key,
|
|
312
|
+
"values": dict(link.values),
|
|
313
|
+
},
|
|
314
|
+
partition=partition,
|
|
315
|
+
element_key=element_key,
|
|
316
|
+
evidence=link.evidence.as_dict(),
|
|
317
|
+
confidence=round(link.confidence, 4),
|
|
318
|
+
decision=decision,
|
|
319
|
+
decided_by=by,
|
|
320
|
+
decided_at=at,
|
|
321
|
+
job_id=job_id,
|
|
322
|
+
match={
|
|
323
|
+
"from": link.source.resolution.as_dict(),
|
|
324
|
+
"to": link.target.resolution.as_dict(),
|
|
325
|
+
"missing": missing_attributes,
|
|
326
|
+
},
|
|
327
|
+
tier=link.tier,
|
|
328
|
+
model=link.model,
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
order = {RECORD_ITEM: 0, VALUE_ITEM: 1, LINK_ITEM: 2}
|
|
334
|
+
drafts.sort(key=lambda d: (order[d[1].item_type], d[0], d[1].partition, d[1].element_key))
|
|
335
|
+
if len(drafts) > settings.max_items_per_job:
|
|
336
|
+
result.count("over_item_limit", len(drafts) - settings.max_items_per_job)
|
|
337
|
+
drafts = drafts[: settings.max_items_per_job]
|
|
338
|
+
result.items = [replace(row, seq=index) for index, (_start, row) in enumerate(drafts)]
|
|
339
|
+
return result
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Candidates the tiers produce before they become Proposal items.
|
|
2
|
+
|
|
3
|
+
Offsets are always offsets into the parsed document text (end exclusive).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
MatchStatus = Literal["matched", "new", "ambiguous", "unresolved"]
|
|
12
|
+
MentionSource = Literal["key", "title", "span", "model"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class Evidence:
|
|
17
|
+
document_id: str
|
|
18
|
+
chunk: int
|
|
19
|
+
start: int
|
|
20
|
+
end: int
|
|
21
|
+
quote: str
|
|
22
|
+
|
|
23
|
+
def as_dict(self) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"document_id": self.document_id,
|
|
26
|
+
"chunk": self.chunk,
|
|
27
|
+
"start": self.start,
|
|
28
|
+
"end": self.end,
|
|
29
|
+
"quote": self.quote,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class Mention:
|
|
35
|
+
"""A piece of text naming a Record of `kind_id` (by key or by title)."""
|
|
36
|
+
|
|
37
|
+
kind_id: str
|
|
38
|
+
text: str
|
|
39
|
+
start: int
|
|
40
|
+
end: int
|
|
41
|
+
chunk: int
|
|
42
|
+
score: float
|
|
43
|
+
tier: int
|
|
44
|
+
source: MentionSource
|
|
45
|
+
extractor: str
|
|
46
|
+
document_id: str
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def evidence(self) -> Evidence:
|
|
50
|
+
return Evidence(self.document_id, self.chunk, self.start, self.end, self.text)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class MatchCandidate:
|
|
55
|
+
kind_id: str
|
|
56
|
+
key: str
|
|
57
|
+
title: str | None
|
|
58
|
+
score: float
|
|
59
|
+
method: str
|
|
60
|
+
|
|
61
|
+
def as_dict(self) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"key": self.key,
|
|
64
|
+
"title": self.title,
|
|
65
|
+
"score": round(self.score, 4),
|
|
66
|
+
"method": self.method,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class Resolution:
|
|
72
|
+
"""Entity resolution of a mention: the Record it names, a new one, or several."""
|
|
73
|
+
|
|
74
|
+
kind_id: str
|
|
75
|
+
status: MatchStatus
|
|
76
|
+
key: str | None
|
|
77
|
+
score: float
|
|
78
|
+
method: str
|
|
79
|
+
candidates: tuple[MatchCandidate, ...] = ()
|
|
80
|
+
|
|
81
|
+
def as_dict(self) -> dict[str, Any]:
|
|
82
|
+
return {
|
|
83
|
+
"status": self.status,
|
|
84
|
+
"method": self.method,
|
|
85
|
+
"score": round(self.score, 4),
|
|
86
|
+
"candidates": [c.as_dict() for c in self.candidates],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class Resolved:
|
|
92
|
+
"""A mention with its resolution; `confidence` combines both."""
|
|
93
|
+
|
|
94
|
+
mention: Mention
|
|
95
|
+
resolution: Resolution
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def key(self) -> str | None:
|
|
99
|
+
return self.resolution.key
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def kind_id(self) -> str:
|
|
103
|
+
return self.resolution.kind_id
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def confidence(self) -> float:
|
|
107
|
+
return self.mention.score * self.resolution.score
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def usable(self) -> bool:
|
|
111
|
+
return self.resolution.status != "unresolved" and self.resolution.key is not None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(slots=True)
|
|
115
|
+
class ValueCandidate:
|
|
116
|
+
"""An Attribute value for the Record `subject` names."""
|
|
117
|
+
|
|
118
|
+
subject: Resolved
|
|
119
|
+
attribute_id: str
|
|
120
|
+
value: Any
|
|
121
|
+
evidence: Evidence
|
|
122
|
+
confidence: float
|
|
123
|
+
tier: int
|
|
124
|
+
model: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(slots=True)
|
|
128
|
+
class LinkCandidate:
|
|
129
|
+
relation_id: str
|
|
130
|
+
source: Resolved
|
|
131
|
+
target: Resolved
|
|
132
|
+
evidence: Evidence
|
|
133
|
+
confidence: float
|
|
134
|
+
tier: int
|
|
135
|
+
model: str
|
|
136
|
+
values: dict[str, Any] = field(default_factory=dict)
|