Skip to content

Commit 4ebc266

Browse files
authored
FEAT FIX: Adding ScenarioResult to the Database (microsoft#1159)
1 parent 0bffa50 commit 4ebc266

16 files changed

Lines changed: 1836 additions & 74 deletions

pyrit/common/apply_defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class GlobalDefaultValues:
4444
applied to class parameters when using the @apply_defaults decorator.
4545
"""
4646

47-
def __init__(self):
47+
def __init__(self) -> None:
4848
self._default_values: Dict[DefaultValueScope, Any] = {}
4949

5050
def set_default_value(

pyrit/memory/azure_sql_memory.py

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, MutableSequence, Optional, Sequence, TypeVar, Union
99

1010
from azure.core.credentials import AccessToken
11-
from sqlalchemy import create_engine, event, text
11+
from sqlalchemy import and_, create_engine, event, exists, text
1212
from sqlalchemy.engine.base import Engine
1313
from sqlalchemy.exc import SQLAlchemyError
1414
from sqlalchemy.orm import joinedload, sessionmaker
@@ -202,6 +202,17 @@ def _add_embeddings_to_memory(self, *, embedding_data: Sequence[EmbeddingDataEnt
202202
self._insert_entries(entries=embedding_data)
203203

204204
def _get_message_pieces_memory_label_conditions(self, *, memory_labels: dict[str, str]) -> list:
205+
"""
206+
Generate SQL conditions for filtering message pieces by memory labels.
207+
208+
Uses JSON_VALUE() function specific to SQL Azure to query label fields in JSON format.
209+
210+
Args:
211+
memory_labels (dict[str, str]): Dictionary of label key-value pairs to filter by.
212+
213+
Returns:
214+
list: List containing a single SQLAlchemy text condition with bound parameters.
215+
"""
205216
json_validation = "ISJSON(labels) = 1"
206217
json_conditions = " AND ".join([f"JSON_VALUE(labels, '$.{key}') = :{key}" for key in memory_labels])
207218
# Combine both conditions
@@ -213,11 +224,33 @@ def _get_message_pieces_memory_label_conditions(self, *, memory_labels: dict[str
213224
return [condition]
214225

215226
def _get_message_pieces_attack_conditions(self, *, attack_id: str) -> Any:
227+
"""
228+
Generate SQL condition for filtering message pieces by attack ID.
229+
230+
Uses JSON_VALUE() function specific to SQL Azure to query the attack identifier.
231+
232+
Args:
233+
attack_id (str): The attack identifier to filter by.
234+
235+
Returns:
236+
Any: SQLAlchemy text condition with bound parameter.
237+
"""
216238
return text("ISJSON(attack_identifier) = 1 AND JSON_VALUE(attack_identifier, '$.id') = :json_id").bindparams(
217239
json_id=str(attack_id)
218240
)
219241

220-
def _get_metadata_conditions(self, *, prompt_metadata: dict[str, Union[str, int]]):
242+
def _get_metadata_conditions(self, *, prompt_metadata: dict[str, Union[str, int]]) -> list:
243+
"""
244+
Generate SQL conditions for filtering by prompt metadata.
245+
246+
Uses JSON_VALUE() function specific to SQL Azure to query metadata fields in JSON format.
247+
248+
Args:
249+
prompt_metadata (dict[str, Union[str, int]]): Dictionary of metadata key-value pairs to filter by.
250+
251+
Returns:
252+
list: List containing a single SQLAlchemy text condition with bound parameters.
253+
"""
221254
json_validation = "ISJSON(prompt_metadata) = 1"
222255
json_conditions = " AND ".join([f"JSON_VALUE(prompt_metadata, '$.{key}') = :{key}" for key in prompt_metadata])
223256
# Combine both conditions
@@ -229,11 +262,158 @@ def _get_metadata_conditions(self, *, prompt_metadata: dict[str, Union[str, int]
229262
return [condition]
230263

231264
def _get_message_pieces_prompt_metadata_conditions(self, *, prompt_metadata: dict[str, Union[str, int]]) -> list:
265+
"""
266+
Generate SQL conditions for filtering message pieces by prompt metadata.
267+
268+
This is a convenience wrapper around _get_metadata_conditions.
269+
270+
Args:
271+
prompt_metadata (dict[str, Union[str, int]]): Dictionary of metadata key-value pairs to filter by.
272+
273+
Returns:
274+
list: List containing SQLAlchemy text conditions with bound parameters.
275+
"""
232276
return self._get_metadata_conditions(prompt_metadata=prompt_metadata)
233277

234278
def _get_seed_metadata_conditions(self, *, metadata: dict[str, Union[str, int]]) -> Any:
279+
"""
280+
Generate SQL condition for filtering seed prompts by metadata.
281+
282+
This is a convenience wrapper around _get_metadata_conditions that returns
283+
the first (and only) condition.
284+
285+
Args:
286+
metadata (dict[str, Union[str, int]]): Dictionary of metadata key-value pairs to filter by.
287+
288+
Returns:
289+
Any: SQLAlchemy text condition with bound parameters.
290+
"""
235291
return self._get_metadata_conditions(prompt_metadata=metadata)[0]
236292

293+
def _get_attack_result_harm_category_condition(self, *, targeted_harm_categories: Sequence[str]) -> Any:
294+
"""
295+
SQL Azure implementation for filtering AttackResults by targeted harm categories.
296+
297+
Uses JSON_QUERY() function specific to SQL Azure to check if categories exist in the JSON array.
298+
299+
Args:
300+
targeted_harm_categories (Sequence[str]): List of harm category strings to filter by.
301+
302+
Returns:
303+
Any: SQLAlchemy exists subquery condition with bound parameters.
304+
"""
305+
# For SQL Azure, we need to use JSON_QUERY to check if a value exists in a JSON array
306+
# OPENJSON can parse the array and we check if the category exists
307+
# Using parameterized queries for safety
308+
harm_conditions = []
309+
bindparams_dict = {}
310+
for i, category in enumerate(targeted_harm_categories):
311+
param_name = f"harm_cat_{i}"
312+
# Check if the JSON array contains the category value
313+
harm_conditions.append(
314+
f"EXISTS(SELECT 1 FROM OPENJSON(targeted_harm_categories) WHERE value = :{param_name})"
315+
)
316+
bindparams_dict[param_name] = category
317+
318+
combined_conditions = " AND ".join(harm_conditions)
319+
320+
targeted_harm_categories_subquery = exists().where(
321+
and_(
322+
PromptMemoryEntry.conversation_id == AttackResultEntry.conversation_id,
323+
PromptMemoryEntry.targeted_harm_categories.isnot(None),
324+
PromptMemoryEntry.targeted_harm_categories != "",
325+
PromptMemoryEntry.targeted_harm_categories != "[]",
326+
text(f"ISJSON(targeted_harm_categories) = 1 AND {combined_conditions}").bindparams(**bindparams_dict),
327+
)
328+
)
329+
return targeted_harm_categories_subquery
330+
331+
def _get_attack_result_label_condition(self, *, labels: dict[str, str]) -> Any:
332+
"""
333+
SQL Azure implementation for filtering AttackResults by labels.
334+
335+
Uses JSON_VALUE() function specific to SQL Azure with parameterized queries.
336+
337+
Args:
338+
labels (dict[str, str]): Dictionary of label key-value pairs to filter by.
339+
340+
Returns:
341+
Any: SQLAlchemy exists subquery condition with bound parameters.
342+
"""
343+
# Build JSON conditions for all labels with parameterized queries
344+
label_conditions = []
345+
bindparams_dict = {}
346+
for key, value in labels.items():
347+
param_name = f"label_{key}"
348+
label_conditions.append(f"JSON_VALUE(labels, '$.{key}') = :{param_name}")
349+
bindparams_dict[param_name] = str(value)
350+
351+
combined_conditions = " AND ".join(label_conditions)
352+
353+
labels_subquery = exists().where(
354+
and_(
355+
PromptMemoryEntry.conversation_id == AttackResultEntry.conversation_id,
356+
PromptMemoryEntry.labels.isnot(None),
357+
text(f"ISJSON(labels) = 1 AND {combined_conditions}").bindparams(**bindparams_dict),
358+
)
359+
)
360+
return labels_subquery
361+
362+
def _get_scenario_result_label_condition(self, *, labels: dict[str, str]) -> Any:
363+
"""
364+
SQL Azure implementation for filtering ScenarioResults by labels.
365+
366+
Uses JSON_VALUE() function specific to SQL Azure.
367+
368+
Args:
369+
labels (dict[str, str]): Dictionary of label key-value pairs to filter by.
370+
371+
Returns:
372+
Any: SQLAlchemy combined condition with bound parameters.
373+
"""
374+
# Return combined conditions for all labels
375+
conditions = []
376+
for key, value in labels.items():
377+
condition = text(f"ISJSON(labels) = 1 AND JSON_VALUE(labels, '$.{key}') = :{key}").bindparams(
378+
**{key: str(value)}
379+
)
380+
conditions.append(condition)
381+
return and_(*conditions)
382+
383+
def _get_scenario_result_target_endpoint_condition(self, *, endpoint: str) -> Any:
384+
"""
385+
SQL Azure implementation for filtering ScenarioResults by target endpoint.
386+
387+
Uses JSON_VALUE() function specific to SQL Azure.
388+
389+
Args:
390+
endpoint (str): The endpoint URL substring to filter by (case-insensitive).
391+
392+
Returns:
393+
Any: SQLAlchemy text condition with bound parameter.
394+
"""
395+
return text(
396+
"""ISJSON(objective_target_identifier) = 1
397+
AND LOWER(JSON_VALUE(objective_target_identifier, '$.endpoint')) LIKE :endpoint"""
398+
).bindparams(endpoint=f"%{endpoint.lower()}%")
399+
400+
def _get_scenario_result_target_model_condition(self, *, model_name: str) -> Any:
401+
"""
402+
SQL Azure implementation for filtering ScenarioResults by target model name.
403+
404+
Uses JSON_VALUE() function specific to SQL Azure.
405+
406+
Args:
407+
model_name (str): The model name substring to filter by (case-insensitive).
408+
409+
Returns:
410+
Any: SQLAlchemy text condition with bound parameter.
411+
"""
412+
return text(
413+
"""ISJSON(objective_target_identifier) = 1
414+
AND LOWER(JSON_VALUE(objective_target_identifier, '$.model_name')) LIKE :model_name"""
415+
).bindparams(model_name=f"%{model_name.lower()}%")
416+
237417
def add_message_pieces_to_memory(self, *, message_pieces: Sequence[MessagePiece]) -> None:
238418
"""
239419
Inserts a list of message pieces into the memory storage.

0 commit comments

Comments
 (0)