azure-ai-evaluation 0.0.0b0__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.

Files changed (100) hide show
  1. azure/ai/evaluation/__init__.py +60 -0
  2. azure/ai/evaluation/_common/__init__.py +16 -0
  3. azure/ai/evaluation/_common/constants.py +65 -0
  4. azure/ai/evaluation/_common/rai_service.py +452 -0
  5. azure/ai/evaluation/_common/utils.py +87 -0
  6. azure/ai/evaluation/_constants.py +50 -0
  7. azure/ai/evaluation/_evaluate/__init__.py +3 -0
  8. azure/ai/evaluation/_evaluate/_batch_run_client/__init__.py +8 -0
  9. azure/ai/evaluation/_evaluate/_batch_run_client/batch_run_context.py +72 -0
  10. azure/ai/evaluation/_evaluate/_batch_run_client/code_client.py +150 -0
  11. azure/ai/evaluation/_evaluate/_batch_run_client/proxy_client.py +61 -0
  12. azure/ai/evaluation/_evaluate/_eval_run.py +494 -0
  13. azure/ai/evaluation/_evaluate/_evaluate.py +689 -0
  14. azure/ai/evaluation/_evaluate/_telemetry/__init__.py +174 -0
  15. azure/ai/evaluation/_evaluate/_utils.py +237 -0
  16. azure/ai/evaluation/_evaluators/__init__.py +3 -0
  17. azure/ai/evaluation/_evaluators/_bleu/__init__.py +9 -0
  18. azure/ai/evaluation/_evaluators/_bleu/_bleu.py +73 -0
  19. azure/ai/evaluation/_evaluators/_chat/__init__.py +9 -0
  20. azure/ai/evaluation/_evaluators/_chat/_chat.py +350 -0
  21. azure/ai/evaluation/_evaluators/_chat/retrieval/__init__.py +9 -0
  22. azure/ai/evaluation/_evaluators/_chat/retrieval/_retrieval.py +163 -0
  23. azure/ai/evaluation/_evaluators/_chat/retrieval/retrieval.prompty +48 -0
  24. azure/ai/evaluation/_evaluators/_coherence/__init__.py +7 -0
  25. azure/ai/evaluation/_evaluators/_coherence/_coherence.py +122 -0
  26. azure/ai/evaluation/_evaluators/_coherence/coherence.prompty +62 -0
  27. azure/ai/evaluation/_evaluators/_content_safety/__init__.py +21 -0
  28. azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +108 -0
  29. azure/ai/evaluation/_evaluators/_content_safety/_content_safety_base.py +66 -0
  30. azure/ai/evaluation/_evaluators/_content_safety/_content_safety_chat.py +296 -0
  31. azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +78 -0
  32. azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +76 -0
  33. azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +76 -0
  34. azure/ai/evaluation/_evaluators/_content_safety/_violence.py +76 -0
  35. azure/ai/evaluation/_evaluators/_eci/__init__.py +0 -0
  36. azure/ai/evaluation/_evaluators/_eci/_eci.py +99 -0
  37. azure/ai/evaluation/_evaluators/_f1_score/__init__.py +9 -0
  38. azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +141 -0
  39. azure/ai/evaluation/_evaluators/_fluency/__init__.py +9 -0
  40. azure/ai/evaluation/_evaluators/_fluency/_fluency.py +122 -0
  41. azure/ai/evaluation/_evaluators/_fluency/fluency.prompty +61 -0
  42. azure/ai/evaluation/_evaluators/_gleu/__init__.py +9 -0
  43. azure/ai/evaluation/_evaluators/_gleu/_gleu.py +71 -0
  44. azure/ai/evaluation/_evaluators/_groundedness/__init__.py +9 -0
  45. azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +123 -0
  46. azure/ai/evaluation/_evaluators/_groundedness/groundedness.prompty +54 -0
  47. azure/ai/evaluation/_evaluators/_meteor/__init__.py +9 -0
  48. azure/ai/evaluation/_evaluators/_meteor/_meteor.py +96 -0
  49. azure/ai/evaluation/_evaluators/_protected_material/__init__.py +5 -0
  50. azure/ai/evaluation/_evaluators/_protected_material/_protected_material.py +104 -0
  51. azure/ai/evaluation/_evaluators/_protected_materials/__init__.py +5 -0
  52. azure/ai/evaluation/_evaluators/_protected_materials/_protected_materials.py +104 -0
  53. azure/ai/evaluation/_evaluators/_qa/__init__.py +9 -0
  54. azure/ai/evaluation/_evaluators/_qa/_qa.py +111 -0
  55. azure/ai/evaluation/_evaluators/_relevance/__init__.py +9 -0
  56. azure/ai/evaluation/_evaluators/_relevance/_relevance.py +131 -0
  57. azure/ai/evaluation/_evaluators/_relevance/relevance.prompty +69 -0
  58. azure/ai/evaluation/_evaluators/_rouge/__init__.py +10 -0
  59. azure/ai/evaluation/_evaluators/_rouge/_rouge.py +98 -0
  60. azure/ai/evaluation/_evaluators/_similarity/__init__.py +9 -0
  61. azure/ai/evaluation/_evaluators/_similarity/_similarity.py +130 -0
  62. azure/ai/evaluation/_evaluators/_similarity/similarity.prompty +71 -0
  63. azure/ai/evaluation/_evaluators/_xpia/__init__.py +5 -0
  64. azure/ai/evaluation/_evaluators/_xpia/xpia.py +140 -0
  65. azure/ai/evaluation/_exceptions.py +107 -0
  66. azure/ai/evaluation/_http_utils.py +395 -0
  67. azure/ai/evaluation/_model_configurations.py +27 -0
  68. azure/ai/evaluation/_user_agent.py +6 -0
  69. azure/ai/evaluation/_version.py +5 -0
  70. azure/ai/evaluation/py.typed +0 -0
  71. azure/ai/evaluation/simulator/__init__.py +15 -0
  72. azure/ai/evaluation/simulator/_adversarial_scenario.py +27 -0
  73. azure/ai/evaluation/simulator/_adversarial_simulator.py +450 -0
  74. azure/ai/evaluation/simulator/_constants.py +17 -0
  75. azure/ai/evaluation/simulator/_conversation/__init__.py +315 -0
  76. azure/ai/evaluation/simulator/_conversation/_conversation.py +178 -0
  77. azure/ai/evaluation/simulator/_conversation/constants.py +30 -0
  78. azure/ai/evaluation/simulator/_direct_attack_simulator.py +252 -0
  79. azure/ai/evaluation/simulator/_helpers/__init__.py +4 -0
  80. azure/ai/evaluation/simulator/_helpers/_language_suffix_mapping.py +17 -0
  81. azure/ai/evaluation/simulator/_helpers/_simulator_data_classes.py +93 -0
  82. azure/ai/evaluation/simulator/_indirect_attack_simulator.py +207 -0
  83. azure/ai/evaluation/simulator/_model_tools/__init__.py +23 -0
  84. azure/ai/evaluation/simulator/_model_tools/_identity_manager.py +147 -0
  85. azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +228 -0
  86. azure/ai/evaluation/simulator/_model_tools/_rai_client.py +157 -0
  87. azure/ai/evaluation/simulator/_model_tools/_template_handler.py +157 -0
  88. azure/ai/evaluation/simulator/_model_tools/models.py +616 -0
  89. azure/ai/evaluation/simulator/_prompty/task_query_response.prompty +69 -0
  90. azure/ai/evaluation/simulator/_prompty/task_simulate.prompty +36 -0
  91. azure/ai/evaluation/simulator/_tracing.py +92 -0
  92. azure/ai/evaluation/simulator/_utils.py +111 -0
  93. azure/ai/evaluation/simulator/simulator.py +579 -0
  94. azure_ai_evaluation-1.0.0b1.dist-info/METADATA +377 -0
  95. azure_ai_evaluation-1.0.0b1.dist-info/RECORD +97 -0
  96. {azure_ai_evaluation-0.0.0b0.dist-info → azure_ai_evaluation-1.0.0b1.dist-info}/WHEEL +1 -1
  97. azure_ai_evaluation-1.0.0b1.dist-info/top_level.txt +1 -0
  98. azure_ai_evaluation-0.0.0b0.dist-info/METADATA +0 -7
  99. azure_ai_evaluation-0.0.0b0.dist-info/RECORD +0 -4
  100. azure_ai_evaluation-0.0.0b0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,147 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import time
