aphilippov/python-server-api
0
1import json2import logging3import os4import tempfile5from pathlib import Path6from typing import Any, Dict, List, Optional, Set, Union7 8from dotenv import find_dotenv, load_dotenv9 10try:11 from openai import OpenAI12 from openai.types.beta.assistant import Assistant13 14 ERROR = None15except ImportError:16 ERROR = ImportError("Please install openai>=1 to use autogen.OpenAIWrapper.")17 OpenAI = object18 Assistant = object19 20NON_CACHE_KEY = ["api_key", "base_url", "api_type", "api_version"]21DEFAULT_AZURE_API_VERSION = "2023-08-01-preview"22OAI_PRICE1K = {23 "text-ada-001": 0.0004,24 "text-babbage-001": 0.0005,25 "text-curie-001": 0.002,26 "code-cushman-001": 0.024,27 "code-davinci-002": 0.1,28 "text-davinci-002": 0.02,29 "text-davinci-003": 0.02,30 "gpt-3.5-turbo-instruct": (0.0015, 0.002),31 "gpt-3.5-turbo-0301": (0.0015, 0.002), # deprecate in Sep32 "gpt-3.5-turbo-0613": (0.0015, 0.002),33 "gpt-3.5-turbo-16k": (0.003, 0.004),34 "gpt-3.5-turbo-16k-0613": (0.003, 0.004),35 "gpt-35-turbo": (0.0015, 0.002),36 "gpt-35-turbo-16k": (0.003, 0.004),37 "gpt-35-turbo-instruct": (0.0015, 0.002),38 "gpt-4": (0.03, 0.06),39 "gpt-4-32k": (0.06, 0.12),40 "gpt-4-0314": (0.03, 0.06), # deprecate in Sep41 "gpt-4-32k-0314": (0.06, 0.12), # deprecate in Sep42 "gpt-4-0613": (0.03, 0.06),43 "gpt-4-32k-0613": (0.06, 0.12),44 # 11-0645 "gpt-3.5-turbo": (0.001, 0.002),46 "gpt-3.5-turbo-1106": (0.001, 0.002),47 "gpt-35-turbo-1106": (0.001, 0.002),48 "gpt-4-1106-preview": (0.01, 0.03),49 "gpt-4-1106-vision-preview": (0.01, 0.03), # TODO: support vision pricing of images50}51 52 53def get_key(config: Dict[str, Any]) -> str:54 """Get a unique identifier of a configuration.55 56 Args:57 config (dict or list): A configuration.58 59 Returns:60 tuple: A unique identifier which can be used as a key for a dict.61 """62 copied = False63 for key in NON_CACHE_KEY:64 if key in config:65 config, copied = config.copy() if not copied else config, True66 config.pop(key)67 # if isinstance(config, dict):68 # return tuple(get_key(x) for x in sorted(config.items()))69 # if isinstance(config, list):70 # return tuple(get_key(x) for x in config)71 # return config72 return json.dumps(config, sort_keys=True)73 74 75def get_config_list(76 api_keys: List, base_urls: Optional[List] = None, api_type: Optional[str] = None, api_version: Optional[str] = None77) -> List[Dict]:78 """Get a list of configs for OpenAI API client.79 80 Args:81 api_keys (list): The api keys for openai api calls.82 base_urls (list, optional): The api bases for openai api calls. If provided, should match the length of api_keys.83 api_type (str, optional): The api type for openai api calls.84 api_version (str, optional): The api version for openai api calls.85 86 Returns:87 list: A list of configs for OepnAI API calls.88 89 Example:90 ```91 # Define a list of API keys92 api_keys = ['key1', 'key2', 'key3']93 94 # Optionally, define a list of base URLs corresponding to each API key95 base_urls = ['https://api.service1.com', 'https://api.service2.com', 'https://api.service3.com']96 97 # Optionally, define the API type and version if they are common for all keys98 api_type = 'azure'99 api_version = '2023-08-01-preview'100 101 # Call the get_config_list function to get a list of configuration dictionaries102 config_list = get_config_list(api_keys, base_urls, api_type, api_version)103 ```104 105 """106 if base_urls is not None:107 assert len(api_keys) == len(base_urls), "The length of api_keys must match the length of base_urls"108 config_list = []109 for i, api_key in enumerate(api_keys):110 if not api_key.strip():111 continue112 config = {"api_key": api_key}113 if base_urls:114 config["base_url"] = base_urls[i]115 if api_type:116 config["api_type"] = api_type117 if api_version:118 config["api_version"] = api_version119 config_list.append(config)120 return config_list121 122 123def config_list_openai_aoai(124 key_file_path: Optional[str] = ".",125 openai_api_key_file: Optional[str] = "key_openai.txt",126 aoai_api_key_file: Optional[str] = "key_aoai.txt",127 openai_api_base_file: Optional[str] = "base_openai.txt",128 aoai_api_base_file: Optional[str] = "base_aoai.txt",129 exclude: Optional[str] = None,130) -> List[Dict]:131 """Get a list of configs for OpenAI API client (including Azure or local model deployments that support OpenAI's chat completion API).132 133 This function constructs configurations by reading API keys and base URLs from environment variables or text files.134 It supports configurations for both OpenAI and Azure OpenAI services, allowing for the exclusion of one or the other.135 When text files are used, the environment variables will be overwritten.136 To prevent text files from being used, set the corresponding file name to None.137 Or set key_file_path to None to disallow reading from text files.138 139 Args:140 key_file_path (str, optional): The directory path where the API key files are located. Defaults to the current directory.141 openai_api_key_file (str, optional): The filename containing the OpenAI API key. Defaults to 'key_openai.txt'.142 aoai_api_key_file (str, optional): The filename containing the Azure OpenAI API key. Defaults to 'key_aoai.txt'.143 openai_api_base_file (str, optional): The filename containing the OpenAI API base URL. Defaults to 'base_openai.txt'.144 aoai_api_base_file (str, optional): The filename containing the Azure OpenAI API base URL. Defaults to 'base_aoai.txt'.145 exclude (str, optional): The API type to exclude from the configuration list. Can be 'openai' or 'aoai'. Defaults to None.146 147 Returns:148 List[Dict]: A list of configuration dictionaries. Each dictionary contains keys for 'api_key',149 and optionally 'base_url', 'api_type', and 'api_version'.150 151 Raises:152 FileNotFoundError: If the specified key files are not found and the corresponding API key is not set in the environment variables.153 154 Example:155 # To generate configurations excluding Azure OpenAI:156 configs = config_list_openai_aoai(exclude='aoai')157 158 File samples:159 - key_aoai.txt160 161 ```162 aoai-12345abcdef67890ghijklmnopqr163 aoai-09876zyxwvuts54321fedcba164 ```165 166 - base_aoai.txt167 168 ```169 https://api.azure.com/v1170 https://api.azure2.com/v1171 ```172 173 Notes:174 - The function checks for API keys and base URLs in the following environment variables: 'OPENAI_API_KEY', 'AZURE_OPENAI_API_KEY',175 'OPENAI_API_BASE' and 'AZURE_OPENAI_API_BASE'. If these are not found, it attempts to read from the specified files in the176 'key_file_path' directory.177 - The API version for Azure configurations is set to DEFAULT_AZURE_API_VERSION by default.178 - If 'exclude' is set to 'openai', only Azure OpenAI configurations are returned, and vice versa.179 - The function assumes that the API keys and base URLs in the environment variables are separated by new lines if there are180 multiple entries.181 """182 if exclude != "openai" and key_file_path is not None:183 # skip if key_file_path is None184 if openai_api_key_file is not None:185 # skip if openai_api_key_file is None186 try:187 with open(f"{key_file_path}/{openai_api_key_file}") as key_file:188 os.environ["OPENAI_API_KEY"] = key_file.read().strip()189 except FileNotFoundError:190 logging.info(191 "OPENAI_API_KEY is not found in os.environ "192 "and key_openai.txt is not found in the specified path. You can specify the api_key in the config_list."193 )194 if openai_api_base_file is not None:195 # skip if openai_api_base_file is None196 try:197 with open(f"{key_file_path}/{openai_api_base_file}") as key_file:198 os.environ["OPENAI_API_BASE"] = key_file.read().strip()199 except FileNotFoundError:200 logging.info(201 "OPENAI_API_BASE is not found in os.environ "202 "and base_openai.txt is not found in the specified path. You can specify the base_url in the config_list."203 )204 if exclude != "aoai" and key_file_path is not None:205 # skip if key_file_path is None206 if aoai_api_key_file is not None:207 try:208 with open(f"{key_file_path}/{aoai_api_key_file}") as key_file:209 os.environ["AZURE_OPENAI_API_KEY"] = key_file.read().strip()210 except FileNotFoundError:211 logging.info(212 "AZURE_OPENAI_API_KEY is not found in os.environ "213 "and key_aoai.txt is not found in the specified path. You can specify the api_key in the config_list."214 )215 if aoai_api_base_file is not None:216 try:217 with open(f"{key_file_path}/{aoai_api_base_file}") as key_file:218 os.environ["AZURE_OPENAI_API_BASE"] = key_file.read().strip()219 except FileNotFoundError:220 logging.info(221 "AZURE_OPENAI_API_BASE is not found in os.environ "222 "and base_aoai.txt is not found in the specified path. You can specify the base_url in the config_list."223 )224 aoai_config = (225 get_config_list(226 # Assuming Azure OpenAI api keys in os.environ["AZURE_OPENAI_API_KEY"], in separated lines227 api_keys=os.environ.get("AZURE_OPENAI_API_KEY", "").split("\n"),228 # Assuming Azure OpenAI api bases in os.environ["AZURE_OPENAI_API_BASE"], in separated lines229 base_urls=os.environ.get("AZURE_OPENAI_API_BASE", "").split("\n"),230 api_type="azure",231 api_version=DEFAULT_AZURE_API_VERSION,232 )233 if exclude != "aoai"234 else []235 )236 # process openai base urls237 base_urls = os.environ.get("OPENAI_API_BASE", None)238 base_urls = base_urls if base_urls is None else base_urls.split("\n")239 openai_config = (240 get_config_list(241 # Assuming OpenAI API_KEY in os.environ["OPENAI_API_KEY"]242 api_keys=os.environ.get("OPENAI_API_KEY", "").split("\n"),243 base_urls=base_urls,244 )245 if exclude != "openai"246 else []247 )248 config_list = openai_config + aoai_config249 return config_list250 251 252def config_list_from_models(253 key_file_path: Optional[str] = ".",254 openai_api_key_file: Optional[str] = "key_openai.txt",255 aoai_api_key_file: Optional[str] = "key_aoai.txt",256 aoai_api_base_file: Optional[str] = "base_aoai.txt",257 exclude: Optional[str] = None,258 model_list: Optional[list] = None,259) -> List[Dict]:260 """261 Get a list of configs for API calls with models specified in the model list.262 263 This function extends `config_list_openai_aoai` by allowing to clone its' out for each of the models provided.264 Each configuration will have a 'model' key with the model name as its value. This is particularly useful when265 all endpoints have same set of models.266 267 Args:268 key_file_path (str, optional): The path to the key files.269 openai_api_key_file (str, optional): The file name of the OpenAI API key.270 aoai_api_key_file (str, optional): The file name of the Azure OpenAI API key.271 aoai_api_base_file (str, optional): The file name of the Azure OpenAI API base.272 exclude (str, optional): The API type to exclude, "openai" or "aoai".273 model_list (list, optional): The list of model names to include in the configs.274 275 Returns:276 list: A list of configs for OpenAI API calls, each including model information.277 278 Example:279 ```280 # Define the path where the API key files are located281 key_file_path = '/path/to/key/files'282 283 # Define the file names for the OpenAI and Azure OpenAI API keys and bases284 openai_api_key_file = 'key_openai.txt'285 aoai_api_key_file = 'key_aoai.txt'286 aoai_api_base_file = 'base_aoai.txt'287 288 # Define the list of models for which to create configurations289 model_list = ['gpt-4', 'gpt-3.5-turbo']290 291 # Call the function to get a list of configuration dictionaries292 config_list = config_list_from_models(293 key_file_path=key_file_path,294 openai_api_key_file=openai_api_key_file,295 aoai_api_key_file=aoai_api_key_file,296 aoai_api_base_file=aoai_api_base_file,297 model_list=model_list298 )299 300 # The `config_list` will contain configurations for the specified models, for example:301 # [302 # {'api_key': '...', 'base_url': 'https://api.openai.com', 'model': 'gpt-4'},303 # {'api_key': '...', 'base_url': 'https://api.openai.com', 'model': 'gpt-3.5-turbo'}304 # ]305 ```306 """307 config_list = config_list_openai_aoai(308 key_file_path,309 openai_api_key_file,310 aoai_api_key_file,311 aoai_api_base_file,312 exclude,313 )314 if model_list:315 config_list = [{**config, "model": model} for model in model_list for config in config_list]316 return config_list317 318 319def config_list_gpt4_gpt35(320 key_file_path: Optional[str] = ".",321 openai_api_key_file: Optional[str] = "key_openai.txt",322 aoai_api_key_file: Optional[str] = "key_aoai.txt",323 aoai_api_base_file: Optional[str] = "base_aoai.txt",324 exclude: Optional[str] = None,325) -> List[Dict]:326 """Get a list of configs for 'gpt-4' followed by 'gpt-3.5-turbo' API calls.327 328 Args:329 key_file_path (str, optional): The path to the key files.330 openai_api_key_file (str, optional): The file name of the openai api key.331 aoai_api_key_file (str, optional): The file name of the azure openai api key.332 aoai_api_base_file (str, optional): The file name of the azure openai api base.333 exclude (str, optional): The api type to exclude, "openai" or "aoai".334 335 Returns:336 list: A list of configs for openai api calls.337 """338 return config_list_from_models(339 key_file_path,340 openai_api_key_file,341 aoai_api_key_file,342 aoai_api_base_file,343 exclude,344 model_list=["gpt-4", "gpt-3.5-turbo"],345 )346 347 348def filter_config(config_list, filter_dict):349 """350 This function filters `config_list` by checking each configuration dictionary against the351 criteria specified in `filter_dict`. A configuration dictionary is retained if for every352 key in `filter_dict`, see example below.353 354 Args:355 config_list (list of dict): A list of configuration dictionaries to be filtered.356 filter_dict (dict): A dictionary representing the filter criteria, where each key is a357 field name to check within the configuration dictionaries, and the358 corresponding value is a list of acceptable values for that field.359 360 Returns:361 list of dict: A list of configuration dictionaries that meet all the criteria specified362 in `filter_dict`.363 364 Example:365 ```366 # Example configuration list with various models and API types367 configs = [368 {'model': 'gpt-3.5-turbo'},369 {'model': 'gpt-4'},370 {'model': 'gpt-3.5-turbo', 'api_type': 'azure'},371 ]372 373 # Define filter criteria to select configurations for the 'gpt-3.5-turbo' model374 # that are also using the 'azure' API type375 filter_criteria = {376 'model': ['gpt-3.5-turbo'], # Only accept configurations for 'gpt-3.5-turbo'377 'api_type': ['azure'] # Only accept configurations for 'azure' API type378 }379 380 # Apply the filter to the configuration list381 filtered_configs = filter_config(configs, filter_criteria)382 383 # The resulting `filtered_configs` will be:384 # [{'model': 'gpt-3.5-turbo', 'api_type': 'azure', ...}]385 ```386 387 Note:388 - If `filter_dict` is empty or None, no filtering is applied and `config_list` is returned as is.389 - If a configuration dictionary in `config_list` does not contain a key specified in `filter_dict`,390 it is considered a non-match and is excluded from the result.391 - If the list of acceptable values for a key in `filter_dict` includes None, then configuration392 dictionaries that do not have that key will also be considered a match.393 """394 if filter_dict:395 config_list = [396 config for config in config_list if all(config.get(key) in value for key, value in filter_dict.items())397 ]398 return config_list399 400 401def config_list_from_json(402 env_or_file: str,403 file_location: Optional[str] = "",404 filter_dict: Optional[Dict[str, Union[List[Union[str, None]], Set[Union[str, None]]]]] = None,405) -> List[Dict]:406 """407 Retrieves a list of API configurations from a JSON stored in an environment variable or a file.408 409 This function attempts to parse JSON data from the given `env_or_file` parameter. If `env_or_file` is an410 environment variable containing JSON data, it will be used directly. Otherwise, it is assumed to be a filename,411 and the function will attempt to read the file from the specified `file_location`.412 413 The `filter_dict` parameter allows for filtering the configurations based on specified criteria. Each key in the414 `filter_dict` corresponds to a field in the configuration dictionaries, and the associated value is a list or set415 of acceptable values for that field. If a field is missing in a configuration and `None` is included in the list416 of acceptable values for that field, the configuration will still be considered a match.417 418 Args:419 env_or_file (str): The name of the environment variable, the filename, or the environment variable of the filename420 that containing the JSON data.421 file_location (str, optional): The directory path where the file is located, if `env_or_file` is a filename.422 filter_dict (dict, optional): A dictionary specifying the filtering criteria for the configurations, with423 keys representing field names and values being lists or sets of acceptable values for those fields.424 425 Example:426 ```427 # Suppose we have an environment variable 'CONFIG_JSON' with the following content:428 # '[{"model": "gpt-3.5-turbo", "api_type": "azure"}, {"model": "gpt-4"}]'429 430 # We can retrieve a filtered list of configurations like this:431 filter_criteria = {"model": ["gpt-3.5-turbo"]}432 configs = config_list_from_json('CONFIG_JSON', filter_dict=filter_criteria)433 # The 'configs' variable will now contain only the configurations that match the filter criteria.434 ```435 436 Returns:437 List[Dict]: A list of configuration dictionaries that match the filtering criteria specified in `filter_dict`.438 439 Raises:440 FileNotFoundError: if env_or_file is neither found as an environment variable nor a file441 """442 env_str = os.environ.get(env_or_file)443 444 if env_str:445 # The environment variable exists. We should use information from it.446 if os.path.exists(env_str):447 # It is a file location, and we need to load the json from the file.448 with open(env_str, "r") as file:449 json_str = file.read()450 else:451 # Else, it should be a JSON string by itself.452 json_str = env_str453 config_list = json.loads(json_str)454 else:455 # The environment variable does not exist.456 # So, `env_or_file` is a filename. We should use the file location.457 config_list_path = os.path.join(file_location, env_or_file)458 with open(config_list_path) as json_file:459 config_list = json.load(json_file)460 return filter_config(config_list, filter_dict)461 462 463def get_config(464 api_key: str, base_url: Optional[str] = None, api_type: Optional[str] = None, api_version: Optional[str] = None465) -> Dict:466 """467 Constructs a configuration dictionary for a single model with the provided API configurations.468 469 Example:470 ```471 config = get_config(472 api_key="sk-abcdef1234567890",473 base_url="https://api.openai.com",474 api_version="v1"475 )476 # The 'config' variable will now contain:477 # {478 # "api_key": "sk-abcdef1234567890",479 # "base_url": "https://api.openai.com",480 # "api_version": "v1"481 # }482 ```483 484 Args:485 api_key (str): The API key for authenticating API requests.486 base_url (Optional[str]): The base URL of the API. If not provided, defaults to None.487 api_type (Optional[str]): The type of API. If not provided, defaults to None.488 api_version (Optional[str]): The version of the API. If not provided, defaults to None.489 490 Returns:491 Dict: A dictionary containing the provided API configurations.492 """493 config = {"api_key": api_key}494 if base_url:495 config["base_url"] = base_url496 if api_type:497 config["api_type"] = api_type498 if api_version:499 config["api_version"] = api_version500 return config501 502 503def config_list_from_dotenv(504 dotenv_file_path: Optional[str] = None, model_api_key_map: Optional[dict] = None, filter_dict: Optional[dict] = None505) -> List[Dict[str, Union[str, Set[str]]]]:506 """507 Load API configurations from a specified .env file or environment variables and construct a list of configurations.508 509 This function will:510 - Load API keys from a provided .env file or from existing environment variables.511 - Create a configuration dictionary for each model using the API keys and additional configurations.512 - Filter and return the configurations based on provided filters.513 514 model_api_key_map will default to `{"gpt-4": "OPENAI_API_KEY", "gpt-3.5-turbo": "OPENAI_API_KEY"}` if none515 516 Args:517 dotenv_file_path (str, optional): The path to the .env file. Defaults to None.518 model_api_key_map (str/dict, optional): A dictionary mapping models to their API key configurations.519 If a string is provided as configuration, it is considered as an environment520 variable name storing the API key.521 If a dict is provided, it should contain at least 'api_key_env_var' key,522 and optionally other API configurations like 'base_url', 'api_type', and 'api_version'.523 Defaults to a basic map with 'gpt-4' and 'gpt-3.5-turbo' mapped to 'OPENAI_API_KEY'.524 filter_dict (dict, optional): A dictionary containing the models to be loaded.525 Containing a 'model' key mapped to a set of model names to be loaded.526 Defaults to None, which loads all found configurations.527 528 Returns:529 List[Dict[str, Union[str, Set[str]]]]: A list of configuration dictionaries for each model.530 531 Raises:532 FileNotFoundError: If the specified .env file does not exist.533 TypeError: If an unsupported type of configuration is provided in model_api_key_map.534 """535 if dotenv_file_path:536 dotenv_path = Path(dotenv_file_path)537 if dotenv_path.exists():538 load_dotenv(dotenv_path)539 else:540 logging.warning(f"The specified .env file {dotenv_path} does not exist.")541 else:542 dotenv_path = find_dotenv()543 if not dotenv_path:544 logging.warning("No .env file found. Loading configurations from environment variables.")545 load_dotenv(dotenv_path)546 547 # Ensure the model_api_key_map is not None to prevent TypeErrors during key assignment.548 model_api_key_map = model_api_key_map or {}549 550 # Ensure default models are always considered551 default_models = ["gpt-4", "gpt-3.5-turbo"]552 553 for model in default_models:554 # Only assign default API key if the model is not present in the map.555 # If model is present but set to invalid/empty, do not overwrite.556 if model not in model_api_key_map:557 model_api_key_map[model] = "OPENAI_API_KEY"558 559 env_var = []560 # Loop over the models and create configuration dictionaries561 for model, config in model_api_key_map.items():562 if isinstance(config, str):563 api_key_env_var = config564 config_dict = get_config(api_key=os.getenv(api_key_env_var))565 elif isinstance(config, dict):566 api_key = os.getenv(config.get("api_key_env_var", "OPENAI_API_KEY"))567 config_without_key_var = {k: v for k, v in config.items() if k != "api_key_env_var"}568 config_dict = get_config(api_key=api_key, **config_without_key_var)569 else:570 logging.warning(f"Unsupported type {type(config)} for model {model} configuration")571 572 if not config_dict["api_key"] or config_dict["api_key"].strip() == "":573 logging.warning(574 f"API key not found or empty for model {model}. Please ensure path to .env file is correct."575 )576 continue # Skip this configuration and continue with the next577 578 # Add model to the configuration and append to the list579 config_dict["model"] = model580 env_var.append(config_dict)581 582 fd, temp_name = tempfile.mkstemp()583 try:584 with os.fdopen(fd, "w+") as temp:585 env_var_str = json.dumps(env_var)586 temp.write(env_var_str)587 temp.flush()588 589 # Assuming config_list_from_json is a valid function from your code590 config_list = config_list_from_json(env_or_file=temp_name, filter_dict=filter_dict)591 finally:592 # The file is deleted after using its name (to prevent windows build from breaking)593 os.remove(temp_name)594 595 if len(config_list) == 0:596 logging.error("No configurations loaded.")597 return []598 599 logging.info(f"Models available: {[config['model'] for config in config_list]}")600 return config_list601 602 603def retrieve_assistants_by_name(client: OpenAI, name: str) -> List[Assistant]:604 """605 Return the assistants with the given name from OAI assistant API606 """607 if ERROR:608 raise ERROR609 assistants = client.beta.assistants.list()610 candidate_assistants = []611 for assistant in assistants.data:612 if assistant.name == name:613 candidate_assistants.append(assistant)614 return candidate_assistants615 