|
16 | 16 |
|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
| 19 | +import builtins |
19 | 20 | import json |
20 | 21 | import os |
21 | 22 | from pathlib import Path |
|
25 | 26 | from google.adk.agents.base_agent import BaseAgent |
26 | 27 | from google.adk.apps.app import App |
27 | 28 | from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService |
| 29 | +from google.adk.evaluation import agent_evaluator as agent_evaluator_module |
28 | 30 | from google.adk.evaluation.agent_evaluator import _EvalMetricResultWithInvocation |
29 | 31 | from google.adk.evaluation.agent_evaluator import AgentEvaluator |
| 32 | +from google.adk.evaluation.agent_evaluator import load_json |
30 | 33 | from google.adk.evaluation.eval_case import EvalCase |
31 | 34 | from google.adk.evaluation.eval_case import Invocation |
32 | 35 | from google.adk.evaluation.eval_config import EvalConfig |
|
42 | 45 | import pandas as pd |
43 | 46 | import pytest |
44 | 47 |
|
| 48 | +_NON_ASCII_TEXT = "😀 你好 café" |
| 49 | +_real_open = builtins.open |
| 50 | + |
45 | 51 |
|
46 | 52 | def _make_eval_set() -> EvalSet: |
47 | 53 | return EvalSet( |
@@ -979,5 +985,89 @@ async def test_evaluate_keeps_positional_initial_session_file_and_print_flag( |
979 | 985 | ) |
980 | 986 |
|
981 | 987 |
|
| 988 | +def _non_utf8_default_open(file, mode="r", *args, **kwargs): |
| 989 | + """Emulates a platform whose default text encoding is not UTF-8. |
| 990 | +
|
| 991 | + On such platforms (for example Windows, where the default is cp1252), |
| 992 | + `open()` calls that omit `encoding=` inherit that non-UTF-8 default. This |
| 993 | + wrapper reproduces that behaviour on any platform by falling back to ASCII |
| 994 | + when a text-mode open does not specify an encoding, so a missing |
| 995 | + `encoding="utf-8"` argument raises instead of silently depending on the |
| 996 | + host locale. |
| 997 | + """ |
| 998 | + if "b" not in mode and "encoding" not in kwargs: |
| 999 | + kwargs["encoding"] = "ascii" |
| 1000 | + return _real_open(file, mode, *args, **kwargs) |
| 1001 | + |
| 1002 | + |
| 1003 | +def test_load_json_reads_non_ascii_with_non_utf8_default(tmp_path, mocker): |
| 1004 | + """`load_json` must decode eval data as UTF-8 regardless of platform locale.""" |
| 1005 | + file_path = tmp_path / "eval.json" |
| 1006 | + file_path.write_text( |
| 1007 | + json.dumps([{"query": _NON_ASCII_TEXT}], ensure_ascii=False), |
| 1008 | + encoding="utf-8", |
| 1009 | + ) |
| 1010 | + |
| 1011 | + mocker.patch.object( |
| 1012 | + agent_evaluator_module, "open", _non_utf8_default_open, create=True |
| 1013 | + ) |
| 1014 | + |
| 1015 | + assert load_json(str(file_path)) == [{"query": _NON_ASCII_TEXT}] |
| 1016 | + |
| 1017 | + |
| 1018 | +def test_get_initial_session_reads_non_ascii_with_non_utf8_default( |
| 1019 | + tmp_path, mocker |
| 1020 | +): |
| 1021 | + """`_get_initial_session` must decode the session file as UTF-8.""" |
| 1022 | + session_file = tmp_path / "initial_session.json" |
| 1023 | + session_file.write_text( |
| 1024 | + json.dumps({"state": {"city": _NON_ASCII_TEXT}}, ensure_ascii=False), |
| 1025 | + encoding="utf-8", |
| 1026 | + ) |
| 1027 | + |
| 1028 | + mocker.patch.object( |
| 1029 | + agent_evaluator_module, "open", _non_utf8_default_open, create=True |
| 1030 | + ) |
| 1031 | + |
| 1032 | + initial_session = AgentEvaluator._get_initial_session(str(session_file)) |
| 1033 | + |
| 1034 | + assert initial_session == {"state": {"city": _NON_ASCII_TEXT}} |
| 1035 | + |
| 1036 | + |
| 1037 | +def test_migrate_eval_data_round_trips_non_ascii_with_non_utf8_default( |
| 1038 | + tmp_path, mocker |
| 1039 | +): |
| 1040 | + """Migration must read the old file and write the new file as UTF-8. |
| 1041 | +
|
| 1042 | + This exercises both the read (`load_json`) and the write |
| 1043 | + (`model_dump_json`) of eval data, which must stay UTF-8 consistent so that |
| 1044 | + datasets containing non-ASCII characters survive migration on any platform. |
| 1045 | + """ |
| 1046 | + old_eval_data_file = tmp_path / "old_format.test.json" |
| 1047 | + old_eval_data_file.write_text( |
| 1048 | + json.dumps( |
| 1049 | + [{ |
| 1050 | + "query": _NON_ASCII_TEXT, |
| 1051 | + "reference": _NON_ASCII_TEXT, |
| 1052 | + "expected_tool_use": [], |
| 1053 | + }], |
| 1054 | + ensure_ascii=False, |
| 1055 | + ), |
| 1056 | + encoding="utf-8", |
| 1057 | + ) |
| 1058 | + new_eval_data_file = tmp_path / "new_format.json" |
| 1059 | + |
| 1060 | + mocker.patch.object( |
| 1061 | + agent_evaluator_module, "open", _non_utf8_default_open, create=True |
| 1062 | + ) |
| 1063 | + |
| 1064 | + AgentEvaluator.migrate_eval_data_to_new_schema( |
| 1065 | + str(old_eval_data_file), str(new_eval_data_file) |
| 1066 | + ) |
| 1067 | + |
| 1068 | + migrated = json.loads(new_eval_data_file.read_text(encoding="utf-8")) |
| 1069 | + assert _NON_ASCII_TEXT in json.dumps(migrated, ensure_ascii=False) |
| 1070 | + |
| 1071 | + |
982 | 1072 | if __name__ == "__main__": |
983 | 1073 | raise SystemExit(pytest.main([__file__, "-v"])) |
0 commit comments