sapiopycommons 2025.4.24a494__py3-none-any.whl → 2025.4.25a496__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

@@ -7,7 +7,8 @@ from typing import Any
7
7
 
8
8
  from grpc import ServicerContext
9
9
 
10
- from sapiopycommons.ai.api.fielddefinitions.proto.velox_field_def_pb2 import VeloxFieldDefProto
10
+ from sapiopycommons.ai.api.fielddefinitions.proto.velox_field_def_pb2 import VeloxFieldDefProto, FieldTypeProto, \
11
+ IntegerProperties, DoubleProperties, SelectionProperties, BooleanProperties
11
12
  from sapiopycommons.general.aliases import FieldMap
12
13
  from sapiopylib.rest.User import SapioUser
13
14
 
@@ -183,22 +184,15 @@ class TextResult(SapioToolResult):
183
184
 
184
185
  class ToolServiceBase(ToolServiceServicer, ABC):
185
186
  """
186
- A base class for implementing a tool service.
187
-
188
- Subclasses must implement the get_details and run methods to provide specific functionality for the tool.
187
+ A base class for implementing a tool service. Subclasses should implement the register_tools method to register
188
+ their tools with the service.
189
189
  """
190
- tools: list[ToolBase]
191
-
192
190
  def GetToolDetails(self, request: ToolDetailsRequest, context: ServicerContext) -> ToolDetailsResponse:
193
191
  try:
194
- # Convert the SapioConnectionInfo proto object to a SapioUser object.
195
- user = self.create_user(request.sapio_conn_info)
196
- # Get the tool details from the subclass.
197
- # TODO: Return something other than the ToolDetails proto objects? Something that's cleaner for the
198
- # implementing class to work with?
199
- details: list[ToolDetails] = self.get_details(user, request, context)
192
+ # Get the tool details from the registered tools.
193
+ details: list[ToolDetails] = self.get_details()
200
194
  return ToolDetailsResponse(tool_framework_version=self.tool_version(), tool_details=details)
201
- except Exception as e:
195
+ except Exception:
202
196
  # TODO: This response doesn't even allow logs. What should we do if an exception occurs in this case?
203
197
  return ToolDetailsResponse()
204
198
 
@@ -206,22 +200,21 @@ class ToolServiceBase(ToolServiceServicer, ABC):
206
200
  try:
207
201
  # Convert the SapioConnectionInfo proto object to a SapioUser object.
208
202
  user = self.create_user(request.sapio_user)
209
- # Get the tool results from the subclass and convert them to proto objects.
203
+ # Get the tool results from the registered tool matching the request and convert them to proto objects.
210
204
  entry_data: list[StepEntryOutputData] = []
211
205
  new_records: list[StepRecord] = []
212
- for result in self.run(user, request, context):
206
+ # TODO: Make use of the success value after the response object has a field for it.
207
+ success, results, logs = self.run(user, request, context)
208
+ for result in results:
213
209
  data: StepEntryOutputData | list[StepRecord] = result.to_proto()
214
210
  if isinstance(data, StepEntryOutputData):
215
211
  entry_data.append(data)
216
212
  else:
217
213
  new_records.extend(data)
218
214
  # Return a ProcessStepResponse proto object containing the output data and new records to the caller.
219
- # TODO: Return an is_success = true value.
220
- # TODO: Allow logging in the tools?
221
- return ProcessStepResponse(entry_data=entry_data, new_records=new_records)
222
- except Exception as e:
223
- # TODO: Do something other than dump the full stack trace into the logs?
224
- # TODO: Eventually we'll return an is_success = false value.
215
+ return ProcessStepResponse(entry_data=entry_data, log=logs, new_records=new_records)
216
+ except Exception:
217
+ # TODO: Return a False success result after the response object has a field for it.
225
218
  return ProcessStepResponse(log=[traceback.format_exc()])
226
219
 
227
220
  @staticmethod
