|
10 | 10 | """ |
11 | 11 |
|
12 | 12 |
|
13 | | -from abc import ABC, abstractmethod |
14 | 13 | from dataclasses import dataclass, field |
15 | | -from typing import Any, Dict, Iterable, List, Mapping, Optional, Union |
| 14 | +from typing import Any, Dict, Iterable, List, Optional, Union |
16 | 15 |
|
17 | 16 | import tiktoken |
18 | 17 | import yaml |
19 | | -from openai import ChatCompletion, Completion |
20 | | -from openai.openai_object import OpenAIObject |
| 18 | +from openai import AzureOpenAI, OpenAI |
| 19 | +from openai.types.chat import ChatCompletion |
21 | 20 | from tiktoken.core import Encoding |
22 | 21 | from tqdm import tqdm |
23 | 22 |
|
@@ -67,206 +66,194 @@ def __call__(self, text: Union[str, Iterable]) -> Union[int, List[int]]: |
67 | 66 |
|
68 | 67 |
|
69 | 68 | @dataclass |
70 | | -class OpenAiCompletionAnswer: |
71 | | - """Answer of an OpenAI completion. |
| 69 | +class OpenAiChatResult: |
| 70 | + """Result of an OpenAI chat completion. |
| 71 | +
|
| 72 | + If you want to convert this to a ``dict`` use ``asdict(open_ai_chat_result)`` |
| 73 | + from the ``dataclasses`` module. |
| 74 | +
|
| 75 | + See Also: |
| 76 | + OpenAI API reference: `The chat completion object <https://platform.openai.com/docs/api-reference/chat/object>`_ |
72 | 77 |
|
73 | 78 | Args: |
74 | | - text: the result of the OpenAI completion |
| 79 | + content: the result of the OpenAI completion |
75 | 80 | model: model name which has been used |
76 | 81 | prompt_tokens: number of tokens of the prompt |
77 | | - completion_tokens: number of tokens of the completion (``text``) |
78 | | - total_tokens: number of total tokens (``prompt_tokens + completion_tokens``) |
| 82 | + completion_tokens: number of tokens of the completion (``content``) |
| 83 | + total_tokens: number of total tokens (``prompt_tokens + content_tokens``) |
79 | 84 | finish_reason: The reason why the completion stopped. |
80 | 85 |
|
81 | 86 | * ``stop``: Means the API returned the full completion without running into any token limit. |
82 | 87 | * ``length``: Means the API stopped the completion because of running into a token limit. |
83 | | - * ``function_call``: When the model called a function. |
| 88 | + * ``content_filter``: When content was omitted due to a flag from the OpenAI content filters. |
| 89 | + * ``tool_calls``: When the model called a tool. |
| 90 | + * ``function_call`` (deprecated): When the model called a function. |
84 | 91 |
|
85 | | - See Also: |
86 | | - * `The chat completion object <https://platform.openai.com/docs/api-reference/chat/object>`_ |
87 | | - * `The completion object <https://platform.openai.com/docs/api-reference/completions/object>`_ |
| 92 | + completion_args: The arguments which have been used for the completion. Examples: |
| 93 | +
|
| 94 | + * ``model``: always set |
| 95 | + * ``max_tokens``: only set if ``completion_kwargs`` contained ``max_tokens`` |
| 96 | + * ``temperature``: only set if ``completion_kwargs`` contained ``temperature`` |
| 97 | + * ``top_p``: only set if ``completion_kwargs`` contained ``top_p`` |
88 | 98 | """ |
89 | 99 |
|
90 | | - text: Optional[str] = None |
| 100 | + content: Optional[str] = None |
91 | 101 | model: Optional[str] = None |
92 | 102 | prompt_tokens: Optional[int] = None |
93 | 103 | completion_tokens: Optional[int] = None |
94 | 104 | total_tokens: Optional[int] = None |
95 | 105 | finish_reason: Optional[str] = None |
| 106 | + completion_args: Optional[Dict[str, Any]] = None |
96 | 107 |
|
97 | 108 | @classmethod |
98 | | - def from_open_ai_object(cls, open_ai_object: OpenAIObject): |
99 | | - """Construct this class from ``OpenAIObject``.""" |
| 109 | + def from_chat_completion( |
| 110 | + cls, |
| 111 | + chat_completion: ChatCompletion, |
| 112 | + completion_kwargs: Optional[Dict[str, Any]] = None, |
| 113 | + ): |
| 114 | + """Construct this class from an OpenAI ``ChatCompletion`` object. |
| 115 | +
|
| 116 | + Args: |
| 117 | + chat_completion: The OpenAI ``ChatCompletion`` object. |
| 118 | + completion_kwargs: The arguments which have been used for the completion. |
| 119 | + Returns: |
| 120 | + The constructed class. |
| 121 | + """ |
100 | 122 | result = {} |
101 | | - result["model"] = open_ai_object.get("model") |
102 | | - usage = open_ai_object.get("usage") |
| 123 | + result["completion_args"] = completion_kwargs |
| 124 | + chat_completion_dict = chat_completion.model_dump() |
| 125 | + result["model"] = chat_completion_dict.get("model") |
| 126 | + usage = chat_completion_dict.get("usage") |
103 | 127 | if usage is not None: |
104 | 128 | result["prompt_tokens"] = usage.get("prompt_tokens") |
105 | 129 | result["completion_tokens"] = usage.get("completion_tokens") |
106 | 130 | result["total_tokens"] = usage.get("total_tokens") |
107 | | - choices = open_ai_object.get("choices") |
| 131 | + choices = chat_completion_dict.get("choices") |
108 | 132 | if choices is not None and len(choices) > 0: |
109 | 133 | choice = choices[0] |
110 | 134 | result["finish_reason"] = choice.get("finish_reason") |
111 | | - if "text" in choice: # non chat models |
112 | | - result["text"] = choice.get("text") |
113 | | - elif "message" in choice: # chat models |
114 | | - message = choice.get("message") |
115 | | - if message is not None: |
116 | | - result["text"] = message.get("content") |
117 | | - return cls(**result) |
| 135 | + message = choice.get("message") |
| 136 | + if message is not None: |
| 137 | + result["content"] = message.get("content") |
| 138 | + return cls(**result) # type: ignore[arg-type] |
118 | 139 |
|
119 | 140 |
|
120 | 141 | @dataclass |
121 | | -class OpenAiBaseCompletion(ABC): |
122 | | - """Abstract base class for OpenAI completion. |
| 142 | +class OpenAiChat: |
| 143 | + """Tool to interact with OpenAI chat models. |
123 | 144 |
|
124 | | - Args: |
125 | | - completion_kwargs: kwargs for the OpenAI completion function |
| 145 | + This also be constructed with :meth:`~OpenAiChat.from_yaml`. |
126 | 146 |
|
127 | 147 | See Also: |
128 | | - * `Create chat completion <https://platform.openai.com/docs/api-reference/chat/create>`_ |
129 | | - * `Create completion <https://platform.openai.com/docs/api-reference/completions/create>`_ |
| 148 | + OpenAI API reference: `Create chat completion <https://platform.openai.com/docs/api-reference/chat/create>`_ |
| 149 | +
|
| 150 | + Args: |
| 151 | + api_key: The OpenAI API key. |
| 152 | + model: The OpenAI model name. |
130 | 153 | """ |
131 | 154 |
|
132 | | - completion_kwargs: Dict[str, Any] |
| 155 | + api_key: str |
| 156 | + model: str |
| 157 | + client: Union[OpenAI, AzureOpenAI] = field(init=False, repr=False) |
| 158 | + |
| 159 | + def __post_init__(self) -> None: |
| 160 | + """Do post init.""" |
| 161 | + self.client = OpenAI(api_key=self.api_key) |
133 | 162 |
|
134 | 163 | @classmethod |
135 | 164 | def from_yaml(cls, yaml_file): |
136 | | - """Construct this class from a yaml file.""" |
| 165 | + """Construct this class from a yaml file. |
| 166 | +
|
| 167 | + Args: |
| 168 | + yaml_file: The yaml file. |
| 169 | + Returns: |
| 170 | + The constructed class. |
| 171 | + """ |
137 | 172 | with open(yaml_file, "r") as file: |
138 | 173 | completion_kwargs = yaml.safe_load(file) |
139 | | - return cls(completion_kwargs) |
140 | | - |
141 | | - @abstractmethod |
142 | | - def _completion( |
143 | | - self, prompt: Union[str, List[Dict[str, str]]], completion_kwargs_for_this_call: Mapping[str, Any] |
144 | | - ) -> OpenAIObject: |
145 | | - """Abstract method to call the OpenAI completion.""" |
| 174 | + return cls(**completion_kwargs) |
146 | 175 |
|
147 | 176 | def __call__( |
148 | | - self, prompt: Union[str, List[Dict[str, str]]], completion_kwargs: Optional[Mapping[str, Any]] = None |
149 | | - ) -> OpenAiCompletionAnswer: |
150 | | - """Call the OpenAI prompt completion. |
| 177 | + self, |
| 178 | + prompt: Union[str, List[Dict[str, str]]], |
| 179 | + completion_kwargs: Optional[Dict[str, Any]] = None, |
| 180 | + ) -> OpenAiChatResult: |
| 181 | + """Create a model response for the given prompt (chat conversation). |
151 | 182 |
|
152 | 183 | Args: |
153 | | - prompt: The prompt to be completed by the LLM. |
154 | | - In case of chat models this can be a string or a list. |
155 | | - In case of "non chat" models only a string is allowed. |
156 | | - completion_kwargs: Overwrite the ``completion_kwargs`` for this call. |
157 | | - This allows you, for example, to change the temperature for this call only. |
158 | | - """ |
159 | | - completion_kwargs_for_this_call = self.completion_kwargs.copy() |
160 | | - if completion_kwargs is not None: |
161 | | - completion_kwargs_for_this_call.update(completion_kwargs) |
162 | | - open_ai_object: OpenAIObject = self._completion(prompt, completion_kwargs_for_this_call) |
163 | | - open_ai_completion_answer = OpenAiCompletionAnswer.from_open_ai_object(open_ai_object) |
164 | | - return open_ai_completion_answer |
| 184 | + prompt: The prompt for the model. |
| 185 | + completion_kwargs: Keyword arguments for the OpenAI completion. |
165 | 186 |
|
| 187 | + - ``model`` can not be set via ``completion_kwargs``! Please set the ``model`` in the initializer. |
| 188 | + - ``messages`` can not be set via ``completion_kwargs``! Please set the ``prompt`` argument. |
166 | 189 |
|
167 | | -@dataclass |
168 | | -class OpenAiChatCompletion(OpenAiBaseCompletion): |
169 | | - """OpenAI chat completion. |
| 190 | + Also see: |
170 | 191 |
|
171 | | - This also be constructed with :meth:`OpenAiBaseCompletion.from_yaml`. |
| 192 | + - ``openai.resources.chat.completions.Completions.create()`` |
| 193 | + - OpenAI API reference: `Create chat completion <https://platform.openai.com/docs/api-reference/chat/create>`_ |
172 | 194 |
|
173 | | - Args: |
174 | | - completion_kwargs: The kwargs for the OpenAI completion function. |
175 | | -
|
176 | | - See Also: |
177 | | - `Create chat completion <https://platform.openai.com/docs/api-reference/chat/create>`_ |
178 | | - """ |
| 195 | + Returns: |
| 196 | + The result of the OpenAI completion. |
| 197 | + """ |
| 198 | + if isinstance(prompt, list): |
| 199 | + for message in prompt: |
| 200 | + if "role" not in message or "content" not in message: |
| 201 | + raise ValueError( |
| 202 | + "If prompt is a list of messages, each message must have a 'role' and 'content' key!" |
| 203 | + ) |
| 204 | + if message["role"] not in ["system", "user", "assistant", "tool"]: |
| 205 | + raise ValueError( |
| 206 | + "If prompt is a list of messages, each message must have a 'role' key with one of the values " |
| 207 | + "'system', 'user', 'assistant' or 'tool'!" |
| 208 | + ) |
179 | 209 |
|
180 | | - def _completion( |
181 | | - self, prompt: Union[str, List[Dict[str, str]]], completion_kwargs_for_this_call: Mapping[str, Any] |
182 | | - ) -> OpenAIObject: |
183 | | - """Call to the OpenAI chat completion.""" |
| 210 | + if completion_kwargs is not None: |
| 211 | + # check keys of completion_kwargs |
| 212 | + if "model" in completion_kwargs: |
| 213 | + raise ValueError( |
| 214 | + "'model' can not be set via 'completion_kwargs'! Please set the 'model' in the initializer." |
| 215 | + ) |
| 216 | + if "messages" in completion_kwargs: |
| 217 | + raise ValueError( |
| 218 | + "'messages' can not be set via 'completion_kwargs'! Please set the 'prompt' argument." |
| 219 | + ) |
| 220 | + else: |
| 221 | + completion_kwargs = {} # set default value |
| 222 | + completion_kwargs["model"] = self.model |
184 | 223 | messages = [{"role": "user", "content": prompt}] if isinstance(prompt, str) else prompt |
185 | | - open_ai_object: OpenAIObject = ChatCompletion.create( |
186 | | - messages=messages, |
187 | | - **completion_kwargs_for_this_call, |
| 224 | + chat_completion = self.client.chat.completions.create( |
| 225 | + messages=messages, # type: ignore[arg-type] |
| 226 | + **completion_kwargs, |
188 | 227 | ) |
189 | | - return open_ai_object |
190 | | - |
191 | | - |
192 | | -def _check_mandatory_azure_completion_kwargs(completion_kwargs: Mapping[str, Any]) -> None: |
193 | | - """Check mandatory Azure ``completion_kwargs``.""" |
194 | | - for mandatory_azure_completion_kwarg in ("api_base", "engine", "api_type", "api_version"): |
195 | | - if mandatory_azure_completion_kwarg not in completion_kwargs: |
196 | | - raise ValueError(f"You must set '{mandatory_azure_completion_kwarg}' for Azure completion!") |
197 | | - if completion_kwargs["api_type"] != "azure": |
198 | | - raise ValueError("You must set 'api_type' to 'azure' for Azure completion!") |
| 228 | + result = OpenAiChatResult.from_chat_completion(chat_completion, completion_kwargs=completion_kwargs) |
| 229 | + return result |
199 | 230 |
|
200 | 231 |
|
201 | 232 | @dataclass |
202 | | -class OpenAiAzureChatCompletion(OpenAiChatCompletion): |
203 | | - """OpenAI Azure chat completion. |
204 | | -
|
205 | | - This also be constructed with :meth:`OpenAiBaseCompletion.from_yaml`. |
| 233 | +class OpenAiAzureChat(OpenAiChat): |
| 234 | + """Tool to interact with Azure OpenAI chat models. |
206 | 235 |
|
207 | | - Args: |
208 | | - completion_kwargs: The kwargs for the OpenAI completion function. |
209 | | - The following Azure specific properties must be specified: |
210 | | -
|
211 | | - * ``api_type`` |
212 | | - * ``api_version`` |
213 | | - * ``api_base`` |
214 | | - * ``engine`` |
| 236 | + This can also be constructed with :meth:`~OpenAiChat.from_yaml`. |
215 | 237 |
|
216 | 238 | See Also: |
217 | | - * `Create chat completion <https://platform.openai.com/docs/api-reference/chat/create>`_ |
218 | | - * `Quickstart: Get started using GPT-35-Turbo and GPT-4 with Azure OpenAI Service <https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line&pivots=programming-language-python>`_ |
219 | | - """ |
220 | | - |
221 | | - def __post_init__(self) -> None: |
222 | | - """Do post init.""" |
223 | | - _check_mandatory_azure_completion_kwargs(self.completion_kwargs) |
224 | | - |
225 | | - |
226 | | -@dataclass |
227 | | -class OpenAiCompletion(OpenAiBaseCompletion): |
228 | | - """OpenAI (non chat) completion. |
229 | | -
|
230 | | - This also be constructed with :meth:`OpenAiBaseCompletion.from_yaml`. |
| 239 | + * OpenAI API reference: `Create chat completion <https://platform.openai.com/docs/api-reference/chat/create>`_ |
| 240 | + * `Quickstart: Get started generating text using Azure OpenAI Service <https://learn.microsoft.com/en-us/azure/ai-services/openai/quickstart?tabs=command-line&pivots=programming-language-python>`_ |
231 | 241 |
|
232 | 242 | Args: |
233 | | - completion_kwargs: The kwargs for the OpenAI completion function. |
234 | | -
|
235 | | - See Also: |
236 | | - `Create completion <https://platform.openai.com/docs/api-reference/completions/create>`_ |
| 243 | + api_key: The OpenAI API key. |
| 244 | + model: The OpenAI model name. |
| 245 | + api_version: The OpenAI API version. |
| 246 | + A common value for this is ``2023-05-15``. |
| 247 | + azure_endpoint: The Azure endpoint. |
237 | 248 | """ |
238 | 249 |
|
239 | | - def _completion( |
240 | | - self, prompt: Union[str, List[Dict[str, str]]], completion_kwargs_for_this_call: Mapping[str, Any] |
241 | | - ) -> OpenAIObject: |
242 | | - """Call to the OpenAI (not chat) completion.""" |
243 | | - open_ai_object: OpenAIObject = Completion.create( |
244 | | - prompt=prompt, |
245 | | - **completion_kwargs_for_this_call, |
246 | | - ) |
247 | | - return open_ai_object |
248 | | - |
249 | | - |
250 | | -@dataclass |
251 | | -class OpenAiAzureCompletion(OpenAiCompletion): |
252 | | - """OpenAI Azure (non chat) completion. |
253 | | -
|
254 | | - This also be constructed with :meth:`OpenAiBaseCompletion.from_yaml`. |
255 | | -
|
256 | | - Args: |
257 | | - completion_kwargs: The kwargs for the OpenAI completion function. |
258 | | - The following Azure specific properties must be specified: |
259 | | -
|
260 | | - * ``api_type`` |
261 | | - * ``api_version`` |
262 | | - * ``api_base`` |
263 | | - * ``engine`` |
264 | | -
|
265 | | - See Also: |
266 | | - * `Create completion <https://platform.openai.com/docs/api-reference/completions/create>`_ |
267 | | - * `Quickstart: Get started generating text using Azure OpenAI Service <https://learn.microsoft.com/en-us/azure/ai-services/openai/quickstart?tabs=command-line&pivots=programming-language-python>`_ |
268 | | - """ |
| 250 | + api_version: str |
| 251 | + azure_endpoint: str |
269 | 252 |
|
270 | 253 | def __post_init__(self) -> None: |
271 | 254 | """Do post init.""" |
272 | | - _check_mandatory_azure_completion_kwargs(self.completion_kwargs) |
| 255 | + self.client = AzureOpenAI( |
| 256 | + api_key=self.api_key, |
| 257 | + api_version=self.api_version, |
| 258 | + azure_endpoint=self.azure_endpoint, |
| 259 | + ) |
0 commit comments