import json import textwrap import pyarrow as pa import pytest from datasets import Features, Value, load_dataset from datasets.builder import InvalidConfigName from datasets.data_files import DataFilesList from datasets.packaged_modules.json.json import AGENT_TRACES_FEATURES, Json, JsonConfig from ..utils import require_teich @pytest.fixture def json_file(tmp_path): filename = tmp_path / "file.json" data = textwrap.dedent( """\ { "col_1": 1, "col_2": 2 } """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def jsonl_file(tmp_path): filename = tmp_path / "file.jsonl" data = textwrap.dedent( """\ {"col_1": -1} {"col_1": 1, "col_2": 2} {"col_1": 10, "col_2": 20} """ ) with open(filename, "w") as f: f.write(data) return str(filename) # ndjson format is no longer maintained (see: https://github.com/ndjson/ndjson-spec/issues/35#issuecomment-1285673417) @pytest.fixture def ndjson_file(tmp_path): filename = tmp_path / "file.ndjson" data = textwrap.dedent( """\ {"col_1": -1} {"col_1": 1, "col_2": 2} {"col_1": 10, "col_2": 20} """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def jsonl_file_utf16_encoded(tmp_path): filename = tmp_path / "file_utf16_encoded.jsonl" data = textwrap.dedent( """\ {"col_1": -1} {"col_1": 1, "col_2": 2} {"col_1": 10, "col_2": 20} """ ) with open(filename, "w", encoding="utf-16") as f: f.write(data) return str(filename) @pytest.fixture def json_file_with_list_of_dicts(tmp_path): filename = tmp_path / "file_with_list_of_dicts.json" data = textwrap.dedent( """\ [ {"col_1": -1}, {"col_1": 1, "col_2": 2}, {"col_1": 10, "col_2": 20} ] """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def json_file_with_list_of_strings(tmp_path): filename = tmp_path / "file_with_list_of_strings.json" data = textwrap.dedent( """\ [ "First text.", "Second text.", "Third text." ] """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def json_file_with_list_of_dicts_field(tmp_path): filename = tmp_path / "file_with_list_of_dicts_field.json" data = textwrap.dedent( """\ { "field1": 1, "field2": "aabb", "field3": [ {"col_1": -1}, {"col_1": 1, "col_2": 2}, {"col_1": 10, "col_2": 20} ] } """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def json_file_with_list_of_strings_field(tmp_path): path = tmp_path / "file.json" data = textwrap.dedent( """\ { "field1": 1, "field2": "aabb", "field3": [ "First text.", "Second text.", "Third text." ] } """ ) with open(path, "w") as f: f.write(data) return str(path) @pytest.fixture def json_file_with_dict_of_lists_field(tmp_path): path = tmp_path / "file.json" data = textwrap.dedent( """\ { "field1": 1, "field2": "aabb", "field3": { "col_1": [-1, 1, 10], "col_2": [null, 2, 20] } } """ ) with open(path, "w") as f: f.write(data) return str(path) @pytest.fixture def json_file_with_list_of_dicts_with_sorted_columns(tmp_path): path = tmp_path / "file.json" data = textwrap.dedent( """\ [ {"ID": 0, "Language": "Language-0", "Topic": "Topic-0"}, {"ID": 1, "Language": "Language-1", "Topic": "Topic-1"}, {"ID": 2, "Language": "Language-2", "Topic": "Topic-2"} ] """ ) with open(path, "w") as f: f.write(data) return str(path) @pytest.fixture def json_file_with_list_of_dicts_with_sorted_columns_field(tmp_path): path = tmp_path / "file.json" data = textwrap.dedent( """\ { "field1": 1, "field2": "aabb", "field3": [ {"ID": 0, "Language": "Language-0", "Topic": "Topic-0"}, {"ID": 1, "Language": "Language-1", "Topic": "Topic-1"}, {"ID": 2, "Language": "Language-2", "Topic": "Topic-2"} ] } """ ) with open(path, "w") as f: f.write(data) return str(path) @pytest.fixture def jsonl_file_with_mix_of_str_and_int(tmp_path): filename = tmp_path / "file.jsonl" data = textwrap.dedent( """\ {"col_1": -1} {"col_1": 1} {"col_1": "foo"} """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def jsonl_file_with_dicts_of_varying_keys(tmp_path): filename = tmp_path / "file.jsonl" data = textwrap.dedent( """\ {"col_1": {"a": 0}} {"col_1": {"b": 0}} {"col_1": {"c": 0}} """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def jsonl_file_with_lists_of_dicts_of_varying_keys(tmp_path): filename = tmp_path / "file.jsonl" data = textwrap.dedent( """\ {"col_1": [{"a": 0}, {"b": 0}]} {"col_1": [{"c": 0}, {"d": 0}]} """ ) with open(filename, "w") as f: f.write(data) return str(filename) @pytest.fixture def jsonl_file_with_lists_of_dicts_of_varying_keys_and_bom(tmp_path): # Same content as jsonl_file_with_lists_of_dicts_of_varying_keys, but the file # starts with a UTF-8 BOM (written via the utf-8-sig codec). filename = tmp_path / "file_with_bom.jsonl" data = textwrap.dedent( """\ {"col_1": [{"a": 0}, {"b": 0}]} {"col_1": [{"c": 0}, {"d": 0}]} """ ) with open(filename, "w", encoding="utf-8-sig") as f: f.write(data) return str(filename) _messages = [ {"role": "user", "content": "Turn on the living room lights and play my electronic music playlist."}, { "role": "assistant", "tool_calls": [ { "type": "function", "function": {"name": "control_light", "arguments": {"room": "living room", "state": "on"}}, }, { "type": "function", "function": { "name": "play_music", "arguments": { "playlist": "electronic" }, # mixed-type here since keys ["playlist"] and ["room", "state"] are different }, }, ], }, {"role": "tool", "name": "control_light", "content": "The lights in the living room are now on."}, {"role": "tool", "name": "play_music", "content": "The music is now playing."}, {"role": "assistant", "content": "Done!"}, ] EXPECTED_ONE = {"col_1": [1], "col_2": [2]} EXPECTED_THREE = {"col_1": [-1, 1, 10], "col_2": [None, 2, 20]} EXPECTED_LIST_OF_STRINGS = {"text": ["First text.", "Second text.", "Third text."]} EXPECTED_MIX = {"col_1": [-1, 1, "foo"]} EXPECTED_DICTS_WITH_VARYING_KEYS = {"col_1": [{"a": 0}, {"b": 0}, {"c": 0}]} EXPECTED_LISTS_OF_DICTS_WITH_VARYING_KEYS = {"col_1": [[{"a": 0}, {"b": 0}], [{"c": 0}, {"d": 0}]]} EXPECTED_MESSAGES = {"messages": [_messages]} @pytest.fixture def jsonl_file_with_messages(tmp_path): filename = tmp_path / "file.jsonl" data = json.dumps({"messages": _messages}) with open(filename, "w") as f: f.write(data) return str(filename) def write_jsonl(path, rows): with open(path, "w") as f: for row in rows: f.write(json.dumps(row) + "\n") return str(path) def generate_agent_traces_output(trace_file): json_builder = Json(features=AGENT_TRACES_FEATURES) base_files = [trace_file] files_iterables = [[trace_file]] original_files = list(base_files) generator = json_builder._generate_tables( base_files=base_files, files_iterables=files_iterables, original_files=original_files ) pa_table = pa.concat_tables([table for _, table in generator]) return Features.from_arrow_schema(pa_table.schema).decode_batch(pa_table.to_pydict()) def assert_agent_traces_output(tmp_path, filename, rows, expected, num_sessions=1): trace_file = write_jsonl(tmp_path / filename, rows) out = generate_agent_traces_output(trace_file) for key, value in zip(AGENT_TRACE_FIELD_NAMES_TO_CHECK, expected): assert out[key] == [value] * num_sessions assert out["file_path"] == [trace_file] * num_sessions assert isinstance(out["messages"], list) assert out["messages"] assert all( isinstance(message["role"], str) and isinstance(message["content"], str) for messages in out["messages"] for message in messages ) assert isinstance(out["tools"], list) assert isinstance(out["metadata"], (dict, list)) assert isinstance(out["trace"], (dict, list)) return trace_file, out AGENT_TRACE_FIELD_NAMES_TO_CHECK = ( "harness", "session_id", "prompt", "sent_at", "num_user_messages", "num_tool_calls", ) CODEX_AGENT_TRACE_ROWS = [ {"type": "session_meta", "payload": {"id": "codex-session"}}, { "type": "response_item", "payload": { "type": "message", "role": "user", "content": [{"type": "input_text", "text": "context-wrapped codex prompt"}], }, }, { "type": "response_item", "payload": { "type": "message", "role": "user", "content": [{"type": "input_text", "text": "actual codex prompt"}], }, }, { "timestamp": "2026-04-01T10:01:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "actual codex prompt"}, }, { "type": "response_item", "payload": {"type": "function_call", "name": "exec_command", "call_id": "call_1"}, }, { "type": "response_item", "payload": {"type": "function_call_output", "call_id": "call_1", "output": "done"}, }, ] CODEX_EXPECTED_AGENT_TRACE_FIELDS = ( "codex", "codex-session", "actual codex prompt", "2026-04-01T10:01:00.000Z", 1, 1, ) HERMES_SESSION = { "id": "20260605_092247_d018ec", "source": "cli", "model": "Qwen/Qwen3.5-35B-A3B", "system_prompt": "You are Hermes.", "started_at": 1_780_665_768.307, "message_count": 4, "tool_call_count": 1, "messages": [ { "session_id": "20260605_092247_d018ec", "role": "user", "content": "Run pwd and date.", "timestamp": 1_780_665_767.307, }, { "session_id": "20260605_092247_d018ec", "role": "assistant", "content": "", "reasoning_content": "The user asked for two shell commands.", "timestamp": 1_780_665_767.308, "tool_calls": [ { "id": "call_677f321e2b3047b4b8c7a1e1", "type": "function", "function": { "name": "terminal", "arguments": json.dumps({"command": "pwd && date -u +%Y-%m-%dT%H:%M:%SZ"}), }, }, ], }, { "session_id": "20260605_092247_d018ec", "role": "tool", "content": json.dumps({"output": "/tmp/work\n2026-06-05T13:22:47Z", "exit_code": 0, "error": None}), "tool_call_id": "call_677f321e2b3047b4b8c7a1e1", "timestamp": 1_780_665_767.309, }, { "session_id": "20260605_092247_d018ec", "role": "assistant", "content": "Working directory: `/tmp/work`.", "reasoning": "The commands executed successfully.", "timestamp": 1_780_665_767.31, }, ], } DROID_SESSION = [ { "type": "session_start", "id": "droid-session", "title": "inspect the project", "sessionTitle": "Inspect project files", "owner": "caleb", "version": 2, "cwd": "/workspace/project", }, { "type": "message", "id": "context-1", "timestamp": "2026-06-02T18:55:29.000Z", "message": { "role": "user", "visibility": "llm_only", "content": [{"type": "text", "text": "injected context"}], }, "parentId": None, }, { "type": "message", "id": "message-1", "timestamp": "2026-06-02T18:55:30.274Z", "message": { "role": "user", "content": [{"type": "text", "text": "Inspect the project"}], }, "parentId": "context-1", }, { "type": "message", "id": "message-2", "timestamp": "2026-06-02T18:55:35.000Z", "message": { "role": "assistant", "content": [ { "type": "thinking", "thinking": "I should list the files first.", "signature": "reasoning_content", "signatureProvider": "generic-chat-completion-api", "durationMs": 1200, }, {"type": "text", "text": "I'll list the files."}, {"type": "tool_use", "id": "LS_0", "name": "LS", "input": {"directory_path": "/workspace/project"}}, ], "chatCompletionReasoningField": "reasoning_content", "chatCompletionReasoningContent": "I should list the files first.", }, "parentId": "message-1", }, { "type": "message", "id": "message-3", "timestamp": "2026-06-02T18:55:36.000Z", "message": { "role": "user", "content": [ {"type": "tool_result", "tool_use_id": "LS_0", "is_error": False, "content": "README.md\nsrc"}, ], }, "parentId": "message-2", }, ] def test_config_raises_when_invalid_name() -> None: with pytest.raises(InvalidConfigName, match="Bad characters"): _ = JsonConfig(name="name-with-*-invalid-character") @pytest.mark.parametrize("data_files", ["str_path", ["str_path"], DataFilesList(["str_path"], [()])]) def test_config_raises_when_invalid_data_files(data_files) -> None: with pytest.raises(ValueError, match="Expected a DataFilesDict"): _ = JsonConfig(name="name", data_files=data_files) @pytest.mark.parametrize( "file_fixture, config_kwargs, expected", [ ("json_file", {}, EXPECTED_ONE), ("jsonl_file", {}, EXPECTED_THREE), ("ndjson_file", {}, EXPECTED_THREE), ("jsonl_file_utf16_encoded", {"encoding": "utf-16"}, EXPECTED_THREE), ("json_file_with_list_of_dicts", {}, EXPECTED_THREE), ("json_file_with_list_of_dicts_field", {"field": "field3"}, EXPECTED_THREE), ("json_file_with_list_of_strings", {}, EXPECTED_LIST_OF_STRINGS), ("json_file_with_list_of_strings_field", {"field": "field3"}, EXPECTED_LIST_OF_STRINGS), ("json_file_with_dict_of_lists_field", {"field": "field3"}, EXPECTED_THREE), ("jsonl_file_with_mix_of_str_and_int", {}, EXPECTED_MIX), ("jsonl_file_with_dicts_of_varying_keys", {}, EXPECTED_DICTS_WITH_VARYING_KEYS), ("jsonl_file_with_lists_of_dicts_of_varying_keys", {}, EXPECTED_LISTS_OF_DICTS_WITH_VARYING_KEYS), ( "jsonl_file_with_lists_of_dicts_of_varying_keys_and_bom", {}, EXPECTED_LISTS_OF_DICTS_WITH_VARYING_KEYS, ), ("jsonl_file_with_messages", {}, EXPECTED_MESSAGES), ], ) def test_json_generate_tables(file_fixture, config_kwargs, expected, request): json = Json(**config_kwargs) base_files = [request.getfixturevalue(file_fixture)] files_iterables = [[file] for file in base_files] original_files = list(base_files) generator = json._generate_tables( base_files=base_files, files_iterables=files_iterables, original_files=original_files ) pa_table = pa.concat_tables([table for _, table in generator]) out = Features.from_arrow_schema(pa_table.schema).decode_batch(pa_table.to_pydict()) assert out == expected @pytest.mark.parametrize( "file_fixture, config_kwargs", [ ( "jsonl_file", {"features": Features({"col_1": Value("int64"), "col_2": Value("int64"), "missing_col": Value("string")})}, ), ( "json_file_with_list_of_dicts", {"features": Features({"col_1": Value("int64"), "col_2": Value("int64"), "missing_col": Value("string")})}, ), ( "json_file_with_list_of_dicts_field", { "field": "field3", "features": Features( {"col_1": Value("int64"), "col_2": Value("int64"), "missing_col": Value("string")} ), }, ), ], ) def test_json_generate_tables_with_missing_features(file_fixture, config_kwargs, request): json = Json(**config_kwargs) base_files = [request.getfixturevalue(file_fixture)] files_iterables = [[file] for file in base_files] original_files = list(base_files) generator = json._generate_tables( base_files=base_files, files_iterables=files_iterables, original_files=original_files ) pa_table = pa.concat_tables([table for _, table in generator]) assert pa_table.to_pydict() == {"col_1": [-1, 1, 10], "col_2": [None, 2, 20], "missing_col": [None, None, None]} @pytest.mark.parametrize( "file_fixture, config_kwargs", [ ("json_file_with_list_of_dicts_with_sorted_columns", {}), ("json_file_with_list_of_dicts_with_sorted_columns_field", {"field": "field3"}), ], ) def test_json_generate_tables_with_sorted_columns(file_fixture, config_kwargs, request): json = Json(**config_kwargs) base_files = [request.getfixturevalue(file_fixture)] files_iterables = [[file] for file in base_files] original_files = list(base_files) generator = json._generate_tables( base_files=base_files, files_iterables=files_iterables, original_files=original_files ) pa_table = pa.concat_tables([table for _, table in generator]) assert pa_table.column_names == ["ID", "Language", "Topic"] @pytest.mark.parametrize( "filename, rows, expected", [ pytest.param("codex.jsonl", CODEX_AGENT_TRACE_ROWS, CODEX_EXPECTED_AGENT_TRACE_FIELDS, id="codex"), pytest.param( "codex_response_item_only.jsonl", [ {"type": "session_meta", "payload": {"id": "codex-session"}}, { "type": "response_item", "payload": { "type": "message", "role": "user", "content": [{"text": "codex response item prompt"}], }, }, { "type": "response_item", "payload": {"type": "function_call", "name": "exec_command", "call_id": "call_1"}, }, ], ("codex", "codex-session", None, None, 0, 1), id="codex-response-item-only", ), pytest.param( "claude.jsonl", [ { "timestamp": "2026-04-02T10:00:00.000Z", "type": "user", "sessionId": "claude-session", "message": {"role": "user", "content": "claude prompt"}, }, { "type": "assistant", "sessionId": "claude-session", "message": { "role": "assistant", "content": [{"type": "tool_use", "id": "toolu_1"}, {"type": "tool_use", "id": "toolu_2"}], }, }, { "type": "user", "sessionId": "claude-session", "message": {"role": "user", "content": [{"type": "tool_result", "content": "done"}]}, }, ], ("claude_code", "claude-session", "claude prompt", "2026-04-02T10:00:00.000Z", 1, 2), id="claude", ), pytest.param( "pi.jsonl", [ {"type": "session", "id": "pi-session"}, { "timestamp": "2026-04-03T10:01:00.000Z", "type": "message", "message": {"role": "user", "content": "pi prompt"}, }, { "type": "message", "message": { "role": "assistant", "content": [{"type": "toolCall", "id": "call_1"}, {"type": "toolCall", "id": "call_2"}], }, }, { "type": "message", "message": {"role": "toolResult", "content": [{"type": "text", "text": "done"}]}, }, { "type": "message", "message": {"role": "user", "content": [{"type": "text", "text": "second pi prompt"}]}, }, ], ("pi", "pi-session", "pi prompt", "2026-04-03T10:01:00.000Z", 2, 2), id="pi", ), pytest.param( "openclaw.jsonl", [ { "type": "session", "id": "openclaw-session", "cwd": "/Users/test/.openclaw/agents/main", }, { "timestamp": "2026-04-03T10:01:00.000Z", "type": "message", "message": {"role": "user", "content": "openclaw prompt"}, }, ], ("openclaw", "openclaw-session", "openclaw prompt", "2026-04-03T10:01:00.000Z", 1, 0), id="openclaw", ), pytest.param( "hermes.jsonl", [HERMES_SESSION], ("hermes", "20260605_092247_d018ec", "Run pwd and date.", "2026-06-05T13:22:48.307Z", 1, 1), ), pytest.param( "hermes_two_sessions.jsonl", [HERMES_SESSION] * 2, ("hermes", "20260605_092247_d018ec", "Run pwd and date.", "2026-06-05T13:22:48.307Z", 1, 1), ), pytest.param( "droid.jsonl", DROID_SESSION, ("droid", "droid-session", "Inspect the project", "2026-06-02T18:55:30.274Z", 1, 1), id="droid", ), pytest.param( "missing_prompt.jsonl", [ {"type": "session_meta", "payload": {"id": "codex-session"}}, { "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"text": "assistant response"}]}, }, ], ("codex", "codex-session", None, None, 0, 0), id="missing-prompt", ), ], ) @require_teich def test_json_generate_tables_with_agent_trace_metadata(tmp_path, filename, rows, expected): num_sessions = 2 if filename == "hermes_two_sessions.jsonl" else 1 _, out = assert_agent_traces_output(tmp_path, filename, rows, expected, num_sessions=num_sessions) if filename == "droid.jsonl": assert out["metadata"][0]["trace_type"] == "droid" assert "models" not in out @require_teich @pytest.mark.parametrize( "filename, rows, expected", [ pytest.param("codex.jsonl", CODEX_AGENT_TRACE_ROWS, CODEX_EXPECTED_AGENT_TRACE_FIELDS, id="codex"), pytest.param( "droid.jsonl", DROID_SESSION, ("droid", "droid-session", "Inspect the project", "2026-06-02T18:55:30.274Z", 1, 1), id="droid", ), ], ) def test_json_load_dataset_with_agent_trace_metadata(tmp_path, filename, rows, expected): trace_file = write_jsonl(tmp_path / filename, rows) dataset = load_dataset("json", data_files=trace_file, split="train", cache_dir=str(tmp_path / "cache")) row = dataset[0] assert dataset.column_names == [ "harness", "session_id", "prompt", "messages", "tools", "metadata", "sent_at", "num_user_messages", "num_tool_calls", "trace", "file_path", ] for key, value in zip(AGENT_TRACE_FIELD_NAMES_TO_CHECK, expected): assert row[key] == value, key if filename == "droid.jsonl": assert row["metadata"]["trace_type"] == "droid" assert json.loads(row["trace"].splitlines()[0])["type"] == "session_start" def test_json_load_dataset_without_droid_marker_stays_ordinary_json(tmp_path): trace_file = write_jsonl( tmp_path / "droid_missing_marker.jsonl", [ {"type": "session_start", "id": "droid-session", "version": 2}, { "type": "message", "id": "message-1", "timestamp": "2026-06-02T18:55:30.274Z", "message": {"role": "user", "content": [{"type": "text", "text": "Inspect the project"}]}, }, ], ) dataset = load_dataset("json", data_files=trace_file, split="train", cache_dir=str(tmp_path / "cache")) assert dataset.column_names == ["type", "id", "version", "timestamp", "message"] assert dataset[0]["type"] == "session_start"