@@ -253,32 +246,40 @@ class ToolServiceBase(ToolServiceServicer, ABC):
253
246
 
254
247
  def _get_tools(self) -> list[ToolBase]:
255
248
  """
256
- Get the tools registered with this service.
249
+ return: Get the tools registered with this service.
257
250
  """
258
- # If the tools have not been registered, call the register_tools method to do so.
259
- if not hasattr(self, "tools"):
260
- self.tools = self.register_tools()
261
- # If the tools are still not registered, raise an exception.
262
- if not self.tools:
251
+ tools: list[ToolBase] = self.register_tools()
252
+ if not tools:
263
253
  raise Exception("No tools registered with this service.")
264
- return self.tools
254
+ return tools
255
+
256
+ def _get_tool(self, name: str) -> ToolBase:
257
+ """
258
+ Get a specific tool by its name.
259
+
260
+ :param name: The name of the tool to retrieve.
261
+ :return: The tool object corresponding to the given name.
262
+ """
263
+ tools: dict[str, ToolBase] = {x.name: x for x in self.register_tools()}
264
+ if not tools:
265
+ raise Exception("No tools registered with this service.")
266
+ if name not in tools:
267
+ raise Exception(f"Tool \"{name}\" not found in registered tools.")
268
+ return tools[name]
265
269
 
266
270
  @abstractmethod
267
271
  def register_tools(self) -> list[ToolBase]:
268
272
  """
269
- Register the tools with this service.
273
+ Register the tools with this service. Create and instantiate ToolBase subclasses to register them.
274
+
275
+ :return: A list of tools to register to this service.
270
276
  """
271
277
  pass
272
278
 
273
- @abstractmethod
274
- def get_details(self, user: SapioUser, request: ToolDetailsRequest, context: ServicerContext) -> list[ToolDetails]:
279
+ def get_details(self) -> list[ToolDetails]:
275
280
  """
276
281
  Get the details of the tool.
277
282
 
278
- :param user: A user object that can be used to initialize manager classes using DataMgmtServer to query the
279
- system.
280
- :param request: The request object containing the input data.
281
- :param context: The gRPC context.
282
283
  :return: A ToolDetailsResponse object containing the tool details.
283
284
  """
284
285
  tool_details: list[ToolDetails] = []
@@ -286,8 +287,8 @@ class ToolServiceBase(ToolServiceServicer, ABC):
286
287
  tool_details.append(tool.to_proto())
287
288
  return tool_details
288
289
 
289
- @abstractmethod
290
- def run(self, user: SapioUser, request: ProcessStepRequest, context: ServicerContext) -> list[SapioToolResult]:
290
+ def run(self, user: SapioUser, request: ProcessStepRequest, context: ServicerContext) \
291
+ -> tuple[bool, list[SapioToolResult], list[str]]:
291
292
  """
292
293
  Execute a tool from this service.
293
294
 
@@ -295,32 +296,48 @@ class ToolServiceBase(ToolServiceServicer, ABC):
295
296
  system.
296
297
  :param request: The request object containing the input data.
297
298
  :param context: The gRPC context.
298
- :return: A SapioToolResults object containing the response data.
299
+ :return: Whether or not the tool succeeded, the results of the tool, and any logs generated by the tool.
299
300
  """
300
- for tool in self._get_tools():
301
- if request.tool_name == tool.name:
302
- return tool.run(user, request, context)
303
- raise Exception(f"Tool {request.tool_name} not found in registered tools.")
301
+ tool = self._get_tool(request.tool_name)
302
+ try:
303
+ results = tool.run(user, request, context)
304
+ return True, results, tool.logs
305
+ except Exception:
306
+ tool.log_message(traceback.format_exc())
307
+ return False, [], tool.logs
304
308
 
305
309
 
306
310
  class ToolBase(ABC):
311
+ """
312
+ A base class for implementing a tool.
313
+ """
307
314
  name: str
