Skip to content

Commit 0ce2acb

Browse files
Jordan Henkeldluc
authored andcommitted
ChatGPT API Python Support (#121)
This PR provides a path to using the ChatGPT API in the Python Preview of Semantic Kernel. In addition, this provides `Azure*` versions of many existing models (so Python users can leverage Azure OpenAI). I think that, in general, it may be worth considering how best to work with models that have different modalities: now we have text completions, embeddings, chat completions (and I'd imagine images/etc. may be nice to support someday too). Regardless, this PR provides a fast path to using the exciting new Chat APIs from OpenAI with SK! See the new `python/tests/chat_gpt_api.py` for a usage example.
1 parent 27e1831 commit 0ce2acb

20 files changed

Lines changed: 773 additions & 68 deletions

python/semantic_kernel/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from semantic_kernel.orchestration.context_variables import ContextVariables
1010
from semantic_kernel.orchestration.sk_context import SKContext
1111
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
12+
from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate
1213
from semantic_kernel.semantic_functions.prompt_template import PromptTemplate
1314
from semantic_kernel.semantic_functions.prompt_template_config import (
1415
PromptTemplateConfig,
@@ -17,7 +18,10 @@
1718
SemanticFunctionConfig,
1819
)
1920
from semantic_kernel.utils.null_logger import NullLogger
20-
from semantic_kernel.utils.settings import openai_settings_from_dot_env
21+
from semantic_kernel.utils.settings import (
22+
azure_openai_settings_from_dot_env,
23+
openai_settings_from_dot_env,
24+
)
2125

2226

2327
def create_kernel() -> KernelBase:
@@ -31,9 +35,11 @@ def kernel_builder() -> KernelBuilder:
3135
__all__ = [
3236
"create_kernel",
3337
"openai_settings_from_dot_env",
38+
"azure_openai_settings_from_dot_env",
3439
"extensions",
3540
"PromptTemplateConfig",
3641
"PromptTemplate",
42+
"ChatPromptTemplate",
3743
"SemanticFunctionConfig",
3844
"ContextVariables",
3945
"SKFunctionBase",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
from abc import ABC, abstractmethod
4+
from logging import Logger
5+
from typing import TYPE_CHECKING, List, Tuple
6+
7+
if TYPE_CHECKING:
8+
from semantic_kernel.ai.chat_request_settings import ChatRequestSettings
9+
10+
11+
class ChatCompletionClientBase(ABC):
12+
@abstractmethod
13+
async def complete_chat_async(
14+
self,
15+
messages: List[Tuple[str, str]],
16+
settings: "ChatRequestSettings",
17+
logger: Logger,
18+
) -> str:
19+
pass
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from semantic_kernel.semantic_functions.prompt_template_config import (
8+
PromptTemplateConfig,
9+
)
10+
11+
12+
@dataclass
13+
class ChatRequestSettings:
14+
temperature: float = 0.0
15+
top_p: float = 1.0
16+
presence_penalty: float = 0.0
17+
frequency_penalty: float = 0.0
18+
max_tokens: int = 256
19+
20+
def update_from_completion_config(
21+
self, completion_config: "PromptTemplateConfig.CompletionConfig"
22+
):
23+
self.temperature = completion_config.temperature
24+
self.top_p = completion_config.top_p
25+
self.presence_penalty = completion_config.presence_penalty
26+
self.frequency_penalty = completion_config.frequency_penalty
27+
self.max_tokens = completion_config.max_tokens
28+
29+
@staticmethod
30+
def from_completion_config(
31+
completion_config: "PromptTemplateConfig.CompletionConfig",
32+
) -> "ChatRequestSettings":
33+
settings = ChatRequestSettings()
34+
settings.update_from_completion_config(completion_config)
35+
return settings
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
4+
from logging import Logger
5+
from typing import Any, Optional
6+
7+
from semantic_kernel.ai.open_ai.services.open_ai_chat_completion import (
8+
OpenAIChatCompletion,
9+
)
10+
from semantic_kernel.diagnostics.verify import Verify
11+
12+
13+
class AzureChatCompletion(OpenAIChatCompletion):
14+
_endpoint: str
15+
_api_version: str
16+
17+
def __init__(
18+
self,
19+
deployment_name: str,
20+
endpoint: str,
21+
api_key: str,
22+
api_version: str,
23+
logger: Optional[Logger] = None,
24+
) -> None:
25+
Verify.not_empty(deployment_name, "You must provide a deployment name")
26+
Verify.not_empty(api_key, "The Azure API key cannot be empty")
27+
Verify.not_empty(endpoint, "The Azure endpoint cannot be empty")
28+
Verify.starts_with(
29+
endpoint, "https://", "The Azure endpoint must start with https://"
30+
)
31+
32+
self._endpoint = endpoint
33+
self._api_version = api_version
34+
35+
super().__init__(deployment_name, api_key, org_id=None, log=logger)
36+
37+
def _setup_open_ai(self) -> Any:
38+
import openai
39+
40+
openai.api_type = "azure"
41+
openai.api_key = self._api_key
42+
openai.api_base = self._endpoint
43+
openai.api_version = self._api_version
44+
45+
return openai
Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

33

4-
class AzureTextCompletion:
5-
pass
4+
from logging import Logger
5+
from typing import Any, Optional
6+
7+
from semantic_kernel.ai.open_ai.services.open_ai_text_completion import (
8+
OpenAITextCompletion,
9+
)
10+
from semantic_kernel.diagnostics.verify import Verify
11+
12+
13+
class AzureTextCompletion(OpenAITextCompletion):
14+
_endpoint: str
15+
_api_version: str
16+
17+
def __init__(
18+
self,
19+
deployment_name: str,
20+
endpoint: str,
21+
api_key: str,
22+
api_version: str,
23+
logger: Optional[Logger] = None,
24+
) -> None:
25+
Verify.not_empty(deployment_name, "You must provide a deployment name")
26+
Verify.not_empty(api_key, "The Azure API key cannot be empty")
27+
Verify.not_empty(endpoint, "The Azure endpoint cannot be empty")
28+
Verify.starts_with(
29+
endpoint, "https://", "The Azure endpoint must start with https://"
30+
)
31+
32+
self._endpoint = endpoint
33+
self._api_version = api_version
34+
35+
super().__init__(deployment_name, api_key, org_id=None, log=logger)
36+
37+
def _setup_open_ai(self) -> Any:
38+
import openai
39+
40+
openai.api_type = "azure"
41+
openai.api_key = self._api_key
42+
openai.api_base = self._endpoint
43+
openai.api_version = self._api_version
44+
45+
return openai
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
4+
from logging import Logger
5+
from typing import Any, Optional
6+
7+
from semantic_kernel.ai.open_ai.services.open_ai_text_embedding import (
8+
OpenAITextEmbedding,
9+
)
10+
from semantic_kernel.diagnostics.verify import Verify
11+
12+
13+
class AzureTextEmbedding(OpenAITextEmbedding):
14+
_endpoint: str
15+
_api_version: str
16+
17+
def __init__(
18+
self,
19+
deployment_name: str,
20+
endpoint: str,
21+
api_key: str,
22+
api_version: str,
23+
logger: Optional[Logger] = None,
24+
) -> None:
25+
Verify.not_empty(deployment_name, "You must provide a deployment name")
26+
Verify.not_empty(api_key, "The Azure API key cannot be empty")
27+
Verify.not_empty(endpoint, "The Azure endpoint cannot be empty")
28+
Verify.starts_with(
29+
endpoint, "https://", "The Azure endpoint must start with https://"
30+
)
31+
32+
self._endpoint = endpoint
33+
self._api_version = api_version
34+
35+
super().__init__(deployment_name, api_key, org_id=None, log=logger)
36+
37+
def _setup_open_ai(self) -> Any:
38+
import openai
39+
40+
openai.api_type = "azure"
41+
openai.api_key = self._api_key
42+
openai.api_base = self._endpoint
43+
openai.api_version = self._api_version
44+
45+
return openai
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
from logging import Logger
4+
from typing import Any, List, Optional, Tuple
5+
6+
from semantic_kernel.ai.ai_exception import AIException
7+
from semantic_kernel.ai.chat_completion_client_base import ChatCompletionClientBase
8+
from semantic_kernel.ai.chat_request_settings import ChatRequestSettings
9+
from semantic_kernel.diagnostics.verify import Verify
10+
from semantic_kernel.utils.null_logger import NullLogger
11+
12+
13+
class OpenAIChatCompletion(ChatCompletionClientBase):
14+
_model_id: str
15+
_api_key: str
16+
_org_id: Optional[str] = None
17+
_log: Logger
18+
19+
def __init__(
20+
self,
21+
model_id: str,
22+
api_key: str,
23+
org_id: Optional[str] = None,
24+
log: Optional[Logger] = None,
25+
) -> None:
26+
"""
27+
Initializes a new instance of the OpenAIChatCompletion class.
28+
29+
Arguments:
30+
model_id {str} -- OpenAI model name, see
31+
https://platform.openai.com/docs/models
32+
api_key {str} -- OpenAI API key, see
33+
https://platform.openai.com/account/api-keys
34+
org_id {Optional[str]} -- OpenAI organization ID.
35+
This is usually optional unless your
36+
account belongs to multiple organizations.
37+
"""
38+
self._model_id = model_id
39+
self._api_key = api_key
40+
self._org_id = org_id
41+
self._log = log if log is not None else NullLogger()
42+
self._messages = []
43+
44+
self.open_ai_instance = self._setup_open_ai()
45+
46+
def _setup_open_ai(self) -> Any:
47+
import openai
48+
49+
openai.api_key = self._api_key
50+
if self._org_id is not None:
51+
openai.organization = self._org_id
52+
53+
return openai
54+
55+
async def complete_chat_async(
56+
self, messages: List[Tuple[str, str]], request_settings: ChatRequestSettings
57+
) -> str:
58+
"""
59+
Completes the given user message. Returns a single string completion.
60+
61+
Arguments:
62+
user_message {str} -- The message (from a user) to respond to.
63+
request_settings {ChatRequestSettings} -- The request settings.
64+
65+
Returns:
66+
str -- The completed text.
67+
"""
68+
Verify.not_null(request_settings, "The request settings cannot be empty")
69+
70+
if request_settings.max_tokens < 1:
71+
raise AIException(
72+
AIException.ErrorCodes.InvalidRequest,
73+
"The max tokens must be greater than 0, "
74+
f"but was {request_settings.max_tokens}",
75+
)
76+
77+
if len(messages) <= 0:
78+
raise AIException(
79+
AIException.ErrorCodes.InvalidRequest,
80+
"To complete a chat you need at least one message",
81+
)
82+
83+
if messages[-1][0] != "user":
84+
raise AIException(
85+
AIException.ErrorCodes.InvalidRequest,
86+
"The last message must be from the user",
87+
)
88+
89+
model_args = {}
90+
if self.open_ai_instance.api_type == "azure":
91+
model_args["engine"] = self._model_id
92+
else:
93+
model_args["model"] = self._model_id
94+
95+
formatted_messages = [
96+
{"role": role, "content": message} for role, message in messages
97+
]
98+
99+
try:
100+
response: Any = await self.open_ai_instance.ChatCompletion.acreate(
101+
**model_args,
102+
messages=formatted_messages,
103+
temperature=request_settings.temperature,
104+
top_p=request_settings.top_p,
105+
presence_penalty=request_settings.presence_penalty,
106+
frequency_penalty=request_settings.frequency_penalty,
107+
max_tokens=request_settings.max_tokens,
108+
)
109+
except Exception as ex:
110+
raise AIException(
111+
AIException.ErrorCodes.ServiceError,
112+
"OpenAI service failed to complete the chat",
113+
ex,
114+
)
115+
116+
# TODO: tracking on token counts/etc.
117+
118+
return response.choices[0].message.content

python/semantic_kernel/ai/open_ai/services/open_ai_text_completion.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ def __init__(
4040
self._org_id = org_id
4141
self._log = log if log is not None else NullLogger()
4242

43+
self.open_ai_instance = self._setup_open_ai()
44+
45+
def _setup_open_ai(self) -> Any:
46+
import openai
47+
48+
openai.api_key = self._api_key
49+
if self._org_id is not None:
50+
openai.organization = self._org_id
51+
52+
return openai
53+
4354
async def complete_simple_async(
4455
self, prompt: str, request_settings: CompleteRequestSettings
4556
) -> str:
@@ -54,8 +65,6 @@ async def complete_simple_async(
5465
Returns:
5566
str -- The completed text.
5667
"""
57-
import openai
58-
5968
Verify.not_empty(prompt, "The prompt is empty")
6069
Verify.not_null(request_settings, "The request settings cannot be empty")
6170

@@ -80,13 +89,15 @@ async def complete_simple_async(
8089
f"but logprobs={request_settings.logprobs} was requested",
8190
)
8291

83-
openai.api_key = self._api_key
84-
if self._org_id is not None:
85-
openai.organization = self._org_id
92+
model_args = {}
93+
if self.open_ai_instance.api_type == "azure":
94+
model_args["engine"] = self._model_id
95+
else:
96+
model_args["model"] = self._model_id
8697

8798
try:
88-
response: Any = await openai.Completion.acreate(
89-
model=self._model_id,
99+
response: Any = await self.open_ai_instance.Completion.acreate(
100+
**model_args,
90101
prompt=prompt,
91102
temperature=request_settings.temperature,
92103
top_p=request_settings.top_p,

0 commit comments

Comments
 (0)