Decodes one wire answer against the question that produced it, and turns any answer into a routing decision.
Decoding needs the question (to know which type is expected, and to check the
answer against the options or levels that were actually sent) and the
TypeSafeAPI.Keys registry built for the request (to hand ids and Choice
options back under the caller's own atoms or strings, never by calling
String.to_atom/1 on anything the API sent).
Decoding is strict: a probability that is not a number, an option or level
the caller never sent, a Noul outside 0.0..1.0, or a choice that is not
the highest-probability option all decode to an Error.unexpected/2 rather
than to a struct that is confidently wrong. The typed layer exists to catch
exactly these, and TypeSafeAPI.Result's raw still holds the response as
it arrived.
gate/2 and confidence/1 work the same way across all three answer types,
so a caller who only cares about "should I trust this" doesn't need to branch
on which question it was. yes?/2 is Noul-only: a Choice or Score has no
yes/no reading.
Summary
Functions
The confidence value gate/2 uses: max(noul, 1 - noul) for Noul, and the
wire confidence for Choice and Score.
Decodes one answer from the wire answers object.
Routes an answer into :act, :review, or :escalate by comparing
confidence/1 against two thresholds.
Whether a Noul answer clears a probability threshold. Defaults to 0.5.
Types
@type t() :: TypeSafeAPI.Answer.Noul.t() | TypeSafeAPI.Answer.Choice.t() | TypeSafeAPI.Answer.Score.t()
Functions
The confidence value gate/2 uses: max(noul, 1 - noul) for Noul, and the
wire confidence for Choice and Score.
The API returns no confidence for a Noul answer, only the probability, so
the Noul value is this library's convention rather than something the model
reported. It reads distance from 0.5 as certainty: 0.92 and 0.08 both give
0.92, and 0.5 gives 0.5. That makes a Noul comparable to a Choice or Score
in gate/2, but it is not the same measurement, so do not tune one
threshold against numbers from the other. It is also the confidence field
on TypeSafeAPI.Answer.Noul, so all three answer structs have the same shape.
@spec decode(map(), String.t(), TypeSafeAPI.Question.t(), TypeSafeAPI.Keys.t()) :: {:ok, t()} | {:error, TypeSafeAPI.Error.t()}
Decodes one answer from the wire answers object.
wire_id is the string key the answer was found under; question is the
normalized question it answers. A question/raw_answer type mismatch, an
unrecognized "type", a missing or invalid field, or a value that disagrees
with the question all decode to an Error.unexpected/2.
Routes an answer into :act, :review, or :escalate by comparing
confidence/1 against two thresholds.
Both :act and :review are required. Raises ArgumentError if :act is
lower than :review, since that would make the review band unreachable.
A Noul answer's confidence is max(noul, 1 - noul), which has a floor of
0.5, so :escalate is unreachable for a Noul unless :review is above 0.5.
Rather than silently never escalating, gate/2 raises ArgumentError when a
Noul answer is given a :review threshold of 0.5 or lower. A Noul sitting at
0.5 is the most uncertain answer the model can give, so the useful escalation
threshold is just above it (review: 0.55, say). Choice and Score
confidences come from the API and can be anything in 0.0..1.0, so they take
any thresholds.
@spec yes?(TypeSafeAPI.Answer.Noul.t(), number()) :: boolean()
Whether a Noul answer clears a probability threshold. Defaults to 0.5.
Noul answers only. A Choice or Score answer has no yes/no reading, so this
raises ArgumentError rather than inventing one; use gate/2 to route on
confidence, or compare the fields you care about instead.