sapiopycommons 2025.9.5a726__py3-none-any.whl → 2025.9.5rc727__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.
Potentially problematic release.
This version of sapiopycommons might be problematic. Click here for more details.
- sapiopycommons/ai/tool_of_tools.py +917 -0
- sapiopycommons/callbacks/callback_util.py +26 -16
- sapiopycommons/eln/experiment_handler.py +12 -5
- sapiopycommons/files/assay_plate_reader.py +93 -0
- sapiopycommons/files/file_text_converter.py +207 -0
- sapiopycommons/flowcyto/flow_cyto.py +2 -24
- sapiopycommons/general/accession_service.py +2 -28
- sapiopycommons/multimodal/multimodal.py +2 -24
- sapiopycommons/recordmodel/record_handler.py +4 -0
- sapiopycommons/rules/eln_rule_handler.py +3 -0
- sapiopycommons/rules/on_save_rule_handler.py +3 -0
- sapiopycommons/webhook/webservice_handlers.py +1 -1
- {sapiopycommons-2025.9.5a726.dist-info → sapiopycommons-2025.9.5rc727.dist-info}/METADATA +2 -2
- {sapiopycommons-2025.9.5a726.dist-info → sapiopycommons-2025.9.5rc727.dist-info}/RECORD +16 -48
- sapiopycommons/ai/converter_service_base.py +0 -132
- sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py +0 -43
- sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi +0 -31
- sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py +0 -24
- sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py +0 -123
- sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi +0 -598
- sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py +0 -24
- sapiopycommons/ai/protoapi/plan/converter/converter_pb2.py +0 -51
- sapiopycommons/ai/protoapi/plan/converter/converter_pb2.pyi +0 -63
- sapiopycommons/ai/protoapi/plan/converter/converter_pb2_grpc.py +0 -149
- sapiopycommons/ai/protoapi/plan/item/item_container_pb2.py +0 -55
- sapiopycommons/ai/protoapi/plan/item/item_container_pb2.pyi +0 -90
- sapiopycommons/ai/protoapi/plan/item/item_container_pb2_grpc.py +0 -24
- sapiopycommons/ai/protoapi/plan/script/script_pb2.py +0 -59
- sapiopycommons/ai/protoapi/plan/script/script_pb2.pyi +0 -102
- sapiopycommons/ai/protoapi/plan/script/script_pb2_grpc.py +0 -153
- sapiopycommons/ai/protoapi/plan/step_output_pb2.py +0 -45
- sapiopycommons/ai/protoapi/plan/step_output_pb2.pyi +0 -42
- sapiopycommons/ai/protoapi/plan/step_output_pb2_grpc.py +0 -24
- sapiopycommons/ai/protoapi/plan/step_pb2.py +0 -43
- sapiopycommons/ai/protoapi/plan/step_pb2.pyi +0 -43
- sapiopycommons/ai/protoapi/plan/step_pb2_grpc.py +0 -24
- sapiopycommons/ai/protoapi/plan/tool/entry_pb2.py +0 -41
- sapiopycommons/ai/protoapi/plan/tool/entry_pb2.pyi +0 -35
- sapiopycommons/ai/protoapi/plan/tool/entry_pb2_grpc.py +0 -24
- sapiopycommons/ai/protoapi/plan/tool/tool_pb2.py +0 -75
- sapiopycommons/ai/protoapi/plan/tool/tool_pb2.pyi +0 -237
- sapiopycommons/ai/protoapi/plan/tool/tool_pb2_grpc.py +0 -154
- sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py +0 -39
- sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi +0 -32
- sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py +0 -24
- sapiopycommons/ai/protobuf_utils.py +0 -504
- sapiopycommons/ai/server.py +0 -106
- sapiopycommons/ai/test_client.py +0 -356
- sapiopycommons/ai/tool_service_base.py +0 -951
- {sapiopycommons-2025.9.5a726.dist-info → sapiopycommons-2025.9.5rc727.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.9.5a726.dist-info → sapiopycommons-2025.9.5rc727.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,951 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import io
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import re
|
|
8
|
-
import traceback
|
|
9
|
-
from abc import abstractmethod, ABC
|
|
10
|
-
from logging import Logger
|
|
11
|
-
from typing import Any, Iterable, Sequence, Mapping
|
|
12
|
-
|
|
13
|
-
from grpc import ServicerContext
|
|
14
|
-
from sapiopylib.rest.User import SapioUser, ensure_logger_initialized
|
|
15
|
-
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition
|
|
16
|
-
|
|
17
|
-
from sapiopycommons.ai.protoapi.fielddefinitions.fields_pb2 import FieldValueMapPbo, FieldValuePbo
|
|
18
|
-
from sapiopycommons.ai.protoapi.fielddefinitions.velox_field_def_pb2 import VeloxFieldDefPbo, FieldTypePbo, \
|
|
19
|
-
SelectionPropertiesPbo, IntegerPropertiesPbo, DoublePropertiesPbo, BooleanPropertiesPbo, StringPropertiesPbo
|
|
20
|
-
from sapiopycommons.ai.protoapi.plan.item.item_container_pb2 import ContentTypePbo
|
|
21
|
-
from sapiopycommons.ai.protoapi.plan.tool.entry_pb2 import StepOutputBatchPbo, StepItemContainerPbo, \
|
|
22
|
-
StepBinaryContainerPbo, StepCsvContainerPbo, StepCsvHeaderRowPbo, StepCsvRowPbo, StepJsonContainerPbo, \
|
|
23
|
-
StepTextContainerPbo, StepInputBatchPbo
|
|
24
|
-
from sapiopycommons.ai.protoapi.plan.tool.tool_pb2 import ToolDetailsRequestPbo, ToolDetailsResponsePbo, \
|
|
25
|
-
ToolDetailsPbo, ProcessStepRequestPbo, ProcessStepResponsePbo, ToolOutputDetailsPbo, ToolIoConfigBasePbo, \
|
|
26
|
-
ToolInputDetailsPbo, ExampleContainerPbo, ProcessStepResponseStatusPbo
|
|
27
|
-
from sapiopycommons.ai.protoapi.plan.tool.tool_pb2_grpc import ToolServiceServicer
|
|
28
|
-
from sapiopycommons.ai.protoapi.session.sapio_conn_info_pb2 import SapioUserSecretTypePbo, SapioConnectionInfoPbo
|
|
29
|
-
from sapiopycommons.ai.protobuf_utils import ProtobufUtils
|
|
30
|
-
from sapiopycommons.ai.test_client import ContainerType
|
|
31
|
-
from sapiopycommons.files.file_util import FileUtil
|
|
32
|
-
from sapiopycommons.general.aliases import FieldMap, FieldValue
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# FR-47422: Created classes.
|
|
36
|
-
class SapioToolResult(ABC):
|
|
37
|
-
"""
|
|
38
|
-
A class representing a result from a Sapio tool. Instantiate one of the subclasses to create a result object.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
@abstractmethod
|
|
42
|
-
def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
|
|
43
|
-
"""
|
|
44
|
-
Convert this SapioToolResult object to a StepOutputBatchPbo or list of FieldValueMapPbo proto objects.
|
|
45
|
-
"""
|
|
46
|
-
pass
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class BinaryResult(SapioToolResult):
|
|
50
|
-
"""
|
|
51
|
-
A class representing binary results from a Sapio tool.
|
|
52
|
-
"""
|
|
53
|
-
binary_data: list[bytes]
|
|
54
|
-
content_type: str
|
|
55
|
-
file_extensions: list[str]
|
|
56
|
-
name: str
|
|
57
|
-
|
|
58
|
-
def __init__(self, binary_data: list[bytes], content_type: str = "binary", file_extensions: list[str] = None,
|
|
59
|
-
name: str | None = None):
|
|
60
|
-
"""
|
|
61
|
-
:param binary_data: The binary data as a list of bytes.
|
|
62
|
-
:param content_type: The content type of the data.
|
|
63
|
-
:param file_extensions: A list of file extensions that this binary data can be saved as.
|
|
64
|
-
:param name: An optional identifying name for this result that will be accessible to the next tool.
|
|
65
|
-
"""
|
|
66
|
-
self.binary_data = binary_data
|
|
67
|
-
self.content_type = content_type
|
|
68
|
-
self.file_extensions = file_extensions if file_extensions else []
|
|
69
|
-
self.name = name
|
|
70
|
-
|
|
71
|
-
def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
|
|
72
|
-
return StepOutputBatchPbo(
|
|
73
|
-
item_container=StepItemContainerPbo(
|
|
74
|
-
content_type=ContentTypePbo(name=self.content_type, extensions=self.file_extensions),
|
|
75
|
-
container_name=self.name,
|
|
76
|
-
binary_container=StepBinaryContainerPbo(items=self.binary_data)
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class CsvResult(SapioToolResult):
|
|
82
|
-
"""
|
|
83
|
-
A class representing CSV results from a Sapio tool.
|
|
84
|
-
"""
|
|
85
|
-
csv_data: list[dict[str, Any]]
|
|
86
|
-
content_type: str
|
|
87
|
-
file_extensions: list[str]
|
|
88
|
-
name: str
|
|
89
|
-
|
|
90
|
-
def __init__(self, csv_data: list[dict[str, Any]], content_type: str = "csv", file_extensions: list[str] = None,
|
|
91
|
-
name: str | None = None):
|
|
92
|
-
"""
|
|
93
|
-
:param csv_data: The list of CSV data results, provided as a list of dictionaries of column name to value.
|
|
94
|
-
:param content_type: The content type of the data.
|
|
95
|
-
:param file_extensions: A list of file extensions that this binary data can be saved as.
|
|
96
|
-
:param name: An optional identifying name for this result that will be accessible to the next tool.
|
|
97
|
-
"""
|
|
98
|
-
self.csv_data = csv_data
|
|
99
|
-
self.content_type = content_type
|
|
100
|
-
self.file_extensions = file_extensions if file_extensions else ["csv"]
|
|
101
|
-
self.name = name
|
|
102
|
-
|
|
103
|
-
def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
|
|
104
|
-
return StepOutputBatchPbo(
|
|
105
|
-
item_container=StepItemContainerPbo(
|
|
106
|
-
content_type=ContentTypePbo(name=self.content_type, extensions=self.file_extensions),
|
|
107
|
-
container_name=self.name,
|
|
108
|
-
csv_container=StepCsvContainerPbo(
|
|
109
|
-
header=StepCsvHeaderRowPbo(cells=self.csv_data[0].keys()),
|
|
110
|
-
items=[StepCsvRowPbo(cells=[str(x) for x in row.values()]) for row in self.csv_data]
|
|
111
|
-
)
|
|
112
|
-
) if self.csv_data else None
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class FieldMapResult(SapioToolResult):
|
|
117
|
-
"""
|
|
118
|
-
A class representing field map results from a Sapio tool.
|
|
119
|
-
"""
|
|
120
|
-
field_maps: list[FieldMap]
|
|
121
|
-
|
|
122
|
-
def __init__(self, field_maps: list[FieldMap]):
|
|
123
|
-
"""
|
|
124
|
-
:param field_maps: A list of field maps, where each map is a dictionary of field names to values. Each entry
|
|
125
|
-
will create a new data record in the system, so long as the tool definition specifies an output data type
|
|
126
|
-
name.
|
|
127
|
-
"""
|
|
128
|
-
self.field_maps = field_maps
|
|
129
|
-
|
|
130
|
-
def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
|
|
131
|
-
new_records: list[FieldValueMapPbo] = []
|
|
132
|
-
for field_map in self.field_maps:
|
|
133
|
-
fields: dict[str, FieldValuePbo] = {}
|
|
134
|
-
for field, value in field_map.items():
|
|
135
|
-
field_value = FieldValuePbo()
|
|
136
|
-
if isinstance(value, str):
|
|
137
|
-
field_value.string_value = value
|
|
138
|
-
elif isinstance(value, int):
|
|
139
|
-
field_value.int_value = value
|
|
140
|
-
elif isinstance(value, float):
|
|
141
|
-
field_value.double_value = value
|
|
142
|
-
elif isinstance(value, bool):
|
|
143
|
-
field_value.bool_value = value
|
|
144
|
-
fields[field] = field_value
|
|
145
|
-
new_records.append(FieldValueMapPbo(fields=fields))
|
|
146
|
-
return new_records
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class JsonResult(SapioToolResult):
|
|
150
|
-
"""
|
|
151
|
-
A class representing JSON results from a Sapio tool.
|
|
152
|
-
"""
|
|
153
|
-
json_data: list[Any]
|
|
154
|
-
content_type: str
|
|
155
|
-
file_extensions: list[str]
|
|
156
|
-
name: str
|
|
157
|
-
|
|
158
|
-
def __init__(self, json_data: list[Any], content_type: str = "json", file_extensions: list[str] = None,
|
|
159
|
-
name: str | None = None):
|
|
160
|
-
"""
|
|
161
|
-
:param json_data: The list of JSON data results. Each entry in the list represents a separate JSON object.
|
|
162
|
-
These entries must be able to be serialized to JSON using json.dumps().
|
|
163
|
-
:param content_type: The content type of the data.
|
|
164
|
-
:param file_extensions: A list of file extensions that this binary data can be saved as.
|
|
165
|
-
:param name: An optional identifying name for this result that will be accessible to the next tool.
|
|
166
|
-
"""
|
|
167
|
-
self.json_data = json_data
|
|
168
|
-
self.content_type = content_type
|
|
169
|
-
self.file_extensions = file_extensions if file_extensions else ["json"]
|
|
170
|
-
self.name = name
|
|
171
|
-
|
|
172
|
-
def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
|
|
173
|
-
return StepOutputBatchPbo(
|
|
174
|
-
item_container=StepItemContainerPbo(
|
|
175
|
-
content_type=ContentTypePbo(name=self.content_type, extensions=self.file_extensions),
|
|
176
|
-
container_name=self.name,
|
|
177
|
-
json_container=StepJsonContainerPbo(items=[json.dumps(x) for x in self.json_data])
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class TextResult(SapioToolResult):
|
|
183
|
-
"""
|
|
184
|
-
A class representing text results from a Sapio tool.
|
|
185
|
-
"""
|
|
186
|
-
text_data: list[str]
|
|
187
|
-
content_type: str
|
|
188
|
-
file_extensions: list[str]
|
|
189
|
-
name: str
|
|
190
|
-
|
|
191
|
-
def __init__(self, text_data: list[str], content_type: str = "text", file_extensions: list[str] = None,
|
|
192
|
-
name: str | None = None):
|
|
193
|
-
"""
|
|
194
|
-
:param text_data: The text data as a list of strings.
|
|
195
|
-
:param content_type: The content type of the data.
|
|
196
|
-
:param file_extensions: A list of file extensions that this binary data can be saved as.
|
|
197
|
-
:param name: An optional identifying name for this result that will be accessible to the next tool.
|
|
198
|
-
"""
|
|
199
|
-
self.text_data = text_data
|
|
200
|
-
self.content_type = content_type
|
|
201
|
-
self.file_extensions = file_extensions if file_extensions else ["txt"]
|
|
202
|
-
self.name = name
|
|
203
|
-
|
|
204
|
-
def to_proto(self) -> StepOutputBatchPbo | list[FieldValueMapPbo]:
|
|
205
|
-
return StepOutputBatchPbo(
|
|
206
|
-
item_container=StepItemContainerPbo(
|
|
207
|
-
content_type=ContentTypePbo(name=self.content_type, extensions=self.file_extensions),
|
|
208
|
-
container_name=self.name,
|
|
209
|
-
text_container=StepTextContainerPbo(items=self.text_data)
|
|
210
|
-
)
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
class ToolServiceBase(ToolServiceServicer, ABC):
|
|
215
|
-
"""
|
|
216
|
-
A base class for implementing a tool service. Subclasses should implement the register_tools method to register
|
|
217
|
-
their tools with the service.
|
|
218
|
-
"""
|
|
219
|
-
def GetToolDetails(self, request: ToolDetailsRequestPbo, context: ServicerContext) -> ToolDetailsResponsePbo:
|
|
220
|
-
try:
|
|
221
|
-
# Get the tool details from the registered tools.
|
|
222
|
-
details: list[ToolDetailsPbo] = []
|
|
223
|
-
for tool in self.register_tools():
|
|
224
|
-
details.append(tool().to_pbo())
|
|
225
|
-
if not details:
|
|
226
|
-
raise Exception("No tools registered with this service.")
|
|
227
|
-
return ToolDetailsResponsePbo(tool_framework_version=self.tool_version(), tool_details=details)
|
|
228
|
-
except Exception as e:
|
|
229
|
-
# Woe to you if you somehow cause an exception to be raised when just initializing your tools.
|
|
230
|
-
# There's no way to log this.
|
|
231
|
-
print(f"CRITICAL ERROR: {e}")
|
|
232
|
-
print(traceback.format_exc())
|
|
233
|
-
return ToolDetailsResponsePbo()
|
|
234
|
-
|
|
235
|
-
def ProcessData(self, request: ProcessStepRequestPbo, context: ServicerContext) -> ProcessStepResponsePbo:
|
|
236
|
-
try:
|
|
237
|
-
# Convert the SapioConnectionInfo proto object to a SapioUser object.
|
|
238
|
-
user = self._create_user(request.sapio_user)
|
|
239
|
-
# Get the tool results from the registered tool matching the request.
|
|
240
|
-
success, msg, results, logs = self.run(user, request, context)
|
|
241
|
-
# Convert the results to protobuf objects.
|
|
242
|
-
output_data: list[StepOutputBatchPbo] = []
|
|
243
|
-
new_records: list[FieldValueMapPbo] = []
|
|
244
|
-
for result in results:
|
|
245
|
-
data: StepOutputBatchPbo | list[FieldValueMapPbo] = result.to_proto()
|
|
246
|
-
if isinstance(data, StepOutputBatchPbo):
|
|
247
|
-
output_data.append(data)
|
|
248
|
-
else:
|
|
249
|
-
new_records.extend(data)
|
|
250
|
-
# Return a ProcessStepResponse proto object containing the results to the caller.
|
|
251
|
-
status = ProcessStepResponseStatusPbo.SUCCESS if success else ProcessStepResponseStatusPbo.FAILURE
|
|
252
|
-
return ProcessStepResponsePbo(status=status, status_message=msg, output=output_data, log=logs,
|
|
253
|
-
new_records=new_records)
|
|
254
|
-
except Exception as e:
|
|
255
|
-
# This try/except should never be needed, as the tool should handle its own exceptions, but better safe
|
|
256
|
-
# than sorry.
|
|
257
|
-
print(f"CRITICAL ERROR: {e}")
|
|
258
|
-
print(traceback.format_exc())
|
|
259
|
-
return ProcessStepResponsePbo(status=ProcessStepResponseStatusPbo.FAILURE,
|
|
260
|
-
status_message=f"CRITICAL ERROR: {e}",
|
|
261
|
-
log=[traceback.format_exc()])
|
|
262
|
-
|
|
263
|
-
@staticmethod
|
|
264
|
-
def _create_user(info: SapioConnectionInfoPbo, timeout_seconds: int = 60) -> SapioUser:
|
|
265
|
-
"""
|
|
266
|
-
Create a SapioUser object from the given SapioConnectionInfo proto object.
|
|
267
|
-
|
|
268
|
-
:param info: The SapioConnectionInfo proto object.
|
|
269
|
-
:param timeout_seconds: The request timeout for calls made from this user object.
|
|
270
|
-
"""
|
|
271
|
-
user = SapioUser(info.webservice_url, True, timeout_seconds, guid=info.app_guid)
|
|
272
|
-
match info.secret_type:
|
|
273
|
-
case SapioUserSecretTypePbo.SESSION_TOKEN:
|
|
274
|
-
user.api_token = info.secret
|
|
275
|
-
case SapioUserSecretTypePbo.PASSWORD:
|
|
276
|
-
secret: str = info.secret
|
|
277
|
-
if secret.startswith("Basic "):
|
|
278
|
-
secret = secret[6:]
|
|
279
|
-
credentials: list[str] = base64.b64decode(secret).decode().split(":", 1)
|
|
280
|
-
user.username = credentials[0]
|
|
281
|
-
user.password = credentials[1]
|
|
282
|
-
case _:
|
|
283
|
-
raise Exception(f"Unexpected secret type: {info.secret_type}")
|
|
284
|
-
return user
|
|
285
|
-
|
|
286
|
-
@staticmethod
|
|
287
|
-
def tool_version() -> int:
|
|
288
|
-
"""
|
|
289
|
-
:return: The version of this set of tools.
|
|
290
|
-
"""
|
|
291
|
-
return 1
|
|
292
|
-
|
|
293
|
-
@abstractmethod
|
|
294
|
-
def register_tools(self) -> list[type[ToolBase]]:
|
|
295
|
-
"""
|
|
296
|
-
Register tool types with this service. Provided tools should implement the ToolBase class.
|
|
297
|
-
|
|
298
|
-
:return: A list of tools to register to this service.
|
|
299
|
-
"""
|
|
300
|
-
pass
|
|
301
|
-
|
|
302
|
-
def run(self, user: SapioUser, request: ProcessStepRequestPbo, context: ServicerContext) \
|
|
303
|
-
-> tuple[bool, str, list[SapioToolResult], list[str]]:
|
|
304
|
-
"""
|
|
305
|
-
Execute a tool from this service.
|
|
306
|
-
|
|
307
|
-
:param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
|
|
308
|
-
system.
|
|
309
|
-
:param request: The request object containing the input data.
|
|
310
|
-
:param context: The gRPC context.
|
|
311
|
-
:return: Whether or not the tool succeeded, the status message, the results of the tool, and any logs
|
|
312
|
-
generated by the tool.
|
|
313
|
-
"""
|
|
314
|
-
# Locate the tool named in the request.
|
|
315
|
-
find_tool: str = request.tool_name
|
|
316
|
-
registered_tools: dict[str, type[ToolBase]] = {t.name(): t for t in self.register_tools()}
|
|
317
|
-
if find_tool not in registered_tools:
|
|
318
|
-
# If the tool is not found, list all of the registered tools for this service so that the LLM can correct
|
|
319
|
-
# the tool it is requesting.
|
|
320
|
-
all_tool_names: str = "\n".join(registered_tools.keys())
|
|
321
|
-
msg: str = (f"Tool \"{find_tool}\" not found in the registered tools for this service. The registered tools "
|
|
322
|
-
f"for this service are: \n{all_tool_names}")
|
|
323
|
-
return False, msg, [], []
|
|
324
|
-
|
|
325
|
-
# Instantiate the tool class.
|
|
326
|
-
tool: ToolBase = registered_tools[find_tool]()
|
|
327
|
-
try:
|
|
328
|
-
# Setup the tool with details from the request.
|
|
329
|
-
tool.setup(user, request, context)
|
|
330
|
-
# Validate that the provided inputs match the tool's expected inputs.
|
|
331
|
-
if len(request.input) != len(tool.input_configs):
|
|
332
|
-
msg: str = f"Expected {len(tool.input_configs)} inputs for this tool, but got {len(request.input)} instead."
|
|
333
|
-
else:
|
|
334
|
-
msg: str = tool.validate_input()
|
|
335
|
-
# If there is no error message, then the inputs are valid.
|
|
336
|
-
success: bool = not bool(msg)
|
|
337
|
-
# If this is a dry run, then provide the fixed dry run output.
|
|
338
|
-
# Otherwise, if the inputs were successfully validated, then the tool is executed normally.
|
|
339
|
-
results: list[SapioToolResult] = []
|
|
340
|
-
if request.dry_run:
|
|
341
|
-
results = tool.dry_run_output()
|
|
342
|
-
elif success:
|
|
343
|
-
results = tool.run(user)
|
|
344
|
-
# Update the status message to reflect the successful execution of the tool.
|
|
345
|
-
msg = f"{tool.name()} successfully completed."
|
|
346
|
-
return success, msg, results, tool.logs
|
|
347
|
-
except Exception as e:
|
|
348
|
-
tool.log_exception("Exception occurred during tool execution.", e)
|
|
349
|
-
return False, str(e), [], tool.logs
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
class ToolBase(ABC):
|
|
353
|
-
"""
|
|
354
|
-
A base class for implementing a tool.
|
|
355
|
-
"""
|
|
356
|
-
_name: str
|
|
357
|
-
_description: str
|
|
358
|
-
_data_type_name: str | None
|
|
359
|
-
input_configs: list[ToolInputDetailsPbo]
|
|
360
|
-
_input_container_types: list[ContainerType]
|
|
361
|
-
output_configs: list[ToolOutputDetailsPbo]
|
|
362
|
-
_output_container_types: list[ContainerType]
|
|
363
|
-
config_fields: list[VeloxFieldDefPbo]
|
|
364
|
-
|
|
365
|
-
logs: list[str]
|
|
366
|
-
logger: Logger
|
|
367
|
-
verbose_logging: bool
|
|
368
|
-
|
|
369
|
-
user: SapioUser
|
|
370
|
-
request: ProcessStepRequestPbo
|
|
371
|
-
context: ServicerContext
|
|
372
|
-
|
|
373
|
-
@staticmethod
|
|
374
|
-
@abstractmethod
|
|
375
|
-
def name() -> str:
|
|
376
|
-
"""
|
|
377
|
-
:return: The name of the tool. This should be unique across all tools in the service.
|
|
378
|
-
"""
|
|
379
|
-
pass
|
|
380
|
-
|
|
381
|
-
@staticmethod
|
|
382
|
-
@abstractmethod
|
|
383
|
-
def description() -> str:
|
|
384
|
-
"""
|
|
385
|
-
:return: A description of the tool.
|
|
386
|
-
"""
|
|
387
|
-
pass
|
|
388
|
-
|
|
389
|
-
@staticmethod
|
|
390
|
-
def data_type_name() -> str | None:
|
|
391
|
-
"""
|
|
392
|
-
:return: The name of the output data type of this tool, if applicable. When this tool returns
|
|
393
|
-
FieldMapResult objects in its run method, this name will be used to set the data type of the output data.
|
|
394
|
-
"""
|
|
395
|
-
return None
|
|
396
|
-
|
|
397
|
-
def __init__(self):
|
|
398
|
-
self._name = self.name()
|
|
399
|
-
self._description = self.description()
|
|
400
|
-
self._data_type_name = self.data_type_name()
|
|
401
|
-
self.input_configs = []
|
|
402
|
-
self._input_container_types = []
|
|
403
|
-
self.output_configs = []
|
|
404
|
-
self._output_container_types = []
|
|
405
|
-
self.config_fields = []
|
|
406
|
-
self.logs = []
|
|
407
|
-
self.logger = logging.getLogger(f"ToolBase.{self._name}")
|
|
408
|
-
ensure_logger_initialized(self.logger)
|
|
409
|
-
|
|
410
|
-
def setup(self, user: SapioUser, request: ProcessStepRequestPbo, context: ServicerContext) -> None:
|
|
411
|
-
"""
|
|
412
|
-
Setup the tool with the user, request, and context. This method can be overridden by subclasses to perform
|
|
413
|
-
additional setup.
|
|
414
|
-
|
|
415
|
-
:param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
|
|
416
|
-
system.
|
|
417
|
-
:param request: The request object containing the input data.
|
|
418
|
-
:param context: The gRPC context.
|
|
419
|
-
"""
|
|
420
|
-
self.user = user
|
|
421
|
-
self.request = request
|
|
422
|
-
self.context = context
|
|
423
|
-
self.verbose_logging = request.verbose_logging
|
|
424
|
-
|
|
425
|
-
def add_input(self, container_type: ContainerType, content_type: str, display_name: str, description: str,
|
|
426
|
-
structure_example: str | bytes | None = None, validation: str | None = None,
|
|
427
|
-
input_count: tuple[int, int] | None = None, is_paged: bool = False,
|
|
428
|
-
page_size: tuple[int, int] | None = None, max_request_bytes: int | None = None) -> None:
|
|
429
|
-
"""
|
|
430
|
-
Add an input configuration to the tool. This determines how many inputs this tool will accept in the plan
|
|
431
|
-
manager, as well as what those inputs are. The IO number of the input will be set to the current number of
|
|
432
|
-
inputs. That is, the first time this is called, the IO number will be 0, the second time it is called, the IO
|
|
433
|
-
number will be 1, and so on.
|
|
434
|
-
|
|
435
|
-
:param container_type: The container type of the input.
|
|
436
|
-
:param content_type: The content type of the input.
|
|
437
|
-
:param display_name: The display name of the input.
|
|
438
|
-
:param description: The description of the input.
|
|
439
|
-
:param structure_example: An optional example of the structure of the input, such as how the structure of a
|
|
440
|
-
JSON output may look. This does not need to be an entirely valid example, and should often be truncated for
|
|
441
|
-
brevity.
|
|
442
|
-
:param validation: An optional validation string for the input.
|
|
443
|
-
:param input_count: A tuple of the minimum and maximum number of inputs allowed for this tool.
|
|
444
|
-
:param is_paged: If true, this input will be paged. If false, this input will not be paged.
|
|
445
|
-
:param page_size: A tuple of the minimum and maximum page size for this tool. The input must be paged in order
|
|
446
|
-
for this to have an effect.
|
|
447
|
-
:param max_request_bytes: The maximum request size in bytes for this tool.
|
|
448
|
-
"""
|
|
449
|
-
structure: ExampleContainerPbo | None = None
|
|
450
|
-
if isinstance(structure_example, str):
|
|
451
|
-
structure = ExampleContainerPbo(text_example=structure_example)
|
|
452
|
-
elif isinstance(structure_example, bytes):
|
|
453
|
-
structure = ExampleContainerPbo(binary_example=structure_example)
|
|
454
|
-
self.input_configs.append(ToolInputDetailsPbo(
|
|
455
|
-
base_config=ToolIoConfigBasePbo(
|
|
456
|
-
io_number=len(self.input_configs),
|
|
457
|
-
content_type=content_type,
|
|
458
|
-
display_name=display_name,
|
|
459
|
-
description=description,
|
|
460
|
-
structure_example=structure
|
|
461
|
-
),
|
|
462
|
-
validation=validation,
|
|
463
|
-
min_input_count=input_count[0] if input_count else None,
|
|
464
|
-
max_input_count=input_count[1] if input_count else None,
|
|
465
|
-
paged=is_paged,
|
|
466
|
-
min_page_size=page_size[0] if page_size else None,
|
|
467
|
-
max_page_size=page_size[1] if page_size else None,
|
|
468
|
-
max_request_bytes=max_request_bytes,
|
|
469
|
-
))
|
|
470
|
-
self._input_container_types.append(container_type)
|
|
471
|
-
|
|
472
|
-
def add_output(self, container_type: ContainerType, content_type: str, display_name: str, description: str,
|
|
473
|
-
testing_example: str | bytes, structure_example: str | bytes | None = None) -> None:
|
|
474
|
-
"""
|
|
475
|
-
Add an output configuration to the tool. This determines how many inputs this tool will accept in the plan
|
|
476
|
-
manager, as well as what those inputs are. The IO number of the output will be set to the current number of
|
|
477
|
-
outputs. That is, the first time this is called, the IO number will be 0, the second time it is called, the IO
|
|
478
|
-
number will be 1, and so on.
|
|
479
|
-
|
|
480
|
-
:param container_type: The container type of the output.
|
|
481
|
-
:param content_type: The content type of the output.
|
|
482
|
-
:param display_name: The display name of the output.
|
|
483
|
-
:param description: The description of the output.
|
|
484
|
-
:param testing_example: An example of the input to be used when testing this tool in the system. This must be
|
|
485
|
-
an entirely valid example of what an output of this tool could look like so that it can be properly used
|
|
486
|
-
to run tests with. The provided example may be a string, such as for representing JSON or CSV outputs,
|
|
487
|
-
or bytes, such as for representing binary outputs like images or files.
|
|
488
|
-
:param structure_example: An optional example of the structure of the input, such as how the structure of a
|
|
489
|
-
JSON output may look. This does not need to be an entirely valid example, and should often be truncated for
|
|
490
|
-
brevity.
|
|
491
|
-
"""
|
|
492
|
-
testing: ExampleContainerPbo | None = None
|
|
493
|
-
if isinstance(testing_example, str):
|
|
494
|
-
testing = ExampleContainerPbo(text_example=testing_example)
|
|
495
|
-
elif isinstance(testing_example, bytes):
|
|
496
|
-
testing = ExampleContainerPbo(binary_example=testing_example)
|
|
497
|
-
|
|
498
|
-
structure: ExampleContainerPbo | None = None
|
|
499
|
-
if isinstance(structure_example, str):
|
|
500
|
-
structure = ExampleContainerPbo(text_example=structure_example)
|
|
501
|
-
elif isinstance(structure_example, bytes):
|
|
502
|
-
structure = ExampleContainerPbo(binary_example=structure_example)
|
|
503
|
-
|
|
504
|
-
self.output_configs.append(ToolOutputDetailsPbo(
|
|
505
|
-
base_config=ToolIoConfigBasePbo(
|
|
506
|
-
io_number=len(self.output_configs),
|
|
507
|
-
content_type=content_type,
|
|
508
|
-
display_name=display_name,
|
|
509
|
-
description=description,
|
|
510
|
-
structure_example=structure,
|
|
511
|
-
testing_example=testing
|
|
512
|
-
)))
|
|
513
|
-
self._output_container_types.append(container_type)
|
|
514
|
-
|
|
515
|
-
def add_config_field(self, field: VeloxFieldDefPbo) -> None:
|
|
516
|
-
"""
|
|
517
|
-
Add a configuration field to the tool. This field will be used to configure the tool in the plan manager.
|
|
518
|
-
|
|
519
|
-
:param field: The configuration field details.
|
|
520
|
-
"""
|
|
521
|
-
self.config_fields.append(field)
|
|
522
|
-
|
|
523
|
-
def add_config_field_def(self, field: AbstractVeloxFieldDefinition) -> None:
|
|
524
|
-
"""
|
|
525
|
-
Add a configuration field to the tool. This field will be used to configure the tool in the plan manager.
|
|
526
|
-
|
|
527
|
-
:param field: The configuration field details.
|
|
528
|
-
"""
|
|
529
|
-
self.config_fields.append(ProtobufUtils.field_def_to_pbo(field))
|
|
530
|
-
|
|
531
|
-
def add_boolean_config_field(self, field_name: str, display_name: str, description: str, default_value: bool,
|
|
532
|
-
optional: bool = False) -> None:
|
|
533
|
-
"""
|
|
534
|
-
Add a boolean configuration field to the tool. This field will be used to configure the tool in the plan
|
|
535
|
-
manager.
|
|
536
|
-
|
|
537
|
-
:param field_name: The name of the field.
|
|
538
|
-
:param display_name: The display name of the field.
|
|
539
|
-
:param description: The description of the field.
|
|
540
|
-
:param default_value: The default value of the field.
|
|
541
|
-
:param optional: If true, this field is optional. If false, this field is required.
|
|
542
|
-
"""
|
|
543
|
-
self.config_fields.append(VeloxFieldDefPbo(
|
|
544
|
-
data_field_type=FieldTypePbo.BOOLEAN,
|
|
545
|
-
data_field_name=field_name,
|
|
546
|
-
display_name=display_name,
|
|
547
|
-
description=description,
|
|
548
|
-
required=not optional,
|
|
549
|
-
editable=True,
|
|
550
|
-
boolean_properties=BooleanPropertiesPbo(
|
|
551
|
-
default_value=default_value
|
|
552
|
-
)
|
|
553
|
-
))
|
|
554
|
-
|
|
555
|
-
def add_double_config_field(self, field_name: str, display_name: str, description: str, default_value: float,
|
|
556
|
-
min_value: float = -10.**120, max_value: float = 10.**120, precision: int = 2,
|
|
557
|
-
optional: bool = False) -> None:
|
|
558
|
-
"""
|
|
559
|
-
Add a double configuration field to the tool. This field will be used to configure the tool in the plan
|
|
560
|
-
manager.
|
|
561
|
-
|
|
562
|
-
:param field_name: The name of the field.
|
|
563
|
-
:param display_name: The display name of the field.
|
|
564
|
-
:param description: The description of the field.
|
|
565
|
-
:param default_value: The default value of the field.
|
|
566
|
-
:param min_value: The minimum value of the field.
|
|
567
|
-
:param max_value: The maximum value of the field.
|
|
568
|
-
:param precision: The precision of the field.
|
|
569
|
-
:param optional: If true, this field is optional. If false, this field is required.
|
|
570
|
-
"""
|
|
571
|
-
self.config_fields.append(VeloxFieldDefPbo(
|
|
572
|
-
data_field_type=FieldTypePbo.DOUBLE,
|
|
573
|
-
data_field_name=field_name,
|
|
574
|
-
display_name=display_name,
|
|
575
|
-
description=description,
|
|
576
|
-
required=not optional,
|
|
577
|
-
editable=True,
|
|
578
|
-
double_properties=DoublePropertiesPbo(
|
|
579
|
-
default_value=default_value,
|
|
580
|
-
min_value=min_value,
|
|
581
|
-
max_value=max_value,
|
|
582
|
-
precision=precision
|
|
583
|
-
)
|
|
584
|
-
))
|
|
585
|
-
|
|
586
|
-
def add_integer_config_field(self, field_name: str, display_name: str, description: str,
|
|
587
|
-
default_value: int, min_value: int = -2**31, max_value: int = 2**31-1,
|
|
588
|
-
optional: bool = False) -> None:
|
|
589
|
-
"""
|
|
590
|
-
Add an integer configuration field to the tool. This field will be used to configure the tool in the plan
|
|
591
|
-
manager.
|
|
592
|
-
|
|
593
|
-
:param field_name: The name of the field.
|
|
594
|
-
:param display_name: The display name of the field.
|
|
595
|
-
:param description: The description of the field.
|
|
596
|
-
:param default_value: The default value of the field.
|
|
597
|
-
:param min_value: The minimum value of the field.
|
|
598
|
-
:param max_value: The maximum value of the field.
|
|
599
|
-
:param optional: If true, this field is optional. If false, this field is required.
|
|
600
|
-
"""
|
|
601
|
-
self.config_fields.append(VeloxFieldDefPbo(
|
|
602
|
-
data_field_type=FieldTypePbo.INTEGER,
|
|
603
|
-
data_field_name=field_name,
|
|
604
|
-
display_name=display_name,
|
|
605
|
-
description=description,
|
|
606
|
-
required=not optional,
|
|
607
|
-
editable=True,
|
|
608
|
-
integer_properties=IntegerPropertiesPbo(
|
|
609
|
-
default_value=default_value,
|
|
610
|
-
min_value=min_value,
|
|
611
|
-
max_value=max_value
|
|
612
|
-
)
|
|
613
|
-
))
|
|
614
|
-
|
|
615
|
-
def add_string_config_field(self, field_name: str, display_name: str, description: str,
|
|
616
|
-
default_value: str, max_length: int = 1000, optional: bool = False) -> None:
|
|
617
|
-
"""
|
|
618
|
-
Add a string configuration field to the tool. This field will be used to configure the tool in the plan
|
|
619
|
-
manager.
|
|
620
|
-
|
|
621
|
-
:param field_name: The name of the field.
|
|
622
|
-
:param display_name: The display name of the field.
|
|
623
|
-
:param description: The description of the field.
|
|
624
|
-
:param default_value: The default value of the field.
|
|
625
|
-
:param max_length: The maximum length of the field.
|
|
626
|
-
:param optional: If true, this field is optional. If false, this field is required.
|
|
627
|
-
"""
|
|
628
|
-
self.config_fields.append(VeloxFieldDefPbo(
|
|
629
|
-
data_field_type=FieldTypePbo.STRING,
|
|
630
|
-
data_field_name=field_name,
|
|
631
|
-
display_name=display_name,
|
|
632
|
-
description=description,
|
|
633
|
-
required=not optional,
|
|
634
|
-
editable=True,
|
|
635
|
-
string_properties=StringPropertiesPbo(
|
|
636
|
-
default_value=default_value,
|
|
637
|
-
max_length=max_length
|
|
638
|
-
)
|
|
639
|
-
))
|
|
640
|
-
|
|
641
|
-
def add_list_config_field(self, field_name: str, display_name: str, description: str, default_value: str,
|
|
642
|
-
allowed_values: list[str], direct_edit: bool = False, optional: bool = False) -> None:
|
|
643
|
-
"""
|
|
644
|
-
Add a list configuration field to the tool. This field will be used to configure the tool in the plan
|
|
645
|
-
manager.
|
|
646
|
-
|
|
647
|
-
:param field_name: The name of the field.
|
|
648
|
-
:param display_name: The display name of the field.
|
|
649
|
-
:param description: The description of the field.
|
|
650
|
-
:param default_value: The default value of the field.
|
|
651
|
-
:param allowed_values: The list of allowed values for the field.
|
|
652
|
-
:param direct_edit: If true, the user can enter a value that is not in the list of allowed values. If false,
|
|
653
|
-
the user can only select from the list of allowed values.
|
|
654
|
-
:param optional: If true, this field is optional. If false, this field is required.
|
|
655
|
-
"""
|
|
656
|
-
self.config_fields.append(VeloxFieldDefPbo(
|
|
657
|
-
data_field_type=FieldTypePbo.SELECTION,
|
|
658
|
-
data_field_name=field_name,
|
|
659
|
-
display_name=display_name,
|
|
660
|
-
description=description,
|
|
661
|
-
required=not optional,
|
|
662
|
-
editable=True,
|
|
663
|
-
selection_properties=SelectionPropertiesPbo(
|
|
664
|
-
default_value=default_value,
|
|
665
|
-
static_list_values=allowed_values,
|
|
666
|
-
direct_edit=direct_edit,
|
|
667
|
-
)
|
|
668
|
-
))
|
|
669
|
-
|
|
670
|
-
def add_multi_list_config_field(self, field_name: str, display_name: str, description: str,
|
|
671
|
-
default_value: list[str], allowed_values: list[str], direct_edit: bool = False,
|
|
672
|
-
optional: bool = False) -> None:
|
|
673
|
-
"""
|
|
674
|
-
Add a multi-select list configuration field to the tool. This field will be used to configure the tool in the
|
|
675
|
-
plan manager.
|
|
676
|
-
|
|
677
|
-
:param field_name: The name of the field.
|
|
678
|
-
:param display_name: The display name of the field.
|
|
679
|
-
:param description: The description of the field.
|
|
680
|
-
:param default_value: The default value of the field.
|
|
681
|
-
:param allowed_values: The list of allowed values for the field.
|
|
682
|
-
:param direct_edit: If true, the user can enter a value that is not in the list of allowed values. If false,
|
|
683
|
-
the user can only select from the list of allowed values.
|
|
684
|
-
:param optional: If true, this field is optional. If false, this field is required.
|
|
685
|
-
"""
|
|
686
|
-
self.config_fields.append(VeloxFieldDefPbo(
|
|
687
|
-
data_field_type=FieldTypePbo.SELECTION,
|
|
688
|
-
data_field_name=field_name,
|
|
689
|
-
display_name=display_name,
|
|
690
|
-
description=description,
|
|
691
|
-
required=not optional,
|
|
692
|
-
editable=True,
|
|
693
|
-
selection_properties=SelectionPropertiesPbo(
|
|
694
|
-
default_value=",".join(default_value),
|
|
695
|
-
static_list_values=allowed_values,
|
|
696
|
-
multi_select=True,
|
|
697
|
-
direct_edit=direct_edit,
|
|
698
|
-
)
|
|
699
|
-
))
|
|
700
|
-
|
|
701
|
-
def to_pbo(self) -> ToolDetailsPbo:
|
|
702
|
-
"""
|
|
703
|
-
:return: The ToolDetailsPbo proto object representing this tool.
|
|
704
|
-
"""
|
|
705
|
-
return ToolDetailsPbo(
|
|
706
|
-
name=self._name,
|
|
707
|
-
description=self._description,
|
|
708
|
-
input_configs=self.input_configs,
|
|
709
|
-
output_configs=self.output_configs,
|
|
710
|
-
output_data_type_name=self._data_type_name,
|
|
711
|
-
config_fields=self.config_fields
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
@abstractmethod
|
|
715
|
-
def validate_input(self) -> str | None:
|
|
716
|
-
"""
|
|
717
|
-
Validate the request given to this tool. If the request is validly formatted, this method should return None.
|
|
718
|
-
If the request is not valid, this method should return an error message indicating what is wrong with the
|
|
719
|
-
request.
|
|
720
|
-
|
|
721
|
-
This method should not perform any actual processing of the request. It should only validate the inputs and
|
|
722
|
-
configurations provided in the request.
|
|
723
|
-
|
|
724
|
-
The request inputs can be accessed using the self.get_input_*() methods.
|
|
725
|
-
The request settings can be accessed using the self.get_config_fields() method.
|
|
726
|
-
The request itself can be accessed using self.request.
|
|
727
|
-
|
|
728
|
-
:return: A tuple containing a boolean indicating whether the request is valid and a message describing the
|
|
729
|
-
result of the validation.
|
|
730
|
-
"""
|
|
731
|
-
pass
|
|
732
|
-
|
|
733
|
-
def dry_run_output(self) -> list[SapioToolResult]:
|
|
734
|
-
"""
|
|
735
|
-
Provide fixed results for a dry run of this tool. This method should not perform any actual processing of the
|
|
736
|
-
request. It should only return example outputs that can be used to test the next tool in the plan.
|
|
737
|
-
|
|
738
|
-
The default implementation of this method looks at the testing_example field of each output configuration
|
|
739
|
-
and returns a SapioToolResult object based on the content type of the output.
|
|
740
|
-
|
|
741
|
-
:return: A list of SapioToolResult objects containing example outputs for this tool. Each result in the list
|
|
742
|
-
corresponds to a separate output from the tool.
|
|
743
|
-
"""
|
|
744
|
-
results: list[SapioToolResult] = []
|
|
745
|
-
for output, container_type in zip(self.output_configs, self._output_container_types):
|
|
746
|
-
config: ToolIoConfigBasePbo = output.base_config
|
|
747
|
-
example: ExampleContainerPbo = config.testing_example
|
|
748
|
-
content_type: str = config.content_type
|
|
749
|
-
match container_type:
|
|
750
|
-
case ContainerType.BINARY:
|
|
751
|
-
example: bytes = example.binary_example
|
|
752
|
-
results.append(BinaryResult(binary_data=[example], content_type=content_type))
|
|
753
|
-
case ContainerType.CSV:
|
|
754
|
-
example: str = example.text_example
|
|
755
|
-
results.append(CsvResult(FileUtil.tokenize_csv(example.encode())[0], content_type=content_type))
|
|
756
|
-
case ContainerType.JSON:
|
|
757
|
-
# The example may be in the JSONL format instead of plain JSON, so we need to use Pandas to parse
|
|
758
|
-
# the example into plain JSON.
|
|
759
|
-
example: str = example.text_example
|
|
760
|
-
# Format the JSONL in a way that Pandas likes. Collapse everything into a single line, and then
|
|
761
|
-
# split it back into multiple lines where each line is a single JSON list or dictionary.
|
|
762
|
-
example: str = re.sub("([]}])\s*([\[{])", r"\1\n\2", example.replace("\n", "")).strip()
|
|
763
|
-
# Read the JSONL into a Pandas DataFrame and convert it back to plain JSON.
|
|
764
|
-
import pandas as pd
|
|
765
|
-
with io.StringIO(example) as stream:
|
|
766
|
-
example: str = pd.read_json(path_or_buf=stream, lines=True).to_json()
|
|
767
|
-
results.append(JsonResult(json_data=[json.loads(example)], content_type=content_type))
|
|
768
|
-
case ContainerType.TEXT:
|
|
769
|
-
example: str = example.text_example
|
|
770
|
-
results.append(TextResult(text_data=[example], content_type=content_type))
|
|
771
|
-
return results
|
|
772
|
-
|
|
773
|
-
@abstractmethod
|
|
774
|
-
def run(self, user: SapioUser) -> list[SapioToolResult]:
|
|
775
|
-
"""
|
|
776
|
-
Execute this tool.
|
|
777
|
-
|
|
778
|
-
The request inputs can be accessed using the self.get_input_*() methods.
|
|
779
|
-
The request settings can be accessed using the self.get_config_fields() method.
|
|
780
|
-
The request itself can be accessed using self.request.
|
|
781
|
-
|
|
782
|
-
:param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
|
|
783
|
-
system.
|
|
784
|
-
:return: A list of SapioToolResult objects containing the response data. Each result in the list corresponds to
|
|
785
|
-
a separate output from the tool. Field map results do not appear as tool output in the plan manager, instead
|
|
786
|
-
appearing as records related to the plan step during the run.
|
|
787
|
-
"""
|
|
788
|
-
pass
|
|
789
|
-
|
|
790
|
-
def log_info(self, message: str) -> None:
|
|
791
|
-
"""
|
|
792
|
-
Log an info message for this tool. If verbose logging is enabled, this message will be included in the logs
|
|
793
|
-
returned to the caller. Empty/None inputs will not be logged.
|
|
794
|
-
|
|
795
|
-
:param message: The message to log.
|
|
796
|
-
"""
|
|
797
|
-
if not message:
|
|
798
|
-
return
|
|
799
|
-
if self.verbose_logging:
|
|
800
|
-
self.logs.append(f"INFO: {self._name}: {message}")
|
|
801
|
-
self.logger.info(message)
|
|
802
|
-
|
|
803
|
-
def log_warning(self, message: str) -> None:
|
|
804
|
-
"""
|
|
805
|
-
Log a warning message for this tool. This message will be included in the logs returned to the caller.
|
|
806
|
-
Empty/None inputs will not be logged.
|
|
807
|
-
|
|
808
|
-
:param message: The message to log.
|
|
809
|
-
"""
|
|
810
|
-
if not message:
|
|
811
|
-
return
|
|
812
|
-
self.logs.append(f"WARNING: {self._name}: {message}")
|
|
813
|
-
self.logger.warning(message)
|
|
814
|
-
|
|
815
|
-
def log_error(self, message: str) -> None:
|
|
816
|
-
"""
|
|
817
|
-
Log an error message for this tool. This message will be included in the logs returned to the caller.
|
|
818
|
-
Empty/None inputs will not be logged.
|
|
819
|
-
|
|
820
|
-
:param message: The message to log.
|
|
821
|
-
"""
|
|
822
|
-
if not message:
|
|
823
|
-
return
|
|
824
|
-
self.logs.append(f"ERROR: {self._name}: {message}")
|
|
825
|
-
self.logger.error(message)
|
|
826
|
-
|
|
827
|
-
def log_exception(self, message: str, e: Exception) -> None:
|
|
828
|
-
"""
|
|
829
|
-
Log an exception for this tool. This message will be included in the logs returned to the caller.
|
|
830
|
-
Empty/None inputs will not be logged.
|
|
831
|
-
|
|
832
|
-
:param message: The message to log.
|
|
833
|
-
:param e: The exception to log.
|
|
834
|
-
"""
|
|
835
|
-
if not message and not e:
|
|
836
|
-
return
|
|
837
|
-
self.logs.append(f"EXCEPTION: {self._name}: {message} - {e}")
|
|
838
|
-
self.logger.error(f"{message}\n{traceback.format_exc()}")
|
|
839
|
-
|
|
840
|
-
def get_input_name(self, index: int = 0) -> str | None:
|
|
841
|
-
"""
|
|
842
|
-
Get the name of the input from the request object.
|
|
843
|
-
|
|
844
|
-
:param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
|
|
845
|
-
:return: The name of the input from the request object, or None if no name is set.
|
|
846
|
-
"""
|
|
847
|
-
return self.request.input[index].item_container.container_name
|
|
848
|
-
|
|
849
|
-
def get_input_binary(self, index: int = 0) -> list[bytes]:
|
|
850
|
-
"""
|
|
851
|
-
Get the binary data from the request object.
|
|
852
|
-
|
|
853
|
-
:param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
|
|
854
|
-
:return: The binary data from the request object.
|
|
855
|
-
"""
|
|
856
|
-
return list(self.request.input[index].item_container.binary_container.items)
|
|
857
|
-
|
|
858
|
-
def get_input_csv(self, index: int = 0) -> tuple[list[str], list[dict[str, str]]]:
|
|
859
|
-
"""
|
|
860
|
-
Parse the CSV data from the request object.
|
|
861
|
-
|
|
862
|
-
:param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
|
|
863
|
-
:return: A tuple containing the header row and the data rows. The header row is a list of strings representing
|
|
864
|
-
the column names, and the data rows are a list of dictionaries where each dictionary represents a row in the
|
|
865
|
-
CSV with the column names as keys and the corresponding values as strings.
|
|
866
|
-
"""
|
|
867
|
-
input_data: Sequence[StepInputBatchPbo] = self.request.input
|
|
868
|
-
ret_val: list[dict[str, str]] = []
|
|
869
|
-
headers: Iterable[str] = input_data[index].item_container.csv_container.header.cells
|
|
870
|
-
for row in input_data[index].item_container.csv_container.items:
|
|
871
|
-
row_dict: dict[str, str] = {}
|
|
872
|
-
for header, value in zip(headers, row.cells):
|
|
873
|
-
row_dict[header] = value
|
|
874
|
-
ret_val.append(row_dict)
|
|
875
|
-
return list(headers), ret_val
|
|
876
|
-
|
|
877
|
-
def get_input_json(self, index: int = 0) -> list[list[Any]] | list[dict[str, Any]]:
|
|
878
|
-
"""
|
|
879
|
-
Parse the JSON data from the request object.
|
|
880
|
-
|
|
881
|
-
:param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
|
|
882
|
-
:return: A list of parsed JSON objects. Each entry in the list represents a separate JSON entry from the input.
|
|
883
|
-
Depending on this tool, this may be a list of dictionaries or a list of lists.
|
|
884
|
-
"""
|
|
885
|
-
return [json.loads(x) for x in self.request.input[index].item_container.json_container.items]
|
|
886
|
-
|
|
887
|
-
def get_input_text(self, index: int = 0) -> list[str]:
|
|
888
|
-
"""
|
|
889
|
-
Parse the text data from the request object.
|
|
890
|
-
|
|
891
|
-
:param index: The index of the input to parse. Defaults to 0. Used for tools that accept multiple inputs.
|
|
892
|
-
:return: A list of text data as strings.
|
|
893
|
-
"""
|
|
894
|
-
return list(self.request.input[index].item_container.text_container.items)
|
|
895
|
-
|
|
896
|
-
def get_config_defs(self) -> dict[str, VeloxFieldDefPbo]:
|
|
897
|
-
"""
|
|
898
|
-
Get the config field definitions for this tool.
|
|
899
|
-
|
|
900
|
-
:return: A dictionary of field definitions, where the keys are the field names and the values are the
|
|
901
|
-
VeloxFieldDefPbo objects representing the field definitions.
|
|
902
|
-
"""
|
|
903
|
-
field_defs: dict[str, VeloxFieldDefPbo] = {}
|
|
904
|
-
for field_def in self.to_pbo().config_fields:
|
|
905
|
-
field_defs[field_def.data_field_name] = field_def
|
|
906
|
-
return field_defs
|
|
907
|
-
|
|
908
|
-
def get_config_fields(self) -> dict[str, FieldValue | list[str]]:
|
|
909
|
-
"""
|
|
910
|
-
Get the configuration field values from the request object. If a field is not present in the request,
|
|
911
|
-
the default value from the config definition will be returned.
|
|
912
|
-
|
|
913
|
-
:return: A dictionary of configuration field names and their values. For multi-select selection list fields,
|
|
914
|
-
a list of strings will be returned. For all other field types, the value will match the field type
|
|
915
|
-
(bool for boolean fields, float for double fields, int for short, integer, long, and enum fields, and
|
|
916
|
-
string for everything else).
|
|
917
|
-
"""
|
|
918
|
-
config_fields: dict[str, Any] = {}
|
|
919
|
-
raw_configs: Mapping[str, FieldValuePbo] = self.request.config_field_values
|
|
920
|
-
for field_name, field_def in self.get_config_defs().items():
|
|
921
|
-
field_value: FieldValue = None
|
|
922
|
-
# If the field is present in the request, convert the protobuf value to a Python value.
|
|
923
|
-
if field_name in raw_configs:
|
|
924
|
-
field_value = ProtobufUtils.field_pbo_to_value(raw_configs[field_name])
|
|
925
|
-
# If the field isn't present or is None, use the default value from the field definition.
|
|
926
|
-
if field_value is None:
|
|
927
|
-
field_value = ProtobufUtils.field_def_pbo_to_default_value(field_def)
|
|
928
|
-
# If the field is a multi-select selection list, split the value by commas and strip whitespace.
|
|
929
|
-
if field_def.data_field_type == FieldTypePbo.SELECTION and field_def.selection_properties.multi_select:
|
|
930
|
-
field_value: list[str] = [x.strip() for x in field_value.split(',') if x.strip()]
|
|
931
|
-
config_fields[field_name] = field_value
|
|
932
|
-
return config_fields
|
|
933
|
-
|
|
934
|
-
@staticmethod
|
|
935
|
-
def read_from_json(json_data: list[dict[str, Any]], key: str) -> list[Any]:
|
|
936
|
-
"""
|
|
937
|
-
From a list of dictionaries, return a list of values for the given key from each dictionary. Skips null values.
|
|
938
|
-
|
|
939
|
-
:param json_data: The JSON data to read from.
|
|
940
|
-
:param key: The key to read the values from.
|
|
941
|
-
:return: A list of values corresponding to the given key in the JSON data.
|
|
942
|
-
"""
|
|
943
|
-
ret_val: list[Any] = []
|
|
944
|
-
for entry in json_data:
|
|
945
|
-
if key in entry:
|
|
946
|
-
value = entry[key]
|
|
947
|
-
if isinstance(value, list):
|
|
948
|
-
ret_val.extend(value)
|
|
949
|
-
elif value is not None:
|
|
950
|
-
ret_val.append(value)
|
|
951
|
-
return ret_val
|