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,579 @@
1
+ # flake8: noqa
2
+ # pylint: disable=W0102,W0613,R0914,C0301,E0401,E0611
3
+ # ---------------------------------------------------------
4
+ # Copyright (c) Microsoft Corporation. All rights reserved.
5
+ # ---------------------------------------------------------
6
+ import re
7
+ import asyncio
8
+ import json
9
+ import os
10
+ from typing import Any, Dict, List, Optional
11
+ import warnings
12
+
13
+ from tqdm import tqdm
14
+
15
+ from promptflow.client import load_flow
16
+ from promptflow.core import AzureOpenAIModelConfiguration
17
+
18
+ from .._user_agent import USER_AGENT
19
+ from ._conversation.constants import ConversationRole
20
+ from ._helpers import ConversationHistory, Turn
21
+ # from ._tracing import monitor_task_simulator
22
+ from ._utils import JsonLineChatProtocol
23
+
24
+
25
+ class Simulator:
26
+ """
27
+ Simulator for generating synthetic conversations.
28
+ """
29
+
30
+ def __init__(self, azure_ai_project: Dict[str, Any], credential: Optional[Any] = None):
31
+ """
32
+ Initializes the task simulator with a project scope.
33
+
34
+ :param azure_ai_project: A dictionary defining the scope of the project, including keys such as
35
+ "subscription_id", "resource_group_name", and "project_name".
36
+ :param credential: Azure credentials to authenticate the user. If None, the default credentials are used.
37
+ :paramtype credential: Optional[Any]
38
+ :raises ValueError: If the azure_ai_project does not contain the required keys or any value is None.
39
+ """
40
+ self._validate_project_config(azure_ai_project)
41
+ self.azure_ai_project = azure_ai_project
42
+ self.azure_ai_project["api_version"] = "2024-02-15-preview"
43
+ self.credential = credential
44
+
45
+ @staticmethod
46
+ def _validate_project_config(azure_ai_project: Dict[str, Any]):
47
+ """
48
+ Validates the azure_ai_project configuration to ensure all required keys are present and have non-None values.
49
+
50
+ :param azure_ai_project: The Azure AI project configuration dictionary.
51
+ :raises ValueError: If required keys are missing or any of the values are None.
52
+ """
53
+ required_keys = ["subscription_id", "resource_group_name", "project_name"]
54
+ if not all(key in azure_ai_project for key in required_keys):
55
+ raise ValueError(f"azure_ai_project must contain keys: {', '.join(required_keys)}")
56
+ if not all(azure_ai_project[key] for key in required_keys):
57
+ raise ValueError("subscription_id, resource_group_name, and project_name must not be None")
58
+
59
+ # @monitor_task_simulator
60
+ async def __call__(
61
+ self,
62
+ *,
63
+ target: callable,
64
+ max_conversation_turns: int = 5,
65
+ tasks: List[Dict] = [],
66
+ text: str = "",
67
+ num_queries: int = 5,
68
+ query_response_generating_prompty: Optional[str] = None,
69
+ user_simulator_prompty: Optional[str] = None,
70
+ api_call_delay_sec: float = 1,
71
+ query_response_generating_prompty_kwargs: Dict[str, Any] = {},
72
+ user_simulator_prompty_kwargs: Dict[str, Any] = {},
73
+ conversation_turns: List[List[str]] = [],
74
+ **kwargs,
75
+ ) -> List[JsonLineChatProtocol]:
76
+ """
77
+ Generates synthetic conversations based on provided parameters.
78
+
79
+ :keyword target: The target function to call during the simulation.
80
+ :paramtype target: callable
81
+ :keyword max_conversation_turns: Maximum number of conversation turns for the simulation. Each turn consists of a user and an assistant message.
82
+ :paramtype max_conversation_turns: int
83
+ :keyword tasks: A list of user tasks, each represented as a list of strings. Text should be relevant for the tasks and facilitate the simulation. One example is to use text to provide context for the tasks.
84
+ :paramtype tasks: List[str]
85
+ :keyword text: The initial input text for generating query responses. Given that the same 'text' is provided for a list of tasks, one example use is to break down a user task into sub-tasks that can share the 'text' variable for context.
86
+ :paramtype text: str
87
+ :keyword num_queries: The number of queries to generate.
88
+ :paramtype num_queries: int
89
+ :keyword query_response_generating_prompty: Path to the query response generating prompty file.
90
+ :paramtype query_response_generating_prompty: Optional[str]
91
+ :keyword user_simulator_prompty: Path to the user simulator prompty file.
92
+ :paramtype user_simulator_prompty: Optional[str]
93
+ :keyword api_call_delay_sec: Delay in seconds between API calls.
94
+ :paramtype api_call_delay_sec: float
95
+ :keyword query_response_generating_prompty_kwargs: Additional keyword arguments for the query response generating prompty.
96
+ :paramtype query_response_generating_prompty_kwargs: Dict[str, Any]
97
+ :keyword user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
98
+ :paramtype user_simulator_prompty_kwargs: Dict[str, Any]
99
+ :keyword conversation_turns: Predefined conversation turns to simulate.
100
+ :paramtype conversation_turns: List[List[str]]
101
+ :return: A list of simulated conversations represented as JsonLineChatProtocol objects.
102
+ :rtype: List[JsonLineChatProtocol]
103
+
104
+ Return Value:
105
+ The method returns a list of JsonLineChatProtocol objects, which are essentially a list of dictionaries where the dictionary contains the messages and context. Context includes all the metadata related to the conversation, such as the task, expected response, and query. The messages contain the conversation history, including the user and assistant messages.
106
+
107
+ Modes:
108
+ - Task-Free Mode: When only num_queries is specified and tasks is not, the method generates num_queries x max_conversation_turns lines of simulated data grounded in the context of the text.
109
+ - Task-Specific Mode: When both num_queries and tasks are specified, the method generates lines of simulated data based on the tasks. If num_queries > len(tasks), the remaining lines are simulated in task-free mode. If num_queries < len(tasks), only the first num_queries tasks are used.
110
+ - Conversation Starter Mode: When conversation_turns are specified, the method starts each conversation with the user-specified queries and then follows the conversation history for the remaining turns.
111
+ """
112
+ if conversation_turns and (text or tasks):
113
+ raise ValueError("Cannot specify both conversation_turns and text/tasks")
114
+
115
+ if num_queries > len(tasks):
116
+ warnings.warn(
117
+ f"You have specified 'num_queries' > len('tasks') ({num_queries} > {len(tasks)}). "
118
+ f"All tasks will be used for generation and the remaining {num_queries - len(tasks)} lines will be simulated in task-free mode"
119
+ )
120
+ elif num_queries < len(tasks):
121
+ warnings.warn(
122
+ f"You have specified 'num_queries' < len('tasks') ({num_queries} < {len(tasks)}). "
123
+ f"Only the first {num_queries} lines of the specified tasks will be simulated."
124
+ )
125
+ num_queries = min(num_queries, len(tasks))
126
+ max_conversation_turns *= 2 # account for both user and assistant turns
127
+
128
+ prompty_model_config = self._build_prompty_model_config()
129
+
130
+ if conversation_turns:
131
+ return await self._simulate_with_predefined_turns(
132
+ target=target,
133
+ max_conversation_turns=max_conversation_turns,
134
+ conversation_turns=conversation_turns,
135
+ user_simulator_prompty=user_simulator_prompty,
136
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
137
+ api_call_delay_sec=api_call_delay_sec,
138
+ prompty_model_config=prompty_model_config,
139
+ )
140
+
141
+ query_responses = await self._generate_query_responses(
142
+ text=text,
143
+ num_queries=num_queries,
144
+ query_response_generating_prompty=query_response_generating_prompty,
145
+ query_response_generating_prompty_kwargs=query_response_generating_prompty_kwargs,
146
+ prompty_model_config=prompty_model_config,
147
+ **kwargs,
148
+ )
149
+
150
+ return await self._create_conversations_from_query_responses(
151
+ query_responses=query_responses,
152
+ max_conversation_turns=max_conversation_turns,
153
+ tasks=tasks,
154
+ user_simulator_prompty=user_simulator_prompty,
155
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
156
+ target=target,
157
+ api_call_delay_sec=api_call_delay_sec,
158
+ )
159
+
160
+ def _build_prompty_model_config(self) -> Dict[str, Any]:
161
+ """
162
+ Constructs the configuration for the prompty model.
163
+
164
+ :return: A dictionary containing the prompty model configuration, including API version and user agent headers if applicable.
165
+ :rtype: Dict[str, Any]
166
+ """
167
+ config = {"configuration": self.azure_ai_project}
168
+ if USER_AGENT and isinstance(self.azure_ai_project, AzureOpenAIModelConfiguration):
169
+ config.update({"parameters": {"extra_headers": {"x-ms-useragent": USER_AGENT}}})
170
+ return config
171
+
172
+ async def _simulate_with_predefined_turns(
173
+ self,
174
+ *,
175
+ target: callable,
176
+ max_conversation_turns: int,
177
+ conversation_turns: List[List[str]],
178
+ user_simulator_prompty: Optional[str],
179
+ user_simulator_prompty_kwargs: Dict[str, Any],
180
+ api_call_delay_sec: float,
181
+ prompty_model_config: Dict[str, Any],
182
+ ) -> List[JsonLineChatProtocol]:
183
+ """
184
+ Simulates conversations using predefined conversation turns.
185
+
186
+ :param target: The target function to call during each turn of the simulation.
187
+ :param max_conversation_turns: Maximum number of turns for the simulation.
188
+ :param conversation_turns: A list of predefined conversation turns.
189
+ :param user_simulator_prompty: Path to the user simulator prompty file.
190
+ :param user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
191
+ :param api_call_delay_sec: Delay in seconds between API calls.
192
+ :param prompty_model_config: The configuration for the prompty model.
193
+ :return: A list of simulated conversations represented as JsonLineChatProtocol objects.
194
+ :rtype: List[JsonLineChatProtocol]
195
+ """
196
+ simulated_conversations = []
197
+ progress_bar = tqdm(
198
+ total=int(len(conversation_turns) * (max_conversation_turns/2)),
199
+ desc="Simulating with predefined conversation turns: ",
200
+ ncols=100,
201
+ unit="messages",
202
+ )
203
+
204
+ for simulation in conversation_turns:
205
+ current_simulation = ConversationHistory()
206
+ for simulated_turn in simulation:
207
+ user_turn = Turn(role=ConversationRole.USER, content=simulated_turn)
208
+ current_simulation.add_to_history(user_turn)
209
+ assistant_response = await self._get_target_response(
210
+ target=target, api_call_delay_sec=api_call_delay_sec, conversation_history=current_simulation
211
+ )
212
+ assistant_turn = Turn(role=ConversationRole.ASSISTANT, content=assistant_response)
213
+ current_simulation.add_to_history(assistant_turn)
214
+ progress_bar.update(1) # Update progress bar for both user and assistant turns
215
+
216
+ if current_simulation.get_length() < max_conversation_turns:
217
+ await self._extend_conversation_with_simulator(
218
+ current_simulation=current_simulation,
219
+ max_conversation_turns=max_conversation_turns,
220
+ user_simulator_prompty=user_simulator_prompty,
221
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
222
+ api_call_delay_sec=api_call_delay_sec,
223
+ prompty_model_config=prompty_model_config,
224
+ target=target,
225
+ progress_bar=progress_bar,
226
+ )
227
+
228
+ simulated_conversations.append(current_simulation.to_list())
229
+
230
+ progress_bar.close()
231
+ return simulated_conversations
232
+
233
+ async def _extend_conversation_with_simulator(
234
+ self,
235
+ *,
236
+ current_simulation: ConversationHistory,
237
+ max_conversation_turns: int,
238
+ user_simulator_prompty: Optional[str],
239
+ user_simulator_prompty_kwargs: Dict[str, Any],
240
+ api_call_delay_sec: float,
241
+ prompty_model_config: Dict[str, Any],
242
+ target: callable,
243
+ progress_bar: tqdm,
244
+ ):
245
+ """
246
+ Extends an ongoing conversation using a user simulator until the maximum number of turns is reached.
247
+
248
+ :param current_simulation: The current state of the conversation history.
249
+ :param max_conversation_turns: The maximum number of conversation turns.
250
+ :param user_simulator_prompty: Path to the user simulator prompty file.
251
+ :param user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
252
+ :param api_call_delay_sec: Delay in seconds between API calls.
253
+ :param prompty_model_config: The configuration for the prompty model.
254
+ :param target: The target function to call for responses.
255
+ :param progress_bar: Progress bar for tracking simulation progress.
256
+ """
257
+ user_flow = self._load_user_simulation_flow(
258
+ user_simulator_prompty=user_simulator_prompty,
259
+ prompty_model_config=prompty_model_config,
260
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
261
+ )
262
+
263
+ while current_simulation.get_length() < max_conversation_turns:
264
+ user_response_content = user_flow(
265
+ task="Continue the conversation", conversation_history=current_simulation.to_list()
266
+ )
267
+ user_response = self._parse_prompty_response(response=user_response_content)
268
+ user_turn = Turn(role=ConversationRole.USER, content=user_response["content"])
269
+ current_simulation.add_to_history(user_turn)
270
+ await asyncio.sleep(api_call_delay_sec)
271
+ assistant_response = await self._get_target_response(
272
+ target=target, api_call_delay_sec=api_call_delay_sec, conversation_history=current_simulation
273
+ )
274
+ assistant_turn = Turn(role=ConversationRole.ASSISTANT, content=assistant_response)
275
+ current_simulation.add_to_history(assistant_turn)
276
+ progress_bar.update(1)
277
+
278
+ def _load_user_simulation_flow(
279
+ self, *, user_simulator_prompty, prompty_model_config, user_simulator_prompty_kwargs
280
+ ):
281
+ """
282
+ Loads the flow for simulating user interactions.
283
+
284
+ :param user_simulator_prompty: Path to the user simulator prompty file.
285
+ :param prompty_model_config: The configuration for the prompty model.
286
+ :param user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
287
+ :return: The loaded flow for simulating user interactions.
288
+ """
289
+ if not user_simulator_prompty:
290
+ current_dir = os.path.dirname(__file__)
291
+ prompty_path = os.path.join(current_dir, "_prompty", "task_simulate.prompty")
292
+ return load_flow(source=prompty_path, model=prompty_model_config)
293
+ return load_flow(
294
+ source=user_simulator_prompty,
295
+ model=prompty_model_config,
296
+ **user_simulator_prompty_kwargs,
297
+ )
298
+
299
+ def _parse_prompty_response(self, *, response: str) -> Dict[str, Any]:
300
+ """
301
+ Parses the response from the prompty execution.
302
+
303
+ :param response: The raw response from the prompty.
304
+ :return: A dictionary representing the parsed response content.
305
+ :rtype: Dict[str, Any]
306
+ :raises ValueError: If the response cannot be parsed.
307
+ """
308
+ try:
309
+ if type(response) == str:
310
+ response = response.replace('\u2019', "'").replace('\u2018', "'")
311
+ response = response.replace('\u201C', '"').replace('\u201D', '"')
312
+
313
+ # Replace None with null
314
+ response = response.replace('None', 'null')
315
+
316
+ # Escape unescaped single quotes inside string values
317
+ def escape_single_quotes(match):
318
+ s = match.group(0)
319
+ # Remove the outer single quotes
320
+ s_content = s[1:-1]
321
+ # Escape single quotes within the content
322
+ s_content_escaped = s_content.replace("'", "\\'")
323
+ return f"'{s_content_escaped}'"
324
+
325
+ # Pattern to match single-quoted strings
326
+ pattern = r"'(.*?)'"
327
+ response = re.sub(pattern, escape_single_quotes, response)
328
+
329
+ # Now replace single quotes around keys and values with double quotes
330
+ response = re.sub(r"'([^']+)'", r'"\1"', response)
331
+ parsed_data = json.loads(response)
332
+ return parsed_data
333
+ return response
334
+ except Exception as e:
335
+ raise ValueError("Error parsing response content") from e
336
+
337
+ async def _generate_query_responses(
338
+ self,
339
+ *,
340
+ text: str,
341
+ num_queries: int,
342
+ query_response_generating_prompty: Optional[str],
343
+ query_response_generating_prompty_kwargs: Dict[str, Any],
344
+ prompty_model_config: Dict[str, Any],
345
+ **kwargs,
346
+ ) -> List[Dict[str, str]]:
347
+ """
348
+ Generates query responses using the specified prompty configuration.
349
+
350
+ :param text: The input text for generating queries.
351
+ :param num_queries: The number of queries to generate.
352
+ :param query_response_generating_prompty: Path to the query response generating prompty file.
353
+ :param query_response_generating_prompty_kwargs: Additional keyword arguments for the query response generating prompty.
354
+ :param prompty_model_config: The configuration for the prompty model.
355
+ :return: A list of query-response dictionaries.
356
+ :rtype: List[Dict[str, str]]
357
+ :raises RuntimeError: If an error occurs during query generation.
358
+ """
359
+ query_flow = self._load_query_generation_flow(
360
+ query_response_generating_prompty=query_response_generating_prompty,
361
+ prompty_model_config=prompty_model_config,
362
+ query_response_generating_prompty_kwargs=query_response_generating_prompty_kwargs,
363
+ )
364
+
365
+ try:
366
+ query_responses = query_flow(text=text, num_queries=num_queries)
367
+ if type(query_responses) == dict:
368
+ keys = list(query_responses.keys())
369
+ return query_responses[keys[0]]
370
+ return json.loads(query_responses)
371
+ except Exception as e:
372
+ raise RuntimeError("Error generating query responses") from e
373
+
374
+ def _load_query_generation_flow(
375
+ self, *, query_response_generating_prompty, prompty_model_config, query_response_generating_prompty_kwargs
376
+ ):
377
+ """
378
+ Loads the flow for generating query responses.
379
+
380
+ :param query_response_generating_prompty: Path to the query response generating prompty file.
381
+ :param prompty_model_config: The configuration for the prompty model.
382
+ :param query_response_generating_prompty_kwargs: Additional keyword arguments for the flow.
383
+ :return: The loaded flow for generating query responses.
384
+ """
385
+ if not query_response_generating_prompty:
386
+ current_dir = os.path.dirname(__file__)
387
+ prompty_path = os.path.join(current_dir, "_prompty", "task_query_response.prompty")
388
+ return load_flow(source=prompty_path, model=prompty_model_config)
389
+ return load_flow(
390
+ source=query_response_generating_prompty,
391
+ model=prompty_model_config,
392
+ **query_response_generating_prompty_kwargs,
393
+ )
394
+
395
+ async def _create_conversations_from_query_responses(
396
+ self,
397
+ *,
398
+ query_responses: List[Dict[str, str]],
399
+ max_conversation_turns: int,
400
+ tasks: List[Dict],
401
+ user_simulator_prompty: Optional[str],
402
+ user_simulator_prompty_kwargs: Dict[str, Any],
403
+ target: callable,
404
+ api_call_delay_sec: float,
405
+ ) -> List[JsonLineChatProtocol]:
406
+ """
407
+ Creates full conversations from query-response pairs.
408
+
409
+ :param query_responses: A list of query-response pairs.
410
+ :param max_conversation_turns: The maximum number of conversation turns.
411
+ :param tasks: A list of tasks for the simulation.
412
+ :param user_simulator_prompty: Path to the user simulator prompty file.
413
+ :param user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
414
+ :param target: The target function to call for responses.
415
+ :param api_call_delay_sec: Delay in seconds between API calls.
416
+ :return: A list of simulated conversations represented as JsonLineChatProtocol objects.
417
+ :rtype: List[JsonLineChatProtocol]
418
+ """
419
+ total_turns = len(query_responses) * max_conversation_turns
420
+
421
+ progress_bar = tqdm(
422
+ total=int(total_turns/2),
423
+ desc="Generating: ",
424
+ ncols=100,
425
+ unit="message",
426
+ )
427
+ all_conversations = []
428
+
429
+ for i, query_response_pair in enumerate(query_responses):
430
+ query = query_response_pair["q"]
431
+ response = query_response_pair["r"]
432
+ task = tasks[i]
433
+
434
+ conversation = await self._complete_conversation(
435
+ conversation_starter=query,
436
+ max_conversation_turns=max_conversation_turns,
437
+ task=task,
438
+ user_simulator_prompty=user_simulator_prompty,
439
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
440
+ target=target,
441
+ api_call_delay_sec=api_call_delay_sec,
442
+ progress_bar=progress_bar,
443
+ )
444
+ all_conversations.append(
445
+ JsonLineChatProtocol(
446
+ {
447
+ "messages": conversation,
448
+ "finish_reason": ["stop"],
449
+ "context": {
450
+ "task": task,
451
+ "expected_response": response,
452
+ "query": query,
453
+ },
454
+ "$schema": "http://azureml/sdk-2-0/ChatConversation.json",
455
+ }
456
+ )
457
+ )
458
+ progress_bar.close()
459
+ return all_conversations
460
+
461
+ async def _complete_conversation(
462
+ self,
463
+ *,
464
+ conversation_starter: str,
465
+ max_conversation_turns: int,
466
+ task: str,
467
+ user_simulator_prompty: Optional[str],
468
+ user_simulator_prompty_kwargs: Dict[str, Any],
469
+ target: callable,
470
+ api_call_delay_sec: float,
471
+ progress_bar: tqdm,
472
+ ) -> List[Dict[str, str]]:
473
+ """
474
+ Completes a conversation with the target model based on the conversation starter.
475
+
476
+ :keyword conversation_starter: The initial message to start the conversation.
477
+ :paramtype conversation_starter: str
478
+ :keyword max_conversation_turns: The maximum number of turns in the conversation.
479
+ :paramtype max_conversation_turns: int
480
+ :keyword task: A string representing the task details.
481
+ :paramtype task: str
482
+ :keyword user_simulator_prompty: Path to the user simulator prompty file.
483
+ :paramtype user_simulator_prompty: Optional[str]
484
+ :keyword user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
485
+ :paramtype user_simulator_prompty_kwargs: Dict[str, Any]
486
+ :keyword target: The target function to call for responses.
487
+ :paramtype target: callable
488
+ :keyword api_call_delay_sec: Delay in seconds between API calls.
489
+ :paramtype api_call_delay_sec: float
490
+ :keyword progress_bar: Progress bar for tracking simulation progress.
491
+ :paramtype progress_bar: tqdm
492
+ :return: A list representing the conversation history with each turn's content.
493
+ :rtype: List[Dict[str, str]]
494
+ """
495
+ conversation_history = ConversationHistory()
496
+ # user_turn = Turn(role=ConversationRole.USER, content=conversation_starter)
497
+ # conversation_history.add_to_history(user_turn)
498
+
499
+ while conversation_history.get_length() < max_conversation_turns:
500
+ user_flow = self._load_user_simulation_flow(
501
+ user_simulator_prompty=user_simulator_prompty,
502
+ prompty_model_config=self._build_prompty_model_config(),
503
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
504
+ )
505
+ conversation_starter_from_simulated_user = user_flow(
506
+ task=task, conversation_history=[{
507
+ "role": "assistant",
508
+ "content": conversation_starter,
509
+ "your_task": "Act as the user and translate the content into a user query."
510
+ }]
511
+ )
512
+ if type(conversation_starter_from_simulated_user) == dict:
513
+ conversation_starter_from_simulated_user = conversation_starter_from_simulated_user["content"]
514
+ user_turn = Turn(role=ConversationRole.USER, content=conversation_starter_from_simulated_user)
515
+ conversation_history.add_to_history(user_turn)
516
+ assistant_response = await self._get_target_response(
517
+ target=target, api_call_delay_sec=api_call_delay_sec, conversation_history=conversation_history
518
+ )
519
+ assistant_turn = Turn(role=ConversationRole.ASSISTANT, content=assistant_response)
520
+ conversation_history.add_to_history(assistant_turn)
521
+ progress_bar.update(1)
522
+
523
+ if conversation_history.get_length() >= max_conversation_turns:
524
+ break
525
+
526
+ return conversation_history.to_list()
527
+
528
+ async def _build_user_simulation_response(
529
+ self,
530
+ task: str,
531
+ conversation_history: List[Dict[str, Any]],
532
+ user_simulator_prompty: Optional[str],
533
+ user_simulator_prompty_kwargs: Dict[str, Any],
534
+ ) -> str:
535
+ """
536
+ Builds a response from the user simulator based on the current conversation history.
537
+
538
+ :param task: A string representing the task details.
539
+ :param conversation_history: The current conversation history as a list of dictionaries.
540
+ :param user_simulator_prompty: Path to the user simulator prompty file.
541
+ :param user_simulator_prompty_kwargs: Additional keyword arguments for the user simulator prompty.
542
+ :return: The generated response content from the user simulator.
543
+ :rtype: str
544
+ :raises RuntimeError: If an error occurs during response generation.
545
+ """
546
+ user_flow = self._load_user_simulation_flow(
547
+ user_simulator_prompty=user_simulator_prompty,
548
+ prompty_model_config=self._build_prompty_model_config(),
549
+ user_simulator_prompty_kwargs=user_simulator_prompty_kwargs,
550
+ )
551
+
552
+ try:
553
+ response_content = user_flow(task=task, conversation_history=conversation_history)
554
+ user_response = self._parse_prompty_response(response=response_content)
555
+ return user_response["content"]
556
+ except Exception as e:
557
+ raise RuntimeError("Error building user simulation response") from e
558
+
559
+ async def _get_target_response(
560
+ self, *, target: callable, api_call_delay_sec: float, conversation_history: ConversationHistory
561
+ ) -> str:
562
+ """
563
+ Retrieves the response from the target callback based on the current conversation history.
564
+
565
+ :param target: The target function to call for a response.
566
+ :param api_call_delay_sec: Delay in seconds before retrieving the response.
567
+ :param conversation_history: The current conversation history.
568
+ :return: The content of the response from the target.
569
+ :rtype: str
570
+ """
571
+ response = await target(
572
+ messages={"messages": conversation_history.to_list()},
573
+ stream=False,
574
+ session_state=None,
575
+ context=None,
576
+ )
577
+ await asyncio.sleep(api_call_delay_sec)
578
+ latest_message = response["messages"][-1]
579
+ return latest_message["content"]