308
315
  description: str
309
316
  data_type_name: str | None
310
317
  inputs: list[ToolInputDetails]
311
318
  outputs: list[ToolOutputDetails]
312
319
  configs: list[VeloxFieldDefProto]
320
+ logs: list[str]
313
321
 
314
322
  def __init__(self, name: str, description: str, data_type_name: str | None = None):
323
+ """
324
+ :param name: The name of the tool.
325
+ :param description: A description of the tool.
326
+ :param data_type_name: The name of the output data type of this tool, if applicable. When this tool returns
327
+ FieldMapResult objects in its run method, this name will be used to set the data type of the output data.
328
+ """
315
329
  self.name = name
316
330
  self.description = description
317
331
  self.data_type_name = data_type_name
318
332
  self.inputs = []
319
333
  self.outputs = []
334
+ self.configs = []
335
+ self.logs = []
320
336
 
321
337
  def add_input(self, details: ToolInputDetails) -> None:
322
338
  """
323
- Add an input configuration to the tool.
339
+ Add an input configuration to the tool. This determines how many inputs this tool will accept in the plan
340
+ manager, as well as what those inputs are.
324
341
 
325
342
  :param details: The input configuration details.
326
343
  """
@@ -328,7 +345,8 @@ class ToolBase(ABC):
328
345
 
329
346
  def add_output(self, details: ToolOutputDetails) -> None:
330
347
  """
331
- Add an output configuration to the tool.
348
+ Add an output configuration to the tool. This determines how many outputs this tool will return in the plan
349
+ manager, as well as what those outputs are.
332
350
 
333
351
  :param details: The output configuration details.
334
352
  """
@@ -336,17 +354,119 @@ class ToolBase(ABC):
336
354
 
337
355
  def add_config_field(self, field: VeloxFieldDefProto) -> None:
338
356
  """
339
- Add a configuration field to the tool.
357
+ Add a configuration field to the tool. This field will be used to configure the tool in the plan manager.
340
358
 
341
359
  :param field: The configuration field details.
342
360
  """
343
361
  self.configs.append(field)
344
362
 
