sapiopycommons 2025.8.14a703__py3-none-any.whl → 2026.1.22a847__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.
- sapiopycommons/ai/agent_service_base.py +2051 -0
- sapiopycommons/ai/converter_service_base.py +163 -0
- sapiopycommons/ai/external_credentials.py +131 -0
- sapiopycommons/ai/protoapi/agent/agent_pb2.py +87 -0
- sapiopycommons/ai/protoapi/agent/agent_pb2.pyi +282 -0
- sapiopycommons/ai/protoapi/agent/agent_pb2_grpc.py +154 -0
- sapiopycommons/ai/protoapi/agent/entry_pb2.py +49 -0
- sapiopycommons/ai/protoapi/agent/entry_pb2.pyi +40 -0
- sapiopycommons/ai/protoapi/agent/entry_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/agent/item/item_container_pb2.py +61 -0
- sapiopycommons/ai/protoapi/agent/item/item_container_pb2.pyi +181 -0
- sapiopycommons/ai/protoapi/agent/item/item_container_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.py +41 -0
- sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.pyi +36 -0
- sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py +51 -0
- sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi +59 -0
- sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py +123 -0
- sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi +599 -0
- sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.py +59 -0
- sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.pyi +68 -0
- sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2_grpc.py +149 -0
- sapiopycommons/ai/protoapi/pipeline/script/script_pb2.py +69 -0
- sapiopycommons/ai/protoapi/pipeline/script/script_pb2.pyi +109 -0
- sapiopycommons/ai/protoapi/pipeline/script/script_pb2_grpc.py +153 -0
- sapiopycommons/ai/protoapi/pipeline/step_output_pb2.py +49 -0
- sapiopycommons/ai/protoapi/pipeline/step_output_pb2.pyi +56 -0
- sapiopycommons/ai/protoapi/pipeline/step_output_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/pipeline/step_pb2.py +43 -0
- sapiopycommons/ai/protoapi/pipeline/step_pb2.pyi +44 -0
- sapiopycommons/ai/protoapi/pipeline/step_pb2_grpc.py +24 -0
- sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py +39 -0
- sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi +33 -0
- sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py +24 -0
- sapiopycommons/ai/protobuf_utils.py +583 -0
- sapiopycommons/ai/request_validation.py +561 -0
- sapiopycommons/ai/server.py +152 -0
- sapiopycommons/ai/test_client.py +534 -0
- sapiopycommons/callbacks/callback_util.py +26 -7
- sapiopycommons/eln/experiment_handler.py +12 -5
- sapiopycommons/files/file_util.py +128 -1
- sapiopycommons/files/temp_files.py +82 -0
- sapiopycommons/general/aliases.py +4 -1
- sapiopycommons/general/macros.py +172 -0
- sapiopycommons/general/time_util.py +199 -4
- sapiopycommons/recordmodel/record_handler.py +47 -12
- 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.8.14a703.dist-info → sapiopycommons-2026.1.22a847.dist-info}/METADATA +2 -2
- sapiopycommons-2026.1.22a847.dist-info/RECORD +113 -0
- sapiopycommons/ai/tool_of_tools.py +0 -917
- sapiopycommons-2025.8.14a703.dist-info/RECORD +0 -72
- {sapiopycommons-2025.8.14a703.dist-info → sapiopycommons-2026.1.22a847.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.8.14a703.dist-info → sapiopycommons-2026.1.22a847.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,2051 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os.path
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import traceback
|
|
10
|
+
from abc import abstractmethod, ABC
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from logging import Logger
|
|
13
|
+
from os import PathLike
|
|
14
|
+
from subprocess import CompletedProcess
|
|
15
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
16
|
+
|
|
17
|
+
from grpc import ServicerContext
|
|
18
|
+
from sapiopylib.rest.User import SapioUser, ensure_logger_initialized
|
|
19
|
+
from sapiopylib.rest.pojo.DataRecord import DataRecord
|
|
20
|
+
from sapiopylib.rest.pojo.DateRange import DateRange
|
|
21
|
+
from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, SapioDoubleFormat, \
|
|
22
|
+
SapioStringFormat
|
|
23
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel, AbstractRecordModel
|
|
24
|
+
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel
|
|
25
|
+
|
|
26
|
+
from sapiopycommons.ai.external_credentials import ExternalCredentials
|
|
27
|
+
from sapiopycommons.ai.protoapi.agent.agent_pb2 import AgentDetailsResponsePbo, \
|
|
28
|
+
AgentDetailsPbo, ProcessStepRequestPbo, ProcessStepResponsePbo, AgentOutputDetailsPbo, AgentIoConfigBasePbo, \
|
|
29
|
+
AgentInputDetailsPbo, ExampleContainerPbo, ProcessStepResponseStatusPbo, AgentCitationPbo
|
|
30
|
+
from sapiopycommons.ai.protoapi.agent.agent_pb2_grpc import AgentServiceServicer
|
|
31
|
+
from sapiopycommons.ai.protoapi.agent.entry_pb2 import StepOutputBatchPbo, StepItemContainerPbo, \
|
|
32
|
+
StepBinaryContainerPbo, StepCsvContainerPbo, StepCsvHeaderRowPbo, StepCsvRowPbo, StepJsonContainerPbo, \
|
|
33
|
+
StepTextContainerPbo
|
|
34
|
+
from sapiopycommons.ai.protoapi.agent.item.item_container_pb2 import ContentTypePbo, StepDataRecordContainerPbo
|
|
35
|
+
from sapiopycommons.ai.protoapi.externalcredentials.external_credentials_pb2 import ExternalCredentialsPbo
|
|
36
|
+
from sapiopycommons.ai.protoapi.fielddefinitions.fields_pb2 import FieldValuePbo, DataRecordPbo
|
|
37
|
+
from sapiopycommons.ai.protoapi.fielddefinitions.velox_field_def_pb2 import VeloxFieldDefPbo, FieldTypePbo, \
|
|
38
|
+
SelectionPropertiesPbo, IntegerPropertiesPbo, DoublePropertiesPbo, BooleanPropertiesPbo, StringPropertiesPbo, \
|
|
39
|
+
DatePropertiesPbo, BooleanDependentFieldEntryPbo, SelectionDependentFieldEntryPbo, DateRangePropertiesPbo
|
|
40
|
+
from sapiopycommons.ai.protoapi.session.sapio_conn_info_pb2 import SapioUserSecretTypePbo, SapioConnectionInfoPbo
|
|
41
|
+
from sapiopycommons.ai.protobuf_utils import ProtobufUtils
|
|
42
|
+
from sapiopycommons.files.file_util import FileUtil
|
|
43
|
+
from sapiopycommons.files.temp_files import TempFileHandler
|
|
44
|
+
from sapiopycommons.general.aliases import FieldMap, FieldValue, SapioRecord, AliasUtil
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# FR-47422: Created classes.
|
|
48
|
+
class ContainerType(Enum):
|
|
49
|
+
"""
|
|
50
|
+
An enum of the different container contents of a StepItemContainerPbo.
|
|
51
|
+
"""
|
|
52
|
+
BINARY = "binary"
|
|
53
|
+
CSV = "csv"
|
|
54
|
+
DATA_RECORDS = "DataRecord"
|
|
55
|
+
JSON = "json"
|
|
56
|
+
TEXT = "text"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AgentResult(ABC):
|
|
60
|
+
"""
|
|
61
|
+
A class representing a result from a Tool of Tools agent. Instantiate one of the subclasses to create a result
|
|
62
|
+
object.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def to_pbo(self, content_type: str, file_extensions: list[str] | None = None,
|
|
67
|
+
data_type_name: str | None = None) -> StepOutputBatchPbo:
|
|
68
|
+
"""
|
|
69
|
+
Convert this AgentResult object to a StepOutputBatchPbo or list of FieldValueMapPbo proto objects.
|
|
70
|
+
|
|
71
|
+
:param content_type: The content type of the result.
|
|
72
|
+
:param file_extensions: The file extensions of the result. These may be used to download the results as a
|
|
73
|
+
file within the platform.
|
|
74
|
+
:param data_type_name: The data type of the result. Only used for DataRecordResults.
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BinaryResult(AgentResult):
|
|
80
|
+
"""
|
|
81
|
+
A class representing binary results from an agent.
|
|
82
|
+
"""
|
|
83
|
+
binary_data: list[bytes]
|
|
84
|
+
name: str
|
|
85
|
+
|
|
86
|
+
def __init__(self, binary_data: list[bytes], name: str | None = None):
|
|
87
|
+
"""
|
|
88
|
+
:param binary_data: The binary data as a list of bytes.
|
|
89
|
+
:param name: An optional identifying name for this result that will be accessible to the next agent.
|
|
90
|
+
"""
|
|
91
|
+
self.binary_data = binary_data
|
|
92
|
+
self.name = name
|
|
93
|
+
|
|
94
|
+
def to_pbo(self, content_type: str, file_extensions: list[str] | None = None,
|
|
95
|
+
data_type_name: str | None = None) -> StepOutputBatchPbo:
|
|
96
|
+
extensions: list[str] = file_extensions if file_extensions else []
|
|
97
|
+
return StepOutputBatchPbo(
|
|
98
|
+
item_container=StepItemContainerPbo(
|
|
99
|
+
content_type=ContentTypePbo(name=content_type, extensions=extensions),
|
|
100
|
+
container_name=self.name,
|
|
101
|
+
binary_container=StepBinaryContainerPbo(items=self.binary_data)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CsvResult(AgentResult):
|
|
107
|
+
"""
|
|
108
|
+
A class representing CSV results from an agent.
|
|
109
|
+
"""
|
|
110
|
+
csv_data: list[dict[str, Any]]
|
|
111
|
+
name: str
|
|
112
|
+
|
|
113
|
+
def __init__(self, csv_data: list[dict[str, Any]], name: str | None = None):
|
|
114
|
+
"""
|
|
115
|
+
:param csv_data: The list of CSV data results, provided as a list of dictionaries of column name to value.
|
|
116
|
+
:param name: An optional identifying name for this result that will be accessible to the next agent.
|
|
117
|
+
"""
|
|
118
|
+
self.csv_data = csv_data
|
|
119
|
+
self.name = name
|
|
120
|
+
|
|
121
|
+
def to_pbo(self, content_type: str, file_extensions: list[str] | None = None,
|
|
122
|
+
data_type_name: str | None = None) -> StepOutputBatchPbo:
|
|
123
|
+
return StepOutputBatchPbo(
|
|
124
|
+
item_container=StepItemContainerPbo(
|
|
125
|
+
content_type=ContentTypePbo(name=content_type, extensions=[".csv"]),
|
|
126
|
+
container_name=self.name,
|
|
127
|
+
csv_container=StepCsvContainerPbo(
|
|
128
|
+
header=StepCsvHeaderRowPbo(cells=self.csv_data[0].keys()),
|
|
129
|
+
items=[StepCsvRowPbo(cells=[str(x) for x in row.values()]) for row in self.csv_data]
|
|
130
|
+
)
|
|
131
|
+
) if self.csv_data else None
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DataRecordResult(AgentResult):
|
|
136
|
+
"""
|
|
137
|
+
A class representing data record results from an agent.
|
|
138
|
+
"""
|
|
139
|
+
new_records: list[FieldMap]
|
|
140
|
+
existing_records: list[DataRecord]
|
|
141
|
+
name: str | None
|
|
142
|
+
data_type_name: str | None
|
|
143
|
+
|
|
144
|
+
def __init__(self, new_records: list[FieldMap] | None = None,
|
|
145
|
+
existing_records: list[SapioRecord] | None = None, name: str | None = None,
|
|
146
|
+
data_type_name: str | None = None):
|
|
147
|
+
"""
|
|
148
|
+
:param new_records: A list of field maps, where each map is a dictionary of field names to values. Each entry
|
|
149
|
+
will create a new data record in the system.
|
|
150
|
+
:param existing_records: A list of records that already exist in the system. Any changes made to the fields of
|
|
151
|
+
the provided records will be committed to the database after this agent's results are returned.
|
|
152
|
+
:param name: An optional identifying name for this result that will be accessible to the next agent.
|
|
153
|
+
:param data_type_name: The data type of the new records created by this result. This is only required if the
|
|
154
|
+
output configuration related to this result does not have a set output data type.
|
|
155
|
+
"""
|
|
156
|
+
self.name = name
|
|
157
|
+
self.data_type_name = data_type_name.strip() if data_type_name else None
|
|
158
|
+
self.new_records = new_records if new_records is not None else []
|
|
159
|
+
self.existing_records = []
|
|
160
|
+
if existing_records is not None:
|
|
161
|
+
for record in existing_records:
|
|
162
|
+
record_id: int | None = AliasUtil.to_record_id(record)
|
|
163
|
+
if record_id is None or record_id <= 0:
|
|
164
|
+
raise Exception(f"A record provided as an existing record has an invalid record ID.\n{record}")
|
|
165
|
+
if isinstance(record, DataRecord):
|
|
166
|
+
self.existing_records.append(record)
|
|
167
|
+
elif isinstance(record, (PyRecordModel, AbstractRecordModel, WrappedRecordModel)):
|
|
168
|
+
data_record: DataRecord = record.get_data_record()
|
|
169
|
+
data_record.set_fields(record.fields.copy_to_dict())
|
|
170
|
+
self.existing_records.append(data_record)
|
|
171
|
+
else:
|
|
172
|
+
raise Exception(f"Unsupported record type: {type(record)}.")
|
|
173
|
+
|
|
174
|
+
def to_pbo(self, content_type: str, file_extensions: list[str] | None = None,
|
|
175
|
+
data_type_name: str | None = None) -> StepOutputBatchPbo:
|
|
176
|
+
dt: str = data_type_name if data_type_name and data_type_name != "Any" else self.data_type_name
|
|
177
|
+
data_records: list[DataRecordPbo] = []
|
|
178
|
+
for field_map in self.new_records:
|
|
179
|
+
fields: dict[str, FieldValuePbo] = ProtobufUtils.field_map_to_pbo(field_map)
|
|
180
|
+
data_records.append(DataRecordPbo(data_type_name=dt, record_id=None, fields=fields))
|
|
181
|
+
for record in self.existing_records:
|
|
182
|
+
fields: dict[str, FieldValuePbo] = ProtobufUtils.field_map_to_pbo(record.fields)
|
|
183
|
+
data_records.append(DataRecordPbo(data_type_name=record.data_type_name, record_id=record.record_id,
|
|
184
|
+
fields=fields))
|
|
185
|
+
return StepOutputBatchPbo(
|
|
186
|
+
item_container=StepItemContainerPbo(
|
|
187
|
+
content_type=ContentTypePbo(name=content_type),
|
|
188
|
+
container_name=self.name,
|
|
189
|
+
data_record_container=StepDataRecordContainerPbo(items=data_records)
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class JsonResult(AgentResult):
|
|
195
|
+
"""
|
|
196
|
+
A class representing JSON results from an agent.
|
|
197
|
+
"""
|
|
198
|
+
json_data: list[dict[str, Any]]
|
|
199
|
+
name: str
|
|
200
|
+
|
|
201
|
+
def __init__(self, json_data: list[dict[str, Any]], name: str | None = None):
|
|
202
|
+
"""
|
|
203
|
+
:param json_data: The list of JSON data results. Each entry in the list represents a separate JSON object.
|
|
204
|
+
These entries must be able to be serialized to JSON using json.dumps().
|
|
205
|
+
:param name: An optional identifying name for this result that will be accessible to the next agent.
|
|
206
|
+
"""
|
|
207
|
+
# Verify that the given json_data is actually a list of dictionaries.
|
|
208
|
+
if not isinstance(json_data, list) or not all(isinstance(x, dict) for x in json_data):
|
|
209
|
+
raise ValueError("json_data must be a list of dictionaries.")
|
|
210
|
+
self.json_data = json_data
|
|
211
|
+
self.name = name
|
|
212
|
+
|
|
213
|
+
def to_pbo(self, content_type: str, file_extensions: list[str] | None = None,
|
|
214
|
+
data_type_name: str | None = None) -> StepOutputBatchPbo:
|
|
215
|
+
return StepOutputBatchPbo(
|
|
216
|
+
item_container=StepItemContainerPbo(
|
|
217
|
+
content_type=ContentTypePbo(name=content_type, extensions=[".json"]),
|
|
218
|
+
container_name=self.name,
|
|
219
|
+
json_container=StepJsonContainerPbo(items=[json.dumps(x) for x in self.json_data])
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TextResult(AgentResult):
|
|
225
|
+
"""
|
|
226
|
+
A class representing text results from an agent.
|
|
227
|
+
"""
|
|
228
|
+
text_data: list[str]
|
|
229
|
+
name: str
|
|
230
|
+
|
|
231
|
+
def __init__(self, text_data: list[str], name: str | None = None):
|
|
232
|
+
"""
|
|
233
|
+
:param text_data: The text data as a list of strings.
|
|
234
|
+
:param name: An optional identifying name for this result that will be accessible to the next agent.
|
|
235
|
+
"""
|
|
236
|
+
self.text_data = text_data
|
|
237
|
+
self.name = name
|
|
238
|
+
|
|
239
|
+
def to_pbo(self, content_type: str, file_extensions: list[str] | None = None,
|
|
240
|
+
data_type_name: str | None = None) -> StepOutputBatchPbo:
|
|
241
|
+
return StepOutputBatchPbo(
|
|
242
|
+
item_container=StepItemContainerPbo(
|
|
243
|
+
content_type=ContentTypePbo(name=content_type, extensions=[".txt"]),
|
|
244
|
+
container_name=self.name,
|
|
245
|
+
text_container=StepTextContainerPbo(items=self.text_data)
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class AgentServiceBase(AgentServiceServicer, ABC):
|
|
251
|
+
"""
|
|
252
|
+
A base class for implementing an agent service. Subclasses should implement the register_agents method to register
|
|
253
|
+
their agents with the service.
|
|
254
|
+
"""
|
|
255
|
+
debug_mode: bool = False
|
|
256
|
+
|
|
257
|
+
def GetAgentDetails(self, request: AgentDetailsPbo, context: ServicerContext) -> AgentDetailsResponsePbo:
|
|
258
|
+
try:
|
|
259
|
+
# Get the agent details from the registered agents.
|
|
260
|
+
details: list[AgentDetailsPbo] = []
|
|
261
|
+
for agent in self.register_agents():
|
|
262
|
+
details.append(agent().to_pbo())
|
|
263
|
+
if not details:
|
|
264
|
+
raise Exception("No agents registered with this service.")
|
|
265
|
+
return AgentDetailsResponsePbo(agent_framework_version=self.server_version(), agent_details=details)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
# Woe to you if you somehow cause an exception to be raised when just initializing your agents.
|
|
268
|
+
# There's no way to log this.
|
|
269
|
+
print(f"CRITICAL ERROR: {e}")
|
|
270
|
+
print(traceback.format_exc())
|
|
271
|
+
return AgentDetailsResponsePbo()
|
|
272
|
+
|
|
273
|
+
def ProcessData(self, request: ProcessStepRequestPbo, context: ServicerContext) -> ProcessStepResponsePbo:
|
|
274
|
+
try:
|
|
275
|
+
# Convert the SapioConnectionInfo proto object to a SapioUser object.
|
|
276
|
+
user = self._create_user(request.sapio_user)
|
|
277
|
+
# Locate the agent. If a string is returned instead of an agent object,
|
|
278
|
+
# that means that the requested agent was not located.
|
|
279
|
+
agent: AgentBase | str = self.find_agent(user, request, context)
|
|
280
|
+
if isinstance(agent, str):
|
|
281
|
+
return ProcessStepResponsePbo(status=ProcessStepResponseStatusPbo.FAILURE, status_message=agent)
|
|
282
|
+
# Get the agent results from the registered agent matching the request.
|
|
283
|
+
success, msg, results, logs = self.run(agent)
|
|
284
|
+
# Convert the results to protobuf objects.
|
|
285
|
+
output_data: list[StepOutputBatchPbo] = []
|
|
286
|
+
for result, output_config, file_extensions in zip(results, agent.output_configs,
|
|
287
|
+
agent.output_file_extensions):
|
|
288
|
+
base_config: AgentIoConfigBasePbo = output_config.base_config
|
|
289
|
+
output_data.append(result.to_pbo(base_config.content_type, file_extensions,
|
|
290
|
+
base_config.data_type_name))
|
|
291
|
+
# Return a ProcessStepResponse proto object containing the results to the caller.
|
|
292
|
+
status = ProcessStepResponseStatusPbo.SUCCESS if success else ProcessStepResponseStatusPbo.FAILURE
|
|
293
|
+
return ProcessStepResponsePbo(status=status, status_message=msg, output=output_data, log=logs)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
# This try/except should never be needed, as the agent should handle its own exceptions, but better safe
|
|
296
|
+
# than sorry.
|
|
297
|
+
print(f"CRITICAL ERROR: {e}")
|
|
298
|
+
print(traceback.format_exc())
|
|
299
|
+
return ProcessStepResponsePbo(status=ProcessStepResponseStatusPbo.FAILURE,
|
|
300
|
+
status_message=f"CRITICAL ERROR: {e}",
|
|
301
|
+
log=[traceback.format_exc()])
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _create_user(info: SapioConnectionInfoPbo, timeout_seconds: int = 60) -> SapioUser:
|
|
305
|
+
"""
|
|
306
|
+
Create a SapioUser object from the given SapioConnectionInfo proto object.
|
|
307
|
+
|
|
308
|
+
:param info: The SapioConnectionInfo proto object.
|
|
309
|
+
:param timeout_seconds: The request timeout for calls made from this user object.
|
|
310
|
+
"""
|
|
311
|
+
guid: str | None = info.app_guid if info.HasField("app_guid") else None
|
|
312
|
+
user = SapioUser(info.webservice_url.rstrip("/"), True, timeout_seconds, guid=guid)
|
|
313
|
+
match info.secret_type:
|
|
314
|
+
case SapioUserSecretTypePbo.SESSION_TOKEN:
|
|
315
|
+
user.api_token = info.secret
|
|
316
|
+
case SapioUserSecretTypePbo.PASSWORD:
|
|
317
|
+
secret: str = info.secret
|
|
318
|
+
if secret.startswith("Basic "):
|
|
319
|
+
secret = secret[6:]
|
|
320
|
+
credentials: list[str] = base64.b64decode(secret).decode().split(":", 1)
|
|
321
|
+
user.username = credentials[0]
|
|
322
|
+
user.password = credentials[1]
|
|
323
|
+
case _:
|
|
324
|
+
raise Exception(f"Unexpected secret type: {info.secret_type}")
|
|
325
|
+
return user
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def server_version() -> int:
|
|
329
|
+
"""
|
|
330
|
+
:return: The version of this set of agents.
|
|
331
|
+
"""
|
|
332
|
+
return 1
|
|
333
|
+
|
|
334
|
+
@abstractmethod
|
|
335
|
+
def register_agents(self) -> list[type[AgentBase]]:
|
|
336
|
+
"""
|
|
337
|
+
Register agent types with this service. Provided agents should implement the AgentBase class.
|
|
338
|
+
|
|
339
|
+
:return: A list of agents to register to this service.
|
|
340
|
+
"""
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
def find_agent(self, user: SapioUser, request: ProcessStepRequestPbo, context: ServicerContext) -> AgentBase | str:
|
|
344
|
+
"""
|
|
345
|
+
Locate the requested agent from within this agent service.
|
|
346
|
+
|
|
347
|
+
:param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
|
|
348
|
+
system.
|
|
349
|
+
:param request: The request object containing the input data.
|
|
350
|
+
:param context: The gRPC context.
|
|
351
|
+
:return: The requested agent, or a string reporting that the agent could not be found that lists all agents
|
|
352
|
+
that this service supports.
|
|
353
|
+
"""
|
|
354
|
+
# Locate the agent named in the request.
|
|
355
|
+
agent_name: str = request.agent_name
|
|
356
|
+
registered_agents: dict[str, type[AgentBase]] = {str(a): a for a in self.register_agents()}
|
|
357
|
+
# If the agent is not found, list all of the registered agents for this service so that the caller can correct
|
|
358
|
+
# the agent it is requesting.
|
|
359
|
+
if agent_name not in registered_agents:
|
|
360
|
+
all_agent_names: str = "\n".join(registered_agents.keys())
|
|
361
|
+
msg: str = (f"Agent \"{agent_name}\" not found in the registered agents for this service. The registered "
|
|
362
|
+
f"agents for this service are: \n{all_agent_names}")
|
|
363
|
+
return msg
|
|
364
|
+
|
|
365
|
+
# Instantiate the agent class.
|
|
366
|
+
agent: AgentBase = registered_agents[agent_name]()
|
|
367
|
+
# Setup the agent with details from the request.
|
|
368
|
+
agent.setup(user, request, context, self.debug_mode)
|
|
369
|
+
return agent
|
|
370
|
+
|
|
371
|
+
def run(self, agent: AgentBase) -> tuple[bool, str, list[AgentResult], list[str]]:
|
|
372
|
+
"""
|
|
373
|
+
Execute an agent from this service.
|
|
374
|
+
|
|
375
|
+
:param agent: The agent to execute.
|
|
376
|
+
:return: Whether or not the agent succeeded, the status message, the results of the agent, and any logs
|
|
377
|
+
generated by the agent.
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
success: bool = True
|
|
381
|
+
msg: str = ""
|
|
382
|
+
results: list[AgentResult] = []
|
|
383
|
+
|
|
384
|
+
# Validate that the provided inputs match the agent's expected inputs.
|
|
385
|
+
errors: list[str] | None = self.validate_input(agent)
|
|
386
|
+
if errors:
|
|
387
|
+
msg = f"Agent input validation error(s):\n\t" + "\n\t".join(errors)
|
|
388
|
+
success = False
|
|
389
|
+
|
|
390
|
+
# If this is a dry run, then provide the fixed dry run output, even if there were errors with the input.
|
|
391
|
+
# Otherwise, if the inputs were successfully validated, then the agent is executed normally.
|
|
392
|
+
if agent.request.dry_run:
|
|
393
|
+
results = agent.dry_run_output()
|
|
394
|
+
# If the input was validated without errors, then set the return message to say so.
|
|
395
|
+
if success:
|
|
396
|
+
msg = f"{agent} dry run input successfully validated."
|
|
397
|
+
elif success:
|
|
398
|
+
results = agent.run(agent.user)
|
|
399
|
+
# Verify that the run results match what is expected of this agent.
|
|
400
|
+
# Otherwise, update the status message to reflect the successful execution of the agent.
|
|
401
|
+
errors: list[str] | None = self.validate_output(agent, results)
|
|
402
|
+
if errors:
|
|
403
|
+
# Wipe the results list so that malformed results aren't returned
|
|
404
|
+
# to the server and mark the run as a failure.
|
|
405
|
+
msg = f"Agent output validation error(s):\n\t" + "\n\t".join(errors)
|
|
406
|
+
results = []
|
|
407
|
+
success = False
|
|
408
|
+
else:
|
|
409
|
+
msg = f"{agent} successfully completed."
|
|
410
|
+
return success, msg, results, agent.logs
|
|
411
|
+
except Exception as e:
|
|
412
|
+
agent.log_exception("Exception occurred during agent execution.", e)
|
|
413
|
+
return False, str(e), [], agent.logs
|
|
414
|
+
finally:
|
|
415
|
+
# Clean up any temporary files created by the agent. If in debug mode, then log the files instead
|
|
416
|
+
# so that they can be manually inspected.
|
|
417
|
+
if self.debug_mode:
|
|
418
|
+
print("Temporary files/directories created during agent execution:")
|
|
419
|
+
for directory in agent.temp_data.directories:
|
|
420
|
+
print(f"\tDirectory: {directory}")
|
|
421
|
+
for file in agent.temp_data.files:
|
|
422
|
+
print(f"\tFile: {file}")
|
|
423
|
+
else:
|
|
424
|
+
agent.temp_data.cleanup()
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def validate_input(agent: AgentBase) -> list[str] | None:
|
|
428
|
+
"""
|
|
429
|
+
Verify that the input from the server matches the expected input according to the agent's input config
|
|
430
|
+
definitions.
|
|
431
|
+
|
|
432
|
+
:param agent: The agent to verify input against.
|
|
433
|
+
:return: A list of errors with the input, if any.
|
|
434
|
+
"""
|
|
435
|
+
if len(agent.request.input) != len(agent.input_configs):
|
|
436
|
+
return [f"Expected {len(agent.input_configs)} inputs for this agent,"
|
|
437
|
+
f"but got {len(agent.request.input)} instead."]
|
|
438
|
+
return agent.validate_input()
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def validate_output(agent: AgentBase, results: list[AgentResult]) -> list[str] | None:
|
|
442
|
+
"""
|
|
443
|
+
Verify that the output from an agent run matches the expected output according to the agent's output config
|
|
444
|
+
definitions.
|
|
445
|
+
|
|
446
|
+
:param agent: The agent to verify output for.
|
|
447
|
+
:param results: The results of the agent run.
|
|
448
|
+
:return: A list of errors with the output, if any.
|
|
449
|
+
"""
|
|
450
|
+
# Validate the output from the agent. Were we actually given a list of results?
|
|
451
|
+
if not isinstance(results, list):
|
|
452
|
+
return [f"Expected a list for results, but got {type(results)} instead."]
|
|
453
|
+
bad_results: list[Any] = [x for x in results if not isinstance(x, AgentResult)]
|
|
454
|
+
if bad_results:
|
|
455
|
+
return [f"The list of results should only contain AgentResult objects. "
|
|
456
|
+
f"Got {[type(x) for x in bad_results]} object(s) instead."]
|
|
457
|
+
|
|
458
|
+
# Look out for missing or out-of-order outputs.
|
|
459
|
+
num_expected_outputs: int = len(agent.output_container_types)
|
|
460
|
+
if len(results) != num_expected_outputs:
|
|
461
|
+
expected_outputs_str: str = ", ".join(x.value for x in agent.output_container_types)
|
|
462
|
+
return [f"Expected {num_expected_outputs} outputs for this agent ({expected_outputs_str}), "
|
|
463
|
+
f"but got {len(results)} instead."]
|
|
464
|
+
|
|
465
|
+
errors: list[str] = []
|
|
466
|
+
output_index: int = 0
|
|
467
|
+
for output in results:
|
|
468
|
+
match agent.output_container_types[output_index]:
|
|
469
|
+
case ContainerType.BINARY:
|
|
470
|
+
if not isinstance(output, BinaryResult):
|
|
471
|
+
errors.append(f"Mismatched output type at index {output_index}: expected "
|
|
472
|
+
f"BinaryResult but got {type(output)}.")
|
|
473
|
+
case ContainerType.CSV:
|
|
474
|
+
if not isinstance(output, CsvResult):
|
|
475
|
+
errors.append(f"Mismatched output type at index {output_index}: expected "
|
|
476
|
+
f"CsvResult but got {type(output)}.")
|
|
477
|
+
case ContainerType.DATA_RECORDS:
|
|
478
|
+
if not isinstance(output, DataRecordResult):
|
|
479
|
+
errors.append(f"Mismatched output type at index {output_index}: expected "
|
|
480
|
+
f"DataRecordResult but got {type(output)}.")
|
|
481
|
+
continue
|
|
482
|
+
config_dt: str = agent.output_configs[output_index].base_config.data_type_name
|
|
483
|
+
result_dt: str = output.data_type_name
|
|
484
|
+
if (not config_dt or config_dt == "Any") and not result_dt and output.new_records:
|
|
485
|
+
errors.append(f"Output {output_index} was not configured with an output data type, "
|
|
486
|
+
f"is creating new records, and did not specify an output data type name "
|
|
487
|
+
f"on the result-level. Data record results that create new records must "
|
|
488
|
+
f"either be configured with a data type name in the agent's init function, "
|
|
489
|
+
f"or provide a data type name on the result object.")
|
|
490
|
+
elif config_dt and config_dt != "Any":
|
|
491
|
+
if result_dt:
|
|
492
|
+
agent.log_warning(f"Output {output_index} was configured with an output data type "
|
|
493
|
+
f"and also specified a data type on the result-level. The result-level "
|
|
494
|
+
f"data type name will be ignored.")
|
|
495
|
+
for record in output.existing_records:
|
|
496
|
+
record_dt: str = record.data_type_name
|
|
497
|
+
if record_dt != config_dt:
|
|
498
|
+
errors.append(f"Output {output_index} expected records of data type "
|
|
499
|
+
f"\"{config_dt}\" but received \"{record_dt}\".\n{record}")
|
|
500
|
+
# Leaving record validation up to the platform. For now, at least. May re-implement agent
|
|
501
|
+
# server level record validation if we see a need for it.
|
|
502
|
+
# else:
|
|
503
|
+
# errors.extend(AgentServiceBase._validate_records_output(agent, config_dt, output))
|
|
504
|
+
case ContainerType.JSON:
|
|
505
|
+
if not isinstance(output, JsonResult):
|
|
506
|
+
errors.append(f"Mismatched output type at index {output_index}: expected "
|
|
507
|
+
f"JsonResult but got {type(output)}.")
|
|
508
|
+
case ContainerType.TEXT:
|
|
509
|
+
if not isinstance(output, TextResult):
|
|
510
|
+
errors.append(f"Mismatched output type at index {output_index}: expected "
|
|
511
|
+
f"TextResult but got {type(output)}.")
|
|
512
|
+
output_index += 1
|
|
513
|
+
|
|
514
|
+
# Run the agent's output validation, but only if no other errors were encountered.
|
|
515
|
+
if not errors:
|
|
516
|
+
errors = agent.validate_output()
|
|
517
|
+
return errors
|
|
518
|
+
|
|
519
|
+
@staticmethod
|
|
520
|
+
def _validate_records_output(agent: AgentBase, data_type_name: str, result: DataRecordResult) -> list[str]:
|
|
521
|
+
"""
|
|
522
|
+
UNUSED: Leaving record validation up to the platform.
|
|
523
|
+
"""
|
|
524
|
+
errors: list[str] = []
|
|
525
|
+
|
|
526
|
+
def value_field(fd: VeloxFieldDefPbo) -> bool:
|
|
527
|
+
valueless_types: list[FieldTypePbo] = [
|
|
528
|
+
FieldTypePbo.ACTION, FieldTypePbo.PARENTLINK, FieldTypePbo.IDENTIFIER, FieldTypePbo.LINK,
|
|
529
|
+
FieldTypePbo.MULTIPARENTLINK, FieldTypePbo.CHILDLINK, FieldTypePbo.AUTO_ACCESSION
|
|
530
|
+
]
|
|
531
|
+
return fd.data_field_type not in valueless_types
|
|
532
|
+
|
|
533
|
+
data_types: dict[str, AgentDataType] = {x.data_type_name: x for x in agent.define_data_types()}
|
|
534
|
+
data_type: AgentDataType = data_types.get(data_type_name)
|
|
535
|
+
if data_type:
|
|
536
|
+
field_defs: dict[str, VeloxFieldDefPbo] = {x.data_field_name: x for x in data_type.field_defs if value_field(x)}
|
|
537
|
+
|
|
538
|
+
for i, field_map in enumerate(result.new_records):
|
|
539
|
+
errors.extend(AgentServiceBase._validate_field_map(i, field_map, field_defs))
|
|
540
|
+
|
|
541
|
+
return errors
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def _validate_field_map(i: int, field_map: FieldMap, field_defs: dict[str, VeloxFieldDefPbo]) -> list[str]:
|
|
545
|
+
"""
|
|
546
|
+
UNUSED: Leaving record validation up to the platform.
|
|
547
|
+
"""
|
|
548
|
+
errors: list[str] = []
|
|
549
|
+
|
|
550
|
+
missing: set[str] = set(field_defs).difference(field_map.keys())
|
|
551
|
+
if missing:
|
|
552
|
+
errors.append(f"Entry {i} is missing field names specified by the AgentDataType: {missing}")
|
|
553
|
+
return errors
|
|
554
|
+
|
|
555
|
+
for field, field_def in field_defs.items():
|
|
556
|
+
value: Any = field_map[field]
|
|
557
|
+
if value is None:
|
|
558
|
+
if field_def.required:
|
|
559
|
+
errors.append(f"Entry {i} is missing required value for field {field}.")
|
|
560
|
+
continue
|
|
561
|
+
match field_def.data_field_type:
|
|
562
|
+
case FieldTypePbo.BOOLEAN:
|
|
563
|
+
if not isinstance(value, bool):
|
|
564
|
+
errors.append(f"Entry {i} field {field} expects a boolean but "
|
|
565
|
+
f"received {type(value)}.")
|
|
566
|
+
case FieldTypePbo.SHORT | FieldTypePbo.INTEGER | FieldTypePbo.LONG \
|
|
567
|
+
| FieldTypePbo.ENUM | FieldTypePbo.DATE | FieldTypePbo.SIDE_LINK:
|
|
568
|
+
if not isinstance(value, int):
|
|
569
|
+
errors.append(f"Entry {i} field {field} expects an integer but "
|
|
570
|
+
f"received {type(value)}.")
|
|
571
|
+
case FieldTypePbo.DOUBLE:
|
|
572
|
+
if not isinstance(value, (int, float)):
|
|
573
|
+
errors.append(f"Entry {i} field {field} expects a float or integer but "
|
|
574
|
+
f"received {type(value)}.")
|
|
575
|
+
case FieldTypePbo.DATE_RANGE:
|
|
576
|
+
if not isinstance(value, str):
|
|
577
|
+
errors.append(f"Entry {i} field {field} expects a string but "
|
|
578
|
+
f"received {type(value)}.")
|
|
579
|
+
else:
|
|
580
|
+
try:
|
|
581
|
+
DateRange.from_str(value)
|
|
582
|
+
except:
|
|
583
|
+
errors.append(f"Entry {i} field {field} expects a date range "
|
|
584
|
+
f"formatted string (<start timestamp>/<end timestamp>) "
|
|
585
|
+
f"but received \"{value}\".")
|
|
586
|
+
case FieldTypePbo.STRING | FieldTypePbo.SELECTION | FieldTypePbo.PICKLIST \
|
|
587
|
+
| FieldTypePbo.ACTION_STRING | FieldTypePbo.FILE_BLOB:
|
|
588
|
+
if not isinstance(value, str):
|
|
589
|
+
errors.append(f"Entry {i} field {field} expects a string but "
|
|
590
|
+
f"received {type(value)}.")
|
|
591
|
+
|
|
592
|
+
return errors
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class AgentMetaData:
|
|
596
|
+
"""
|
|
597
|
+
A class that allows agents to self-describe their metadata.
|
|
598
|
+
"""
|
|
599
|
+
name: str
|
|
600
|
+
description: str
|
|
601
|
+
category: str
|
|
602
|
+
citations: dict[str, str]
|
|
603
|
+
sub_category: str | None
|
|
604
|
+
icon: bytes | None
|
|
605
|
+
license_flag: str | None
|
|
606
|
+
|
|
607
|
+
def __init__(self, name: str, description: str, category: str, citations: dict[str, str],
|
|
608
|
+
sub_category: str | None = None, icon: bytes | None = None, license_flag: str | None = None):
|
|
609
|
+
"""
|
|
610
|
+
:param name: The display name of the agent. This should be unique across all agents in the service.
|
|
611
|
+
:param description: The description of the agent.
|
|
612
|
+
:param category: The category of the agent. This is used to group similar agents in the pipeline manager.
|
|
613
|
+
:param citations: Any citations or references for this agent, as a dictionary of citation name to URL.
|
|
614
|
+
:param sub_category: The sub-category of the agent. Agents with the same category and sub-category will appear
|
|
615
|
+
as one item in the list of agents in the pipeline manager. Clicking on this item will then expand to allow
|
|
616
|
+
the selection of which agent in the sub-category should be used. This is used to group multiple agents
|
|
617
|
+
from the same service into one item as not to clutter the list of agents, as some services may have a large
|
|
618
|
+
number of individual agents created for them.
|
|
619
|
+
:param icon: The icon to use for the agent. This will appear in the list of agents and on individual steps
|
|
620
|
+
in the pipeline manager.
|
|
621
|
+
:param license_flag: The license flag for this agent. The system must have this license in order to use this
|
|
622
|
+
agent. If None, the agent is not license locked.
|
|
623
|
+
"""
|
|
624
|
+
self.name = name
|
|
625
|
+
self.description = description
|
|
626
|
+
self.category = category
|
|
627
|
+
self.sub_category = sub_category
|
|
628
|
+
self.citations = citations
|
|
629
|
+
self.icon = icon
|
|
630
|
+
self.license_flag = license_flag
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class AgentDataType:
|
|
634
|
+
"""
|
|
635
|
+
A class that allows agents to self-describe the fields that will be present in the FieldMapResults of the agent.
|
|
636
|
+
"""
|
|
637
|
+
data_type_name: str
|
|
638
|
+
display_name: str
|
|
639
|
+
field_defs: list[VeloxFieldDefPbo]
|
|
640
|
+
icon: bytes | None
|
|
641
|
+
|
|
642
|
+
def __init__(self, data_type_name: str, display_name: str,
|
|
643
|
+
field_defs: list[AbstractVeloxFieldDefinition | VeloxFieldDefPbo],
|
|
644
|
+
icon: bytes | None = None):
|
|
645
|
+
"""
|
|
646
|
+
:param data_type_name: The name of the data type to use. The length of the data type name
|
|
647
|
+
cannot exceed 63 characters.
|
|
648
|
+
:param display_name: The display name of the data type to use.
|
|
649
|
+
:param field_defs: The fields to use for this data type. The length of the data field names
|
|
650
|
+
cannot exceed 63 characters.
|
|
651
|
+
:param icon: The icon to use for this data type.
|
|
652
|
+
"""
|
|
653
|
+
self.data_type_name = data_type_name
|
|
654
|
+
self.display_name = display_name
|
|
655
|
+
self.icon = icon
|
|
656
|
+
self.field_defs = []
|
|
657
|
+
for field_def in field_defs:
|
|
658
|
+
if isinstance(field_def, AbstractVeloxFieldDefinition):
|
|
659
|
+
self.field_defs.append(ProtobufUtils.field_def_to_pbo(field_def))
|
|
660
|
+
else:
|
|
661
|
+
self.field_defs.append(field_def)
|
|
662
|
+
|
|
663
|
+
if len(self.data_type_name) > 63:
|
|
664
|
+
raise Exception(f"Data type name \"{self.data_type_name}\" exceeds the limit of 63 characters.")
|
|
665
|
+
for field_def in self.field_defs:
|
|
666
|
+
if len(field_def.data_field_name) > 63:
|
|
667
|
+
raise Exception(f"Data field name \"{field_def.data_field_name}\" exceeds the limit of 63 characters.")
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class AgentBase(ABC):
|
|
671
|
+
"""
|
|
672
|
+
A base class for implementing an agent.
|
|
673
|
+
"""
|
|
674
|
+
input_configs: list[AgentInputDetailsPbo]
|
|
675
|
+
input_container_types: list[ContainerType]
|
|
676
|
+
output_configs: list[AgentOutputDetailsPbo]
|
|
677
|
+
output_container_types: list[ContainerType]
|
|
678
|
+
output_file_extensions: list[list[str]]
|
|
679
|
+
config_fields: list[VeloxFieldDefPbo]
|
|
680
|
+
|
|
681
|
+
logs: list[str]
|
|
682
|
+
logger: Logger
|
|
683
|
+
_verbose_logging: bool | None = None
|
|
684
|
+
|
|
685
|
+
_temp_data: TempFileHandler | None = None
|
|
686
|
+
|
|
687
|
+
_user: SapioUser | None = None
|
|
688
|
+
_request: ProcessStepRequestPbo | None = None
|
|
689
|
+
_context: ServicerContext | None = None
|
|
690
|
+
_debug_mode: bool | None = None
|
|
691
|
+
|
|
692
|
+
__is_setup: bool
|
|
693
|
+
|
|
694
|
+
@property
|
|
695
|
+
def verbose_logging(self) -> bool:
|
|
696
|
+
if not self.__is_setup:
|
|
697
|
+
raise Exception("Agent must be set up to respond to a request before accessing this property.")
|
|
698
|
+
return self._verbose_logging
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def temp_data(self) -> TempFileHandler:
|
|
702
|
+
if not self.__is_setup:
|
|
703
|
+
raise Exception("Agent must be set up to respond to a request before accessing this property.")
|
|
704
|
+
return self._temp_data
|
|
705
|
+
|
|
706
|
+
@property
|
|
707
|
+
def user(self) -> SapioUser:
|
|
708
|
+
if not self.__is_setup:
|
|
709
|
+
raise Exception("Agent must be set up to respond to a request before accessing this property.")
|
|
710
|
+
return self._user
|
|
711
|
+
|
|
712
|
+
@property
|
|
713
|
+
def request(self) -> ProcessStepRequestPbo:
|
|
714
|
+
if not self.__is_setup:
|
|
715
|
+
raise Exception("Agent must be set up to respond to a request before accessing this property.")
|
|
716
|
+
return self._request
|
|
717
|
+
|
|
718
|
+
@property
|
|
719
|
+
def context(self) -> ServicerContext:
|
|
720
|
+
if not self.__is_setup:
|
|
721
|
+
raise Exception("Agent must be set up to respond to a request before accessing this property.")
|
|
722
|
+
return self._context
|
|
723
|
+
|
|
724
|
+
@property
|
|
725
|
+
def debug_mode(self) -> bool:
|
|
726
|
+
if not self.__is_setup:
|
|
727
|
+
raise Exception("Agent must be set up to respond to a request before accessing this property.")
|
|
728
|
+
return self._debug_mode
|
|
729
|
+
|
|
730
|
+
@classmethod
|
|
731
|
+
@abstractmethod
|
|
732
|
+
def identifier(cls):
|
|
733
|
+
"""
|
|
734
|
+
:return: The unique identifier of the agent. This is used by the system to determine which agent should be
|
|
735
|
+
updated if an agent is re-imported. This should not be changed after the first time that an agent is
|
|
736
|
+
imported, otherwise a duplicate agent will be created.
|
|
737
|
+
"""
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
@classmethod
|
|
741
|
+
@abstractmethod
|
|
742
|
+
def agent_meta_data(cls) -> AgentMetaData:
|
|
743
|
+
"""
|
|
744
|
+
:return: The metadata information of this agent.
|
|
745
|
+
"""
|
|
746
|
+
pass
|
|
747
|
+
|
|
748
|
+
@classmethod
|
|
749
|
+
@abstractmethod
|
|
750
|
+
def define_data_types(cls) -> list[AgentDataType] | None:
|
|
751
|
+
"""
|
|
752
|
+
:return: The information of the data types of this agent, if applicable. When this agent is first
|
|
753
|
+
imported, these details will be used to create new data types in the system. Note that further
|
|
754
|
+
customization may be made to the data type after it has been imported that can cause it to differ
|
|
755
|
+
from this definition.
|
|
756
|
+
"""
|
|
757
|
+
pass
|
|
758
|
+
|
|
759
|
+
def __init__(self):
|
|
760
|
+
self.__is_setup = False
|
|
761
|
+
self.input_configs = []
|
|
762
|
+
self.input_container_types = []
|
|
763
|
+
self.output_configs = []
|
|
764
|
+
self.output_container_types = []
|
|
765
|
+
self.output_file_extensions = []
|
|
766
|
+
self.config_fields = []
|
|
767
|
+
self.logs = []
|
|
768
|
+
self.logger = logging.getLogger(f"AgentBase.{self.__class__.__name__}")
|
|
769
|
+
ensure_logger_initialized(self.logger)
|
|
770
|
+
|
|
771
|
+
def __str__(self):
|
|
772
|
+
return f"{self.agent_meta_data().name} ({self.identifier()})"
|
|
773
|
+
|
|
774
|
+
def __hash__(self):
|
|
775
|
+
return hash(self.identifier())
|
|
776
|
+
|
|
777
|
+
def to_pbo(self) -> AgentDetailsPbo:
|
|
778
|
+
"""
|
|
779
|
+
:return: The AgentDetailsPbo proto object representing this agent.
|
|
780
|
+
"""
|
|
781
|
+
metadata: AgentMetaData = self.agent_meta_data()
|
|
782
|
+
# TODO: Support multiple data types.
|
|
783
|
+
data_types: list[AgentDataType] | None = self.define_data_types()
|
|
784
|
+
data_type: AgentDataType | None = data_types[0] if data_types else None
|
|
785
|
+
return AgentDetailsPbo(
|
|
786
|
+
import_id=self.identifier(),
|
|
787
|
+
name=metadata.name,
|
|
788
|
+
description=metadata.description,
|
|
789
|
+
category=metadata.category,
|
|
790
|
+
sub_category=metadata.sub_category,
|
|
791
|
+
icon=metadata.icon,
|
|
792
|
+
citation=[AgentCitationPbo(title=x, url=y) for x, y in metadata.citations.items()],
|
|
793
|
+
license_info=metadata.license_flag,
|
|
794
|
+
config_fields=self.config_fields,
|
|
795
|
+
input_configs=self.input_configs,
|
|
796
|
+
output_configs=self.output_configs,
|
|
797
|
+
output_data_type_name=data_type.data_type_name if data_type else None,
|
|
798
|
+
output_data_type_display_name=data_type.display_name if data_type else None,
|
|
799
|
+
output_type_fields=data_type.field_defs if data_type else None,
|
|
800
|
+
output_data_type_icon=data_type.icon if data_type else None,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
def setup(self, user: SapioUser, request: ProcessStepRequestPbo, context: ServicerContext, debug_mode: bool) -> None:
|
|
804
|
+
"""
|
|
805
|
+
Setup the agent with the user, request, and context. This method can be overridden by subclasses to perform
|
|
806
|
+
additional setup.
|
|
807
|
+
|
|
808
|
+
:param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
|
|
809
|
+
system.
|
|
810
|
+
:param request: The request object containing the input data.
|
|
811
|
+
:param context: The gRPC context.
|
|
812
|
+
:param debug_mode: If true, the agent should run in debug mode, providing additional logging and not cleaning
|
|
813
|
+
up temporary files.
|
|
814
|
+
"""
|
|
815
|
+
self.__is_setup = True
|
|
816
|
+
self._user = user
|
|
817
|
+
self._request = request
|
|
818
|
+
self._context = context
|
|
819
|
+
self._verbose_logging = request.verbose_logging
|
|
820
|
+
self._debug_mode = debug_mode
|
|
821
|
+
self._temp_data = TempFileHandler()
|
|
822
|
+
|
|
823
|
+
@staticmethod
|
|
824
|
+
def _parse_jsonl(example: str) -> list[dict[str, Any]]:
|
|
825
|
+
"""
|
|
826
|
+
Given a testing example for a JSON output, parse the JSONL into plain JSON.
|
|
827
|
+
"""
|
|
828
|
+
# Use a JSON decoder to find all valid JSON strings within the testing example.
|
|
829
|
+
decoder = json.JSONDecoder()
|
|
830
|
+
idx: int = 0
|
|
831
|
+
json_strings: list[str] = []
|
|
832
|
+
while idx < len(example):
|
|
833
|
+
try:
|
|
834
|
+
# Find the next valid JSON object in the example.
|
|
835
|
+
obj, end = decoder.raw_decode(example, idx)
|
|
836
|
+
# Extract the exact substring corresponding to this JSON object
|
|
837
|
+
json_strings.append(example[idx:end].strip())
|
|
838
|
+
idx = end
|
|
839
|
+
except json.JSONDecodeError:
|
|
840
|
+
# Skip invalid character and keep searching
|
|
841
|
+
idx += 1
|
|
842
|
+
# If no valid JSON was encountered, then a bad example was given.
|
|
843
|
+
if not json_strings:
|
|
844
|
+
raise Exception("No valid JSON encountered in testing example.")
|
|
845
|
+
|
|
846
|
+
# At this point, each line is its own top-level JSON object. Verify that every line is a dictionary.
|
|
847
|
+
# For lines that are dictionaries, parse them as JSON.
|
|
848
|
+
data: list[dict[str, Any]] = []
|
|
849
|
+
for line in json_strings:
|
|
850
|
+
line = line.strip()
|
|
851
|
+
if not line.startswith("{") and not line.endswith("}"):
|
|
852
|
+
raise Exception("Testing examples must be JSON dictionaries in the JSONL format. "
|
|
853
|
+
"(The top-level object may not be a list.)")
|
|
854
|
+
data.append(json.loads(line))
|
|
855
|
+
return data
|
|
856
|
+
|
|
857
|
+
@staticmethod
|
|
858
|
+
def get_file_string(path: str) -> str | None:
|
|
859
|
+
"""
|
|
860
|
+
:param path: The path of the file to read.
|
|
861
|
+
:return: The file contents as a string, or None if the file does not exist.
|
|
862
|
+
"""
|
|
863
|
+
if not os.path.exists(path):
|
|
864
|
+
return None
|
|
865
|
+
with open(path, "r") as f:
|
|
866
|
+
return f.read()
|
|
867
|
+
|
|
868
|
+
@staticmethod
|
|
869
|
+
def get_file_bytes(path: str) -> bytes | None:
|
|
870
|
+
"""
|
|
871
|
+
:param path: The path of the file to read.
|
|
872
|
+
:return: The file contents as bytes, or None if the file does not exist.
|
|
873
|
+
"""
|
|
874
|
+
if not os.path.exists(path):
|
|
875
|
+
return None
|
|
876
|
+
with open(path, "rb") as f:
|
|
877
|
+
return f.read()
|
|
878
|
+
|
|
879
|
+
@staticmethod
|
|
880
|
+
def _build_example_container(example: str | bytes | None) -> ExampleContainerPbo | None:
|
|
881
|
+
"""
|
|
882
|
+
Create an example container from the given example data.
|
|
883
|
+
"""
|
|
884
|
+
if example is None:
|
|
885
|
+
return None
|
|
886
|
+
if isinstance(example, str):
|
|
887
|
+
return ExampleContainerPbo(text_example=example)
|
|
888
|
+
if isinstance(example, bytes):
|
|
889
|
+
return ExampleContainerPbo(binary_example=example)
|
|
890
|
+
raise ValueError(f"Unexpected example type: {type(example)}")
|
|
891
|
+
|
|
892
|
+
@staticmethod
|
|
893
|
+
def _data_record_content_type(data_type_name: str | None) -> str:
|
|
894
|
+
data_type: str = data_type_name.strip() if data_type_name else "Any"
|
|
895
|
+
return f"{data_type} DataRecord"
|
|
896
|
+
|
|
897
|
+
def add_binary_input(self, content_type: str, display_name: str, description: str,
|
|
898
|
+
validation: str | None = None, input_count: tuple[int, int] | None = None,
|
|
899
|
+
is_paged: bool = False, page_size: tuple[int, int] | None = None,
|
|
900
|
+
max_request_bytes: int | None = None) -> None:
|
|
901
|
+
"""
|
|
902
|
+
Add a binary input configuration to the agent. This determines how many inputs this agent will accept in the
|
|
903
|
+
plan manager, as well as what those inputs are. The IO number of the input will be set to the current number of
|
|
904
|
+
inputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time it
|
|
905
|
+
is called, the IO number will be 1, and so on.
|
|
906
|
+
|
|
907
|
+
:param content_type: The content type of the input.
|
|
908
|
+
:param display_name: The display name of the input.
|
|
909
|
+
:param description: The description of the input.
|
|
910
|
+
:param validation: An optional validation string for the input.
|
|
911
|
+
:param input_count: A tuple of the minimum and maximum number of inputs allowed for this agent.
|
|
912
|
+
:param is_paged: If true, this input will be paged. If false, this input will not be paged.
|
|
913
|
+
:param page_size: A tuple of the minimum and maximum page size for this agent. The input must be paged in order
|
|
914
|
+
for this to have an effect.
|
|
915
|
+
:param max_request_bytes: The maximum request size in bytes for this agent.
|
|
916
|
+
"""
|
|
917
|
+
base_config = AgentIoConfigBasePbo(
|
|
918
|
+
io_number=len(self.input_configs),
|
|
919
|
+
content_type=content_type,
|
|
920
|
+
display_name=display_name,
|
|
921
|
+
description=description,
|
|
922
|
+
structure_example=None,
|
|
923
|
+
# The testing example on the input is never used, hence why it can't be set by this function.
|
|
924
|
+
# The testing example is only used during dry runs, in which the testing_example of the output
|
|
925
|
+
# of the previous step is what gets passed to the next step's input validation.
|
|
926
|
+
testing_example=None
|
|
927
|
+
)
|
|
928
|
+
self._add_input_config(ContainerType.BINARY, base_config, validation, input_count, is_paged, page_size,
|
|
929
|
+
max_request_bytes)
|
|
930
|
+
|
|
931
|
+
def add_csv_input(self, content_type: str, display_name: str, description: str,
|
|
932
|
+
structure_example: str | bytes, validation: str | None = None,
|
|
933
|
+
input_count: tuple[int, int] | None = None, is_paged: bool = False,
|
|
934
|
+
page_size: tuple[int, int] | None = None, max_request_bytes: int | None = None) -> None:
|
|
935
|
+
"""
|
|
936
|
+
Add a CSV input configuration to the agent. This determines how many inputs this agent will accept in the
|
|
937
|
+
plan manager, as well as what those inputs are. The IO number of the input will be set to the current number of
|
|
938
|
+
inputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time it
|
|
939
|
+
is called, the IO number will be 1, and so on.
|
|
940
|
+
|
|
941
|
+
:param content_type: The content type of the input.
|
|
942
|
+
:param display_name: The display name of the input.
|
|
943
|
+
:param description: The description of the input.
|
|
944
|
+
:param structure_example: An example of the structure of the input. This does not need to be an entirely valid
|
|
945
|
+
example, and should often be truncated for brevity.
|
|
946
|
+
:param validation: An optional validation string for the input.
|
|
947
|
+
:param input_count: A tuple of the minimum and maximum number of inputs allowed for this agent.
|
|
948
|
+
:param is_paged: If true, this input will be paged. If false, this input will not be paged.
|
|
949
|
+
:param page_size: A tuple of the minimum and maximum page size for this agent. The input must be paged in order
|
|
950
|
+
for this to have an effect.
|
|
951
|
+
:param max_request_bytes: The maximum request size in bytes for this agent.
|
|
952
|
+
"""
|
|
953
|
+
base_config = AgentIoConfigBasePbo(
|
|
954
|
+
io_number=len(self.input_configs),
|
|
955
|
+
content_type=content_type,
|
|
956
|
+
display_name=display_name,
|
|
957
|
+
description=description,
|
|
958
|
+
structure_example=self._build_example_container(structure_example),
|
|
959
|
+
testing_example=None
|
|
960
|
+
)
|
|
961
|
+
self._add_input_config(ContainerType.CSV, base_config, validation, input_count, is_paged, page_size,
|
|
962
|
+
max_request_bytes)
|
|
963
|
+
|
|
964
|
+
def add_data_record_input(self, data_type_name: str | None, display_name: str, description: str,
|
|
965
|
+
structure_example: str | bytes, validation: str | None = None,
|
|
966
|
+
input_count: tuple[int, int] | None = None, is_paged: bool = False,
|
|
967
|
+
page_size: tuple[int, int] | None = None, max_request_bytes: int | None = None) -> None:
|
|
968
|
+
"""
|
|
969
|
+
Add a data record input configuration to the agent. This determines how many inputs this agent will accept in
|
|
970
|
+
the plan manager, as well as what those inputs are. The IO number of the input will be set to the current number
|
|
971
|
+
of inputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time
|
|
972
|
+
it is called, the IO number will be 1, and so on.
|
|
973
|
+
|
|
974
|
+
:param data_type_name: The data type name for the data records input. If None, then any data record can be
|
|
975
|
+
provided as input.
|
|
976
|
+
:param display_name: The display name of the input.
|
|
977
|
+
:param description: The description of the input.
|
|
978
|
+
:param structure_example: An example of the structure of the input. This does not need to be an entirely valid
|
|
979
|
+
example, and should often be truncated for brevity.
|
|
980
|
+
:param validation: An optional validation string for the input.
|
|
981
|
+
:param input_count: A tuple of the minimum and maximum number of inputs allowed for this agent.
|
|
982
|
+
:param is_paged: If true, this input will be paged. If false, this input will not be paged.
|
|
983
|
+
:param page_size: A tuple of the minimum and maximum page size for this agent. The input must be paged in order
|
|
984
|
+
for this to have an effect.
|
|
985
|
+
:param max_request_bytes: The maximum request size in bytes for this agent.
|
|
986
|
+
"""
|
|
987
|
+
if data_type_name:
|
|
988
|
+
data_type_name = data_type_name.strip()
|
|
989
|
+
base_config = AgentIoConfigBasePbo(
|
|
990
|
+
io_number=len(self.input_configs),
|
|
991
|
+
content_type=self._data_record_content_type(data_type_name),
|
|
992
|
+
data_type_name=data_type_name,
|
|
993
|
+
display_name=display_name,
|
|
994
|
+
description=description,
|
|
995
|
+
structure_example=self._build_example_container(structure_example),
|
|
996
|
+
testing_example=None
|
|
997
|
+
)
|
|
998
|
+
self._add_input_config(ContainerType.DATA_RECORDS, base_config, validation, input_count, is_paged, page_size,
|
|
999
|
+
max_request_bytes)
|
|
1000
|
+
|
|
1001
|
+
def add_json_input(self, content_type: str, display_name: str, description: str,
|
|
1002
|
+
structure_example: str | bytes, validation: str | None = None,
|
|
1003
|
+
input_count: tuple[int, int] | None = None, is_paged: bool = False,
|
|
1004
|
+
page_size: tuple[int, int] | None = None, max_request_bytes: int | None = None) -> None:
|
|
1005
|
+
"""
|
|
1006
|
+
Add a JSON input configuration to the agent. This determines how many inputs this agent will accept in the
|
|
1007
|
+
plan manager, as well as what those inputs are. The IO number of the input will be set to the current number of
|
|
1008
|
+
inputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time it
|
|
1009
|
+
is called, the IO number will be 1, and so on.
|
|
1010
|
+
|
|
1011
|
+
:param content_type: The content type of the input.
|
|
1012
|
+
:param display_name: The display name of the input.
|
|
1013
|
+
:param description: The description of the input.
|
|
1014
|
+
:param structure_example: An example of the structure of the input. This does not need to be an entirely valid
|
|
1015
|
+
example, and should often be truncated for brevity.
|
|
1016
|
+
:param validation: An optional validation string for the input.
|
|
1017
|
+
:param input_count: A tuple of the minimum and maximum number of inputs allowed for this agent.
|
|
1018
|
+
:param is_paged: If true, this input will be paged. If false, this input will not be paged.
|
|
1019
|
+
:param page_size: A tuple of the minimum and maximum page size for this agent. The input must be paged in order
|
|
1020
|
+
for this to have an effect.
|
|
1021
|
+
:param max_request_bytes: The maximum request size in bytes for this agent.
|
|
1022
|
+
"""
|
|
1023
|
+
base_config = AgentIoConfigBasePbo(
|
|
1024
|
+
io_number=len(self.input_configs),
|
|
1025
|
+
content_type=content_type,
|
|
1026
|
+
display_name=display_name,
|
|
1027
|
+
description=description,
|
|
1028
|
+
structure_example=self._build_example_container(structure_example),
|
|
1029
|
+
testing_example=None
|
|
1030
|
+
)
|
|
1031
|
+
self._add_input_config(ContainerType.JSON, base_config, validation, input_count, is_paged, page_size,
|
|
1032
|
+
max_request_bytes)
|
|
1033
|
+
|
|
1034
|
+
def add_text_input(self, content_type: str, display_name: str, description: str,
|
|
1035
|
+
structure_example: str | bytes, validation: str | None = None,
|
|
1036
|
+
input_count: tuple[int, int] | None = None, is_paged: bool = False,
|
|
1037
|
+
page_size: tuple[int, int] | None = None, max_request_bytes: int | None = None) -> None:
|
|
1038
|
+
"""
|
|
1039
|
+
Add a text input configuration to the agent. This determines how many inputs this agent will accept in the
|
|
1040
|
+
plan manager, as well as what those inputs are. The IO number of the input will be set to the current number of
|
|
1041
|
+
inputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time it
|
|
1042
|
+
is called, the IO number will be 1, and so on.
|
|
1043
|
+
|
|
1044
|
+
:param content_type: The content type of the input.
|
|
1045
|
+
:param display_name: The display name of the input.
|
|
1046
|
+
:param description: The description of the input.
|
|
1047
|
+
:param structure_example: An example of the structure of the input. This does not need to be an entirely valid
|
|
1048
|
+
example, and should often be truncated for brevity.
|
|
1049
|
+
:param validation: An optional validation string for the input.
|
|
1050
|
+
:param input_count: A tuple of the minimum and maximum number of inputs allowed for this agent.
|
|
1051
|
+
:param is_paged: If true, this input will be paged. If false, this input will not be paged.
|
|
1052
|
+
:param page_size: A tuple of the minimum and maximum page size for this agent. The input must be paged in order
|
|
1053
|
+
for this to have an effect.
|
|
1054
|
+
:param max_request_bytes: The maximum request size in bytes for this agent.
|
|
1055
|
+
"""
|
|
1056
|
+
base_config = AgentIoConfigBasePbo(
|
|
1057
|
+
io_number=len(self.input_configs),
|
|
1058
|
+
content_type=content_type,
|
|
1059
|
+
display_name=display_name,
|
|
1060
|
+
description=description,
|
|
1061
|
+
structure_example=self._build_example_container(structure_example),
|
|
1062
|
+
testing_example=None
|
|
1063
|
+
)
|
|
1064
|
+
self._add_input_config(ContainerType.TEXT, base_config, validation, input_count, is_paged, page_size,
|
|
1065
|
+
max_request_bytes)
|
|
1066
|
+
|
|
1067
|
+
def _add_input_config(self, container_type: ContainerType, base_config: AgentIoConfigBasePbo,
|
|
1068
|
+
validation: str | None = None, input_count: tuple[int, int] | None = None,
|
|
1069
|
+
is_paged: bool = False, page_size: tuple[int, int] | None = None,
|
|
1070
|
+
max_request_bytes: int | None = None) -> None:
|
|
1071
|
+
"""
|
|
1072
|
+
Add an input configuration to the agent.
|
|
1073
|
+
"""
|
|
1074
|
+
self.input_configs.append(AgentInputDetailsPbo(
|
|
1075
|
+
base_config=base_config,
|
|
1076
|
+
validation=validation,
|
|
1077
|
+
min_input_count=input_count[0] if input_count else None,
|
|
1078
|
+
max_input_count=input_count[1] if input_count else None,
|
|
1079
|
+
paged=is_paged,
|
|
1080
|
+
min_page_size=page_size[0] if page_size else None,
|
|
1081
|
+
max_page_size=page_size[1] if page_size else None,
|
|
1082
|
+
max_request_bytes=max_request_bytes,
|
|
1083
|
+
))
|
|
1084
|
+
self.input_container_types.append(container_type)
|
|
1085
|
+
|
|
1086
|
+
def add_binary_output(self, content_type: str, display_name: str, description: str,
|
|
1087
|
+
testing_example: str | bytes, file_extensions: list[str] | None = None) -> None:
|
|
1088
|
+
"""
|
|
1089
|
+
Add a binary output configuration to the agent. This determines how many outputs this agent will produce in the
|
|
1090
|
+
plan manager, as well as what those outputs are. The IO number of the output will be set to the current number
|
|
1091
|
+
of outputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time
|
|
1092
|
+
it is called, the IO number will be 1, and so on.
|
|
1093
|
+
|
|
1094
|
+
:param content_type: The content type of the output.
|
|
1095
|
+
:param display_name: The display name of the output.
|
|
1096
|
+
:param description: The description of the output.
|
|
1097
|
+
:param testing_example: An example of the input to be used when testing this agent in the system. This must be
|
|
1098
|
+
an entirely valid example of what an output of this agent could look like so that it can be properly used
|
|
1099
|
+
to run tests with. The provided example may be a string, such as for representing JSON or CSV outputs,
|
|
1100
|
+
or bytes, such as for representing binary outputs like images or files.
|
|
1101
|
+
:param file_extensions: A list of file extensions that this binary output can be saved as.
|
|
1102
|
+
"""
|
|
1103
|
+
base_config = AgentIoConfigBasePbo(
|
|
1104
|
+
io_number=len(self.output_configs),
|
|
1105
|
+
content_type=content_type,
|
|
1106
|
+
display_name=display_name,
|
|
1107
|
+
description=description,
|
|
1108
|
+
structure_example=None,
|
|
1109
|
+
testing_example=self._build_example_container(testing_example)
|
|
1110
|
+
)
|
|
1111
|
+
self._add_output_config(ContainerType.BINARY, base_config, file_extensions=file_extensions)
|
|
1112
|
+
|
|
1113
|
+
def add_csv_output(self, content_type: str, display_name: str, description: str,
|
|
1114
|
+
structure_example: str | bytes, testing_example: str | bytes) -> None:
|
|
1115
|
+
"""
|
|
1116
|
+
Add a CSV output configuration to the agent. This determines how many outputs this agent will produce in the
|
|
1117
|
+
plan manager, as well as what those outputs are. The IO number of the output will be set to the current number
|
|
1118
|
+
of outputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time
|
|
1119
|
+
it is called, the IO number will be 1, and so on.
|
|
1120
|
+
|
|
1121
|
+
:param content_type: The content type of the output.
|
|
1122
|
+
:param display_name: The display name of the output.
|
|
1123
|
+
:param description: The description of the output.
|
|
1124
|
+
:param structure_example: An optional example of the structure of the input. This does not need to be an
|
|
1125
|
+
entirely valid example, and should often be truncated for brevity.
|
|
1126
|
+
:param testing_example: An example of the input to be used when testing this agent in the system. This must be
|
|
1127
|
+
an entirely valid example of what an output of this agent could look like so that it can be properly used
|
|
1128
|
+
to run tests with. The provided example may be a string, such as for representing JSON or CSV outputs,
|
|
1129
|
+
or bytes, such as for representing binary outputs like images or files.
|
|
1130
|
+
"""
|
|
1131
|
+
base_config = AgentIoConfigBasePbo(
|
|
1132
|
+
io_number=len(self.output_configs),
|
|
1133
|
+
content_type=content_type,
|
|
1134
|
+
display_name=display_name,
|
|
1135
|
+
description=description,
|
|
1136
|
+
structure_example=self._build_example_container(structure_example),
|
|
1137
|
+
testing_example=self._build_example_container(testing_example)
|
|
1138
|
+
)
|
|
1139
|
+
self._add_output_config(ContainerType.CSV, base_config, file_extensions=[".csv"])
|
|
1140
|
+
|
|
1141
|
+
def add_data_record_output(self, data_type_name: str | None, display_name: str, description: str,
|
|
1142
|
+
structure_example: str | bytes, testing_example: str | bytes) -> None:
|
|
1143
|
+
"""
|
|
1144
|
+
Add a data record output configuration to the agent. This determines how many outputs this agent will produce in
|
|
1145
|
+
the plan manager, as well as what those outputs are. The IO number of the output will be set to the current
|
|
1146
|
+
number of outputs. That is, the first time an add_X_input function is called, the IO number will be 0, the
|
|
1147
|
+
second time it is called, the IO number will be 1, and so on.
|
|
1148
|
+
|
|
1149
|
+
:param data_type_name: The data type name for the data record output. If None, then any data record may be
|
|
1150
|
+
provided as input.
|
|
1151
|
+
:param display_name: The display name of the output.
|
|
1152
|
+
:param description: The description of the output.
|
|
1153
|
+
:param structure_example: An optional example of the structure of the input. This does not need to be an
|
|
1154
|
+
entirely valid example, and should often be truncated for brevity.
|
|
1155
|
+
:param testing_example: An example of the input to be used when testing this agent in the system. This must be
|
|
1156
|
+
an entirely valid example of what an output of this agent could look like so that it can be properly used
|
|
1157
|
+
to run tests with. The provided example may be a string, such as for representing JSON or CSV outputs,
|
|
1158
|
+
or bytes, such as for representing binary outputs like images or files.
|
|
1159
|
+
"""
|
|
1160
|
+
if data_type_name:
|
|
1161
|
+
data_type_name = data_type_name.strip()
|
|
1162
|
+
# See add_json_output for details on parsing testing examples.
|
|
1163
|
+
self._parse_jsonl(testing_example)
|
|
1164
|
+
base_config = AgentIoConfigBasePbo(
|
|
1165
|
+
io_number=len(self.output_configs),
|
|
1166
|
+
content_type=self._data_record_content_type(data_type_name),
|
|
1167
|
+
data_type_name=data_type_name,
|
|
1168
|
+
display_name=display_name,
|
|
1169
|
+
description=description,
|
|
1170
|
+
structure_example=self._build_example_container(structure_example),
|
|
1171
|
+
testing_example=self._build_example_container(testing_example)
|
|
1172
|
+
)
|
|
1173
|
+
self._add_output_config(ContainerType.DATA_RECORDS, base_config, file_extensions=[])
|
|
1174
|
+
|
|
1175
|
+
def add_json_output(self, content_type: str, display_name: str, description: str,
|
|
1176
|
+
structure_example: str | bytes, testing_example: str | bytes) -> None:
|
|
1177
|
+
"""
|
|
1178
|
+
Add a JSON output configuration to the agent. This determines how many outputs this agent will produce in the
|
|
1179
|
+
plan manager, as well as what those outputs are. The IO number of the output will be set to the current number
|
|
1180
|
+
of outputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time
|
|
1181
|
+
it is called, the IO number will be 1, and so on.
|
|
1182
|
+
|
|
1183
|
+
:param content_type: The content type of the output.
|
|
1184
|
+
:param display_name: The display name of the output.
|
|
1185
|
+
:param description: The description of the output.
|
|
1186
|
+
:param structure_example: An optional example of the structure of the input. This does not need to be an
|
|
1187
|
+
entirely valid example, and should often be truncated for brevity.
|
|
1188
|
+
:param testing_example: An example of the input to be used when testing this agent in the system. This must be
|
|
1189
|
+
an entirely valid example of what an output of this agent could look like so that it can be properly used
|
|
1190
|
+
to run tests with. The provided example may be a string, such as for representing JSON or CSV outputs,
|
|
1191
|
+
or bytes, such as for representing binary outputs like images or files.
|
|
1192
|
+
"""
|
|
1193
|
+
# The JSON testing example MUST be in the JSONL format with top-level objects being dictionaries.
|
|
1194
|
+
# Attempt to parse it as JSONL. An exception will be thrown if this fails.
|
|
1195
|
+
self._parse_jsonl(testing_example)
|
|
1196
|
+
base_config = AgentIoConfigBasePbo(
|
|
1197
|
+
io_number=len(self.output_configs),
|
|
1198
|
+
content_type=content_type,
|
|
1199
|
+
display_name=display_name,
|
|
1200
|
+
description=description,
|
|
1201
|
+
structure_example=self._build_example_container(structure_example),
|
|
1202
|
+
testing_example=self._build_example_container(testing_example)
|
|
1203
|
+
)
|
|
1204
|
+
self._add_output_config(ContainerType.JSON, base_config, file_extensions=[".json"])
|
|
1205
|
+
|
|
1206
|
+
def add_text_output(self, content_type: str, display_name: str, description: str,
|
|
1207
|
+
structure_example: str | bytes, testing_example: str | bytes) -> None:
|
|
1208
|
+
"""
|
|
1209
|
+
Add a text output configuration to the agent. This determines how many outputs this agent will produce in the
|
|
1210
|
+
plan manager, as well as what those outputs are. The IO number of the output will be set to the current number
|
|
1211
|
+
of outputs. That is, the first time an add_X_input function is called, the IO number will be 0, the second time
|
|
1212
|
+
it is called, the IO number will be 1, and so on.
|
|
1213
|
+
|
|
1214
|
+
:param content_type: The content type of the output.
|
|
1215
|
+
:param display_name: The display name of the output.
|
|
1216
|
+
:param description: The description of the output.
|
|
1217
|
+
:param structure_example: An optional example of the structure of the input. This does not need to be an
|
|
1218
|
+
entirely valid example, and should often be truncated for brevity.
|
|
1219
|
+
:param testing_example: An example of the input to be used when testing this agent in the system. This must be
|
|
1220
|
+
an entirely valid example of what an output of this agent could look like so that it can be properly used
|
|
1221
|
+
to run tests with. The provided example may be a string, such as for representing JSON or CSV outputs,
|
|
1222
|
+
or bytes, such as for representing binary outputs like images or files.
|
|
1223
|
+
"""
|
|
1224
|
+
base_config = AgentIoConfigBasePbo(
|
|
1225
|
+
io_number=len(self.output_configs),
|
|
1226
|
+
content_type=content_type,
|
|
1227
|
+
display_name=display_name,
|
|
1228
|
+
description=description,
|
|
1229
|
+
structure_example=self._build_example_container(structure_example),
|
|
1230
|
+
testing_example=self._build_example_container(testing_example)
|
|
1231
|
+
)
|
|
1232
|
+
self._add_output_config(ContainerType.TEXT, base_config, file_extensions=[".txt"])
|
|
1233
|
+
|
|
1234
|
+
def _add_output_config(self, container_type: ContainerType, base_config: AgentIoConfigBasePbo,
|
|
1235
|
+
file_extensions: list[str] | None = None) -> None:
|
|
1236
|
+
"""
|
|
1237
|
+
Add an output configuration to the agent for the given container type.
|
|
1238
|
+
"""
|
|
1239
|
+
self.output_configs.append(AgentOutputDetailsPbo(base_config=base_config))
|
|
1240
|
+
self.output_container_types.append(container_type)
|
|
1241
|
+
self.output_file_extensions.append(file_extensions if file_extensions else [])
|
|
1242
|
+
|
|
1243
|
+
def add_config_field(self, field: VeloxFieldDefPbo) -> None:
|
|
1244
|
+
"""
|
|
1245
|
+
Add a configuration field to the agent. This field will be used to configure the agent in the plan manager.
|
|
1246
|
+
|
|
1247
|
+
:param field: The configuration field details.
|
|
1248
|
+
"""
|
|
1249
|
+
self.config_fields.append(field)
|
|
1250
|
+
|
|
1251
|
+
def add_config_field_def(self, field: AbstractVeloxFieldDefinition) -> None:
|
|
1252
|
+
"""
|
|
1253
|
+
Add a configuration field to the agent. This field will be used to configure the agent in the plan manager.
|
|
1254
|
+
|
|
1255
|
+
:param field: The configuration field details.
|
|
1256
|
+
"""
|
|
1257
|
+
self.config_fields.append(ProtobufUtils.field_def_to_pbo(field))
|
|
1258
|
+
|
|
1259
|
+
def add_boolean_config_field(self, field_name: str, display_name: str, description: str,
|
|
1260
|
+
default_value: bool | None = None, optional: bool = False,
|
|
1261
|
+
dependencies: dict[bool, list[str]] | None = None,
|
|
1262
|
+
is_hide_disabled_fields: bool = False) -> None:
|
|
1263
|
+
"""
|
|
1264
|
+
Add a boolean configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1265
|
+
manager.
|
|
1266
|
+
|
|
1267
|
+
:param field_name: The name of the field.
|
|
1268
|
+
:param display_name: The display name of the field.
|
|
1269
|
+
:param description: The description of the field.
|
|
1270
|
+
:param default_value: The default value of the field.
|
|
1271
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1272
|
+
:param dependencies: A dictionary of field dependencies. The value of the dictionary is a possible value of
|
|
1273
|
+
this field, and the key is a list of field names for other config fields of this agent that will be
|
|
1274
|
+
disabled if the config field matches the corresponding value.
|
|
1275
|
+
:param is_hide_disabled_fields: If true, fields disabled by a field dependency will be hidden. If false, the
|
|
1276
|
+
dependent fields will be visible, but uneditable.
|
|
1277
|
+
"""
|
|
1278
|
+
dependent_fields: list[BooleanDependentFieldEntryPbo] | None = None
|
|
1279
|
+
if dependencies:
|
|
1280
|
+
dependent_fields = []
|
|
1281
|
+
for key, values in dependencies.items():
|
|
1282
|
+
dependent_fields.append(BooleanDependentFieldEntryPbo(key=key, dependent_field_names=values))
|
|
1283
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1284
|
+
data_field_type=FieldTypePbo.BOOLEAN,
|
|
1285
|
+
data_field_name=field_name,
|
|
1286
|
+
display_name=display_name,
|
|
1287
|
+
description=description,
|
|
1288
|
+
required=not optional,
|
|
1289
|
+
editable=True,
|
|
1290
|
+
boolean_properties=BooleanPropertiesPbo(
|
|
1291
|
+
default_value=default_value,
|
|
1292
|
+
dependent_fields=dependent_fields,
|
|
1293
|
+
is_hide_disabled_fields=is_hide_disabled_fields
|
|
1294
|
+
)
|
|
1295
|
+
))
|
|
1296
|
+
|
|
1297
|
+
def add_double_config_field(self, field_name: str, display_name: str, description: str,
|
|
1298
|
+
default_value: float | None = None, min_value: float = -10.**120,
|
|
1299
|
+
max_value: float = 10.**120, precision: int = 2, optional: bool = False,
|
|
1300
|
+
double_format: SapioDoubleFormat | None = None) -> None:
|
|
1301
|
+
"""
|
|
1302
|
+
Add a double configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1303
|
+
manager.
|
|
1304
|
+
|
|
1305
|
+
:param field_name: The name of the field.
|
|
1306
|
+
:param display_name: The display name of the field.
|
|
1307
|
+
:param description: The description of the field.
|
|
1308
|
+
:param default_value: The default value of the field.
|
|
1309
|
+
:param min_value: The minimum value of the field.
|
|
1310
|
+
:param max_value: The maximum value of the field.
|
|
1311
|
+
:param precision: The precision of the field.
|
|
1312
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1313
|
+
:param double_format: The format of the field.
|
|
1314
|
+
"""
|
|
1315
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1316
|
+
data_field_type=FieldTypePbo.DOUBLE,
|
|
1317
|
+
data_field_name=field_name,
|
|
1318
|
+
display_name=display_name,
|
|
1319
|
+
description=description,
|
|
1320
|
+
required=not optional,
|
|
1321
|
+
editable=True,
|
|
1322
|
+
double_properties=DoublePropertiesPbo(
|
|
1323
|
+
default_value=default_value,
|
|
1324
|
+
min_value=min_value,
|
|
1325
|
+
max_value=max_value,
|
|
1326
|
+
precision=precision,
|
|
1327
|
+
double_format=ProtobufUtils.double_format_to_pbo(double_format)
|
|
1328
|
+
)
|
|
1329
|
+
))
|
|
1330
|
+
|
|
1331
|
+
def add_integer_config_field(self, field_name: str, display_name: str, description: str,
|
|
1332
|
+
default_value: int | None = None, min_value: int = -2**31, max_value: int = 2**31-1,
|
|
1333
|
+
optional: bool = False) -> None:
|
|
1334
|
+
"""
|
|
1335
|
+
Add an integer configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1336
|
+
manager.
|
|
1337
|
+
|
|
1338
|
+
:param field_name: The name of the field.
|
|
1339
|
+
:param display_name: The display name of the field.
|
|
1340
|
+
:param description: The description of the field.
|
|
1341
|
+
:param default_value: The default value of the field.
|
|
1342
|
+
:param min_value: The minimum value of the field.
|
|
1343
|
+
:param max_value: The maximum value of the field.
|
|
1344
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1345
|
+
"""
|
|
1346
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1347
|
+
data_field_type=FieldTypePbo.INTEGER,
|
|
1348
|
+
data_field_name=field_name,
|
|
1349
|
+
display_name=display_name,
|
|
1350
|
+
description=description,
|
|
1351
|
+
required=not optional,
|
|
1352
|
+
editable=True,
|
|
1353
|
+
integer_properties=IntegerPropertiesPbo(
|
|
1354
|
+
default_value=default_value,
|
|
1355
|
+
min_value=min_value,
|
|
1356
|
+
max_value=max_value
|
|
1357
|
+
)
|
|
1358
|
+
))
|
|
1359
|
+
|
|
1360
|
+
def add_string_config_field(self, field_name: str, display_name: str, description: str,
|
|
1361
|
+
default_value: str | None = None, max_length: int = 1000, optional: bool = False,
|
|
1362
|
+
validation_regex: str | None = None, error_msg: str | None = None,
|
|
1363
|
+
string_format: SapioStringFormat | None = None) -> None:
|
|
1364
|
+
"""
|
|
1365
|
+
Add a string configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1366
|
+
manager.
|
|
1367
|
+
|
|
1368
|
+
:param field_name: The name of the field.
|
|
1369
|
+
:param display_name: The display name of the field.
|
|
1370
|
+
:param description: The description of the field.
|
|
1371
|
+
:param default_value: The default value of the field.
|
|
1372
|
+
:param max_length: The maximum length of the field.
|
|
1373
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1374
|
+
:param validation_regex: An optional regex that the field value must match.
|
|
1375
|
+
:param error_msg: An optional error message to display if the field value does not match the regex.
|
|
1376
|
+
:param string_format: The format of the field.
|
|
1377
|
+
"""
|
|
1378
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1379
|
+
data_field_type=FieldTypePbo.STRING,
|
|
1380
|
+
data_field_name=field_name,
|
|
1381
|
+
display_name=display_name,
|
|
1382
|
+
description=description,
|
|
1383
|
+
required=not optional,
|
|
1384
|
+
editable=True,
|
|
1385
|
+
string_properties=StringPropertiesPbo(
|
|
1386
|
+
default_value=default_value,
|
|
1387
|
+
max_length=max_length,
|
|
1388
|
+
field_validator=ProtobufUtils.field_validator_pbo(validation_regex, error_msg),
|
|
1389
|
+
string_format=ProtobufUtils.string_format_to_pbo(string_format)
|
|
1390
|
+
)
|
|
1391
|
+
))
|
|
1392
|
+
|
|
1393
|
+
def add_list_config_field(self, field_name: str, display_name: str, description: str,
|
|
1394
|
+
default_value: str | None = None, allowed_values: list[str] | None = None,
|
|
1395
|
+
direct_edit: bool = False, optional: bool = False,
|
|
1396
|
+
validation_regex: str | None = None, error_msg: str | None = None,
|
|
1397
|
+
dependencies: dict[str, list[str]] | None = None,
|
|
1398
|
+
is_hide_disabled_fields: bool = False) -> None:
|
|
1399
|
+
"""
|
|
1400
|
+
Add a list configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1401
|
+
manager.
|
|
1402
|
+
|
|
1403
|
+
:param field_name: The name of the field.
|
|
1404
|
+
:param display_name: The display name of the field.
|
|
1405
|
+
:param description: The description of the field.
|
|
1406
|
+
:param default_value: The default value of the field.
|
|
1407
|
+
:param allowed_values: The list of allowed values for the field.
|
|
1408
|
+
:param direct_edit: If true, the user can enter a value that is not in the list of allowed values. If false,
|
|
1409
|
+
the user can only select from the list of allowed values.
|
|
1410
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1411
|
+
:param validation_regex: An optional regex that the field value must match.
|
|
1412
|
+
:param error_msg: An optional error message to display if the field value does not match the regex.
|
|
1413
|
+
:param dependencies: A dictionary of field values to the fields that should be disabled while this field
|
|
1414
|
+
is set to the key value.
|
|
1415
|
+
:param dependencies: A dictionary of field dependencies. The value of the dictionary is a possible value of
|
|
1416
|
+
this field, and the key is a list of field names for other config fields of this agent that will be
|
|
1417
|
+
disabled if the config field matches the corresponding value.
|
|
1418
|
+
:param is_hide_disabled_fields: If true, fields disabled by a field dependency will be hidden. If false, the
|
|
1419
|
+
dependent fields will be visible, but uneditable.
|
|
1420
|
+
"""
|
|
1421
|
+
dependent_fields: list[SelectionDependentFieldEntryPbo] | None = None
|
|
1422
|
+
if dependencies:
|
|
1423
|
+
dependent_fields = []
|
|
1424
|
+
for key, values in dependencies.items():
|
|
1425
|
+
dependent_fields.append(SelectionDependentFieldEntryPbo(key=key, dependent_field_names=values))
|
|
1426
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1427
|
+
data_field_type=FieldTypePbo.SELECTION,
|
|
1428
|
+
data_field_name=field_name,
|
|
1429
|
+
display_name=display_name,
|
|
1430
|
+
description=description,
|
|
1431
|
+
required=not optional,
|
|
1432
|
+
editable=True,
|
|
1433
|
+
selection_properties=SelectionPropertiesPbo(
|
|
1434
|
+
default_value=default_value,
|
|
1435
|
+
static_list_values=allowed_values,
|
|
1436
|
+
direct_edit=direct_edit,
|
|
1437
|
+
field_validator=ProtobufUtils.field_validator_pbo(validation_regex, error_msg),
|
|
1438
|
+
dependent_fields=dependent_fields,
|
|
1439
|
+
is_hide_disabled_fields=is_hide_disabled_fields
|
|
1440
|
+
)
|
|
1441
|
+
))
|
|
1442
|
+
|
|
1443
|
+
def add_multi_list_config_field(self, field_name: str, display_name: str, description: str,
|
|
1444
|
+
default_value: list[str] | None = None, allowed_values: list[str] | None = None,
|
|
1445
|
+
direct_edit: bool = False, optional: bool = False,
|
|
1446
|
+
validation_regex: str | None = None, error_msg: str | None = None,
|
|
1447
|
+
dependencies: dict[str, list[str]] | None = None,
|
|
1448
|
+
is_hide_disabled_fields: bool = False) -> None:
|
|
1449
|
+
"""
|
|
1450
|
+
Add a multi-select list configuration field to the agent. This field will be used to configure the agent in the
|
|
1451
|
+
plan manager.
|
|
1452
|
+
|
|
1453
|
+
:param field_name: The name of the field.
|
|
1454
|
+
:param display_name: The display name of the field.
|
|
1455
|
+
:param description: The description of the field.
|
|
1456
|
+
:param default_value: The default value of the field.
|
|
1457
|
+
:param allowed_values: The list of allowed values for the field.
|
|
1458
|
+
:param direct_edit: If true, the user can enter a value that is not in the list of allowed values. If false,
|
|
1459
|
+
the user can only select from the list of allowed values.
|
|
1460
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1461
|
+
:param validation_regex: An optional regex that the field value must match.
|
|
1462
|
+
:param error_msg: An optional error message to display if the field value does not match the regex.
|
|
1463
|
+
:param dependencies: A dictionary of field dependencies. The value of the dictionary is a possible value of
|
|
1464
|
+
this field, and the key is a list of field names for other config fields of this agent that will be
|
|
1465
|
+
disabled if the config field matches the corresponding value.
|
|
1466
|
+
:param is_hide_disabled_fields: If true, fields disabled by a field dependency will be hidden. If false, the
|
|
1467
|
+
dependent fields will be visible, but uneditable.
|
|
1468
|
+
"""
|
|
1469
|
+
dependent_fields: list[SelectionDependentFieldEntryPbo] | None = None
|
|
1470
|
+
if dependencies:
|
|
1471
|
+
dependent_fields = []
|
|
1472
|
+
for key, values in dependencies.items():
|
|
1473
|
+
dependent_fields.append(SelectionDependentFieldEntryPbo(key=key, dependent_field_names=values))
|
|
1474
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1475
|
+
data_field_type=FieldTypePbo.SELECTION,
|
|
1476
|
+
data_field_name=field_name,
|
|
1477
|
+
display_name=display_name,
|
|
1478
|
+
description=description,
|
|
1479
|
+
required=not optional,
|
|
1480
|
+
editable=True,
|
|
1481
|
+
selection_properties=SelectionPropertiesPbo(
|
|
1482
|
+
default_value=",".join(default_value) if default_value else None,
|
|
1483
|
+
static_list_values=allowed_values,
|
|
1484
|
+
multi_select=True,
|
|
1485
|
+
direct_edit=direct_edit,
|
|
1486
|
+
field_validator=ProtobufUtils.field_validator_pbo(validation_regex, error_msg),
|
|
1487
|
+
dependent_fields=dependent_fields,
|
|
1488
|
+
is_hide_disabled_fields=is_hide_disabled_fields
|
|
1489
|
+
)
|
|
1490
|
+
))
|
|
1491
|
+
|
|
1492
|
+
def add_date_config_field(self, field_name: str, display_name: str, description: str, optional: bool = False,
|
|
1493
|
+
date_time_format: str = "MMM dd, yyyy", default_to_today: bool = False,
|
|
1494
|
+
is_static_date: bool = False) -> None:
|
|
1495
|
+
"""
|
|
1496
|
+
Add a date configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1497
|
+
manager.
|
|
1498
|
+
|
|
1499
|
+
:param field_name: The name of the field.
|
|
1500
|
+
:param display_name: The display name of the field.
|
|
1501
|
+
:param description: The description of the field.
|
|
1502
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1503
|
+
:param date_time_format: The format that this date field should appear in. The date format is Java-style.
|
|
1504
|
+
See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/text/SimpleDateFormat.html for more
|
|
1505
|
+
details.
|
|
1506
|
+
:param default_to_today: If true, the default value of the field will be set to today's date. If false, the
|
|
1507
|
+
default value will be None.
|
|
1508
|
+
:param is_static_date: If true, the user will input the date as UTC. If false, the user will input the date
|
|
1509
|
+
as local time.
|
|
1510
|
+
"""
|
|
1511
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1512
|
+
data_field_type=FieldTypePbo.DATE,
|
|
1513
|
+
data_field_name=field_name,
|
|
1514
|
+
display_name=display_name,
|
|
1515
|
+
description=description,
|
|
1516
|
+
required=not optional,
|
|
1517
|
+
editable=True,
|
|
1518
|
+
date_properties=DatePropertiesPbo(
|
|
1519
|
+
default_value="@Today" if default_to_today else None,
|
|
1520
|
+
static_date=is_static_date,
|
|
1521
|
+
date_time_format=date_time_format
|
|
1522
|
+
)
|
|
1523
|
+
))
|
|
1524
|
+
|
|
1525
|
+
def add_date_range_config_field(self, field_name: str, display_name: str, description: str, optional: bool = False,
|
|
1526
|
+
date_time_format: str = "MMM dd, yyyy", is_static_date: bool = False) -> None:
|
|
1527
|
+
"""
|
|
1528
|
+
Add a date range configuration field to the agent. This field will be used to configure the agent in the plan
|
|
1529
|
+
manager. The returned value of a date range field is a string that should be parsed using the DateRange class.
|
|
1530
|
+
|
|
1531
|
+
:param field_name: The name of the field.
|
|
1532
|
+
:param display_name: The display name of the field.
|
|
1533
|
+
:param description: The description of the field.
|
|
1534
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1535
|
+
:param date_time_format: The format that this date field should appear in. The date format is Java-style.
|
|
1536
|
+
See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/text/SimpleDateFormat.html for more
|
|
1537
|
+
details.
|
|
1538
|
+
:param is_static_date: If true, the user will input the date as UTC. If false, the user will input the date
|
|
1539
|
+
as local time.
|
|
1540
|
+
"""
|
|
1541
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1542
|
+
data_field_type=FieldTypePbo.DATE_RANGE,
|
|
1543
|
+
data_field_name=field_name,
|
|
1544
|
+
display_name=display_name,
|
|
1545
|
+
description=description,
|
|
1546
|
+
required=not optional,
|
|
1547
|
+
editable=True,
|
|
1548
|
+
date_range_properties=DateRangePropertiesPbo(
|
|
1549
|
+
is_static=is_static_date,
|
|
1550
|
+
date_time_format=date_time_format
|
|
1551
|
+
)
|
|
1552
|
+
))
|
|
1553
|
+
|
|
1554
|
+
def add_credentials_config_field(self, field_name: str, display_name: str, description: str, optional: bool = False,
|
|
1555
|
+
category: str | None = None, dependencies: dict[str, list[str]] | None = None,
|
|
1556
|
+
is_hide_disabled_fields: bool = False) -> None:
|
|
1557
|
+
"""
|
|
1558
|
+
Add a list field that asks the user to choose which credentials to use. This field will be used to
|
|
1559
|
+
configure the agent in the plan manager.
|
|
1560
|
+
|
|
1561
|
+
:param field_name: The name of the field.
|
|
1562
|
+
:param display_name: The display name of the field.
|
|
1563
|
+
:param description: The description of the field.
|
|
1564
|
+
:param optional: If true, this field is optional. If false, this field is required.
|
|
1565
|
+
:param category: If provided, only credentials in this category will be shown to the user.
|
|
1566
|
+
:param dependencies: A dictionary of field dependencies. The value of the dictionary is a possible value of
|
|
1567
|
+
this field, and the key is a list of field names for other config fields of this agent that will be
|
|
1568
|
+
disabled if the config field matches the corresponding value.
|
|
1569
|
+
:param is_hide_disabled_fields: If true, fields disabled by a field dependency will be hidden. If false, the
|
|
1570
|
+
dependent fields will be visible, but uneditable.
|
|
1571
|
+
"""
|
|
1572
|
+
dependent_fields: list[SelectionDependentFieldEntryPbo] | None = None
|
|
1573
|
+
if dependencies:
|
|
1574
|
+
dependent_fields = []
|
|
1575
|
+
for key, values in dependencies.items():
|
|
1576
|
+
dependent_fields.append(SelectionDependentFieldEntryPbo(key=key, dependent_field_names=values))
|
|
1577
|
+
self.config_fields.append(VeloxFieldDefPbo(
|
|
1578
|
+
data_field_type=FieldTypePbo.SELECTION,
|
|
1579
|
+
data_field_name=field_name,
|
|
1580
|
+
display_name=display_name,
|
|
1581
|
+
description=description,
|
|
1582
|
+
required=not optional,
|
|
1583
|
+
editable=True,
|
|
1584
|
+
selection_properties=SelectionPropertiesPbo(
|
|
1585
|
+
# A credentials field is just a selection field with its list mode set to [ExternalCredentials].
|
|
1586
|
+
list_mode=f"[ExternalCredentials]{category.strip() if category else ''}",
|
|
1587
|
+
multi_select=False,
|
|
1588
|
+
default_value=None,
|
|
1589
|
+
direct_edit=False,
|
|
1590
|
+
dependent_fields=dependent_fields,
|
|
1591
|
+
is_hide_disabled_fields=is_hide_disabled_fields
|
|
1592
|
+
)
|
|
1593
|
+
))
|
|
1594
|
+
|
|
1595
|
+
@abstractmethod
|
|
1596
|
+
def validate_input(self) -> list[str] | None:
|
|
1597
|
+
"""
|
|
1598
|
+
Validate the request given to this agent. If the request is validly formatted, this method should return None.
|
|
1599
|
+
If the request is not valid, this method should return an error message indicating what is wrong with the
|
|
1600
|
+
request.
|
|
1601
|
+
|
|
1602
|
+
This method should not perform any actual processing of the request. It should only validate the inputs and
|
|
1603
|
+
configurations provided in the request.
|
|
1604
|
+
|
|
1605
|
+
The request inputs can be accessed using the self.get_input_*() methods.
|
|
1606
|
+
The request settings can be accessed using the self.get_config_fields() method.
|
|
1607
|
+
The request itself can be accessed using self.request.
|
|
1608
|
+
|
|
1609
|
+
:return: A list of the error messages if the request is not valid. If the request is valid, return an empty
|
|
1610
|
+
list or None.
|
|
1611
|
+
"""
|
|
1612
|
+
pass
|
|
1613
|
+
|
|
1614
|
+
def validate_output(self: list[AgentResult]) -> list[str] | None:
|
|
1615
|
+
"""
|
|
1616
|
+
Validate the output returned by this agent. This is an optional check that agents are able to run before
|
|
1617
|
+
returning their results to the server. This is called after the agent service verifies that the results
|
|
1618
|
+
match the output configurations defined by the agent.
|
|
1619
|
+
|
|
1620
|
+
:return: A list of the error messages if the results are not valid. If the results are not valid, return an
|
|
1621
|
+
empty list or None.
|
|
1622
|
+
"""
|
|
1623
|
+
pass
|
|
1624
|
+
|
|
1625
|
+
def dry_run_output(self) -> list[AgentResult]:
|
|
1626
|
+
"""
|
|
1627
|
+
Provide fixed results for a dry run of this agent. This method should not perform any actual processing of the
|
|
1628
|
+
request. It should only return example outputs that can be used to test the next agent in the plan.
|
|
1629
|
+
|
|
1630
|
+
The default implementation of this method looks at the testing_example field of each output configuration
|
|
1631
|
+
and returns a AgentResult object based on the content type of the output.
|
|
1632
|
+
|
|
1633
|
+
:return: A list of AgentResult objects containing example outputs for this agent. Each result in the list
|
|
1634
|
+
corresponds to a separate output from the agent.
|
|
1635
|
+
"""
|
|
1636
|
+
results: list[AgentResult] = []
|
|
1637
|
+
for output, container_type in zip(self.output_configs, self.output_container_types):
|
|
1638
|
+
config: AgentIoConfigBasePbo = output.base_config
|
|
1639
|
+
example: ExampleContainerPbo = config.testing_example
|
|
1640
|
+
match container_type:
|
|
1641
|
+
case ContainerType.BINARY:
|
|
1642
|
+
example: bytes = example.binary_example
|
|
1643
|
+
results.append(BinaryResult(binary_data=[example]))
|
|
1644
|
+
case ContainerType.CSV:
|
|
1645
|
+
example: str = example.text_example
|
|
1646
|
+
results.append(CsvResult(FileUtil.tokenize_csv(example.encode())[0]))
|
|
1647
|
+
case ContainerType.DATA_RECORDS:
|
|
1648
|
+
example: str = example.text_example
|
|
1649
|
+
records: list[DataRecord] = [DataRecord.from_json(x) for x in self._parse_jsonl(example)]
|
|
1650
|
+
results.append(DataRecordResult(existing_records=records))
|
|
1651
|
+
case ContainerType.JSON:
|
|
1652
|
+
example: str = example.text_example
|
|
1653
|
+
results.append(JsonResult(json_data=self._parse_jsonl(example)))
|
|
1654
|
+
case ContainerType.TEXT:
|
|
1655
|
+
example: str = example.text_example
|
|
1656
|
+
results.append(TextResult(text_data=[example]))
|
|
1657
|
+
return results
|
|
1658
|
+
|
|
1659
|
+
@abstractmethod
|
|
1660
|
+
def run(self, user: SapioUser) -> list[AgentResult]:
|
|
1661
|
+
"""
|
|
1662
|
+
Execute this agent.
|
|
1663
|
+
|
|
1664
|
+
The request inputs can be accessed using the self.get_input_*() methods.
|
|
1665
|
+
The request settings can be accessed using the self.get_config_fields() method.
|
|
1666
|
+
The request itself can be accessed using self.request.
|
|
1667
|
+
|
|
1668
|
+
:param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
|
|
1669
|
+
system.
|
|
1670
|
+
:return: A list of AgentResult objects containing the response data. Each result in the list corresponds to
|
|
1671
|
+
a separate output from the agent. Field map results do not appear as agent output in the plan manager,
|
|
1672
|
+
instead appearing as records related to the plan step during the run.
|
|
1673
|
+
"""
|
|
1674
|
+
pass
|
|
1675
|
+
|
|
1676
|
+
def get_credentials(self, identifier: str | None = None, display_name: str | None = None,
|
|
1677
|
+
category: str | None = None) -> ExternalCredentials:
|
|
1678
|
+
"""
|
|
1679
|
+
Get credentials for the given criteria.
|
|
1680
|
+
|
|
1681
|
+
:param identifier: The unique identifier for the credentials.
|
|
1682
|
+
:param display_name: The display name of the credentials.
|
|
1683
|
+
:param category: The category that the credentials are in.
|
|
1684
|
+
:return: An ExternalCredentials object containing the credentials for the given category and host.
|
|
1685
|
+
"""
|
|
1686
|
+
if not self.__is_setup:
|
|
1687
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1688
|
+
# Remove leading/trailing whitespace
|
|
1689
|
+
identifier = identifier.strip() if identifier else None
|
|
1690
|
+
display_name = display_name.strip() if display_name else None
|
|
1691
|
+
category = category.strip() if category else None
|
|
1692
|
+
|
|
1693
|
+
matching_creds: list[ExternalCredentialsPbo] = []
|
|
1694
|
+
for cred in self.request.external_credential:
|
|
1695
|
+
# Do case insensitive comparison
|
|
1696
|
+
if identifier and cred.id.lower != identifier.lower():
|
|
1697
|
+
continue
|
|
1698
|
+
if display_name and cred.display_name.lower != display_name.lower():
|
|
1699
|
+
continue
|
|
1700
|
+
if category and cred.category.lower() != category.lower():
|
|
1701
|
+
continue
|
|
1702
|
+
matching_creds.append(cred)
|
|
1703
|
+
if len(matching_creds) == 0:
|
|
1704
|
+
raise ValueError(f"No credentials found for the criteria. "
|
|
1705
|
+
f"(identifier={identifier}, display_name={display_name}, category={category})")
|
|
1706
|
+
if len(matching_creds) > 1:
|
|
1707
|
+
raise ValueError(f"Multiple credentials found for the given criteria. "
|
|
1708
|
+
f"(identifier={identifier}, display_name={display_name}, category={category})")
|
|
1709
|
+
|
|
1710
|
+
return ExternalCredentials.from_pbo(matching_creds[0])
|
|
1711
|
+
|
|
1712
|
+
def _get_credentials_from_config(self, value: str) -> ExternalCredentials:
|
|
1713
|
+
"""
|
|
1714
|
+
Get credentials given the value of a credentials config field.
|
|
1715
|
+
|
|
1716
|
+
:param value: The value of the credentials config field.
|
|
1717
|
+
:return: An ExternalCredentials object containing the credentials.
|
|
1718
|
+
"""
|
|
1719
|
+
if not self.__is_setup:
|
|
1720
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1721
|
+
# Values should be of the format "Name (Identifier)"
|
|
1722
|
+
match = re.match(r"^(.*) \((.*)\)$", value)
|
|
1723
|
+
if not match:
|
|
1724
|
+
raise ValueError(f"Invalid credentials value '{value}'. Expected format 'Name (Identifier)'.")
|
|
1725
|
+
identifier: str = match.group(2)
|
|
1726
|
+
for cred in self.request.external_credential:
|
|
1727
|
+
if cred.id == identifier:
|
|
1728
|
+
return ExternalCredentials.from_pbo(cred)
|
|
1729
|
+
raise ValueError(f"No credentials found with identifier '{identifier}'.")
|
|
1730
|
+
|
|
1731
|
+
def call_subprocess(self,
|
|
1732
|
+
args: str | bytes | PathLike[str] | PathLike[bytes] | Sequence[str | bytes | PathLike[str] | PathLike[bytes]],
|
|
1733
|
+
cwd: str | bytes | PathLike[str] | PathLike[bytes] | None = None,
|
|
1734
|
+
**kwargs) -> CompletedProcess[str]:
|
|
1735
|
+
"""
|
|
1736
|
+
Call a subprocess with the given arguments, logging the command and any errors that occur.
|
|
1737
|
+
This function will raise an exception if the return code of the subprocess is non-zero. The output of the
|
|
1738
|
+
subprocess will be captured and returned as part of the CompletedProcess object.
|
|
1739
|
+
|
|
1740
|
+
:param args: The list of arguments to pass to the subprocess.
|
|
1741
|
+
:param cwd: The working directory to run the subprocess in. If None, the current working directory is used.
|
|
1742
|
+
:param kwargs: Additional keyword arguments to pass to subprocess.run().
|
|
1743
|
+
:return: The CompletedProcess object returned by subprocess.run().
|
|
1744
|
+
"""
|
|
1745
|
+
try:
|
|
1746
|
+
self.log_info(f"Running subprocess with command: {' '.join(args)}")
|
|
1747
|
+
p: CompletedProcess[str] = subprocess.run(args, check=True, capture_output=True, text=True, cwd=cwd,
|
|
1748
|
+
**kwargs)
|
|
1749
|
+
if p.stdout:
|
|
1750
|
+
self.log_info(f"STDOUT: {p.stdout}")
|
|
1751
|
+
if p.stderr:
|
|
1752
|
+
self.log_info(f"STDERR: {p.stderr}")
|
|
1753
|
+
return p
|
|
1754
|
+
except subprocess.CalledProcessError as e:
|
|
1755
|
+
self.log_error(f"Error running subprocess. Return code: {e.returncode}")
|
|
1756
|
+
if e.stdout:
|
|
1757
|
+
self.log_error(f"STDOUT: {e.stdout}")
|
|
1758
|
+
if e.stderr:
|
|
1759
|
+
self.log_error(f"STDERR: {e.stderr}")
|
|
1760
|
+
raise
|
|
1761
|
+
|
|
1762
|
+
def log_info(self, message: str) -> None:
|
|
1763
|
+
"""
|
|
1764
|
+
Log an info message for this agent. If verbose logging is enabled, this message will be included in the logs
|
|
1765
|
+
returned to the caller. Empty/None inputs will not be logged.
|
|
1766
|
+
|
|
1767
|
+
Logging info can be done during initialization, but those logs will not be returned to the caller. Other
|
|
1768
|
+
log calls will be returned to the caller, even if done during initialization.
|
|
1769
|
+
|
|
1770
|
+
:param message: The message to log.
|
|
1771
|
+
"""
|
|
1772
|
+
if not message:
|
|
1773
|
+
return
|
|
1774
|
+
if self.__is_setup and self.verbose_logging:
|
|
1775
|
+
self.logs.append(f"INFO: {self}: {message}")
|
|
1776
|
+
self.logger.info(message)
|
|
1777
|
+
|
|
1778
|
+
def log_warning(self, message: str) -> None:
|
|
1779
|
+
"""
|
|
1780
|
+
Log a warning message for this agent. This message will be included in the logs returned to the caller.
|
|
1781
|
+
Empty/None inputs will not be logged.
|
|
1782
|
+
|
|
1783
|
+
:param message: The message to log.
|
|
1784
|
+
"""
|
|
1785
|
+
if not message:
|
|
1786
|
+
return
|
|
1787
|
+
self.logs.append(f"WARNING: {self}: {message}")
|
|
1788
|
+
self.logger.warning(message)
|
|
1789
|
+
|
|
1790
|
+
def log_error(self, message: str) -> None:
|
|
1791
|
+
"""
|
|
1792
|
+
Log an error message for this agent. This message will be included in the logs returned to the caller.
|
|
1793
|
+
Empty/None inputs will not be logged.
|
|
1794
|
+
|
|
1795
|
+
:param message: The message to log.
|
|
1796
|
+
"""
|
|
1797
|
+
if not message:
|
|
1798
|
+
return
|
|
1799
|
+
self.logs.append(f"ERROR: {self}: {message}")
|
|
1800
|
+
self.logger.error(message)
|
|
1801
|
+
|
|
1802
|
+
def log_exception(self, message: str, e: Exception) -> None:
|
|
1803
|
+
"""
|
|
1804
|
+
Log an exception for this agent. This message will be included in the logs returned to the caller.
|
|
1805
|
+
Empty/None inputs will not be logged.
|
|
1806
|
+
|
|
1807
|
+
:param message: The message to log.
|
|
1808
|
+
:param e: The exception to log.
|
|
1809
|
+
"""
|
|
1810
|
+
if not message and not e:
|
|
1811
|
+
return
|
|
1812
|
+
self.logs.append(f"EXCEPTION: {self}: {message} - {e}")
|
|
1813
|
+
self.logger.error(f"{message}\n{traceback.format_exc()}")
|
|
1814
|
+
|
|
1815
|
+
def is_input_partial(self, index: int = 0) -> bool:
|
|
1816
|
+
"""
|
|
1817
|
+
Check if the input at the given index is marked as partial.
|
|
1818
|
+
|
|
1819
|
+
:param index: The index of the input to check. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1820
|
+
:return: True if the input is marked as partial, False otherwise.
|
|
1821
|
+
"""
|
|
1822
|
+
if not self.__is_setup:
|
|
1823
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1824
|
+
return self.request.input[index].is_partial
|
|
1825
|
+
|
|
1826
|
+
def get_input_name(self, index: int = 0) -> str | None:
|
|
1827
|
+
"""
|
|
1828
|
+
Get the name of the input from the request object.
|
|
1829
|
+
|
|
1830
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1831
|
+
:return: The name of the input from the request object, or None if no name is set.
|
|
1832
|
+
"""
|
|
1833
|
+
if not self.__is_setup:
|
|
1834
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1835
|
+
return self.request.input[index].item_container.container_name
|
|
1836
|
+
|
|
1837
|
+
def get_input_content_type(self, index: int = 0) -> ContentTypePbo:
|
|
1838
|
+
"""
|
|
1839
|
+
Get the content type of the input from the request object.
|
|
1840
|
+
|
|
1841
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1842
|
+
:return: The content type of the input from the request object.
|
|
1843
|
+
"""
|
|
1844
|
+
if not self.__is_setup:
|
|
1845
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1846
|
+
return self.request.input[index].item_container.content_type
|
|
1847
|
+
|
|
1848
|
+
def _validate_get_input(self, index: int, get_type: ContainerType) -> None:
|
|
1849
|
+
"""
|
|
1850
|
+
Given an index and the container type being requested from it, validate that the agent is setup to respond
|
|
1851
|
+
to a request, that the index is not out of range, and that the input type from the config matches the
|
|
1852
|
+
input type being requested. If any errors are encountered, raise an exception.
|
|
1853
|
+
|
|
1854
|
+
:param index: The index of the input.
|
|
1855
|
+
:param get_type: The type of the input being requested.
|
|
1856
|
+
"""
|
|
1857
|
+
if not self.__is_setup:
|
|
1858
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1859
|
+
total_inputs: int = len(self.request.input)
|
|
1860
|
+
if index >= total_inputs:
|
|
1861
|
+
raise Exception(f"Index out of range. This agent only has {total_inputs} inputs. Attempted to retrieve "
|
|
1862
|
+
f"input with index {index}.")
|
|
1863
|
+
config: ContainerType = self.input_container_types[index]
|
|
1864
|
+
if config != get_type:
|
|
1865
|
+
raise Exception(f"Input {index} is not a \"{get_type.value}\" input. The container type for this input "
|
|
1866
|
+
f"is \"{config.value}\".")
|
|
1867
|
+
|
|
1868
|
+
def get_input_binary(self, index: int = 0) -> list[bytes]:
|
|
1869
|
+
"""
|
|
1870
|
+
Get the binary data from the request object.
|
|
1871
|
+
|
|
1872
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1873
|
+
:return: The binary data from the request object.
|
|
1874
|
+
"""
|
|
1875
|
+
self._validate_get_input(index, ContainerType.BINARY)
|
|
1876
|
+
container: StepItemContainerPbo = self.request.input[index].item_container
|
|
1877
|
+
if not container.HasField("binary_container"):
|
|
1878
|
+
return []
|
|
1879
|
+
return list(container.binary_container.items)
|
|
1880
|
+
|
|
1881
|
+
def get_input_csv(self, index: int = 0) -> tuple[list[str], list[dict[str, str]]]:
|
|
1882
|
+
"""
|
|
1883
|
+
Parse the CSV data from the request object.
|
|
1884
|
+
|
|
1885
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1886
|
+
:return: A tuple containing the header row and the data rows. The header row is a list of strings representing
|
|
1887
|
+
the column names, and the data rows are a list of dictionaries where each dictionary represents a row in the
|
|
1888
|
+
CSV with the column names as keys and the corresponding values as strings.
|
|
1889
|
+
"""
|
|
1890
|
+
self._validate_get_input(index, ContainerType.CSV)
|
|
1891
|
+
container: StepItemContainerPbo = self.request.input[index].item_container
|
|
1892
|
+
if not container.HasField("csv_container"):
|
|
1893
|
+
return [], []
|
|
1894
|
+
ret_val: list[dict[str, str]] = []
|
|
1895
|
+
headers: Iterable[str] = container.csv_container.header.cells
|
|
1896
|
+
for row in container.csv_container.items:
|
|
1897
|
+
row_dict: dict[str, str] = {}
|
|
1898
|
+
for header, value in zip(headers, row.cells):
|
|
1899
|
+
row_dict[header] = value
|
|
1900
|
+
ret_val.append(row_dict)
|
|
1901
|
+
return list(headers), ret_val
|
|
1902
|
+
|
|
1903
|
+
def get_input_records(self, index: int = 0) -> list[DataRecord]:
|
|
1904
|
+
"""
|
|
1905
|
+
Parse the DATA_RECORD data from the request object.
|
|
1906
|
+
|
|
1907
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1908
|
+
:return: A list containing the data records from the request object.
|
|
1909
|
+
"""
|
|
1910
|
+
self._validate_get_input(index, ContainerType.DATA_RECORDS)
|
|
1911
|
+
container: StepItemContainerPbo = self.request.input[index].item_container
|
|
1912
|
+
if not container.HasField("data_record_container"):
|
|
1913
|
+
return []
|
|
1914
|
+
ret_val: list[DataRecord] = []
|
|
1915
|
+
for record in container.data_record_container.items:
|
|
1916
|
+
ret_val.append(ProtobufUtils.pbo_to_data_record(record))
|
|
1917
|
+
return ret_val
|
|
1918
|
+
|
|
1919
|
+
def get_input_json(self, index: int = 0) -> list[dict[str, Any]]:
|
|
1920
|
+
"""
|
|
1921
|
+
Parse the JSON data from the request object.
|
|
1922
|
+
|
|
1923
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1924
|
+
:return: A list of parsed JSON objects, which are represented as dictionaries.
|
|
1925
|
+
"""
|
|
1926
|
+
self._validate_get_input(index, ContainerType.JSON)
|
|
1927
|
+
container: StepItemContainerPbo = self.request.input[index].item_container
|
|
1928
|
+
if not container.HasField("json_container"):
|
|
1929
|
+
return []
|
|
1930
|
+
input_json: list[Any] = [json.loads(x) for x in container.json_container.items]
|
|
1931
|
+
# Verify that the given JSON actually is a list of dictionaries. If they aren't then the previous step provided
|
|
1932
|
+
# bad input. Agents are enforced to result in a list of dictionaries when returning JSON data, so this is likely
|
|
1933
|
+
# an error caused by a script or static input step.
|
|
1934
|
+
for i, entry in enumerate(input_json):
|
|
1935
|
+
if not isinstance(entry, dict):
|
|
1936
|
+
raise Exception(f"Element {i} of input {index} is not a dictionary object. All top-level JSON inputs "
|
|
1937
|
+
f"are expected to be dictionaries.")
|
|
1938
|
+
return input_json
|
|
1939
|
+
|
|
1940
|
+
def get_input_text(self, index: int = 0) -> list[str]:
|
|
1941
|
+
"""
|
|
1942
|
+
Parse the text data from the request object.
|
|
1943
|
+
|
|
1944
|
+
:param index: The index of the input to parse. Defaults to 0. Used for agents that accept multiple inputs.
|
|
1945
|
+
:return: A list of text data as strings.
|
|
1946
|
+
"""
|
|
1947
|
+
self._validate_get_input(index, ContainerType.TEXT)
|
|
1948
|
+
container: StepItemContainerPbo = self.request.input[index].item_container
|
|
1949
|
+
if not container.HasField("text_container"):
|
|
1950
|
+
return []
|
|
1951
|
+
return list(container.text_container.items)
|
|
1952
|
+
|
|
1953
|
+
def get_config_defs(self) -> dict[str, VeloxFieldDefPbo]:
|
|
1954
|
+
"""
|
|
1955
|
+
Get the config field definitions for this agent.
|
|
1956
|
+
|
|
1957
|
+
:return: A dictionary of field definitions, where the keys are the field names and the values are the
|
|
1958
|
+
VeloxFieldDefPbo objects representing the field definitions.
|
|
1959
|
+
"""
|
|
1960
|
+
field_defs: dict[str, VeloxFieldDefPbo] = {}
|
|
1961
|
+
for field_def in self.to_pbo().config_fields:
|
|
1962
|
+
field_defs[field_def.data_field_name] = field_def
|
|
1963
|
+
return field_defs
|
|
1964
|
+
|
|
1965
|
+
def get_config_fields(self) -> dict[str, FieldValue | list[str] | DateRange | ExternalCredentials]:
|
|
1966
|
+
"""
|
|
1967
|
+
Get the configuration field values from the request object. If a field is not present in the request,
|
|
1968
|
+
the default value from the config definition will be returned. The returned value for each field will match the
|
|
1969
|
+
field type (e.g. numeric fields return int or float, boolean fields return bool, etc.),
|
|
1970
|
+
along with a few special cases:
|
|
1971
|
+
- Multi-select selection list fields will return a list of strings.
|
|
1972
|
+
- Credentials selection list fields will return an ExternalCredentials object.
|
|
1973
|
+
- Date range fields will return a DateRange object.
|
|
1974
|
+
|
|
1975
|
+
:return: A dictionary of configuration field names and their values.
|
|
1976
|
+
"""
|
|
1977
|
+
if not self.__is_setup:
|
|
1978
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
1979
|
+
config_fields: dict[str, Any] = {}
|
|
1980
|
+
raw_configs: Mapping[str, FieldValuePbo] = self.request.config_field_values
|
|
1981
|
+
for field_name, field_def in self.get_config_defs().items():
|
|
1982
|
+
# If the field is present in the request, convert the protobuf value to a Python value.
|
|
1983
|
+
if field_name in raw_configs:
|
|
1984
|
+
field_value: FieldValue = ProtobufUtils.field_pbo_to_value(raw_configs[field_name])
|
|
1985
|
+
# If the field isn't present, use the default value from the field definition.
|
|
1986
|
+
else:
|
|
1987
|
+
field_value: FieldValue = ProtobufUtils.field_def_pbo_to_default_value(field_def)
|
|
1988
|
+
# If the field value is None, continue to the next field.
|
|
1989
|
+
if field_value is None:
|
|
1990
|
+
config_fields[field_name] = field_value
|
|
1991
|
+
continue
|
|
1992
|
+
# If the field is a multi-select selection list, split the value by commas and strip whitespace.
|
|
1993
|
+
# If the field is a credentials selection list, convert the string value(s) to an ExternalCredentials.
|
|
1994
|
+
if field_def.data_field_type == FieldTypePbo.SELECTION:
|
|
1995
|
+
if field_def.selection_properties.multi_select:
|
|
1996
|
+
field_value: list[str] = [x.strip() for x in re.split(r',(?!\s)', field_value) if x.strip()]
|
|
1997
|
+
if field_def.selection_properties.list_mode.startswith("[ExternalCredentials]"):
|
|
1998
|
+
if isinstance(field_value, list):
|
|
1999
|
+
field_value: list[ExternalCredentials] = [self._get_credentials_from_config(x) for x in field_value]
|
|
2000
|
+
else:
|
|
2001
|
+
field_value: ExternalCredentials = self._get_credentials_from_config(field_value)
|
|
2002
|
+
# If the field type is a date range, convert the string value to a DateRange.
|
|
2003
|
+
elif field_def.data_field_type == FieldTypePbo.DATE_RANGE:
|
|
2004
|
+
field_value: DateRange = DateRange.from_str(field_value)
|
|
2005
|
+
config_fields[field_name] = field_value
|
|
2006
|
+
return config_fields
|
|
2007
|
+
|
|
2008
|
+
def get_current_data_type_name(self) -> str:
|
|
2009
|
+
"""
|
|
2010
|
+
:return: The data type name that is currently configured for this agent in the system. If the data type name
|
|
2011
|
+
of the output data type is needed during the agent's run function, this is how it should be accessed, as
|
|
2012
|
+
opposed to getting the name from the output_data_type function, as the data type name that the system is
|
|
2013
|
+
currently using may differ from the initial self-described name.
|
|
2014
|
+
"""
|
|
2015
|
+
if not self.__is_setup:
|
|
2016
|
+
raise Exception("Cannot call this function before the agent has been set up to respond to a request.")
|
|
2017
|
+
return self.request.output_data_type_name
|
|
2018
|
+
|
|
2019
|
+
@staticmethod
|
|
2020
|
+
def read_from_json(json_data: list[dict[str, Any]], key: str) -> list[Any]:
|
|
2021
|
+
"""
|
|
2022
|
+
From a list of dictionaries, return a list of values for the given key from each dictionary. Skips null values.
|
|
2023
|
+
|
|
2024
|
+
:param json_data: The JSON data to read from.
|
|
2025
|
+
:param key: The key to read the values from.
|
|
2026
|
+
:return: A list of values corresponding to the given key in the JSON data.
|
|
2027
|
+
"""
|
|
2028
|
+
ret_val: list[Any] = []
|
|
2029
|
+
for entry in json_data:
|
|
2030
|
+
if key in entry:
|
|
2031
|
+
value = entry[key]
|
|
2032
|
+
if isinstance(value, list):
|
|
2033
|
+
ret_val.extend(value)
|
|
2034
|
+
elif value is not None:
|
|
2035
|
+
ret_val.append(value)
|
|
2036
|
+
return ret_val
|
|
2037
|
+
|
|
2038
|
+
@staticmethod
|
|
2039
|
+
def flatten_text(text_data: list[str]) -> list[str]:
|
|
2040
|
+
"""
|
|
2041
|
+
From a list of strings that come from a text input, flatten the list by splitting each string on newlines and
|
|
2042
|
+
stripping whitespace. Empty lines will be removed.
|
|
2043
|
+
|
|
2044
|
+
:param text_data: The text data to flatten.
|
|
2045
|
+
:return: A flattened list of strings.
|
|
2046
|
+
"""
|
|
2047
|
+
ret_val: list[str] = []
|
|
2048
|
+
for entry in text_data:
|
|
2049
|
+
lines: list[str] = [x.strip() for x in entry.splitlines() if x.strip()]
|
|
2050
|
+
ret_val.extend(lines)
|
|
2051
|
+
return ret_val
|