From 11ae44bde70b29d1ee0e5a5b51cb8867d01569d5 Mon Sep 17 00:00:00 2001 From: Jason Summer Date: Mon, 25 Nov 2024 12:09:57 -0600 Subject: [PATCH] Evalanche release 11-25-2024 --- framework-evalanche/README.md | 15 +- framework-evalanche/pages/data.py | 347 +++++++++++++-------- framework-evalanche/pages/results.py | 119 ++++--- framework-evalanche/setup/cli_setup.sql | 51 ++- framework-evalanche/setup/git_setup.sql | 51 ++- framework-evalanche/src.zip | Bin 78561 -> 78811 bytes framework-evalanche/src/app_utils.py | 21 ++ framework-evalanche/src/metrics.py | 36 ++- framework-evalanche/src/prompts.py | 5 +- framework-evalanche/src/snowflake_utils.py | 20 +- 10 files changed, 471 insertions(+), 194 deletions(-) diff --git a/framework-evalanche/README.md b/framework-evalanche/README.md index 13678d2..6d20c37 100644 --- a/framework-evalanche/README.md +++ b/framework-evalanche/README.md @@ -21,7 +21,7 @@ Please see TAGGING.md for details on object comments. * [Running](#release) * [Extras](#extras) + [Custom Metrics](#custom-metrics) - + [Crafting a LLM Pipeline Stored Procedure](#crafting-a-llm-pipeline-stored-procedure) + + [Generating Results to Evaluate](#generating-results-to-evaluate) # Overview Evalanche is a Streamlit in Snowflake (SiS) application that provides a single location to evaluate and compare generative AI use case outputs in a streamlined, on demand, and automated fashion. Regardless if your goal is to measure the quality of RAG-based LLM solutions or accuracy of SQL generation, Evalanche provides a scalable, customizable, and trackable way to do it. @@ -95,7 +95,10 @@ CALL GENAI_UTILITIES.EVALUATION.DELETE_METRIC('Rudeness'); Lastly, please be aware that Streamlit in Snowflake now supports multiple python versions. Custom metrics may only be available with consistent Python versions. For example, if you create a custom metric while running the app with Python version 3.11, the custom metric will only be available in subsequent sessions when running Python 3.11. -## Crafting a LLM Pipeline Stored Procedure +## Generating Results to Evaluate +Evalanche primarily assumes you've saved LLM outputs to table(s) in Snowflake for us to evaluate. That may not be the case. Evalanche supports two ways to generate outputs using either a custom LLM pipeline or a Cortex Analyst runner. Both options are available from the data page (under "Need to Generate Results?") once you've selected your desired Metric(s). + +### Crafting a Stored Procedure for your Custom LLM Pipeline To run a reference dataset through your desired LLM pipelines on the data page, we must first encapsulated the pipeline logic in a Stored Procedure. To take advantage of this feature, the stored procedure must have a single VARIANT input type and return a single value. When we execute the stored procedure, a single row from the reference dataset will be passed in the form of a Python dictionary. In other words, a row in the reference dataset that looks like: ```markdown | TASK | PERSONA | @@ -109,7 +112,7 @@ will be passed to the stored procedure as: "PERSONA": "Pirate" } ``` -A appropriately crafted stored procedure could look like the below. +An appropriately crafted stored procedure could look like the below. ```sql CREATE OR REPLACE PROCEDURE MY_PIPELINE(INPUT VARIANT) RETURNS STRING @@ -131,3 +134,9 @@ def run(session, INPUT): prompt = prompt) $$; ``` + +### Using the Cortex Analyst Runner +To run a gold or reference set of questions through Cortex Analyst, select the target semantic model and the table containing the reference questions. The SQL results will be written to a table for further evaluation with the Cortex Analyst-suggested metric. + +# Feedback +Please add issues to GitHub or email Jason Summer (jason.summer@snowflake.com). \ No newline at end of file diff --git a/framework-evalanche/pages/data.py b/framework-evalanche/pages/data.py index 2609a58..19cd2ec 100644 --- a/framework-evalanche/pages/data.py +++ b/framework-evalanche/pages/data.py @@ -27,16 +27,11 @@ ) TITLE = "Data Selection" -if ( - st.session_state.get("selected_metrics", None) is not None - and st.session_state.get("eval_funnel", None) == "new" -): - INSTRUCTIONS = """ - Select your evaluation data below. - The evaluation data should contain all metric inputs and any additional columns to retain through evaluation. - You can specify a single dataset or separate datasets for expected and actual results, if applicable.""" -else: - INSTRUCTIONS = "Please first select a metric from home." + +INSTRUCTIONS = """ +Select your evaluation data below. +The evaluation data should contain all metric inputs and any additional columns to retain through evaluation. +You can specify a single dataset or separate datasets for expected and actual results, if applicable.""" st.set_page_config( page_title=TITLE, @@ -57,6 +52,16 @@ FROM """ +BESPOKE_INSTRUCTIONS = """Before you start, your LLM pipeline must be encapsulated in a stored procedure that takes a VARIANT input and returns a single value. + Every row of the reference table will be passed through the stored procedure as a dictionary. + Every column in the reference table will be passed to the stored procedure but only those columns selected will be passed to the stored procedure. + Please see [Snowflake Stored Procedure documentation](https://docs.snowflake.com/en/developer-guide/stored-procedure/stored-procedures-overview) + for details on stored procedures and these [specific instructions](https://github.com/sfc-gh-jsummer/evalanche#crafting-a-llm-pipeline-stored-procedure) on crafting these stored procedures.""" + +CORTEX_ANALYST_INSTRUCTIONS = """Have reference questions to run through Cortex Analyst? + Select the Semantic Model in stage, table containing the reference questions, and a destination table. + We will do the rest. Take note of the table name as it will be used in the next step to evaluate the results.""" + def check_models(models: List[str]) -> None: """Check if models are available in the Snowflake account region.""" @@ -211,13 +216,22 @@ def sproc_runner(session: Session, sproc_name: str, inputs: Dict[str, Any]) -> T elapsed_time = time.time() - start_time return (record_result, elapsed_time) +def cortex_analyst_sproc_runner(session: Session, sproc_name: str, question: str, semantic_model_path: str) -> Tuple[Union[int, float], Any]: + start_time = time.time() + record_result = session.sql(f"""CALL {sproc_name}('{question}', '{semantic_model_path}')""").collect_nowait().result()[0][0] + # record_result = session.call(sproc_name, inputs) # Once Snowpark supports thread-safe calls without parameter change + elapsed_time = time.time() - start_time + return (record_result, elapsed_time) + def pipeline_runner( session: Session, sproc: str, input_tablename: str, output_tablename: str, columns: List[str], -) -> None: + cortex_analyst: bool = False, + semantic_model: str = None, +) -> str: """Runs stored procedures asynchronously over input from Snowflake table. Stored procedures may not be asynchronous but calling of them is done asynchronously in the app. @@ -233,93 +247,178 @@ def pipeline_runner( input_tablename (string): Fully-qualified name of table with input values. output_tablename (string): Fully-qualified name of table to write results to. columns (list): List of columns to pass to stored procedure. + cortex_analyst (bool): Whether to run Cortex Analyst SQL generation. + semantic_model (string): Fully-qualified path to semantic model for Cortex Analyst. + Returns: + string: Fully-qualified name of table where results are written. """ import multiprocessing - from joblib import Parallel, delayed + from snowflake.snowpark.functions import lit + from src.snowflake_utils import add_row_id, save_eval_to_table df = add_row_id(session.table(input_tablename)) + first_column = columns[0] # We will use this to pass to Cortex Analyst sproc as the semantic file path columns = columns + ["ROW_ID"] - for pandas_df in df.select(*columns).to_pandas_batches(): - # for pandas_df in df.to_pandas_batches(): - results = Parallel(n_jobs=multiprocessing.cpu_count(), backend="threading")( - delayed( - lambda row: { - "ROW_ID": row["ROW_ID"], # Capture ROW_ID - "RESPONSE": (response := sproc_runner(session, sproc, row.to_dict()))[0], - "ELAPSED_TIME": response[1], - } - )(row) - for _, row in pandas_df.iterrows() - ) + # Add semantic model as additional column for tracking purposes + if cortex_analyst: + if semantic_model is not None: + df = df.withColumn("MODEL", lit(semantic_model)) + columns = columns + ["MODEL"] + + for pandas_df in df.select(*columns).to_pandas_batches(): + results = [] + for _, row in pandas_df.iterrows(): + result = { + "ROW_ID": row["ROW_ID"], # Capture ROW_ID + "CORTEX_ANALYST_SQL": (response := cortex_analyst_sproc_runner( + session, + sproc, + row.to_dict()[first_column], + semantic_model))[0], + "ELAPSED_TIME": response[1], + } + results.append(result) + time.sleep(3) # Add a 3-second delay between processing each record to avoid overloading the system + + else: + for pandas_df in df.select(*columns).to_pandas_batches(): + # for pandas_df in df.to_pandas_batches(): + results = Parallel(n_jobs=multiprocessing.cpu_count(), backend="threading")( + delayed( + lambda row: { + "ROW_ID": row["ROW_ID"], # Capture ROW_ID + "RESPONSE": (response := sproc_runner(session, sproc, row.to_dict()))[0], + "ELAPSED_TIME": response[1], + } + )(row) + for _, row in pandas_df.iterrows() + ) result = session.create_dataframe(results).join(df, on="ROW_ID", how="left") save_eval_to_table(result, output_tablename) + return output_tablename @st.experimental_dialog("Run your LLM Pipeline", width="large") def pipeline_runner_dialog() -> None: """Dialog to run reference data through LLM pipeline and record results for evaluation.""" - from src.app_utils import get_sprocs, select_schema_context - - st.write(""" - Have reference questions or inputs but still need to run them through your LLM pipeline? - Use this dialog to run your reference set through your LLM pipeline and record the results to evaluate here. - - Before you start, your LLM pipeline must be encapsulated in a stored procedure that takes a VARIANT input and returns a single value. - Every row of the reference table will be passed through the stored procedure as a dictionary. - Every column in the reference table will be passed to the stored procedure but only those columns selected will be passed to the stored procedure. - Please see [Snowflake Stored Procedure documentation](https://docs.snowflake.com/en/developer-guide/stored-procedure/stored-procedures-overview) - for details on stored procedures and these [specific instructions](https://github.com/sfc-gh-jsummer/evalanche#crafting-a-llm-pipeline-stored-procedure) on crafting these stored procedures.""") - - name = "runner" - st.write("Select the stored procedure that encapsulates your LLM pipeline.") - schema_context = select_schema_context(name, on_change=get_sprocs, args=(name,)) - if f"{name}_sprocs" not in st.session_state: - st.session_state[f"{name}_sprocs"] = [] - sproc_name = st.selectbox( - "Select Stored Procedure", - st.session_state[f"{name}_sprocs"], - index=None, - ) - sproc_name = f"{schema_context['database']}.{schema_context['schema']}.{sproc_name}" - table = st.text_input("Enter Name for Generated Table", key=f"new_table_{name}") - new_tablename = f"{schema_context['database']}.{schema_context['schema']}.{table}" - st.divider() - - st.write("Select the reference data.") - name = "runner_output" - table_spec = table_data_selector(name, new_table=False) - data_table = ( - f'{table_spec["database"]}.{table_spec["schema"]}.{table_spec["table"]}' - ) - available_columns = fetch_columns(table_spec["database"],table_spec["schema"],table_spec["table"]) - selected_columns = st.multiselect( - "Select Columns", available_columns, default=None, key=f"columns_{name}", - help = "Select the columns to explicitly passed to the stored procedure." - ) + from src.app_utils import get_sprocs, select_schema_context, get_stages, get_semantic_models - if st.button("Run"): - with st.spinner("Running pipeline..."): - pipeline_runner( - st.session_state["session"], - sproc_name.split("(")[0], - data_table, - new_tablename, - selected_columns + + st.write("""Have reference questions or inputs but still need to run them through your LLM pipeline? + Use this dialog to run a reference set through your LLM pipeline and record the results.""") + + pipeline_selection = st.selectbox("Do you want to run **Cortex Analyst SQL Generation** or a **custom LLM Pipeline**?", + options=["Cortex Analyst", "Custom"], index=None) + + if pipeline_selection is not None: + if pipeline_selection == "Custom": + st.write(f'**Instructions:** {BESPOKE_INSTRUCTIONS}') + else: + st.write(f'**Instructions:** {CORTEX_ANALYST_INSTRUCTIONS}') + + name = "runner" + if pipeline_selection == "Custom": + st.write("Select the stored procedure that encapsulates your LLM pipeline.") + schema_context = select_schema_context(name, on_change=get_sprocs, args=(name,)) + + if f"{name}_sprocs" not in st.session_state: + st.session_state[f"{name}_sprocs"] = [] + sproc_name = st.selectbox( + "Select Stored Procedure", + st.session_state[f"{name}_sprocs"], + index=None, ) - # Set result_data to None so first rendering on results - # page will create it as pandas dataframe from Snowpark result dataframe - set_session_var_to_none('result_data') - st.success(f"Results written to {new_tablename}.") - time.sleep(2) - st.rerun() + sproc_name = f"{schema_context['database']}.{schema_context['schema']}.{sproc_name}" + + else: + st.write("Select the stage that contains your semantic model for Cortex Analyst.") + schema_context = select_schema_context(name, on_change=get_stages, args=(name,)) + + if f"{name}_stages" not in st.session_state: + st.session_state[f"{name}_stages"] = [] + if f"{name}_models" not in st.session_state: + st.session_state[f"{name}_models"] = [] + stage_name = st.selectbox( + "Select Snowflake Stage", + st.session_state[f"{name}_stages"], + index=None, + key=f"{name}_stage", + on_change=get_semantic_models, + args=(name,) + ) + semantic_model = st.selectbox( + "Select Semantic Model", + st.session_state[f"{name}_models"], + index=None, + key=f"{name}_model", + ) + qualified_semantic_model = f"{schema_context['database']}.{schema_context['schema']}.{stage_name}/{semantic_model}" + + + table = st.text_input("Enter Name for Generated Table", key=f"new_table_{name}", help = "Fully qualify if you would like to save in a different database/schema than above.") + if '.' not in table: + new_tablename = f"{schema_context['database']}.{schema_context['schema']}.{table}" + else: + new_tablename = table + st.divider() + + st.write("Select the reference question set.") + name = "runner_output" + table_spec = table_data_selector(name, new_table=False) + data_table = ( + f'{table_spec["database"]}.{table_spec["schema"]}.{table_spec["table"]}' + ) + available_columns = fetch_columns(table_spec["database"],table_spec["schema"],table_spec["table"]) + + if pipeline_selection == "Custom": + selected_columns = st.multiselect( + "Select Columns", available_columns, default=None, key=f"columns_{name}", + help = "Select the columns to be explicitly passed to the stored procedure." + ) + else: + selected_columns = st.selectbox( + "Select Column containing Reference Question", available_columns, index = None, key=f"columns_{name}", + help = "Select the column that contains the reference questions for Cortex Analyst." + ) + if st.button("Run"): + try: + if pipeline_selection == "Custom": + with st.spinner("Running pipeline..."): + st.session_state['returned_tablename'] = pipeline_runner( + st.session_state["session"], + sproc_name.split("(")[0], + data_table, + new_tablename, + selected_columns + ) + else: + with st.spinner("Running Cortex Analyst..."): + st.session_state['returned_tablename'] = pipeline_runner( + session = st.session_state["session"], + sproc = "GENAI_UTILITIES.EVALUATION.CORTEX_ANALYST_SQL", + input_tablename = data_table, + output_tablename = new_tablename, + columns = [selected_columns], + cortex_analyst = True, + semantic_model = qualified_semantic_model, + ) + # Set result_data to None so first rendering on results + # page will create it as pandas dataframe from Snowpark result dataframe + set_session_var_to_none('result_data') + st.success(f"Results written to {new_tablename}.") + except Exception as e: + st.error(f"Error: {e}") + st.stop() + time.sleep(2) + st.rerun() @st.experimental_dialog("Configure Metrics", width="large") @@ -424,56 +523,58 @@ def run_eval() -> None: def pick_data() -> None: """Main rendering function for page.""" - if ( - st.session_state.get("selected_metrics", None) is not None - and st.session_state.get("eval_funnel", None) == "new" - ): - data_split, runner_col, _ = st.columns([1, 1, 2]) - with data_split: - data_toggle = st.toggle( - "Separate Expected & Actual", - help="""Turn on to specify expected and actual datasets separately. - A join key will be necessary to compare the two datasets.""", - value=False, + data_split, runner_col, _ = st.columns([1, 1, 2]) + # Show table if results were written to a table in stored procedure runner + if 'returned_tablename' in st.session_state: + st.info(f"Recent results written to {st.session_state['returned_tablename']}.") + with data_split: + data_toggle = st.toggle( + "Separate Expected & Actual", + help="""Turn on to specify expected and actual datasets separately. + A join key will be necessary to compare the two datasets.""", + value=False, + ) + with runner_col: + runner_button = st.button( + "Need to generate results?", + use_container_width=True, + help="""Have reference questions or inputs but still need to run them through your LLM pipeline? + Use this dialog to run a reference set through your LLM pipeline and record the results to evaluate.""", + ) + if runner_button: + pipeline_runner_dialog() + if not data_toggle: + single_col, _ = st.columns(2) + with single_col: + data_spec( + key_name="single_source", + instructions="Select your evaluation dataset.", + join_key=False, ) - with runner_col: - runner_button = st.button( - "Need to generate results?", - use_container_width=True, - help="""Have reference questions or inputs but still need to run them through your LLM pipeline? - Use this dialog to run your reference set through your LLM pipeline and record the results to evaluate here.""", + else: + inf_col, ground_col = st.columns(2) + with inf_col: + data_spec( + key_name="ground", instructions="Select your expected results." ) - if runner_button: - pipeline_runner_dialog() - if not data_toggle: - single_col, _ = st.columns(2) - with single_col: - data_spec( - key_name="single_source", - instructions="Select your evaluation dataset.", - join_key=False, - ) - else: - inf_col, ground_col = st.columns(2) - with inf_col: - data_spec( - key_name="ground", instructions="Select your expected results." - ) - with ground_col: - data_spec( - key_name="inference", instructions="Select your actual results." - ) - button_container = row(10, vertical_align="center") - preview_button = button_container.button(":mag_right: Preview", use_container_width=True) - configure_button = button_container.button( - "▶️ Configure", use_container_width=True, - type="primary", - ) + with ground_col: + data_spec( + key_name="inference", instructions="Select your actual results." + ) + button_container = row(10, vertical_align="center") + preview_button = button_container.button(":mag_right: Preview", use_container_width=True) + configure_button = button_container.button( + "▶️ Configure", + use_container_width=True, + help = "Select metrics and data to configure your evaluation.", + type="primary", + disabled = len(st.session_state.get("selected_metrics", []))==0 + ) - if preview_button: - preview_merge_data() - if configure_button: - configure_metrics() + if preview_button: + preview_merge_data() + if configure_button: + configure_metrics() pick_data() diff --git a/framework-evalanche/pages/results.py b/framework-evalanche/pages/results.py index cf577b7..a434c22 100644 --- a/framework-evalanche/pages/results.py +++ b/framework-evalanche/pages/results.py @@ -21,8 +21,9 @@ SAVED_EVAL_TABLE, STAGE_NAME, add_row_id, + run_async_sql_to_dataframe, ) -from src.metrics import Metric +from src.metrics import Metric, SQLResultsAccuracy def get_result_title() -> str: @@ -268,7 +269,7 @@ def give_recommendation_instruction() -> None: ) -def get_metric_cols(current_df: DataFrame) -> list: +def get_metric_cols(current_df: Union[DataFrame, pd.DataFrame]) -> list: """Returns list of columns in dataframe that contain metric values. Some metric names have spaces and Snowpark keeps them in lower case with double quotes. @@ -279,6 +280,7 @@ def get_metric_cols(current_df: DataFrame) -> list: return [c_name for c_name in df_columns if c_name.upper() in (m_name.upper() for m_name in metric_names)] + def show_metric() -> None: """Renders metric KPIs based on selected metrics.""" @@ -297,12 +299,13 @@ def show_metric() -> None: Please create a new evaluation or select an existing one from the homepage.""") st.stop() - if st.session_state.get("metric_result_data", None) is not None: - df = st.session_state["metric_result_data"] + if st.session_state.get("result_data", None) is not None: + df = st.session_state["result_data"] metric_names = [metric.get_column() for metric in st.session_state["metrics_in_results"]] kpi_row = row(6, vertical_align="top") # Placing entire dataframe in memory seems to be more stable than iterating over columns and averaging in snowpark - metric_values = df.select(*metric_names).to_pandas() + # metric_values = df.select(*metric_names).to_pandas() + metric_values = df[metric_names] for metric_name, metric_value in metric_values.mean().to_dict().items(): kpi_row.metric(label=metric_name, value=round(metric_value, 2)) @@ -390,13 +393,37 @@ def update_record(table_update_inputs: Dict[str, str], selected_metric_name: str st.session_state["result_data"] = df -# metrics = fetch_metrics(st.session_state["session"], STAGE_NAME) +def show_cortex_analyst_sql_results(metric: Metric, prompt_inputs: Dict[str, str]) -> None: + """Displays data retrieved from SQL used in Cortex Analyst metrics. + + Shows results for generated_sql and expected_sql in the prompt_inputs dictionary. + Only shows results if metric matches the name property of SQLResultsAccuracy. + + Args: + metric (Metric): Column name keys with updated values to replace in dataframe. + prompt_inputs (dict[str, str]): Dictionary of prompt inputs for the metric. + """ + + if type(metric).__name__ is (type(SQLResultsAccuracy()).__name__): + with st.expander("Retrieved Data", expanded=False): + st.caption("Results limited to 100 rows.") + for key in ["generated_sql", "expected_sql"]: + st.write(f"{key.upper()} Result") + if key in prompt_inputs: + try: + inference_data = run_async_sql_to_dataframe(metric.session, prompt_inputs[key]) + st.dataframe(inference_data, + hide_index = True,) + except Exception as e: + st.write(f"Error: {e}") + else: + st.write("No data returned") @st.experimental_dialog("Review Record", width="large") def review_record() -> None: """Render dialog box to review a metric result record.""" - + st.write("Analyze and explore the selected record. Model selection will be used for analysis and metric rerunning. Updates can be saved to viewed results.") if st.session_state["selected_dict"] is None or len(st.session_state["selected_dict"]) == 0: st.write("Please select a record to review.") elif len(st.session_state["selected_dict"]) > 1: @@ -404,7 +431,6 @@ def review_record() -> None: else: # Only first record is selected for analysis selected_record = st.session_state["selected_dict"][0] - # metrics = fetch_metrics(st.session_state["session"], STAGE_NAME) metric_cols = get_metric_cols(st.session_state.get("metric_result_data", None)) metric_col, model_col = st.columns(2) @@ -435,7 +461,10 @@ def review_record() -> None: for key, value in st.session_state["param_selection"][ matching_metric.name ].items(): - entered_value = st.text_area(value, selected_record[value]) + entered_value = st.text_area(value, + selected_record[value], + key = value) + prompt_inputs[key] = entered_value table_update_inputs[value] = entered_value metric_col, comment_col = st.columns((1, 4)) @@ -452,16 +481,29 @@ def review_record() -> None: on_click = rerun_metric, args = (prompt_inputs, matching_metric), use_container_width=True,) save = bottom_selection.button("Save", disabled = selected_metric_name is None, - use_container_width=True,) + use_container_width=True, + help = "Save changes to record in current view.") + + # Unsaved changes in the dialog may linger if user navigates away and returns. + # Here we provide a reset button to clear out any unsaved changes. + reset = bottom_selection.button("Reset", disabled = selected_metric_name is None, + use_container_width=True, + help = "Reset all unsaved changed to selected record.") if st.session_state.get('analysis', None) is not None: st.write(f"**Analysis:** {st.session_state['analysis']}") - + + # If evaluating SQL, show SQL results of current inputs + show_cortex_analyst_sql_results(matching_metric, prompt_inputs) + if save: update_record(table_update_inputs, selected_metric_name, selected_record['ROW_ID']) st.rerun() + if reset: + st.rerun() + def show_dataframe_results() -> Optional[pd.DataFrame]: """ @@ -477,15 +519,8 @@ def show_dataframe_results() -> Optional[pd.DataFrame]: pandas Dataframe """ - if st.session_state.get("metric_result_data", None) is not None: - if st.session_state.get('result_data', None) is None: - st.session_state["result_data"] = add_row_id(st.session_state["metric_result_data"])\ - .withColumn("REVIEW", F.lit(False))\ - .withColumn("COMMENT", F.lit(None)).to_pandas() - - # Store available metrics in session state - st.session_state["metrics"] = fetch_metrics(st.session_state["session"], STAGE_NAME) - + + if st.session_state.get('result_data', None) is not None: df_selection = st.data_editor( st.session_state["result_data"], hide_index=True, @@ -498,7 +533,6 @@ def show_dataframe_results() -> Optional[pd.DataFrame]: return df_selection else: - st.session_state["result_data"] = None return None @@ -509,18 +543,14 @@ def trend_avg_metrics() -> None: """ if ( - st.session_state.get("metric_result_data", None) is not None + st.session_state.get("result_data", None) is not None and st.session_state.get("metrics_in_results", None) is not None ): - metric_cols = get_metric_cols(st.session_state.get("metric_result_data", None)) + metric_cols = get_metric_cols(st.session_state.get("result_data", None)) - # We cast to variant in case the metric is a boolean + df = st.session_state["result_data"].groupby('METRIC_DATETIME')[metric_cols].mean() + # METRIC_DATETIME is batched for every run so there should be many rows per metric calculation set - df = ( - st.session_state["metric_result_data"] - .group_by("METRIC_DATETIME") - .agg(*[F.avg(F.to_variant(col)).alias(col) for col in metric_cols]) - ) st.write("Average Metric Scores over Time") st.line_chart( df, @@ -536,12 +566,12 @@ def trend_count_metrics() -> None: """ if ( - st.session_state.get("metric_result_data", None) is not None + st.session_state.get("result_data", None) is not None and st.session_state.get("metrics_in_results", None) is not None ): - metric_cols = get_metric_cols(st.session_state.get("metric_result_data", None)) + metric_cols = get_metric_cols(st.session_state.get("result_data", None)) - df = st.session_state["metric_result_data"] + df = st.session_state["result_data"] st.write("Metric Scores over Time") st.bar_chart( df, @@ -557,20 +587,16 @@ def bar_chart_metrics() -> None: """ if ( - st.session_state.get("metric_result_data", None) is not None + st.session_state.get("result_data", None) is not None and len(st.session_state.get("metrics_in_results", []))>0 ): - metric_cols = get_metric_cols(st.session_state.get("metric_result_data", None)) + metric_cols = get_metric_cols(st.session_state.get("result_data", None)) - df = st.session_state["metric_result_data"] - chart_df = ( - df.select(metric_cols) - .unpivot("SCORE", "METRIC", metric_cols) - .group_by("METRIC", "SCORE") - .count() - ) + df = pd.melt(st.session_state["result_data"], + value_vars=metric_cols, var_name = 'METRIC', value_name = 'SCORE')\ + .groupby(['METRIC', 'SCORE']).size().reset_index(name='COUNT') st.write("Score Counts by Metric") - st.bar_chart(chart_df, x="SCORE", y="COUNT", color="METRIC") + st.bar_chart(df, x="SCORE", y="COUNT", color="METRIC") def get_trendable_column() -> Union[None, str]: @@ -617,6 +643,15 @@ def show_results(): from src.app_utils import fetch_warehouses + if st.session_state.get("metric_result_data", None) is not None: + if st.session_state.get('result_data', None) is None: + st.session_state["result_data"] = add_row_id(st.session_state["metric_result_data"])\ + .withColumn("REVIEW", F.lit(False))\ + .withColumn("COMMENT", F.lit(None)).to_pandas() + + # Store available metrics in session state + st.session_state["metrics"] = fetch_metrics(st.session_state["session"], STAGE_NAME) + show_metric() if st.session_state["eval_funnel"] is not None: top_row = row(5, vertical_align="top") diff --git a/framework-evalanche/setup/cli_setup.sql b/framework-evalanche/setup/cli_setup.sql index 9013399..79c219c 100644 --- a/framework-evalanche/setup/cli_setup.sql +++ b/framework-evalanche/setup/cli_setup.sql @@ -1,5 +1,5 @@ SET major = 2; -SET minor = 0; +SET minor = 1; SET COMMENT = concat('{"origin": "sf_sit", "name": "evalanche", "version": {"major": ',$major,', "minor": ',$minor,'}}'); @@ -77,6 +77,55 @@ def run(session, metric_name): return f"An error occurred: {e}" $$; +-- Cortex Analyst runner +CREATE OR REPLACE PROCEDURE GENAI_UTILITIES.EVALUATION.CORTEX_ANALYST_SQL(prompt STRING, semantic_file_path STRING) +RETURNS STRING +LANGUAGE PYTHON +PACKAGES = ('snowflake-snowpark-python') +RUNTIME_VERSION = '3.9' +HANDLER = 'process_message' +as +$$ +import _snowflake +import json +def send_message(messages, semantic_file_path): + """Calls the REST API and returns the response.""" + + request_body = { + "messages": messages, + "semantic_model_file": f"@{semantic_file_path}", + } + resp = _snowflake.send_snow_api_request( + "POST", + f"/api/v2/cortex/analyst/message", + {}, + {}, + request_body, + {}, + 30000, + ) + if resp["status"] < 400: + response_content = json.loads(resp["content"]) + return response_content + else: + raise Exception( + f"Failed request with status {resp['status']}: {resp}" + ) + +def process_message(session, prompt, semantic_file_path): + """Processes a message and adds the response to the chat.""" + messages = [] + messages.append( + {"role": "user", "content": [{"type": "text", "text": prompt}]} + ) + response = send_message(messages, semantic_file_path) + for item in response["message"]["content"]: + if item["type"] == "sql": + return item.get("statement", None) + else: + return None +$$; + -- Create Streamlit CREATE OR REPLACE STREAMLIT GENAI_UTILITIES.EVALUATION.EVALUATION_APP ROOT_LOCATION = '@GENAI_UTILITIES.EVALUATION.STREAMLIT_STAGE' diff --git a/framework-evalanche/setup/git_setup.sql b/framework-evalanche/setup/git_setup.sql index db46bd5..1933275 100644 --- a/framework-evalanche/setup/git_setup.sql +++ b/framework-evalanche/setup/git_setup.sql @@ -1,5 +1,5 @@ SET major = 2; -SET minor = 0; +SET minor = 1; SET COMMENT = concat('{"origin": "sf_sit", "name": "evalanche", "version": {"major": ',$major,', "minor": ',$minor,'}}'); @@ -107,6 +107,55 @@ def run(session, metric_name): return f"An error occurred: {e}" $$; +-- Cortex Analyst runner +CREATE OR REPLACE PROCEDURE GENAI_UTILITIES.EVALUATION.CORTEX_ANALYST_SQL(prompt STRING, database STRING, SCHEMA STRING, STAGE STRING, SEMANTIC_FILE STRING) +RETURNS STRING +LANGUAGE PYTHON +PACKAGES = ('snowflake-snowpark-python') +RUNTIME_VERSION = '3.9' +HANDLER = 'process_message' +as +$$ +import _snowflake +import json +def send_message(messages, database, schema, stage, semantic_file): + """Calls the REST API and returns the response.""" + + request_body = { + "messages": messages, + "semantic_model_file": f"@{database}.{schema}.{stage}/{semantic_file}", + } + resp = _snowflake.send_snow_api_request( + "POST", + f"/api/v2/cortex/analyst/message", + {}, + {}, + request_body, + {}, + 30000, + ) + if resp["status"] < 400: + response_content = json.loads(resp["content"]) + return response_content + else: + raise Exception( + f"Failed request with status {resp['status']}: {resp}" + ) + +def process_message(session, prompt, database, schema, stage, semantic_file): + """Processes a message and adds the response to the chat.""" + messages = [] + messages.append( + {"role": "user", "content": [{"type": "text", "text": prompt}]} + ) + response = send_message(messages, database, schema, stage, semantic_file) + for item in response["message"]["content"]: + if item["type"] == "sql": + return item.get("statement", None) + else: + return None +$$; + -- Create Streamlit CREATE OR REPLACE STREAMLIT GENAI_UTILITIES.EVALUATION.EVALUATION_APP ROOT_LOCATION = '@GENAI_UTILITIES.EVALUATION.STREAMLIT_STAGE' diff --git a/framework-evalanche/src.zip b/framework-evalanche/src.zip index 67bbb86601a33f71042d53a44a8d406c2787a3a6..6df8180372faee17156783934ef387876dffb4af 100644 GIT binary patch delta 38010 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 diff --git a/framework-evalanche/src/app_utils.py b/framework-evalanche/src/app_utils.py index f386de8..617ccf9 100644 --- a/framework-evalanche/src/app_utils.py +++ b/framework-evalanche/src/app_utils.py @@ -308,6 +308,27 @@ def get_stages(name: str): else: st.session_state[f"{name}_stages"] = [] +def get_semantic_models(name: str): + """Call back function to associate available semantic model selector with corresponding stage selection.""" + + if ( + st.session_state[f"{name}_database"] is not None + and st.session_state[f"{name}_schema"] is not None + and st.session_state[f"{name}_stage"] is not None + ): + if "session" not in st.session_state: + session = get_connection() + else: + session = st.session_state["session"] + stage = f'{st.session_state[f"{name}_database"]}.{st.session_state[f"{name}_schema"]}.{st.session_state[f"{name}_stage"]}' + query = f"ls @{stage} pattern='.*\\yaml'" + result = session.sql(query) + files = [file[0].split("/")[-1] for file in result.collect()] + if len(files) > 0: + st.session_state[f"{name}_models"] = files + else: + st.session_state[f"{name}_models"] = [] + def get_sprocs(name: str): """Call back function to associate database and schema selector with corresponding stored procedures.""" diff --git a/framework-evalanche/src/metrics.py b/framework-evalanche/src/metrics.py index bcc4eb0..ca31ce6 100644 --- a/framework-evalanche/src/metrics.py +++ b/framework-evalanche/src/metrics.py @@ -40,13 +40,15 @@ def evaluate( import re model_to_use = model if model else self.model - - prompt = self.get_prompt(**kwargs) + try: + prompt = self.get_prompt(**kwargs) - response = run_async_sql_complete(self.session, model_to_use, prompt) - rating = re.search(r'\d+', response) - if rating: - return int(rating.group()) + response = run_async_sql_complete(self.session, model_to_use, prompt) + rating = re.search(r'\d+', response) + if rating: + return int(rating.group()) + except Exception: + return None else: return None @@ -69,7 +71,7 @@ def __init__( prompt=SQLAccuracy_prompt, required={ "question": "User question", - "inference_sql": "LLM-generated SQL statement", + "generated_sql": "LLM-generated SQL statement", "expected_sql": "Ground truth SQL statement", }, ) @@ -81,11 +83,11 @@ def get_prompt( if self.prompt is not None: from src.snowflake_utils import return_sql_result - if "inference_sql" in kwargs: - inference_data = return_sql_result(self.session, kwargs["inference_sql"]) + if "generated_sql" in kwargs: + inference_data = return_sql_result(self.session, kwargs["generated_sql"]) else: inference_data = "No data returned" - if "inference_sql" in kwargs: + if "expected_sql" in kwargs: expected_data = return_sql_result(self.session, kwargs["expected_sql"]) else: expected_data = "No data returned" @@ -110,13 +112,15 @@ def evaluate( ): model_to_use = model if model else self.model + try: + prompt = self.get_prompt(**kwargs) - prompt = self.get_prompt(**kwargs) - - response = run_async_sql_complete(self.session, model_to_use, prompt) - if "true" in response.lower(): - return True - else: + response = run_async_sql_complete(self.session, model_to_use, prompt) + if "true" in response.lower(): + return True + else: + return False + except Exception: return False diff --git a/framework-evalanche/src/prompts.py b/framework-evalanche/src/prompts.py index 5c938fe..8690afd 100644 --- a/framework-evalanche/src/prompts.py +++ b/framework-evalanche/src/prompts.py @@ -2,9 +2,10 @@ The JSON data is the output of a SQL query generated to answer a user question. You are to determine if the provided JSON data matches the ground truth JSON data and answers the user question. +The Inference JSON does not have to match the Ground Truth JSON perfectly but should contain the correct answer as denoted by the Ground Truth JSON. Your answer should be either "True" or "False". -Answer "True" if you believe the JSON data matches the ground truth JSON data response. -Answer "False" if you do not believe the JSON data matches the ground truth JSON data. +Answer "True" if you believe the Inference JSON data reflects the Ground Truth JSON data given the user question. +Otherwise, answer "False". [User Question] {question} diff --git a/framework-evalanche/src/snowflake_utils.py b/framework-evalanche/src/snowflake_utils.py index ed0306e..84bdf53 100644 --- a/framework-evalanche/src/snowflake_utils.py +++ b/framework-evalanche/src/snowflake_utils.py @@ -143,13 +143,12 @@ def return_sql_result(session: Session, sql: str) -> Union[str, None]: """ from snowflake.snowpark import functions as F - - result = ( - session.sql(sql.replace(";", "")) - .limit(100) - .select(F.to_varchar(F.array_agg(F.object_construct("*")))) - ) try: + result = ( + session.sql(sql.replace(";", "")) + .limit(100) + .select(F.to_varchar(F.array_agg(F.object_construct("*")))) + ) return result.collect_nowait().result()[0][0] except Exception as e: st.error(f"Error: {e}") @@ -280,3 +279,12 @@ def call_sproc(session: Session, name: str) -> Any: def call_async_sproc(session: Session, sproc: str, input_value: Dict[str, Any]) -> Any: return session.sql(f"CALL {sproc}({input_value})").collect_nowait().result()[0][0] + +def run_async_sql_to_dataframe(session: Session, query: str) -> DataFrame: + """Runs a SQL query and returns the result as a Snowpark DataFrame.""" + query_id = session.sql(query.replace(';','')).collect_nowait().query_id + async_job = session.create_async_job(query_id) + + return async_job.to_df() + +