363
+ def add_integer_config_field(self, field_name: str, display_name: str, description: str,
364
+ default_value: int, min_value: int = -2**31, max_value: int = 2**31-1) -> None:
365
+ """
366
+ Add an integer configuration field to the tool. This field will be used to configure the tool in the plan
367
+ manager.
368
+
369
+ :param field_name: The name of the field.
370
+ :param display_name: The display name of the field.
371
+ :param description: The description of the field.
372
+ :param default_value: The default value of the field.
373
+ :param min_value: The minimum value of the field.
374
+ :param max_value: The maximum value of the field.
375
+ """
376
+ self.configs.append(VeloxFieldDefProto(
377
+ data_field_type=FieldTypeProto.INTEGER,
378
+ data_field_name=field_name,
379
+ display_name=display_name,
380
+ description=description,
381
+ required=True,
382
+ editable=True,
383
+ integer_properties=IntegerProperties(
384
+ default_value=default_value,
385
+ min_value=min_value,
386
+ max_value=max_value
387
+ )
388
+ ))
389
+
390
+ def add_double_config_field(self, field_name: str, display_name: str, description: str, default_value: float,
391
+ min_value: float = -10.**120, max_value: float = 10.**120, precision: int = 2) -> None:
392
+ """
393
+ Add a double configuration field to the tool. This field will be used to configure the tool in the plan
394
+ manager.
395
+
396
+ :param field_name: The name of the field.
397
+ :param display_name: The display name of the field.
398
+ :param description: The description of the field.
399
+ :param default_value: The default value of the field.
400
+ :param min_value: The minimum value of the field.
401
+ :param max_value: The maximum value of the field.
402
+ :param precision: The precision of the field.
403
+ """
404
+ self.configs.append(VeloxFieldDefProto(
405
+ data_field_type=FieldTypeProto.DOUBLE,
406
+ data_field_name=field_name,
407
+ display_name=display_name,
408
+ description=description,
409
+ required=True,
410
+ editable=True,
411
+ double_properties=DoubleProperties(
412
+ default_value=default_value,
413
+ min_value=min_value,
414
+ max_value=max_value,
415
+ precision=precision
416
+ )
417
+ ))
418
+
419
+ def add_list_config_field(self, field_name: str, display_name: str, description: str, default_value: str,
420
+ allowed_values: list[str]) -> None:
421
+ """
422
+ Add a list configuration field to the tool. This field will be used to configure the tool in the plan
423
+ manager.
424
+
425
+ :param field_name: The name of the field.
426
+ :param display_name: The display name of the field.
427
+ :param description: The description of the field.
428
+ :param default_value: The default value of the field.
429
+ :param allowed_values: The list of allowed values for the field.
430
+ """
431
+ self.configs.append(VeloxFieldDefProto(
432
+ data_field_type=FieldTypeProto.SELECTION,
433
+ data_field_name=field_name,
434
+ display_name=display_name,
435
+ description=description,
436
+ required=True,
437
+ editable=True,
438
+ selection_properties=SelectionProperties(
439
+ default_value=default_value,
440
+ static_list_values=allowed_values,
441
+ )
442
+ ))
443
+
444
+ def add_boolean_config_field(self, field_name: str, display_name: str, description: str, default_value: bool) \
445
+ -> None:
446
+ """
447
+ Add a boolean configuration field to the tool. This field will be used to configure the tool in the plan
448
+ manager.
449
+
450
+ :param field_name: The name of the field.
451
+ :param display_name: The display name of the field.
452
+ :param description: The description of the field.
453
+ :param default_value: The default value of the field.
454
+ """
455
+ self.configs.append(VeloxFieldDefProto(
456
+ data_field_type=FieldTypeProto.BOOLEAN,
457
+ data_field_name=field_name,
458
+ display_name=display_name,
459
+ description=description,
460
+ required=True,
461
+ editable=True,
462
+ boolean_properties=BooleanProperties(
463
+ default_value=default_value
464
+ )
465
+ ))
466
+
345
467
  def to_proto(self) -> ToolDetails:
346
468
  """
347
- Convert this tool to a ToolDetails proto object.
348
-
349
- :return: The ToolDetails proto object.
469
+ :return: The ToolDetails proto object representing this tool.
350
470
  """
351
471
  return ToolDetails(
352
472
  name=self.name,
@@ -357,6 +477,14 @@ class ToolBase(ABC):
357
477
  config_fields=self.configs
358
478
  )
359
479
 
480
+ def log_message(self, message: str) -> None:
481
+ """
482
+ Log a message for this tool. This message will be included in the logs returned to the caller.
483
+
484
+ :param message: The message to log.
485
+ """
486
+ self.logs.append(message)
487
+
360
488
  @abstractmethod
361
489
  def run(self, user: SapioUser, request: ProcessStepRequest, context: ServicerContext) -> list[SapioToolResult]:
362
490
  """
@@ -369,3 +497,56 @@ class ToolBase(ABC):
369
497
  :return: A SapioToolResults object containing the response data.
370
498
  """
371
499
  pass
