Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:39:52 +08:00

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