llama-index
383 строки · 12.0 Кб
1import logging2import os3import time4from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union5
6import openai7from deprecated import deprecated8from openai.types.chat import ChatCompletionMessageParam, ChatCompletionMessageToolCall9from openai.types.chat.chat_completion_chunk import ChoiceDeltaToolCall10from openai.types.chat.chat_completion_message import ChatCompletionMessage11from tenacity import (12before_sleep_log,13retry,14retry_if_exception_type,15stop_after_attempt,16stop_after_delay,17wait_exponential,18wait_random_exponential,19)
20from tenacity.stop import stop_base21
22from llama_index.legacy.bridge.pydantic import BaseModel23from llama_index.legacy.core.llms.types import ChatMessage24from llama_index.legacy.llms.generic_utils import get_from_param_or_env25
26DEFAULT_OPENAI_API_TYPE = "open_ai"27DEFAULT_OPENAI_API_BASE = "https://api.openai.com/v1"28DEFAULT_OPENAI_API_VERSION = ""29
30
31GPT4_MODELS: Dict[str, int] = {32# stable model names:33# resolves to gpt-4-0314 before 2023-06-27,34# resolves to gpt-4-0613 after35"gpt-4": 8192,36"gpt-4-32k": 32768,37# turbo models (Turbo, JSON mode)38"gpt-4-1106-preview": 128000,39"gpt-4-0125-preview": 128000,40"gpt-4-turbo-preview": 128000,41# multimodal model42"gpt-4-vision-preview": 128000,43# 0613 models (function calling):44# https://openai.com/blog/function-calling-and-other-api-updates45"gpt-4-0613": 8192,46"gpt-4-32k-0613": 32768,47# 0314 models48"gpt-4-0314": 8192,49"gpt-4-32k-0314": 32768,50}
51
52AZURE_TURBO_MODELS: Dict[str, int] = {53"gpt-35-turbo-16k": 16384,54"gpt-35-turbo": 4096,55# 1106 model (JSON mode)56"gpt-35-turbo-1106": 16384,57# 0613 models (function calling):58"gpt-35-turbo-0613": 4096,59"gpt-35-turbo-16k-0613": 16384,60}
61
62TURBO_MODELS: Dict[str, int] = {63# stable model names:64# resolves to gpt-3.5-turbo-0301 before 2023-06-27,65# resolves to gpt-3.5-turbo-0613 until 2023-12-11,66# resolves to gpt-3.5-turbo-1106 after67"gpt-3.5-turbo": 4096,68# resolves to gpt-3.5-turbo-16k-0613 until 2023-12-1169# resolves to gpt-3.5-turbo-1106 after70"gpt-3.5-turbo-16k": 16384,71# 0125 (2024) model (JSON mode)72"gpt-3.5-turbo-0125": 16385,73# 1106 model (JSON mode)74"gpt-3.5-turbo-1106": 16384,75# 0613 models (function calling):76# https://openai.com/blog/function-calling-and-other-api-updates77"gpt-3.5-turbo-0613": 4096,78"gpt-3.5-turbo-16k-0613": 16384,79# 0301 models80"gpt-3.5-turbo-0301": 4096,81}
82
83GPT3_5_MODELS: Dict[str, int] = {84"text-davinci-003": 4097,85"text-davinci-002": 4097,86# instruct models87"gpt-3.5-turbo-instruct": 4096,88}
89
90GPT3_MODELS: Dict[str, int] = {91"text-ada-001": 2049,92"text-babbage-001": 2040,93"text-curie-001": 2049,94"ada": 2049,95"babbage": 2049,96"curie": 2049,97"davinci": 2049,98}
99
100ALL_AVAILABLE_MODELS = {101**GPT4_MODELS,102**TURBO_MODELS,103**GPT3_5_MODELS,104**GPT3_MODELS,105**AZURE_TURBO_MODELS,106}
107
108CHAT_MODELS = {109**GPT4_MODELS,110**TURBO_MODELS,111**AZURE_TURBO_MODELS,112}
113
114
115DISCONTINUED_MODELS = {116"code-davinci-002": 8001,117"code-davinci-001": 8001,118"code-cushman-002": 2048,119"code-cushman-001": 2048,120}
121
122MISSING_API_KEY_ERROR_MESSAGE = """No API key found for OpenAI.123Please set either the OPENAI_API_KEY environment variable or \
124openai.api_key prior to initialization.
125API keys can be found or created at \
126https://platform.openai.com/account/api-keys
127"""
128
129logger = logging.getLogger(__name__)130
131OpenAIToolCall = Union[ChatCompletionMessageToolCall, ChoiceDeltaToolCall]132
133
134def create_retry_decorator(135max_retries: int,136random_exponential: bool = False,137stop_after_delay_seconds: Optional[float] = None,138min_seconds: float = 4,139max_seconds: float = 10,140) -> Callable[[Any], Any]:141wait_strategy = (142wait_random_exponential(min=min_seconds, max=max_seconds)143if random_exponential144else wait_exponential(multiplier=1, min=min_seconds, max=max_seconds)145)146
147stop_strategy: stop_base = stop_after_attempt(max_retries)148if stop_after_delay_seconds is not None:149stop_strategy = stop_strategy | stop_after_delay(stop_after_delay_seconds)150
151return retry(152reraise=True,153stop=stop_strategy,154wait=wait_strategy,155retry=(156retry_if_exception_type(157(158openai.APITimeoutError,159openai.APIError,160openai.APIConnectionError,161openai.RateLimitError,162openai.APIStatusError,163)164)165),166before_sleep=before_sleep_log(logger, logging.WARNING),167)168
169
170def openai_modelname_to_contextsize(modelname: str) -> int:171"""Calculate the maximum number of tokens possible to generate for a model.172
173Args:
174modelname: The modelname we want to know the context size for.
175
176Returns:
177The maximum context size
178
179Example:
180.. code-block:: python
181
182max_tokens = openai.modelname_to_contextsize("text-davinci-003")
183
184Modified from:
185https://github.com/hwchase17/langchain/blob/master/langchain/llms/openai.py
186"""
187# handling finetuned models188if modelname.startswith("ft:"):189modelname = modelname.split(":")[1]190elif ":ft-" in modelname: # legacy fine-tuning191modelname = modelname.split(":")[0]192
193if modelname in DISCONTINUED_MODELS:194raise ValueError(195f"OpenAI model {modelname} has been discontinued. "196"Please choose another model."197)198if modelname not in ALL_AVAILABLE_MODELS:199raise ValueError(200f"Unknown model {modelname!r}. Please provide a valid OpenAI model name in:"201f" {', '.join(ALL_AVAILABLE_MODELS.keys())}"202)203return ALL_AVAILABLE_MODELS[modelname]204
205
206def is_chat_model(model: str) -> bool:207return model in CHAT_MODELS208
209
210def is_function_calling_model(model: str) -> bool:211is_chat_model_ = is_chat_model(model)212is_old = "0314" in model or "0301" in model213return is_chat_model_ and not is_old214
215
216def to_openai_message_dict(217message: ChatMessage, drop_none: bool = False218) -> ChatCompletionMessageParam:219"""Convert generic message to OpenAI message dict."""220message_dict = {221"role": message.role.value,222"content": message.content,223}224
225# NOTE: openai messages have additional arguments:226# - function messages have `name`227# - assistant messages have optional `function_call`228message_dict.update(message.additional_kwargs)229
230null_keys = [key for key, value in message_dict.items() if value is None]231# if drop_none is True, remove keys with None values232if drop_none:233for key in null_keys:234message_dict.pop(key)235
236return message_dict # type: ignore237
238
239def to_openai_message_dicts(240messages: Sequence[ChatMessage], drop_none: bool = False241) -> List[ChatCompletionMessageParam]:242"""Convert generic messages to OpenAI message dicts."""243return [244to_openai_message_dict(message, drop_none=drop_none) for message in messages245]246
247
248def from_openai_message(openai_message: ChatCompletionMessage) -> ChatMessage:249"""Convert openai message dict to generic message."""250role = openai_message.role251# NOTE: Azure OpenAI returns function calling messages without a content key252content = openai_message.content253
254function_call = None # deprecated in OpenAI v 1.1.0255
256additional_kwargs: Dict[str, Any] = {}257if openai_message.tool_calls is not None:258tool_calls: List[ChatCompletionMessageToolCall] = openai_message.tool_calls259additional_kwargs.update(tool_calls=tool_calls)260
261return ChatMessage(role=role, content=content, additional_kwargs=additional_kwargs)262
263
264def from_openai_messages(265openai_messages: Sequence[ChatCompletionMessage],266) -> List[ChatMessage]:267"""Convert openai message dicts to generic messages."""268return [from_openai_message(message) for message in openai_messages]269
270
271def from_openai_message_dict(message_dict: dict) -> ChatMessage:272"""Convert openai message dict to generic message."""273role = message_dict["role"]274# NOTE: Azure OpenAI returns function calling messages without a content key275content = message_dict.get("content", None)276
277additional_kwargs = message_dict.copy()278additional_kwargs.pop("role")279additional_kwargs.pop("content", None)280
281return ChatMessage(role=role, content=content, additional_kwargs=additional_kwargs)282
283
284def from_openai_message_dicts(message_dicts: Sequence[dict]) -> List[ChatMessage]:285"""Convert openai message dicts to generic messages."""286return [from_openai_message_dict(message_dict) for message_dict in message_dicts]287
288
289@deprecated("Deprecated in favor of `to_openai_tool`, which should be used instead.")290def to_openai_function(pydantic_class: Type[BaseModel]) -> Dict[str, Any]:291"""Deprecated in favor of `to_openai_tool`.292
293Convert pydantic class to OpenAI function.
294"""
295return to_openai_tool(pydantic_class, description=None)296
297
298def to_openai_tool(299pydantic_class: Type[BaseModel], description: Optional[str] = None300) -> Dict[str, Any]:301"""Convert pydantic class to OpenAI tool."""302schema = pydantic_class.schema()303schema_description = schema.get("description", None) or description304function = {305"name": schema["title"],306"description": schema_description,307"parameters": pydantic_class.schema(),308}309return {"type": "function", "function": function}310
311
312def resolve_openai_credentials(313api_key: Optional[str] = None,314api_base: Optional[str] = None,315api_version: Optional[str] = None,316) -> Tuple[Optional[str], str, str]:317""" "Resolve OpenAI credentials.318
319The order of precedence is:
3201. param
3212. env
3223. openai module
3234. default
324"""
325# resolve from param or env326api_key = get_from_param_or_env("api_key", api_key, "OPENAI_API_KEY", "")327api_base = get_from_param_or_env("api_base", api_base, "OPENAI_API_BASE", "")328api_version = get_from_param_or_env(329"api_version", api_version, "OPENAI_API_VERSION", ""330)331
332# resolve from openai module or default333final_api_key = api_key or openai.api_key or ""334final_api_base = api_base or openai.base_url or DEFAULT_OPENAI_API_BASE335final_api_version = api_version or openai.api_version or DEFAULT_OPENAI_API_VERSION336
337return final_api_key, str(final_api_base), final_api_version338
339
340def refresh_openai_azuread_token(341azure_ad_token: Any = None,342) -> Any:343"""344Checks the validity of the associated token, if any, and tries to refresh it
345using the credentials available in the current context. Different authentication
346methods are tried, in order, until a successful one is found as defined at the
347package `azure-indentity`.
348"""
349try:350from azure.core.exceptions import ClientAuthenticationError351from azure.identity import DefaultAzureCredential352except ImportError as ex:353raise ValueError(354"Using API type `azure_ad` or `azuread` requires the package"355" `azure-identity` to be installed."356) from ex357
358if not azure_ad_token or azure_ad_token.expires_on < time.time() + 60:359try:360credential = DefaultAzureCredential()361azure_ad_token = credential.get_token(362"https://cognitiveservices.azure.com/.default"363)364except ClientAuthenticationError as err:365raise ValueError(366"Unable to acquire a valid Microsoft Entra ID (former Azure AD) token for "367f"the resource due to the following error: {err.message}"368) from err369return azure_ad_token370
371
372def resolve_from_aliases(*args: Optional[str]) -> Optional[str]:373for arg in args:374if arg is not None:375return arg376return None377
378
379def validate_openai_api_key(api_key: Optional[str] = None) -> None:380openai_api_key = api_key or os.environ.get("OPENAI_API_KEY", "")381
382if not openai_api_key:383raise ValueError(MISSING_API_KEY_ERROR_MESSAGE)384