500
+
501
+ @staticmethod
502
+ def get_request_json(request: ProcessStepRequest, index: int = 0) -> list[Any]:
503
+ """
504
+ Parse the JSON data from the request object.
505
+
506
+ :param request: The request object containing the input data.
507
+ :param index: The index of the input to parse. Defaults to 0.
508
+ :return: A list of parsed JSON objects. Each entry in the list represents a separate JSON entry from the input.
509
+ """
510
+ entry_data = request.entry_data
511
+ input_data: list[Any] = [json.loads(x) for x in entry_data[index].entry_data.json_data.entries]
512
+ return input_data
513
+
514
+ @staticmethod
515
+ def read_from_json(json_data: list[dict[str, Any]], key: str) -> list[Any]:
516
+ """
517
+ From a list of dictionaries, return a list of values for the given key from each dictionary. Skips null values.
518
+
519
+ :param json_data: The JSON data to read from.
520
+ :param key: The key to read the values from.
521
+ :return: A list of values corresponding to the given key in the JSON data.
522
+ """
523
+ ret_val: list[Any] = []
524
+ for entry in json_data:
525
+ if key in entry:
526
+ value = entry[key]
527
+ if isinstance(value, list):
528
+ ret_val.extend(value)
529
+ elif value is not None:
530
+ ret_val.append(value)
531
+ return ret_val
532
+
533
+ @staticmethod
534
+ def get_config_fields(request: ProcessStepRequest) -> dict[str, Any]:
535
+ """
536
+ Get the configuration fields from the request object.
537
+
538
+ :param request: The request object containing the input data.
539
+ :return: A dictionary of configuration field names and their values.
540
+ """
541
+ config_fields: dict[str, Any] = {}
542
+ for field, value in request.config_field_values.items():
543
+ if value.bool_value is not None:
544
+ value = value.bool_value
545
+ elif value.string_value is not None:
546
+ value = value.string_value
547
+ elif value.int_value is not None:
548
+ value = value.int_value
549
+ elif value.double_value is not None:
550
+ value = value.double_value
551
+ config_fields[field] = value
552
+ return config_fields
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.4.24a494
3
+ Version: 2025.4.25a496
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -1,7 +1,7 @@
1
1
  sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  sapiopycommons/ai/tool_of_tools.py,sha256=zYmQ4rNX-qYQnc-vNDnYZjtv9JgmQAmVVuHfVOdBF3w,46984
4
- sapiopycommons/ai/tool_service_base.py,sha256=tftW0TFatccbx47QtfiKcsZaPpOHoOlEfDlrU_IQVUU,14376
4
+ sapiopycommons/ai/tool_service_base.py,sha256=1SwXGsqT4ju3qpEQObNU7FWkAjxBlu4Ctxt7dcu9-JQ,22052
5
5
  sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py,sha256=qPkyQsREtTLMliV9JB6tC5-NhmdWVWHJr70XNfcAfDI,20605
6
6
  sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi,sha256=gVXRsuscx9XavKsTcepzXWf0LDAAyQ_J5ZjFK6kPYuo,34028
7
7
  sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py,sha256=4vD4jWanaJ4uclSkFmS7JIz_lwYXDWBE3DomuPjUyII,941
@@ -82,7 +82,7 @@ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
82
82
  sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
83
83
  sapiopycommons/webhook/webhook_handlers.py,sha256=L0HetSm43NvA5KyW3xbLpGFh2DbAaeZJVtXIEl2fvV8,39689
84
84
  sapiopycommons/webhook/webservice_handlers.py,sha256=Y5dHx_UFWFuSqaoPL6Re-fsKYRuxvCWZ8bj6KSZ3jfM,14285
85
- sapiopycommons-2025.4.24a494.dist-info/METADATA,sha256=sLGtVufAGHGHXHpjTPI4TYwjcG1ya14ACJRNVWkLPHw,3143
86
- sapiopycommons-2025.4.24a494.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
87
- sapiopycommons-2025.4.24a494.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
88
- sapiopycommons-2025.4.24a494.dist-info/RECORD,,
85
+ sapiopycommons-2025.4.25a496.dist-info/METADATA,sha256=OP1zX7iUcVnA1H5hQFt5yD_ldzTXOsYZi400tQQWNsU,3143
86
+ sapiopycommons-2025.4.25a496.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
87
+ sapiopycommons-2025.4.25a496.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
88
+ sapiopycommons-2025.4.25a496.dist-info/RECORD,,