88from typing import Any , MutableSequence , Optional , Sequence , TypeVar , Union
99
1010from azure .core .credentials import AccessToken
11- from sqlalchemy import create_engine , event , text
11+ from sqlalchemy import and_ , create_engine , event , exists , text
1212from sqlalchemy .engine .base import Engine
1313from sqlalchemy .exc import SQLAlchemyError
1414from 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