1044 lines
41 KiB
Python
1044 lines
41 KiB
Python
# Copyright 2025 Google LLC
|
|
#
|
|
# 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 datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
import pandas as pd
|
|
import streamlit as st
|
|
import vertexai
|
|
from dotenv import load_dotenv
|
|
from google import genai
|
|
from google.cloud import storage
|
|
from pydantic import BaseModel
|
|
from src.gcp_prompt import GcpPrompt as gcp_prompt
|
|
from vertexai.evaluation import (
|
|
EvalTask,
|
|
MetricPromptTemplateExamples,
|
|
PairwiseMetricPromptTemplate,
|
|
PointwiseMetricPromptTemplate,
|
|
)
|
|
from vertexai.preview import prompts
|
|
|
|
load_dotenv("src/.env")
|
|
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_metric_object_by_name(metric_name: str):
|
|
"""Retrieves a metric template object from its string name."""
|
|
try:
|
|
return MetricPromptTemplateExamples._PROMPT_TEMPLATE_MAP[metric_name]
|
|
except Exception as e:
|
|
logger.exception(f"Failed to get metric object for {metric_name}: {e}")
|
|
raise
|
|
|
|
|
|
def refresh_bucket() -> list[str]:
|
|
"""Refreshes the list of available dataset URIs from the GCS bucket.
|
|
|
|
This function lists all blobs in the configured GCS bucket, filters for
|
|
CSV and JSONL files located within the 'datasets/' prefix, and constructs a list
|
|
of their full gs:// URI paths.
|
|
|
|
Returns:
|
|
A list of strings, where each string is a GCS URI to a dataset file.
|
|
"""
|
|
logger.info("Bucket: %s", os.getenv("BUCKET"))
|
|
bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET"))
|
|
blobs = bucket.list_blobs()
|
|
data_uris = []
|
|
for i in blobs:
|
|
if i.name.split("/")[0] == "datasets" and (
|
|
i.name.endswith(".csv") or i.name.endswith(".jsonl")
|
|
):
|
|
data_uris.append(f"gs://{i.bucket.name}/{i.name}")
|
|
logger.info("Data URIs: %s", data_uris)
|
|
return data_uris
|
|
|
|
|
|
def get_autorater_pairwise_response(metric_prompt: str, model: str) -> dict:
|
|
"""Gets a response from the autorater model for pairwise evaluation.
|
|
|
|
Args:
|
|
metric_prompt: The prompt to send to the autorater model.
|
|
model: The name of the evaluation model to use.
|
|
|
|
Returns:
|
|
A dictionary containing the autorater's response.
|
|
"""
|
|
|
|
class AutoraterResponse(BaseModel):
|
|
pairwise_choice: str
|
|
explanation: str
|
|
|
|
client = genai.Client(
|
|
vertexai=True,
|
|
project=os.getenv("PROJECT_ID"),
|
|
location=os.getenv("LOCATION"),
|
|
)
|
|
|
|
response = client.models.generate_content(
|
|
model=model,
|
|
contents=metric_prompt,
|
|
config={
|
|
"response_mime_type": "application/json",
|
|
"response_schema": AutoraterResponse,
|
|
"safety_settings": [
|
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
|
{
|
|
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
"threshold": "BLOCK_NONE",
|
|
},
|
|
{
|
|
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
"threshold": "BLOCK_NONE",
|
|
},
|
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
|
],
|
|
},
|
|
)
|
|
|
|
if response.parsed:
|
|
return response.parsed.model_dump()
|
|
|
|
try:
|
|
return json.loads(response.text)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to parse response: {e}")
|
|
return {}
|
|
|
|
|
|
def format_display_text(text):
|
|
"""Formats text for display in st.text_area, handling dicts/lists as JSON."""
|
|
if isinstance(text, (dict, list)):
|
|
try:
|
|
return json.dumps(text, indent=2)
|
|
except (TypeError, ValueError):
|
|
return str(text)
|
|
return str(text)
|
|
|
|
|
|
def main() -> None:
|
|
"""Initializes and runs the Streamlit evaluation application.
|
|
|
|
This function sets up the Streamlit page configuration, initializes session state
|
|
variables, and orchestrates the user interface for the evaluation workflow.
|
|
It handles dataset and prompt selection, response generation or loading,
|
|
human-in-the-loop rating, and the launching of automated evaluations.
|
|
"""
|
|
st.set_page_config(
|
|
layout="wide",
|
|
page_title="Prompt Engineering App",
|
|
page_icon="assets/favicon.ico",
|
|
)
|
|
|
|
st.header("Evaluation Set-Up")
|
|
|
|
if "storage_client" not in st.session_state:
|
|
st.session_state["storage_client"] = storage.Client()
|
|
|
|
if "data_uris" not in st.session_state:
|
|
st.session_state["data_uris"] = refresh_bucket()
|
|
|
|
if "current_index" not in st.session_state:
|
|
st.session_state.current_index = 0
|
|
|
|
if "eval_result" not in st.session_state:
|
|
st.session_state.eval_result = None
|
|
|
|
if "custom_eval_result" not in st.session_state:
|
|
st.session_state.custom_eval_result = None
|
|
|
|
if "df_data" not in st.session_state:
|
|
st.session_state.df_data = None
|
|
|
|
if "df_dataset_eval" not in st.session_state:
|
|
st.session_state.df_dataset_eval = None
|
|
|
|
if "all_metrics_eval" not in st.session_state:
|
|
st.session_state.all_metrics_eval = None
|
|
|
|
if "metrics" not in st.session_state:
|
|
st.session_state.metrics = None
|
|
|
|
if "local_prompt" not in st.session_state:
|
|
st.session_state.local_prompt = gcp_prompt()
|
|
|
|
if "cached_data_files" not in st.session_state:
|
|
st.session_state.cached_data_files = {}
|
|
if "last_selected_dataset_for_cache" not in st.session_state:
|
|
st.session_state.last_selected_dataset_for_cache = None
|
|
|
|
if "cached_prompt_versions" not in st.session_state:
|
|
st.session_state.cached_prompt_versions = {}
|
|
|
|
if "last_selected_prompt_for_versions_cache" not in st.session_state:
|
|
st.session_state.last_selected_prompt_for_versions_cache = None
|
|
|
|
if "human_rated_dict" not in st.session_state:
|
|
st.session_state.human_rated_dict = {}
|
|
|
|
if "metric_preview_index" not in st.session_state:
|
|
st.session_state.metric_preview_index = 0
|
|
|
|
if "vertex_session_init" not in st.session_state:
|
|
vertexai.init(
|
|
project=os.getenv("PROJECT_ID"),
|
|
location=os.getenv("LOCATION"),
|
|
staging_bucket=os.getenv("BUCKET"),
|
|
experiment=os.getenv("EXPERIMENT_NAME"),
|
|
)
|
|
st.session_state.vertex_session_init = True
|
|
|
|
data_sets = list({i.split("/")[4] for i in st.session_state.data_uris})
|
|
logger.info(f"Data Sets: {data_sets}")
|
|
st.selectbox(
|
|
"Select an Existing Dataset", options=[None, *data_sets], key="selected_dataset"
|
|
)
|
|
|
|
files_to_display_in_selectbox = []
|
|
if st.session_state.selected_dataset:
|
|
if (
|
|
st.session_state.selected_dataset
|
|
!= st.session_state.last_selected_dataset_for_cache
|
|
or st.session_state.selected_dataset
|
|
not in st.session_state.cached_data_files
|
|
):
|
|
logger.info(
|
|
"Cache miss or dataset changed for files. Fetching for: %s",
|
|
st.session_state.selected_dataset,
|
|
)
|
|
bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET"))
|
|
prefix = f"datasets/{st.session_state.selected_dataset}/"
|
|
blobs_iterator = bucket.list_blobs(prefix=prefix)
|
|
|
|
current_dataset_files = []
|
|
for blob in blobs_iterator:
|
|
if (
|
|
blob.name.endswith(".csv") or blob.name.endswith(".jsonl")
|
|
) and not blob.name.endswith("/"):
|
|
filename = blob.name[len(prefix) :]
|
|
if filename:
|
|
current_dataset_files.append(filename)
|
|
|
|
st.session_state.cached_data_files[st.session_state.selected_dataset] = (
|
|
sorted(set(current_dataset_files))
|
|
)
|
|
st.session_state.last_selected_dataset_for_cache = (
|
|
st.session_state.selected_dataset
|
|
)
|
|
logger.info(
|
|
"Cached files for %s: %s",
|
|
st.session_state.selected_dataset,
|
|
st.session_state.cached_data_files[st.session_state.selected_dataset],
|
|
)
|
|
|
|
if "selected_file_from_dataset" in st.session_state:
|
|
st.session_state.selected_file_from_dataset = None
|
|
logger.info("Reset selected_file_from_dataset due to dataset change.")
|
|
|
|
files_to_display_in_selectbox = st.session_state.cached_data_files.get(
|
|
st.session_state.selected_dataset, []
|
|
)
|
|
|
|
st.selectbox(
|
|
"Select a file from this dataset:",
|
|
options=[None, *files_to_display_in_selectbox],
|
|
key="selected_file_from_dataset",
|
|
)
|
|
|
|
st.text_input("Number of Samples", key="n_samples")
|
|
|
|
st.text_input(
|
|
"Ground Truth Column Name",
|
|
key="ground_truth_column_name",
|
|
value="target",
|
|
help="The name of the column in your dataset that contains the ground truth or target response.",
|
|
)
|
|
|
|
st.selectbox(
|
|
"Select Existing Prompt",
|
|
options=[None, *list(st.session_state.local_prompt.existing_prompts.keys())],
|
|
placeholder="Select Prompt...",
|
|
key="selected_prompt",
|
|
)
|
|
|
|
versions_to_display_in_selectbox = []
|
|
if st.session_state.selected_prompt:
|
|
st.session_state.local_prompt.prompt_meta["name"] = (
|
|
st.session_state.selected_prompt
|
|
)
|
|
|
|
selected_prompt_obj = st.session_state.local_prompt.existing_prompts[
|
|
st.session_state.selected_prompt
|
|
]
|
|
prompt_resource_name_for_cache = str(selected_prompt_obj)
|
|
|
|
if (
|
|
st.session_state.selected_prompt
|
|
!= st.session_state.last_selected_prompt_for_versions_cache
|
|
or prompt_resource_name_for_cache
|
|
not in st.session_state.cached_prompt_versions
|
|
):
|
|
logger.info(
|
|
"Cache miss or prompt changed for versions. Fetching for: %s",
|
|
st.session_state.selected_prompt,
|
|
)
|
|
fetched_versions = [
|
|
v.version_id for v in prompts.list_versions(selected_prompt_obj)
|
|
]
|
|
|
|
st.session_state.cached_prompt_versions[prompt_resource_name_for_cache] = (
|
|
fetched_versions
|
|
)
|
|
st.session_state.last_selected_prompt_for_versions_cache = (
|
|
st.session_state.selected_prompt
|
|
)
|
|
logger.info(
|
|
"Cached versions for %s: %s",
|
|
st.session_state.selected_prompt,
|
|
fetched_versions,
|
|
)
|
|
|
|
if "selected_version" in st.session_state:
|
|
st.session_state.selected_version = None
|
|
logger.info("Reset selected_version due to prompt change.")
|
|
versions_to_display_in_selectbox = st.session_state.cached_prompt_versions.get(
|
|
prompt_resource_name_for_cache, []
|
|
)
|
|
|
|
st.selectbox(
|
|
"Select Version",
|
|
options=versions_to_display_in_selectbox,
|
|
placeholder="Select Version...",
|
|
key="selected_version",
|
|
)
|
|
|
|
st.button("Load Prompt", key="load_prompt_button")
|
|
if st.session_state.load_prompt_button:
|
|
if not st.session_state.get("selected_prompt"):
|
|
st.warning("Please select a prompt before loading.")
|
|
else:
|
|
logger.info(
|
|
f"Selected Prompt ID: {st.session_state.local_prompt.existing_prompts[st.session_state.selected_prompt]}"
|
|
)
|
|
logger.info(f"Version: {st.session_state.selected_version}")
|
|
st.session_state.local_prompt.load_prompt(
|
|
st.session_state.local_prompt.existing_prompts[
|
|
st.session_state.selected_prompt
|
|
],
|
|
st.session_state.selected_prompt,
|
|
st.session_state.selected_version,
|
|
)
|
|
logger.info(
|
|
f"Local Prompt Meta: {st.session_state.local_prompt.prompt_meta}"
|
|
)
|
|
logger.info(
|
|
f"Local Prompt Meta Dict Keys: {st.session_state.local_prompt.prompt_meta.keys()}"
|
|
)
|
|
|
|
st.session_state.prompt_name = (
|
|
st.session_state.local_prompt.prompt_to_run.prompt_name
|
|
)
|
|
st.session_state.prompt_data = (
|
|
st.session_state.local_prompt.prompt_to_run.prompt_data
|
|
)
|
|
st.session_state.model_name = (
|
|
st.session_state.local_prompt.prompt_to_run.model_name.split("/")[-1]
|
|
)
|
|
st.session_state.system_instructions = (
|
|
st.session_state.local_prompt.prompt_to_run.system_instruction
|
|
)
|
|
st.session_state.response_schema = json.dumps(
|
|
st.session_state.local_prompt.prompt_meta.get("response_schema", {})
|
|
)
|
|
st.session_state.generation_config = json.dumps(
|
|
st.session_state.local_prompt.prompt_meta.get("generation_config", {})
|
|
)
|
|
st.session_state.meta_tags = st.session_state.local_prompt.prompt_meta[
|
|
"meta_tags"
|
|
]
|
|
|
|
st.button("Upload Data and Get Responses", key="upload_data_get_responses_button")
|
|
|
|
if (
|
|
st.session_state.upload_data_get_responses_button
|
|
and st.session_state.n_samples
|
|
and st.session_state.selected_dataset
|
|
):
|
|
if not st.session_state.n_samples:
|
|
st.warning("Please enter the Number of Samples.")
|
|
return
|
|
if not st.session_state.selected_dataset:
|
|
st.warning("Please select an Existing Dataset.")
|
|
return
|
|
if not st.session_state.selected_file_from_dataset:
|
|
st.warning("Please select a file from the dataset.")
|
|
return
|
|
|
|
try:
|
|
num_samples = int(st.session_state.n_samples)
|
|
if num_samples <= 0:
|
|
st.warning("Number of Samples must be a positive integer.")
|
|
return
|
|
except ValueError:
|
|
st.warning("Number of Samples must be a valid integer.")
|
|
return
|
|
|
|
gcs_path = f"gs://{os.getenv('BUCKET')}/datasets/{st.session_state.selected_dataset}/{st.session_state.selected_file_from_dataset}"
|
|
st.session_state["input_data_uri"] = gcs_path
|
|
try:
|
|
if gcs_path.endswith(".csv"):
|
|
df_full = pd.read_csv(gcs_path)
|
|
elif gcs_path.endswith(".jsonl"):
|
|
df_full = pd.read_json(gcs_path, lines=True)
|
|
else:
|
|
st.error(
|
|
f"Unsupported file type: {gcs_path.rsplit('.', maxsplit=1)[-1]}"
|
|
)
|
|
return
|
|
except Exception as e:
|
|
st.error(f"Error reading data from {gcs_path}: {e}")
|
|
return
|
|
|
|
df = df_full.iloc[:num_samples]
|
|
if df.empty:
|
|
st.warning(
|
|
"No data found for the first %s samples in %s, or the file is smaller than requested.",
|
|
num_samples,
|
|
st.session_state.selected_file_from_dataset,
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.ratings = []
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
return
|
|
|
|
user_input_list = []
|
|
expected_result_list = []
|
|
assistant_response_list = []
|
|
baseline_model_response_list = []
|
|
|
|
generate = False
|
|
ground_truth_col = st.session_state.ground_truth_column_name
|
|
required_cols_for_loading_existing = [
|
|
"user_input",
|
|
ground_truth_col,
|
|
"assistant_response",
|
|
]
|
|
|
|
if "assistant_response" in df.columns:
|
|
missing_loading_cols = [
|
|
col
|
|
for col in required_cols_for_loading_existing
|
|
if col not in df.columns
|
|
]
|
|
if not missing_loading_cols:
|
|
logger.info("Sufficient columns found to load existing responses.")
|
|
generate = False
|
|
else:
|
|
st.error(
|
|
f"The file has 'assistant_response' column, but is missing other essential columns for loading: {missing_loading_cols}. Required for loading: {required_cols_for_loading_existing}. Found columns: {df.columns.tolist()}",
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.ratings = []
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
return
|
|
else:
|
|
if not st.session_state.get("prompt_data"):
|
|
st.error(
|
|
"To generate new responses, please load a prompt first using the 'Load Prompt' button."
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.ratings = []
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
return
|
|
|
|
template_vars = re.findall(r"{(\w+)}", st.session_state.prompt_data)
|
|
required_cols_for_generating_new = list(set(template_vars))
|
|
|
|
all_required_cols = [*required_cols_for_generating_new, ground_truth_col]
|
|
missing_generating_cols = [
|
|
col for col in all_required_cols if col not in df.columns
|
|
]
|
|
|
|
if not missing_generating_cols:
|
|
logger.info(
|
|
"'assistant_response' column not found. Required columns for generating new responses are present. Will generate."
|
|
)
|
|
generate = True
|
|
else:
|
|
st.error(
|
|
f"The file does not have 'assistant_response' column, and is also missing columns required for generating new responses based on the loaded prompt: {missing_generating_cols}. Required for generation: {all_required_cols}. Found columns: {df.columns.tolist()}",
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.ratings = []
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
return
|
|
|
|
logger.info("Generate flag set to: %s for %s samples.", generate, len(df))
|
|
|
|
if generate:
|
|
logger.info("Proceeding with generating new assistant responses.")
|
|
if (
|
|
not st.session_state.selected_prompt
|
|
or not st.session_state.selected_version
|
|
):
|
|
st.error(
|
|
"A prompt and version must be loaded to generate new responses. Please use the 'Load Prompt' button."
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.ratings = []
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
return
|
|
if not st.session_state.local_prompt.prompt_to_run.prompt_data:
|
|
st.error(
|
|
"Prompt data is missing from the loaded prompt. Cannot generate. Please re-load the prompt using 'Load Prompt'."
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.ratings = []
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
return
|
|
|
|
import concurrent.futures
|
|
|
|
template_vars = re.findall(r"{(\w+)}", st.session_state.prompt_data)
|
|
required_cols_for_generating_new = list(set(template_vars))
|
|
|
|
tasks = []
|
|
for idx, r in df.iterrows():
|
|
current_user_input_item = {
|
|
col: r[col] for col in required_cols_for_generating_new
|
|
}
|
|
expected_res = r[st.session_state.ground_truth_column_name]
|
|
tasks.append((idx, current_user_input_item, expected_res))
|
|
|
|
# Bind the function reference to avoid accessing st.session_state directly in threads
|
|
generate_function = st.session_state.local_prompt.generate_response
|
|
|
|
def process_task(task_args):
|
|
t_idx, item, expected = task_args
|
|
try:
|
|
res = generate_function(item)
|
|
return t_idx, item, expected, res, None
|
|
except Exception as e:
|
|
return t_idx, item, expected, None, e
|
|
|
|
completed = 0
|
|
total = len(tasks)
|
|
results = []
|
|
|
|
if total > 0:
|
|
progress_bar = st.progress(
|
|
0, text="Generating responses in parallel..."
|
|
)
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
future_to_idx = {
|
|
executor.submit(process_task, t): t[0] for t in tasks
|
|
}
|
|
for future in concurrent.futures.as_completed(future_to_idx):
|
|
t_idx, item, expected, res, error = future.result()
|
|
completed += 1
|
|
|
|
progress_text = f"Generating response {completed} of {total}..."
|
|
progress_bar.progress(completed / total, text=progress_text)
|
|
|
|
if error:
|
|
logger.error(
|
|
"Error generating response for row index %s: %s",
|
|
t_idx,
|
|
error,
|
|
)
|
|
else:
|
|
results.append((t_idx, item, expected, res))
|
|
|
|
progress_bar.empty()
|
|
if len(results) < total:
|
|
st.warning(
|
|
f"Generated {len(results)} responses. {total - len(results)} failed."
|
|
)
|
|
|
|
results.sort(key=lambda x: x[0])
|
|
for t_idx, item, expected, res in results:
|
|
user_input_list.append(item)
|
|
expected_result_list.append(expected)
|
|
assistant_response_list.append(res)
|
|
|
|
if len(user_input_list) < len(df):
|
|
st.info(
|
|
f"Successfully generated responses for {len(user_input_list)} out of {len(df)} requested samples due to errors during generation."
|
|
)
|
|
else:
|
|
logger.info(
|
|
"Proceeding with loading existing assistant responses from file."
|
|
)
|
|
parsed_user_inputs_temp = []
|
|
for item_str in df["user_input"].astype(str).tolist():
|
|
try:
|
|
parsed_user_inputs_temp.append(json.loads(item_str))
|
|
except json.JSONDecodeError:
|
|
logger.debug(
|
|
"User input item is not valid JSON, using as raw string: %s",
|
|
item_str[:100],
|
|
)
|
|
parsed_user_inputs_temp.append(item_str)
|
|
user_input_list = parsed_user_inputs_temp
|
|
expected_result_list = df[
|
|
st.session_state.ground_truth_column_name
|
|
].tolist()
|
|
assistant_response_list = df.assistant_response.tolist()
|
|
baseline_model_response_list = []
|
|
if "baseline_model_response" in df.columns:
|
|
baseline_model_response_list = df.baseline_model_response.tolist()
|
|
|
|
st.session_state.human_rated_dict = {
|
|
"user_input": user_input_list,
|
|
"ground_truth": expected_result_list,
|
|
"assistant_response": assistant_response_list,
|
|
}
|
|
if baseline_model_response_list:
|
|
st.session_state.human_rated_dict["baseline_model_response"] = (
|
|
baseline_model_response_list
|
|
)
|
|
num_processed_items = len(user_input_list)
|
|
|
|
if num_processed_items > 0:
|
|
st.session_state.include_in_evaluations = [True] * num_processed_items
|
|
st.session_state.current_index = 0
|
|
st.success(f"Successfully processed {num_processed_items} samples.")
|
|
else:
|
|
st.warning(
|
|
"No data items were processed successfully. Check logs for errors or review file structure."
|
|
)
|
|
st.session_state.human_rated_dict = {}
|
|
st.session_state.include_in_evaluations = []
|
|
st.session_state.current_index = 0
|
|
|
|
st.divider()
|
|
|
|
if st.session_state.human_rated_dict:
|
|
st.title("Review Responses")
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
with col1:
|
|
st.subheader("User Input")
|
|
st.text_area(
|
|
label="User's original query/text",
|
|
value=format_display_text(
|
|
st.session_state.human_rated_dict["user_input"][
|
|
st.session_state.current_index
|
|
]
|
|
),
|
|
height=200,
|
|
disabled=True,
|
|
)
|
|
|
|
with col2:
|
|
st.subheader("Ground Truth")
|
|
st.text_area(
|
|
label="The ideal/target response",
|
|
value=format_display_text(
|
|
st.session_state.human_rated_dict["ground_truth"][
|
|
st.session_state.current_index
|
|
]
|
|
),
|
|
height=200,
|
|
disabled=True,
|
|
)
|
|
|
|
with col3:
|
|
st.subheader("Assistant Response")
|
|
st.text_area(
|
|
label="The assistant's generated response",
|
|
value=format_display_text(
|
|
st.session_state.human_rated_dict["assistant_response"][
|
|
st.session_state.current_index
|
|
]
|
|
),
|
|
height=200,
|
|
disabled=True,
|
|
)
|
|
|
|
eval_include = st.checkbox(
|
|
"Include in Evaluation",
|
|
value=st.session_state.include_in_evaluations[
|
|
st.session_state.current_index
|
|
],
|
|
)
|
|
|
|
if (
|
|
st.session_state.include_in_evaluations
|
|
and eval_include
|
|
!= st.session_state.include_in_evaluations[st.session_state.current_index]
|
|
):
|
|
st.session_state.include_in_evaluations[st.session_state.current_index] = (
|
|
eval_include
|
|
)
|
|
|
|
st.markdown("---")
|
|
col_prev, col_spacer, col_next = st.columns([1, 3, 1])
|
|
|
|
with col_prev:
|
|
if st.button("Previous", disabled=(st.session_state.current_index == 0)):
|
|
st.session_state.current_index -= 1
|
|
st.rerun()
|
|
|
|
with col_next:
|
|
if st.button(
|
|
"Next",
|
|
disabled=(
|
|
st.session_state.current_index
|
|
== len(st.session_state.human_rated_dict["user_input"]) - 1
|
|
),
|
|
):
|
|
st.session_state.current_index += 1
|
|
st.rerun()
|
|
|
|
st.markdown(
|
|
f"<p style='text-align: center; font-size: 1.2em;'>Case {st.session_state.current_index + 1} of {len(st.session_state.human_rated_dict['user_input'])}</p>",
|
|
unsafe_allow_html=True,
|
|
)
|
|
|
|
st.markdown("---")
|
|
st.subheader("Launch Eval")
|
|
|
|
st.subheader("Metrics Selection")
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
st.write("**Model-Based**")
|
|
metric_names = MetricPromptTemplateExamples.list_example_metric_names()
|
|
selected_model_based_metrics = st.multiselect(
|
|
"Select from model-based metrics",
|
|
metric_names,
|
|
key="selected_model_based_metrics",
|
|
label_visibility="collapsed",
|
|
)
|
|
if selected_model_based_metrics:
|
|
num_metrics = len(selected_model_based_metrics)
|
|
if st.session_state.metric_preview_index >= num_metrics:
|
|
st.session_state.metric_preview_index = 0
|
|
|
|
current_metric_name = selected_model_based_metrics[
|
|
st.session_state.metric_preview_index
|
|
]
|
|
|
|
st.markdown(
|
|
f"**Previewing Template: {current_metric_name} ({st.session_state.metric_preview_index + 1}/{num_metrics})**"
|
|
)
|
|
|
|
try:
|
|
metric_object = get_metric_object_by_name(current_metric_name)
|
|
if isinstance(
|
|
metric_object,
|
|
PointwiseMetricPromptTemplate | PairwiseMetricPromptTemplate,
|
|
):
|
|
st.text_area(
|
|
"Template Preview",
|
|
metric_object.metric_prompt_template,
|
|
height=200,
|
|
)
|
|
except Exception as e:
|
|
st.error(
|
|
f"Could not retrieve template for {current_metric_name}: {e}"
|
|
)
|
|
|
|
if num_metrics > 1:
|
|
prev_col, next_col = st.columns(2)
|
|
with prev_col:
|
|
if st.button(
|
|
"Previous Template",
|
|
disabled=st.session_state.metric_preview_index <= 0,
|
|
):
|
|
st.session_state.metric_preview_index -= 1
|
|
st.rerun()
|
|
with next_col:
|
|
if st.button(
|
|
"Next Template",
|
|
disabled=st.session_state.metric_preview_index
|
|
>= num_metrics - 1,
|
|
):
|
|
st.session_state.metric_preview_index += 1
|
|
st.rerun()
|
|
|
|
with col2:
|
|
st.write("**Computation-Based Pointwise**")
|
|
computation_based_pointwise = [
|
|
"bleu",
|
|
"rouge_1",
|
|
"rouge_2",
|
|
"rouge_l",
|
|
"rouge_l_sum",
|
|
"exact_match",
|
|
]
|
|
st.multiselect(
|
|
"Select from computation-based pointwise metrics",
|
|
computation_based_pointwise,
|
|
key="selected_cbp",
|
|
label_visibility="collapsed",
|
|
)
|
|
|
|
st.selectbox(
|
|
"Select Evaluation Model",
|
|
options=[
|
|
"gemini-2.0-flash-lite",
|
|
"gemini-2.5-flash",
|
|
"gemini-2.5-pro",
|
|
],
|
|
key="selected_evaluation_model",
|
|
)
|
|
|
|
st.button("Launch Eval", key="launch_eval_button")
|
|
|
|
if st.session_state.launch_eval_button:
|
|
selected_mbp_names = st.session_state.get(
|
|
"selected_model_based_metrics", []
|
|
)
|
|
selected_cbp_metrics = st.session_state.get("selected_cbp", [])
|
|
|
|
all_metrics = selected_mbp_names + selected_cbp_metrics
|
|
|
|
if not all_metrics:
|
|
st.warning("Please select at least one evaluation metric.")
|
|
return
|
|
|
|
evaluation_data_list = []
|
|
for idx, include_item in enumerate(st.session_state.include_in_evaluations):
|
|
if include_item:
|
|
user_input_values = st.session_state.human_rated_dict["user_input"][
|
|
idx
|
|
]
|
|
prompt_template = (
|
|
st.session_state.local_prompt.prompt_to_run.prompt_data
|
|
)
|
|
logger.info(f"Prompt template: {prompt_template}")
|
|
system_instruction = (
|
|
st.session_state.local_prompt.prompt_to_run.system_instruction
|
|
)
|
|
prediction = str(
|
|
st.session_state.human_rated_dict["assistant_response"][idx]
|
|
)
|
|
|
|
# Process reference value like in the old code
|
|
reference_val = st.session_state.human_rated_dict["ground_truth"][
|
|
idx
|
|
]
|
|
final_reference_str = ""
|
|
if isinstance(reference_val, int | float | bool):
|
|
final_reference_str = json.dumps({"value": reference_val})
|
|
elif isinstance(reference_val, str):
|
|
try:
|
|
parsed_json = json.loads(reference_val)
|
|
if isinstance(parsed_json, int | float | bool):
|
|
final_reference_str = json.dumps({"value": parsed_json})
|
|
else:
|
|
final_reference_str = reference_val
|
|
except json.JSONDecodeError:
|
|
final_reference_str = reference_val
|
|
elif isinstance(reference_val, dict | list):
|
|
final_reference_str = json.dumps(reference_val)
|
|
else:
|
|
final_reference_str = str(reference_val)
|
|
|
|
# Extract all variables from the template
|
|
template_vars = re.findall(r"\{(\w+)\}", prompt_template)
|
|
|
|
if template_vars:
|
|
# Template expects variables
|
|
if isinstance(user_input_values, dict):
|
|
# Ensure all expected vars are present, default to empty string if missing
|
|
formatted_input = {
|
|
v: user_input_values.get(v, "") for v in template_vars
|
|
}
|
|
instruction = prompt_template.format(**formatted_input)
|
|
# Input is not a dict but template expects vars
|
|
# Try to use the input as the first variable if there's only one, otherwise empty
|
|
elif len(template_vars) == 1:
|
|
instruction = prompt_template.format(
|
|
**{template_vars[0]: user_input_values}
|
|
)
|
|
else:
|
|
instruction = prompt_template # Fallback
|
|
else:
|
|
# No variables in template
|
|
instruction = prompt_template
|
|
context = system_instruction if system_instruction else ""
|
|
prompt_str = (
|
|
json.dumps(user_input_values)
|
|
if isinstance(user_input_values, dict)
|
|
else str(user_input_values)
|
|
)
|
|
|
|
eval_item = {
|
|
"context": context,
|
|
"instruction": instruction,
|
|
"prompt": prompt_str,
|
|
"prediction": prediction,
|
|
"reference": final_reference_str,
|
|
}
|
|
evaluation_data_list.append(eval_item)
|
|
|
|
if not evaluation_data_list:
|
|
st.warning(
|
|
"No items were selected for evaluation. Please check the 'Include in Evaluation' checkboxes."
|
|
)
|
|
return
|
|
|
|
df_dataset = pd.DataFrame(evaluation_data_list)
|
|
st.session_state.df_dataset_eval = df_dataset
|
|
st.session_state.all_metrics_eval = all_metrics
|
|
logger.info(f"Evaluation DataFrame columns: {df_dataset.columns.tolist()}")
|
|
logger.info(f"Evaluation DataFrame head:\n{df_dataset.head()}")
|
|
|
|
task = EvalTask(
|
|
dataset=df_dataset,
|
|
metrics=all_metrics,
|
|
experiment=os.getenv("EXPERIMENT_NAME"),
|
|
)
|
|
st.session_state.eval_result = task.evaluate(
|
|
response_column_name="prediction",
|
|
baseline_model_response_column_name="reference",
|
|
)
|
|
|
|
st.markdown("---")
|
|
st.subheader("View Eval")
|
|
|
|
st.button("View Evaluation Results", key="eval_results_button")
|
|
|
|
if st.session_state.eval_result and st.session_state.eval_results_button:
|
|
print(st.session_state.eval_result.metrics_table)
|
|
st.dataframe(st.session_state.eval_result.metrics_table)
|
|
if st.session_state.eval_result:
|
|
st.markdown("---")
|
|
st.subheader("Summary Scores")
|
|
|
|
mean_scores = {}
|
|
if (
|
|
st.session_state.eval_result
|
|
and hasattr(st.session_state.eval_result, "metrics_table")
|
|
and not st.session_state.eval_result.metrics_table.empty
|
|
):
|
|
for col in st.session_state.eval_result.metrics_table.columns:
|
|
if col.endswith("/score"):
|
|
scores = pd.to_numeric(
|
|
st.session_state.eval_result.metrics_table[col],
|
|
errors="coerce",
|
|
)
|
|
if not scores.dropna().empty:
|
|
mean_scores[col] = scores.dropna().mean()
|
|
if mean_scores:
|
|
for metric, score in mean_scores.items():
|
|
st.metric(label=f"Mean {metric}", value=f"{score:.2f}")
|
|
else:
|
|
st.metric(
|
|
label="Mean Automated Score",
|
|
value="N/A",
|
|
)
|
|
|
|
st.markdown("---")
|
|
st.subheader("Save to Prompt Records")
|
|
save_to_records = st.checkbox(
|
|
"I want to save the results of this evaluation to the prompt records.",
|
|
key="save_to_records_checkbox",
|
|
)
|
|
if st.button("Save to Prompt Records", key="save_to_records_button"):
|
|
if save_to_records:
|
|
prompt_name = st.session_state.selected_prompt
|
|
prompt_version = st.session_state.selected_version
|
|
data_file = st.session_state.input_data_uri
|
|
|
|
if "df_dataset_eval" in st.session_state:
|
|
data = st.session_state.df_dataset_eval.to_dict(
|
|
orient="records"
|
|
)
|
|
else:
|
|
st.error(
|
|
"Evaluation data not found in session state. Please re-run evaluation."
|
|
)
|
|
return
|
|
|
|
if "all_metrics_eval" in st.session_state:
|
|
metrics = st.session_state.all_metrics_eval
|
|
else:
|
|
st.error(
|
|
"Metrics not found in session state. Please re-run evaluation."
|
|
)
|
|
return
|
|
|
|
scores_df = st.session_state.eval_result.metrics_table
|
|
scores = scores_df.to_dict(orient="records")
|
|
|
|
mean_scores_to_save = {}
|
|
for col in scores_df.columns:
|
|
if col.endswith("/score"):
|
|
s = pd.to_numeric(scores_df[col], errors="coerce")
|
|
if not s.dropna().empty:
|
|
mean_scores_to_save[col] = s.dropna().mean()
|
|
|
|
record_data = {
|
|
"prompt_name": prompt_name,
|
|
"prompt_version": prompt_version,
|
|
"data_file": data_file,
|
|
"metrics": metrics,
|
|
"mean_scores": mean_scores_to_save,
|
|
"scores": scores,
|
|
"evaluation_data": data,
|
|
"timestamp": datetime.datetime.now().isoformat(),
|
|
}
|
|
|
|
try:
|
|
filename = f"record_{prompt_name}_v{prompt_version}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.json"
|
|
bucket = st.session_state.storage_client.bucket(
|
|
os.getenv("BUCKET")
|
|
)
|
|
blob = bucket.blob(f"records/{filename}")
|
|
|
|
json_data = json.dumps(record_data, indent=4)
|
|
blob.upload_from_string(
|
|
json_data, content_type="application/json"
|
|
)
|
|
|
|
gcs_path = f"gs://{os.getenv('BUCKET')}/records/{filename}"
|
|
|
|
st.success(
|
|
f"Successfully saved to prompt records at: {gcs_path}"
|
|
)
|
|
st.json(json_data)
|
|
except Exception as e:
|
|
st.error(f"Failed to save to GCS: {e}")
|
|
else:
|
|
st.warning(
|
|
"Please check the box to confirm you want to save to prompt records."
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|