import logging from importlib.metadata import version from types import ModuleType from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type import openai from packaging.version import parse from tenacity import ( before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential, ) from llama_index.bridge.pydantic import BaseModel from llama_index.llms.generic_utils import get_from_param_or_env from llama_index.llms.types import ChatMessage DEFAULT_KONKO_API_TYPE = "open_ai" DEFAULT_KONKO_API_BASE = "https://api.konko.ai/v1" DEFAULT_KONKO_API_VERSION = "" MISSING_API_KEY_ERROR_MESSAGE = """No Konko API key found for LLM. E.g. to use konko Please set the KONKO_API_KEY environment variable or \ konko.api_key prior to initialization. API keys can be found or created at \ https://www.konko.ai/ """ logger = logging.getLogger(__name__) def import_konko() -> ModuleType: try: import konko return konko except ImportError: raise ImportError( "Could not import konko python package. " "Please install it with `pip install konko`." ) def is_openai_v1() -> bool: try: _version = parse(version("openai")) major_version = _version.major except AttributeError: # Handle the case where version or major attribute is not present return False return bool(major_version >= 1) def _create_retry_decorator(max_retries: int) -> Callable[[Any], Any]: min_seconds = 4 max_seconds = 10 # Wait 2^x * 1 second between each retry starting with # 4 seconds, then up to 10 seconds, then 10 seconds afterwards return retry( reraise=True, stop=stop_after_attempt(max_retries), wait=wait_exponential(multiplier=1, min=min_seconds, max=max_seconds), retry=( retry_if_exception_type(openai.APITimeoutError) | retry_if_exception_type(openai.APIError) | retry_if_exception_type(openai.APIConnectionError) | retry_if_exception_type(openai.RateLimitError) | retry_if_exception_type(openai.APIStatusError) ), before_sleep=before_sleep_log(logger, logging.WARNING), ) def completion_with_retry(is_chat_model: bool, max_retries: int, **kwargs: Any) -> Any: """Use tenacity to retry the completion call.""" retry_decorator = _create_retry_decorator(max_retries=max_retries) @retry_decorator def _completion_with_retry(**kwargs: Any) -> Any: client = get_completion_endpoint(is_chat_model) return client.create(**kwargs) return _completion_with_retry(**kwargs) def get_completion_endpoint(is_chat_model: bool) -> Any: """ Get the appropriate completion endpoint based on the model type and API version. Args: - is_chat_model (bool): A flag indicating whether the model is a chat model. Returns: - The appropriate completion endpoint based on the model type and API version. Raises: - NotImplementedError: If the combination of is_chat_model and API version is not supported. """ konko = import_konko() # For OpenAI version 1 if is_openai_v1(): return konko.chat.completions if is_chat_model else konko.completions # For other versions if not is_openai_v1(): return konko.ChatCompletion if is_chat_model else konko.Completion # Raise error if the combination of is_chat_model and API version is not covered raise NotImplementedError( "The combination of model type and API version is not supported." ) def to_openai_message_dict(message: ChatMessage) -> dict: """Convert generic message to OpenAI message dict.""" message_dict = { "role": message.role, "content": message.content, } message_dict.update(message.additional_kwargs) return message_dict def to_openai_message_dicts(messages: Sequence[ChatMessage]) -> List[dict]: """Convert generic messages to OpenAI message dicts.""" return [to_openai_message_dict(message) for message in messages] def from_openai_message_dict(message_dict: Any) -> ChatMessage: """Convert openai message dict to generic message.""" if is_openai_v1(): # Handling for OpenAI version 1 role = message_dict.role content = message_dict.content additional_kwargs = { attr: getattr(message_dict, attr) for attr in dir(message_dict) if not attr.startswith("_") and attr not in ["role", "content"] } else: # Handling for OpenAI version 0 role = message_dict.get("role") content = message_dict.get("content", None) additional_kwargs = { key: value for key, value in message_dict.items() if key not in ["role", "content"] } return ChatMessage(role=role, content=content, additional_kwargs=additional_kwargs) def from_openai_message_dicts(message_dicts: Sequence[dict]) -> List[ChatMessage]: """Convert openai message dicts to generic messages.""" return [from_openai_message_dict(message_dict) for message_dict in message_dicts] def to_openai_function(pydantic_class: Type[BaseModel]) -> Dict[str, Any]: """Convert pydantic class to OpenAI function.""" schema = pydantic_class.schema() return { "name": schema["title"], "description": schema["description"], "parameters": pydantic_class.schema(), } def resolve_konko_credentials( konko_api_key: Optional[str] = None, openai_api_key: Optional[str] = None, api_type: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, ) -> Tuple[str, str, str, str, str]: """ "Resolve KonkoAI credentials. The order of precedence is: 1. param 2. env 3. konkoai module 4. default """ konko = import_konko() # resolve from param or env konko_api_key = get_from_param_or_env( "konko_api_key", konko_api_key, "KONKO_API_KEY", "" ) openai_api_key = get_from_param_or_env( "openai_api_key", openai_api_key, "OPENAI_API_KEY", "" ) api_type = get_from_param_or_env("api_type", api_type, "KONKO_API_TYPE", "") api_base = DEFAULT_KONKO_API_BASE api_version = get_from_param_or_env( "api_version", api_version, "KONKO_API_VERSION", "" ) # resolve from konko module or default konko_api_key = konko_api_key openai_api_key = openai_api_key api_type = api_type or DEFAULT_KONKO_API_TYPE api_base = api_base or konko.api_base or DEFAULT_KONKO_API_BASE api_version = api_version or DEFAULT_KONKO_API_VERSION if not konko_api_key: raise ValueError(MISSING_API_KEY_ERROR_MESSAGE) return konko_api_key, openai_api_key, api_type, api_base, api_version async def acompletion_with_retry( is_chat_model: bool, max_retries: int, **kwargs: Any ) -> Any: """Use tenacity to retry the async completion call.""" konko = import_konko() retry_decorator = _create_retry_decorator(max_retries=max_retries) @retry_decorator async def _completion_with_retry(**kwargs: Any) -> Any: if is_chat_model: if is_openai_v1(): return await konko.AsyncKonko().chat.completions.create(**kwargs) else: return await konko.ChatCompletion.acreate(**kwargs) else: if is_openai_v1(): return await konko.AsyncKonko().completions.create(**kwargs) else: return await konko.Completion.acreate(**kwargs) return await _completion_with_retry(**kwargs)