9
+ from abc import ABC, abstractmethod
10
+ from enum import Enum
11
+ from typing import Dict, Optional, Union
12
+
13
+ from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
14
+
15
+ AZURE_TOKEN_REFRESH_INTERVAL = 600 # seconds
16
+
17
+
18
+ class TokenScope(Enum):
19
+ """Token scopes for Azure endpoints"""
20
+
21
+ DEFAULT_AZURE_MANAGEMENT = "https://management.azure.com/.default"
22
+
23
+
24
+ class APITokenManager(ABC):
25
+ """Base class for managing API tokens. Subclasses should implement the get_token method.
26
+
27
+ :param logger: Logger object
28
+ :type logger: logging.Logger
29
+ :param auth_header: Authorization header prefix. Defaults to "Bearer"
30
+ :type auth_header: str
31
+ :param credential: Azure credential object
32
+ :type credential: Optional[Union[azure.identity.DefaultAzureCredential, azure.identity.ManagedIdentityCredential]
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ logger: logging.Logger,
38
+ auth_header: str = "Bearer",
39
+ credential: Optional[Union[DefaultAzureCredential, ManagedIdentityCredential]] = None,
40
+ ) -> None:
41
+ self.logger = logger
42
+ self.auth_header = auth_header
43
+ self._lock = None
44
+ if credential is not None:
45
+ self.credential = credential
46
+ else:
47
+ self.credential = self.get_aad_credential()
48
+ self.token = None
49
+ self.last_refresh_time = None
50
+
51
+ @property
52
+ def lock(self) -> asyncio.Lock:
53
+ """Return object for managing concurrent access to the token.
54
+
55
+ If the lock object does not exist, it will be created first.
56
+
57
+ :return: Lock object
58
+ :rtype: asyncio.Lock
59
+ """
60
+ if self._lock is None:
61
+ self._lock = asyncio.Lock()
62
+ return self._lock
63
+
64
+ def get_aad_credential(self) -> Union[DefaultAzureCredential, ManagedIdentityCredential]:
65
+ """Return the AAD credential object.
66
+
67
+ If the environment variable DEFAULT_IDENTITY_CLIENT_ID is set, ManagedIdentityCredential will be used with
68
+ the specified client ID. Otherwise, DefaultAzureCredential will be used.
69
+
70
+ :return: The AAD credential object
71
+ :rtype: Union[DefaultAzureCredential, ManagedIdentityCredential]
72
+ """
73
+ identity_client_id = os.environ.get("DEFAULT_IDENTITY_CLIENT_ID", None)
74
+ if identity_client_id is not None:
75
+ self.logger.info(f"Using DEFAULT_IDENTITY_CLIENT_ID: {identity_client_id}")
76
+ credential = ManagedIdentityCredential(client_id=identity_client_id)
77
+ else:
78
+ self.logger.info("Environment variable DEFAULT_IDENTITY_CLIENT_ID is not set, using DefaultAzureCredential")
79
+ credential = DefaultAzureCredential()
80
+ return credential
81
+
82
+ @abstractmethod
83
+ async def get_token(self) -> str:
84
+ """Async method to get the API token. Subclasses should implement this method.
85
+
86
+ :return: API token
87
+ :rtype: str
88
+ """
89
+ pass # pylint: disable=unnecessary-pass
90
+
91
+
92
+ class ManagedIdentityAPITokenManager(APITokenManager):
93
+ """API Token Manager for Azure Managed Identity
94
+
95
+ :param token_scope: Token scope for Azure endpoint
96
+ :type token_scope: ~azure.ai.evaluation.simulator._model_tools.TokenScope
97
+ :param logger: Logger object
98
+ :type logger: logging.Logger
99
+ :keyword kwargs: Additional keyword arguments
100
+ :paramtype kwargs: Dict
101
+ """
102
+
103
+ def __init__(self, token_scope: TokenScope, logger: logging.Logger, **kwargs: Dict):
104
+ super().__init__(logger, **kwargs)
105
+ self.token_scope = token_scope
106
+
107
+ # Bug 3353724: This get_token is sync method, but it is defined as async method in the base class
108
+ def get_token(self) -> str: # pylint: disable=invalid-overridden-method
109
+ """Get the API token. If the token is not available or has expired, refresh the token.
110
+
111
+ :return: API token
112
+ :rtype: str
113
+ """
114
+ if (
115
+ self.token is None
116
+ or self.last_refresh_time is None
117
+ or time.time() - self.last_refresh_time > AZURE_TOKEN_REFRESH_INTERVAL
118
+ ):
119
+ self.last_refresh_time = time.time()
120
+ self.token = self.credential.get_token(self.token_scope.value).token
121
+ self.logger.info("Refreshed Azure endpoint token.")
122
+
123
+ return self.token
124
+
125
+
126
+ class PlainTokenManager(APITokenManager):
127
+ """Plain API Token Manager
128
+
129
+ :param openapi_key: OpenAPI key
130
+ :type openapi_key: str
131
+ :param logger: Logger object
132
+ :type logger: logging.Logger
133
+ :keyword kwargs: Optional keyword arguments
134
+ :paramtype kwargs: Dict
135
+ """
136
+
137
+ def __init__(self, openapi_key: str, logger: logging.Logger, **kwargs: Dict):
138
+ super().__init__(logger, **kwargs)
139
+ self.token = openapi_key
140
+
141
+ async def get_token(self) -> str:
142
+ """Get the API token
143
+
144
+ :return: API token
145
+ :rtype: str
146
+ """
147
+ return self.token
@@ -0,0 +1,228 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ import asyncio
5
+ import copy
6
+ import json
7
+ import time
8
+ import uuid
9
+ from typing import Dict, List
10
+
11
+ from azure.core.exceptions import HttpResponseError
12
+ from azure.core.pipeline.policies import AsyncRetryPolicy, RetryMode
13
+
14
+ from azure.ai.evaluation._http_utils import AsyncHttpPipeline, get_async_http_client
15
+ from azure.ai.evaluation._user_agent import USER_AGENT
16
+
17
+ from .models import OpenAIChatCompletionsModel
18
+
19
+
20
+ class SimulationRequestDTO:
21
+ """Simulation Request Data Transfer Object
22
+
23
+ :param url: The URL to send the request to.
24
+ :type url: str
25
+ :param headers: The headers to send with the request.
26
+ :type headers: Dict[str, str]
27
+ :param payload: The payload to send with the request.
28
+ :type payload: Dict[str, Any]
29
+ :param params: The parameters to send with the request.
30
+ :type params: Dict[str, str]
31
+ :param template_key: The template key to use for the request.
32
+ :type template_key: str
33
+ :param template_parameters: The template parameters to use for the request.
34
+ :type template_parameters: Dict
35
+ """
36
+
37
+ def __init__(self, url, headers, payload, params, templatekey, template_parameters):
38
+ self.url = url
39
+ self.headers = headers
40
+ self.json = json.dumps(payload)
41
+ self.params = params
42
+ self.templatekey = templatekey
43
+ self.templateParameters = template_parameters
44
+
45
+ def to_dict(self) -> Dict:
46
+ """Convert the DTO to a dictionary.
47
+
48
+ :return: The DTO as a dictionary.
49
+ :rtype: Dict
50
+ """
51
+ if self.templateParameters is not None:
52
+ self.templateParameters = {str(k): str(v) for k, v in self.templateParameters.items()}
53
+ return self.__dict__
54
+
55
+ def to_json(self):
56
+ """Convert the DTO to a JSON string.
57
+
58
+ :return: The DTO as a JSON string.
59
+ :rtype: str
60
+ """
61
+ return json.dumps(self.__dict__)
62
+
63
+
64
+ class ProxyChatCompletionsModel(OpenAIChatCompletionsModel):
65
+ """A chat completion model that uses a proxy to query the model with a body of data.
66
+
67
+ :param name: The name of the model.
68
+ :type name: str
69
+ :param template_key: The template key to use for the request.
70
+ :type template_key: str
71
+ :param template_parameters: The template parameters to use for the request.
72
+ :type template_parameters: Dict
73
+ :keyword args: Additional arguments to pass to the parent class.
74
+ :keyword kwargs: Additional keyword arguments to pass to the parent class.
75
+ """
76
+
77
+ def __init__(self, name: str, template_key: str, template_parameters, *args, **kwargs) -> None:
78
+ self.tkey = template_key
79
+ self.tparam = template_parameters
80
+ self.result_url = None
81
+
82
+ super().__init__(name=name, *args, **kwargs)
83
+
84
+ def format_request_data(self, messages: List[Dict], **request_params) -> Dict: # type: ignore[override]
85
+ """Format the request data to query the model with.
86
+
87
+ :param messages: List of messages to query the model with.
88
+ Expected format: [{"role": "user", "content": "Hello!"}, ...]
89
+ :type messages: List[Dict]
90
+ :keyword request_params: Additional parameters to pass to the model.
91
+ :paramtype request_params: Dict
92
+ :return: The formatted request data.
93
+ :rtype: Dict
94
+ """
95
+ request_data = {"messages": messages, **self.get_model_params()}
96
+ request_data.update(request_params)
97
+ return request_data
98
+
99
+ async def get_conversation_completion(
100
+ self,
101
+ messages: List[Dict],
102
+ session: AsyncHttpPipeline,
103
+ role: str = "assistant", # pylint: disable=unused-argument
104
+ **request_params,
105
+ ) -> dict:
106
+ """
107
+ Query the model a single time with a message.
108
+
109
+ :param messages: List of messages to query the model with.
110
+ Expected format: [{"role": "user", "content": "Hello!"}, ...]
111
+ :type messages: List[Dict]
112
+ :param session: AsyncHttpPipeline object to query the model with.
113
+ :type session: ~azure.ai.evaluation._http_utils.AsyncHttpPipeline
114
+ :param role: The role of the user sending the message. This parameter is not used in this method;
115
+ however, it must be included to match the method signature of the parent class. Defaults to "assistant".
116
+ :type role: str
117
+ :keyword request_params: Additional parameters to pass to the model.
118
+ :paramtype request_params: Dict
119
+ :return: A dictionary representing the completion of the conversation query.
120
+ :rtype: Dict
121
+ """
122
+ request_data = self.format_request_data(
123
+ messages=messages,
124
+ **request_params,
125
+ )
126
+ return await self.request_api(
127
+ session=session,
128
+ request_data=request_data,
129
+ )
130
+
131
+ async def request_api(
132
+ self,
133
+ session: AsyncHttpPipeline,
134
+ request_data: dict,
135
+ ) -> dict:
136
+ """
137
+ Request the model with a body of data.
138
+
139
+ :param session: HTTPS Session for invoking the endpoint.
140
+ :type session: AsyncHttpPipeline
141
+ :param request_data: Prompt dictionary to query the model with. (Pass {"prompt": prompt} instead of prompt.)
142
+ :type request_data: Dict[str, Any]
143
+ :return: A body of data resulting from the model query.
144
+ :rtype: Dict[str, Any]
145
+ """
146
+
147
+ self._log_request(request_data)
148
+
149
+ token = self.token_manager.get_token()
150
+
151
+ proxy_headers = {
152
+ "Authorization": f"Bearer {token}",
153
+ "Content-Type": "application/json",
154
+ "User-Agent": USER_AGENT,
155
+ }
156
+
157
+ headers = {
158
+ "Content-Type": "application/json",
159
+ "X-CV": f"{uuid.uuid4()}",
160
+ "X-ModelType": self.model or "",
161
+ }
162
+ # add all additional headers
163
+ headers.update(self.additional_headers) # type: ignore[arg-type]
164
+
165
+ params = {}
166
+ if self.api_version:
167
+ params["api-version"] = self.api_version
168
+
169
+ sim_request_dto = SimulationRequestDTO(
170
+ url=self.endpoint_url,
171
+ headers=headers,
172
+ payload=request_data,
173
+ params=params,
174
+ templatekey=self.tkey,
175
+ template_parameters=self.tparam,
176
+ )
177
+
178
+ time_start = time.time()
179
+ full_response = None
180
+
181
+ response = await session.post(url=self.endpoint_url, headers=proxy_headers, json=sim_request_dto.to_dict())
182
+
183
+ if response.status_code != 202:
184
+ raise HttpResponseError(
185
+ message=f"Received unexpected HTTP status: {response.status_code} {response.text()}", response=response
186
+ )
187
+
188
+ response = response.json()
189
+ self.result_url = response["location"]
190
+
191
+ retry_policy = AsyncRetryPolicy( # set up retry configuration
192
+ retry_on_status_codes=[202], # on which statuses to retry
193
+ retry_total=7,
194
+ retry_backoff_factor=10.0,
195
+ retry_backoff_max=180,
196
+ retry_mode=RetryMode.Exponential,
197
+ )
198
+
199
+ # initial 15 seconds wait before attempting to fetch result
200
+ # Need to wait both in this thread and in the async thread for some reason?
201
+ # Someone not under a crunch and with better async understandings should dig into this more.
202
+ await asyncio.sleep(15)
203
+ time.sleep(15)
204
+
205
+ async with get_async_http_client().with_policies(retry_policy=retry_policy) as exp_retry_client:
206
+ response = await exp_retry_client.get( # pylint: disable=too-many-function-args,unexpected-keyword-arg
207
+ self.result_url, headers=proxy_headers
208
+ )
209
+
210
+ response.raise_for_status()
211
+
212
+ response_data = response.json()
213
+ self.logger.info("Response: %s", response_data)
214
+
215
+ # Copy the full response and return it to be saved in jsonl.
216
+ full_response = copy.copy(response_data)
217
+
218
+ time_taken = time.time() - time_start
219
+
220
+ # pylint: disable=unexpected-keyword-arg
221
+ parsed_response = self._parse_response(response_data, request_data=request_data) # type: ignore[call-arg]
222
+
223
+ return {
224
+ "request": request_data,
225
+ "response": parsed_response,
226
+ "time_taken": time_taken,
227
+ "full_response": full_response,
228
+ }
@@ -0,0 +1,157 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ import os
5
+ from typing import Any, Dict
6
+ from urllib.parse import urljoin, urlparse
7
+
8
+ from azure.core.pipeline.policies import AsyncRetryPolicy, RetryMode
9
+
10
+ from azure.ai.evaluation._http_utils import AsyncHttpPipeline, get_async_http_client, get_http_client
11
+ from azure.ai.evaluation._user_agent import USER_AGENT
12
+ from azure.ai.evaluation._exceptions import EvaluationException, ErrorBlame, ErrorCategory, ErrorTarget
13
+ from azure.ai.evaluation._model_configurations import AzureAIProject
14
+
15
+ from ._identity_manager import APITokenManager
16
+
17
+ api_url = None
18
+ if "RAI_SVC_URL" in os.environ:
19
+ api_url = os.environ["RAI_SVC_URL"]
20
+ api_url = api_url.rstrip("/")
21
+ print(f"Found RAI_SVC_URL in environment variable, using {api_url} for the service endpoint.")
22
+
23
+
24
+ class RAIClient:
25
+ """Client for the Responsible AI Service
26
+
27
+ :param azure_ai_project: The scope of the Azure AI project. It contains subscription id, resource group, and project
28
+ name.
29
+ :type azure_ai_project: ~azure.ai.evaluation.AzureAIProject
30
+ :param token_manager: The token manager
31
+ :type token_manage: ~azure.ai.evaluation.simulator._model_tools._identity_manager.APITokenManager
32
+ """
33
+
34
+ def __init__(self, azure_ai_project: AzureAIProject, token_manager: APITokenManager) -> None:
35
+ self.azure_ai_project = azure_ai_project
36
+ self.token_manager = token_manager
37
+
38
+ self.contentharm_parameters = None
39
+ self.jailbreaks_dataset = None
40
+
41
+ if api_url is not None:
42
+ host = api_url
43
+
44
+ else:
45
+ host = self._get_service_discovery_url()
46
+ segments = [
47
+ host.rstrip("/"),
48
+ "raisvc/v1.0/subscriptions",
49
+ self.azure_ai_project["subscription_id"],
50
+ "resourceGroups",
51
+ self.azure_ai_project["resource_group_name"],
52
+ "providers/Microsoft.MachineLearningServices/workspaces",
53
+ self.azure_ai_project["project_name"],
54
+ ]
55
+ self.api_url = "/".join(segments)
56
+ # add a "/" at the end of the url
57
+ self.api_url = self.api_url.rstrip("/") + "/"
58
+ self.parameter_json_endpoint = urljoin(self.api_url, "simulation/template/parameters")
59
+ self.jailbreaks_json_endpoint = urljoin(self.api_url, "simulation/jailbreak")
60
+ self.simulation_submit_endpoint = urljoin(self.api_url, "simulation/chat/completions/submit")
61
+ self.xpia_jailbreaks_json_endpoint = urljoin(self.api_url, "simulation/jailbreak/xpia")
62
+
63
+ def _get_service_discovery_url(self):
64
+ bearer_token = self.token_manager.get_token()
65
+ headers = {"Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json"}
66
+ http_client = get_http_client()
67
+ response = http_client.get( # pylint: disable=too-many-function-args,unexpected-keyword-arg
68
+ f"https://management.azure.com/subscriptions/{self.azure_ai_project['subscription_id']}/"
69
+ f"resourceGroups/{self.azure_ai_project['resource_group_name']}/"
70
+ f"providers/Microsoft.MachineLearningServices/workspaces/{self.azure_ai_project['project_name']}?"
71
+ f"api-version=2023-08-01-preview",
72
+ headers=headers,
73
+ timeout=5,
74
+ )
75
+ if response.status_code != 200:
76
+ msg = f"Failed to retrieve the discovery service URL."
77
+ raise EvaluationException(
78
+ message=msg,
79
+ internal_message=msg,
80
+ target=ErrorTarget.RAI_CLIENT,
81
+ category=ErrorCategory.SERVICE_UNAVAILABLE,
82
+ blame=ErrorBlame.UNKNOWN,
83
+ )
84
+ base_url = urlparse(response.json()["properties"]["discoveryUrl"])
85
+ return f"{base_url.scheme}://{base_url.netloc}"
86
+
87
+ def _create_async_client(self) -> AsyncHttpPipeline:
88
+ """Create an async http client with retry mechanism
89
+
90
+ Number of retries is set to 6, and the timeout is set to 5 seconds.
91
+
92
+ :return: The async http client
93
+ :rtype: ~azure.ai.evaluation._http_utils.AsyncHttpPipeline
94
+ """
95
+ return get_async_http_client().with_policies(
96
+ retry_policy=AsyncRetryPolicy(retry_total=6, retry_backoff_factor=5, retry_mode=RetryMode.Fixed)
97
+ )
98
+
99
+ async def get_contentharm_parameters(self) -> Any:
100
+ """Get the content harm parameters, if they exist"""
101
+ if self.contentharm_parameters is None:
102
+ self.contentharm_parameters = await self.get(self.parameter_json_endpoint)
103
+
104
+ return self.contentharm_parameters
105
+
106
+ async def get_jailbreaks_dataset(self, type: str) -> Any:
107
+ "Get the jailbreaks dataset, if exists"
108
+ if self.jailbreaks_dataset is None:
109
+ if type == "xpia":
110
+ self.jailbreaks_dataset = await self.get(self.xpia_jailbreaks_json_endpoint)
111
+ elif type == "upia":
112
+ self.jailbreaks_dataset = await self.get(self.jailbreaks_json_endpoint)
113
+ else:
114
+ msg = f"Invalid jailbreak type: {type}. Supported types: ['xpia', 'upia']"
115
+ raise EvaluationException(
116
+ message=msg,
117
+ internal_message=msg,
118
+ target=ErrorTarget.ADVERSARIAL_SIMULATOR,
119
+ category=ErrorCategory.INVALID_VALUE,
120
+ blame=ErrorBlame.USER_ERROR,
121
+ )
122
+
123
+ return self.jailbreaks_dataset
124
+
125
+ async def get(self, url: str) -> Any:
126
+ """Make a GET request to the given url
127
+
128
+ :param url: The url
129
+ :type url: str
130
+ :raises EvaluationException: If the Azure safety evaluation service is not available in the current region
131
+ :return: The response
132
+ :rtype: Any
133
+ """
134
+ token = self.token_manager.get_token()
135
+ headers = {
136
+ "Authorization": f"Bearer {token}",
137
+ "Content-Type": "application/json",
138
+ "User-Agent": USER_AGENT,
139
+ }
140
+
141
+ session = self._create_async_client()
142
+
143
+ async with session:
144
+ response = await session.get(url=url, headers=headers) # pylint: disable=unexpected-keyword-arg
145
+
146
+ if response.status_code == 200:
147
+ return response.json()
148
+
149
+ msg = "Azure safety evaluation service is not available in your current region, "
150
+ "please go to https://aka.ms/azureaistudiosafetyeval to see which regions are supported"
151
+ raise EvaluationException(
152
+ message=msg,
153
+ internal_message=msg,
154
+ target=ErrorTarget.RAI_CLIENT,
155
+ category=ErrorCategory.UNKNOWN,
156
+ blame=ErrorBlame.USER_ERROR,
157
+ )
@@ -0,0 +1,157 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from azure.ai.evaluation._model_configurations import AzureAIProject
8
+
9
+ from ._rai_client import RAIClient
10
+
11
+ CONTENT_HARM_TEMPLATES_COLLECTION_KEY = set(
12
+ [
13
+ "adv_qa",
14
+ "adv_conversation",
15
+ "adv_summarization",
16
+ "adv_search",
17
+ "adv_rewrite",
18
+ "adv_content_gen_ungrounded",
19
+ "adv_content_gen_grounded",
20
+ "adv_content_protected_material",
21
+ "adv_politics",
22
+ ]
23
+ )
24
+
25
+
26
+ class ContentHarmTemplatesUtils:
27
+ """Content harm templates utility functions."""
28
+
29
+ @staticmethod
30
+ def get_template_category(key: str) -> str:
31
+ """Parse category from template key
32
+
33
+ :param key: The template key
34
+ :type key: str
35
+ :return: The category
36
+ :rtype: str
37
+ """
38
+ # Check for datasets whose names do not align with the normal
39
+ # naming convention where the first segment of the name is the category.
40
+ if key == "conversation/public/ip/bing_ip.json":
41
+ return "content_protected_material"
42
+ return key.split("/")[0]
43
+
44
+ @staticmethod
45
+ def get_template_key(key: str) -> str:
46
+ """Given a template dataset name (which looks like a .json file name) convert it into
47
+ the corresponding template key (which looks like a .md file name). This allows us to
48
+ properly link datasets to the LLM that must be used to simulate them.
49
+
50
+ :param key: The dataset key.
51
+ :type key: str
52
+ :return: The template key.
53
+ :rtype: str
54
+ """
55
+ filepath = key.rsplit(".json")[0]
56
+ parts = str(filepath).split("/")
57
+ filename = ContentHarmTemplatesUtils.json_name_to_md_name(parts[-1])
58
+ prefix = parts[:-1]
59
+ prefix.append(filename)
60
+
61
+ return "/".join(prefix)
62
+
63
+ @staticmethod
64
+ def json_name_to_md_name(name) -> str:
65
+ """Convert JSON filename to Markdown filename
66
+
67
+ :param name: The JSON filename
68
+ :type name: str
69
+ :return: The Markdown filename
70
+ :rtype: str
71
+ """
72
+ result = name.replace("_aml", "")
73
+
74
+ return result + ".md"
75
+
76
+
77
+ class AdversarialTemplate:
78
+ """Template for adversarial scenarios.
79
+
80
+ :param template_name: The name of the template.
81
+ :type template_name: str
82
+ :param text: The template text.
83
+ :type text: str
84
+ :param context_key: The context key.
85
+ :param template_parameters: The template parameters.
86
+ """
87
+
88
+ def __init__(self, template_name, text, context_key, template_parameters=None) -> None:
89
+ self.text = text
90
+ self.context_key = context_key
91
+ self.template_name = template_name
92
+ self.template_parameters = template_parameters
93
+
94
+ def __str__(self):
95
+ return "{{ch_template_placeholder}}"
96
+
97
+
98
+ class AdversarialTemplateHandler:
99
+ """
100
+ Adversarial template handler constructor.
101
+
102
+ :param azure_ai_project: The Azure AI project.
103
+ :type azure_ai_project: ~azure.ai.evaluation.AzureAIProject
104
+ :param rai_client: The RAI client.
105
+ :type rai_client: ~azure.ai.evaluation.simulator._model_tools.RAIClient
106
+ """
107
+
108
+ def __init__(self, azure_ai_project: AzureAIProject, rai_client: RAIClient) -> None:
109
+ self.cached_templates_source = {}
110
+ # self.template_env = JinjaEnvironment(loader=JinjaFileSystemLoader(searchpath=template_dir))
111
+ self.azure_ai_project = azure_ai_project
112
+ self.categorized_ch_parameters = None
113
+ self.rai_client = rai_client
114
+
115
+ async def _get_content_harm_template_collections(self, collection_key):
116
+
117
+ if self.categorized_ch_parameters is None:
118
+ categorized_parameters = {}
119
+ util = ContentHarmTemplatesUtils
120
+
121
+ parameters = await self.rai_client.get_contentharm_parameters()
122
+
123
+ for k in parameters.keys():
124
+ template_key = util.get_template_key(k)
125
+ categorized_parameters[template_key] = {
126
+ "parameters": parameters[k],
127
+ "category": util.get_template_category(k),
128
+ "parameters_key": k,
129
+ }
130
+ self.categorized_ch_parameters = categorized_parameters
131
+
132
+ template_category = collection_key.split("adv_")[-1]
133
+
134
+ plist = self.categorized_ch_parameters
135
+ ch_templates = []
136
+ for key, value in plist.items():
137
+ if value["category"] == template_category:
138
+ params = value["parameters"]
139
+ for p in params:
140
+ p.update({"ch_template_placeholder": "{{ch_template_placeholder}}"})
141
+
142
+ template = AdversarialTemplate(template_name=key, text=None, context_key=[], template_parameters=params)
143
+
144
+ ch_templates.append(template)
145
+ return ch_templates
146
+
147
+ def get_template(self, template_name: str) -> Optional[AdversarialTemplate]:
148
+ """Generate content harm template.
149
+
150
+ :param template_name: The name of the template.
151
+ :type template_name: str
152
+ :return: The generated content harm template.
153
+ :rtype: Optional[~azure.ai.evaluation.simulator._model_tools.AdversarialTemplate]
154
+ """
155
+ if template_name in CONTENT_HARM_TEMPLATES_COLLECTION_KEY:
156
+ return AdversarialTemplate(template_name=template_name, text=None, context_key=[], template_parameters=None)
157
+ return None