Skip to content

Commit b70434b

Browse files
nagkumar91Nagkumar ArkalgudNagkumar Arkalgud
authored
Add t1 language support for simulators (#37348)
* Add t1 language support for simulators * Update __init__.py * Update _conversation.py * Update xpia_simulator.py * Update _language_suffix_mapping.py * remove promptflow.evals --------- Co-authored-by: Nagkumar Arkalgud <[email protected]> Co-authored-by: Nagkumar Arkalgud <[email protected]>
1 parent 89cdab7 commit b70434b

File tree

10 files changed

+180
-6
lines changed

10 files changed

+180
-6
lines changed

sdk/evaluation/azure-ai-evaluation/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,108 @@ if __name__ == "__main__":
9393
pprint(result)
9494
```
9595

96+
Simulator expects the user to have a callback method that invokes their AI application.
97+
Here's a sample of a callback which invokes AsyncAzureOpenAI:
98+
99+
```python
100+
from from azure.ai.evaluation.synthetic import AdversarialSimulator, AdversarialScenario
101+
from azure.identity import DefaultAzureCredential
102+
from typing import Any, Dict, List, Optional
103+
import asyncio
104+
105+
106+
azure_ai_project = {
107+
"subscription_id": <subscription_id>,
108+
"resource_group_name": <resource_group_name>,
109+
"project_name": <project_name>
110+
}
111+
112+
async def callback(
113+
messages: List[Dict],
114+
stream: bool = False,
115+
session_state: Any = None,
116+
context: Dict[str, Any] = None
117+
) -> dict:
118+
messages_list = messages["messages"]
119+
# get last message
120+
latest_message = messages_list[-1]
121+
query = latest_message["content"]
122+
context = None
123+
if 'file_content' in messages["template_parameters"]:
124+
query += messages["template_parameters"]['file_content']
125+
# the next few lines explains how to use the AsyncAzureOpenAI's chat.completions
126+
# to respond to the simulator. You should replace it with a call to your model/endpoint/application
127+
# make sure you pass the `query` and format the response as we have shown below
128+
from openai import AsyncAzureOpenAI
129+
oai_client = AsyncAzureOpenAI(
130+
api_key=<api_key>,
131+
azure_endpoint=<endpoint>,
132+
api_version="2023-12-01-preview",
133+
)
134+
try:
135+
response_from_oai_chat_completions = await oai_client.chat.completions.create(messages=[{"content": query, "role": "user"}], model="gpt-4", max_tokens=300)
136+
except Exception as e:
137+
print(f"Error: {e}")
138+
# to continue the conversation, return the messages, else you can fail the adversarial with an exception
139+
message = {
140+
"content": "Something went wrong. Check the exception e for more details.",
141+
"role": "assistant",
142+
"context": None,
143+
}
144+
messages["messages"].append(message)
145+
return {
146+
"messages": messages["messages"],
147+
"stream": stream,
148+
"session_state": session_state
149+
}
150+
response_result = response_from_oai_chat_completions.choices[0].message.content
151+
formatted_response = {
152+
"content": response_result,
153+
"role": "assistant",
154+
"context": {},
155+
}
156+
messages["messages"].append(formatted_response)
157+
return {
158+
"messages": messages["messages"],
159+
"stream": stream,
160+
"session_state": session_state,
161+
"context": context
162+
}
163+
164+
```
165+
### Adversarial QA:
166+
```python
167+
scenario = AdversarialScenario.ADVERSARIAL_QA
168+
simulator = AdversarialSimulator(azure_ai_project=azure_ai_project, credential=DefaultAzureCredential())
169+
170+
outputs = asyncio.run(
171+
simulator(
172+
scenario=scenario,
173+
max_conversation_turns=1,
174+
max_simulation_results=3,
175+
target=callback
176+
)
177+
)
178+
179+
print(outputs.to_eval_qa_json_lines())
180+
```
181+
### Direct Attack Simulator
182+
183+
```python
184+
scenario = AdversarialScenario.ADVERSARIAL_QA
185+
simulator = DirectAttackSimulator(azure_ai_project=azure_ai_project, credential=DefaultAzureCredential())
186+
187+
outputs = asyncio.run(
188+
simulator(
189+
scenario=scenario,
190+
max_conversation_turns=1,
191+
max_simulation_results=2,
192+
target=callback
193+
)
194+
)
195+
196+
print(outputs)
197+
```
96198
## Troubleshooting
97199

98200
## Next steps
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from .adversarial_scenario import AdversarialScenario
22
from .adversarial_simulator import AdversarialSimulator
3+
from .constants import SupportedLanguages
34
from .direct_attack_simulator import DirectAttackSimulator
45
from .indirect_attack_simulator import IndirectAttackSimulator
56

6-
__all__ = ["AdversarialSimulator", "AdversarialScenario", "DirectAttackSimulator", "IndirectAttackSimulator"]
7+
__all__ = [
8+
"AdversarialSimulator",
9+
"AdversarialScenario",
10+
"DirectAttackSimulator",
11+
"IndirectAttackSimulator",
12+
"SupportedLanguages",
13+
]

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/synthetic/_conversation/_conversation.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import logging
77
from typing import Callable, Dict, List, Tuple, Union
88

9+
from azure.ai.evaluation.synthetic._helpers._language_suffix_mapping import SUPPORTED_LANGUAGES_MAPPING
10+
from azure.ai.evaluation.synthetic.constants import SupportedLanguages
11+
912
from ..._http_utils import AsyncHttpPipeline
1013
from . import ConversationBot, ConversationTurn
1114

@@ -60,8 +63,10 @@ def is_closing_message_helper(response: str) -> bool:
6063

6164

6265
async def simulate_conversation(
66+
*,
6367
bots: List[ConversationBot],
6468
session: AsyncHttpPipeline,
69+
language: SupportedLanguages,
6570
stopping_criteria: Callable[[str], bool] = is_closing_message,
6671
turn_limit: int = 10,
6772
history_limit: int = 5,
@@ -101,6 +106,13 @@ async def simulate_conversation(
101106
else:
102107
conversation_id = None
103108
first_prompt = first_response["samples"][0]
109+
if language != SupportedLanguages.English:
110+
if not isinstance(language, SupportedLanguages) or language not in SupportedLanguages:
111+
raise Exception( # pylint: disable=broad-exception-raised
112+
f"Language option '{language}' isn't supported. Select a supported language option from "
113+
f"azure.ai.evaluation.synthetic._constants.SupportedLanguages: {[f'{e}' for e in SupportedLanguages]}"
114+
)
115+
first_prompt += f" {SUPPORTED_LANGUAGES_MAPPING[language]}"
104116
# Add all generated turns into array to pass for each bot while generating
105117
# new responses. We add generated response and the person generating it.
106118
# in the case of the first turn, it is supposed to be the user search query

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/synthetic/_conversation/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,5 @@
2525

2626
class ConversationRole(Enum):
2727
"""Role in a chatbot conversation"""
28-
2928
USER = "user"
3029
ASSISTANT = "assistant"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._language_suffix_mapping import SUPPORTED_LANGUAGES_MAPPING
2+
3+
__all__ = ["SUPPORTED_LANGUAGES_MAPPING"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
from azure.ai.evaluation.synthetic.constants import SupportedLanguages
5+
6+
BASE_SUFFIX = "Make the conversation in __language__ language."
7+
8+
SUPPORTED_LANGUAGES_MAPPING = {
9+
SupportedLanguages.English: BASE_SUFFIX.replace("__language__", "english"),
10+
SupportedLanguages.Spanish: BASE_SUFFIX.replace("__language__", "spanish"),
11+
SupportedLanguages.Italian: BASE_SUFFIX.replace("__language__", "italian"),
12+
SupportedLanguages.French: BASE_SUFFIX.replace("__language__", "french"),
13+
SupportedLanguages.German: BASE_SUFFIX.replace("__language__", "german"),
14+
SupportedLanguages.SimplifiedChinese: BASE_SUFFIX.replace("__language__", "simplified chinese"),
15+
SupportedLanguages.Portuguese: BASE_SUFFIX.replace("__language__", "portuguese"),
16+
SupportedLanguages.Japanese: BASE_SUFFIX.replace("__language__", "japanese"),
17+
}

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/synthetic/adversarial_simulator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# ---------------------------------------------------------
44
# noqa: E501
5+
# pylint: disable=E0401,E0611
56
import asyncio
67
import functools
78
import logging
@@ -26,6 +27,7 @@
2627
TokenScope,
2728
)
2829
from ._utils import JsonLineList
30+
from .constants import SupportedLanguages
2931

3032
logger = logging.getLogger(__name__)
3133

@@ -44,13 +46,15 @@ def wrapper(*args, **kwargs):
4446
scenario = str(kwargs.get("scenario", None))
4547
max_conversation_turns = kwargs.get("max_conversation_turns", None)
4648
max_simulation_results = kwargs.get("max_simulation_results", None)
49+
selected_language = kwargs.get("language", SupportedLanguages.English)
4750
decorated_func = monitor_operation(
4851
activity_name="adversarial.simulator.call",
4952
activity_type=ActivityType.PUBLICAPI,
5053
custom_dimensions={
5154
"scenario": scenario,
5255
"max_conversation_turns": max_conversation_turns,
5356
"max_simulation_results": max_simulation_results,
57+
"selected_language": selected_language,
5458
},
5559
)(func)
5660

@@ -114,6 +118,7 @@ async def __call__(
114118
api_call_delay_sec: int = 0,
115119
concurrent_async_task: int = 3,
116120
_jailbreak_type: Optional[str] = None,
121+
language: SupportedLanguages = SupportedLanguages.English,
117122
randomize_order: bool = True,
118123
randomization_seed: Optional[int] = None,
119124
):
@@ -147,6 +152,8 @@ async def __call__(
147152
:keyword concurrent_async_task: The number of asynchronous tasks to run concurrently during the simulation.
148153
Defaults to 3.
149154
:paramtype concurrent_async_task: int
155+
:keyword language: The language in which the conversation should be generated. Defaults to English.
156+
:paramtype language: azure.ai.evaluation.synthetic.constants.SupportedLanguages
150157
:keyword randomize_order: Whether or not the order of the prompts should be randomized. Defaults to True.
151158
:paramtype randomize_order: bool
152159
:keyword randomization_seed: The seed used to randomize prompt selection. If unset, the system's
@@ -244,6 +251,7 @@ async def __call__(
244251
api_call_retry_limit=api_call_retry_limit,
245252
api_call_retry_sleep_sec=api_call_retry_sleep_sec,
246253
api_call_delay_sec=api_call_delay_sec,
254+
language=language,
247255
semaphore=semaphore,
248256
)
249257
)
@@ -296,6 +304,7 @@ async def _simulate_async(
296304
api_call_retry_limit,
297305
api_call_retry_sleep_sec,
298306
api_call_delay_sec,
307+
language,
299308
semaphore,
300309
) -> List[Dict]:
301310
user_bot = self._setup_bot(role=ConversationRole.USER, template=template, parameters=parameters)
@@ -317,6 +326,7 @@ async def _simulate_async(
317326
session=session,
318327
turn_limit=max_conversation_turns,
319328
api_call_delay_sec=api_call_delay_sec,
329+
language=language,
320330
)
321331
return self._to_chat_protocol(conversation_history=conversation_history, template_parameters=parameters)
322332

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
from enum import Enum
5+
6+
7+
class SupportedLanguages(Enum):
8+
"""Supported languages for evaluation, using ISO standard language codes."""
9+
10+
Spanish = "es"
11+
Italian = "it"
12+
French = "fr"
13+
German = "de"
14+
SimplifiedChinese = "zh-cn"
15+
Portuguese = "pt"
16+
Japanese = "ja"
17+
English = "en"

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/synthetic/direct_attack_simulator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ class DirectAttackSimulator:
5757
* "subscription_id": Azure subscription ID.
5858
* "resource_group_name": Name of the Azure resource group.
5959
* "project_name": Name of the Azure Machine Learning workspace.
60-
:type azure_ai_project: Dict[str, Any]
6160
:param credential: The credential for connecting to Azure AI project.
6261
:type credential: ~azure.core.credentials.TokenCredential
62+
:type azure_ai_project: Dict[str, Any]
6363
"""
6464

6565
def __init__(self, *, azure_ai_project: Dict[str, Any], credential=None):

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/synthetic/indirect_attack_simulator.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
import logging
77
from typing import Any, Callable, Dict
88

9-
from azure.ai.evaluation.synthetic.adversarial_scenario import AdversarialScenario
109
from azure.identity import DefaultAzureCredential
1110

1211
from promptflow._sdk._telemetry import ActivityType, monitor_operation
12+
from azure.ai.evaluation.synthetic.adversarial_scenario import AdversarialScenario
1313

1414
from ._model_tools import AdversarialTemplateHandler, ManagedIdentityAPITokenManager, RAIClient, TokenScope
1515
from .adversarial_simulator import AdversarialSimulator
@@ -19,6 +19,7 @@
1919

2020
def monitor_adversarial_scenario(func) -> Callable:
2121
"""Decorator to monitor adversarial scenario.
22+
2223
:param func: The function to be decorated.
2324
:type func: Callable
2425
:return: The decorated function.
@@ -54,9 +55,9 @@ class IndirectAttackSimulator:
5455
* "subscription_id": Azure subscription ID.
5556
* "resource_group_name": Name of the Azure resource group.
5657
* "project_name": Name of the Azure Machine Learning workspace.
57-
:type azure_ai_project: Dict[str, Any]
5858
:param credential: The credential for connecting to Azure AI project.
5959
:type credential: ~azure.core.credentials.TokenCredential
60+
:type azure_ai_project: Dict[str, Any]
6061
"""
6162

6263
def __init__(self, *, azure_ai_project: Dict[str, Any], credential=None):
@@ -106,8 +107,9 @@ async def __call__(
106107
This simulator converses with your AI system using prompts injected into the context to interrupt normal
107108
expected functionality by eliciting manipulated content, intrusion and attempting to gather information outside
108109
the scope of your AI system.
110+
109111
:keyword scenario: Enum value specifying the adversarial scenario used for generating inputs.
110-
:paramtype scenario: promptflow.evals.synthetic.adversarial_scenario.AdversarialScenario
112+
:paramtype scenario: azure.ai.evaluation.synthetic.adversarial_scenario.AdversarialScenario
111113
:keyword target: The target function to simulate adversarial inputs against.
112114
This function should be asynchronous and accept a dictionary representing the adversarial input.
113115
:paramtype target: Callable
@@ -130,16 +132,21 @@ async def __call__(
130132
Defaults to 3.
131133
:paramtype concurrent_async_task: int
132134
:return: A list of dictionaries, each representing a simulated conversation. Each dictionary contains:
135+
133136
- 'template_parameters': A dictionary with parameters used in the conversation template,
134137
including 'conversation_starter'.
135138
- 'messages': A list of dictionaries, each representing a turn in the conversation.
136139
Each message dictionary includes 'content' (the message text) and
137140
'role' (indicating whether the message is from the 'user' or the 'assistant').
138141
- '**$schema**': A string indicating the schema URL for the conversation format.
142+
139143
The 'content' for 'assistant' role messages may includes the messages that your callback returned.
140144
:rtype: List[Dict[str, Any]]
145+
141146
**Output format**
147+
142148
.. code-block:: python
149+
143150
return_value = [
144151
{
145152
'template_parameters': {},

0 commit comments

Comments
 (0)