azure-ai-evaluation 1.2.0__py3-none-any.whl → 1.4.0__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 (134) hide show
  1. azure/ai/evaluation/__init__.py +42 -14
  2. azure/ai/evaluation/_azure/_models.py +6 -6
  3. azure/ai/evaluation/_common/constants.py +6 -2
  4. azure/ai/evaluation/_common/rai_service.py +38 -4
  5. azure/ai/evaluation/_common/raiclient/__init__.py +34 -0
  6. azure/ai/evaluation/_common/raiclient/_client.py +128 -0
  7. azure/ai/evaluation/_common/raiclient/_configuration.py +87 -0
  8. azure/ai/evaluation/_common/raiclient/_model_base.py +1235 -0
  9. azure/ai/evaluation/_common/raiclient/_patch.py +20 -0
  10. azure/ai/evaluation/_common/raiclient/_serialization.py +2050 -0
  11. azure/ai/evaluation/_common/raiclient/_version.py +9 -0
  12. azure/ai/evaluation/_common/raiclient/aio/__init__.py +29 -0
  13. azure/ai/evaluation/_common/raiclient/aio/_client.py +130 -0
  14. azure/ai/evaluation/_common/raiclient/aio/_configuration.py +87 -0
  15. azure/ai/evaluation/_common/raiclient/aio/_patch.py +20 -0
  16. azure/ai/evaluation/_common/raiclient/aio/operations/__init__.py +25 -0
  17. azure/ai/evaluation/_common/raiclient/aio/operations/_operations.py +981 -0
  18. azure/ai/evaluation/_common/raiclient/aio/operations/_patch.py +20 -0
  19. azure/ai/evaluation/_common/raiclient/models/__init__.py +60 -0
  20. azure/ai/evaluation/_common/raiclient/models/_enums.py +18 -0
  21. azure/ai/evaluation/_common/raiclient/models/_models.py +651 -0
  22. azure/ai/evaluation/_common/raiclient/models/_patch.py +20 -0
  23. azure/ai/evaluation/_common/raiclient/operations/__init__.py +25 -0
  24. azure/ai/evaluation/_common/raiclient/operations/_operations.py +1225 -0
  25. azure/ai/evaluation/_common/raiclient/operations/_patch.py +20 -0
  26. azure/ai/evaluation/_common/raiclient/py.typed +1 -0
  27. azure/ai/evaluation/_common/utils.py +30 -10
  28. azure/ai/evaluation/_constants.py +10 -0
  29. azure/ai/evaluation/_converters/__init__.py +3 -0
  30. azure/ai/evaluation/_converters/_ai_services.py +804 -0
  31. azure/ai/evaluation/_converters/_models.py +302 -0
  32. azure/ai/evaluation/_evaluate/_batch_run/__init__.py +10 -3
  33. azure/ai/evaluation/_evaluate/_batch_run/_run_submitter_client.py +104 -0
  34. azure/ai/evaluation/_evaluate/_batch_run/batch_clients.py +82 -0
  35. azure/ai/evaluation/_evaluate/_eval_run.py +1 -1
  36. azure/ai/evaluation/_evaluate/_evaluate.py +36 -4
  37. azure/ai/evaluation/_evaluators/_bleu/_bleu.py +23 -3
  38. azure/ai/evaluation/_evaluators/_code_vulnerability/__init__.py +5 -0
  39. azure/ai/evaluation/_evaluators/_code_vulnerability/_code_vulnerability.py +120 -0
  40. azure/ai/evaluation/_evaluators/_coherence/_coherence.py +21 -2
  41. azure/ai/evaluation/_evaluators/_common/_base_eval.py +43 -3
  42. azure/ai/evaluation/_evaluators/_common/_base_multi_eval.py +3 -1
  43. azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +43 -4
  44. azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +16 -4
  45. azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +42 -5
  46. azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +15 -0
  47. azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +15 -0
  48. azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +15 -0
  49. azure/ai/evaluation/_evaluators/_content_safety/_violence.py +15 -0
  50. azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +28 -4
  51. azure/ai/evaluation/_evaluators/_fluency/_fluency.py +21 -2
  52. azure/ai/evaluation/_evaluators/_gleu/_gleu.py +26 -3
  53. azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +21 -3
  54. azure/ai/evaluation/_evaluators/_intent_resolution/__init__.py +7 -0
  55. azure/ai/evaluation/_evaluators/_intent_resolution/_intent_resolution.py +152 -0
  56. azure/ai/evaluation/_evaluators/_intent_resolution/intent_resolution.prompty +161 -0
  57. azure/ai/evaluation/_evaluators/_meteor/_meteor.py +26 -3
  58. azure/ai/evaluation/_evaluators/_qa/_qa.py +51 -7
  59. azure/ai/evaluation/_evaluators/_relevance/_relevance.py +26 -2
  60. azure/ai/evaluation/_evaluators/_response_completeness/__init__.py +7 -0
  61. azure/ai/evaluation/_evaluators/_response_completeness/_response_completeness.py +157 -0
  62. azure/ai/evaluation/_evaluators/_response_completeness/response_completeness.prompty +99 -0
  63. azure/ai/evaluation/_evaluators/_retrieval/_retrieval.py +21 -2
  64. azure/ai/evaluation/_evaluators/_rouge/_rouge.py +113 -4
  65. azure/ai/evaluation/_evaluators/_service_groundedness/_service_groundedness.py +23 -3
  66. azure/ai/evaluation/_evaluators/_similarity/_similarity.py +24 -5
  67. azure/ai/evaluation/_evaluators/_task_adherence/__init__.py +7 -0
  68. azure/ai/evaluation/_evaluators/_task_adherence/_task_adherence.py +148 -0
  69. azure/ai/evaluation/_evaluators/_task_adherence/task_adherence.prompty +117 -0
  70. azure/ai/evaluation/_evaluators/_tool_call_accuracy/__init__.py +9 -0
  71. azure/ai/evaluation/_evaluators/_tool_call_accuracy/_tool_call_accuracy.py +292 -0
  72. azure/ai/evaluation/_evaluators/_tool_call_accuracy/tool_call_accuracy.prompty +71 -0
  73. azure/ai/evaluation/_evaluators/_ungrounded_attributes/__init__.py +5 -0
  74. azure/ai/evaluation/_evaluators/_ungrounded_attributes/_ungrounded_attributes.py +103 -0
  75. azure/ai/evaluation/_evaluators/_xpia/xpia.py +2 -0
  76. azure/ai/evaluation/_exceptions.py +5 -1
  77. azure/ai/evaluation/_legacy/__init__.py +3 -0
  78. azure/ai/evaluation/_legacy/_batch_engine/__init__.py +9 -0
  79. azure/ai/evaluation/_legacy/_batch_engine/_config.py +45 -0
  80. azure/ai/evaluation/_legacy/_batch_engine/_engine.py +368 -0
  81. azure/ai/evaluation/_legacy/_batch_engine/_exceptions.py +88 -0
  82. azure/ai/evaluation/_legacy/_batch_engine/_logging.py +292 -0
  83. azure/ai/evaluation/_legacy/_batch_engine/_openai_injector.py +23 -0
  84. azure/ai/evaluation/_legacy/_batch_engine/_result.py +99 -0
  85. azure/ai/evaluation/_legacy/_batch_engine/_run.py +121 -0
  86. azure/ai/evaluation/_legacy/_batch_engine/_run_storage.py +128 -0
  87. azure/ai/evaluation/_legacy/_batch_engine/_run_submitter.py +217 -0
  88. azure/ai/evaluation/_legacy/_batch_engine/_status.py +25 -0
  89. azure/ai/evaluation/_legacy/_batch_engine/_trace.py +105 -0
  90. azure/ai/evaluation/_legacy/_batch_engine/_utils.py +82 -0
  91. azure/ai/evaluation/_legacy/_batch_engine/_utils_deprecated.py +131 -0
  92. azure/ai/evaluation/_legacy/prompty/__init__.py +36 -0
  93. azure/ai/evaluation/_legacy/prompty/_connection.py +182 -0
  94. azure/ai/evaluation/_legacy/prompty/_exceptions.py +59 -0
  95. azure/ai/evaluation/_legacy/prompty/_prompty.py +313 -0
  96. azure/ai/evaluation/_legacy/prompty/_utils.py +545 -0
  97. azure/ai/evaluation/_legacy/prompty/_yaml_utils.py +99 -0
  98. azure/ai/evaluation/_red_team/__init__.py +3 -0
  99. azure/ai/evaluation/_red_team/_attack_objective_generator.py +192 -0
  100. azure/ai/evaluation/_red_team/_attack_strategy.py +42 -0
  101. azure/ai/evaluation/_red_team/_callback_chat_target.py +74 -0
  102. azure/ai/evaluation/_red_team/_default_converter.py +21 -0
  103. azure/ai/evaluation/_red_team/_red_team.py +1858 -0
  104. azure/ai/evaluation/_red_team/_red_team_result.py +246 -0
  105. azure/ai/evaluation/_red_team/_utils/__init__.py +3 -0
  106. azure/ai/evaluation/_red_team/_utils/constants.py +64 -0
  107. azure/ai/evaluation/_red_team/_utils/formatting_utils.py +164 -0
  108. azure/ai/evaluation/_red_team/_utils/logging_utils.py +139 -0
  109. azure/ai/evaluation/_red_team/_utils/strategy_utils.py +188 -0
  110. azure/ai/evaluation/_safety_evaluation/__init__.py +3 -0
  111. azure/ai/evaluation/_safety_evaluation/_generated_rai_client.py +0 -0
  112. azure/ai/evaluation/_safety_evaluation/_safety_evaluation.py +741 -0
  113. azure/ai/evaluation/_version.py +2 -1
  114. azure/ai/evaluation/simulator/_adversarial_scenario.py +3 -1
  115. azure/ai/evaluation/simulator/_adversarial_simulator.py +61 -27
  116. azure/ai/evaluation/simulator/_conversation/__init__.py +4 -5
  117. azure/ai/evaluation/simulator/_conversation/_conversation.py +4 -0
  118. azure/ai/evaluation/simulator/_model_tools/_generated_rai_client.py +145 -0
  119. azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +2 -0
  120. azure/ai/evaluation/simulator/_model_tools/_rai_client.py +71 -1
  121. {azure_ai_evaluation-1.2.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/METADATA +75 -15
  122. azure_ai_evaluation-1.4.0.dist-info/RECORD +197 -0
  123. {azure_ai_evaluation-1.2.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/WHEEL +1 -1
  124. azure/ai/evaluation/_evaluators/_multimodal/__init__.py +0 -20
  125. azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal.py +0 -132
  126. azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal_base.py +0 -55
  127. azure/ai/evaluation/_evaluators/_multimodal/_hate_unfairness.py +0 -100
  128. azure/ai/evaluation/_evaluators/_multimodal/_protected_material.py +0 -124
  129. azure/ai/evaluation/_evaluators/_multimodal/_self_harm.py +0 -100
  130. azure/ai/evaluation/_evaluators/_multimodal/_sexual.py +0 -100
  131. azure/ai/evaluation/_evaluators/_multimodal/_violence.py +0 -100
  132. azure_ai_evaluation-1.2.0.dist-info/RECORD +0 -125
  133. {azure_ai_evaluation-1.2.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/NOTICE.txt +0 -0
  134. {azure_ai_evaluation-1.2.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,36 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ from azure.ai.evaluation._legacy.prompty._prompty import AsyncPrompty
6
+ from azure.ai.evaluation._legacy.prompty._connection import Connection, OpenAIConnection, AzureOpenAIConnection
7
+ from azure.ai.evaluation._legacy.prompty._exceptions import (
8
+ PromptyException,
9
+ MissingRequiredInputError,
10
+ InvalidInputError,
11
+ JinjaTemplateError,
12
+ NotSupportedError,
13
+ )
14
+
15
+ # =========================================================================================================
16
+ # NOTE: All of the code here is largely copy of code from Promptflow. Generally speaking, the following
17
+ # changes were made:
18
+ # - Added type annotations
19
+ # - Legacy or deprecated functionality has been removed (e.g. no more support for completions API)
20
+ # - Reworked the way images are handled to 1) Reduce the amount of code brought over, 2) remove
21
+ # the need to do two passes over the template to insert images, 3) remove the completely unnecessary
22
+ # loading of image data from the internet when it is not actually needed
23
+ # - Minor obvious tweaks to improve code readability, and removal of unused code paths
24
+ # =========================================================================================================
25
+
26
+ __all__ = [
27
+ "AsyncPrompty",
28
+ "Connection",
29
+ "AzureOpenAIConnection",
30
+ "OpenAIConnection",
31
+ "PromptyException",
32
+ "MissingRequiredInputError",
33
+ "InvalidInputError",
34
+ "JinjaTemplateError",
35
+ "NotSupportedError",
36
+ ]
@@ -0,0 +1,182 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ import os
6
+ import re
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass
9
+ from typing import Any, ClassVar, Mapping, Optional, Set, Union
10
+
11
+ from azure.ai.evaluation._legacy.prompty._exceptions import MissingRequiredInputError
12
+ from azure.ai.evaluation._legacy.prompty._utils import dataclass_from_dict
13
+
14
+
15
+ ENV_VAR_PATTERN = re.compile(r"^\$\{env:(.*)\}$")
16
+
17
+
18
+ def _parse_environment_variable(value: Union[str, Any]) -> Union[str, Any]:
19
+ """Get environment variable from ${env:ENV_NAME}. If not found, return original value.
20
+
21
+ :param value: The value to parse.
22
+ :type value: str | Any
23
+ :return: The parsed value
24
+ :rtype: str | Any"""
25
+ if not isinstance(value, str):
26
+ return value
27
+
28
+ result = re.match(ENV_VAR_PATTERN, value)
29
+ if result:
30
+ env_name = result.groups()[0]
31
+ return os.environ.get(env_name, value)
32
+
33
+ return value
34
+
35
+
36
+ def _is_empty_connection_config(connection_dict: Mapping[str, Any]) -> bool:
37
+ ignored_fields = set(["azure_deployment", "model"])
38
+ keys = {k for k, v in connection_dict.items() if v}
39
+ return len(keys - ignored_fields) == 0
40
+
41
+
42
+ @dataclass
43
+ class Connection(ABC):
44
+ """Base class for all connection classes."""
45
+
46
+ @property
47
+ @abstractmethod
48
+ def type(self) -> str:
49
+ """Gets the type of the connection.
50
+
51
+ :return: The type of the connection.
52
+ :rtype: str"""
53
+ ...
54
+
55
+ @abstractmethod
56
+ def is_valid(self, missing_fields: Optional[Set[str]] = None) -> bool:
57
+ """Check if the connection is valid.
58
+
59
+ :param missing_fields: If set, this will be populated with the missing required fields.
60
+ :type missing_fields: Set[str] | None
61
+ :return: True if the connection is valid, False otherwise.
62
+ :rtype: bool"""
63
+ ...
64
+
65
+ @staticmethod
66
+ def parse_from_config(model_configuration: Mapping[str, Any]) -> "Connection":
67
+ """Parse a connection from a model configuration.
68
+
69
+ :param model_configuration: The model configuration.
70
+ :type model_configuration: Mapping[str, Any]
71
+ :return: The connection.
72
+ :rtype: Connection
73
+ """
74
+ connection: Connection
75
+ connection_dict = {k: _parse_environment_variable(v) for k, v in model_configuration.items()}
76
+ connection_type = connection_dict.pop("type", "")
77
+
78
+ if connection_type in [AzureOpenAIConnection.TYPE, "azure_openai"]:
79
+ if _is_empty_connection_config(connection_dict):
80
+ connection = AzureOpenAIConnection.from_env()
81
+ else:
82
+ connection = dataclass_from_dict(AzureOpenAIConnection, connection_dict)
83
+
84
+ elif connection_type in [OpenAIConnection.TYPE, "openai"]:
85
+ if _is_empty_connection_config(connection_dict):
86
+ connection = OpenAIConnection.from_env()
87
+ else:
88
+ connection = dataclass_from_dict(OpenAIConnection, connection_dict)
89
+
90
+ else:
91
+ error_message = (
92
+ f"'{connection_type}' is not a supported connection type. Valid values are "
93
+ f"[{AzureOpenAIConnection.TYPE}, {OpenAIConnection.TYPE}]"
94
+ )
95
+ raise MissingRequiredInputError(error_message)
96
+
97
+ missing_fields: Set[str] = set()
98
+ if not connection.is_valid(missing_fields):
99
+ raise MissingRequiredInputError(
100
+ f"The following required fields are missing for connection {connection.type}: "
101
+ f"{', '.join(missing_fields)}"
102
+ )
103
+
104
+ return connection
105
+
106
+
107
+ @dataclass
108
+ class OpenAIConnection(Connection):
109
+ """Connection class for OpenAI endpoints."""
110
+
111
+ base_url: str
112
+ api_key: Optional[str] = None
113
+ organization: Optional[str] = None
114
+
115
+ TYPE: ClassVar[str] = "openai"
116
+
117
+ @property
118
+ def type(self) -> str:
119
+ return OpenAIConnection.TYPE
120
+
121
+ @classmethod
122
+ def from_env(cls) -> "OpenAIConnection":
123
+ return cls(
124
+ base_url=os.environ.get("OPENAI_BASE_URL", ""),
125
+ api_key=os.environ.get("OPENAI_API_KEY"),
126
+ organization=os.environ.get("OPENAI_ORG_ID"),
127
+ )
128
+
129
+ def is_valid(self, missing_fields: Optional[Set[str]] = None) -> bool:
130
+ if missing_fields is None:
131
+ missing_fields = set()
132
+ if not self.base_url:
133
+ missing_fields.add("base_url")
134
+ if not self.api_key:
135
+ missing_fields.add("api_key")
136
+ if not self.organization:
137
+ missing_fields.add("organization")
138
+ return not bool(missing_fields)
139
+
140
+
141
+ @dataclass
142
+ class AzureOpenAIConnection(Connection):
143
+ """Connection class for Azure OpenAI endpoints."""
144
+
145
+ azure_endpoint: str
146
+ api_key: Optional[str] = None # TODO ralphe: Replace this TokenCredential to allow for more flexible authentication
147
+ azure_deployment: Optional[str] = None
148
+ api_version: Optional[str] = None
149
+ resource_id: Optional[str] = None
150
+
151
+ TYPE: ClassVar[str] = "azure_openai"
152
+
153
+ @property
154
+ def type(self) -> str:
155
+ return AzureOpenAIConnection.TYPE
156
+
157
+ @classmethod
158
+ def from_env(cls) -> "AzureOpenAIConnection":
159
+ return cls(
160
+ azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT", ""),
161
+ api_key=os.environ.get("AZURE_OPENAI_API_KEY"),
162
+ azure_deployment=os.environ.get("AZURE_OPENAI_DEPLOYMENT"),
163
+ api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-01"),
164
+ )
165
+
166
+ def __post_init__(self):
167
+ # set default API version
168
+ if not self.api_version:
169
+ self.api_version = "2024-02-01"
170
+
171
+ def is_valid(self, missing_fields: Optional[Set[str]] = None) -> bool:
172
+ if missing_fields is None:
173
+ missing_fields = set()
174
+ if not self.azure_endpoint:
175
+ missing_fields.add("azure_endpoint")
176
+ if not self.api_key:
177
+ missing_fields.add("api_key")
178
+ if not self.azure_deployment:
179
+ missing_fields.add("azure_deployment")
180
+ if not self.api_version:
181
+ missing_fields.add("api_version")
182
+ return not bool(missing_fields)
@@ -0,0 +1,59 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ from azure.ai.evaluation._exceptions import ErrorCategory, ErrorBlame, ErrorTarget, EvaluationException
6
+
7
+
8
+ class PromptyException(EvaluationException):
9
+ """Exception class for Prompty related errors.
10
+
11
+ This exception is used to indicate that the error was caused by Prompty execution.
12
+
13
+ :param message: The error message.
14
+ :type message: str
15
+ """
16
+
17
+ def __init__(self, message: str, **kwargs):
18
+ kwargs.setdefault("category", ErrorCategory.INVALID_VALUE)
19
+ kwargs.setdefault("target", ErrorTarget.UNKNOWN)
20
+ kwargs.setdefault("blame", ErrorBlame.USER_ERROR)
21
+
22
+ super().__init__(message, **kwargs)
23
+
24
+
25
+ class MissingRequiredInputError(PromptyException):
26
+ """Exception raised when missing required input"""
27
+
28
+ def __init__(self, message: str, **kwargs):
29
+ kwargs.setdefault("category", ErrorCategory.MISSING_FIELD)
30
+ kwargs.setdefault("target", ErrorTarget.EVAL_RUN)
31
+ super().__init__(message, **kwargs)
32
+
33
+
34
+ class InvalidInputError(PromptyException):
35
+ """Exception raised when an input is invalid, could not be loaded, or is not the expected format."""
36
+
37
+ def __init__(self, message: str, **kwargs):
38
+ kwargs.setdefault("category", ErrorCategory.INVALID_VALUE)
39
+ kwargs.setdefault("target", ErrorTarget.EVAL_RUN)
40
+ super().__init__(message, **kwargs)
41
+
42
+
43
+ class JinjaTemplateError(PromptyException):
44
+ """Exception raised when the Jinja template is invalid or could not be rendered."""
45
+
46
+ def __init__(self, message: str, **kwargs):
47
+ kwargs.setdefault("category", ErrorCategory.INVALID_VALUE)
48
+ kwargs.setdefault("target", ErrorTarget.EVAL_RUN)
49
+ super().__init__(message, **kwargs)
50
+
51
+
52
+ class NotSupportedError(PromptyException):
53
+ """Exception raised when the operation is not supported."""
54
+
55
+ def __init__(self, message: str, **kwargs):
56
+ kwargs.setdefault("category", ErrorCategory.INVALID_VALUE)
57
+ kwargs.setdefault("target", ErrorTarget.UNKNOWN)
58
+ kwargs.setdefault("blame", ErrorBlame.SYSTEM_ERROR)
59
+ super().__init__(message, **kwargs)
@@ -0,0 +1,313 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ import re
6
+ from os import PathLike
7
+ from pathlib import Path
8
+ from typing import Any, AsyncGenerator, Dict, Final, List, Mapping, Optional, Sequence, Tuple, Union, cast
9
+
10
+ from openai import AsyncAzureOpenAI, AsyncOpenAI, NotGiven
11
+
12
+ from azure.ai.evaluation._exceptions import ErrorTarget
13
+ from azure.ai.evaluation._constants import DefaultOpenEncoding
14
+ from azure.ai.evaluation._legacy.prompty._exceptions import (
15
+ InvalidInputError,
16
+ PromptyException,
17
+ MissingRequiredInputError,
18
+ NotSupportedError,
19
+ )
20
+ from azure.ai.evaluation._legacy.prompty._connection import AzureOpenAIConnection, Connection, OpenAIConnection
21
+ from azure.ai.evaluation._legacy.prompty._yaml_utils import load_yaml_string
22
+ from azure.ai.evaluation._legacy.prompty._utils import (
23
+ dataclass_from_dict,
24
+ PromptyModelConfiguration,
25
+ OpenAIChatResponseType,
26
+ build_messages,
27
+ format_llm_response,
28
+ prepare_open_ai_request_params,
29
+ resolve_references,
30
+ update_dict_recursively,
31
+ )
32
+
33
+
34
+ PROMPTY_EXTENSION: Final[str] = ".prompty"
35
+
36
+
37
+ class AsyncPrompty:
38
+ """A prompty is a prompt with predefined metadata like inputs, and can be executed directly like a flow.
39
+ A prompty is represented as a templated markdown file with a modified front matter.
40
+ The front matter is a yaml file that contains meta fields like model configuration, inputs, etc..
41
+
42
+ Prompty example:
43
+ .. code-block::
44
+
45
+ ---
46
+ name: Hello Prompty
47
+ description: A basic prompt
48
+ model:
49
+ api: chat
50
+ configuration:
51
+ type: azure_openai
52
+ azure_deployment: gpt-35-turbo
53
+ api_key="${env:AZURE_OPENAI_API_KEY}",
54
+ api_version=${env:AZURE_OPENAI_API_VERSION}",
55
+ azure_endpoint="${env:AZURE_OPENAI_ENDPOINT}",
56
+ parameters:
57
+ max_tokens: 128
58
+ temperature: 0.2
59
+ inputs:
60
+ text:
61
+ type: string
62
+ ---
63
+ system:
64
+ Write a simple {{text}} program that displays the greeting message.
65
+
66
+ Prompty as function example:
67
+
68
+ .. code-block:: python
69
+
70
+ from azure.ai.evaluation._legacy.prompty import AsyncPrompty
71
+ prompty = Prompty(path="path/to/prompty.prompty")
72
+ result = prompty(input_a=1, input_b=2)
73
+
74
+ # Override model config with dict
75
+ model_config = {
76
+ "api": "chat",
77
+ "configuration": {
78
+ "type": "azure_openai",
79
+ "azure_deployment": "gpt-35-turbo",
80
+ "api_key": "${env:AZURE_OPENAI_API_KEY}",
81
+ "api_version": "${env:AZURE_OPENAI_API_VERSION}",
82
+ "azure_endpoint": "${env:AZURE_OPENAI_ENDPOINT}",
83
+ },
84
+ "parameters": {
85
+ "max_token": 512
86
+ }
87
+ }
88
+ prompty = Prompty.load(source="path/to/prompty.prompty", model=model_config)
89
+ result = prompty(input_a=1, input_b=2)
90
+
91
+ # Override model config with configuration
92
+ from azure.ai.evaluation._legacy.prompty._connection import AzureOpenAIConnection
93
+ model_config = {
94
+ "api": "chat",
95
+ "configuration": AzureOpenAIModelConfiguration(
96
+ azure_deployment="gpt-35-turbo",
97
+ api_key="${env:AZURE_OPENAI_API_KEY}",
98
+ api_version="${env:AZURE_OPENAI_API_VERSION}",
99
+ azure_endpoint="${env:AZURE_OPENAI_ENDPOINT}",
100
+ ),
101
+ "parameters": {
102
+ "max_token": 512
103
+ }
104
+ }
105
+ prompty = Prompty(path="path/to/prompty.prompty", model=model_config)
106
+ result = prompty(input_a=1, input_b=2)
107
+
108
+ # Override model config with created connection
109
+ from azure.ai.evaluation._legacy.prompty._connection import AzureOpenAIConnection
110
+ model_config = {
111
+ "api": "chat",
112
+ "configuration": AzureOpenAIModelConfiguration(
113
+ connection="azure_open_ai_connection",
114
+ azure_deployment="gpt-35-turbo",
115
+ ),
116
+ "parameters": {
117
+ "max_token": 512
118
+ }
119
+ }
120
+ prompty = Prompty(path="path/to/prompty.prompty", model=model_config)
121
+ result = prompty(input_a=1, input_b=2)
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ path: Union[str, PathLike],
127
+ **kwargs: Any,
128
+ ):
129
+ path = Path(path)
130
+ configs, self._template = self._parse_prompty(path)
131
+ configs = resolve_references(configs, base_path=path.parent)
132
+ configs = update_dict_recursively(configs, resolve_references(kwargs, base_path=path.parent))
133
+
134
+ if configs["model"].get("api") == "completion":
135
+ raise InvalidInputError(
136
+ "Prompty does not support the completion API. Please use the 'chat' completions API instead."
137
+ )
138
+
139
+ self._data = configs
140
+ self._path = path
141
+ self._model = dataclass_from_dict(PromptyModelConfiguration, configs["model"])
142
+ self._inputs: Dict[str, Any] = configs.get("inputs", {})
143
+ self._outputs: Dict[str, Any] = configs.get("outputs", {})
144
+ self._name: str = configs.get("name", path.stem)
145
+
146
+ @property
147
+ def path(self) -> Path:
148
+ """Path of the prompty file.
149
+
150
+ :return: The path of the prompty file.
151
+ :rtype: Path
152
+ """
153
+ return self._path
154
+
155
+ @property
156
+ def name(self) -> str:
157
+ """Name of the prompty.
158
+
159
+ :return: The name of the prompty.
160
+ :rtype: str
161
+ """
162
+ return self._name
163
+
164
+ @property
165
+ def description(self) -> Optional[str]:
166
+ """Description of the prompty.
167
+
168
+ :return: The description of the prompty.
169
+ :rtype: str
170
+ """
171
+ return self._data.get("description")
172
+
173
+ @classmethod
174
+ def load(
175
+ cls,
176
+ source: Union[str, PathLike],
177
+ **kwargs,
178
+ ) -> "AsyncPrompty":
179
+ """
180
+ Loads the prompty file.
181
+
182
+ :param source: The local prompty file. Must be a path to a local file.
183
+ An exception is raised if the file does not exist.
184
+ :type source: Union[PathLike, str]
185
+ :return: A Prompty object
186
+ :rtype: Prompty
187
+ """
188
+ source_path = Path(source)
189
+ if not source_path.exists():
190
+ raise PromptyException(f"Source {source_path.absolute().as_posix()} does not exist")
191
+
192
+ if source_path.suffix != PROMPTY_EXTENSION:
193
+ raise PromptyException("Source must be a file with .prompty extension.")
194
+
195
+ return cls(path=source_path, **kwargs)
196
+
197
+ @staticmethod
198
+ def _parse_prompty(path) -> Tuple[Dict[str, Any], str]:
199
+ with open(path, "r", encoding=DefaultOpenEncoding.READ) as f:
200
+ prompty_content = f.read()
201
+ pattern = r"-{3,}\n(.*)-{3,}\n(.*)"
202
+ result = re.search(pattern, prompty_content, re.DOTALL)
203
+ if not result:
204
+ raise PromptyException(
205
+ "Illegal formatting of prompty. The prompt file is in markdown format and can be divided into two "
206
+ "parts, the first part is in YAML format and contains connection and model information. The second "
207
+ "part is the prompt template."
208
+ )
209
+ config_content, prompt_template = result.groups()
210
+ configs = load_yaml_string(config_content)
211
+ return configs, prompt_template
212
+
213
+ def _resolve_inputs(self, input_values: Dict[str, Any]) -> Mapping[str, Any]:
214
+ """
215
+ Resolve prompty inputs. If not provide input_values, sample data will be regarded as input value.
216
+ For inputs are not provided, the default value in the input signature will be used.
217
+
218
+ :param Dict[str, Any] input_values: The input values provided by the user.
219
+ :return: The resolved inputs.
220
+ :rtype: Mapping[str, Any]
221
+ """
222
+
223
+ resolved_inputs: Dict[str, Any] = {}
224
+ missing_inputs: List[str] = []
225
+ for input_name, value in self._inputs.items():
226
+ if input_name not in input_values and "default" not in value:
227
+ missing_inputs.append(input_name)
228
+ continue
229
+
230
+ resolved_inputs[input_name] = input_values.get(input_name, value.get("default", None))
231
+
232
+ if missing_inputs:
233
+ raise MissingRequiredInputError(f"Missing required inputs: {missing_inputs}")
234
+
235
+ return resolved_inputs
236
+
237
+ # TODO ralphe: error handling
238
+ # @trace
239
+ # @handle_openai_error()
240
+ async def __call__( # pylint: disable=docstring-keyword-should-match-keyword-only
241
+ self,
242
+ **kwargs: Any,
243
+ ) -> Union[OpenAIChatResponseType, AsyncGenerator[str, None], str, Mapping[str, Any]]:
244
+ """Calling prompty as a function in async, the inputs should be provided with key word arguments.
245
+ Returns the output of the prompty.
246
+
247
+ The function call throws PromptyException if the Prompty file is not valid or the inputs are not valid.
248
+
249
+ :keyword kwargs: Additional keyword arguments passed to the parent class.
250
+ :paramtype kwargs: Any
251
+ :return: The output of the prompty.
252
+ :rtype: ChatCompletion | AsyncStream[ChatCompletionChunk] | AsyncGenerator[str] | str | Mapping[str, Any]
253
+ """
254
+
255
+ inputs = self._resolve_inputs(kwargs)
256
+ connection = Connection.parse_from_config(self._model.configuration)
257
+ messages = build_messages(prompt=self._template, working_dir=self.path.parent, **inputs)
258
+ params = prepare_open_ai_request_params(self._model, messages)
259
+
260
+ timeout: Union[NotGiven, float] = NotGiven()
261
+ if timeout_val := cast(Any, kwargs.get("timeout", None)):
262
+ timeout = float(timeout_val)
263
+
264
+ # disable OpenAI's built-in retry mechanism by using our own retry
265
+ # for better debugging and real-time status updates.
266
+ max_retries = 0
267
+
268
+ api_client: Union[AsyncAzureOpenAI, AsyncOpenAI]
269
+ if isinstance(connection, AzureOpenAIConnection):
270
+ api_client = AsyncAzureOpenAI(
271
+ azure_endpoint=connection.azure_endpoint,
272
+ api_key=connection.api_key,
273
+ azure_deployment=connection.azure_deployment,
274
+ api_version=connection.api_version,
275
+ max_retries=max_retries,
276
+ )
277
+ elif isinstance(connection, OpenAIConnection):
278
+ api_client = AsyncOpenAI(
279
+ base_url=connection.base_url,
280
+ api_key=connection.api_key,
281
+ organization=connection.organization,
282
+ max_retries=max_retries,
283
+ )
284
+ else:
285
+ raise NotSupportedError(
286
+ f"'{type(connection).__name__}' is not a supported connection type.", target=ErrorTarget.EVAL_RUN
287
+ )
288
+
289
+ response: OpenAIChatResponseType = await api_client.with_options(timeout=timeout).chat.completions.create(
290
+ **params
291
+ )
292
+
293
+ return await format_llm_response(
294
+ response=response,
295
+ is_first_choice=self._data.get("model", {}).get("response", "first").lower() == "first",
296
+ response_format=params.get("response_format", {}),
297
+ outputs=self._outputs,
298
+ )
299
+
300
+ def render( # pylint: disable=docstring-keyword-should-match-keyword-only
301
+ self, **kwargs: Any
302
+ ) -> Sequence[Mapping[str, Any]]:
303
+ """Render the prompt content.
304
+
305
+ :keyword kwargs: Additional keyword arguments passed to the parent class.
306
+ :paramtype kwargs: Any
307
+ :return: Prompt content
308
+ :rtype: Sequence[Mapping[str, Any]]
309
+ """
310
+
311
+ inputs = self._resolve_inputs(kwargs)
312
+ messages = build_messages(prompt=self._template, working_dir=self.path.parent, **inputs)
313
+ return messages