CoolFace
Apppublic

aphilippov/python-server-api

sourceHugging Faceapache-2.0updated 3y agoView on Hugging Face
0likes
completion.py1197 linesDownload Raw Back to oai
1from time import sleep2import logging3import time4from typing import List, Optional, Dict, Callable, Union5import sys6import shutil7import numpy as np8from flaml import tune, BlendSearch9from flaml.tune.space import is_constant10from flaml.automl.logger import logger_formatter11from .openai_utils import get_key12from collections import defaultdict13 14try:15    import openai16    from openai import (17        RateLimitError,18        APIError,19        BadRequestError,20        APIConnectionError,21        Timeout,22        AuthenticationError,23    )24    from openai import Completion as openai_Completion25    import diskcache26 27    ERROR = None28    assert openai.__version__ < "1"29except (AssertionError, ImportError):30    openai_Completion = object31    # The autogen.Completion class requires openai<132    ERROR = AssertionError("(Deprecated) The autogen.Completion class requires openai<1 and diskcache. ")33 34logger = logging.getLogger(__name__)35if not logger.handlers:36    # Add the console handler.37    _ch = logging.StreamHandler(stream=sys.stdout)38    _ch.setFormatter(logger_formatter)39    logger.addHandler(_ch)40 41 42class Completion(openai_Completion):43    """(openai<1) A class for OpenAI completion API.44 45    It also supports: ChatCompletion, Azure OpenAI API.46    """47 48    # set of models that support chat completion49    chat_models = {50        "gpt-3.5-turbo",51        "gpt-3.5-turbo-0301",  # deprecate in Sep52        "gpt-3.5-turbo-0613",53        "gpt-3.5-turbo-16k",54        "gpt-3.5-turbo-16k-0613",55        "gpt-35-turbo",56        "gpt-35-turbo-16k",57        "gpt-4",58        "gpt-4-32k",59        "gpt-4-32k-0314",  # deprecate in Sep60        "gpt-4-0314",  # deprecate in Sep61        "gpt-4-0613",62        "gpt-4-32k-0613",63    }64 65    # price per 1k tokens66    price1K = {67        "text-ada-001": 0.0004,68        "text-babbage-001": 0.0005,69        "text-curie-001": 0.002,70        "code-cushman-001": 0.024,71        "code-davinci-002": 0.1,72        "text-davinci-002": 0.02,73        "text-davinci-003": 0.02,74        "gpt-3.5-turbo": (0.0015, 0.002),75        "gpt-3.5-turbo-instruct": (0.0015, 0.002),76        "gpt-3.5-turbo-0301": (0.0015, 0.002),  # deprecate in Sep77        "gpt-3.5-turbo-0613": (0.0015, 0.002),78        "gpt-3.5-turbo-16k": (0.003, 0.004),79        "gpt-3.5-turbo-16k-0613": (0.003, 0.004),80        "gpt-35-turbo": (0.0015, 0.002),81        "gpt-35-turbo-16k": (0.003, 0.004),82        "gpt-35-turbo-instruct": (0.0015, 0.002),83        "gpt-4": (0.03, 0.06),84        "gpt-4-32k": (0.06, 0.12),85        "gpt-4-0314": (0.03, 0.06),  # deprecate in Sep86        "gpt-4-32k-0314": (0.06, 0.12),  # deprecate in Sep87        "gpt-4-0613": (0.03, 0.06),88        "gpt-4-32k-0613": (0.06, 0.12),89    }90 91    default_search_space = {92        "model": tune.choice(93            [94                "text-ada-001",95                "text-babbage-001",96                "text-davinci-003",97                "gpt-3.5-turbo",98                "gpt-4",99            ]100        ),101        "temperature_or_top_p": tune.choice(102            [103                {"temperature": tune.uniform(0, 2)},104                {"top_p": tune.uniform(0, 1)},105            ]106        ),107        "max_tokens": tune.lograndint(50, 1000),108        "n": tune.randint(1, 100),109        "prompt": "{prompt}",110    }111 112    cache_seed = 41113    cache_path = f".cache/{cache_seed}"114    # retry after this many seconds115    retry_wait_time = 10116    # fail a request after hitting RateLimitError for this many seconds117    max_retry_period = 120118    # time out for request to openai server119    request_timeout = 60120 121    openai_completion_class = not ERROR and openai.Completion122    _total_cost = 0123    optimization_budget = None124 125    _history_dict = _count_create = None126 127    @classmethod128    def set_cache(cls, seed: Optional[int] = 41, cache_path_root: Optional[str] = ".cache"):129        """Set cache path.130 131        Args:132            seed (int, Optional): The integer identifier for the pseudo seed.133                Results corresponding to different seeds will be cached in different places.134            cache_path (str, Optional): The root path for the cache.135                The complete cache path will be {cache_path}/{seed}.136        """137        cls.cache_seed = seed138        cls.cache_path = f"{cache_path_root}/{seed}"139 140    @classmethod141    def clear_cache(cls, seed: Optional[int] = None, cache_path_root: Optional[str] = ".cache"):142        """Clear cache.143 144        Args:145            seed (int, Optional): The integer identifier for the pseudo seed.146                If omitted, all caches under cache_path_root will be cleared.147            cache_path (str, Optional): The root path for the cache.148                The complete cache path will be {cache_path}/{cache_seed}.149        """150        if seed is None:151            shutil.rmtree(cache_path_root, ignore_errors=True)152            return153        with diskcache.Cache(f"{cache_path_root}/{seed}") as cache:154            cache.clear()155 156    @classmethod157    def _book_keeping(cls, config: Dict, response):158        """Book keeping for the created completions."""159        if response != -1 and "cost" not in response:160            response["cost"] = cls.cost(response)161        if cls._history_dict is None:162            return163        if cls._history_compact:164            value = {165                "created_at": [],166                "cost": [],167                "token_count": [],168            }169            if "messages" in config:170                messages = config["messages"]171                if len(messages) > 1 and messages[-1]["role"] != "assistant":172                    existing_key = get_key(messages[:-1])173                    value = cls._history_dict.pop(existing_key, value)174                key = get_key(messages + [choice["message"] for choice in response["choices"]])175            else:176                key = get_key([config["prompt"]] + [choice.get("text") for choice in response["choices"]])177            value["created_at"].append(cls._count_create)178            value["cost"].append(response["cost"])179            value["token_count"].append(180                {181                    "model": response["model"],182                    "prompt_tokens": response["usage"]["prompt_tokens"],183                    "completion_tokens": response["usage"].get("completion_tokens", 0),184                    "total_tokens": response["usage"]["total_tokens"],185                }186            )187            cls._history_dict[key] = value188            cls._count_create += 1189            return190        cls._history_dict[cls._count_create] = {191            "request": config,192            "response": response.to_dict_recursive(),193        }194        cls._count_create += 1195 196    @classmethod197    def _get_response(cls, config: Dict, raise_on_ratelimit_or_timeout=False, use_cache=True):198        """Get the response from the openai api call.199 200        Try cache first. If not found, call the openai api. If the api call fails, retry after retry_wait_time.201        """202        config = config.copy()203        key = get_key(config)204        if use_cache:205            response = cls._cache.get(key, None)206            if response is not None and (response != -1 or not raise_on_ratelimit_or_timeout):207                # print("using cached response")208                cls._book_keeping(config, response)209                return response210        openai_completion = (211            openai.ChatCompletion212            if config["model"].replace("gpt-35-turbo", "gpt-3.5-turbo") in cls.chat_models213            or issubclass(cls, ChatCompletion)214            else openai.Completion215        )216        start_time = time.time()217        request_timeout = cls.request_timeout218        max_retry_period = config.pop("max_retry_period", cls.max_retry_period)219        retry_wait_time = config.pop("retry_wait_time", cls.retry_wait_time)220        while True:221            try:222                if "request_timeout" in config:223                    response = openai_completion.create(**config)224                else:225                    response = openai_completion.create(request_timeout=request_timeout, **config)226            except APIConnectionError:227                # transient error228                logger.info(f"retrying in {retry_wait_time} seconds...", exc_info=1)229                sleep(retry_wait_time)230            except APIError as err:231                error_code = err and err.json_body and isinstance(err.json_body, dict) and err.json_body.get("error")232                if isinstance(error_code, dict):233                    error_code = error_code.get("code")234                if error_code == "content_filter":235                    raise236                # transient error237                logger.info(f"retrying in {retry_wait_time} seconds...", exc_info=1)238                sleep(retry_wait_time)239            except (RateLimitError, Timeout) as err:240                time_left = max_retry_period - (time.time() - start_time + retry_wait_time)241                if (242                    time_left > 0243                    and isinstance(err, RateLimitError)244                    or time_left > request_timeout245                    and isinstance(err, Timeout)246                    and "request_timeout" not in config247                ):248                    if isinstance(err, Timeout):249                        request_timeout <<= 1250                    request_timeout = min(request_timeout, time_left)251                    logger.info(f"retrying in {retry_wait_time} seconds...", exc_info=1)252                    sleep(retry_wait_time)253                elif raise_on_ratelimit_or_timeout:254                    raise255                else:256                    response = -1257                    if use_cache and isinstance(err, Timeout):258                        cls._cache.set(key, response)259                    logger.warning(260                        f"Failed to get response from openai api due to getting RateLimitError or Timeout for {max_retry_period} seconds."261                    )262                    return response263            except BadRequestError:264                if "azure" in config.get("api_type", openai.api_type) and "model" in config:265                    # azure api uses "engine" instead of "model"266                    config["engine"] = config.pop("model").replace("gpt-3.5-turbo", "gpt-35-turbo")267                else:268                    raise269            else:270                if use_cache:271                    cls._cache.set(key, response)272                cls._book_keeping(config, response)273                return response274 275    @classmethod276    def _get_max_valid_n(cls, key, max_tokens):277        # find the max value in max_valid_n_per_max_tokens278        # whose key is equal or larger than max_tokens279        return max(280            (value for k, value in cls._max_valid_n_per_max_tokens.get(key, {}).items() if k >= max_tokens),281            default=1,282        )283 284    @classmethod285    def _get_min_invalid_n(cls, key, max_tokens):286        # find the min value in min_invalid_n_per_max_tokens287        # whose key is equal or smaller than max_tokens288        return min(289            (value for k, value in cls._min_invalid_n_per_max_tokens.get(key, {}).items() if k <= max_tokens),290            default=None,291        )292 293    @classmethod294    def _get_region_key(cls, config):295        # get a key for the valid/invalid region corresponding to the given config296        config = cls._pop_subspace(config, always_copy=False)297        return (298            config["model"],299            config.get("prompt", config.get("messages")),300            config.get("stop"),301        )302 303    @classmethod304    def _update_invalid_n(cls, prune, region_key, max_tokens, num_completions):305        if prune:306            # update invalid n and prune this config307            cls._min_invalid_n_per_max_tokens[region_key] = invalid_n = cls._min_invalid_n_per_max_tokens.get(308                region_key, {}309            )310            invalid_n[max_tokens] = min(num_completions, invalid_n.get(max_tokens, np.inf))311 312    @classmethod313    def _pop_subspace(cls, config, always_copy=True):314        if "subspace" in config:315            config = config.copy()316            config.update(config.pop("subspace"))317        return config.copy() if always_copy else config318 319    @classmethod320    def _get_params_for_create(cls, config: Dict) -> Dict:321        """Get the params for the openai api call from a config in the search space."""322        params = cls._pop_subspace(config)323        if cls._prompts:324            params["prompt"] = cls._prompts[config["prompt"]]325        else:326            params["messages"] = cls._messages[config["messages"]]327        if "stop" in params:328            params["stop"] = cls._stops and cls._stops[params["stop"]]329        temperature_or_top_p = params.pop("temperature_or_top_p", None)330        if temperature_or_top_p:331            params.update(temperature_or_top_p)332        if cls._config_list and "config_list" not in params:333            params["config_list"] = cls._config_list334        return params335 336    @classmethod337    def _eval(cls, config: dict, prune=True, eval_only=False):338        """Evaluate the given config as the hyperparameter setting for the openai api call.339 340        Args:341            config (dict): Hyperparameter setting for the openai api call.342            prune (bool, optional): Whether to enable pruning. Defaults to True.343            eval_only (bool, optional): Whether to evaluate only344              (ignore the inference budget and do not raise error when a request fails).345              Defaults to False.346 347        Returns:348            dict: Evaluation results.349        """350        cost = 0351        data = cls.data352        params = cls._get_params_for_create(config)353        model = params["model"]354        data_length = len(data)355        price = cls.price1K.get(model)356        price_input, price_output = price if isinstance(price, tuple) else (price, price)357        inference_budget = getattr(cls, "inference_budget", None)358        prune_hp = getattr(cls, "_prune_hp", "n")359        metric = cls._metric360        config_n = params.get(prune_hp, 1)  # default value in OpenAI is 1361        max_tokens = params.get(362            "max_tokens", np.inf if model in cls.chat_models or issubclass(cls, ChatCompletion) else 16363        )364        target_output_tokens = None365        if not cls.avg_input_tokens:366            input_tokens = [None] * data_length367        prune = prune and inference_budget and not eval_only368        if prune:369            region_key = cls._get_region_key(config)370            max_valid_n = cls._get_max_valid_n(region_key, max_tokens)371            if cls.avg_input_tokens:372                target_output_tokens = (inference_budget * 1000 - cls.avg_input_tokens * price_input) / price_output373                # max_tokens bounds the maximum tokens374                # so using it we can calculate a valid n according to the avg # input tokens375                max_valid_n = max(376                    max_valid_n,377                    int(target_output_tokens // max_tokens),378                )379            if config_n <= max_valid_n:380                start_n = config_n381            else:382                min_invalid_n = cls._get_min_invalid_n(region_key, max_tokens)383                if min_invalid_n is not None and config_n >= min_invalid_n:384                    # prune this config385                    return {386                        "inference_cost": np.inf,387                        metric: np.inf if cls._mode == "min" else -np.inf,388                        "cost": cost,389                    }390                start_n = max_valid_n + 1391        else:392            start_n = config_n393            region_key = None394        num_completions, previous_num_completions = start_n, 0395        n_tokens_list, result, responses_list = [], {}, []396        while True:  # n <= config_n397            params[prune_hp] = num_completions - previous_num_completions398            data_limit = 1 if prune else data_length399            prev_data_limit = 0400            data_early_stop = False  # whether data early stop happens for this n401            while True:  # data_limit <= data_length402                # limit the number of data points to avoid rate limit403                for i in range(prev_data_limit, data_limit):404                    logger.debug(f"num_completions={num_completions}, data instance={i}")405                    data_i = data[i]406                    response = cls.create(data_i, raise_on_ratelimit_or_timeout=eval_only, **params)407                    if response == -1:  # rate limit/timeout error, treat as invalid408                        cls._update_invalid_n(prune, region_key, max_tokens, num_completions)409                        result[metric] = 0410                        result["cost"] = cost411                        return result412                    # evaluate the quality of the responses413                    responses = cls.extract_text_or_function_call(response)414                    usage = response["usage"]415                    n_input_tokens = usage["prompt_tokens"]416                    n_output_tokens = usage.get("completion_tokens", 0)417                    if not cls.avg_input_tokens and not input_tokens[i]:418                        # store the # input tokens419                        input_tokens[i] = n_input_tokens420                    query_cost = response["cost"]421                    cls._total_cost += query_cost422                    cost += query_cost423                    if cls.optimization_budget and cls._total_cost >= cls.optimization_budget and not eval_only:424                        # limit the total tuning cost425                        return {426                            metric: 0,427                            "total_cost": cls._total_cost,428                            "cost": cost,429                        }430                    if previous_num_completions:431                        n_tokens_list[i] += n_output_tokens432                        responses_list[i].extend(responses)433                        # Assumption 1: assuming requesting n1, n2 responses separately then combining them434                        # is the same as requesting (n1+n2) responses together435                    else:436                        n_tokens_list.append(n_output_tokens)437                        responses_list.append(responses)438                avg_n_tokens = np.mean(n_tokens_list[:data_limit])439                rho = (440                    (1 - data_limit / data_length) * (1 + 1 / data_limit)441                    if data_limit << 1 > data_length442                    else (1 - (data_limit - 1) / data_length)443                )444                # Hoeffding-Serfling bound445                ratio = 0.1 * np.sqrt(rho / data_limit)446                if target_output_tokens and avg_n_tokens > target_output_tokens * (1 + ratio) and not eval_only:447                    cls._update_invalid_n(prune, region_key, max_tokens, num_completions)448                    result[metric] = 0449                    result["total_cost"] = cls._total_cost450                    result["cost"] = cost451                    return result452                if (453                    prune454                    and target_output_tokens455                    and avg_n_tokens <= target_output_tokens * (1 - ratio)456                    and (num_completions < config_n or num_completions == config_n and data_limit == data_length)457                ):458                    # update valid n459                    cls._max_valid_n_per_max_tokens[region_key] = valid_n = cls._max_valid_n_per_max_tokens.get(460                        region_key, {}461                    )462                    valid_n[max_tokens] = max(num_completions, valid_n.get(max_tokens, 0))463                    if num_completions < config_n:464                        # valid already, skip the rest of the data465                        data_limit = data_length466                        data_early_stop = True467                        break468                prev_data_limit = data_limit469                if data_limit < data_length:470                    data_limit = min(data_limit << 1, data_length)471                else:472                    break473            # use exponential search to increase n474            if num_completions == config_n:475                for i in range(data_limit):476                    data_i = data[i]477                    responses = responses_list[i]478                    metrics = cls._eval_func(responses, **data_i)479                    if result:480                        for key, value in metrics.items():481                            if isinstance(value, (float, int)):482                                result[key] += value483                    else:484                        result = metrics485                for key in result.keys():486                    if isinstance(result[key], (float, int)):487                        result[key] /= data_limit488                result["total_cost"] = cls._total_cost489                result["cost"] = cost490                if not cls.avg_input_tokens:491                    cls.avg_input_tokens = np.mean(input_tokens)492                    if prune:493                        target_output_tokens = (494                            inference_budget * 1000 - cls.avg_input_tokens * price_input495                        ) / price_output496                result["inference_cost"] = (avg_n_tokens * price_output + cls.avg_input_tokens * price_input) / 1000497                break498            else:499                if data_early_stop:500                    previous_num_completions = 0501                    n_tokens_list.clear()502                    responses_list.clear()503                else:504                    previous_num_completions = num_completions505                num_completions = min(num_completions << 1, config_n)506        return result507 508    @classmethod509    def tune(510        cls,511        data: List[Dict],512        metric: str,513        mode: str,514        eval_func: Callable,515        log_file_name: Optional[str] = None,516        inference_budget: Optional[float] = None,517        optimization_budget: Optional[float] = None,518        num_samples: Optional[int] = 1,519        logging_level: Optional[int] = logging.WARNING,520        **config,521    ):522        """Tune the parameters for the OpenAI API call.523 524        TODO: support parallel tuning with ray or spark.525        TODO: support agg_method as in test526 527        Args:528            data (list): The list of data points.529            metric (str): The metric to optimize.530            mode (str): The optimization mode, "min" or "max.531            eval_func (Callable): The evaluation function for responses.532                The function should take a list of responses and a data point as input,533                and return a dict of metrics. For example,534 535        ```python536        def eval_func(responses, **data):537            solution = data["solution"]538            success_list = []539            n = len(responses)540            for i in range(n):541                response = responses[i]542                succeed = is_equiv_chain_of_thought(response, solution)543                success_list.append(succeed)544            return {545                "expected_success": 1 - pow(1 - sum(success_list) / n, n),546                "success": any(s for s in success_list),547            }548        ```549 550            log_file_name (str, optional): The log file.551            inference_budget (float, optional): The inference budget, dollar per instance.552            optimization_budget (float, optional): The optimization budget, dollar in total.553            num_samples (int, optional): The number of samples to evaluate.554                -1 means no hard restriction in the number of trials555                and the actual number is decided by optimization_budget. Defaults to 1.556            logging_level (optional): logging level. Defaults to logging.WARNING.557            **config (dict): The search space to update over the default search.558                For prompt, please provide a string/Callable or a list of strings/Callables.559                    - If prompt is provided for chat models, it will be converted to messages under role "user".560                    - Do not provide both prompt and messages for chat models, but provide either of them.561                    - A string template will be used to generate a prompt for each data instance562                      using `prompt.format(**data)`.563                    - A callable template will be used to generate a prompt for each data instance564                      using `prompt(data)`.565                For stop, please provide a string, a list of strings, or a list of lists of strings.566                For messages (chat models only), please provide a list of messages (for a single chat prefix)567                or a list of lists of messages (for multiple choices of chat prefix to choose from).568                Each message should be a dict with keys "role" and "content". The value of "content" can be a string/Callable template.569 570        Returns:571            dict: The optimized hyperparameter setting.572            tune.ExperimentAnalysis: The tuning results.573        """574        logger.warning(575            "tuning via Completion.tune is deprecated in pyautogen v0.2 and openai>=1. "576            "flaml.tune supports tuning more generically."577        )578        if ERROR:579            raise ERROR580        space = cls.default_search_space.copy()581        if config is not None:582            space.update(config)583            if "messages" in space:584                space.pop("prompt", None)585            temperature = space.pop("temperature", None)586            top_p = space.pop("top_p", None)587            if temperature is not None and top_p is None:588                space["temperature_or_top_p"] = {"temperature": temperature}589            elif temperature is None and top_p is not None:590                space["temperature_or_top_p"] = {"top_p": top_p}591            elif temperature is not None and top_p is not None:592                space.pop("temperature_or_top_p")593                space["temperature"] = temperature594                space["top_p"] = top_p595                logger.warning("temperature and top_p are not recommended to vary together.")596        cls._max_valid_n_per_max_tokens, cls._min_invalid_n_per_max_tokens = {}, {}597        cls.optimization_budget = optimization_budget598        cls.inference_budget = inference_budget599        cls._prune_hp = "best_of" if space.get("best_of", 1) != 1 else "n"600        cls._prompts = space.get("prompt")601        if cls._prompts is None:602            cls._messages = space.get("messages")603            if not all((isinstance(cls._messages, list), isinstance(cls._messages[0], (dict, list)))):604                error_msg = "messages must be a list of dicts or a list of lists."605                logger.error(error_msg)606                raise AssertionError(error_msg)607            if isinstance(cls._messages[0], dict):608                cls._messages = [cls._messages]609            space["messages"] = tune.choice(list(range(len(cls._messages))))610        else:611            if space.get("messages") is not None:612                error_msg = "messages and prompt cannot be provided at the same time."613                logger.error(error_msg)614                raise AssertionError(error_msg)615            if not isinstance(cls._prompts, (str, list)):616                error_msg = "prompt must be a string or a list of strings."617                logger.error(error_msg)618                raise AssertionError(error_msg)619            if isinstance(cls._prompts, str):620                cls._prompts = [cls._prompts]621            space["prompt"] = tune.choice(list(range(len(cls._prompts))))622        cls._stops = space.get("stop")623        if cls._stops:624            if not isinstance(cls._stops, (str, list)):625                error_msg = "stop must be a string, a list of strings, or a list of lists of strings."626                logger.error(error_msg)627                raise AssertionError(error_msg)628            if not (isinstance(cls._stops, list) and isinstance(cls._stops[0], list)):629                cls._stops = [cls._stops]630            space["stop"] = tune.choice(list(range(len(cls._stops))))631        cls._config_list = space.get("config_list")632        if cls._config_list is not None:633            is_const = is_constant(cls._config_list)634            if is_const:635                space.pop("config_list")636        cls._metric, cls._mode = metric, mode637        cls._total_cost = 0  # total optimization cost638        cls._eval_func = eval_func639        cls.data = data640        cls.avg_input_tokens = None641 642        space_model = space["model"]643        if not isinstance(space_model, str) and len(space_model) > 1:644            # make a hierarchical search space645            subspace = {}646            if "max_tokens" in space:647                subspace["max_tokens"] = space.pop("max_tokens")648            if "temperature_or_top_p" in space:649                subspace["temperature_or_top_p"] = space.pop("temperature_or_top_p")650            if "best_of" in space:651                subspace["best_of"] = space.pop("best_of")652            if "n" in space:653                subspace["n"] = space.pop("n")654            choices = []655            for model in space["model"]:656                choices.append({"model": model, **subspace})657            space["subspace"] = tune.choice(choices)658            space.pop("model")659            # start all the models with the same hp config660            search_alg = BlendSearch(661                cost_attr="cost",662                cost_budget=optimization_budget,663                metric=metric,664                mode=mode,665                space=space,666            )667            config0 = search_alg.suggest("t0")668            points_to_evaluate = [config0]669            for model in space_model:670                if model != config0["subspace"]["model"]:671                    point = config0.copy()672                    point["subspace"] = point["subspace"].copy()673                    point["subspace"]["model"] = model674                    points_to_evaluate.append(point)675            search_alg = BlendSearch(676                cost_attr="cost",677                cost_budget=optimization_budget,678                metric=metric,679                mode=mode,680                space=space,681                points_to_evaluate=points_to_evaluate,682            )683        else:684            search_alg = BlendSearch(685                cost_attr="cost",686                cost_budget=optimization_budget,687                metric=metric,688                mode=mode,689                space=space,690            )691        old_level = logger.getEffectiveLevel()692        logger.setLevel(logging_level)693        with diskcache.Cache(cls.cache_path) as cls._cache:694            analysis = tune.run(695                cls._eval,696                search_alg=search_alg,697                num_samples=num_samples,698                log_file_name=log_file_name,699                verbose=3,700            )701        config = analysis.best_config702        params = cls._get_params_for_create(config)703        if cls._config_list is not None and is_const:704            params.pop("config_list")705        logger.setLevel(old_level)706        return params, analysis707 708    @classmethod709    def create(710        cls,711        context: Optional[Dict] = None,712        use_cache: Optional[bool] = True,713        config_list: Optional[List[Dict]] = None,714        filter_func: Optional[Callable[[Dict, Dict], bool]] = None,715        raise_on_ratelimit_or_timeout: Optional[bool] = True,716        allow_format_str_template: Optional[bool] = False,717        **config,718    ):719        """Make a completion for a given context.720 721        Args:722            context (Dict, Optional): The context to instantiate the prompt.723                It needs to contain keys that are used by the prompt template or the filter function.724                E.g., `prompt="Complete the following sentence: {prefix}, context={"prefix": "Today I feel"}`.725                The actual prompt will be:726                "Complete the following sentence: Today I feel".727                More examples can be found at [templating](https://microsoft.github.io/autogen/docs/Use-Cases/enhanced_inference#templating).728            use_cache (bool, Optional): Whether to use cached responses.729            config_list (List, Optional): List of configurations for the completion to try.730                The first one that does not raise an error will be used.731                Only the differences from the default config need to be provided.732                E.g.,733 734        ```python735        response = oai.Completion.create(736            config_list=[737                {738                    "model": "gpt-4",739                    "api_key": os.environ.get("AZURE_OPENAI_API_KEY"),740                    "api_type": "azure",741                    "base_url": os.environ.get("AZURE_OPENAI_API_BASE"),742                    "api_version": "2023-03-15-preview",743                },744                {745                    "model": "gpt-3.5-turbo",746                    "api_key": os.environ.get("OPENAI_API_KEY"),747                    "api_type": "open_ai",748                    "base_url": "https://api.openai.com/v1",749                },750                {751                    "model": "llama-7B",752                    "base_url": "http://127.0.0.1:8080",753                    "api_type": "open_ai",754                }755            ],756            prompt="Hi",757        )758        ```759 760            filter_func (Callable, Optional): A function that takes in the context and the response and returns a boolean to indicate whether the response is valid. E.g.,761 762        ```python763        def yes_or_no_filter(context, config, response):764            return context.get("yes_or_no_choice", False) is False or any(765                text in ["Yes.", "No."] for text in oai.Completion.extract_text(response)766            )767        ```768 769            raise_on_ratelimit_or_timeout (bool, Optional): Whether to raise RateLimitError or Timeout when all configs fail.770                When set to False, -1 will be returned when all configs fail.771            allow_format_str_template (bool, Optional): Whether to allow format string template in the config.772            **config: Configuration for the openai API call. This is used as parameters for calling openai API.773                The "prompt" or "messages" parameter can contain a template (str or Callable) which will be instantiated with the context.774                Besides the parameters for the openai API call, it can also contain:775                - `max_retry_period` (int): the total time (in seconds) allowed for retrying failed requests.776                - `retry_wait_time` (int): the time interval to wait (in seconds) before retrying a failed request.777                - `cache_seed` (int) for the cache. This is useful when implementing "controlled randomness" for the completion.778 779        Returns:780            Responses from OpenAI API, with additional fields.781                - `cost`: the total cost.782            When `config_list` is provided, the response will contain a few more fields:783                - `config_id`: the index of the config in the config_list that is used to generate the response.784                - `pass_filter`: whether the response passes the filter function. None if no filter is provided.785        """786        logger.warning(787            "Completion.create is deprecated in pyautogen v0.2 and openai>=1. "788            "The new openai requires initiating a client for inference. "789            "Please refer to https://microsoft.github.io/autogen/docs/Use-Cases/enhanced_inference#api-unification"790        )791        if ERROR:792            raise ERROR793 794        # Warn if a config list was provided but was empty795        if type(config_list) is list and len(config_list) == 0:796            logger.warning(797                "Completion was provided with a config_list, but the list was empty. Adopting default OpenAI behavior, which reads from the 'model' parameter instead."798            )799 800        if config_list:801            last = len(config_list) - 1802            cost = 0803            for i, each_config in enumerate(config_list):804                base_config = config.copy()805                base_config["allow_format_str_template"] = allow_format_str_template806                base_config.update(each_config)807                if i < last and filter_func is None and "max_retry_period" not in base_config:808                    # max_retry_period = 0 to avoid retrying when no filter is given809                    base_config["max_retry_period"] = 0810                try:811                    response = cls.create(812                        context,813                        use_cache,814                        raise_on_ratelimit_or_timeout=i < last or raise_on_ratelimit_or_timeout,815                        **base_config,816                    )817                    if response == -1:818                        return response819                    pass_filter = filter_func is None or filter_func(context=context, response=response)820                    if pass_filter or i == last:821                        response["cost"] = cost + response["cost"]822                        response["config_id"] = i823                        response["pass_filter"] = pass_filter824                        return response825                    cost += response["cost"]826                except (AuthenticationError, RateLimitError, Timeout, BadRequestError):827                    logger.debug(f"failed with config {i}", exc_info=1)828                    if i == last:829                        raise830        params = cls._construct_params(context, config, allow_format_str_template=allow_format_str_template)831        if not use_cache:832            return cls._get_response(833                params, raise_on_ratelimit_or_timeout=raise_on_ratelimit_or_timeout, use_cache=False834            )835        cache_seed = cls.cache_seed836        if "cache_seed" in params:837            cls.set_cache(params.pop("cache_seed"))838        with diskcache.Cache(cls.cache_path) as cls._cache:839            cls.set_cache(cache_seed)840            return cls._get_response(params, raise_on_ratelimit_or_timeout=raise_on_ratelimit_or_timeout)841 842    @classmethod843    def instantiate(844        cls,845        template: Union[str, None],846        context: Optional[Dict] = None,847        allow_format_str_template: Optional[bool] = False,848    ):849        if not context or template is None:850            return template851        if isinstance(template, str):852            return template.format(**context) if allow_format_str_template else template853        return template(context)854 855    @classmethod856    def _construct_params(cls, context, config, prompt=None, messages=None, allow_format_str_template=False):857        params = config.copy()858        model = config["model"]859        prompt = config.get("prompt") if prompt is None else prompt860        messages = config.get("messages") if messages is None else messages861        # either "prompt" should be in config (for being compatible with non-chat models)862        # or "messages" should be in config (for tuning chat models only)863        if prompt is None and (model in cls.chat_models or issubclass(cls, ChatCompletion)):864            if messages is None:865                raise ValueError("Either prompt or messages should be in config for chat models.")866        if prompt is None:867            params["messages"] = (868                [869                    {870                        **m,871                        "content": cls.instantiate(m["content"], context, allow_format_str_template),872                    }873                    if m.get("content")874                    else m875                    for m in messages876                ]877                if context878                else messages879            )880        elif model in cls.chat_models or issubclass(cls, ChatCompletion):881            # convert prompt to messages882            params["messages"] = [883                {884                    "role": "user",885                    "content": cls.instantiate(prompt, context, allow_format_str_template),886                },887            ]888            params.pop("prompt", None)889        else:890            params["prompt"] = cls.instantiate(prompt, context, allow_format_str_template)891        return params892 893    @classmethod894    def test(895        cls,896        data,897        eval_func=None,898        use_cache=True,899        agg_method="avg",900        return_responses_and_per_instance_result=False,901        logging_level=logging.WARNING,902        **config,903    ):904        """Evaluate the responses created with the config for the OpenAI API call.905 906        Args:907            data (list): The list of test data points.908            eval_func (Callable): The evaluation function for responses per data instance.909                The function should take a list of responses and a data point as input,910                and return a dict of metrics. You need to either provide a valid callable911                eval_func; or do not provide one (set None) but call the test function after912                calling the tune function in which a eval_func is provided.913                In the latter case we will use the eval_func provided via tune function.914                Defaults to None.915 916        ```python917        def eval_func(responses, **data):918            solution = data["solution"]919            success_list = []920            n = len(responses)921            for i in range(n):922                response = responses[i]923                succeed = is_equiv_chain_of_thought(response, solution)924                success_list.append(succeed)925            return {926                "expected_success": 1 - pow(1 - sum(success_list) / n, n),927                "success": any(s for s in success_list),928            }929        ```930            use_cache (bool, Optional): Whether to use cached responses. Defaults to True.931            agg_method (str, Callable or a dict of Callable): Result aggregation method (across932                multiple instances) for each of the metrics. Defaults to 'avg'.933                An example agg_method in str:934 935        ```python936        agg_method = 'median'937        ```938                An example agg_method in a Callable:939 940        ```python941        agg_method = np.median942        ```943 944                An example agg_method in a dict of Callable:945 946        ```python947        agg_method={'median_success': np.median, 'avg_success': np.mean}948        ```949 950            return_responses_and_per_instance_result (bool): Whether to also return responses951                and per instance results in addition to the aggregated results.952            logging_level (optional): logging level. Defaults to logging.WARNING.953            **config (dict): parameters passed to the openai api call `create()`.954 955        Returns:956            None when no valid eval_func is provided in either test or tune;957            Otherwise, a dict of aggregated results, responses and per instance results if `return_responses_and_per_instance_result` is True;958            Otherwise, a dict of aggregated results (responses and per instance results are not returned).959        """960        result_agg, responses_list, result_list = {}, [], []961        metric_keys = None962        cost = 0963        old_level = logger.getEffectiveLevel()964        logger.setLevel(logging_level)965        for i, data_i in enumerate(data):966            logger.info(f"evaluating data instance {i}")967            response = cls.create(data_i, use_cache, **config)968            cost += response["cost"]969            # evaluate the quality of the responses970            responses = cls.extract_text_or_function_call(response)971            if eval_func is not None:972                metrics = eval_func(responses, **data_i)973            elif hasattr(cls, "_eval_func"):974                metrics = cls._eval_func(responses, **data_i)975            else:976                logger.warning(977                    "Please either provide a valid eval_func or do the test after the tune function is called."978                )979                return980            if not metric_keys:981                metric_keys = []982                for k in metrics.keys():983                    try:984                        _ = float(metrics[k])985                        metric_keys.append(k)986                    except ValueError:987                        pass988            result_list.append(metrics)989            if return_responses_and_per_instance_result:990                responses_list.append(responses)991        if isinstance(agg_method, str):992            if agg_method in ["avg", "average"]:993                for key in metric_keys:994                    result_agg[key] = np.mean([r[key] for r in result_list])995            elif agg_method == "median":996                for key in metric_keys:997                    result_agg[key] = np.median([r[key] for r in result_list])998            else:999                logger.warning(1000                    f"Aggregation method {agg_method} not supported. Please write your own aggregation method as a callable(s)."1001                )1002        elif callable(agg_method):1003            for key in metric_keys:1004                result_agg[key] = agg_method([r[key] for r in result_list])1005        elif isinstance(agg_method, dict):1006            for key in metric_keys:1007                metric_agg_method = agg_method[key]1008                if not callable(metric_agg_method):1009                    error_msg = "please provide a callable for each metric"1010                    logger.error(error_msg)1011                    raise AssertionError(error_msg)1012                result_agg[key] = metric_agg_method([r[key] for r in result_list])1013        else:1014            raise ValueError(1015                "agg_method needs to be a string ('avg' or 'median'),\1016                or a callable, or a dictionary of callable."1017            )1018        logger.setLevel(old_level)1019        # should we also return the result_list and responses_list or not?1020        if "cost" not in result_agg:1021            result_agg["cost"] = cost1022        if "inference_cost" not in result_agg:1023            result_agg["inference_cost"] = cost / len(data)1024        if return_responses_and_per_instance_result:1025            return result_agg, result_list, responses_list1026        else:1027            return result_agg1028 1029    @classmethod1030    def cost(cls, response: dict):1031        """Compute the cost of an API call.1032 1033        Args:1034            response (dict): The response from OpenAI API.1035 1036        Returns:1037            The cost in USD. 0 if the model is not supported.1038        """1039        model = response.get("model")1040        if model not in cls.price1K:1041            return 01042            # raise ValueError(f"Unknown model: {model}")1043        usage = response["usage"]1044        n_input_tokens = usage["prompt_tokens"]1045        n_output_tokens = usage.get("completion_tokens", 0)1046        price1K = cls.price1K[model]1047        if isinstance(price1K, tuple):1048            return (price1K[0] * n_input_tokens + price1K[1] * n_output_tokens) / 10001049        return price1K * (n_input_tokens + n_output_tokens) / 10001050 1051    @classmethod1052    def extract_text(cls, response: dict) -> List[str]:1053        """Extract the text from a completion or chat response.1054 1055        Args:1056            response (dict): The response from OpenAI API.1057 1058        Returns:1059            A list of text in the responses.1060        """1061        choices = response["choices"]1062        if "text" in choices[0]:1063            return [choice["text"] for choice in choices]1064        return [choice["message"].get("content", "") for choice in choices]1065 1066    @classmethod1067    def extract_text_or_function_call(cls, response: dict) -> List[str]:1068        """Extract the text or function calls from a completion or chat response.1069 1070        Args:1071            response (dict): The response from OpenAI API.1072 1073        Returns:1074            A list of text or function calls in the responses.1075        """1076        choices = response["choices"]1077        if "text" in choices[0]:1078            return [choice["text"] for choice in choices]1079        return [1080            choice["message"] if "function_call" in choice["message"] else choice["message"].get("content", "")1081            for choice in choices1082        ]1083 1084    @classmethod1085    @property1086    def logged_history(cls) -> Dict:1087        """Return the book keeping dictionary."""1088        return cls._history_dict1089 1090    @classmethod1091    def print_usage_summary(cls) -> Dict:1092        """Return the usage summary."""1093        if cls._history_dict is None:1094            print("No usage summary available.", flush=True)1095 1096        token_count_summary = defaultdict(lambda: {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0})1097 1098        if not cls._history_compact:1099            source = cls._history_dict.values()1100            total_cost = sum(msg_pair["response"]["cost"] for msg_pair in source)1101        else:1102            # source = cls._history_dict["token_count"]1103            # total_cost = sum(cls._history_dict['cost'])1104            total_cost = sum(sum(value_list["cost"]) for value_list in cls._history_dict.values())1105            source = (1106                token_data for value_list in cls._history_dict.values() for token_data in value_list["token_count"]1107            )1108 1109        for entry in source:1110            if not cls._history_compact:1111                model = entry["response"]["model"]1112                token_data = entry["response"]["usage"]1113            else:1114                model = entry["model"]1115                token_data = entry1116 1117            token_count_summary[model]["prompt_tokens"] += token_data["prompt_tokens"]1118            token_count_summary[model]["completion_tokens"] += token_data["completion_tokens"]1119            token_count_summary[model]["total_tokens"] += token_data["total_tokens"]1120 1121        print(f"Total cost: {total_cost}", flush=True)1122        for model, counts in token_count_summary.items():1123            print(1124                f"Token count summary for model {model}: prompt_tokens: {counts['prompt_tokens']}, completion_tokens: {counts['completion_tokens']}, total_tokens: {counts['total_tokens']}",1125                flush=True,1126            )1127 1128    @classmethod1129    def start_logging(1130        cls, history_dict: Optional[Dict] = None, compact: Optional[bool] = True, reset_counter: Optional[bool] = True1131    ):1132        """Start book keeping.1133 1134        Args:1135            history_dict (Dict): A dictionary for book keeping.1136                If no provided, a new one will be created.1137            compact (bool): Whether to keep the history dictionary compact.1138                Compact history contains one key per conversation, and the value is a dictionary1139                like:1140        ```python1141        {1142            "create_at": [0, 1],1143            "cost": [0.1, 0.2],1144        }1145        ```1146                where "created_at" is the index of API calls indicating the order of all the calls,1147                and "cost" is the cost of each call. This example shows that the conversation is based1148                on two API calls. The compact format is useful for condensing the history of a conversation.1149                If compact is False, the history dictionary will contain all the API calls: the key1150                is the index of the API call, and the value is a dictionary like:1151        ```python1152        {1153            "request": request_dict,1154            "response": response_dict,1155        }1156        ```1157                where request_dict is the request sent to OpenAI API, and response_dict is the response.1158                For a conversation containing two API calls, the non-compact history dictionary will be like:1159        ```python1160        {1161            0: {1162                "request": request_dict_0,1163                "response": response_dict_0,1164            },1165            1: {1166                "request": request_dict_1,1167                "response": response_dict_1,1168            },1169        ```1170                The first request's messages plus the response is equal to the second request's messages.1171                For a conversation with many turns, the non-compact history dictionary has a quadratic size1172                while the compact history dict has a linear size.1173            reset_counter (bool): whether to reset the counter of the number of API calls.1174        """1175        logger.warning(1176            "logging via Completion.start_logging is deprecated in pyautogen v0.2. "1177            "logging via OpenAIWrapper will be added back in a future release."1178        )1179        if ERROR:1180            raise ERROR1181        cls._history_dict = {} if history_dict is None else history_dict1182        cls._history_compact = compact1183        cls._count_create = 0 if reset_counter or cls._count_create is None else cls._count_create1184 1185    @classmethod1186    def stop_logging(cls):1187        """End book keeping."""1188        cls._history_dict = cls._count_create = None1189 1190 1191class ChatCompletion(Completion):1192    """(openai<1) A class for OpenAI API ChatCompletion. Share the same API as Completion."""1193 1194    default_search_space = Completion.default_search_space.copy()1195    default_search_space["model"] = tune.choice(["gpt-3.5-turbo", "gpt-4"])1196    openai_completion_class = not ERROR and openai.ChatCompletion1197