e768098d0e
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled
185 lines
5.7 KiB
Python
185 lines
5.7 KiB
Python
import logging
|
|
import logging.config
|
|
import re
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
import promptflow
|
|
import yaml
|
|
from openai import AzureOpenAI
|
|
from promptflow.connections import AzureOpenAIConnection
|
|
from tenacity import (
|
|
RetryError,
|
|
Retrying,
|
|
after_log,
|
|
before_sleep_log,
|
|
stop_after_attempt,
|
|
wait_random_exponential,
|
|
)
|
|
|
|
|
|
class Logger:
|
|
"""
|
|
A class for setting up and getting a logger object.
|
|
|
|
Attributes:
|
|
config_file (str): Path to the YAML file containing the logger configuration.
|
|
logger (logging.Logger): The logger object.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""
|
|
Initializes the Logger class.
|
|
|
|
Args:
|
|
config_file (str): Path to YAML file containing the logger configuration.
|
|
"""
|
|
config_file = Path(__file__).parent.joinpath("log_config.yaml").resolve()
|
|
with open(config_file, "r") as f:
|
|
config = yaml.safe_load(f.read())
|
|
logging.config.dictConfig(config)
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
def get_logger(self):
|
|
"""
|
|
Returns the logger object.
|
|
|
|
Returns:
|
|
logging.Logger: The logger object.
|
|
"""
|
|
return self.logger
|
|
|
|
|
|
logger = Logger().get_logger()
|
|
|
|
|
|
def parse_output(output: str, max: float) -> float:
|
|
"""
|
|
Function that extracts numerical score from the beginning of string
|
|
|
|
Args:
|
|
output (str): String to search
|
|
max (float): Maximum score allowed
|
|
|
|
Returns:
|
|
float: The extracted score
|
|
"""
|
|
# match with either non-negative float or integer
|
|
# if number has non-whitespace characture before that, it won't match
|
|
matched: List[str] = re.findall(r"(?<!\S)\d+(?:\.\d+)?", output)
|
|
if matched:
|
|
if len(matched) == 1:
|
|
score = float(matched[0])
|
|
if score > max:
|
|
raise ValueError(
|
|
f"Parsed number: {score} was larger than max score: {max}"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"More than one number detected in input. Input to parser was: {output}"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f'No number detected in input. Input to parser was "{output}". '
|
|
)
|
|
return score
|
|
|
|
|
|
@promptflow.tool
|
|
def geval_summarization(
|
|
prompt_with_src_and_gen: str,
|
|
max_score: float,
|
|
connection: AzureOpenAIConnection,
|
|
deployment_name: str = "gpt-4",
|
|
) -> float:
|
|
"""Using GPT, evaluate a generated summary with respect to a source document from
|
|
which it was generated. This function should be used for four dimensions of
|
|
summarization evaluation inline with the SummEval benchmark: fluency, coherence,
|
|
consistency, relevance.
|
|
|
|
Args:
|
|
prompt_with_src_and_gen (str): The prompt containing the source document and generated summary.
|
|
max_score (float): The maximum score allowed.
|
|
connection (AzureOpenAIConnection): The connection object for Azure OpenAI.
|
|
deployment_name (str, optional): The name of the deployment. Defaults to "gpt-4".
|
|
|
|
Returns:
|
|
float: The evaluation score
|
|
"""
|
|
# make sure you use the same api version/model with the one used for meta evaluation
|
|
logger.info(
|
|
f"OpenAI API Base: {connection.api_base} - Version: {connection.api_version}"
|
|
f" - Deployment: {deployment_name}"
|
|
)
|
|
client = AzureOpenAI(
|
|
azure_endpoint=connection.api_base,
|
|
api_version=connection.api_version,
|
|
api_key=connection.api_key,
|
|
)
|
|
|
|
message = {"role": "system", "content": prompt_with_src_and_gen}
|
|
try:
|
|
for attempt in Retrying(
|
|
reraise=True,
|
|
before_sleep=before_sleep_log(logger, logging.INFO),
|
|
after=after_log(logger, logging.INFO),
|
|
wait=wait_random_exponential(multiplier=1, min=1, max=120),
|
|
stop=stop_after_attempt(10),
|
|
):
|
|
with attempt:
|
|
response = client.chat.completions.create(
|
|
model=deployment_name,
|
|
messages=[message],
|
|
temperature=2,
|
|
max_tokens=5,
|
|
top_p=1,
|
|
frequency_penalty=0,
|
|
presence_penalty=0,
|
|
stop=None,
|
|
n=20,
|
|
)
|
|
except RetryError:
|
|
logger.exception(f"geval openai call failed\nInput prompt was: {message}")
|
|
raise
|
|
|
|
all_responses = []
|
|
for i in range(len(response.choices)):
|
|
try:
|
|
content = response.choices[i].message.content
|
|
all_responses.append(content)
|
|
except KeyError:
|
|
# `content` won't exist in returned json when openai content_filter is triggered
|
|
logger.exception(
|
|
f"""data with key missing was: {response.choices[i]}\nInput prompt was: {message}"""
|
|
)
|
|
|
|
return aggregate_llm_scores(all_responses, max_score=max_score)
|
|
|
|
|
|
def aggregate_llm_scores(llm_responses: List[str], max_score: int) -> float:
|
|
"""Parse and average valid scores from the generated responses of
|
|
the G-Eval LLM call.
|
|
|
|
Args:
|
|
llm_responses (List[str]): List of scores from multiple LLMs
|
|
max_score (float): The maximum score allowed.
|
|
|
|
Returns:
|
|
float: The average of all the valid scores
|
|
"""
|
|
all_scores = []
|
|
error_count = 0
|
|
for generated in llm_responses:
|
|
try:
|
|
parsed = parse_output(generated, max_score)
|
|
all_scores.append(parsed)
|
|
except ValueError as e:
|
|
logger.warning(e)
|
|
error_count += 1
|
|
if error_count:
|
|
logger.warning(
|
|
f"{error_count} out of 20 scores were discarded due to corrupt g-eval generation"
|
|
)
|
|
score = sum(all_scores) / len(all_scores)
|
|
return score
|