2024-01-05 23:03:28 +00:00
|
|
|
"""Azure OpenAI chat wrapper."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import os
|
2024-06-17 20:37:41 +00:00
|
|
|
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Type, Union
|
2024-01-05 23:03:28 +00:00
|
|
|
|
|
|
|
import openai
|
2024-06-17 20:37:41 +00:00
|
|
|
from langchain_core.language_models import LanguageModelInput
|
2024-05-17 17:51:26 +00:00
|
|
|
from langchain_core.language_models.chat_models import LangSmithParams
|
2024-06-17 20:37:41 +00:00
|
|
|
from langchain_core.messages import BaseMessage
|
2024-01-05 23:03:28 +00:00
|
|
|
from langchain_core.outputs import ChatResult
|
2024-06-17 20:37:41 +00:00
|
|
|
from langchain_core.pydantic_v1 import BaseModel, Field, SecretStr, root_validator
|
|
|
|
from langchain_core.runnables import Runnable
|
|
|
|
from langchain_core.tools import BaseTool
|
2024-01-30 23:49:56 +00:00
|
|
|
from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env
|
2024-06-17 20:37:41 +00:00
|
|
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
2024-01-05 23:03:28 +00:00
|
|
|
|
2024-05-01 22:03:29 +00:00
|
|
|
from langchain_openai.chat_models.base import BaseChatOpenAI
|
2024-01-05 23:03:28 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2024-05-01 22:03:29 +00:00
|
|
|
class AzureChatOpenAI(BaseChatOpenAI):
|
2024-01-05 23:03:28 +00:00
|
|
|
"""`Azure OpenAI` Chat Completion API.
|
|
|
|
|
|
|
|
To use this class you
|
|
|
|
must have a deployed model on Azure OpenAI. Use `deployment_name` in the
|
|
|
|
constructor to refer to the "Model deployment name" in the Azure portal.
|
|
|
|
|
|
|
|
In addition, you should have the
|
|
|
|
following environment variables set or passed in constructor in lower case:
|
|
|
|
- ``AZURE_OPENAI_API_KEY``
|
|
|
|
- ``AZURE_OPENAI_ENDPOINT``
|
|
|
|
- ``AZURE_OPENAI_AD_TOKEN``
|
|
|
|
- ``OPENAI_API_VERSION``
|
|
|
|
- ``OPENAI_PROXY``
|
|
|
|
|
|
|
|
For example, if you have `gpt-3.5-turbo` deployed, with the deployment name
|
|
|
|
`35-turbo-dev`, the constructor should look like:
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
2024-01-25 23:16:04 +00:00
|
|
|
from langchain_openai import AzureChatOpenAI
|
|
|
|
|
2024-01-05 23:03:28 +00:00
|
|
|
AzureChatOpenAI(
|
|
|
|
azure_deployment="35-turbo-dev",
|
|
|
|
openai_api_version="2023-05-15",
|
|
|
|
)
|
|
|
|
|
|
|
|
Be aware the API version may change.
|
|
|
|
|
|
|
|
You can also specify the version of the model using ``model_version`` constructor
|
|
|
|
parameter, as Azure OpenAI doesn't return model version with the response.
|
|
|
|
|
|
|
|
Default is empty. When you specify the version, it will be appended to the
|
|
|
|
model name in the response. Setting correct version will help you to calculate the
|
|
|
|
cost properly. Model version is not validated, so make sure you set it correctly
|
|
|
|
to get the correct cost.
|
|
|
|
|
|
|
|
Any parameters that are valid to be passed to the openai.create call can be passed
|
|
|
|
in, even if not explicitly saved on this class.
|
|
|
|
"""
|
|
|
|
|
|
|
|
azure_endpoint: Union[str, None] = None
|
|
|
|
"""Your Azure endpoint, including the resource.
|
|
|
|
|
|
|
|
Automatically inferred from env var `AZURE_OPENAI_ENDPOINT` if not provided.
|
|
|
|
|
|
|
|
Example: `https://example-resource.azure.openai.com/`
|
|
|
|
"""
|
|
|
|
deployment_name: Union[str, None] = Field(default=None, alias="azure_deployment")
|
|
|
|
"""A model deployment.
|
|
|
|
|
|
|
|
If given sets the base client URL to include `/deployments/{azure_deployment}`.
|
|
|
|
Note: this means you won't be able to use non-deployment endpoints.
|
|
|
|
"""
|
|
|
|
openai_api_version: str = Field(default="", alias="api_version")
|
|
|
|
"""Automatically inferred from env var `OPENAI_API_VERSION` if not provided."""
|
2024-01-30 23:49:56 +00:00
|
|
|
openai_api_key: Optional[SecretStr] = Field(default=None, alias="api_key")
|
2024-01-05 23:03:28 +00:00
|
|
|
"""Automatically inferred from env var `AZURE_OPENAI_API_KEY` if not provided."""
|
2024-01-30 23:49:56 +00:00
|
|
|
azure_ad_token: Optional[SecretStr] = None
|
2024-01-05 23:03:28 +00:00
|
|
|
"""Your Azure Active Directory token.
|
|
|
|
|
|
|
|
Automatically inferred from env var `AZURE_OPENAI_AD_TOKEN` if not provided.
|
|
|
|
|
|
|
|
For more:
|
|
|
|
https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id.
|
2024-05-22 22:21:08 +00:00
|
|
|
"""
|
2024-01-05 23:03:28 +00:00
|
|
|
azure_ad_token_provider: Union[Callable[[], str], None] = None
|
|
|
|
"""A function that returns an Azure Active Directory token.
|
|
|
|
|
|
|
|
Will be invoked on every request.
|
|
|
|
"""
|
|
|
|
model_version: str = ""
|
|
|
|
"""Legacy, for openai<1.0.0 support."""
|
|
|
|
openai_api_type: str = ""
|
|
|
|
"""Legacy, for openai<1.0.0 support."""
|
|
|
|
validate_base_url: bool = True
|
|
|
|
"""For backwards compatibility. If legacy val openai_api_base is passed in, try to
|
|
|
|
infer if it is a base_url or azure_endpoint and update accordingly.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_lc_namespace(cls) -> List[str]:
|
|
|
|
"""Get the namespace of the langchain object."""
|
|
|
|
return ["langchain", "chat_models", "azure_openai"]
|
|
|
|
|
2024-05-01 22:03:29 +00:00
|
|
|
@property
|
|
|
|
def lc_secrets(self) -> Dict[str, str]:
|
|
|
|
return {
|
|
|
|
"openai_api_key": "AZURE_OPENAI_API_KEY",
|
|
|
|
"azure_ad_token": "AZURE_OPENAI_AD_TOKEN",
|
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def is_lc_serializable(cls) -> bool:
|
|
|
|
return True
|
|
|
|
|
2024-01-05 23:03:28 +00:00
|
|
|
@root_validator()
|
|
|
|
def validate_environment(cls, values: Dict) -> Dict:
|
|
|
|
"""Validate that api key and python package exists in environment."""
|
|
|
|
if values["n"] < 1:
|
|
|
|
raise ValueError("n must be at least 1.")
|
|
|
|
if values["n"] > 1 and values["streaming"]:
|
|
|
|
raise ValueError("n must be 1 when streaming.")
|
|
|
|
|
|
|
|
# Check OPENAI_KEY for backwards compatibility.
|
|
|
|
# TODO: Remove OPENAI_API_KEY support to avoid possible conflict when using
|
|
|
|
# other forms of azure credentials.
|
2024-01-30 23:49:56 +00:00
|
|
|
openai_api_key = (
|
2024-01-05 23:03:28 +00:00
|
|
|
values["openai_api_key"]
|
|
|
|
or os.getenv("AZURE_OPENAI_API_KEY")
|
|
|
|
or os.getenv("OPENAI_API_KEY")
|
|
|
|
)
|
2024-01-30 23:49:56 +00:00
|
|
|
values["openai_api_key"] = (
|
|
|
|
convert_to_secret_str(openai_api_key) if openai_api_key else None
|
|
|
|
)
|
2024-01-05 23:03:28 +00:00
|
|
|
values["openai_api_base"] = values["openai_api_base"] or os.getenv(
|
|
|
|
"OPENAI_API_BASE"
|
|
|
|
)
|
|
|
|
values["openai_api_version"] = values["openai_api_version"] or os.getenv(
|
|
|
|
"OPENAI_API_VERSION"
|
|
|
|
)
|
|
|
|
# Check OPENAI_ORGANIZATION for backwards compatibility.
|
|
|
|
values["openai_organization"] = (
|
|
|
|
values["openai_organization"]
|
|
|
|
or os.getenv("OPENAI_ORG_ID")
|
|
|
|
or os.getenv("OPENAI_ORGANIZATION")
|
|
|
|
)
|
|
|
|
values["azure_endpoint"] = values["azure_endpoint"] or os.getenv(
|
|
|
|
"AZURE_OPENAI_ENDPOINT"
|
|
|
|
)
|
2024-01-30 23:49:56 +00:00
|
|
|
azure_ad_token = values["azure_ad_token"] or os.getenv("AZURE_OPENAI_AD_TOKEN")
|
|
|
|
values["azure_ad_token"] = (
|
|
|
|
convert_to_secret_str(azure_ad_token) if azure_ad_token else None
|
2024-01-05 23:03:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
values["openai_api_type"] = get_from_dict_or_env(
|
|
|
|
values, "openai_api_type", "OPENAI_API_TYPE", default="azure"
|
|
|
|
)
|
|
|
|
values["openai_proxy"] = get_from_dict_or_env(
|
|
|
|
values, "openai_proxy", "OPENAI_PROXY", default=""
|
|
|
|
)
|
|
|
|
# For backwards compatibility. Before openai v1, no distinction was made
|
|
|
|
# between azure_endpoint and base_url (openai_api_base).
|
|
|
|
openai_api_base = values["openai_api_base"]
|
|
|
|
if openai_api_base and values["validate_base_url"]:
|
|
|
|
if "/openai" not in openai_api_base:
|
|
|
|
raise ValueError(
|
|
|
|
"As of openai>=1.0.0, Azure endpoints should be specified via "
|
|
|
|
"the `azure_endpoint` param not `openai_api_base` "
|
|
|
|
"(or alias `base_url`)."
|
|
|
|
)
|
|
|
|
if values["deployment_name"]:
|
|
|
|
raise ValueError(
|
2024-01-17 20:43:14 +00:00
|
|
|
"As of openai>=1.0.0, if `azure_deployment` (or alias "
|
|
|
|
"`deployment_name`) is specified then "
|
|
|
|
"`base_url` (or alias `openai_api_base`) should not be. "
|
|
|
|
"If specifying `azure_deployment`/`deployment_name` then use "
|
|
|
|
"`azure_endpoint` instead of `base_url`.\n\n"
|
|
|
|
"For example, you could specify:\n\n"
|
2024-02-22 17:03:16 +00:00
|
|
|
'azure_endpoint="https://xxx.openai.azure.com/", '
|
2024-03-09 00:52:55 +00:00
|
|
|
'azure_deployment="my-deployment"\n\n'
|
2024-01-17 20:43:14 +00:00
|
|
|
"Or you can equivalently specify:\n\n"
|
2024-05-22 22:21:08 +00:00
|
|
|
'base_url="https://xxx.openai.azure.com/openai/deployments/my-deployment"'
|
2024-01-05 23:03:28 +00:00
|
|
|
)
|
|
|
|
client_params = {
|
|
|
|
"api_version": values["openai_api_version"],
|
|
|
|
"azure_endpoint": values["azure_endpoint"],
|
|
|
|
"azure_deployment": values["deployment_name"],
|
2024-01-30 23:49:56 +00:00
|
|
|
"api_key": values["openai_api_key"].get_secret_value()
|
|
|
|
if values["openai_api_key"]
|
|
|
|
else None,
|
|
|
|
"azure_ad_token": values["azure_ad_token"].get_secret_value()
|
|
|
|
if values["azure_ad_token"]
|
|
|
|
else None,
|
2024-01-05 23:03:28 +00:00
|
|
|
"azure_ad_token_provider": values["azure_ad_token_provider"],
|
|
|
|
"organization": values["openai_organization"],
|
|
|
|
"base_url": values["openai_api_base"],
|
|
|
|
"timeout": values["request_timeout"],
|
|
|
|
"max_retries": values["max_retries"],
|
|
|
|
"default_headers": values["default_headers"],
|
|
|
|
"default_query": values["default_query"],
|
|
|
|
}
|
2024-03-17 00:50:22 +00:00
|
|
|
if not values.get("client"):
|
|
|
|
sync_specific = {"http_client": values["http_client"]}
|
|
|
|
values["client"] = openai.AzureOpenAI(
|
|
|
|
**client_params, **sync_specific
|
|
|
|
).chat.completions
|
|
|
|
if not values.get("async_client"):
|
|
|
|
async_specific = {"http_client": values["http_async_client"]}
|
|
|
|
values["async_client"] = openai.AsyncAzureOpenAI(
|
|
|
|
**client_params, **async_specific
|
|
|
|
).chat.completions
|
2024-01-05 23:03:28 +00:00
|
|
|
return values
|
|
|
|
|
2024-06-17 20:37:41 +00:00
|
|
|
def bind_tools(
|
|
|
|
self,
|
|
|
|
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
|
|
|
|
*,
|
|
|
|
tool_choice: Optional[
|
|
|
|
Union[dict, str, Literal["auto", "none", "required", "any"], bool]
|
|
|
|
] = None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> Runnable[LanguageModelInput, BaseMessage]:
|
|
|
|
# As of 05/2024 Azure OpenAI doesn't support tool_choice="required".
|
|
|
|
# TODO: Update this condition once tool_choice="required" is supported.
|
|
|
|
if tool_choice in ("any", "required", True):
|
|
|
|
if len(tools) > 1:
|
|
|
|
raise ValueError(
|
|
|
|
f"Azure OpenAI does not currently support {tool_choice=}. Should "
|
|
|
|
f"be one of 'auto', 'none', or the name of the tool to call."
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
tool_choice = convert_to_openai_tool(tools[0])["function"]["name"]
|
|
|
|
return super().bind_tools(tools, tool_choice=tool_choice, **kwargs)
|
|
|
|
|
2024-01-05 23:03:28 +00:00
|
|
|
@property
|
|
|
|
def _identifying_params(self) -> Dict[str, Any]:
|
|
|
|
"""Get the identifying parameters."""
|
2024-03-27 05:31:36 +00:00
|
|
|
return {
|
|
|
|
**{"azure_deployment": self.deployment_name},
|
|
|
|
**super()._identifying_params,
|
|
|
|
}
|
2024-01-05 23:03:28 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def _llm_type(self) -> str:
|
|
|
|
return "azure-openai-chat"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def lc_attributes(self) -> Dict[str, Any]:
|
|
|
|
return {
|
|
|
|
"openai_api_type": self.openai_api_type,
|
|
|
|
"openai_api_version": self.openai_api_version,
|
|
|
|
}
|
|
|
|
|
2024-05-17 17:51:26 +00:00
|
|
|
def _get_ls_params(
|
|
|
|
self, stop: Optional[List[str]] = None, **kwargs: Any
|
|
|
|
) -> LangSmithParams:
|
|
|
|
"""Get the parameters used to invoke the model."""
|
|
|
|
params = super()._get_ls_params(stop=stop, **kwargs)
|
|
|
|
params["ls_provider"] = "azure"
|
|
|
|
if self.deployment_name:
|
|
|
|
params["ls_model_name"] = self.deployment_name
|
|
|
|
return params
|
|
|
|
|
2024-02-21 00:57:34 +00:00
|
|
|
def _create_chat_result(
|
|
|
|
self, response: Union[dict, openai.BaseModel]
|
|
|
|
) -> ChatResult:
|
2024-01-05 23:03:28 +00:00
|
|
|
if not isinstance(response, dict):
|
2024-02-21 00:57:34 +00:00
|
|
|
response = response.model_dump()
|
2024-01-05 23:03:28 +00:00
|
|
|
for res in response["choices"]:
|
|
|
|
if res.get("finish_reason", None) == "content_filter":
|
|
|
|
raise ValueError(
|
|
|
|
"Azure has not provided the response due to a content filter "
|
|
|
|
"being triggered"
|
|
|
|
)
|
|
|
|
chat_result = super()._create_chat_result(response)
|
|
|
|
|
|
|
|
if "model" in response:
|
|
|
|
model = response["model"]
|
|
|
|
if self.model_version:
|
|
|
|
model = f"{model}-{self.model_version}"
|
|
|
|
|
2024-01-29 21:31:09 +00:00
|
|
|
chat_result.llm_output = chat_result.llm_output or {}
|
|
|
|
chat_result.llm_output["model_name"] = model
|
|
|
|
if "prompt_filter_results" in response:
|
|
|
|
chat_result.llm_output = chat_result.llm_output or {}
|
|
|
|
chat_result.llm_output["prompt_filter_results"] = response[
|
|
|
|
"prompt_filter_results"
|
|
|
|
]
|
|
|
|
for chat_gen, response_choice in zip(
|
|
|
|
chat_result.generations, response["choices"]
|
|
|
|
):
|
|
|
|
chat_gen.generation_info = chat_gen.generation_info or {}
|
|
|
|
chat_gen.generation_info["content_filter_results"] = response_choice.get(
|
|
|
|
"content_filter_results", {}
|
|
|
|
)
|
2024-01-05 23:03:28 +00:00
|
|
|
|
|
|
|
return chat_result
|