|
24 | 24 | import vertexai |
25 | 25 | from google.cloud.aiplatform import initializer as aiplatform_initializer |
26 | 26 | from vertexai import _genai |
| 27 | +from vertexai._genai import _evals_data_converters |
27 | 28 | from vertexai._genai import evals |
28 | 29 | from vertexai._genai import types as vertexai_genai_types |
29 | 30 | from google.genai import client |
@@ -955,3 +956,255 @@ def test_prompt_template_assemble_multimodal_variable_integration(self): |
955 | 956 | assembled_content.parts[2].text |
956 | 957 | == " and then answer: This is a simple text." |
957 | 958 | ) |
| 959 | + |
| 960 | + |
| 961 | +class TestGeminiEvalDataConverter: |
| 962 | + """Unit tests for the _GeminiEvalDataConverter class.""" |
| 963 | + |
| 964 | + def setup_method(self): |
| 965 | + self.converter = _evals_data_converters._GeminiEvalDataConverter() |
| 966 | + |
| 967 | + def test_convert_simple_prompt_response(self): |
| 968 | + raw_data = [ |
| 969 | + { |
| 970 | + "request": { |
| 971 | + "contents": [{"role": "user", "parts": [{"text": "Hello"}]}] |
| 972 | + }, |
| 973 | + "response": { |
| 974 | + "candidates": [ |
| 975 | + { |
| 976 | + "content": {"role": "model", "parts": [{"text": "Hi"}]}, |
| 977 | + "finish_reason": "STOP", |
| 978 | + } |
| 979 | + ], |
| 980 | + "usage_metadata": { |
| 981 | + "prompt_token_count": 1, |
| 982 | + "candidates_token_count": 1, |
| 983 | + "total_token_count": 2, |
| 984 | + }, |
| 985 | + }, |
| 986 | + } |
| 987 | + ] |
| 988 | + result_dataset = self.converter.convert(raw_data) |
| 989 | + assert isinstance(result_dataset, vertexai_genai_types.EvaluationDataset) |
| 990 | + assert len(result_dataset.eval_cases) == 1 |
| 991 | + eval_case = result_dataset.eval_cases[0] |
| 992 | + |
| 993 | + assert eval_case.prompt == genai_types.Content( |
| 994 | + parts=[genai_types.Part(text="Hello")], role="user" |
| 995 | + ) |
| 996 | + assert len(eval_case.responses) == 1 |
| 997 | + assert eval_case.responses[0].response == genai_types.Content( |
| 998 | + parts=[genai_types.Part(text="Hi")], role="model" |
| 999 | + ) |
| 1000 | + assert eval_case.reference.response is None |
| 1001 | + assert eval_case.system_instruction.parts is None |
| 1002 | + assert eval_case.conversation_history == [] |
| 1003 | + |
| 1004 | + def test_convert_with_system_instruction(self): |
| 1005 | + raw_data = [ |
| 1006 | + { |
| 1007 | + "request": { |
| 1008 | + "system_instruction": { |
| 1009 | + "role": "system", |
| 1010 | + "parts": [{"text": "Be nice."}], |
| 1011 | + }, |
| 1012 | + "contents": [{"role": "user", "parts": [{"text": "Hello"}]}], |
| 1013 | + }, |
| 1014 | + "response": { |
| 1015 | + "candidates": [ |
| 1016 | + { |
| 1017 | + "content": { |
| 1018 | + "role": "model", |
| 1019 | + "parts": [{"text": "Hi there!"}], |
| 1020 | + } |
| 1021 | + } |
| 1022 | + ] |
| 1023 | + }, |
| 1024 | + } |
| 1025 | + ] |
| 1026 | + result_dataset = self.converter.convert(raw_data) |
| 1027 | + eval_case = result_dataset.eval_cases[0] |
| 1028 | + assert eval_case.system_instruction == genai_types.Content( |
| 1029 | + parts=[genai_types.Part(text="Be nice.")], role="system" |
| 1030 | + ) |
| 1031 | + assert eval_case.prompt == genai_types.Content( |
| 1032 | + parts=[genai_types.Part(text="Hello")], role="user" |
| 1033 | + ) |
| 1034 | + |
| 1035 | + def test_convert_with_conversation_history_and_reference(self): |
| 1036 | + raw_data_for_reference = [ |
| 1037 | + { |
| 1038 | + "request": { |
| 1039 | + "contents": [ |
| 1040 | + { |
| 1041 | + "role": "user", |
| 1042 | + "parts": [{"text": "Initial user"}], |
| 1043 | + }, # history |
| 1044 | + { |
| 1045 | + "role": "model", |
| 1046 | + "parts": [{"text": "Initial model"}], |
| 1047 | + }, # history |
| 1048 | + { |
| 1049 | + "role": "user", |
| 1050 | + "parts": [{"text": "Actual prompt"}], |
| 1051 | + }, # prompt |
| 1052 | + { |
| 1053 | + "role": "model", |
| 1054 | + "parts": [{"text": "This is reference"}], |
| 1055 | + }, # reference |
| 1056 | + ] |
| 1057 | + }, |
| 1058 | + "response": { |
| 1059 | + "candidates": [ |
| 1060 | + { |
| 1061 | + "content": { |
| 1062 | + "role": "model", |
| 1063 | + "parts": [{"text": "Actual response"}], |
| 1064 | + } |
| 1065 | + } |
| 1066 | + ] |
| 1067 | + }, |
| 1068 | + } |
| 1069 | + ] |
| 1070 | + result_dataset = self.converter.convert(raw_data_for_reference) |
| 1071 | + eval_case = result_dataset.eval_cases[0] |
| 1072 | + |
| 1073 | + assert eval_case.prompt == genai_types.Content( |
| 1074 | + parts=[genai_types.Part(text="Actual prompt")], role="user" |
| 1075 | + ) |
| 1076 | + assert eval_case.reference.response == genai_types.Content( |
| 1077 | + parts=[genai_types.Part(text="This is reference")], role="model" |
| 1078 | + ) |
| 1079 | + assert len(eval_case.conversation_history) == 2 |
| 1080 | + assert eval_case.conversation_history[0].content == genai_types.Content( |
| 1081 | + parts=[genai_types.Part(text="Initial user")], role="user" |
| 1082 | + ) |
| 1083 | + assert eval_case.conversation_history[1].content == genai_types.Content( |
| 1084 | + parts=[genai_types.Part(text="Initial model")], role="model" |
| 1085 | + ) |
| 1086 | + assert eval_case.responses[0].response == genai_types.Content( |
| 1087 | + parts=[genai_types.Part(text="Actual response")], role="model" |
| 1088 | + ) |
| 1089 | + |
| 1090 | + def test_convert_with_conversation_history_no_reference(self): |
| 1091 | + # Last message in contents is from user, so it becomes the prompt. |
| 1092 | + raw_data = [ |
| 1093 | + { |
| 1094 | + "request": { |
| 1095 | + "contents": [ |
| 1096 | + {"role": "user", "parts": [{"text": "Old user msg"}]}, |
| 1097 | + {"role": "model", "parts": [{"text": "Old model msg"}]}, |
| 1098 | + {"role": "user", "parts": [{"text": "Current prompt"}]}, |
| 1099 | + ] |
| 1100 | + }, |
| 1101 | + "response": { |
| 1102 | + "candidates": [ |
| 1103 | + { |
| 1104 | + "content": { |
| 1105 | + "role": "model", |
| 1106 | + "parts": [{"text": "A response"}], |
| 1107 | + } |
| 1108 | + } |
| 1109 | + ] |
| 1110 | + }, |
| 1111 | + } |
| 1112 | + ] |
| 1113 | + result_dataset = self.converter.convert(raw_data) |
| 1114 | + eval_case = result_dataset.eval_cases[0] |
| 1115 | + |
| 1116 | + assert eval_case.prompt == genai_types.Content( |
| 1117 | + parts=[genai_types.Part(text="Current prompt")], role="user" |
| 1118 | + ) |
| 1119 | + assert eval_case.reference.response is None |
| 1120 | + assert len(eval_case.conversation_history) == 2 |
| 1121 | + assert eval_case.conversation_history[0].content.parts[0].text == "Old user msg" |
| 1122 | + assert ( |
| 1123 | + eval_case.conversation_history[1].content.parts[0].text == "Old model msg" |
| 1124 | + ) |
| 1125 | + |
| 1126 | + def test_convert_no_candidates_in_response(self): |
| 1127 | + raw_data = [ |
| 1128 | + { |
| 1129 | + "request": { |
| 1130 | + "contents": [{"role": "user", "parts": [{"text": "Hello"}]}] |
| 1131 | + }, |
| 1132 | + "response": { |
| 1133 | + "candidates": [], |
| 1134 | + "prompt_feedback": {"block_reason": "SAFETY"}, |
| 1135 | + }, |
| 1136 | + } |
| 1137 | + ] |
| 1138 | + result_dataset = self.converter.convert(raw_data) |
| 1139 | + eval_case = result_dataset.eval_cases[0] |
| 1140 | + assert len(eval_case.responses) == 1 |
| 1141 | + assert eval_case.responses[0].response is None |
| 1142 | + |
| 1143 | + def test_convert_invalid_content_structure_raises_value_error(self): |
| 1144 | + raw_data = [ |
| 1145 | + { |
| 1146 | + "request": {"contents": ["not a dict"]}, # Invalid content |
| 1147 | + "response": { |
| 1148 | + "candidates": [ |
| 1149 | + {"content": {"role": "model", "parts": [{"text": "Hi"}]}} |
| 1150 | + ] |
| 1151 | + }, |
| 1152 | + } |
| 1153 | + ] |
| 1154 | + with pytest.raises( |
| 1155 | + TypeError, match="Expected a dictionary for content at turn 0" |
| 1156 | + ): |
| 1157 | + self.converter.convert(raw_data) |
| 1158 | + |
| 1159 | + raw_data_missing_parts = [ |
| 1160 | + { |
| 1161 | + "request": {"contents": [{"role": "user"}]}, # Missing 'parts' |
| 1162 | + "response": { |
| 1163 | + "candidates": [ |
| 1164 | + {"content": {"role": "model", "parts": [{"text": "Hi"}]}} |
| 1165 | + ] |
| 1166 | + }, |
| 1167 | + } |
| 1168 | + ] |
| 1169 | + with pytest.raises( |
| 1170 | + ValueError, match="Missing 'parts' key in content structure at turn 0" |
| 1171 | + ): |
| 1172 | + self.converter.convert(raw_data_missing_parts) |
| 1173 | + |
| 1174 | + def test_convert_multiple_items(self): |
| 1175 | + raw_data = [ |
| 1176 | + { |
| 1177 | + "request": { |
| 1178 | + "contents": [{"role": "user", "parts": [{"text": "Item 1"}]}] |
| 1179 | + }, |
| 1180 | + "response": { |
| 1181 | + "candidates": [ |
| 1182 | + { |
| 1183 | + "content": { |
| 1184 | + "role": "model", |
| 1185 | + "parts": [{"text": "Resp 1"}], |
| 1186 | + } |
| 1187 | + } |
| 1188 | + ] |
| 1189 | + }, |
| 1190 | + }, |
| 1191 | + { |
| 1192 | + "request": { |
| 1193 | + "contents": [{"role": "user", "parts": [{"text": "Item 2"}]}] |
| 1194 | + }, |
| 1195 | + "response": { |
| 1196 | + "candidates": [ |
| 1197 | + { |
| 1198 | + "content": { |
| 1199 | + "role": "model", |
| 1200 | + "parts": [{"text": "Resp 2"}], |
| 1201 | + } |
| 1202 | + } |
| 1203 | + ] |
| 1204 | + }, |
| 1205 | + }, |
| 1206 | + ] |
| 1207 | + result_dataset = self.converter.convert(raw_data) |
| 1208 | + assert len(result_dataset.eval_cases) == 2 |
| 1209 | + assert result_dataset.eval_cases[0].prompt.parts[0].text == "Item 1" |
| 1210 | + assert result_dataset.eval_cases[1].prompt.parts[0].text == "Item 2" |
0 commit comments