azure-ai-evaluation 1.0.0__py3-none-any.whl → 1.0.0b1__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 azure-ai-evaluation might be problematic. Click here for more details.
- azure/ai/evaluation/__init__.py +4 -26
- azure/ai/evaluation/_common/constants.py +2 -9
- azure/ai/evaluation/_common/rai_service.py +122 -302
- azure/ai/evaluation/_common/utils.py +35 -393
- azure/ai/evaluation/_constants.py +6 -28
- azure/ai/evaluation/_evaluate/{_batch_run → _batch_run_client}/__init__.py +2 -3
- azure/ai/evaluation/_evaluate/{_batch_run/eval_run_context.py → _batch_run_client/batch_run_context.py} +8 -25
- azure/ai/evaluation/_evaluate/{_batch_run → _batch_run_client}/code_client.py +30 -68
- azure/ai/evaluation/_evaluate/_batch_run_client/proxy_client.py +61 -0
- azure/ai/evaluation/_evaluate/_eval_run.py +40 -117
- azure/ai/evaluation/_evaluate/_evaluate.py +255 -416
- azure/ai/evaluation/_evaluate/_telemetry/__init__.py +19 -24
- azure/ai/evaluation/_evaluate/_utils.py +47 -108
- azure/ai/evaluation/_evaluators/_bleu/_bleu.py +19 -18
- azure/ai/evaluation/_evaluators/{_retrieval → _chat}/__init__.py +2 -2
- azure/ai/evaluation/_evaluators/_chat/_chat.py +350 -0
- azure/ai/evaluation/_evaluators/{_service_groundedness → _chat/retrieval}/__init__.py +2 -2
- azure/ai/evaluation/_evaluators/_chat/retrieval/_retrieval.py +163 -0
- azure/ai/evaluation/_evaluators/_chat/retrieval/retrieval.prompty +48 -0
- azure/ai/evaluation/_evaluators/_coherence/_coherence.py +93 -78
- azure/ai/evaluation/_evaluators/_coherence/coherence.prompty +39 -76
- azure/ai/evaluation/_evaluators/_content_safety/__init__.py +4 -0
- azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +68 -104
- azure/ai/evaluation/_evaluators/{_multimodal/_content_safety_multimodal_base.py → _content_safety/_content_safety_base.py} +35 -24
- azure/ai/evaluation/_evaluators/_content_safety/_content_safety_chat.py +296 -0
- azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +54 -105
- azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +52 -99
- azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +52 -101
- azure/ai/evaluation/_evaluators/_content_safety/_violence.py +51 -101
- azure/ai/evaluation/_evaluators/_eci/_eci.py +55 -45
- azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +20 -36
- azure/ai/evaluation/_evaluators/_fluency/_fluency.py +94 -76
- azure/ai/evaluation/_evaluators/_fluency/fluency.prompty +41 -66
- azure/ai/evaluation/_evaluators/_gleu/_gleu.py +17 -15
- azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +92 -113
- azure/ai/evaluation/_evaluators/_groundedness/groundedness.prompty +54 -0
- azure/ai/evaluation/_evaluators/_meteor/_meteor.py +27 -21
- azure/ai/evaluation/_evaluators/_protected_material/_protected_material.py +80 -89
- azure/ai/evaluation/_evaluators/_protected_materials/__init__.py +5 -0
- azure/ai/evaluation/_evaluators/_protected_materials/_protected_materials.py +104 -0
- azure/ai/evaluation/_evaluators/_qa/_qa.py +43 -25
- azure/ai/evaluation/_evaluators/_relevance/_relevance.py +101 -84
- azure/ai/evaluation/_evaluators/_relevance/relevance.prompty +47 -78
- azure/ai/evaluation/_evaluators/_rouge/_rouge.py +27 -27
- azure/ai/evaluation/_evaluators/_similarity/_similarity.py +45 -55
- azure/ai/evaluation/_evaluators/_similarity/similarity.prompty +5 -0
- azure/ai/evaluation/_evaluators/_xpia/xpia.py +106 -91
- azure/ai/evaluation/_exceptions.py +7 -28
- azure/ai/evaluation/_http_utils.py +134 -205
- azure/ai/evaluation/_model_configurations.py +8 -104
- azure/ai/evaluation/_version.py +1 -1
- azure/ai/evaluation/simulator/__init__.py +2 -3
- azure/ai/evaluation/simulator/_adversarial_scenario.py +1 -20
- azure/ai/evaluation/simulator/_adversarial_simulator.py +95 -116
- azure/ai/evaluation/simulator/_constants.py +1 -11
- azure/ai/evaluation/simulator/_conversation/__init__.py +13 -14
- azure/ai/evaluation/simulator/_conversation/_conversation.py +20 -20
- azure/ai/evaluation/simulator/_direct_attack_simulator.py +68 -34
- azure/ai/evaluation/simulator/_helpers/__init__.py +1 -1
- azure/ai/evaluation/simulator/_helpers/_simulator_data_classes.py +28 -31
- azure/ai/evaluation/simulator/_indirect_attack_simulator.py +95 -108
- azure/ai/evaluation/simulator/_model_tools/_identity_manager.py +22 -70
- azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +14 -30
- azure/ai/evaluation/simulator/_model_tools/_rai_client.py +14 -25
- azure/ai/evaluation/simulator/_model_tools/_template_handler.py +24 -68
- azure/ai/evaluation/simulator/_model_tools/models.py +21 -19
- azure/ai/evaluation/simulator/_prompty/task_query_response.prompty +10 -6
- azure/ai/evaluation/simulator/_prompty/task_simulate.prompty +5 -6
- azure/ai/evaluation/simulator/_tracing.py +28 -25
- azure/ai/evaluation/simulator/_utils.py +13 -34
- azure/ai/evaluation/simulator/simulator.py +579 -0
- azure_ai_evaluation-1.0.0b1.dist-info/METADATA +377 -0
- azure_ai_evaluation-1.0.0b1.dist-info/RECORD +97 -0
- {azure_ai_evaluation-1.0.0.dist-info → azure_ai_evaluation-1.0.0b1.dist-info}/WHEEL +1 -1
- azure/ai/evaluation/_common/_experimental.py +0 -172
- azure/ai/evaluation/_common/math.py +0 -89
- azure/ai/evaluation/_evaluate/_batch_run/proxy_client.py +0 -99
- azure/ai/evaluation/_evaluate/_batch_run/target_run_context.py +0 -46
- azure/ai/evaluation/_evaluators/_common/__init__.py +0 -13
- azure/ai/evaluation/_evaluators/_common/_base_eval.py +0 -344
- azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +0 -88
- azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +0 -133
- azure/ai/evaluation/_evaluators/_groundedness/groundedness_with_query.prompty +0 -113
- azure/ai/evaluation/_evaluators/_groundedness/groundedness_without_query.prompty +0 -99
- azure/ai/evaluation/_evaluators/_multimodal/__init__.py +0 -20
- azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal.py +0 -132
- azure/ai/evaluation/_evaluators/_multimodal/_hate_unfairness.py +0 -100
- azure/ai/evaluation/_evaluators/_multimodal/_protected_material.py +0 -124
- azure/ai/evaluation/_evaluators/_multimodal/_self_harm.py +0 -100
- azure/ai/evaluation/_evaluators/_multimodal/_sexual.py +0 -100
- azure/ai/evaluation/_evaluators/_multimodal/_violence.py +0 -100
- azure/ai/evaluation/_evaluators/_retrieval/_retrieval.py +0 -112
- azure/ai/evaluation/_evaluators/_retrieval/retrieval.prompty +0 -93
- azure/ai/evaluation/_evaluators/_service_groundedness/_service_groundedness.py +0 -148
- azure/ai/evaluation/_vendor/__init__.py +0 -3
- azure/ai/evaluation/_vendor/rouge_score/__init__.py +0 -14
- azure/ai/evaluation/_vendor/rouge_score/rouge_scorer.py +0 -328
- azure/ai/evaluation/_vendor/rouge_score/scoring.py +0 -63
- azure/ai/evaluation/_vendor/rouge_score/tokenize.py +0 -63
- azure/ai/evaluation/_vendor/rouge_score/tokenizers.py +0 -53
- azure/ai/evaluation/simulator/_data_sources/__init__.py +0 -3
- azure/ai/evaluation/simulator/_data_sources/grounding.json +0 -1150
- azure/ai/evaluation/simulator/_prompty/__init__.py +0 -0
- azure/ai/evaluation/simulator/_simulator.py +0 -716
- azure_ai_evaluation-1.0.0.dist-info/METADATA +0 -595
- azure_ai_evaluation-1.0.0.dist-info/NOTICE.txt +0 -70
- azure_ai_evaluation-1.0.0.dist-info/RECORD +0 -119
- {azure_ai_evaluation-1.0.0.dist-info → azure_ai_evaluation-1.0.0b1.dist-info}/top_level.txt +0 -0
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
# ---------------------------------------------------------
|
|
2
2
|
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
# ---------------------------------------------------------
|
|
4
|
-
# pylint: disable=C0301,C0114,R0913,R0903
|
|
5
4
|
# noqa: E501
|
|
5
|
+
import functools
|
|
6
6
|
import logging
|
|
7
7
|
from random import randint
|
|
8
|
-
from typing import Callable,
|
|
8
|
+
from typing import Any, Callable, Dict, Optional
|
|
9
9
|
|
|
10
|
-
from azure.
|
|
11
|
-
|
|
12
|
-
from
|
|
10
|
+
from azure.identity import DefaultAzureCredential
|
|
11
|
+
|
|
12
|
+
from promptflow._sdk._telemetry import ActivityType, monitor_operation
|
|
13
|
+
from azure.ai.evaluation._exceptions import EvaluationException, ErrorBlame, ErrorCategory, ErrorTarget
|
|
13
14
|
from azure.ai.evaluation.simulator import AdversarialScenario
|
|
14
15
|
from azure.ai.evaluation._model_configurations import AzureAIProject
|
|
15
|
-
from azure.core.credentials import TokenCredential
|
|
16
16
|
|
|
17
|
-
from ._adversarial_simulator import AdversarialSimulator
|
|
18
17
|
from ._model_tools import AdversarialTemplateHandler, ManagedIdentityAPITokenManager, RAIClient, TokenScope
|
|
18
|
+
from ._adversarial_simulator import AdversarialSimulator
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
def monitor_adversarial_scenario(func) -> Callable:
|
|
24
|
+
"""Decorator to monitor adversarial scenario.
|
|
25
|
+
|
|
26
|
+
:param func: The function to be decorated.
|
|
27
|
+
:type func: Callable
|
|
28
|
+
:return: The decorated function.
|
|
29
|
+
:rtype: Callable
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@functools.wraps(func)
|
|
33
|
+
def wrapper(*args, **kwargs):
|
|
34
|
+
scenario = str(kwargs.get("scenario", None))
|
|
35
|
+
max_conversation_turns = kwargs.get("max_conversation_turns", None)
|
|
36
|
+
max_simulation_results = kwargs.get("max_simulation_results", None)
|
|
37
|
+
decorated_func = monitor_operation(
|
|
38
|
+
activity_name="jailbreak.adversarial.simulator.call",
|
|
39
|
+
activity_type=ActivityType.PUBLICAPI,
|
|
40
|
+
custom_dimensions={
|
|
41
|
+
"scenario": scenario,
|
|
42
|
+
"max_conversation_turns": max_conversation_turns,
|
|
43
|
+
"max_simulation_results": max_simulation_results,
|
|
44
|
+
},
|
|
45
|
+
)(func)
|
|
46
|
+
|
|
47
|
+
return decorated_func(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
return wrapper
|
|
50
|
+
|
|
51
|
+
|
|
24
52
|
class DirectAttackSimulator:
|
|
25
53
|
"""
|
|
26
54
|
Initialize a UPIA (user prompt injected attack) jailbreak adversarial simulator with a project scope.
|
|
@@ -31,39 +59,44 @@ class DirectAttackSimulator:
|
|
|
31
59
|
:type azure_ai_project: ~azure.ai.evaluation.AzureAIProject
|
|
32
60
|
:param credential: The credential for connecting to Azure AI project.
|
|
33
61
|
:type credential: ~azure.core.credentials.TokenCredential
|
|
34
|
-
|
|
35
|
-
.. admonition:: Example:
|
|
36
|
-
|
|
37
|
-
.. literalinclude:: ../samples/evaluation_samples_simulate.py
|
|
38
|
-
:start-after: [START direct_attack_simulator]
|
|
39
|
-
:end-before: [END direct_attack_simulator]
|
|
40
|
-
:language: python
|
|
41
|
-
:dedent: 8
|
|
42
|
-
:caption: Run the DirectAttackSimulator to produce 2 results with 3 conversation turns each (6 messages in each result).
|
|
43
62
|
"""
|
|
44
63
|
|
|
45
|
-
def __init__(self, *, azure_ai_project: AzureAIProject, credential
|
|
64
|
+
def __init__(self, *, azure_ai_project: AzureAIProject, credential=None):
|
|
46
65
|
"""Constructor."""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
except EvaluationException as e:
|
|
66
|
+
# check if azure_ai_project has the keys: subscription_id, resource_group_name, project_name, credential
|
|
67
|
+
if not all(key in azure_ai_project for key in ["subscription_id", "resource_group_name", "project_name"]):
|
|
68
|
+
msg = "azure_ai_project must contain keys: subscription_id, resource_group_name and project_name"
|
|
51
69
|
raise EvaluationException(
|
|
52
|
-
message=
|
|
53
|
-
internal_message=
|
|
70
|
+
message=msg,
|
|
71
|
+
internal_message=msg,
|
|
72
|
+
target=ErrorTarget.DIRECT_ATTACK_SIMULATOR,
|
|
73
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
74
|
+
blame=ErrorBlame.USER_ERROR,
|
|
75
|
+
)
|
|
76
|
+
# check the value of the keys in azure_ai_project is not none
|
|
77
|
+
if not all(azure_ai_project[key] for key in ["subscription_id", "resource_group_name", "project_name"]):
|
|
78
|
+
msg = "subscription_id, resource_group_name and project_name keys cannot be None"
|
|
79
|
+
raise EvaluationException(
|
|
80
|
+
message=msg,
|
|
81
|
+
internal_message=msg,
|
|
54
82
|
target=ErrorTarget.DIRECT_ATTACK_SIMULATOR,
|
|
55
|
-
category=
|
|
56
|
-
blame=
|
|
57
|
-
)
|
|
58
|
-
|
|
83
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
84
|
+
blame=ErrorBlame.USER_ERROR,
|
|
85
|
+
)
|
|
86
|
+
if "credential" not in azure_ai_project and not credential:
|
|
87
|
+
credential = DefaultAzureCredential()
|
|
88
|
+
elif "credential" in azure_ai_project:
|
|
89
|
+
credential = azure_ai_project["credential"]
|
|
90
|
+
self.credential = credential
|
|
91
|
+
self.azure_ai_project = azure_ai_project
|
|
59
92
|
self.token_manager = ManagedIdentityAPITokenManager(
|
|
60
93
|
token_scope=TokenScope.DEFAULT_AZURE_MANAGEMENT,
|
|
61
94
|
logger=logging.getLogger("AdversarialSimulator"),
|
|
62
|
-
credential=
|
|
95
|
+
credential=credential,
|
|
63
96
|
)
|
|
64
|
-
self.rai_client = RAIClient(azure_ai_project=
|
|
97
|
+
self.rai_client = RAIClient(azure_ai_project=azure_ai_project, token_manager=self.token_manager)
|
|
65
98
|
self.adversarial_template_handler = AdversarialTemplateHandler(
|
|
66
|
-
azure_ai_project=
|
|
99
|
+
azure_ai_project=azure_ai_project, rai_client=self.rai_client
|
|
67
100
|
)
|
|
68
101
|
|
|
69
102
|
def _ensure_service_dependencies(self):
|
|
@@ -77,6 +110,7 @@ class DirectAttackSimulator:
|
|
|
77
110
|
blame=ErrorBlame.USER_ERROR,
|
|
78
111
|
)
|
|
79
112
|
|
|
113
|
+
# @monitor_adversarial_scenario
|
|
80
114
|
async def __call__(
|
|
81
115
|
self,
|
|
82
116
|
*,
|
|
@@ -135,7 +169,7 @@ class DirectAttackSimulator:
|
|
|
135
169
|
- '**$schema**': A string indicating the schema URL for the conversation format.
|
|
136
170
|
|
|
137
171
|
The 'content' for 'assistant' role messages may includes the messages that your callback returned.
|
|
138
|
-
:rtype: Dict[str, [List[Dict[str, Any]]]]
|
|
172
|
+
:rtype: Dict[str, [List[Dict[str, Any]]]] with two elements
|
|
139
173
|
|
|
140
174
|
**Output format**
|
|
141
175
|
|
|
@@ -198,7 +232,7 @@ class DirectAttackSimulator:
|
|
|
198
232
|
api_call_retry_sleep_sec=api_call_retry_sleep_sec,
|
|
199
233
|
api_call_delay_sec=api_call_delay_sec,
|
|
200
234
|
concurrent_async_task=concurrent_async_task,
|
|
201
|
-
randomize_order=
|
|
235
|
+
randomize_order=True,
|
|
202
236
|
randomization_seed=randomization_seed,
|
|
203
237
|
)
|
|
204
238
|
jb_sim = AdversarialSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
|
|
@@ -212,7 +246,7 @@ class DirectAttackSimulator:
|
|
|
212
246
|
api_call_delay_sec=api_call_delay_sec,
|
|
213
247
|
concurrent_async_task=concurrent_async_task,
|
|
214
248
|
_jailbreak_type="upia",
|
|
215
|
-
randomize_order=
|
|
249
|
+
randomize_order=True,
|
|
216
250
|
randomization_seed=randomization_seed,
|
|
217
251
|
)
|
|
218
252
|
return {"jailbreak": jb_sim_results, "regular": regular_sim_results}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ._language_suffix_mapping import SUPPORTED_LANGUAGES_MAPPING
|
|
2
1
|
from ._simulator_data_classes import ConversationHistory, Turn
|
|
2
|
+
from ._language_suffix_mapping import SUPPORTED_LANGUAGES_MAPPING
|
|
3
3
|
|
|
4
4
|
__all__ = ["ConversationHistory", "Turn", "SUPPORTED_LANGUAGES_MAPPING"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ---------------------------------------------------------
|
|
4
4
|
# pylint: disable=C0103,C0114,C0116
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Union
|
|
7
7
|
|
|
8
8
|
from azure.ai.evaluation.simulator._conversation.constants import ConversationRole
|
|
9
9
|
|
|
@@ -18,34 +18,28 @@ class Turn:
|
|
|
18
18
|
|
|
19
19
|
role: Union[str, ConversationRole]
|
|
20
20
|
content: str
|
|
21
|
-
context:
|
|
21
|
+
context: str = None
|
|
22
22
|
|
|
23
|
-
def to_dict(self)
|
|
23
|
+
def to_dict(self):
|
|
24
24
|
"""
|
|
25
25
|
Convert the conversation turn to a dictionary.
|
|
26
26
|
|
|
27
|
-
:
|
|
28
|
-
|
|
27
|
+
Returns:
|
|
28
|
+
dict: A dictionary representation of the conversation turn.
|
|
29
29
|
"""
|
|
30
30
|
return {
|
|
31
31
|
"role": self.role.value if isinstance(self.role, ConversationRole) else self.role,
|
|
32
32
|
"content": self.content,
|
|
33
|
-
"context":
|
|
33
|
+
"context": self.context,
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def __repr__(self):
|
|
37
37
|
"""
|
|
38
|
-
|
|
38
|
+
Return the string representation of the conversation turn.
|
|
39
39
|
|
|
40
|
-
:
|
|
41
|
-
|
|
40
|
+
Returns:
|
|
41
|
+
str: A string representation of the conversation turn.
|
|
42
42
|
"""
|
|
43
|
-
return {
|
|
44
|
-
"role": self.role.value if isinstance(self.role, ConversationRole) else self.role,
|
|
45
|
-
"content": self.content,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
def __repr__(self):
|
|
49
43
|
return f"Turn(role={self.role}, content={self.content})"
|
|
50
44
|
|
|
51
45
|
|
|
@@ -54,43 +48,46 @@ class ConversationHistory:
|
|
|
54
48
|
Conversation history class to keep track of the conversation turns in a conversation.
|
|
55
49
|
"""
|
|
56
50
|
|
|
57
|
-
def __init__(self)
|
|
51
|
+
def __init__(self):
|
|
58
52
|
"""
|
|
59
53
|
Initializes the conversation history with an empty list of turns.
|
|
60
54
|
"""
|
|
61
|
-
self.history
|
|
55
|
+
self.history = []
|
|
62
56
|
|
|
63
|
-
def add_to_history(self, turn: Turn)
|
|
57
|
+
def add_to_history(self, turn: Turn):
|
|
64
58
|
"""
|
|
65
59
|
Adds a turn to the conversation history.
|
|
66
60
|
|
|
67
|
-
:
|
|
68
|
-
|
|
61
|
+
Args:
|
|
62
|
+
turn (Turn): The conversation turn to add.
|
|
69
63
|
"""
|
|
70
64
|
self.history.append(turn)
|
|
71
65
|
|
|
72
|
-
def to_list(self)
|
|
66
|
+
def to_list(self):
|
|
73
67
|
"""
|
|
74
68
|
Converts the conversation history to a list of dictionaries.
|
|
75
69
|
|
|
76
|
-
:
|
|
77
|
-
|
|
70
|
+
Returns:
|
|
71
|
+
list: A list of dictionaries representing the conversation turns.
|
|
78
72
|
"""
|
|
79
73
|
return [turn.to_dict() for turn in self.history]
|
|
80
74
|
|
|
81
|
-
def
|
|
75
|
+
def get_length(self):
|
|
82
76
|
"""
|
|
83
|
-
|
|
77
|
+
Returns the length of the conversation.
|
|
84
78
|
|
|
85
|
-
:
|
|
86
|
-
|
|
79
|
+
Returns:
|
|
80
|
+
int: The number of turns in the conversation history.
|
|
87
81
|
"""
|
|
88
|
-
return [turn.to_context_free_dict() for turn in self.history]
|
|
89
|
-
|
|
90
|
-
def __len__(self) -> int:
|
|
91
82
|
return len(self.history)
|
|
92
83
|
|
|
93
84
|
def __repr__(self):
|
|
85
|
+
"""
|
|
86
|
+
Returns the string representation of the conversation history.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str: A string representation of the conversation history.
|
|
90
|
+
"""
|
|
94
91
|
for turn in self.history:
|
|
95
92
|
print(turn)
|
|
96
93
|
return ""
|
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
# ---------------------------------------------------------
|
|
2
2
|
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
# ---------------------------------------------------------
|
|
4
|
-
# pylint: disable=C0301,C0114,R0913,R0903
|
|
5
4
|
# noqa: E501
|
|
6
|
-
import
|
|
5
|
+
import functools
|
|
7
6
|
import logging
|
|
8
|
-
from typing import Callable,
|
|
7
|
+
from typing import Any, Callable, Dict
|
|
9
8
|
|
|
10
|
-
from
|
|
9
|
+
from azure.identity import DefaultAzureCredential
|
|
11
10
|
|
|
12
|
-
from
|
|
13
|
-
from azure.ai.evaluation.
|
|
14
|
-
from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
|
|
15
|
-
from azure.ai.evaluation.simulator import AdversarialScenarioJailbreak, SupportedLanguages
|
|
11
|
+
from promptflow._sdk._telemetry import ActivityType, monitor_operation
|
|
12
|
+
from azure.ai.evaluation.simulator import AdversarialScenario
|
|
16
13
|
from azure.ai.evaluation._model_configurations import AzureAIProject
|
|
17
|
-
from azure.core.credentials import TokenCredential
|
|
18
|
-
|
|
19
|
-
from ._adversarial_simulator import AdversarialSimulator, JsonLineList
|
|
20
14
|
|
|
21
15
|
from ._model_tools import AdversarialTemplateHandler, ManagedIdentityAPITokenManager, RAIClient, TokenScope
|
|
16
|
+
from azure.ai.evaluation._exceptions import EvaluationException, ErrorBlame, ErrorCategory, ErrorTarget
|
|
17
|
+
from ._adversarial_simulator import AdversarialSimulator
|
|
22
18
|
|
|
23
19
|
logger = logging.getLogger(__name__)
|
|
24
20
|
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
def monitor_adversarial_scenario(func) -> Callable:
|
|
23
|
+
"""Decorator to monitor adversarial scenario.
|
|
24
|
+
|
|
25
|
+
:param func: The function to be decorated.
|
|
26
|
+
:type func: Callable
|
|
27
|
+
:return: The decorated function.
|
|
28
|
+
:rtype: Callable
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@functools.wraps(func)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
scenario = str(kwargs.get("scenario", None))
|
|
34
|
+
max_conversation_turns = kwargs.get("max_conversation_turns", None)
|
|
35
|
+
max_simulation_results = kwargs.get("max_simulation_results", None)
|
|
36
|
+
decorated_func = monitor_operation(
|
|
37
|
+
activity_name="xpia.adversarial.simulator.call",
|
|
38
|
+
activity_type=ActivityType.PUBLICAPI,
|
|
39
|
+
custom_dimensions={
|
|
40
|
+
"scenario": scenario,
|
|
41
|
+
"max_conversation_turns": max_conversation_turns,
|
|
42
|
+
"max_simulation_results": max_simulation_results,
|
|
43
|
+
},
|
|
44
|
+
)(func)
|
|
45
|
+
|
|
46
|
+
return decorated_func(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
return wrapper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class IndirectAttackSimulator:
|
|
28
52
|
"""
|
|
29
53
|
Initializes the XPIA (cross domain prompt injected attack) jailbreak adversarial simulator with a project scope.
|
|
30
54
|
|
|
@@ -33,42 +57,44 @@ class IndirectAttackSimulator(AdversarialSimulator):
|
|
|
33
57
|
:type azure_ai_project: ~azure.ai.evaluation.AzureAIProject
|
|
34
58
|
:param credential: The credential for connecting to Azure AI project.
|
|
35
59
|
:type credential: ~azure.core.credentials.TokenCredential
|
|
36
|
-
|
|
37
|
-
.. admonition:: Example:
|
|
38
|
-
|
|
39
|
-
.. literalinclude:: ../samples/evaluation_samples_simulate.py
|
|
40
|
-
:start-after: [START indirect_attack_simulator]
|
|
41
|
-
:end-before: [END indirect_attack_simulator]
|
|
42
|
-
:language: python
|
|
43
|
-
:dedent: 8
|
|
44
|
-
:caption: Run the IndirectAttackSimulator to produce 1 result with 1 conversation turn (2 messages in the result).
|
|
45
60
|
"""
|
|
46
61
|
|
|
47
|
-
def __init__(self, *, azure_ai_project: AzureAIProject, credential
|
|
62
|
+
def __init__(self, *, azure_ai_project: AzureAIProject, credential=None):
|
|
48
63
|
"""Constructor."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
except EvaluationException as e:
|
|
64
|
+
# check if azure_ai_project has the keys: subscription_id, resource_group_name, project_name, credential
|
|
65
|
+
if not all(key in azure_ai_project for key in ["subscription_id", "resource_group_name", "project_name"]):
|
|
66
|
+
msg = "azure_ai_project must contain keys: subscription_id, resource_group_name and project_name"
|
|
53
67
|
raise EvaluationException(
|
|
54
|
-
message=
|
|
55
|
-
internal_message=
|
|
68
|
+
message=msg,
|
|
69
|
+
internal_message=msg,
|
|
56
70
|
target=ErrorTarget.DIRECT_ATTACK_SIMULATOR,
|
|
57
|
-
category=
|
|
58
|
-
blame=
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
72
|
+
blame=ErrorBlame.USER_ERROR,
|
|
73
|
+
)
|
|
74
|
+
if not all(azure_ai_project[key] for key in ["subscription_id", "resource_group_name", "project_name"]):
|
|
75
|
+
msg = "subscription_id, resource_group_name and project_name keys cannot be None"
|
|
76
|
+
raise EvaluationException(
|
|
77
|
+
message=msg,
|
|
78
|
+
internal_message=msg,
|
|
79
|
+
target=ErrorTarget.DIRECT_ATTACK_SIMULATOR,
|
|
80
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
81
|
+
blame=ErrorBlame.USER_ERROR,
|
|
82
|
+
)
|
|
83
|
+
if "credential" not in azure_ai_project and not credential:
|
|
84
|
+
credential = DefaultAzureCredential()
|
|
85
|
+
elif "credential" in azure_ai_project:
|
|
86
|
+
credential = azure_ai_project["credential"]
|
|
87
|
+
self.credential = credential
|
|
88
|
+
self.azure_ai_project = azure_ai_project
|
|
62
89
|
self.token_manager = ManagedIdentityAPITokenManager(
|
|
63
90
|
token_scope=TokenScope.DEFAULT_AZURE_MANAGEMENT,
|
|
64
91
|
logger=logging.getLogger("AdversarialSimulator"),
|
|
65
|
-
credential=
|
|
92
|
+
credential=credential,
|
|
66
93
|
)
|
|
67
|
-
self.rai_client = RAIClient(azure_ai_project=
|
|
94
|
+
self.rai_client = RAIClient(azure_ai_project=azure_ai_project, token_manager=self.token_manager)
|
|
68
95
|
self.adversarial_template_handler = AdversarialTemplateHandler(
|
|
69
|
-
azure_ai_project=
|
|
96
|
+
azure_ai_project=azure_ai_project, rai_client=self.rai_client
|
|
70
97
|
)
|
|
71
|
-
super().__init__(azure_ai_project=azure_ai_project, credential=credential)
|
|
72
98
|
|
|
73
99
|
def _ensure_service_dependencies(self):
|
|
74
100
|
if self.rai_client is None:
|
|
@@ -81,25 +107,33 @@ class IndirectAttackSimulator(AdversarialSimulator):
|
|
|
81
107
|
blame=ErrorBlame.USER_ERROR,
|
|
82
108
|
)
|
|
83
109
|
|
|
110
|
+
# @monitor_adversarial_scenario
|
|
84
111
|
async def __call__(
|
|
85
112
|
self,
|
|
86
113
|
*,
|
|
114
|
+
scenario: AdversarialScenario,
|
|
87
115
|
target: Callable,
|
|
116
|
+
max_conversation_turns: int = 1,
|
|
88
117
|
max_simulation_results: int = 3,
|
|
89
118
|
api_call_retry_limit: int = 3,
|
|
90
119
|
api_call_retry_sleep_sec: int = 1,
|
|
91
120
|
api_call_delay_sec: int = 0,
|
|
92
121
|
concurrent_async_task: int = 3,
|
|
93
|
-
**kwargs,
|
|
94
122
|
):
|
|
95
123
|
"""
|
|
96
124
|
Initializes the XPIA (cross domain prompt injected attack) jailbreak adversarial simulator with a project scope.
|
|
97
125
|
This simulator converses with your AI system using prompts injected into the context to interrupt normal
|
|
98
126
|
expected functionality by eliciting manipulated content, intrusion and attempting to gather information outside
|
|
99
127
|
the scope of your AI system.
|
|
128
|
+
|
|
129
|
+
:keyword scenario: Enum value specifying the adversarial scenario used for generating inputs.
|
|
130
|
+
:paramtype scenario: azure.ai.evaluation.simulator.AdversarialScenario
|
|
100
131
|
:keyword target: The target function to simulate adversarial inputs against.
|
|
101
132
|
This function should be asynchronous and accept a dictionary representing the adversarial input.
|
|
102
133
|
:paramtype target: Callable
|
|
134
|
+
:keyword max_conversation_turns: The maximum number of conversation turns to simulate.
|
|
135
|
+
Defaults to 1.
|
|
136
|
+
:paramtype max_conversation_turns: int
|
|
103
137
|
:keyword max_simulation_results: The maximum number of simulation results to return.
|
|
104
138
|
Defaults to 3.
|
|
105
139
|
:paramtype max_simulation_results: int
|
|
@@ -136,11 +170,11 @@ class IndirectAttackSimulator(AdversarialSimulator):
|
|
|
136
170
|
'template_parameters': {},
|
|
137
171
|
'messages': [
|
|
138
172
|
{
|
|
139
|
-
'content': '<adversarial query>',
|
|
173
|
+
'content': '<jailbreak prompt> <adversarial query>',
|
|
140
174
|
'role': 'user'
|
|
141
175
|
},
|
|
142
176
|
{
|
|
143
|
-
'content': "<response from
|
|
177
|
+
'content': "<response from endpoint>",
|
|
144
178
|
'role': 'assistant',
|
|
145
179
|
'context': None
|
|
146
180
|
}
|
|
@@ -149,72 +183,25 @@ class IndirectAttackSimulator(AdversarialSimulator):
|
|
|
149
183
|
}]
|
|
150
184
|
}
|
|
151
185
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
sim_results = []
|
|
161
|
-
tasks = []
|
|
162
|
-
total_tasks = sum(len(t.template_parameters) for t in templates)
|
|
163
|
-
if max_simulation_results > total_tasks:
|
|
164
|
-
logger.warning(
|
|
165
|
-
"Cannot provide %s results due to maximum number of adversarial simulations that can be generated: %s."
|
|
166
|
-
"\n %s simulations will be generated.",
|
|
167
|
-
max_simulation_results,
|
|
168
|
-
total_tasks,
|
|
169
|
-
total_tasks,
|
|
186
|
+
if scenario not in AdversarialScenario.__members__.values():
|
|
187
|
+
msg = f"Invalid scenario: {scenario}. Supported scenarios: {AdversarialScenario.__members__.values()}"
|
|
188
|
+
raise EvaluationException(
|
|
189
|
+
message=msg,
|
|
190
|
+
internal_message=msg,
|
|
191
|
+
target=ErrorTarget.DIRECT_ATTACK_SIMULATOR,
|
|
192
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
193
|
+
blame=ErrorBlame.USER_ERROR,
|
|
170
194
|
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
jb_sim = AdversarialSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
|
|
196
|
+
jb_sim_results = await jb_sim(
|
|
197
|
+
scenario=scenario,
|
|
198
|
+
target=target,
|
|
199
|
+
max_conversation_turns=max_conversation_turns,
|
|
200
|
+
max_simulation_results=max_simulation_results,
|
|
201
|
+
api_call_retry_limit=api_call_retry_limit,
|
|
202
|
+
api_call_retry_sleep_sec=api_call_retry_sleep_sec,
|
|
203
|
+
api_call_delay_sec=api_call_delay_sec,
|
|
204
|
+
concurrent_async_task=concurrent_async_task,
|
|
205
|
+
_jailbreak_type="xpia",
|
|
177
206
|
)
|
|
178
|
-
|
|
179
|
-
for parameter in template.template_parameters:
|
|
180
|
-
tasks.append(
|
|
181
|
-
asyncio.create_task(
|
|
182
|
-
self._simulate_async(
|
|
183
|
-
target=target,
|
|
184
|
-
template=template,
|
|
185
|
-
parameters=parameter,
|
|
186
|
-
max_conversation_turns=max_conversation_turns,
|
|
187
|
-
api_call_retry_limit=api_call_retry_limit,
|
|
188
|
-
api_call_retry_sleep_sec=api_call_retry_sleep_sec,
|
|
189
|
-
api_call_delay_sec=api_call_delay_sec,
|
|
190
|
-
language=language,
|
|
191
|
-
semaphore=semaphore,
|
|
192
|
-
)
|
|
193
|
-
)
|
|
194
|
-
)
|
|
195
|
-
if len(tasks) >= max_simulation_results:
|
|
196
|
-
break
|
|
197
|
-
if len(tasks) >= max_simulation_results:
|
|
198
|
-
break
|
|
199
|
-
for task in asyncio.as_completed(tasks):
|
|
200
|
-
completed_task = await task # type: ignore
|
|
201
|
-
template_parameters = completed_task.get("template_parameters", {}) # type: ignore
|
|
202
|
-
xpia_attack_type = template_parameters.get("xpia_attack_type", "") # type: ignore
|
|
203
|
-
action = template_parameters.get("action", "") # type: ignore
|
|
204
|
-
document_type = template_parameters.get("document_type", "") # type: ignore
|
|
205
|
-
sim_results.append(
|
|
206
|
-
{
|
|
207
|
-
"messages": completed_task["messages"], # type: ignore
|
|
208
|
-
"$schema": "http://azureml/sdk-2-0/ChatConversation.json",
|
|
209
|
-
"template_parameters": {
|
|
210
|
-
"metadata": {
|
|
211
|
-
"xpia_attack_type": xpia_attack_type,
|
|
212
|
-
"action": action,
|
|
213
|
-
"document_type": document_type,
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
}
|
|
217
|
-
)
|
|
218
|
-
progress_bar.update(1)
|
|
219
|
-
progress_bar.close()
|
|
220
|
-
return JsonLineList(sim_results)
|
|
207
|
+
return jb_sim_results
|