# `ALLM.ClassificationQuestion`
[🔗](https://github.com/cykod/ALLM/blob/v0.6.1/lib/allm/classification_question.ex#L1)

One typed question inside an `ALLM.ClassificationRequest` — Layer A
serializable data.

    iex> q = ALLM.ClassificationQuestion.choice("Which team should handle this?", ["billing", "technical"])
    iex> q.type
    :choice
    iex> q.criteria
    %{"billing" => nil, "technical" => nil}

## Question types

There are three, and a request may mix them freely:

  * `:choice` — pick exactly one option from a named set. The answer
    reports the chosen option plus a probability per option.
  * `:score` — place the input on an ordered scale of levels, low to high.
    The answer reports a (fractional) position on that scale.
  * `:yes_no` — the probability that the answer is "yes". Some providers
    use their own name for this type (TypeSafe calls it "noul"); ALLM
    always spells it `:yes_no`, and an adapter translates on the wire.

## Instructions and criteria

`:instructions` is the question itself. It is a string, or a JSON object or
array when the question is better expressed as structured data.

`:criteria` depends on the type:

| Type | `:criteria` |
|------|-------------|
| `:choice` | a map of option name to an optional description (`nil`, a string, or structured data) |
| `:score` | a list of level descriptions, where index = level |
| `:yes_no` | `nil`, or a map whose keys are among `"true"` and `"false"`, describing what each answer means |

Use the builders `choice/2`, `score/2` and `yes_no/2`; they produce these
shapes and stringify atom option names so the question survives a JSON
round trip unchanged.

## Construction

`new/1` is a bare `struct!/2` pass-through: unknown keys raise `KeyError`
and nothing else is checked. The builders raise on a wrongly-typed
argument (`FunctionClauseError`, or `ArgumentError` for an unknown
`yes_no/2` option or colliding `choice/2` option names), but counts, emptiness and provider limits are left to
`ALLM.Validate.classification_request/1`, so a hand-built struct and a
builder-made one are judged by the same rules.

# `question_type`

```elixir
@type question_type() :: :choice | :score | :yes_no
```

A question type. `:yes_no` is ALLM's neutral name for a yes/no probability.

# `structured`

```elixir
@type structured() :: String.t() | map() | list()
```

String, JSON object, or JSON array: structured instructions or criteria.

# `t`

```elixir
@type t() :: %ALLM.ClassificationQuestion{
  criteria:
    %{required(String.t()) =&gt; structured() | nil} | [structured()] | map() | nil,
  instructions: structured() | nil,
  type: question_type() | nil
}
```

# `choice`

```elixir
@spec choice(
  structured(),
  [String.t() | atom()] | %{required(String.t() | atom()) =&gt; structured() | nil}
) :: t()
```

Choice over options.

`options` is a list of names (each description is `nil`) or a map of
name to description. Atom names are stringified. An `options` that is
neither a list nor a map raises `FunctionClauseError`. A keyword list is
still a list, so `choice(q, billing: "Payments")` treats each
`{name, description}` pair as a name and raises `Protocol.UndefinedError` —
pass a map for named descriptions. Two distinct names that become the same
string (`:billing` and `"billing"`) raise `ArgumentError` rather than
silently merging into one option; repeating the identical name in a list
is harmless.

## Examples

    iex> q = ALLM.ClassificationQuestion.choice("Which team?", [:billing, "sales"])
    iex> q.criteria
    %{"billing" => nil, "sales" => nil}

    iex> q = ALLM.ClassificationQuestion.choice("Which team?", %{billing: "Payments and invoices"})
    iex> q.criteria
    %{"billing" => "Payments and invoices"}

# `new`

```elixir
@spec new(keyword()) :: t()
```

Build a `%ClassificationQuestion{}` from keyword opts.

Unknown keys raise `KeyError` via `struct!/2`. No validation.

## Examples

    iex> q = ALLM.ClassificationQuestion.new(type: :yes_no, instructions: "Is this spam?")
    iex> {q.type, q.criteria}
    {:yes_no, nil}

# `score`

```elixir
@spec score(structured(), [structured()]) :: t()
```

Score on ordered levels, low to high. Level N is `Enum.at(levels, N)`.

A non-list `levels` raises `FunctionClauseError`.

## Examples

    iex> q = ALLM.ClassificationQuestion.score("How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"])
    iex> {q.type, Enum.at(q.criteria, 1)}
    {:score, "Frustrated"}

# `yes_no`

```elixir
@spec yes_no(
  structured(),
  keyword()
) :: t()
```

Yes/no. Optional `true:` / `false:` describe what each answer means.

With neither option `:criteria` is `nil`; otherwise it carries only the
keys given. Any other option raises `ArgumentError`.

## Examples

    iex> ALLM.ClassificationQuestion.yes_no("Is a refund requested?").criteria
    nil

    iex> ALLM.ClassificationQuestion.yes_no("Is a refund requested?", true: "The customer asks for money back").criteria
    %{"true" => "The customer asks for money back"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
