591 lines
21 KiB
Python
591 lines
21 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
|
||
# https://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
|
||
|
||
"""Streamlit user interface for managing prompts in the LLM EvalKit.
|
||
|
||
This page provides a comprehensive interface for prompt engineering, allowing users
|
||
to create, load, edit, and test prompts that are stored and versioned in a
|
||
backend service (e.g., Google Cloud's Vertex AI Prompt Management).
|
||
|
||
The page is divided into two main sections:
|
||
1. **Create New Prompt**: A form to define a new prompt from scratch, including
|
||
its name, text, model, system instructions, and other metadata. Users can
|
||
test the prompt with sample input before saving it.
|
||
2. **Load & Edit Prompt**: A section to load existing prompts and their specific
|
||
versions. Users can modify the loaded prompt's details and save the changes
|
||
as a new version, facilitating iterative development and A/B testing.
|
||
|
||
Helper functions handle JSON parsing, data type conversions, and interactions
|
||
with the `gcp_prompt` object, which abstracts the backend communication.
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
from typing import Any
|
||
|
||
import streamlit as st
|
||
from dotenv import load_dotenv
|
||
from src.gcp_prompt import GcpPrompt as gcp_prompt
|
||
from vertexai.preview import prompts
|
||
|
||
# --- Initial Configuration ---
|
||
load_dotenv("src/.env")
|
||
logging.basicConfig(
|
||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# --- Constants ---
|
||
AVAILABLE_PROMPT_TASKS = [
|
||
"Classification",
|
||
"Summarization",
|
||
"Translation",
|
||
"Creative Writing",
|
||
"Q&A",
|
||
]
|
||
|
||
|
||
# --- Helper Functions ---
|
||
def _parse_json_input(json_string: str, field_name: str) -> dict[str, Any] | None:
|
||
"""Safely parses a JSON string from a text area.
|
||
|
||
Cleans the input string to handle common copy-paste errors and displays
|
||
an error in the Streamlit UI if parsing fails.
|
||
|
||
Args:
|
||
json_string: The raw string from a Streamlit text_area.
|
||
field_name: The user-facing name of the field for error messages.
|
||
|
||
Returns:
|
||
A dictionary if parsing is successful, otherwise None.
|
||
"""
|
||
if not json_string:
|
||
return None
|
||
try:
|
||
# Clean up common copy-paste issues like smart quotes and newlines
|
||
json_string_cleaned = (
|
||
json_string.replace("’", "'")
|
||
.replace("\n", " ")
|
||
.replace("\t", " ")
|
||
.replace("\r", "")
|
||
)
|
||
return json.loads(json_string_cleaned)
|
||
except json.JSONDecodeError as e:
|
||
st.error(f"Invalid JSON format for {field_name}: {e}")
|
||
return None
|
||
|
||
|
||
def _apply_generation_config_typing(config: dict[str, Any]) -> dict[str, Any]:
|
||
"""Applies correct data types to generation config parameters.
|
||
|
||
Streamlit text inputs return strings, but the underlying API requires
|
||
specific types (e.g., float for temperature). This function converts
|
||
common configuration values to their expected types.
|
||
|
||
Args:
|
||
config: The generation configuration dictionary with string values.
|
||
|
||
Returns:
|
||
The configuration dictionary with values cast to the correct types.
|
||
"""
|
||
if "temperature" in config:
|
||
config["temperature"] = float(config["temperature"])
|
||
if "top_p" in config:
|
||
config["top_p"] = float(config["top_p"])
|
||
if "max_output_tokens" in config:
|
||
config["max_output_tokens"] = int(config["max_output_tokens"])
|
||
return config
|
||
|
||
|
||
# --- Handlers for "Create New Prompt" Tab ---
|
||
def _handle_save_new_prompt() -> None:
|
||
"""Validates inputs and saves a new prompt.
|
||
|
||
Retrieves all necessary data from the Streamlit session state for the
|
||
"Create New Prompt" tab, validates that required fields are filled,
|
||
constructs the prompt object, and calls the backend service to save it.
|
||
Displays success or error messages in the UI.
|
||
"""
|
||
required_fields = {
|
||
"new_prompt_name": "Prompt Name",
|
||
"new_prompt_data": "Prompt Text",
|
||
"new_model_name": "Model Name",
|
||
"new_system_instructions": "System Instructions",
|
||
}
|
||
for key, name in required_fields.items():
|
||
if not st.session_state.get(key):
|
||
st.warning(f"Please enter a value for {name}.")
|
||
return
|
||
|
||
prompt_obj = st.session_state.local_prompt
|
||
prompt_obj.prompt_to_run.prompt_name = st.session_state.new_prompt_name
|
||
prompt_obj.prompt_to_run.prompt_data = st.session_state.new_prompt_data
|
||
prompt_obj.prompt_to_run.model_name = st.session_state.new_model_name.strip()
|
||
prompt_obj.prompt_to_run.system_instruction = (
|
||
st.session_state.new_system_instructions
|
||
)
|
||
|
||
response_schema = _parse_json_input(
|
||
st.session_state.new_response_schema, "Response Schema"
|
||
)
|
||
generation_config = _parse_json_input(
|
||
st.session_state.new_generation_config, "Generation Config"
|
||
)
|
||
|
||
if generation_config:
|
||
generation_config = _apply_generation_config_typing(generation_config)
|
||
if response_schema:
|
||
generation_config["response_schema"] = response_schema
|
||
prompt_obj.prompt_meta["generation_config"] = generation_config
|
||
|
||
if response_schema:
|
||
prompt_obj.prompt_meta["response_schema"] = response_schema
|
||
prompt_obj.prompt_meta["meta_tags"] = st.session_state.new_meta_tags
|
||
|
||
try:
|
||
logger.info("Saving new prompt...")
|
||
prompt_meta_info = prompt_obj.save_prompt(check_existing=True)
|
||
logger.info("Prompt saved successfully: %s", prompt_meta_info)
|
||
st.success("Prompt saved successfully!")
|
||
except Exception as e:
|
||
logger.error("Failed to save prompt: %s", e, exc_info=True)
|
||
st.error(f"Failed to save prompt: {e}")
|
||
|
||
|
||
def _handle_generate_test_for_new() -> None:
|
||
"""Generates a test response for the new prompt form.
|
||
|
||
Takes the user-provided sample input and the current prompt configuration
|
||
from the "Create" tab, sends it to the model for a response, and displays
|
||
the output in the UI. This allows for quick testing before saving.
|
||
"""
|
||
user_input_str = st.session_state.new_sample_user_input
|
||
if not user_input_str:
|
||
st.warning("Please provide sample user input to generate a response.")
|
||
return
|
||
|
||
sample_user_input = _parse_json_input(user_input_str, "User Input")
|
||
if sample_user_input is None:
|
||
return
|
||
|
||
try:
|
||
prompt_obj = st.session_state.local_prompt
|
||
prompt_obj.prompt_to_run.prompt_data = st.session_state.new_prompt_data
|
||
prompt_obj.prompt_to_run.model_name = st.session_state.new_model_name.strip()
|
||
prompt_obj.prompt_to_run.system_instruction = (
|
||
st.session_state.new_system_instructions
|
||
)
|
||
prompt_obj.prompt_meta["sample_user_input"] = sample_user_input
|
||
|
||
with st.spinner("Generating response..."):
|
||
response = prompt_obj.generate_response(sample_user_input)
|
||
st.session_state.new_sample_output = response
|
||
st.success("Prompt response generated!")
|
||
except Exception as e:
|
||
logger.error("Error during test generation: %s", e, exc_info=True)
|
||
st.error(f"An error occurred during generation: {e}")
|
||
|
||
|
||
# --- Handlers for "Load & Edit Prompt" Tab ---
|
||
def _populate_ui_from_prompt() -> None:
|
||
"""Populates session state for UI widgets from the loaded prompt object.
|
||
|
||
After a prompt is loaded from the backend, this function takes the data
|
||
from the `gcp_prompt` object and sets the corresponding values in the
|
||
Streamlit session state. This updates the "Load & Edit" tab's input
|
||
widgets to display the loaded prompt's information.
|
||
"""
|
||
prompt_obj = st.session_state.local_prompt
|
||
st.session_state.edit_prompt_name = prompt_obj.prompt_to_run.prompt_name
|
||
st.session_state.edit_prompt_data = prompt_obj.prompt_to_run.prompt_data
|
||
st.session_state.edit_model_name = prompt_obj.prompt_to_run.model_name.split("/")[
|
||
-1
|
||
]
|
||
st.session_state.edit_system_instructions = (
|
||
prompt_obj.prompt_to_run.system_instruction
|
||
)
|
||
st.session_state.edit_response_schema = json.dumps(
|
||
prompt_obj.prompt_meta.get("response_schema", {}), indent=2
|
||
)
|
||
st.session_state.edit_generation_config = json.dumps(
|
||
prompt_obj.prompt_meta.get("generation_config", {}), indent=2
|
||
)
|
||
st.session_state.edit_meta_tags = prompt_obj.prompt_meta.get("meta_tags", [])
|
||
st.session_state.edit_sample_user_input = json.dumps(
|
||
prompt_obj.prompt_meta.get("sample_user_input", {}), indent=2
|
||
)
|
||
st.session_state.edit_sample_output = "" # Clear previous output
|
||
|
||
|
||
def _handle_load_prompt() -> None:
|
||
"""Loads the selected prompt and version and populates the UI.
|
||
|
||
Triggered by the 'Load Prompt' button. It retrieves the selected prompt
|
||
name and version from the UI, calls the backend to fetch the data,
|
||
and then uses `_populate_ui_from_prompt` to display it.
|
||
"""
|
||
if not st.session_state.get("selected_prompt") or not st.session_state.get(
|
||
"selected_version"
|
||
):
|
||
st.warning("Please select both a prompt and a version to load.")
|
||
return
|
||
|
||
prompt_name = st.session_state.selected_prompt
|
||
prompt_id = st.session_state.local_prompt.existing_prompts[prompt_name]
|
||
version_id = st.session_state.selected_version
|
||
|
||
try:
|
||
with st.spinner(f"Loading version '{version_id}' of prompt '{prompt_name}'..."):
|
||
st.session_state.local_prompt.load_prompt(
|
||
prompt_id, prompt_name, version_id
|
||
)
|
||
logger.info(
|
||
"Successfully loaded prompt '%s' version '%s'.", prompt_name, version_id
|
||
)
|
||
_populate_ui_from_prompt()
|
||
st.success(f"Loaded prompt '{prompt_name}' (Version: {version_id}).")
|
||
except Exception as e:
|
||
logger.error("Failed to load prompt: %s", e, exc_info=True)
|
||
st.error(f"Failed to load prompt: {e}")
|
||
|
||
|
||
def _handle_save_edited_prompt() -> None:
|
||
"""Validates inputs and saves the current prompt config as a new version.
|
||
|
||
Similar to saving a new prompt, but it takes the data from the "Edit" tab's
|
||
widgets. It saves the current configuration as a new version of the
|
||
already existing prompt.
|
||
"""
|
||
if not st.session_state.get("edit_prompt_name"):
|
||
st.warning("Cannot save. Please load a prompt first.")
|
||
return
|
||
|
||
required_fields = {
|
||
"edit_prompt_data": "Prompt Text",
|
||
"edit_model_name": "Model Name",
|
||
"edit_system_instructions": "System Instructions",
|
||
}
|
||
for key, name in required_fields.items():
|
||
if not st.session_state.get(key):
|
||
st.warning(f"Please ensure '{name}' is not empty.")
|
||
return
|
||
|
||
prompt_obj = st.session_state.local_prompt
|
||
prompt_obj.prompt_to_run.prompt_name = st.session_state.edit_prompt_name
|
||
prompt_obj.prompt_to_run.prompt_data = st.session_state.edit_prompt_data
|
||
prompt_obj.prompt_to_run.model_name = st.session_state.edit_model_name.strip()
|
||
prompt_obj.prompt_to_run.system_instruction = (
|
||
st.session_state.edit_system_instructions
|
||
)
|
||
|
||
response_schema = _parse_json_input(
|
||
st.session_state.edit_response_schema, "Response Schema"
|
||
)
|
||
generation_config = _parse_json_input(
|
||
st.session_state.edit_generation_config, "Generation Config"
|
||
)
|
||
|
||
if generation_config:
|
||
generation_config = _apply_generation_config_typing(generation_config)
|
||
if response_schema:
|
||
generation_config["response_schema"] = response_schema
|
||
prompt_obj.prompt_meta["generation_config"] = generation_config
|
||
|
||
if response_schema:
|
||
prompt_obj.prompt_meta["response_schema"] = response_schema
|
||
prompt_obj.prompt_meta["meta_tags"] = st.session_state.edit_meta_tags
|
||
|
||
try:
|
||
with st.spinner("Saving as new version..."):
|
||
prompt_meta_info = prompt_obj.save_prompt(check_existing=False)
|
||
logger.info("Prompt saved successfully: %s", prompt_meta_info)
|
||
st.success("Saved as a new version successfully!")
|
||
st.session_state.local_prompt.refresh_prompt_cache()
|
||
except Exception as e:
|
||
logger.error("Failed to save prompt: %s", e, exc_info=True)
|
||
st.error(f"Failed to save prompt: {e}")
|
||
|
||
|
||
def _handle_generate_test_for_edit() -> None:
|
||
"""Generates a test response for the edited prompt.
|
||
|
||
Allows users to test changes made in the "Edit" tab before saving them
|
||
as a new version. It uses the current values in the UI fields to generate
|
||
a response from the model.
|
||
"""
|
||
if not st.session_state.get("edit_prompt_name"):
|
||
st.warning("Please load a prompt before generating a response.")
|
||
return
|
||
|
||
user_input_str = st.session_state.get("edit_sample_user_input", "")
|
||
if not user_input_str:
|
||
st.warning("Please provide sample user input to generate a response.")
|
||
return
|
||
|
||
sample_user_input = _parse_json_input(user_input_str, "Sample User Input")
|
||
if sample_user_input is None:
|
||
return
|
||
|
||
try:
|
||
prompt_obj = st.session_state.local_prompt
|
||
prompt_obj.prompt_to_run.prompt_data = st.session_state.edit_prompt_data
|
||
prompt_obj.prompt_to_run.system_instruction = (
|
||
st.session_state.edit_system_instructions
|
||
)
|
||
prompt_obj.prompt_meta["sample_user_input"] = sample_user_input
|
||
|
||
with st.spinner("Generating response..."):
|
||
response = prompt_obj.generate_response(sample_user_input)
|
||
st.session_state.edit_sample_output = response
|
||
st.success("Prompt response generated!")
|
||
except Exception as e:
|
||
logger.error("Error during test generation: %s", e, exc_info=True)
|
||
st.error(f"An error occurred during generation: {e}")
|
||
|
||
|
||
# --- UI Rendering Functions ---
|
||
def render_create_tab() -> None:
|
||
"""Renders the UI components for the 'Create New Prompt' tab.
|
||
|
||
This function defines and lays out all the Streamlit widgets (text inputs,
|
||
buttons, etc.) for the prompt creation workflow.
|
||
"""
|
||
st.subheader("1. Define Prompt Details")
|
||
st.text_input(
|
||
"**Prompt Name**",
|
||
key="new_prompt_name",
|
||
placeholder="e.g., customer_sentiment_classifier_v1",
|
||
help="A unique name to identify your prompt.",
|
||
)
|
||
st.text_area(
|
||
"**Prompt Text**",
|
||
key="new_prompt_data",
|
||
height=150,
|
||
placeholder="e.g., Classify the sentiment of the following text: {customer_review}",
|
||
help="The core text of your prompt. Use curly braces `{}` for variables.",
|
||
)
|
||
st.text_input(
|
||
"**Model Name**",
|
||
key="new_model_name",
|
||
placeholder="gemini-2.5-pro-001",
|
||
help="The specific model version to use (e.g., gemini-2.5-pro).",
|
||
)
|
||
st.text_area(
|
||
"**System Instructions**",
|
||
key="new_system_instructions",
|
||
height=300,
|
||
placeholder="e.g., You are an expert in sentiment analysis...",
|
||
help="Optional instructions to guide the model's behavior.",
|
||
)
|
||
st.multiselect(
|
||
"**Prompt Task**",
|
||
options=AVAILABLE_PROMPT_TASKS,
|
||
key="new_meta_tags",
|
||
help="Select the most appropriate task type for this prompt.",
|
||
)
|
||
st.text_area(
|
||
"**Response Schema (JSON)**",
|
||
key="new_response_schema",
|
||
height=150,
|
||
placeholder='{\n "type": "object", ... \n}',
|
||
help="Define the desired JSON structure for the model's output.",
|
||
)
|
||
st.text_area(
|
||
"**Generation Config (JSON)**",
|
||
key="new_generation_config",
|
||
height=150,
|
||
placeholder='{\n "temperature": 0.2, ... \n}',
|
||
help="A dictionary of generation parameters.",
|
||
)
|
||
|
||
if st.button(
|
||
"Save Prompt", type="primary", use_container_width=True, key="save_new"
|
||
):
|
||
_handle_save_new_prompt()
|
||
|
||
st.divider()
|
||
|
||
st.subheader("2. Test Your Prompt")
|
||
st.markdown("You can test your prompt here before saving.")
|
||
st.text_area(
|
||
"**Sample User Input (JSON)**",
|
||
key="new_sample_user_input",
|
||
height=150,
|
||
placeholder='{\n "customer_review": "The product was amazing!"\n}',
|
||
help="A JSON object where keys match the variables in your prompt text.",
|
||
)
|
||
|
||
if st.button("Generate Test Response", use_container_width=True, key="test_new"):
|
||
_handle_generate_test_for_new()
|
||
|
||
st.text_area(
|
||
"**Test Output**",
|
||
key="new_sample_output",
|
||
height=150,
|
||
placeholder="The model's response will be displayed here.",
|
||
disabled=True,
|
||
)
|
||
|
||
|
||
def render_edit_tab() -> None:
|
||
"""Renders the UI components for the 'Load & Edit Prompt' tab.
|
||
|
||
This function defines and lays out all the Streamlit widgets for loading,
|
||
editing, and versioning existing prompts.
|
||
"""
|
||
st.subheader("1. Load Prompt")
|
||
if st.button("Refresh List"):
|
||
with st.spinner("Refreshing..."):
|
||
st.session_state.local_prompt.refresh_prompt_cache()
|
||
st.toast("Prompt list refreshed.")
|
||
|
||
col1, col2 = st.columns(2)
|
||
with col1:
|
||
selected_prompt_name = st.selectbox(
|
||
"Select Existing Prompt",
|
||
options=st.session_state.local_prompt.existing_prompts.keys(),
|
||
placeholder="Select Prompt...",
|
||
key="selected_prompt",
|
||
help="Choose the prompt you want to load.",
|
||
)
|
||
|
||
with col2:
|
||
versions = []
|
||
if selected_prompt_name:
|
||
try:
|
||
prompt_id = st.session_state.local_prompt.existing_prompts[
|
||
selected_prompt_name
|
||
]
|
||
versions = [v.version_id for v in prompts.list_versions(prompt_id)]
|
||
except Exception as e:
|
||
st.error(f"Could not fetch versions: {e}")
|
||
st.selectbox(
|
||
"Select Version",
|
||
options=versions,
|
||
placeholder="Select Version...",
|
||
key="selected_version",
|
||
help="Choose the specific version to load.",
|
||
)
|
||
|
||
st.button(
|
||
"Load Prompt",
|
||
on_click=_handle_load_prompt,
|
||
use_container_width=True,
|
||
type="primary",
|
||
)
|
||
|
||
st.divider()
|
||
|
||
st.subheader("2. Edit Prompt Details")
|
||
st.text_input("Prompt Name", key="edit_prompt_name", disabled=True)
|
||
st.text_area("Prompt Text", key="edit_prompt_data", height=150)
|
||
st.text_input("Model Name", key="edit_model_name")
|
||
st.text_area("System Instructions", key="edit_system_instructions", height=300)
|
||
st.multiselect("Prompt Task", options=AVAILABLE_PROMPT_TASKS, key="edit_meta_tags")
|
||
|
||
col_schema, col_config = st.columns(2)
|
||
with col_schema:
|
||
st.text_area("Response Schema (JSON)", key="edit_response_schema", height=200)
|
||
with col_config:
|
||
st.text_area(
|
||
"Generation Config (JSON)", key="edit_generation_config", height=200
|
||
)
|
||
|
||
if st.button(
|
||
"Save as New Version", type="primary", use_container_width=True, key="save_edit"
|
||
):
|
||
_handle_save_edited_prompt()
|
||
|
||
st.divider()
|
||
|
||
st.subheader("3. Test Your Prompt")
|
||
st.text_area("Sample User Input (JSON)", key="edit_sample_user_input", height=150)
|
||
|
||
if st.button("Generate Test Response", use_container_width=True, key="test_edit"):
|
||
_handle_generate_test_for_edit()
|
||
|
||
st.text_area(
|
||
"Test Output",
|
||
key="edit_sample_output",
|
||
height=150,
|
||
placeholder="The model's response will be displayed here.",
|
||
disabled=True,
|
||
)
|
||
|
||
|
||
# --- Main Application ---
|
||
def main() -> None:
|
||
"""Renders the main Prompt Management page.
|
||
|
||
Sets the page configuration, initializes the session state (including the
|
||
`gcp_prompt` object and UI field defaults), and renders the main title
|
||
and tabbed layout for creating and editing prompts.
|
||
"""
|
||
st.set_page_config(
|
||
layout="wide",
|
||
page_title="Prompt Management",
|
||
page_icon="assets/favicon.ico",
|
||
)
|
||
|
||
# Initialize session state object and UI fields
|
||
if "local_prompt" not in st.session_state:
|
||
st.session_state.local_prompt = gcp_prompt()
|
||
|
||
ui_fields = {
|
||
"new_prompt_name": "",
|
||
"new_prompt_data": "",
|
||
"new_model_name": "",
|
||
"new_system_instructions": "",
|
||
"new_response_schema": "",
|
||
"new_generation_config": "",
|
||
"new_meta_tags": [],
|
||
"new_sample_user_input": "",
|
||
"new_sample_output": "",
|
||
"edit_prompt_name": "",
|
||
"edit_prompt_data": "",
|
||
"edit_model_name": "",
|
||
"edit_system_instructions": "",
|
||
"edit_response_schema": "",
|
||
"edit_generation_config": "",
|
||
"edit_meta_tags": [],
|
||
"edit_sample_user_input": "",
|
||
"edit_sample_output": "",
|
||
}
|
||
for field, default_val in ui_fields.items():
|
||
if field not in st.session_state:
|
||
st.session_state[field] = default_val
|
||
|
||
st.title("Prompt Management")
|
||
st.markdown(
|
||
"Create new prompts or load, edit, and test existing ones from the Prompt Management service."
|
||
)
|
||
st.divider()
|
||
|
||
# Use st.radio to create stateful tabs that persist across reruns.
|
||
# This prevents the UI from resetting to the first tab on every interaction.
|
||
selected_tab = st.radio(
|
||
"Select Action",
|
||
["Create New Prompt", "Load & Edit Prompt"],
|
||
key="prompt_management_tab",
|
||
horizontal=True,
|
||
label_visibility="collapsed",
|
||
)
|
||
|
||
if selected_tab == "Create New Prompt":
|
||
render_create_tab()
|
||
elif selected_tab == "Load & Edit Prompt":
|
||
render_edit_tab()
|
||
|
||
st.caption("LLM EvalKit | Prompt Management")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|