enkryptai-sdk 0.1.7__py3-none-any.whl → 1.0.1__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.
- enkryptai_sdk/datasets.py +8 -2
- enkryptai_sdk/dto/__init__.py +1 -0
- enkryptai_sdk/dto/red_team.py +91 -57
- enkryptai_sdk/red_team.py +126 -21
- {enkryptai_sdk-0.1.7.dist-info → enkryptai_sdk-1.0.1.dist-info}/METADATA +279 -134
- {enkryptai_sdk-0.1.7.dist-info → enkryptai_sdk-1.0.1.dist-info}/RECORD +9 -9
- {enkryptai_sdk-0.1.7.dist-info → enkryptai_sdk-1.0.1.dist-info}/WHEEL +1 -1
- {enkryptai_sdk-0.1.7.dist-info → enkryptai_sdk-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {enkryptai_sdk-0.1.7.dist-info → enkryptai_sdk-1.0.1.dist-info}/top_level.txt +0 -0
enkryptai_sdk/datasets.py
CHANGED
|
@@ -128,15 +128,21 @@ class DatasetClient(BaseClient):
|
|
|
128
128
|
response["dataset_name"] = dataset_name
|
|
129
129
|
return DatasetSummary.from_dict(response)
|
|
130
130
|
|
|
131
|
-
def list_datasets(self):
|
|
131
|
+
def list_datasets(self, status: str = None):
|
|
132
132
|
"""
|
|
133
133
|
Get a list of all available dataset tasks.
|
|
134
134
|
|
|
135
|
+
Args:
|
|
136
|
+
status (str): Filter the list of dataset tasks by status
|
|
137
|
+
|
|
135
138
|
Returns:
|
|
136
139
|
dict: Response from the API containing the list of dataset tasks
|
|
137
140
|
"""
|
|
138
141
|
|
|
139
|
-
|
|
142
|
+
url = "/datasets/list-tasks"
|
|
143
|
+
if status:
|
|
144
|
+
url += f"?status={status}"
|
|
145
|
+
response = self._request("GET", url)
|
|
140
146
|
if response.get("error"):
|
|
141
147
|
raise DatasetClientError(response["error"])
|
|
142
148
|
return DatasetCollection.from_dict(response)
|
enkryptai_sdk/dto/__init__.py
CHANGED
enkryptai_sdk/dto/red_team.py
CHANGED
|
@@ -73,6 +73,28 @@ class StatisticItem(BaseDTO):
|
|
|
73
73
|
return d
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
@dataclass
|
|
77
|
+
class StatisticItemWithTestType(BaseDTO):
|
|
78
|
+
success_percentage: float
|
|
79
|
+
total: int
|
|
80
|
+
test_type: Optional[str] = None
|
|
81
|
+
_extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict) -> "StatisticItemWithTestType":
|
|
85
|
+
return cls(
|
|
86
|
+
success_percentage=data.get("success(%)", 0.0),
|
|
87
|
+
total=data.get("total", 0),
|
|
88
|
+
test_type=data.get("test_type", None),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> Dict:
|
|
92
|
+
d = super().to_dict()
|
|
93
|
+
# Special handling for success percentage key
|
|
94
|
+
d["success(%)"] = d.pop("success_percentage")
|
|
95
|
+
return d
|
|
96
|
+
|
|
97
|
+
|
|
76
98
|
@dataclass
|
|
77
99
|
class ResultSummary(BaseDTO):
|
|
78
100
|
test_date: str
|
|
@@ -86,7 +108,7 @@ class ResultSummary(BaseDTO):
|
|
|
86
108
|
test_type: Dict[str, StatisticItem]
|
|
87
109
|
nist_category: Dict[str, StatisticItem]
|
|
88
110
|
scenario: Dict[str, StatisticItem]
|
|
89
|
-
category: Dict[str,
|
|
111
|
+
category: Dict[str, StatisticItemWithTestType]
|
|
90
112
|
attack_method: Dict[str, StatisticItem]
|
|
91
113
|
_extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
92
114
|
|
|
@@ -98,6 +120,13 @@ class ResultSummary(BaseDTO):
|
|
|
98
120
|
for key, value in item.items():
|
|
99
121
|
result[key] = StatisticItem.from_dict(value)
|
|
100
122
|
return result
|
|
123
|
+
|
|
124
|
+
def convert_stat_test_type_list(stat_list: List[Dict]) -> Dict[str, StatisticItemWithTestType]:
|
|
125
|
+
result = {}
|
|
126
|
+
for item in stat_list:
|
|
127
|
+
for key, value in item.items():
|
|
128
|
+
result[key] = StatisticItemWithTestType.from_dict(value)
|
|
129
|
+
return result
|
|
101
130
|
|
|
102
131
|
return cls(
|
|
103
132
|
test_date=data.get("test_date", ""),
|
|
@@ -111,20 +140,23 @@ class ResultSummary(BaseDTO):
|
|
|
111
140
|
test_type=convert_stat_list(data.get("test_type", [])),
|
|
112
141
|
nist_category=convert_stat_list(data.get("nist_category", [])),
|
|
113
142
|
scenario=convert_stat_list(data.get("scenario", [])),
|
|
114
|
-
category=
|
|
143
|
+
category=convert_stat_test_type_list(data.get("category", [])),
|
|
115
144
|
attack_method=convert_stat_list(data.get("attack_method", [])),
|
|
116
145
|
)
|
|
117
146
|
|
|
118
147
|
def to_dict(self) -> Dict:
|
|
119
148
|
def convert_stat_dict(stat_dict: Dict[str, StatisticItem]) -> List[Dict]:
|
|
120
149
|
return [{key: value.to_dict()} for key, value in stat_dict.items()]
|
|
150
|
+
|
|
151
|
+
def convert_stat_test_type_dict(stat_dict: Dict[str, StatisticItemWithTestType]) -> List[Dict]:
|
|
152
|
+
return [{key: value.to_dict()} for key, value in stat_dict.items()]
|
|
121
153
|
|
|
122
154
|
d = super().to_dict()
|
|
123
155
|
# Convert stat dictionaries to lists of dictionaries
|
|
124
156
|
d["test_type"] = convert_stat_dict(self.test_type)
|
|
125
157
|
d["nist_category"] = convert_stat_dict(self.nist_category)
|
|
126
158
|
d["scenario"] = convert_stat_dict(self.scenario)
|
|
127
|
-
d["category"] =
|
|
159
|
+
d["category"] = convert_stat_test_type_dict(self.category)
|
|
128
160
|
d["attack_method"] = convert_stat_dict(self.attack_method)
|
|
129
161
|
return d
|
|
130
162
|
|
|
@@ -132,12 +164,17 @@ class ResultSummary(BaseDTO):
|
|
|
132
164
|
@dataclass
|
|
133
165
|
class RedTeamResultSummary(BaseDTO):
|
|
134
166
|
summary: ResultSummary
|
|
167
|
+
task_status: Optional[str] = None
|
|
135
168
|
_extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
136
169
|
|
|
137
170
|
@classmethod
|
|
138
171
|
def from_dict(cls, data: Dict) -> "RedTeamResultSummary":
|
|
139
172
|
if not data or "summary" not in data:
|
|
140
173
|
return cls(summary=ResultSummary.from_dict({}))
|
|
174
|
+
|
|
175
|
+
if "task_status" in data:
|
|
176
|
+
return cls(summary=ResultSummary.from_dict({}), task_status=data["task_status"])
|
|
177
|
+
|
|
141
178
|
return cls(summary=ResultSummary.from_dict(data["summary"]))
|
|
142
179
|
|
|
143
180
|
def to_dict(self) -> Dict:
|
|
@@ -174,42 +211,47 @@ class TestResult(BaseDTO):
|
|
|
174
211
|
@dataclass
|
|
175
212
|
class RedTeamResultDetails(BaseDTO):
|
|
176
213
|
details: List[TestResult]
|
|
214
|
+
task_status: Optional[str] = None
|
|
177
215
|
_extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
178
216
|
|
|
179
217
|
@classmethod
|
|
180
218
|
def from_dict(cls, data: Dict) -> "RedTeamResultDetails":
|
|
181
219
|
if not data or "details" not in data:
|
|
182
220
|
return cls(details=[])
|
|
221
|
+
|
|
222
|
+
if "task_status" in data:
|
|
223
|
+
return cls(details=[], task_status=data["task_status"])
|
|
183
224
|
|
|
184
|
-
details = []
|
|
185
|
-
for result in data["details"]:
|
|
225
|
+
# details = []
|
|
226
|
+
# for result in data["details"]:
|
|
186
227
|
# Convert eval_tokens dict to TestEvalTokens object
|
|
187
|
-
eval_tokens = TestEvalTokens(**result["eval_tokens"])
|
|
228
|
+
# eval_tokens = TestEvalTokens(**result["eval_tokens"])
|
|
188
229
|
|
|
189
230
|
# Create a copy of the result dict and replace eval_tokens
|
|
190
|
-
result_copy = dict(result)
|
|
191
|
-
result_copy["eval_tokens"] = eval_tokens
|
|
231
|
+
# result_copy = dict(result["details"])
|
|
232
|
+
# result_copy["eval_tokens"] = eval_tokens
|
|
192
233
|
|
|
193
234
|
# Create TestResult object
|
|
194
|
-
test_result = TestResult(**result_copy)
|
|
195
|
-
details.append(test_result)
|
|
235
|
+
# test_result = TestResult(**result_copy)
|
|
236
|
+
# details.append(test_result)
|
|
196
237
|
|
|
197
|
-
return cls(details=details)
|
|
238
|
+
return cls(details=data["details"])
|
|
198
239
|
|
|
199
240
|
def to_dict(self) -> Dict:
|
|
200
241
|
return {
|
|
201
|
-
"details": [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
]
|
|
242
|
+
# "details": [
|
|
243
|
+
# {**result.to_dict(), "eval_tokens": result.eval_tokens.to_dict()}
|
|
244
|
+
# for result in self.details
|
|
245
|
+
# ]
|
|
246
|
+
"details": self.details
|
|
205
247
|
}
|
|
206
248
|
|
|
207
|
-
def to_dataframe(self) -> pd.DataFrame:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
# def to_dataframe(self) -> pd.DataFrame:
|
|
250
|
+
# data = [
|
|
251
|
+
# {**result.to_dict(), "eval_tokens": result.eval_tokens.to_dict()}
|
|
252
|
+
# for result in self.details
|
|
253
|
+
# ]
|
|
254
|
+
# return pd.DataFrame(data)
|
|
213
255
|
|
|
214
256
|
|
|
215
257
|
@dataclass
|
|
@@ -330,7 +372,6 @@ class RedteamModelHealthResponse(BaseDTO):
|
|
|
330
372
|
class RedTeamConfig(BaseDTO):
|
|
331
373
|
test_name: str = "Test Name"
|
|
332
374
|
dataset_name: str = "standard"
|
|
333
|
-
model_saved_name: str = "gpt-4o-mini"
|
|
334
375
|
|
|
335
376
|
redteam_test_configurations: RedTeamTestConfigurations = field(
|
|
336
377
|
default_factory=RedTeamTestConfigurations
|
|
@@ -363,41 +404,33 @@ class RedTeamConfig(BaseDTO):
|
|
|
363
404
|
)
|
|
364
405
|
|
|
365
406
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
# target_config = TargetModelConfiguration.from_dict(
|
|
394
|
-
# data.pop("target_model_configuration", {})
|
|
395
|
-
# )
|
|
396
|
-
# return cls(
|
|
397
|
-
# **data,
|
|
398
|
-
# redteam_test_configurations=test_configs,
|
|
399
|
-
# target_model_configuration=target_config,
|
|
400
|
-
# )
|
|
407
|
+
@dataclass
|
|
408
|
+
class RedTeamConfigWithSavedModel(BaseDTO):
|
|
409
|
+
test_name: str = "Test Name"
|
|
410
|
+
dataset_name: str = "standard"
|
|
411
|
+
model_saved_name: str = "gpt-4o-mini"
|
|
412
|
+
|
|
413
|
+
redteam_test_configurations: RedTeamTestConfigurations = field(
|
|
414
|
+
default_factory=RedTeamTestConfigurations
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
_extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
418
|
+
|
|
419
|
+
def to_dict(self) -> dict:
|
|
420
|
+
d = asdict(self)
|
|
421
|
+
d["redteam_test_configurations"] = self.redteam_test_configurations.to_dict()
|
|
422
|
+
return d
|
|
423
|
+
|
|
424
|
+
@classmethod
|
|
425
|
+
def from_dict(cls, data: dict):
|
|
426
|
+
data = data.copy()
|
|
427
|
+
test_configs = RedTeamTestConfigurations.from_dict(
|
|
428
|
+
data.pop("redteam_test_configurations", {})
|
|
429
|
+
)
|
|
430
|
+
return cls(
|
|
431
|
+
**data,
|
|
432
|
+
redteam_test_configurations=test_configs,
|
|
433
|
+
)
|
|
401
434
|
|
|
402
435
|
|
|
403
436
|
@dataclass
|
|
@@ -411,3 +444,4 @@ class RedTeamTaskList(BaseDTO):
|
|
|
411
444
|
|
|
412
445
|
# Default configurations
|
|
413
446
|
DEFAULT_REDTEAM_CONFIG = RedTeamConfig()
|
|
447
|
+
DEFAULT_REDTEAM_CONFIG_WITH_SAVED_MODEL = RedTeamConfigWithSavedModel()
|
enkryptai_sdk/red_team.py
CHANGED
|
@@ -5,7 +5,7 @@ from .dto import (
|
|
|
5
5
|
RedTeamModelHealthConfig,
|
|
6
6
|
RedteamModelHealthResponse,
|
|
7
7
|
RedTeamConfig,
|
|
8
|
-
|
|
8
|
+
RedTeamConfigWithSavedModel,
|
|
9
9
|
RedTeamResponse,
|
|
10
10
|
RedTeamResultSummary,
|
|
11
11
|
RedTeamResultDetails,
|
|
@@ -103,22 +103,7 @@ class RedTeamClient(BaseClient):
|
|
|
103
103
|
},
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
if saved_model:
|
|
108
|
-
headers = {
|
|
109
|
-
"X-Enkrypt-Model": saved_model,
|
|
110
|
-
"Content-Type": "application/json",
|
|
111
|
-
}
|
|
112
|
-
response = self._request(
|
|
113
|
-
"POST",
|
|
114
|
-
"/redteam/v2/model/add-task",
|
|
115
|
-
headers=headers,
|
|
116
|
-
json=payload,
|
|
117
|
-
)
|
|
118
|
-
if response.get("error"):
|
|
119
|
-
raise RedTeamClientError(response["error"])
|
|
120
|
-
return RedTeamResponse.from_dict(response)
|
|
121
|
-
elif config.target_model_configuration:
|
|
106
|
+
if config.target_model_configuration:
|
|
122
107
|
payload["target_model_configuration"] = (
|
|
123
108
|
config.target_model_configuration.to_dict()
|
|
124
109
|
)
|
|
@@ -133,9 +118,48 @@ class RedTeamClient(BaseClient):
|
|
|
133
118
|
return RedTeamResponse.from_dict(response)
|
|
134
119
|
else:
|
|
135
120
|
raise RedTeamClientError(
|
|
136
|
-
"Please
|
|
121
|
+
"Please provide a target model configuration"
|
|
137
122
|
)
|
|
138
123
|
|
|
124
|
+
def add_task_with_saved_model(
|
|
125
|
+
self,
|
|
126
|
+
config: RedTeamConfigWithSavedModel,
|
|
127
|
+
model_saved_name: str,
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Add a new red teaming task using a saved model.
|
|
131
|
+
"""
|
|
132
|
+
if not model_saved_name:
|
|
133
|
+
raise RedTeamClientError("Please provide a model_saved_name")
|
|
134
|
+
|
|
135
|
+
config = RedTeamConfigWithSavedModel.from_dict(config)
|
|
136
|
+
test_configs = config.redteam_test_configurations.to_dict()
|
|
137
|
+
# Remove None or empty test configurations
|
|
138
|
+
test_configs = {k: v for k, v in test_configs.items() if v is not None}
|
|
139
|
+
|
|
140
|
+
payload = {
|
|
141
|
+
# "async": config.async_enabled,
|
|
142
|
+
"dataset_name": config.dataset_name,
|
|
143
|
+
"test_name": config.test_name,
|
|
144
|
+
"redteam_test_configurations": {
|
|
145
|
+
k: v.to_dict() for k, v in test_configs.items()
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
headers = {
|
|
150
|
+
"X-Enkrypt-Model": model_saved_name,
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
}
|
|
153
|
+
response = self._request(
|
|
154
|
+
"POST",
|
|
155
|
+
"/redteam/v2/model/add-task",
|
|
156
|
+
headers=headers,
|
|
157
|
+
json=payload,
|
|
158
|
+
)
|
|
159
|
+
if response.get("error"):
|
|
160
|
+
raise RedTeamClientError(response["error"])
|
|
161
|
+
return RedTeamResponse.from_dict(response)
|
|
162
|
+
|
|
139
163
|
def status(self, task_id: str = None, test_name: str = None):
|
|
140
164
|
"""
|
|
141
165
|
Get the status of a specific red teaming task.
|
|
@@ -253,6 +277,40 @@ class RedTeamClient(BaseClient):
|
|
|
253
277
|
raise RedTeamClientError(response["error"])
|
|
254
278
|
print(f"Response: {response}")
|
|
255
279
|
return RedTeamResultSummary.from_dict(response)
|
|
280
|
+
|
|
281
|
+
def get_result_summary_test_type(self, task_id: str = None, test_name: str = None, test_type: str = None):
|
|
282
|
+
"""
|
|
283
|
+
Get the summary of results for a specific red teaming task for a specific test type.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
task_id (str, optional): The ID of the task to get results for
|
|
287
|
+
test_name (str, optional): The name of the test to get results for
|
|
288
|
+
test_type (str, optional): The type of test to get results for
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
dict: The summary of the task results for the specified test type
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
RedTeamClientError: If neither task_id nor test_name is provided, or if there's an error from the API
|
|
295
|
+
"""
|
|
296
|
+
if not task_id and not test_name:
|
|
297
|
+
raise RedTeamClientError("Either task_id or test_name must be provided")
|
|
298
|
+
|
|
299
|
+
if not test_type:
|
|
300
|
+
raise RedTeamClientError("test_type must be provided")
|
|
301
|
+
|
|
302
|
+
headers = {}
|
|
303
|
+
if task_id:
|
|
304
|
+
headers["X-Enkrypt-Task-ID"] = task_id
|
|
305
|
+
if test_name:
|
|
306
|
+
headers["X-Enkrypt-Test-Name"] = test_name
|
|
307
|
+
|
|
308
|
+
url = f"/redteam/v2/results/summary/{test_type}"
|
|
309
|
+
response = self._request("GET", url, headers=headers)
|
|
310
|
+
if response.get("error"):
|
|
311
|
+
raise RedTeamClientError(response["error"])
|
|
312
|
+
print(f"Response: {response}")
|
|
313
|
+
return RedTeamResultSummary.from_dict(response)
|
|
256
314
|
|
|
257
315
|
def get_result_details(self, task_id: str = None, test_name: str = None):
|
|
258
316
|
"""
|
|
@@ -277,13 +335,60 @@ class RedTeamClient(BaseClient):
|
|
|
277
335
|
if test_name:
|
|
278
336
|
headers["X-Enkrypt-Test-Name"] = test_name
|
|
279
337
|
|
|
280
|
-
response = self._request("GET", "/redteam/results/details", headers=headers)
|
|
338
|
+
response = self._request("GET", "/redteam/v2/results/details", headers=headers)
|
|
281
339
|
if response.get("error"):
|
|
282
340
|
raise RedTeamClientError(response["error"])
|
|
283
341
|
return RedTeamResultDetails.from_dict(response)
|
|
342
|
+
|
|
343
|
+
def get_result_details_test_type(self, task_id: str = None, test_name: str = None, test_type: str = None):
|
|
344
|
+
"""
|
|
345
|
+
Get the detailed results for a specific red teaming task for a specific test type.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
task_id (str, optional): The ID of the task to get detailed results for
|
|
349
|
+
test_name (str, optional): The name of the test to get detailed results for
|
|
350
|
+
test_type (str, optional): The type of test to get detailed results for
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
dict: The detailed task results
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
RedTeamClientError: If neither task_id nor test_name is provided, or if there's an error from the API
|
|
357
|
+
"""
|
|
358
|
+
if not task_id and not test_name:
|
|
359
|
+
raise RedTeamClientError("Either task_id or test_name must be provided")
|
|
360
|
+
|
|
361
|
+
if not test_type:
|
|
362
|
+
raise RedTeamClientError("test_type must be provided")
|
|
363
|
+
|
|
364
|
+
headers = {}
|
|
365
|
+
if task_id:
|
|
366
|
+
headers["X-Enkrypt-Task-ID"] = task_id
|
|
367
|
+
if test_name:
|
|
368
|
+
headers["X-Enkrypt-Test-Name"] = test_name
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
url = f"/redteam/v2/results/details/{test_type}"
|
|
372
|
+
response = self._request("GET", url, headers=headers)
|
|
373
|
+
if response.get("error"):
|
|
374
|
+
raise RedTeamClientError(response["error"])
|
|
375
|
+
return RedTeamResultDetails.from_dict(response)
|
|
376
|
+
|
|
377
|
+
def get_task_list(self, status: str = None):
|
|
378
|
+
"""
|
|
379
|
+
Get a list of red teaming tasks.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
status (str, optional): The status of the tasks to retrieve
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
dict: The list of tasks
|
|
386
|
+
"""
|
|
387
|
+
url = "/redteam/list-tasks"
|
|
388
|
+
if status:
|
|
389
|
+
url += f"?status={status}"
|
|
284
390
|
|
|
285
|
-
|
|
286
|
-
response = self._request("GET", "/redteam/list-tasks")
|
|
391
|
+
response = self._request("GET", url)
|
|
287
392
|
if response.get("error"):
|
|
288
393
|
raise RedTeamClientError(response["error"])
|
|
289
394
|
return RedTeamTaskList.from_dict(response)
|