sapiopycommons 2025.10.16a785__py3-none-any.whl → 2025.10.17a787__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.

Files changed (50) hide show
  1. sapiopycommons/ai/tool_of_tools.py +917 -0
  2. sapiopycommons/callbacks/callback_util.py +18 -4
  3. sapiopycommons/files/file_util.py +1 -128
  4. sapiopycommons/general/aliases.py +3 -0
  5. sapiopycommons/webhook/webservice_handlers.py +1 -1
  6. {sapiopycommons-2025.10.16a785.dist-info → sapiopycommons-2025.10.17a787.dist-info}/METADATA +1 -1
  7. {sapiopycommons-2025.10.16a785.dist-info → sapiopycommons-2025.10.17a787.dist-info}/RECORD +9 -49
  8. sapiopycommons/ai/agent_service_base.py +0 -1226
  9. sapiopycommons/ai/converter_service_base.py +0 -163
  10. sapiopycommons/ai/external_credentials.py +0 -128
  11. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.py +0 -41
  12. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.pyi +0 -35
  13. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2_grpc.py +0 -24
  14. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py +0 -43
  15. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi +0 -31
  16. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py +0 -24
  17. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py +0 -123
  18. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi +0 -598
  19. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py +0 -24
  20. sapiopycommons/ai/protoapi/plan/converter/converter_pb2.py +0 -51
  21. sapiopycommons/ai/protoapi/plan/converter/converter_pb2.pyi +0 -63
  22. sapiopycommons/ai/protoapi/plan/converter/converter_pb2_grpc.py +0 -149
  23. sapiopycommons/ai/protoapi/plan/item/item_container_pb2.py +0 -55
  24. sapiopycommons/ai/protoapi/plan/item/item_container_pb2.pyi +0 -90
  25. sapiopycommons/ai/protoapi/plan/item/item_container_pb2_grpc.py +0 -24
  26. sapiopycommons/ai/protoapi/plan/script/script_pb2.py +0 -61
  27. sapiopycommons/ai/protoapi/plan/script/script_pb2.pyi +0 -108
  28. sapiopycommons/ai/protoapi/plan/script/script_pb2_grpc.py +0 -153
  29. sapiopycommons/ai/protoapi/plan/step_output_pb2.py +0 -45
  30. sapiopycommons/ai/protoapi/plan/step_output_pb2.pyi +0 -42
  31. sapiopycommons/ai/protoapi/plan/step_output_pb2_grpc.py +0 -24
  32. sapiopycommons/ai/protoapi/plan/step_pb2.py +0 -43
  33. sapiopycommons/ai/protoapi/plan/step_pb2.pyi +0 -43
  34. sapiopycommons/ai/protoapi/plan/step_pb2_grpc.py +0 -24
  35. sapiopycommons/ai/protoapi/plan/tool/entry_pb2.py +0 -41
  36. sapiopycommons/ai/protoapi/plan/tool/entry_pb2.pyi +0 -35
  37. sapiopycommons/ai/protoapi/plan/tool/entry_pb2_grpc.py +0 -24
  38. sapiopycommons/ai/protoapi/plan/tool/tool_pb2.py +0 -79
  39. sapiopycommons/ai/protoapi/plan/tool/tool_pb2.pyi +0 -261
  40. sapiopycommons/ai/protoapi/plan/tool/tool_pb2_grpc.py +0 -154
  41. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py +0 -39
  42. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi +0 -32
  43. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py +0 -24
  44. sapiopycommons/ai/protobuf_utils.py +0 -504
  45. sapiopycommons/ai/request_validation.py +0 -470
  46. sapiopycommons/ai/server.py +0 -152
  47. sapiopycommons/ai/test_client.py +0 -446
  48. sapiopycommons/files/temp_files.py +0 -82
  49. {sapiopycommons-2025.10.16a785.dist-info → sapiopycommons-2025.10.17a787.dist-info}/WHEEL +0 -0
  50. {sapiopycommons-2025.10.16a785.dist-info → sapiopycommons-2025.10.17a787.dist-info}/licenses/LICENSE +0 -0
