e768098d0e
Flake8 Lint / flake8 (push) Waiting to run
Spell check CI / Spell_Check (push) Waiting to run
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
171 lines
5.3 KiB
Python
171 lines
5.3 KiB
Python
import json
|
|
import re
|
|
from collections import namedtuple
|
|
|
|
from numpy.random import default_rng
|
|
|
|
from promptflow.connections import AzureOpenAIConnection, OpenAIConnection
|
|
from promptflow.tools.aoai import chat as aoai_chat
|
|
from promptflow.tools.openai import chat as openai_chat
|
|
|
|
|
|
class QuestionType:
|
|
SIMPLE = "simple"
|
|
# MULTI_CONTEXT = "multi_context"
|
|
|
|
|
|
class ValidateObj:
|
|
QUESTION = "validate_question"
|
|
TEXT_CHUNK = "validate_text_chunk"
|
|
SUGGESTED_ANSWER = "validate_suggested_answer"
|
|
|
|
|
|
class ResponseFormat:
|
|
TEXT = "text"
|
|
JSON = "json_object"
|
|
|
|
|
|
class ErrorMsg:
|
|
INVALID_JSON_FORMAT = "Invalid json format. Response: {0}"
|
|
INVALID_TEXT_CHUNK = "Skipping generating seed question due to invalid text chunk: {0}"
|
|
INVALID_QUESTION = "Invalid seed question: {0}"
|
|
INVALID_ANSWER = "Invalid answer: {0}"
|
|
|
|
|
|
ValidationResult = namedtuple("ValidationResult", ["pass_validation", "reason"])
|
|
ScoreResult = namedtuple("ScoreResult", ["score", "reason", "pass_validation"])
|
|
|
|
|
|
def llm_call(
|
|
connection, model, deployment_name, prompt, response_format=ResponseFormat.TEXT, temperature=1.0, max_tokens=None
|
|
):
|
|
response_format = "json_object" if response_format.lower() == "json" else response_format
|
|
# avoid unnecessary jinja2 template re-rendering and potential error.
|
|
prompt = f"{{% raw %}}{prompt}{{% endraw %}}"
|
|
if isinstance(connection, AzureOpenAIConnection):
|
|
return aoai_chat(
|
|
connection=connection,
|
|
prompt=prompt,
|
|
deployment_name=deployment_name,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
response_format={"type": response_format},
|
|
)
|
|
elif isinstance(connection, OpenAIConnection):
|
|
return openai_chat(
|
|
connection=connection,
|
|
prompt=prompt,
|
|
model=model,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
response_format={"type": response_format},
|
|
)
|
|
|
|
|
|
def get_question_type(testset_distribution) -> str:
|
|
"""
|
|
Decides question evolution type based on probability
|
|
"""
|
|
rng = default_rng()
|
|
prob = rng.uniform(0, 1)
|
|
return next((key for key in testset_distribution.keys() if prob <= testset_distribution[key]), QuestionType.SIMPLE)
|
|
|
|
|
|
def get_suggested_answer_validation_res(
|
|
connection,
|
|
model,
|
|
deployment_name,
|
|
prompt,
|
|
suggested_answer: str,
|
|
temperature: float,
|
|
max_tokens: int = None,
|
|
response_format: ResponseFormat = ResponseFormat.TEXT,
|
|
):
|
|
rsp = llm_call(
|
|
connection,
|
|
model,
|
|
deployment_name,
|
|
prompt,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
response_format=response_format,
|
|
)
|
|
return retrieve_verdict_and_print_reason(
|
|
rsp=rsp, validate_obj_name=ValidateObj.SUGGESTED_ANSWER, validate_obj=suggested_answer
|
|
)
|
|
|
|
|
|
def get_question_validation_res(
|
|
connection,
|
|
model,
|
|
deployment_name,
|
|
prompt,
|
|
question: str,
|
|
response_format: ResponseFormat,
|
|
temperature: float,
|
|
max_tokens: int = None,
|
|
):
|
|
rsp = llm_call(connection, model, deployment_name, prompt, response_format, temperature, max_tokens)
|
|
return retrieve_verdict_and_print_reason(rsp=rsp, validate_obj_name=ValidateObj.QUESTION, validate_obj=question)
|
|
|
|
|
|
def get_text_chunk_score(
|
|
connection,
|
|
model,
|
|
deployment_name,
|
|
prompt,
|
|
response_format: ResponseFormat,
|
|
score_threshold: float,
|
|
temperature: float,
|
|
max_tokens: int = None,
|
|
):
|
|
rsp = llm_call(connection, model, deployment_name, prompt, response_format, temperature, max_tokens)
|
|
data = _load_json_rsp(rsp)
|
|
score_float = 0
|
|
reason = ""
|
|
|
|
if data and isinstance(data, dict) and "score" in data and "reason" in data:
|
|
# Extract the verdict and reason
|
|
score = data["score"].lower()
|
|
reason = data["reason"]
|
|
print(f"Score {ValidateObj.TEXT_CHUNK}: {score}\nReason: {reason}")
|
|
try:
|
|
score_float = float(score)
|
|
except ValueError:
|
|
reason = ErrorMsg.INVALID_JSON_FORMAT.format(rsp)
|
|
else:
|
|
reason = ErrorMsg.INVALID_JSON_FORMAT.format(rsp)
|
|
pass_validation = score_float >= score_threshold
|
|
|
|
return ScoreResult(score_float, reason, pass_validation)
|
|
|
|
|
|
def retrieve_verdict_and_print_reason(rsp: str, validate_obj_name: str, validate_obj: str) -> ValidationResult:
|
|
data = _load_json_rsp(rsp)
|
|
|
|
if data and isinstance(data, dict) and "verdict" in data and "reason" in data:
|
|
# Extract the verdict and reason
|
|
verdict = data["verdict"].lower()
|
|
reason = data["reason"]
|
|
print(f"Is valid {validate_obj_name}: {verdict}\nReason: {reason}")
|
|
if verdict == "yes":
|
|
return ValidationResult(True, reason)
|
|
elif verdict == "no":
|
|
return ValidationResult(False, reason)
|
|
else:
|
|
print(f"Unexpected llm response to validate {validate_obj_name}: {validate_obj}")
|
|
|
|
return ValidationResult(False, ErrorMsg.INVALID_JSON_FORMAT.format(rsp))
|
|
|
|
|
|
def _load_json_rsp(rsp: str):
|
|
try:
|
|
# It is possible that even the response format is required as json, the response still contains ```json\n
|
|
rsp = re.sub(r"```json\n?|```", "", rsp)
|
|
data = json.loads(rsp)
|
|
except json.decoder.JSONDecodeError:
|
|
print(ErrorMsg.INVALID_JSON_FORMAT.format(rsp))
|
|
data = None
|
|
|
|
return data
|