Skip to content

Commit defa6f4

Browse files
authored
Add support for new OpenAI API. (#144)
* Update openai version in pyproject.toml * add OpenAiChatCompletion * Update OpenAI client and add AzureOpenAI support * Add OpenAiChatResult and OpenAiChat classes * Refactor OpenAiChat.create_model_response method signature and add docstring * Add docstring to OpenAiChat class * improve doc * Add OpenAiChat tests for error handling * Update OpenAiAzureChat class constructor * improve typing * Fix method signature in OpenAiChat class
1 parent c562d9b commit defa6f4

3 files changed

Lines changed: 166 additions & 146 deletions

File tree

mltb2/openai.py

Lines changed: 131 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
"""
1111

1212

13-
from abc import ABC, abstractmethod
1413
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
1615

1716
import tiktoken
1817
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
2120
from tiktoken.core import Encoding
2221
from tqdm import tqdm
2322

@@ -67,206 +66,194 @@ def __call__(self, text: Union[str, Iterable]) -> Union[int, List[int]]:
6766

6867

6968
@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>`_
7277
7378
Args:
74-
text: the result of the OpenAI completion
79+
content: the result of the OpenAI completion
7580
model: model name which has been used
7681
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``)
7984
finish_reason: The reason why the completion stopped.
8085
8186
* ``stop``: Means the API returned the full completion without running into any token limit.
8287
* ``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.
8491
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``
8898
"""
8999

90-
text: Optional[str] = None
100+
content: Optional[str] = None
91101
model: Optional[str] = None
92102
prompt_tokens: Optional[int] = None
93103
completion_tokens: Optional[int] = None
94104
total_tokens: Optional[int] = None
95105
finish_reason: Optional[str] = None
106+
completion_args: Optional[Dict[str, Any]] = None
96107

97108
@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+
"""
100122
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")
103127
if usage is not None:
104128
result["prompt_tokens"] = usage.get("prompt_tokens")
105129
result["completion_tokens"] = usage.get("completion_tokens")
106130
result["total_tokens"] = usage.get("total_tokens")
107-
choices = open_ai_object.get("choices")
131+
choices = chat_completion_dict.get("choices")
108132
if choices is not None and len(choices) > 0:
109133
choice = choices[0]
110134
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]
118139

119140

120141
@dataclass
121-
class OpenAiBaseCompletion(ABC):
122-
"""Abstract base class for OpenAI completion.
142+
class OpenAiChat:
143+
"""Tool to interact with OpenAI chat models.
123144
124-
Args:
125-
completion_kwargs: kwargs for the OpenAI completion function
145+
This also be constructed with :meth:`~OpenAiChat.from_yaml`.
126146
127147
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.
130153
"""
131154

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)
133162

134163
@classmethod
135164
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+
"""
137172
with open(yaml_file, "r") as file:
138173
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)
146175

147176
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).
151182
152183
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.
165186
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.
166189
167-
@dataclass
168-
class OpenAiChatCompletion(OpenAiBaseCompletion):
169-
"""OpenAI chat completion.
190+
Also see:
170191
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>`_
172194
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+
)
179209

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
184223
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,
188227
)
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
199230

200231

201232
@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.
206235
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`.
215237
216238
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>`_
231241
232242
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.
237248
"""
238249

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
269252

270253
def __post_init__(self) -> None:
271254
"""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+
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ torch = {version = "!=2.0.1,!=2.1.0", optional = true} # some versions have poe
6969
transformers = {version = "*", optional = true}
7070
tiktoken = {version = "*", optional = true}
7171
safetensors = {version = "!=0.3.2", optional = true} # version 0.3.2 has poetry issues
72-
openai = {version = "^0", optional = true}
72+
openai = {version = "^1", optional = true}
7373
pyyaml = {version = "*", optional = true}
7474
pandas = {version = "*", optional = true}
7575
beautifulsoup4 = {version = "*", optional = true}

0 commit comments

Comments
 (0)