Source code for qianfan.common.prompt.prompt

# Copyright (c) 2023 Baidu, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import time
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple, cast

from qianfan.common.hub.interface import HubSerializable
from qianfan.config import encoding
from qianfan.consts import (
    PromptFrameworkType,
    PromptSceneType,
    PromptScoreStandard,
    PromptType,
)
from qianfan.errors import InvalidArgumentError, RequestError
from qianfan.resources.console.prompt import Prompt as PromptResource
from qianfan.resources.llm.completion import Completion
from qianfan.resources.typing import Literal, QfResponse
from qianfan.utils import log_warn


[docs]@dataclass class PromptLabel(HubSerializable): """ Class of prompt label """ id: int """ id of the label """ name: str """ name of the label """ color: str """ the color displayed in the console """
[docs]class Prompt(HubSerializable): """ Prompt """ id: Optional[str] = None name: Optional[str] = None template: str variables: List[str] identifier: Literal["{}", "{{}}", "[]", "[[]]", "()", "(())"] labels: List[PromptLabel] type: PromptType scene_type: PromptSceneType framework_type: PromptFrameworkType negative_template: Optional[str] = None negative_variables: Optional[List[str]] = None creator_name: Optional[str] = None _mode: str def __init__( self, template: Optional[str] = None, name: Optional[str] = None, id: Optional[str] = None, identifier: Literal["{}", "{{}}", "[]", "[[]]", "()", "(())"] = "{}", variables: Optional[List[str]] = None, labels: List[PromptLabel] = [], type: PromptType = PromptType.User, scene_type: PromptSceneType = PromptSceneType.Text2Text, framework_type: PromptFrameworkType = PromptFrameworkType.NotUse, negative_template: Optional[str] = None, negative_variables: Optional[List[str]] = None, creator_name: Optional[str] = None, ) -> None: """ Initializes a Prompt object. Parameters: template (Optional[str]): The template string for the prompt, required if mode id "local". name (Optional[str]): The name of the prompt, required if mode is "remote". id (Optional[int]): The id of the prompt on platform. identifier (Literal["{}", "{{}}", "[]", "[[]]", "()", "(())"]): The identifier format used in the template, e.g., "{}", "{{}}", "[]". variables (Optional[List[str]]): A list of variables used in the prompt template. SDK will extract them if not specified. labels (List[PromptLabel]): A list of labels associated with the prompt. type (PromptType): The type of the prompt, e.g., PromptType.User. scene_type (PromptSceneType): The scene type of the prompt, e.g., PromptSceneType.Text2Text. framework_type (PromptFrameworkType): The framework type used in the prompt, e.g., PromptFrameworkType.Fewshot. negative_template (Optional[str]): The negative template string for the prompt. negative_variables (Optional[List[str]]): A list of variables used in the negative template. SDK will extract them if not specified. creator_name (Optional[str]): The name of the creator of the prompt. """ self.id = id self.name = name if template is None: raise InvalidArgumentError("template is required if using local prompt") self.template = template self.identifier = identifier self.labels = labels self.type = type self.scene_type = scene_type self.framework_type = framework_type if variables is not None: self.variables = variables else: self.variables = PromptResource._extract_variables(template, identifier) self.negative_template = negative_template if self.negative_template is not None: # if user does not provide negative varibles # extract them from negative template if self.negative_variables is not None: self.negative_variables = negative_variables else: self.negative_variables = PromptResource._extract_variables( self.negative_template, identifier ) self.creator_name = creator_name self._mode = "local" @classmethod def _hub_pull(cls, name: str) -> "Prompt": """ Init the prompt from api by name. """ resp = PromptResource.list(name=name, type=PromptType.User) prompt_info = None # try to find the prompt in user prompts for item in resp["result"]["items"]: if item["templateName"] == name: prompt_info = item break # if not found, try to find the prompt in preset prompts if prompt_info is None: resp = PromptResource.list(name=name, type=PromptType.Preset) for item in resp["result"]["items"]: if item["templateName"] == name: prompt_info = item break # if still not found, raise error if prompt_info is None: raise InvalidArgumentError(f"Prompt `{name}` does not exist") prompt = cls( name=prompt_info["templateName"], id=prompt_info["templatePK"], template=prompt_info["templateContent"], variables=( [] if prompt_info["templateVariables"] == "" else prompt_info["templateVariables"].split(",") ), labels=[ PromptLabel(label["labelId"], label["labelName"], label["color"]) for label in prompt_info["labels"] ], identifier=prompt_info["variableIdentifier"], type=PromptType(prompt_info["type"]), scene_type=PromptSceneType(prompt_info["sceneType"]), framework_type=PromptFrameworkType(prompt_info["frameworkType"]), creator_name=prompt_info["creatorName"], ) if prompt.scene_type == PromptSceneType.Text2Image: prompt.negative_template = prompt_info["negativeTemplateContent"] # sometimes negativeTemplateVariables is not set in api response if "negativeTemplateVariables" in prompt_info: prompt.negative_variables = prompt_info[ "negativeTemplateVariables" ].split(",") else: prompt.negative_variables = [] prompt._mode = "remote" return prompt
[docs] @classmethod def from_file( cls, path: str, identifier: Literal["{}", "{{}}", "[]", "[[]]", "()", "(())"] = "{}", ) -> "Prompt": """ Create a Prompt object from file. The file should only contain the prompt template. Parameters: path (str): The path of the prompt file. identifier (Literal["{}", "{{}}", "[]", "[[]]", "()", "(())"]): The identifier of the prompt. """ with open(path, "r", encoding=encoding()) as f: content = f.read() return cls(template=content, identifier=identifier)
[docs] def save_to_file(self, path: str) -> None: """ Save the prompt template to file. Parameters: path (str): The path of the prompt file. """ with open(path, "w", encoding=encoding()) as f: f.write(self.template)
def _hub_push(self) -> None: """ Upload the prompt to Qianfan. If the prompt is local, it will create a new one on the server. Otherwise, it will update the existing prompt. """ # local prompt, create it if self._mode == "local" or self.id is None: if self.name is None: raise InvalidArgumentError( "name is required when uploading to the server" ) resp = PromptResource.create( name=self.name, template=self.template, identifier=self.identifier, scene=self.scene_type, framework=self.framework_type, label_ids=[label.id for label in self.labels], negative_template=self.negative_template, negative_variables=self.negative_variables, ) if not resp["success"]: raise InvalidArgumentError( f"Failed to create prompt: {resp['message']['global']}" ) self.id = resp["result"]["templatePK"] self._mode = "remote" else: if self.name is None: raise InvalidArgumentError( "name is required when uploading to the server" ) resp = PromptResource.update( id=self.id, name=self.name, label_ids=[label.id for label in self.labels], template=self.template, identifier=self.identifier, negative_template=self.negative_template, ) if not resp["success"]: raise InvalidArgumentError( f"Failed to update prompt: {resp['message']['global']}" ) self.id = resp["result"]["templatePK"]
[docs] def render(self, **kwargs: str) -> Tuple[str, Optional[str]]: """ Render the prompt with given variables. Parameters: kwargs (Any): The value of the variables to be used for variable replacement in the template. """ prompt = self.template left_id, right_id = PromptResource._split_identifier(self.identifier) for v in self.variables: if v not in kwargs: raise InvalidArgumentError(f"variable `{v}` is not provided") prompt = prompt.replace(f"{left_id}{v}{right_id}", str(kwargs[v])) neg_prompt = None if self.negative_template is not None: if self.negative_variables is None: self.negative_variables = [] neg_prompt = self.negative_template for v in self.negative_variables: if v not in kwargs: raise InvalidArgumentError(f"variable `{v}` is not provided") neg_prompt = neg_prompt.replace( f"{left_id}{v}{right_id}", str(kwargs[v]) ) return prompt, neg_prompt
[docs] def delete(self) -> None: """ Delete the prompt from Qianfan. """ if self._mode == "local" or self.id is None: log_warn("local prompt does not need to be deleted") return if self.type == PromptType.Preset: log_warn(f"preset prompt {self.name} cannot be deleted") return resp = PromptResource.delete(id=self.id) if not resp["success"]: raise InvalidArgumentError( f"Failed to delete prompt: {resp['message']['global']}" ) self.id = None self._mode = "local"
[docs] def set_template(self, template: str) -> None: """ Set the prompt's template. The variables in the template will be extracted. Parameters: template (str): The new template. """ self.template = template self.variables = PromptResource._extract_variables(template, self.identifier)
[docs] def set_negative_template(self, template: str) -> None: """ Set the prompt's negative template. The variables in the template will be extracted. Parameters: template (str): The new negative template. """ self.negative_template = template self.negative_variables = PromptResource._extract_variables( template, self.identifier )
def _hub_serialize(self) -> Dict[str, Any]: """ Implement `HubSerializable` protocol. Convert the prompt to a dictionary that can be used for serialization. """ dic = super()._hub_serialize() del dic["args"]["_mode"] del dic["args"]["id"] return dic @classmethod def _hub_deserialize(cls, dic: Dict[str, Any]) -> "Prompt": """ Implement `HubSerializable` protocol. Convert a dictionary to a prompt. """ return cls(**dic)
[docs] @classmethod def base_prompt( cls, prompt: str, background: str = "", additional_data: str = "", output_schema: str = "", ) -> str: """ Generates a base type prompt for language models. Parameters: prompt (str): The main text prompt that defines the task or query for the language model. background (str, optional): Additional context or background information to provide more context to the model. additional_data (str, optional): Extra data that can be included to enhance the specificity or details of the prompt. output_schema (str, optional): The desired schema for formatting the output of the language model. Please refer to the following link for more details about CRISPE prompt. API Doc: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Zlo55g7t3 """ prompt = f"指令:{prompt}" background = f"背景信息:{background}" if background != "" else "" additional_data = f"补充数据:{additional_data}" if additional_data != "" else "" output_schema = f"输出格式:{output_schema}" if output_schema != "" else "" return "\n".join([prompt, background, additional_data, output_schema])
[docs] @classmethod def crispe_prompt( cls, statement: str, capacity: str = "", insight: str = "", personality: str = "", experiment: str = "", ) -> str: """ Generates a CRISPE-type prompt for fine-tuning models. Parameters: statement (str): The main task that the model should focus on. capacity (str, optional): Capacity information specifying what role you want the model to play. insight (str, optional): Insights or guidance to provide additional context for the fine-tuning task. personality (str, optional): The output style or personality of the model. experiment (str, optional): The limit of the output range of the model. Please refer to the following link for more details about CRISPE prompt. API Doc: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Hlo56qd21 """ prompt_list = [] if capacity != "": prompt_list.append(f"能力与角色:{capacity}") if insight != "": prompt_list.append(f"背景信息:{insight}") if statement != "": prompt_list.append(f"指令:{statement}") if personality != "": prompt_list.append(f"输出风格:{personality}") if experiment != "": prompt_list.append(f"输出范围:{experiment}") return "\n".join(prompt_list)
[docs] @classmethod def fewshot_prompt( cls, prompt: str = "", examples: List[Tuple[str, str]] = [], ) -> str: """ Generates a few-shot prompt for model input. Parameters: prompt (str): The main prompt that sets the context for what task should the model focus on. examples (List[Tuple[str, str]]): A list of example tuples, where each tuple contains a model input string and its corresponding expected output. These examples help the model understand the desired behavior. Please refer to the following link for more details about fewshot prompt. API Doc: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlo57dbf4 """ if len(examples) == 0: raise InvalidArgumentError("At least one example is required.") examples_prompt = [f"输入:{inp}\n输出:{outp}" for inp, outp in examples] output = "\n\n".join(examples_prompt) if prompt != "": output = prompt + "\n\n" + output return output
[docs] def optimize( self, optimize_quality: bool = True, simplify_prompt: bool = False, iteration_round: Literal[1, 2] = 1, enable_cot: bool = False, app_id: Optional[int] = None, service_name: Optional[str] = None, **kwargs: Any, ) -> "Prompt": """ Optimize a prompt for better performance and effectiveness. Parameters: optimize_quality (bool): Flag indicating whether to optimize for prompt quality. simplify_prompt (bool): Flag indicating whether to simplify the prompt structure. iteration_round (Literal[1, 2]): The number of optimization iterations to perform (1 or 2). enable_cot (bool): Flag indicating whether to enable chain of thought. app_id (Optional[int]): Optional application ID for context-specific optimizations. service_name (Optional[str]): Optional service used for optimizing. **kwargs (Any): Additional keyword arguments for future extensibility. Returns: Prompt: The optimized Prompt object. Please refer to the following link for more details about prompt optimization. API Doc: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/olr8svd33 """ operations = [] operations.append( { "opType": 1, "payload": 1 if optimize_quality else 0, } ) operations.append( { "opType": 2, "payload": 1 if simplify_prompt else 0, } ) operations.append({"opType": 3, "payload": iteration_round}) operations.append( { "opType": 4, "payload": 1 if enable_cot else 0, } ) resp = PromptResource.create_optimiztion_task( self.template, operations, app_id=app_id, service_name=service_name, **kwargs, ) task_id = resp["result"]["id"] while True: resp = PromptResource.get_optimization_task(task_id, **kwargs) status = resp["result"]["processStatus"] if status == 2: # fininished break elif status == 3: # failed raise RequestError("Prompt optimization task failed.") time.sleep(1) optimized_prompt = resp["result"]["optimizeContent"] return Prompt(optimized_prompt)
[docs] @dataclass class PromptEvaluateResult(object): """ Evaluation result of a prompt """ prompt: "Prompt" scene: List[Dict[str, Any]] summary: str
[docs] @classmethod def evaluate( cls, prompt_list: List["Prompt"], scenes: List[Dict[str, Any]], model: Completion, standard: PromptScoreStandard = PromptScoreStandard.Semantic, ) -> List[PromptEvaluateResult]: """ Evaluate a list of prompts against specified scenes using the given model. Parameters: prompt_list (List["Prompt"]): A list of prompt templates to be evaluated. scenes (List[Dict[str, Any]]): List of scenes represented as dictionaries containing relevant information. The dict should contain the following keys: - args: A dict containing the variables to be replaced in the prompt. - expected: The expected output of the prompt. client (Completion): An instance of the Completion client. standard (PromptScoreStandard, optional): The scoring standard to be used for evaluating prompts. Returns: List[PromptEvaluateResult]: A list of evaluation results, each containing the original prompt, the result of each scene, and a summary string. Example: result_list = PromptEvaluateResult.evaluate( prompt_list=[prompt1, prompt2, prompt3], scenes=[{ "args": {"name": "Alice"}, "expected": "Hello, Alice!" }], client=Completion(model="ERNIE-Bot-4"), standard=PromptScoreStandard.Semantic ) """ results = [ Prompt.PromptEvaluateResult( scene=[ { "new_prompt": prompt.render(**scene["args"])[0], "variables": scene["args"], "expected_target": scene["expected"], } for scene in scenes ], prompt=prompt, summary="", ) for prompt in prompt_list ] for i in range(len(results)): for j in range(len(results[i].scene)): resp = cast(QfResponse, model.do(results[i].scene[j]["new_prompt"])) results[i].scene[j]["response"] = resp["result"] eval_summary_req = [ { "prompt": prompt.prompt.template, "scenes": [ { "variables": scene["variables"], "expected_target": scene["expected_target"], "response": scene["response"], "new_prompt": scene["new_prompt"], } for scene in prompt.scene ], "response_list": [r["response"] for r in prompt.scene], } for prompt in results ] eval_score_req = [ { "scene": scenes[j]["expected"], "response_list": [ results[i].scene[j]["response"] for i in range(len(prompt_list)) ], } for j in range(len(scenes)) ] summary_resp = PromptResource.evaluation_summary(eval_summary_req) summary = summary_resp["result"]["responses"] for i in range(len(results)): results[i].summary = summary[i]["response"] score_resp = PromptResource.evaluation_score(standard.value, eval_score_req) score = score_resp["result"]["scores"] for i in range(len(results)): for j in range(len(results[i].scene)): results[i].scene[j]["score"] = score[j][i] return results