Skip to content

Commit a9233f4

Browse files
nagkumar91Nagkumar ArkalgudNagkumar ArkalgudNagkumar Arkalgud
authored
Adding the non adversarial simulator (#37350)
* Adding the non adversarial simulator * Remove promptflow.evals * Fixed the import bug * Fixed the parse response error * Fixed the output to be in json format * Fix progress bar * remove pdb * sphinx please be happy with this * Move application calling logic to a separate function * Synthetic to simulator * Removed old init * Add unittests * Docstring and warning message * First turn through the simulator * Fixes to test and user flow * More checks and changes to messages * Fix the progress bar --------- Co-authored-by: Nagkumar Arkalgud <[email protected]> Co-authored-by: Nagkumar Arkalgud <[email protected]> Co-authored-by: Nagkumar Arkalgud <[email protected]>
1 parent b9824f7 commit a9233f4

File tree

11 files changed

+1185
-34
lines changed

11 files changed

+1185
-34
lines changed

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,132 @@ if __name__ == "__main__":
8787

8888
pprint(result)
8989
```
90+
## Simulator
91+
92+
Sample application prompty
93+
94+
```yaml
95+
---
96+
name: ApplicationPrompty
97+
description: Simulates an application
98+
model:
99+
api: chat
100+
configuration:
101+
type: azure_openai
102+
azure_deployment: ${env:AZURE_DEPLOYMENT}
103+
api_key: ${env:AZURE_OPENAI_API_KEY}
104+
azure_endpoint: ${env:AZURE_OPENAI_ENDPOINT}
105+
parameters:
106+
temperature: 0.0
107+
top_p: 1.0
108+
presence_penalty: 0
109+
frequency_penalty: 0
110+
response_format:
111+
type: text
112+
113+
inputs:
114+
conversation_history:
115+
type: dict
116+
117+
---
118+
system:
119+
You are a helpful assistant and you're helping with the user's query. Keep the conversation engaging and interesting.
120+
121+
Output with a string that continues the conversation, responding to the latest message from the user, given the conversation history:
122+
{{ conversation_history }}
123+
124+
```
125+
Application code:
126+
127+
```python
128+
import json
129+
import asyncio
130+
from typing import Any, Dict, List, Optional
131+
from azure.ai.evaluation.synthetic import Simulator
132+
from promptflow.client import load_flow
133+
from azure.identity import DefaultAzureCredential
134+
import os
135+
136+
azure_ai_project = {
137+
"subscription_id": os.environ.get("AZURE_SUBSCRIPTION_ID"),
138+
"resource_group_name": os.environ.get("RESOURCE_GROUP"),
139+
"project_name": os.environ.get("PROJECT_NAME"),
140+
"credential": DefaultAzureCredential(),
141+
}
142+
143+
import wikipedia
144+
wiki_search_term = "Leonardo da vinci"
145+
wiki_title = wikipedia.search(wiki_search_term)[0]
146+
wiki_page = wikipedia.page(wiki_title)
147+
text = wiki_page.summary[:1000]
148+
149+
def method_to_invoke_application_prompty(query: str):
150+
try:
151+
current_dir = os.path.dirname(__file__)
152+
prompty_path = os.path.join(current_dir, "application.prompty")
153+
_flow = load_flow(source=prompty_path, model={
154+
"configuration": azure_ai_project
155+
})
156+
response = _flow(
157+
query=query,
158+
context=context,
159+
conversation_history=messages_list
160+
)
161+
return response
162+
except:
163+
print("Something went wrong invoking the prompty")
164+
return "something went wrong"
165+
166+
async def callback(
167+
messages: List[Dict],
168+
stream: bool = False,
169+
session_state: Any = None, # noqa: ANN401
170+
context: Optional[Dict[str, Any]] = None,
171+
) -> dict:
172+
messages_list = messages["messages"]
173+
# get last message
174+
latest_message = messages_list[-1]
175+
query = latest_message["content"]
176+
context = None
177+
# call your endpoint or ai application here
178+
response = method_to_invoke_application_prompty(query)
179+
# we are formatting the response to follow the openAI chat protocol format
180+
formatted_response = {
181+
"content": response,
182+
"role": "assistant",
183+
"context": {
184+
"citations": None,
185+
},
186+
}
187+
messages["messages"].append(formatted_response)
188+
return {"messages": messages["messages"], "stream": stream, "session_state": session_state, "context": context}
189+
190+
191+
192+
async def main():
193+
simulator = Simulator(azure_ai_project=azure_ai_project, credential=DefaultAzureCredential())
194+
outputs = await simulator(
195+
target=callback,
196+
text=text,
197+
num_queries=2,
198+
max_conversation_turns=4,
199+
user_persona=[
200+
f"I am a student and I want to learn more about {wiki_search_term}",
201+
f"I am a teacher and I want to teach my students about {wiki_search_term}"
202+
],
203+
)
204+
print(json.dumps(outputs))
205+
206+
if __name__ == "__main__":
207+
os.environ["AZURE_SUBSCRIPTION_ID"] = ""
208+
os.environ["RESOURCE_GROUP"] = ""
209+
os.environ["PROJECT_NAME"] = ""
210+
os.environ["AZURE_OPENAI_API_KEY"] = ""
211+
os.environ["AZURE_OPENAI_ENDPOINT"] = ""
212+
os.environ["AZURE_DEPLOYMENT"] = ""
213+
asyncio.run(main())
214+
print("done!")
215+
```
90216

91217
Simulators allow users to generate synthentic data using their application. Simulator expects the user to have a callback method that invokes
92218
their AI application. Here's a sample of a callback which invokes AsyncAzureOpenAI:

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/simulator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from ._constants import SupportedLanguages
44
from ._direct_attack_simulator import DirectAttackSimulator
55
from ._indirect_attack_simulator import IndirectAttackSimulator
6+
from .simulator import Simulator
67

78
__all__ = [
89
"AdversarialSimulator",
910
"AdversarialScenario",
1011
"DirectAttackSimulator",
1112
"IndirectAttackSimulator",
1213
"SupportedLanguages",
14+
"Simulator",
1315
]

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/simulator/_adversarial_simulator.py

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# noqa: E501
55
# pylint: disable=E0401,E0611
66
import asyncio
7-
import functools
87
import logging
98
import random
109
from typing import Any, Callable, Dict, List, Optional
@@ -13,7 +12,6 @@
1312
from azure.identity import DefaultAzureCredential
1413
from tqdm import tqdm
1514

16-
from promptflow._sdk._telemetry import ActivityType, monitor_operation
1715
from azure.ai.evaluation._http_utils import get_async_http_client
1816
from azure.ai.evaluation._exceptions import EvaluationException, ErrorBlame, ErrorCategory, ErrorTarget
1917
from azure.ai.evaluation._model_configurations import AzureAIProject
@@ -29,43 +27,13 @@
2927
RAIClient,
3028
TokenScope,
3129
)
30+
from ._tracing import monitor_adversarial_scenario
3231
from ._utils import JsonLineList
3332
from ._constants import SupportedLanguages
3433

3534
logger = logging.getLogger(__name__)
3635

3736

38-
def monitor_adversarial_scenario(func) -> Callable:
39-
"""Monitor an adversarial scenario with logging
40-
41-
:param func: The function to be monitored
42-
:type func: Callable
43-
:return: The decorated function
44-
:rtype: Callable
45-
"""
46-
47-
@functools.wraps(func)
48-
def wrapper(*args, **kwargs):
49-
scenario = str(kwargs.get("scenario", None))
50-
max_conversation_turns = kwargs.get("max_conversation_turns", None)
51-
max_simulation_results = kwargs.get("max_simulation_results", None)
52-
selected_language = kwargs.get("language", SupportedLanguages.English)
53-
decorated_func = monitor_operation(
54-
activity_name="adversarial.simulator.call",
55-
activity_type=ActivityType.PUBLICAPI,
56-
custom_dimensions={
57-
"scenario": scenario,
58-
"max_conversation_turns": max_conversation_turns,
59-
"max_simulation_results": max_simulation_results,
60-
"selected_language": selected_language,
61-
},
62-
)(func)
63-
64-
return decorated_func(*args, **kwargs)
65-
66-
return wrapper
67-
68-
6937
class AdversarialSimulator:
7038
"""
7139
Initializes the adversarial simulator with a project scope.
@@ -414,6 +382,7 @@ def _join_conversation_starter(self, parameters, to_join):
414382
def call_sync(
415383
self,
416384
*,
385+
scenario: AdversarialScenario,
417386
max_conversation_turns: int,
418387
max_simulation_results: int,
419388
target: Callable,
@@ -423,6 +392,12 @@ def call_sync(
423392
concurrent_async_task: int,
424393
) -> List[Dict[str, Any]]:
425394
"""Call the adversarial simulator synchronously.
395+
:keyword scenario: Enum value specifying the adversarial scenario used for generating inputs.
396+
example:
397+
398+
- :py:const:`azure.ai.evaluation.simulator.adversarial_scenario.AdversarialScenario.ADVERSARIAL_QA`
399+
- :py:const:`azure.ai.evaluation.simulator.adversarial_scenario.AdversarialScenario.ADVERSARIAL_CONVERSATION`
400+
:paramtype scenario: azure.ai.evaluation.simulator.adversarial_scenario.AdversarialScenario
426401
427402
:keyword max_conversation_turns: The maximum number of conversation turns to simulate.
428403
:paramtype max_conversation_turns: int
@@ -448,6 +423,7 @@ def call_sync(
448423
# Note: This approach might not be suitable in all contexts, especially with nested async calls
449424
future = asyncio.ensure_future(
450425
self(
426+
scenario=scenario,
451427
max_conversation_turns=max_conversation_turns,
452428
max_simulation_results=max_simulation_results,
453429
target=target,
@@ -462,6 +438,7 @@ def call_sync(
462438
# If no event loop is running, use asyncio.run (Python 3.7+)
463439
return asyncio.run(
464440
self(
441+
scenario=scenario,
465442
max_conversation_turns=max_conversation_turns,
466443
max_simulation_results=max_simulation_results,
467444
target=target,
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._simulator_data_classes import ConversationHistory, Turn
12
from ._language_suffix_mapping import SUPPORTED_LANGUAGES_MAPPING
23

3-
__all__ = ["SUPPORTED_LANGUAGES_MAPPING"]
4+
__all__ = ["ConversationHistory", "Turn", "SUPPORTED_LANGUAGES_MAPPING"]
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
# pylint: disable=C0103,C0114,C0116
5+
from dataclasses import dataclass
6+
from typing import Union
7+
8+
from azure.ai.evaluation.simulator._conversation.constants import ConversationRole
9+
10+
11+
@dataclass
12+
class Turn:
13+
"""
14+
Represents a conversation turn,
15+
keeping track of the role, content,
16+
and context of a turn in a conversation.
17+
"""
18+
19+
role: Union[str, ConversationRole]
20+
content: str
21+
context: str = None
22+
23+
def to_dict(self):
24+
"""
25+
Convert the conversation turn to a dictionary.
26+
27+
Returns:
28+
dict: A dictionary representation of the conversation turn.
29+
"""
30+
return {
31+
"role": self.role.value if isinstance(self.role, ConversationRole) else self.role,
32+
"content": self.content,
33+
"context": self.context,
34+
}
35+
36+
def __repr__(self):
37+
"""
38+
Return the string representation of the conversation turn.
39+
40+
Returns:
41+
str: A string representation of the conversation turn.
42+
"""
43+
return f"Turn(role={self.role}, content={self.content})"
44+
45+
46+
class ConversationHistory:
47+
"""
48+
Conversation history class to keep track of the conversation turns in a conversation.
49+
"""
50+
51+
def __init__(self):
52+
"""
53+
Initializes the conversation history with an empty list of turns.
54+
"""
55+
self.history = []
56+
57+
def add_to_history(self, turn: Turn):
58+
"""
59+
Adds a turn to the conversation history.
60+
61+
Args:
62+
turn (Turn): The conversation turn to add.
63+
"""
64+
self.history.append(turn)
65+
66+
def to_list(self):
67+
"""
68+
Converts the conversation history to a list of dictionaries.
69+
70+
Returns:
71+
list: A list of dictionaries representing the conversation turns.
72+
"""
73+
return [turn.to_dict() for turn in self.history]
74+
75+
def get_length(self):
76+
"""
77+
Returns the length of the conversation.
78+
79+
Returns:
80+
int: The number of turns in the conversation history.
81+
"""
82+
return len(self.history)
83+
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+
"""
91+
for turn in self.history:
92+
print(turn)
93+
return ""

0 commit comments

Comments
 (0)