indico_toolkit 6.14.0__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.
- indico_toolkit/__init__.py +24 -0
- indico_toolkit/association/__init__.py +15 -0
- indico_toolkit/association/association.py +65 -0
- indico_toolkit/association/extracted_tokens.py +106 -0
- indico_toolkit/association/line_items.py +189 -0
- indico_toolkit/association/positioning.py +301 -0
- indico_toolkit/association/splitting.py +41 -0
- indico_toolkit/auto_populate/__init__.py +3 -0
- indico_toolkit/auto_populate/populator.py +362 -0
- indico_toolkit/auto_populate/types.py +66 -0
- indico_toolkit/auto_review/__init__.py +6 -0
- indico_toolkit/auto_review/auto_review_functions.py +123 -0
- indico_toolkit/auto_review/auto_reviewer.py +62 -0
- indico_toolkit/client.py +35 -0
- indico_toolkit/errors.py +32 -0
- indico_toolkit/etloutput/__init__.py +210 -0
- indico_toolkit/etloutput/cell.py +47 -0
- indico_toolkit/etloutput/errors.py +16 -0
- indico_toolkit/etloutput/etloutput.py +110 -0
- indico_toolkit/etloutput/range.py +30 -0
- indico_toolkit/etloutput/table.py +44 -0
- indico_toolkit/etloutput/token.py +25 -0
- indico_toolkit/indico_wrapper/__init__.py +15 -0
- indico_toolkit/indico_wrapper/dataset.py +114 -0
- indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
- indico_toolkit/indico_wrapper/download.py +193 -0
- indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
- indico_toolkit/indico_wrapper/modelop.py +200 -0
- indico_toolkit/indico_wrapper/reviewer.py +111 -0
- indico_toolkit/indico_wrapper/workflow.py +231 -0
- indico_toolkit/metrics/__init__.py +11 -0
- indico_toolkit/metrics/compare_ground_truth.py +123 -0
- indico_toolkit/metrics/compare_models.py +180 -0
- indico_toolkit/metrics/metrics.py +373 -0
- indico_toolkit/metrics/plotting.py +102 -0
- indico_toolkit/ocr/__init__.py +9 -0
- indico_toolkit/ocr/customocr_object.py +42 -0
- indico_toolkit/ocr/ondoc_object.py +93 -0
- indico_toolkit/ocr/standard_object.py +54 -0
- indico_toolkit/pipelines/__init__.py +3 -0
- indico_toolkit/pipelines/file_processing.py +157 -0
- indico_toolkit/polling/__init__.py +8 -0
- indico_toolkit/polling/autoreview.py +204 -0
- indico_toolkit/polling/downstream.py +145 -0
- indico_toolkit/polling/queries.py +68 -0
- indico_toolkit/py.typed +0 -0
- indico_toolkit/results/__init__.py +104 -0
- indico_toolkit/results/document.py +78 -0
- indico_toolkit/results/errors.py +4 -0
- indico_toolkit/results/model.py +60 -0
- indico_toolkit/results/normalization.py +159 -0
- indico_toolkit/results/predictionlist.py +395 -0
- indico_toolkit/results/predictions/__init__.py +88 -0
- indico_toolkit/results/predictions/box.py +58 -0
- indico_toolkit/results/predictions/citation.py +47 -0
- indico_toolkit/results/predictions/classification.py +50 -0
- indico_toolkit/results/predictions/documentextraction.py +160 -0
- indico_toolkit/results/predictions/extraction.py +35 -0
- indico_toolkit/results/predictions/formextraction.py +121 -0
- indico_toolkit/results/predictions/group.py +29 -0
- indico_toolkit/results/predictions/prediction.py +41 -0
- indico_toolkit/results/predictions/span.py +43 -0
- indico_toolkit/results/predictions/summarization.py +103 -0
- indico_toolkit/results/predictions/unbundling.py +51 -0
- indico_toolkit/results/result.py +152 -0
- indico_toolkit/results/review.py +32 -0
- indico_toolkit/results/utilities.py +73 -0
- indico_toolkit/retry.py +94 -0
- indico_toolkit/snapshots/__init__.py +3 -0
- indico_toolkit/snapshots/snapshot.py +340 -0
- indico_toolkit/structure/__init__.py +3 -0
- indico_toolkit/structure/create_structure.py +232 -0
- indico_toolkit/structure/queries.py +119 -0
- indico_toolkit/structure/utils.py +16 -0
- indico_toolkit/types/__init__.py +12 -0
- indico_toolkit/types/classification.py +92 -0
- indico_toolkit/types/extractions.py +227 -0
- indico_toolkit/types/predictions.py +28 -0
- indico_toolkit/types/workflow_object.py +133 -0
- indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
- indico_toolkit-6.14.0.dist-info/METADATA +144 -0
- indico_toolkit-6.14.0.dist-info/RECORD +83 -0
- indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import time
|
|
3
|
+
from json import loads
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Tuple, Union
|
|
7
|
+
|
|
8
|
+
from indico import IndicoClient
|
|
9
|
+
from indico.queries import (
|
|
10
|
+
CreateExport,
|
|
11
|
+
DownloadExport,
|
|
12
|
+
GetDataset,
|
|
13
|
+
GetModelGroup,
|
|
14
|
+
)
|
|
15
|
+
from indico.types import Workflow
|
|
16
|
+
|
|
17
|
+
from ..errors import ToolkitPopulationError
|
|
18
|
+
from ..structure.create_structure import Structure
|
|
19
|
+
from .types import (
|
|
20
|
+
Example,
|
|
21
|
+
ExampleList,
|
|
22
|
+
LabelInput,
|
|
23
|
+
LabelInst,
|
|
24
|
+
TokenSpanInput,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AutoPopulator:
|
|
29
|
+
def __init__(self, client: IndicoClient):
|
|
30
|
+
"""
|
|
31
|
+
Module for manipulating and creating new workflows and teach tasks.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
client (IndicoClient): instantiated Indico Client
|
|
35
|
+
"""
|
|
36
|
+
self.client = client
|
|
37
|
+
self.structure = Structure(client)
|
|
38
|
+
self._exceptions = []
|
|
39
|
+
|
|
40
|
+
def create_auto_classification_workflow(
|
|
41
|
+
self,
|
|
42
|
+
directory_path: Union[str, PathLike[str]],
|
|
43
|
+
dataset_name: str,
|
|
44
|
+
workflow_name: str,
|
|
45
|
+
teach_task_name: str,
|
|
46
|
+
accepted_types: Tuple[str, ...] = (
|
|
47
|
+
"csv",
|
|
48
|
+
"doc",
|
|
49
|
+
"docx",
|
|
50
|
+
"eml",
|
|
51
|
+
"jpeg",
|
|
52
|
+
"jpg",
|
|
53
|
+
"msg",
|
|
54
|
+
"pdf",
|
|
55
|
+
"png",
|
|
56
|
+
"pptx",
|
|
57
|
+
"rtf",
|
|
58
|
+
"svg",
|
|
59
|
+
"tif",
|
|
60
|
+
"tiff",
|
|
61
|
+
"txt",
|
|
62
|
+
"xls",
|
|
63
|
+
"xlsx",
|
|
64
|
+
),
|
|
65
|
+
) -> Workflow:
|
|
66
|
+
"""
|
|
67
|
+
Label and train a model based on a directory structure or existing teach task.
|
|
68
|
+
You should have a base directory containing sub directories where each
|
|
69
|
+
directory contains a unique file type and only that file type.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
base_directory/
|
|
73
|
+
base_directory/invoices/ -> folder containing only invoices
|
|
74
|
+
base_directory/disclosures/ -> folder containing only disclosures
|
|
75
|
+
etc. etc.
|
|
76
|
+
Args:
|
|
77
|
+
directory_path (str): Path to a directory containing your filepath structure
|
|
78
|
+
dataset_name (str): Name of created dataset
|
|
79
|
+
worlflow_name (str): Name of created workflow
|
|
80
|
+
teach_task_name (str): Name of created teach task
|
|
81
|
+
accepted_types (Tuple[str], optional): List of accepted file types to search
|
|
82
|
+
Returns:
|
|
83
|
+
Workflow: a Workflow object representation of the newly created workflow
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def valid_file(file: Path) -> bool:
|
|
87
|
+
return (
|
|
88
|
+
file.is_file() and file.suffix.strip(".").casefold() in accepted_types
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
folder = Path(directory_path)
|
|
92
|
+
files = list(filter(valid_file, folder.glob("*/*")))
|
|
93
|
+
classes = list(set(file.parent.name for file in files))
|
|
94
|
+
labeled_files = {file.name: [{"label": file.parent.name}] for file in files}
|
|
95
|
+
|
|
96
|
+
if len(classes) < 2:
|
|
97
|
+
raise ToolkitPopulationError(
|
|
98
|
+
"You must have documents in at least 2 directories, "
|
|
99
|
+
f"you only have {len(classes)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Upload files to a new dataset.
|
|
103
|
+
dataset = self.structure.create_dataset(
|
|
104
|
+
dataset_name=dataset_name,
|
|
105
|
+
files_to_upload=files,
|
|
106
|
+
read_api=True,
|
|
107
|
+
single_column=False,
|
|
108
|
+
auto_rotate=False,
|
|
109
|
+
upscale_images=True,
|
|
110
|
+
languages=["ENG"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Create a new workflow with classification model.
|
|
114
|
+
workflow = self.structure.create_workflow(workflow_name, dataset.id)
|
|
115
|
+
workflow = self.structure.add_teach_task(
|
|
116
|
+
task_name=teach_task_name,
|
|
117
|
+
labelset_name=f"{teach_task_name}_labelset",
|
|
118
|
+
target_names=classes,
|
|
119
|
+
dataset_id=dataset.id,
|
|
120
|
+
workflow_id=workflow.id,
|
|
121
|
+
model_type="classification",
|
|
122
|
+
)
|
|
123
|
+
teach_task_id = workflow.components[-1].model_group.questionnaire_id
|
|
124
|
+
labelset_id, model_group_id, label_map = self._get_teach_task_details(
|
|
125
|
+
teach_task_id
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
labels = self.get_labels_by_filename(model_group_id, labeled_files, label_map)
|
|
129
|
+
self.structure.label_teach_task(
|
|
130
|
+
label_set_id=labelset_id,
|
|
131
|
+
labels=list(map(dataclasses.asdict, labels)),
|
|
132
|
+
model_group_id=model_group_id,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return workflow
|
|
136
|
+
|
|
137
|
+
def copy_teach_task(
|
|
138
|
+
self,
|
|
139
|
+
dataset_id: int,
|
|
140
|
+
teach_task_id: int,
|
|
141
|
+
workflow_name: str,
|
|
142
|
+
data_column: str = "document",
|
|
143
|
+
rename_labels: Dict[str, str] = None,
|
|
144
|
+
remove_labels: List[str] = None,
|
|
145
|
+
) -> Workflow:
|
|
146
|
+
"""
|
|
147
|
+
Create duplicate teach task in same Indico platform.
|
|
148
|
+
|
|
149
|
+
Note: Does not work with datasets created with a snapshot
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
dataset_id (int): The dataset id of the dataset you wish to copy
|
|
153
|
+
teach_task_id (int): The teach task id of the corresponding teach task to
|
|
154
|
+
the dataset
|
|
155
|
+
workflow_name (string): The name of the newly created workflow
|
|
156
|
+
data_column_id (str, optional): The datacolumn id of the corresponding
|
|
157
|
+
dataset. Defaults to 'document'
|
|
158
|
+
rename_labels (dict, optional): Dictionary in format
|
|
159
|
+
{old_label_name : new_label_name}
|
|
160
|
+
remove_labels (list, optional): List of labels to remove from old teach task
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Workflow: a Workflow object representation of the newly created workflow
|
|
164
|
+
"""
|
|
165
|
+
dataset = self.client.call(GetDataset(dataset_id))
|
|
166
|
+
(
|
|
167
|
+
old_labelset_id,
|
|
168
|
+
old_model_group_id,
|
|
169
|
+
old_target_name_map,
|
|
170
|
+
) = self._get_teach_task_details(teach_task_id=teach_task_id)
|
|
171
|
+
# get dataset snapshot
|
|
172
|
+
export = self.client.call(
|
|
173
|
+
CreateExport(dataset_id=dataset.id, labelset_id=old_labelset_id, wait=True)
|
|
174
|
+
)
|
|
175
|
+
csv = self.client.call(DownloadExport(export.id))
|
|
176
|
+
print("Obtained snapshot")
|
|
177
|
+
|
|
178
|
+
# create workflow
|
|
179
|
+
workflow = self.structure.create_workflow(
|
|
180
|
+
name=workflow_name, dataset_id=dataset_id
|
|
181
|
+
)
|
|
182
|
+
time.sleep(2)
|
|
183
|
+
print("Created workflow")
|
|
184
|
+
old_model_group = self.client.call(
|
|
185
|
+
GetModelGroup(id=old_model_group_id, wait=True)
|
|
186
|
+
)
|
|
187
|
+
model_type = old_model_group.task_type.lower()
|
|
188
|
+
# Create new teach task
|
|
189
|
+
workflow = self.structure.add_teach_task(
|
|
190
|
+
task_name=workflow_name,
|
|
191
|
+
labelset_name=workflow_name,
|
|
192
|
+
target_names=list(old_target_name_map.keys()),
|
|
193
|
+
dataset_id=dataset.id,
|
|
194
|
+
workflow_id=workflow.id,
|
|
195
|
+
model_type=model_type,
|
|
196
|
+
data_column=data_column,
|
|
197
|
+
)
|
|
198
|
+
(
|
|
199
|
+
new_labelset_id,
|
|
200
|
+
new_model_group_id,
|
|
201
|
+
new_target_name_map,
|
|
202
|
+
) = self._get_teach_task_details(
|
|
203
|
+
workflow.components[-1].model_group.questionnaire_id
|
|
204
|
+
)
|
|
205
|
+
# Get file_to_targets from export CSV
|
|
206
|
+
file_to_targets = {}
|
|
207
|
+
for _, row in csv.iterrows():
|
|
208
|
+
# Check for NaN filled rows
|
|
209
|
+
if isinstance(row[2], float):
|
|
210
|
+
continue
|
|
211
|
+
old_example_id = row[0]
|
|
212
|
+
old_examples = self._get_example_list(old_model_group_id)
|
|
213
|
+
targets_list = loads(row[2])["targets"]
|
|
214
|
+
file_to_targets[old_examples.get_example(old_example_id).data_file_name] = (
|
|
215
|
+
targets_list
|
|
216
|
+
)
|
|
217
|
+
labels = self.get_labels_by_filename(
|
|
218
|
+
new_model_group_id,
|
|
219
|
+
file_to_targets,
|
|
220
|
+
new_target_name_map,
|
|
221
|
+
rename_labels,
|
|
222
|
+
remove_labels,
|
|
223
|
+
)
|
|
224
|
+
# Label new teach task
|
|
225
|
+
result = self.structure.label_teach_task(
|
|
226
|
+
label_set_id=new_labelset_id,
|
|
227
|
+
labels=[dataclasses.asdict(label) for label in labels],
|
|
228
|
+
model_group_id=new_model_group_id,
|
|
229
|
+
)
|
|
230
|
+
if not result["submitLabelsV2"]["success"]:
|
|
231
|
+
raise ToolkitPopulationError("Error: Failed to submit labels")
|
|
232
|
+
return workflow
|
|
233
|
+
|
|
234
|
+
def inject_labels_into_teach_task(
|
|
235
|
+
self,
|
|
236
|
+
workflow_id: int,
|
|
237
|
+
teach_task_id: int,
|
|
238
|
+
file_to_targets: dict,
|
|
239
|
+
rename_labels: Dict[str, str] = None,
|
|
240
|
+
remove_labels: List[str] = None,
|
|
241
|
+
):
|
|
242
|
+
"""
|
|
243
|
+
Add label data into existing teach task
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
workflow_id (int): Id of the workflow you wish to add labels to
|
|
247
|
+
teach_task_id (int): Id of the corresponding teach task to the workflow
|
|
248
|
+
file_to_targets (dict): mapping of filenames to target label data
|
|
249
|
+
rename_labels (dict, optional): Dictionary in format
|
|
250
|
+
{old_label_name : new_label_name}
|
|
251
|
+
remove_labels (list, optional): List of labels to remove from old teach task
|
|
252
|
+
"""
|
|
253
|
+
(
|
|
254
|
+
labelset_id,
|
|
255
|
+
model_group_id,
|
|
256
|
+
target_name_map,
|
|
257
|
+
) = self._get_teach_task_details(teach_task_id)
|
|
258
|
+
labels = self.get_labels_by_filename(
|
|
259
|
+
model_group_id,
|
|
260
|
+
file_to_targets,
|
|
261
|
+
target_name_map,
|
|
262
|
+
rename_labels,
|
|
263
|
+
remove_labels,
|
|
264
|
+
)
|
|
265
|
+
# Label new teach task
|
|
266
|
+
result = self.structure.label_teach_task(
|
|
267
|
+
label_set_id=labelset_id,
|
|
268
|
+
labels=[dataclasses.asdict(label) for label in labels],
|
|
269
|
+
model_group_id=model_group_id,
|
|
270
|
+
)
|
|
271
|
+
if not result["submitLabelsV2"]["success"]:
|
|
272
|
+
raise ToolkitPopulationError("Error: Failed to submit labels")
|
|
273
|
+
|
|
274
|
+
def get_labels_by_filename(
|
|
275
|
+
self,
|
|
276
|
+
model_group_id: int,
|
|
277
|
+
file_to_targets: dict,
|
|
278
|
+
target_name_map: dict,
|
|
279
|
+
rename_labels: Dict[str, str] = None,
|
|
280
|
+
remove_labels: List[str] = None,
|
|
281
|
+
) -> List[LabelInput]:
|
|
282
|
+
"""
|
|
283
|
+
Args:
|
|
284
|
+
model_group_id (int): ID of the model group to be labeled
|
|
285
|
+
file_to_targets (dict): mapping in the format {filename : targets_list}
|
|
286
|
+
target_name_map (dict): mapping of field name to corresponding target ID
|
|
287
|
+
rename_labels (dict, optional): Dictionary in format
|
|
288
|
+
{old_label_name : new_label_name}
|
|
289
|
+
remove_labels (list, optional): List of labels to remove from old teach task
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
A list of LabelInput to be ingested by the platform via submitLabelsV2
|
|
293
|
+
"""
|
|
294
|
+
labels = []
|
|
295
|
+
# Retrieve examples and match against filename
|
|
296
|
+
examples = self._get_example_list(model_group_id)
|
|
297
|
+
|
|
298
|
+
for filename, targets_list in file_to_targets.items():
|
|
299
|
+
if rename_labels or remove_labels:
|
|
300
|
+
targets_list = self._edit_labels(
|
|
301
|
+
targets_list, rename_labels, remove_labels
|
|
302
|
+
)
|
|
303
|
+
targets_list = self._convert_label(targets_list, target_name_map)
|
|
304
|
+
example_id = examples.get_example_id(filename)
|
|
305
|
+
if example_id:
|
|
306
|
+
labels.append(LabelInput(example_id, targets_list))
|
|
307
|
+
return labels
|
|
308
|
+
|
|
309
|
+
def _edit_labels(
|
|
310
|
+
self,
|
|
311
|
+
targets_list: List[dict],
|
|
312
|
+
rename_labels: Dict[str, str],
|
|
313
|
+
remove_labels: List[str],
|
|
314
|
+
):
|
|
315
|
+
new_targets_list = []
|
|
316
|
+
for target in targets_list:
|
|
317
|
+
if remove_labels and target["label"] not in remove_labels:
|
|
318
|
+
if rename_labels and rename_labels.get(target["label"]):
|
|
319
|
+
target["label"] = rename_labels[target["label"]]
|
|
320
|
+
new_targets_list.append(target)
|
|
321
|
+
return new_targets_list
|
|
322
|
+
|
|
323
|
+
def _convert_label(
|
|
324
|
+
self, targets_list: List[dict], target_name_map: dict
|
|
325
|
+
) -> List[LabelInst]:
|
|
326
|
+
updated_labels = []
|
|
327
|
+
for target in targets_list:
|
|
328
|
+
updated_label = LabelInst(target_name_map[target["label"]])
|
|
329
|
+
if target.get("spans"):
|
|
330
|
+
updated_spans = [
|
|
331
|
+
TokenSpanInput(span["start"], span["end"], span["page_num"])
|
|
332
|
+
for span in target["spans"]
|
|
333
|
+
]
|
|
334
|
+
updated_label.spans = updated_spans
|
|
335
|
+
updated_labels.append(updated_label)
|
|
336
|
+
return updated_labels
|
|
337
|
+
|
|
338
|
+
def _get_teach_task_details(self, teach_task_id: int):
|
|
339
|
+
teach_task_details = self.structure.get_teach_details(
|
|
340
|
+
teach_task_id=teach_task_id
|
|
341
|
+
)
|
|
342
|
+
labelset_id = teach_task_details["questionnaire"]["question"]["labelset"]["id"]
|
|
343
|
+
model_group_id = teach_task_details["questionnaire"]["question"]["modelGroupId"]
|
|
344
|
+
target_names = teach_task_details["questionnaire"]["question"]["labelset"][
|
|
345
|
+
"targetNames"
|
|
346
|
+
]
|
|
347
|
+
target_name_map = {}
|
|
348
|
+
for target in target_names:
|
|
349
|
+
target_name_map[target["name"]] = target["id"]
|
|
350
|
+
return labelset_id, model_group_id, target_name_map
|
|
351
|
+
|
|
352
|
+
def _get_example_list(self, model_group_id: int, limit=1000):
|
|
353
|
+
examples = self.structure.get_example_ids(
|
|
354
|
+
model_group_id=model_group_id, limit=limit
|
|
355
|
+
)
|
|
356
|
+
examples = ExampleList(
|
|
357
|
+
examples=[
|
|
358
|
+
Example(i["id"], i["datafile"]["name"])
|
|
359
|
+
for i in examples["modelGroup"]["pagedExamples"]["examples"]
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
return examples
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Example:
|
|
7
|
+
id: int
|
|
8
|
+
data_file_name: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExampleList:
|
|
12
|
+
def __init__(self, examples: List[Example]):
|
|
13
|
+
self.examples = examples
|
|
14
|
+
|
|
15
|
+
def get_example(self, example_id: int) -> Example:
|
|
16
|
+
"""
|
|
17
|
+
Returns example with matching example_id. If no matching example id found,
|
|
18
|
+
return None.
|
|
19
|
+
"""
|
|
20
|
+
for example in self.examples:
|
|
21
|
+
if example.id == example_id:
|
|
22
|
+
return example
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def get_example_id(self, example_data_file_name: str) -> int:
|
|
26
|
+
"""
|
|
27
|
+
Returns id for a specific example with the same name as example_data_file_name.
|
|
28
|
+
If no matching example found, return None. Assumes no duplicate filenames in
|
|
29
|
+
dataset
|
|
30
|
+
"""
|
|
31
|
+
for example in self.examples:
|
|
32
|
+
if example.data_file_name == example_data_file_name:
|
|
33
|
+
return example.id
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TokenSpanInput:
|
|
39
|
+
start: int
|
|
40
|
+
end: int
|
|
41
|
+
pageNum: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SpatialSpanInput:
|
|
46
|
+
top: int
|
|
47
|
+
bottom: int
|
|
48
|
+
left: int
|
|
49
|
+
right: int
|
|
50
|
+
pageNum: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class LabelInst:
|
|
55
|
+
clsId: int
|
|
56
|
+
spans: List[TokenSpanInput] = None
|
|
57
|
+
bounds: List[SpatialSpanInput] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class LabelInput:
|
|
62
|
+
exampleId: int
|
|
63
|
+
targets: List[LabelInst]
|
|
64
|
+
rejected: bool = None
|
|
65
|
+
override: bool = None
|
|
66
|
+
partial: bool = None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
ACCEPTED = "accepted"
|
|
5
|
+
REJECTED = "rejected"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def empty_or_matching_label(prediction: dict, labels: list) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Returns:
|
|
11
|
+
bool: if labels list is empty or prediction label matches a label in the list
|
|
12
|
+
"""
|
|
13
|
+
if len(labels) == 0 or prediction["label"] in labels:
|
|
14
|
+
return True
|
|
15
|
+
else:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def reject_by_confidence(
|
|
20
|
+
predictions: List[dict], labels: List[str] = [], conf_threshold=0.50
|
|
21
|
+
) -> List[dict]:
|
|
22
|
+
"""
|
|
23
|
+
Rejects predictions below a given confidence threshold
|
|
24
|
+
Returns:
|
|
25
|
+
predictions List[dict]: all predictions
|
|
26
|
+
"""
|
|
27
|
+
for prediction in predictions:
|
|
28
|
+
if REJECTED not in prediction:
|
|
29
|
+
if not empty_or_matching_label(prediction, labels):
|
|
30
|
+
continue
|
|
31
|
+
if prediction["confidence"][prediction["label"]] < conf_threshold:
|
|
32
|
+
prediction[REJECTED] = True
|
|
33
|
+
prediction.pop(ACCEPTED, None)
|
|
34
|
+
return predictions
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def remove_by_confidence(
|
|
38
|
+
predictions: List[dict], labels: List[str] = [], conf_threshold=0.50
|
|
39
|
+
) -> List[dict]:
|
|
40
|
+
"""
|
|
41
|
+
Removes predictions below a given confidence threshold
|
|
42
|
+
Returns:
|
|
43
|
+
updated_predictions List[dict]: predictions above conf_threshold
|
|
44
|
+
"""
|
|
45
|
+
for prediction in predictions:
|
|
46
|
+
if REJECTED not in prediction:
|
|
47
|
+
if not empty_or_matching_label(prediction, labels):
|
|
48
|
+
continue
|
|
49
|
+
if prediction["confidence"][prediction["label"]] < conf_threshold:
|
|
50
|
+
predictions.remove(prediction)
|
|
51
|
+
return predictions
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def accept_by_confidence(
|
|
55
|
+
predictions: List[dict], labels: List[str] = [], conf_threshold=0.98
|
|
56
|
+
) -> List[dict]:
|
|
57
|
+
"""
|
|
58
|
+
Accepts predictions above a given confidence threshold
|
|
59
|
+
Returns:
|
|
60
|
+
predictions List[dict]: all predictions
|
|
61
|
+
"""
|
|
62
|
+
for prediction in predictions:
|
|
63
|
+
if REJECTED not in prediction:
|
|
64
|
+
if not empty_or_matching_label(prediction, labels):
|
|
65
|
+
continue
|
|
66
|
+
if prediction["confidence"][prediction["label"]] > conf_threshold:
|
|
67
|
+
prediction[ACCEPTED] = True
|
|
68
|
+
return predictions
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def accept_by_all_match_and_confidence(
|
|
72
|
+
predictions: List[dict], labels: List[str] = [], conf_threshold=0.98
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Accepts all predictions for a class if all their values are the same,
|
|
76
|
+
and all confidence scores are above a given confidence threshold
|
|
77
|
+
Returns:
|
|
78
|
+
predictions List[dict]: all predictions
|
|
79
|
+
"""
|
|
80
|
+
pred_map = defaultdict(set)
|
|
81
|
+
for pred in predictions:
|
|
82
|
+
if REJECTED not in pred:
|
|
83
|
+
if not empty_or_matching_label(pred, labels):
|
|
84
|
+
continue
|
|
85
|
+
if pred["confidence"][pred["label"]] > conf_threshold:
|
|
86
|
+
pred_map[pred["label"]].add(pred["text"])
|
|
87
|
+
else:
|
|
88
|
+
pred_map[pred["label"]].update((1, 2))
|
|
89
|
+
for pred in predictions:
|
|
90
|
+
if pred["label"] in pred_map:
|
|
91
|
+
if len(pred_map[pred["label"]]) == 1:
|
|
92
|
+
pred[ACCEPTED] = True
|
|
93
|
+
return predictions
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def reject_by_min_character_length(
|
|
97
|
+
predictions: List[dict], labels: List[str] = [], min_length_threshold=3
|
|
98
|
+
) -> List[dict]:
|
|
99
|
+
"""
|
|
100
|
+
Rejects predictions shorter than a given minimum length
|
|
101
|
+
Returns:
|
|
102
|
+
predictions List[dict]: all predictions
|
|
103
|
+
"""
|
|
104
|
+
for prediction in predictions:
|
|
105
|
+
if empty_or_matching_label(prediction, labels):
|
|
106
|
+
if len(prediction["text"]) < min_length_threshold:
|
|
107
|
+
prediction[REJECTED] = True
|
|
108
|
+
return predictions
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def reject_by_max_character_length(
|
|
112
|
+
predictions: List[dict], labels: List[str] = [], max_length_threshold=10
|
|
113
|
+
) -> List[dict]:
|
|
114
|
+
"""
|
|
115
|
+
Rejects predictions longer than a given maximum length
|
|
116
|
+
Returns:
|
|
117
|
+
predictions List[dict]: all prediction
|
|
118
|
+
"""
|
|
119
|
+
for prediction in predictions:
|
|
120
|
+
if empty_or_matching_label(prediction, labels):
|
|
121
|
+
if len(prediction["text"]) > max_length_threshold:
|
|
122
|
+
prediction[REJECTED] = True
|
|
123
|
+
return predictions
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Callable, Dict, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AutoReviewFunction:
|
|
5
|
+
"""
|
|
6
|
+
Class for hosting functions to manipulate predictions before sending to
|
|
7
|
+
auto review
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
function (Callable): method to be invoked when applying reviews.
|
|
11
|
+
The Callable must have the following arguments in the following order:
|
|
12
|
+
predictions (List[dict]),
|
|
13
|
+
labels (List[str]),
|
|
14
|
+
**kwargs,
|
|
15
|
+
labels (List[str]): list of labels to invoke method on. Defaults to all labels
|
|
16
|
+
kwargs (Dict[str, str]): dictionary containing additional arguments needed in
|
|
17
|
+
calling function
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
function: Callable,
|
|
23
|
+
labels: List[str] = [],
|
|
24
|
+
kwargs: Dict[str, str] = {},
|
|
25
|
+
):
|
|
26
|
+
self.function = function
|
|
27
|
+
self.labels = labels
|
|
28
|
+
self.kwargs = kwargs
|
|
29
|
+
|
|
30
|
+
def apply(self, predictions: List[dict]):
|
|
31
|
+
if predictions and not self.labels:
|
|
32
|
+
self.labels = list(set([pred["label"] for pred in predictions]))
|
|
33
|
+
return self.function(predictions, self.labels, **self.kwargs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AutoReviewer:
|
|
37
|
+
"""
|
|
38
|
+
Class for programatically reviewing workflow predictions
|
|
39
|
+
|
|
40
|
+
Example Usage:
|
|
41
|
+
|
|
42
|
+
reviewer = AutoReviewer(
|
|
43
|
+
predictions, functions
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Get your updated predictions
|
|
47
|
+
updated_predictions: List[dict] = reviewer.apply_reviews()
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
predictions: List[dict],
|
|
53
|
+
functions: List[AutoReviewFunction] = [],
|
|
54
|
+
):
|
|
55
|
+
self.predictions = predictions
|
|
56
|
+
self.updated_predictions = predictions
|
|
57
|
+
self.functions = functions
|
|
58
|
+
|
|
59
|
+
def apply_reviews(self) -> list:
|
|
60
|
+
for function in self.functions:
|
|
61
|
+
self.updated_predictions = function.apply(self.updated_predictions)
|
|
62
|
+
return self.updated_predictions
|
indico_toolkit/client.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from indico import IndicoClient, IndicoConfig
|
|
4
|
+
from indico.errors import IndicoAuthenticationFailed, IndicoRequestError
|
|
5
|
+
|
|
6
|
+
from .errors import ToolkitAuthError
|
|
7
|
+
from .retry import retry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@retry(IndicoRequestError, ConnectionError)
|
|
11
|
+
def create_client(
|
|
12
|
+
host: str,
|
|
13
|
+
api_token_path: "str | None" = None,
|
|
14
|
+
api_token_string: "str | None" = None,
|
|
15
|
+
verify_ssl: bool = True,
|
|
16
|
+
**kwargs: Any,
|
|
17
|
+
) -> IndicoClient:
|
|
18
|
+
"""
|
|
19
|
+
Instantiate your Indico API client.
|
|
20
|
+
Specify either the path to your token or the token string itself.
|
|
21
|
+
"""
|
|
22
|
+
config = IndicoConfig(
|
|
23
|
+
host=host,
|
|
24
|
+
api_token_path=api_token_path,
|
|
25
|
+
api_token=api_token_string,
|
|
26
|
+
verify_ssl=verify_ssl,
|
|
27
|
+
**kwargs,
|
|
28
|
+
)
|
|
29
|
+
try:
|
|
30
|
+
return IndicoClient(config)
|
|
31
|
+
except IndicoAuthenticationFailed as e:
|
|
32
|
+
raise ToolkitAuthError(
|
|
33
|
+
f"{e}\nEnsure that you are using your most recently downloaded token with "
|
|
34
|
+
"the correct host URL"
|
|
35
|
+
)
|
indico_toolkit/errors.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class ToolkitError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ToolkitAuthError(ToolkitError):
|
|
6
|
+
def __init__(self, msg: str):
|
|
7
|
+
super().__init__(msg)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolkitStatusError(ToolkitError):
|
|
11
|
+
def __init__(self, msg: str):
|
|
12
|
+
super().__init__(msg)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ToolkitInputError(ToolkitError):
|
|
16
|
+
def __init__(self, msg: str):
|
|
17
|
+
super().__init__(msg)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ToolkitInstantiationError(ToolkitError):
|
|
21
|
+
def __init__(self, msg: str):
|
|
22
|
+
super().__init__(msg)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolkitPopulationError(ToolkitError):
|
|
26
|
+
def __init__(self, msg: str):
|
|
27
|
+
super().__init__(msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ToolkitStaggeredLoopError(ToolkitError):
|
|
31
|
+
def __init__(self, msg: str):
|
|
32
|
+
super().__init__(msg)
|