Packages
Dataset management and caching for AI research benchmarks
Current section
Files
Jump to
Current section
Files
lib/dataset_manager/field_mapping.ex
defmodule CrucibleDatasets.FieldMapping do
@moduledoc """
Declarative field mapping for dataset loading.
Maps to inspect_ai's FieldSpec pattern.
## Examples
iex> mapping = FieldMapping.new(
...> input: "question",
...> expected: "answer",
...> metadata: ["difficulty", "subject"]
...> )
iex> record = %{"question" => "Q1", "answer" => "A1", "difficulty" => "easy"}
iex> FieldMapping.apply(mapping, record)
%{id: nil, input: "Q1", expected: "A1", metadata: %{difficulty: "easy"}}
"""
@type field_spec :: %{
source: String.t() | atom(),
target: :input | :expected | :id | {:metadata, atom()},
transform: (term() -> term()) | nil
}
@type t :: %__MODULE__{
input: String.t() | atom(),
expected: String.t() | atom(),
id: String.t() | atom() | nil,
choices: String.t() | atom() | nil,
metadata: [String.t() | atom()] | nil,
transforms: %{atom() => (term() -> term())}
}
defstruct [
:input,
:expected,
:id,
:choices,
:metadata,
:transforms
]
@doc """
Create a field mapping specification.
## Options
- `:input` - Source field for input (default: `:input`)
- `:expected` - Source field for expected output (default: `:expected`)
- `:id` - Source field for item ID (default: `:id`)
- `:choices` - Source field for multiple choice options (default: `nil`)
- `:metadata` - List of fields to include in metadata (default: `nil`)
- `:transforms` - Map of field transforms (default: `%{}`)
## Examples
iex> FieldMapping.new(
...> input: "question",
...> expected: "answer",
...> metadata: ["difficulty", "subject"]
...> )
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
%__MODULE__{
input: Keyword.get(opts, :input, :input),
expected: Keyword.get(opts, :expected, :expected),
id: Keyword.get(opts, :id, :id),
choices: Keyword.get(opts, :choices),
metadata: Keyword.get(opts, :metadata),
transforms: Keyword.get(opts, :transforms, %{})
}
end
@doc """
Apply field mapping to a raw record.
## Examples
iex> mapping = FieldMapping.new(input: "question", expected: "answer")
iex> record = %{"question" => "What is 2+2?", "answer" => "4"}
iex> FieldMapping.apply(mapping, record)
%{id: nil, input: "What is 2+2?", expected: "4", metadata: %{}}
"""
@spec apply(t(), map()) :: map()
def apply(%__MODULE__{} = mapping, record) when is_map(record) do
base = %{
id: get_field(record, mapping.id),
input: build_input(record, mapping),
expected: get_and_transform(record, mapping.expected, mapping.transforms[:expected])
}
metadata =
if mapping.metadata do
mapping.metadata
|> Enum.map(fn field ->
{to_atom(field), get_field(record, field)}
end)
|> Map.new()
else
%{}
end
Map.put(base, :metadata, metadata)
end
defp build_input(record, %{choices: nil} = mapping) do
get_and_transform(record, mapping.input, mapping.transforms[:input])
end
defp build_input(record, mapping) do
%{
question: get_and_transform(record, mapping.input, mapping.transforms[:input]),
choices: get_field(record, mapping.choices) |> List.wrap()
}
end
defp get_field(_record, nil), do: nil
defp get_field(record, field) when is_atom(field) do
Map.get(record, field) || Map.get(record, Atom.to_string(field))
end
defp get_field(record, field) when is_binary(field) do
Map.get(record, field) || Map.get(record, String.to_existing_atom(field))
rescue
ArgumentError -> Map.get(record, field)
end
defp get_and_transform(record, field, nil), do: get_field(record, field)
defp get_and_transform(record, field, transform) do
record |> get_field(field) |> transform.()
end
defp to_atom(field) when is_atom(field), do: field
defp to_atom(field) when is_binary(field), do: String.to_atom(field)
end