@@ -1,470 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC
4
- from typing import Any, Callable
5
-
6
- from sapiopycommons.ai.test_client import ContainerType
7
- from sapiopycommons.ai.agent_service_base import AgentBase
8
-
9
-
10
- class InputValidation(ABC):
11
- """
12
- A base class for validating the input to an agent.
13
- """
14
- index: int
15
- max_entries: int | None
16
- allow_empty_input: bool
17
- allow_empty_entries: bool
18
-
19
- def __init__(self, index: int, max_entries: int | None = None,
20
- allow_empty_input: bool = False, allow_empty_entries: bool = False):
21
- """
22
- :param index: The index of the input to validate.
23
- :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
24
- :param allow_empty_input: If true, then the input can be completely empty.
25
- :param allow_empty_entries: If true, then individual entries in the input can be empty.
26
- """
27
- self.index = index
28
- self.max_entries = max_entries
29
- self.allow_empty_input = allow_empty_input
30
- self.allow_empty_entries = allow_empty_entries
31
-
32
-
33
- class BinaryValidation(InputValidation):
34
- """
35
- A class representing a validation requirement for a binary input.
36
- """
37
- func: Callable[[int, bytes], list[str]] | None
38
-
39
- def __init__(self, index: int, max_entries: int | None = None,
40
- allow_empty_input: bool = False, allow_empty_entries: bool = False,
41
- func: Callable[[int, bytes], list[str]] | None = None):
42
- """
43
- :param index: The index of the input to validate.
44
- :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
45
- :param allow_empty_input: If true, then the input can be completely empty.
46
- :param allow_empty_entries: If true, then individual entries in the input can be empty
47
- :param func: An optional function to run on each entry in the input. The function should take the index of
48
- the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
49
- entry is valid, the function should return an empty list. This function will not be called if the input or
50
- entry are empty.
51
- """
52
- super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
53
- self.func = func
54
-
55
-
56
- class CsvValidation(InputValidation):
57
- """
58
- A class representing a validation requirement for a CSV input.
59
- """
60
- required_headers: list[str] | None = None
61
-
62
- func: Callable[[int, dict[str, Any]], list[str]] | None
63
-
64
- def __init__(self, index: int, max_entries: int | None = None,
65
- allow_empty_input: bool = False, allow_empty_entries: bool = False,
66
- required_headers: list[str] | None = None,
67
- func: Callable[[int, dict[str, Any]], list[str]] | None = None):
68
- """
69
- :param index: The index of the input to validate.
70
- :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
71
- :param allow_empty_input: If true, then the input can be completely empty.
72
- :param allow_empty_entries: If true, then individual entries in the input can be empty.
73
- :param required_headers: A list of headers that must be present in the CSV input. If None, then no header
74
- validation will be performed.
75
- :param func: An optional function to run on each entry in the input. The function should take the index of
76
- the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
77
- entry is valid, the function should return an empty list. This function will not be called if the input or
78
- entry are empty.
79
- """
80
- super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
81
- self.required_headers = required_headers
82
- self.func = func
83
-
84
-
85
- class JsonValidation(InputValidation):
86
- """
87
- A class representing a validation requirement for a JSON input.
88
- """
89
- json_requirements: dict[str, JsonKeyValidation]
90
-
91
- func: Callable[[int, dict[str, Any]], list[str]] | None
92
-
93
- def __init__(self, index: int, max_entries: int | None = None,
94
- allow_empty_input: bool = False, allow_empty_entries: bool = False,
95
- json_requirements: list[JsonKeyValidation] | None = None,
96
- func: Callable[[int, dict[str, Any]], list[str]] | None = None):
97
- """
98
- :param index: The index of the input to validate.
99
- :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
100
- :param allow_empty_input: If true, then the input can be completely empty.
101
- :param allow_empty_entries: If true, then individual entries in the input can be empty.
102
- :param json_requirements: A list of JSON requirements to validate for JSON inputs. Each requirement
103
- specifies a key to validate, the expected type of the value for that key, and any nested requirements
104
- for that key. Only applicable to JSON inputs.
105
- :param func: An optional function to run on each entry in the input. The function should take the index of
106
- the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
107
- entry is valid, the function should return an empty list. This function will not be called if the input or
108
- entry are empty.
109
- """
110
- super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
111
- self.json_requirements = {}
112
- if json_requirements:
113
- for req in json_requirements:
114
- if req.key in self.json_requirements:
115
- raise ValueError(f"Duplicate JSON requirement key {req.key} for input index {index}.")
116
- self.json_requirements[req.key] = req
117
-
118
- self.func = func
119
-
120
-
121
- class JsonKeyValidation:
122
- """
123
- A class representing a validation requirement for a specific key in a JSON input.
124
- """
125
- key: str
126
- json_type: type
127
- required: bool
128
- allow_empty: bool
129
-
130
- list_type: type | None = None
131
- nested_requirements: dict[str, JsonKeyValidation]
132
-
133
- func: Callable[[str, Any], list[str]] | None = None
134
-
135
- def __init__(self, key: str, json_type: type, required: bool = True, allow_empty: bool = False,
136
- list_type: type | None = None, nested_requirements: list[JsonKeyValidation] | None = None,
137
- func: Callable[[str, Any], list[str]] | None = None):
138
- """
139
- :param key: The key in the JSON input to validate.
140
- :param json_type: The expected type of the value for this key. This should be one of: str, int, float, bool,
141
- list, or dict.
142
- :param required: If true, then this key must be present in the JSON input. If false, then the key is optional,
143
- but if present, it must still match the other expected criteria.
144
- :param allow_empty: If true, then the value for this key can be empty (e.g., an empty string, list, or dict).
145
- If false, then the value must not be empty.
146
- :param list_type: The expected type of the entries in the list if json_type is list.
147
- :param nested_requirements: A list of nested JSON requirements to validate for this key if it is a dict. Each
148
- requirement specifies a key to validate, the expected type of the value for that key, and any nested
149
- requirements for that key. Only applicable if json_type is dict, or if json_type is list and list_type is
150
- dict.
151
- :param func: An optional function to run on the value for this key. The function should take the path and the
152
- value as arguments, and return a list of error messages if the value is not valid. If the value is valid,
153
- the function should return an empty list. This function will not be called if the key is missing,
154
- the value is of the wrong type, or the value is an empty str/list/dict and allow_empty is false.
155
- """
156
- self.key = key
157
- self.json_type = json_type
158
- self.required = required
159
- self.allow_empty = allow_empty
160
-
161
- self.list_type = list_type
162
- self.nested_requirements = {}
163
- if nested_requirements:
164
- for req in nested_requirements:
165
- if req.key in self.nested_requirements:
166
- raise ValueError(f"Duplicate nested requirement key {req.key} for JSON key {key}.")
167
- self.nested_requirements[req.key] = req
168
-
169
- self.func = func
170
-
171
- allowed_types: set[type] = {str, int, float, bool, list, dict}
172
- if self.json_type not in allowed_types:
173
- raise ValueError(f"Invalid json_type {self.json_type} for key {key}. Must be one of: "
174
- f"{', '.join([t.__name__ for t in allowed_types])}.")
175
- if self.list_type is not None and self.list_type not in allowed_types:
176
- raise ValueError(f"Invalid list_type {self.list_type} for key {key}. Must be one of: "
177
- f"{', '.join([t.__name__ for t in allowed_types])}.")
178
-
179
-
180
- class TextValidation(InputValidation):
181
- """
182
- A class representing a validation requirement for a text input.
183
- """
184
- flatten: bool
185
- disallowed_characters: str | None = None
186
- regex: str | None = None
187
-
188
- func: Callable[[int, str], list[str]] | None = None
189
-
190
- def __init__(self, index: int, max_entries: int | None = None,
191
- allow_empty_input: bool = False, allow_empty_entries: bool = False, flatten: bool = False,
192
- disallow_characters: str | None = None, regex: str | None = None,
193
- func: Callable[[int, str], list[str]] | None = None):
194
- """
195
- :param index: The index of the input to validate.
196
- :param max_entries: The maximum number of entries allowed for this input. If None, then there is no limit.
197
- :param allow_empty_input: If true, then the input can be completely empty.
198
- :param allow_empty_entries: If true, then individual entries in the input can be empty.
199
- :param flatten: If true, then the input will be flattened before validation
200
- :param disallow_characters: A string of characters that are not allowed in any entry in the input. If None,
201
- then no character validation will be performed. This parameter will not be used if the input or entry are
202
- empty.
203
- :param regex: An optional regular expression that each entry in the input must fully match. If None, then no
204
- regex validation will be performed. This parameter will not be used if the input or entry are empty.
205
- :param func: An optional function to run on each entry in the input. The function should take the index of
206
- the input and the entry as arguments, and return a list of error messages if the entry is not valid. If the
207
- entry is valid, the function should return an empty list. The function will only be called if the entry
208
- passes those previous checks (e.g. not empty, doesn't include disallowed characters,
209
- passes the regex, etc.).
210
- """
211
- super().__init__(index, max_entries, allow_empty_input, allow_empty_entries)
212
- self.flatten = flatten
213
- self.disallowed_characters = disallow_characters
214
- self.regex = regex
215
- self.func = func
216
-
217
-
218
-
219
- class InputValidator:
220
- """
221
- A class for validating the inputs to an agent based on their container types and specified validation requirements.
222
- """
223
- agent: AgentBase
224
- requirements: dict[int, InputValidation]
225
-
226
- def __init__(self, agent: AgentBase, requirements: list[InputValidation] | None = None):
227
- """
228
- :param agent: The agent to validate the request of.
229
- :param requirements: A list of validation requirements to apply to the request. If a validation object is
230
- not provided for a given input, then default validation will be applied. Default validation requires that
231
- the input is not empty, and that the entries in the input are not empty.
232
- """
233
- self.agent = agent
234
- self.requirements = {}
235
- for req in requirements:
236
- if req.index < 0 or req.index >= len(agent.input_configs):
237
- raise ValueError(f"Validation requirement index {req.index} is out of range for agent "
238
- f"{agent.name()} with {len(agent.input_configs)} inputs.")
239
- if req.index in self.requirements:
240
- raise ValueError(f"Duplicate validation requirement index {req.index} for agent {agent.name()}.")
241
- self.requirements[req.index] = req
242
-
243
- def run(self) -> list[str]:
244
- """
245
- Run simple validation on all the inputs based on their container types. This requires the following:
246
- - The input may not be empty.
247
- - The entries in the input may not be empty, unless allow_empty is set to true.
248
- - If provided, the number of entries in the input may not exceed a maximum size.
249
- - If provided, certain keys must be present in the JSON input, and they must match the above behavior.
250
-
251
- :return: A list of the error messages if the request is not valid. If the request is valid, return an empty
252
- list.
253
- """
254
- errors: list[str] = []
255
- for i, input_type in enumerate(self.agent.input_container_types):
256
- match input_type:
257
- case ContainerType.BINARY:
258
- r: InputValidation = self.requirements.get(i, BinaryValidation(i))
259
- if not isinstance(r, BinaryValidation):
260
- raise ValueError(f"Validation requirement for binary input at index {i} must be a "
261
- f"BinaryValidation object. Got {type(r)} instead.")
262
- errors.extend(self.validate_input_binary(i, r))
263
- case ContainerType.CSV:
264
- r: InputValidation = self.requirements.get(i, CsvValidation(i))
265
- if not isinstance(r, CsvValidation):
266
- raise ValueError(f"Validation requirement for CSV input at index {i} must be a "
267
- f"CsvValidation object. Got {type(r)} instead.")
268
- errors.extend(self.validate_input_csv(i, r))
269
- case ContainerType.JSON:
270
- r: InputValidation = self.requirements.get(i, JsonValidation(i))
271
- if not isinstance(r, JsonValidation):
272
- raise ValueError(f"Validation requirement for JSON input at index {i} must be a "
273
- f"JsonValidation object. Got {type(r)} instead.")
274
- errors.extend(self.validate_input_json(i, r))
275
- case ContainerType.TEXT:
276
- r: InputValidation = self.requirements.get(i, TextValidation(i))
277
- if not isinstance(r, TextValidation):
278
- raise ValueError(f"Validation requirement for text input at index {i} must be a "
279
- f"TextValidation object. Got {type(r)} instead.")
280
- errors.extend(self.validate_input_text(i, r))
281
- return errors
282
-
283
- def validate_input_binary(self, index: int, r: BinaryValidation) -> list[str]:
284
- """
285
- Run simple validation on the binary input at the given index.
286
-
287
- :param index: The index of the input to validate.
288
- :param r: The validation requirement to use for this input.
289
- :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
290
- """
291
- input_files: list[bytes] = self.agent.get_input_binary(index)
292
- errors: list[str] = []
293
- if not input_files:
294
- if not r.allow_empty_input:
295
- errors.append(f"Input {index} is empty.")
296
- elif r.max_entries is not None and len(input_files) > r.max_entries:
297
- errors.append(f"Input {index} contains {len(input_files)} entries, which exceeds the maximum allowed "
298
- f"number of {r.max_entries}.")
299
- elif not r.allow_empty_entries or r.func:
300
- for i, entry in enumerate(input_files):
301
- if not entry.strip():
302
- if not r.allow_empty_entries:
303
- errors.append(f"Entry {i} of input {index} is empty or contains only whitespace.")
304
- elif r.func:
305
- errors.extend(r.func(i, entry))
306
- return errors
307
-
308
- def validate_input_csv(self, index: int, r: CsvValidation) -> list[str]:
309
- """
310
- Run simple validation on the CSV input at the given index.
311
-
312
- :param index: The index of the input to validate.
313
- :param r: The validation requirement to use for this input.
314
- :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
315
- """
316
- headers, csv = self.agent.get_input_csv(index)
317
- headers: list[str]
318
- csv: list[dict[str, Any]]
319
-
320
- errors: list[str] = []
321
- if r.required_headers:
322
- missing_headers: list[str] = [h for h in r.required_headers if h not in headers]
323
- if missing_headers:
324
- errors.append(f"Input {index} is missing required headers: {', '.join(missing_headers)}.")
325
-
326
- if not csv:
327
- if not r.allow_empty_input:
328
- errors.append(f"Input {index} is empty.")
329
- elif r.max_entries is not None and len(csv) > r.max_entries:
330
- errors.append(f"Input {index} contains {len(csv)} entries, which exceeds the maximum allowed "
331
- f"number of {r.max_entries}.")
332
- elif not r.allow_empty_entries or r.func:
333
- for i, entry in enumerate(csv):
334
- if not entry or all(not cell.strip() for cell in entry):
335
- if not r.allow_empty_entries:
336
- errors.append(f"Entry {i} of input {index} is empty or contains only whitespace.")
337
- elif r.func:
338
- errors.extend(r.func(i, entry))
339
- return errors
340
-
341
- def validate_input_json(self, index: int, r: JsonValidation) -> list[str]:
342
- """
343
- Run simple validation on the JSON input at the given index.
344
-
345
- :param index: The index of the input to validate.
346
- :param r: The validation requirement to use for this input.
347
- :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
348
- """
349
- input_json: list[dict[str, Any]] = self.agent.get_input_json(index)
350
- errors: list[str] = []
351
- if not input_json:
352
- if not r.allow_empty_input:
353
- errors.append(f"Input {index} is empty.")
354
- elif r.max_entries is not None and len(input_json) > r.max_entries:
355
- errors.append(f"Input {index} contains {len(input_json)} entries, which exceeds the maximum allowed "
356
- f"number of {r.max_entries}.")
357
- elif not r.allow_empty_entries or r.func:
358
- for i, entry in enumerate(input_json):
359
- if not entry:
360
- if not r.allow_empty_entries:
361
- errors.append(f"Entry {i} of input {index} is empty.")
362
- elif r.func:
363
- errors.extend(r.func(i, entry))
364
-
365
- for key, rk in r.json_requirements.items():
366
- for i, entry in enumerate(input_json):
367
- errors.extend(self._validate_input_json_key(entry, rk, f"input[{index}][{i}]"))
368
-
369
- return errors
370
-
371
- def _validate_input_json_key(self, data: dict[str, Any], rk: JsonKeyValidation, path: str) -> list[str]:
372
- """
373
- Recursively validate a JSON key in a JSON object.
374
-
375
- :param data: The JSON object to validate.
376
- :param rk: The JSON key validation requirement to use.
377
- :param path: The path to the current JSON object, for error reporting.
378
- :return: A list of error messages if the JSON object is not valid. If the JSON object is valid, return an empty
379
- list.
380
- """
381
- errors: list[str] = []
382
- if rk.key not in data:
383
- if rk.required:
384
- errors.append(f"Missing required key '{rk.key}' at path '{path}'.")
385
- return errors
386
-
387
- value: Any = data[rk.key]
388
- if not isinstance(value, rk.json_type):
389
- errors.append(f"Key '{rk.key}' at path '{path}' is expected to be of type "
390
- f"{rk.json_type.__name__}, but got {type(value).__name__}.")
391
- return errors
392
-
393
- if isinstance(value, (str, list, dict)) and not value:
394
- if not rk.allow_empty:
395
- errors.append(f"Key '{rk.key}' at path '{path}' is empty, but empty values are not allowed.")
396
- return errors
397
-
398
- correct_type: bool = True
399
- if rk.json_type is list and rk.list_type is not None:
400
- if not isinstance(value, list):
401
- raise RuntimeError("This should never happen; value was already checked to be of type list.")
402
- for i, item in enumerate(value):
403
- if not isinstance(item, rk.list_type):
404
- errors.append(f"Entry {i} of list key '{rk.key}' at path '{path}' is expected to be of type "
405
- f"{rk.list_type.__name__}, but got {type(item).__name__}.")
406
- correct_type = False
407
- elif rk.list_type is dict and rk.nested_requirements:
408
- if not isinstance(item, dict):
409
- raise RuntimeError("This should never happen; item was already checked to be of type dict.")
410
- for nk, nrk in rk.nested_requirements.items():
411
- errors.extend(self._validate_input_json_key(item, nrk, f"{path}.{rk.key}[{i}]"))
412
-
413
- elif rk.json_type is dict and rk.nested_requirements:
414
- if not isinstance(value, dict):
415
- raise RuntimeError("This should never happen; value was already checked to be of type dict.")
416
- for nk, nrk in rk.nested_requirements.items():
417
- errors.extend(self._validate_input_json_key(value, nrk, f"{path}.{rk.key}"))
418
-
419
- if rk.func and correct_type:
420
- errors.extend(rk.func(f"{path}.{rk.key}", value))
421
-
422
- return errors
423
-
424
- def validate_input_text(self, index: int, r: TextValidation) -> list[str]:
425
- """
426
- Run simple validation on the binary input at the given index.
427
-
428
- :param index: The index of the input to validate.
429
- :param r: The validation requirement to use for this input.
430
- :return: A list of error messages if the input is not valid. If the input is valid, return an empty list.
431
- """
432
- input_text: list[str] = self.agent.get_input_text(index)
433
- if r.flatten:
434
- input_text = self.agent.flatten_text(input_text)
435
-
436
- errors: list[str] = []
437
- if not input_text:
438
- if not r.allow_empty_input:
439
- errors.append(f"Input {index} is empty.")
440
- elif r.max_entries is not None and len(input_text) > r.max_entries:
441
- errors.append(f"Input {index} contains {len(input_text)} entries, which exceeds the maximum allowed "
442
- f"number of {r.max_entries}.")
443
- elif not r.allow_empty_entries or r.regex or r.func:
444
- for i, entry in enumerate(input_text):
445
- if not entry.strip():
446
- if not r.allow_empty_entries:
447
- errors.append(f"Entry {i} of input {index} is empty or contains only whitespace.")
448
- elif r.disallowed_characters:
449
- for c in r.disallowed_characters:
450
- # Replace special characters with their escaped versions for better error messages.
451
- if c == "\r":
452
- c = r"\r"
453
- elif c == '\n':
454
- c = r"\n"
455
- elif c == "\t":
456
- c = r"\t"
457
- if c in entry:
458
- errors.append(f"Entry {i} of input {index} contains disallowed character '{c}'.")
459
- elif r.regex:
460
- import re
461
- if not re.fullmatch(r.regex, entry):
462
- errors.append(f"Entry {i} of input {index} does not fully match the expected regex format "
463
- f"{r.regex}.")
464
- elif r.func:
465
- errors.extend(r.func(i, entry))
466
- if errors and r.flatten:
467
- errors.append(f"Note that input flattening is enabled for input {index}, which may increase the number "
468
- f"of entries reported in the above errors. Flattening splits each entry on newlines, removes "
469
- f"empty lines, and iterates over every line in the input as opposed to each entry as a whole.")
470
- return errors
@@ -1,152 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from argparse import ArgumentParser
5
- from typing import Any
6
-
7
- import grpc
8
-
9
- from sapiopycommons.ai.converter_service_base import ConverterServiceBase
10
- from sapiopycommons.ai.protoapi.plan.converter.converter_pb2_grpc import add_ConverterServiceServicer_to_server, \
11
- ConverterServiceServicer
12
- from sapiopycommons.ai.protoapi.plan.script.script_pb2_grpc import add_ScriptServiceServicer_to_server, \
13
- ScriptServiceServicer
14
- from sapiopycommons.ai.protoapi.plan.tool.tool_pb2_grpc import add_ToolServiceServicer_to_server, ToolServiceServicer
15
- from sapiopycommons.ai.agent_service_base import AgentServiceBase
16
-
17
-
18
- class SapioGrpcServer:
19
- """
20
- A gRPC server for handling the various Sapio gRPC services.
21
- """
22
- port: int
23
- options: list[tuple[str, Any]]
24
- debug_mode: bool
25
- _converter_services: list[ConverterServiceServicer]
26
- _script_services: list[ScriptServiceServicer]
27
- _agent_services: list[ToolServiceServicer]
28
-
29
- @staticmethod
30
- def args_parser() -> ArgumentParser:
31
- """
32
- Create an argument parser for the gRPC server.
33
-
34
- :return: The argument parser.
35
- """
36
- parser = ArgumentParser()
37
- parser.add_argument("--debug_mode", "-d", action="store_true")
38
- parser.add_argument("--port", "-p", default=50051, type=int)
39
- parser.add_argument("--message_mb_size", "-s", default=1024, type=int)
40
- return parser
41
-
42
- @staticmethod
43
- def from_args(options: list[tuple[str, Any]] | None = None) -> SapioGrpcServer:
44
- return SapioGrpcServer(options=options, **vars(SapioGrpcServer.args_parser().parse_args()))
45
-
46
- def __init__(self, port: int = 50051, message_mb_size: int = 1024, debug_mode: bool = False,
47
- options: list[tuple[str, Any]] | None = None) -> None:
48
- """
49
- Initialize the gRPC server with the specified port and message size.
50
-
51
- :param port: The port to listen on for incoming gRPC requests.
52
- :param message_mb_size: The maximum size of a sent or received message in megabytes.
53
- :param debug_mode: Sets the debug mode for services.
54
- :param options: Additional gRPC server options to set. This should be a list of tuples where the first item is
55
- the option name and the second item is the option value.
56
- """
57
- if isinstance(port, str):
58
- port = int(port)
59
- self.port = port
60
- self.options = [
61
- ('grpc.max_send_message_length', message_mb_size * 1024 * 1024),
62
- ('grpc.max_receive_message_length', message_mb_size * 1024 * 1024)
63
- ]
64
- if options:
65
- self.options.extend(options)
66
- self.debug_mode = debug_mode
67
- if debug_mode:
68
- print("Debug mode is enabled.")
69
- self._converter_services = []
70
- self._script_services = []
71
- self._agent_services = []
72
-
73
- def update_message_size(self, message_mb_size: int) -> None:
74
- """
75
- Update the maximum message size for the gRPC server.
76
-
77
- :param message_mb_size: The new maximum message size in megabytes.
78
- """
79
- for i, (option_name, _) in enumerate(self.options):
80
- if option_name in ('grpc.max_send_message_length', 'grpc.max_receive_message_length'):
81
- self.options[i] = (option_name, message_mb_size * 1024 * 1024)
82
-
83
- def add_converter_service(self, service: ConverterServiceBase) -> None:
84
- """
85
- Add a converter service to the gRPC server.
86
-
87
- :param service: The converter service to register with the server.
88
- """
89
- service.debug_mode = self.debug_mode
90
- self._converter_services.append(service)
91
-
92
- def add_script_service(self, service: ScriptServiceServicer) -> None:
93
- """
94
- Add a script service to the gRPC server.
95
-
96
- :param service: The script service to register with the server.
97
- """
98
- self._script_services.append(service)
99
-
100
- def add_agent_service(self, service: AgentServiceBase) -> None:
101
- """
102
- Add an agent service to the gRPC server.
103
-
104
- :param service: The agent service to register with the server.
105
- """
106
- service.debug_mode = self.debug_mode
107
- self._agent_services.append(service)
108
-
109
- def start(self) -> None:
110
- """
111
- Start the gRPC server for the provided servicers.
112
- """
113
- if not (self._converter_services or self._script_services or self._agent_services):
114
- raise ValueError("No services have been added to the server. Use add_converter_service, add_script_service,"
115
- "or add_agent_service to register a service before starting the server.")
116
-
117
- async def serve():
118
- server = grpc.aio.server(options=self.options)
119
-
120
- for service in self._converter_services:
121
- print(f"Registering Converter service: {service.__class__.__name__}")
122
- add_ConverterServiceServicer_to_server(service, server)
123
- for service in self._script_services:
124
- print(f"Registering Script service: {service.__class__.__name__}")
125
- add_ScriptServiceServicer_to_server(service, server)
126
- for service in self._agent_services:
127
- print(f"Registering Agent service: {service.__class__.__name__}")
128
- add_ToolServiceServicer_to_server(service, server)
129
-
130
- from grpc_health.v1 import health_pb2, health_pb2_grpc
131
- from grpc_health.v1.health import HealthServicer
132
- health_servicer = HealthServicer()
133
- health_servicer.set("", health_pb2.HealthCheckResponse.ServingStatus.SERVING)
134
- health_servicer.set("ScriptService", health_pb2.HealthCheckResponse.ServingStatus.SERVING)
135
- health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
136
-
137
- server.add_insecure_port(f"[::]:{self.port}")
138
- await server.start()
139
- print(f"Server started, listening on {self.port}")
140
- try:
141
- await server.wait_for_termination()
142
- finally:
143
- print("Stopping server...")
144
- await server.stop(0)
145
- print("Server stopped.")
146
-
147
- try:
148
- asyncio.run(serve())
149
- except KeyboardInterrupt:
150
- print("Server stopped by user.")
151
- except Exception as e:
152
- print(f"An error occurred: {e}")