|
18 | 18 | import asyncio |
19 | 19 | import enum |
20 | 20 | import importlib |
| 21 | +import io |
21 | 22 | import json |
22 | 23 | import os |
23 | 24 | import re |
|
45 | 46 | types as agentplatform_genai_types, |
46 | 47 | ) |
47 | 48 | from agentplatform._genai.types import common as common_types |
| 49 | +from vertexai._genai import ( |
| 50 | + _evals_data_converters as vertexai_evals_data_converters, |
| 51 | +) |
48 | 52 | from google.genai import client |
49 | 53 | from google.genai import errors as genai_errors |
50 | 54 | from google.genai import types as genai_types |
| 55 | +import numpy as np |
51 | 56 | import pandas as pd |
52 | 57 | import pydantic |
53 | 58 | import pytest |
|
61 | 66 |
|
62 | 67 | pytestmark = pytest.mark.usefixtures("google_auth_mock") |
63 | 68 |
|
| 69 | +_CONVERTER_MODULES = pytest.mark.parametrize( |
| 70 | + "converters", |
| 71 | + [_evals_data_converters, vertexai_evals_data_converters], |
| 72 | + ids=["agent_platform", "vertexai"], |
| 73 | +) |
| 74 | + |
64 | 75 |
|
65 | 76 | class TestDropEmptyColumns: |
66 | 77 | """Unit tests for the _drop_empty_columns function.""" |
@@ -5671,6 +5682,16 @@ def test_convert_with_raw_string_response(self): |
5671 | 5682 | ) |
5672 | 5683 |
|
5673 | 5684 |
|
| 5685 | +_GEMINI_HISTORY = [ |
| 5686 | + {"role": "user", "parts": [{"text": "My code word is BLUE."}]}, |
| 5687 | + {"role": "model", "parts": [{"text": "Noted."}]}, |
| 5688 | +] |
| 5689 | + |
| 5690 | + |
| 5691 | +def _csv_round_trip(row: dict[str, object]) -> pd.DataFrame: |
| 5692 | + return pd.read_csv(io.StringIO(pd.DataFrame([row]).to_csv(index=False))) |
| 5693 | + |
| 5694 | + |
5674 | 5695 | class TestFlattenEvalDataConverter: |
5675 | 5696 | """Unit tests for the _FlattenEvalDataConverter class.""" |
5676 | 5697 |
|
@@ -5892,6 +5913,158 @@ def test_convert_with_intermediate_events_as_event_objects(self): |
5892 | 5913 | == "intermediate event" |
5893 | 5914 | ) |
5894 | 5915 |
|
| 5916 | + @_CONVERTER_MODULES |
| 5917 | + @pytest.mark.parametrize("column", ["conversation_history", "history"]) |
| 5918 | + def test_convert_json_string_history_from_csv(self, converters, column): |
| 5919 | + raw_data_df = _csv_round_trip( |
| 5920 | + { |
| 5921 | + "prompt": "Code word?", |
| 5922 | + "response": "BLUE", |
| 5923 | + column: json.dumps(_GEMINI_HISTORY), |
| 5924 | + } |
| 5925 | + ) |
| 5926 | + |
| 5927 | + result_dataset = converters._FlattenEvalDataConverter().convert( |
| 5928 | + raw_data_df.to_dict(orient="records") |
| 5929 | + ) |
| 5930 | + |
| 5931 | + assert [ |
| 5932 | + message.content |
| 5933 | + for message in result_dataset.eval_cases[0].conversation_history |
| 5934 | + ] == [genai_types.Content.model_validate(turn) for turn in _GEMINI_HISTORY] |
| 5935 | + |
| 5936 | + @_CONVERTER_MODULES |
| 5937 | + @pytest.mark.parametrize( |
| 5938 | + "history", |
| 5939 | + [ |
| 5940 | + np.array( |
| 5941 | + [ |
| 5942 | + {"role": turn["role"], "parts": np.array(turn["parts"])} |
| 5943 | + for turn in _GEMINI_HISTORY |
| 5944 | + ] |
| 5945 | + ), |
| 5946 | + tuple(_GEMINI_HISTORY), |
| 5947 | + ], |
| 5948 | + ids=["ndarray", "tuple"], |
| 5949 | + ) |
| 5950 | + def test_convert_array_history(self, converters, history): |
| 5951 | + result_dataset = converters._FlattenEvalDataConverter().convert( |
| 5952 | + [{"prompt": "Code word?", "response": "BLUE", "history": history}] |
| 5953 | + ) |
| 5954 | + |
| 5955 | + assert [ |
| 5956 | + message.content |
| 5957 | + for message in result_dataset.eval_cases[0].conversation_history |
| 5958 | + ] == [genai_types.Content.model_validate(turn) for turn in _GEMINI_HISTORY] |
| 5959 | + |
| 5960 | + @_CONVERTER_MODULES |
| 5961 | + @pytest.mark.parametrize( |
| 5962 | + "column,value,expected_warning", |
| 5963 | + [ |
| 5964 | + ( |
| 5965 | + "conversation_history", |
| 5966 | + "[not json", |
| 5967 | + "Could not decode JSON string in 'conversation_history' column for" |
| 5968 | + " case eval_case_0", |
| 5969 | + ), |
| 5970 | + ( |
| 5971 | + "history", |
| 5972 | + json.dumps(_GEMINI_HISTORY[0]), |
| 5973 | + "Invalid type in 'history' column for case eval_case_0. Expected", |
| 5974 | + ), |
| 5975 | + ( |
| 5976 | + "history", |
| 5977 | + np.array(1.5), |
| 5978 | + "Invalid type in 'history' column for case eval_case_0. Expected", |
| 5979 | + ), |
| 5980 | + ], |
| 5981 | + ids=["invalid_json", "json_object", "zero_dim_array"], |
| 5982 | + ) |
| 5983 | + def test_convert_invalid_history_value_logs_warning( |
| 5984 | + self, converters, column, value, expected_warning, caplog |
| 5985 | + ): |
| 5986 | + with caplog.at_level("WARNING", logger=converters.logger.name): |
| 5987 | + result_dataset = converters._FlattenEvalDataConverter().convert( |
| 5988 | + [{"prompt": "Hello", "response": "Hi", column: value}] |
| 5989 | + ) |
| 5990 | + |
| 5991 | + assert result_dataset.eval_cases[0].conversation_history is None |
| 5992 | + assert len(caplog.records) == 1 |
| 5993 | + assert expected_warning in caplog.text |
| 5994 | + |
| 5995 | + @_CONVERTER_MODULES |
| 5996 | + @pytest.mark.parametrize( |
| 5997 | + "value", |
| 5998 | + [None, float("nan"), np.float32("nan"), pd.NA, pd.NaT, " "], |
| 5999 | + ids=["none", "nan", "float32_nan", "pd_na", "pd_nat", "blank"], |
| 6000 | + ) |
| 6001 | + def test_convert_empty_history_value_without_warning( |
| 6002 | + self, converters, value, caplog |
| 6003 | + ): |
| 6004 | + with caplog.at_level("WARNING", logger=converters.logger.name): |
| 6005 | + result_dataset = converters._FlattenEvalDataConverter().convert( |
| 6006 | + [{"prompt": "Hello", "response": "Hi", "conversation_history": value}] |
| 6007 | + ) |
| 6008 | + |
| 6009 | + assert result_dataset.eval_cases[0].conversation_history is None |
| 6010 | + assert not caplog.records |
| 6011 | + |
| 6012 | + @_CONVERTER_MODULES |
| 6013 | + def test_convert_missing_conversation_history_falls_back_to_history( |
| 6014 | + self, converters |
| 6015 | + ): |
| 6016 | + raw_data_df = _csv_round_trip( |
| 6017 | + { |
| 6018 | + "prompt": "Code word?", |
| 6019 | + "response": "BLUE", |
| 6020 | + "conversation_history": None, |
| 6021 | + "history": json.dumps(_GEMINI_HISTORY), |
| 6022 | + } |
| 6023 | + ) |
| 6024 | + |
| 6025 | + result_dataset = converters._FlattenEvalDataConverter().convert( |
| 6026 | + raw_data_df.to_dict(orient="records") |
| 6027 | + ) |
| 6028 | + |
| 6029 | + assert [ |
| 6030 | + message.content |
| 6031 | + for message in result_dataset.eval_cases[0].conversation_history |
| 6032 | + ] == [genai_types.Content.model_validate(turn) for turn in _GEMINI_HISTORY] |
| 6033 | + |
| 6034 | + @pytest.mark.usefixtures("mock_eval_dependencies") |
| 6035 | + def test_evaluate_sends_csv_history_to_multi_turn_metric( |
| 6036 | + self, mock_api_client_fixture |
| 6037 | + ): |
| 6038 | + dataset_df = _csv_round_trip( |
| 6039 | + { |
| 6040 | + "prompt": "Code word?", |
| 6041 | + "response": "BLUE", |
| 6042 | + "conversation_history": json.dumps(_GEMINI_HISTORY), |
| 6043 | + } |
| 6044 | + ) |
| 6045 | + |
| 6046 | + with mock.patch.object( |
| 6047 | + evals.Evals, "_evaluate_instances" |
| 6048 | + ) as mock_evaluate_instances: |
| 6049 | + mock_evaluate_instances.return_value = ( |
| 6050 | + agentplatform_genai_types.EvaluateInstancesResponse( |
| 6051 | + metric_results=[agentplatform_genai_types.MetricResult(score=1.0)] |
| 6052 | + ) |
| 6053 | + ) |
| 6054 | + evals.Evals(api_client_=mock_api_client_fixture).evaluate( |
| 6055 | + dataset=dataset_df, |
| 6056 | + metrics=[ |
| 6057 | + agentplatform_genai_types.Metric( |
| 6058 | + name="multi_turn_general_quality_v1" |
| 6059 | + ) |
| 6060 | + ], |
| 6061 | + ) |
| 6062 | + |
| 6063 | + instance = mock_evaluate_instances.call_args.kwargs["instance"] |
| 6064 | + assert [ |
| 6065 | + content.parts[0].text for content in instance.prompt.contents.contents |
| 6066 | + ] == ["My code word is BLUE.", "Noted.", "Code word?"] |
| 6067 | + |
5895 | 6068 |
|
5896 | 6069 | class TestOpenAIDataConverter: |
5897 | 6070 | """Unit tests for the _OpenAIDataConverter class.""" |
|
0 commit comments