Skip to main content

typesafe_sdk/
response.rs

1//! What a call answers with.
2//!
3//! The response type is generic in its answers from the start, so the map of
4//! answers a runtime question set produces and the struct a derived one
5//! produces are the same type at different parameters rather than two types
6//! that drift apart.
7//!
8//! The received bytes are kept beside the decoded answers. An answer of a kind
9//! this version does not model is dropped by the decoder and is still there in
10//! the raw body, so a caller is never left with no way to read what the server
11//! actually said.
12//!
13//! Every container keeps the order that means something: the answers and a
14//! choice's probabilities are in the order the server sent them, which is the
15//! order the caller asked in, while a score's legend and probabilities are
16//! sorted by level, so two responses that differ only in the order of a score's
17//! keys compare equal and print the same.
18
19use std::fmt;
20
21use bytes::Bytes;
22use http::{HeaderMap, StatusCode};
23use serde::{
24    Serialize, Serializer,
25    ser::{SerializeMap, SerializeStruct},
26};
27
28use crate::{constants::request_id, content::Content, name::Name};
29
30// ---------------------------------------------------------------- response
31
32/// The answers to one System One call, with the model and token usage the
33/// server reported and the HTTP response they came in.
34///
35/// `A` is what the answers decode into: [`Answers`], a lookup by question name,
36/// unless the question set was declared as a struct, in which case each answer
37/// lands in that struct's field of the same name.
38///
39/// Serializing a response writes `model`, `usage` and `answers` only; the HTTP
40/// metadata in [`meta`](SystemOneResponse::meta) is runtime state, not part of
41/// the API payload.
42#[derive(Debug, Clone, PartialEq)]
43pub struct SystemOneResponse<A = Answers> {
44    model: Name,
45    usage: Usage,
46    answers: A,
47    meta: ResponseMeta,
48}
49
50impl<A> SystemOneResponse<A> {
51    /// Assembles a decoded response.
52    pub(crate) fn from_parts(model: Name, usage: Usage, answers: A, meta: ResponseMeta) -> Self {
53        Self { model, usage, answers, meta }
54    }
55
56    /// The model that answered. It may differ from the alias the request
57    /// named: asking for `jev-latest` is answered by a concrete model.
58    #[must_use]
59    pub fn model(&self) -> &str {
60        self.model.as_str()
61    }
62
63    /// The tokens the call used.
64    #[must_use]
65    pub fn usage(&self) -> &Usage {
66        &self.usage
67    }
68
69    /// The answers, one per question.
70    #[must_use]
71    pub fn answers(&self) -> &A {
72        &self.answers
73    }
74
75    /// The status, headers and raw body of the HTTP response.
76    #[must_use]
77    pub fn meta(&self) -> &ResponseMeta {
78        &self.meta
79    }
80
81    /// Gives up everything but the answers.
82    #[must_use]
83    pub fn into_answers(self) -> A {
84        self.answers
85    }
86}
87
88impl<A> Serialize for SystemOneResponse<A>
89where
90    A: Serialize,
91{
92    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
93    where
94        S: Serializer,
95    {
96        let mut out = serializer.serialize_struct("SystemOneResponse", 3)?;
97        out.serialize_field("model", &self.model)?;
98        out.serialize_field("usage", &self.usage)?;
99        out.serialize_field("answers", &self.answers)?;
100        out.end()
101    }
102}
103
104/// The HTTP side of a successful response.
105///
106/// The body is the exact bytes the server sent, kept as a reference-counted
107/// buffer rather than a copy. It is the way back to anything the decoder left
108/// out, such as an answer of a type this version does not know.
109///
110/// `Debug` prints the header count and the body length, not their contents: a
111/// response header may carry a cookie, and the body is the caller's to log.
112#[derive(Clone, PartialEq)]
113pub struct ResponseMeta {
114    status: StatusCode,
115    headers: HeaderMap,
116    body: Bytes,
117}
118
119impl ResponseMeta {
120    /// Keeps what a response arrived with.
121    pub(crate) fn new(status: StatusCode, headers: HeaderMap, body: Bytes) -> Self {
122        Self { status, headers, body }
123    }
124
125    /// Hands the parts back, for a failure that has to carry them.
126    pub(crate) fn into_parts(self) -> (StatusCode, HeaderMap, Bytes) {
127        (self.status, self.headers, self.body)
128    }
129
130    /// The HTTP status, which is always a success status here.
131    #[must_use]
132    pub fn status(&self) -> StatusCode {
133        self.status
134    }
135
136    /// The response headers.
137    #[must_use]
138    pub fn headers(&self) -> &HeaderMap {
139        &self.headers
140    }
141
142    /// The server's identifier for the request, from the
143    /// `x-typesafe-request-id` header, when the server sent one that is valid
144    /// text.
145    #[must_use]
146    pub fn request_id(&self) -> Option<&str> {
147        request_id(&self.headers)
148    }
149
150    /// The body exactly as it was received.
151    #[must_use]
152    pub fn raw_body(&self) -> &Bytes {
153        &self.body
154    }
155}
156
157impl fmt::Debug for ResponseMeta {
158    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159        formatter
160            .debug_struct("ResponseMeta")
161            .field("status", &self.status.as_u16())
162            .field("request_id", &self.request_id())
163            .field("headers", &format_args!("<{} headers>", self.headers.len()))
164            .field("body", &format_args!("<{} bytes>", self.body.len()))
165            .finish()
166    }
167}
168
169/// Token counts for a call, when the server reported them.
170///
171/// Both counts are optional because the API may leave either out; an absent
172/// count is `None`, never zero.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize)]
174pub struct Usage {
175    input_tokens: Option<u64>,
176    output_tokens: Option<u64>,
177}
178
179impl Usage {
180    /// Builds a usage record, for example to stand in for a real response in a
181    /// test.
182    #[must_use]
183    pub fn new(input_tokens: Option<u64>, output_tokens: Option<u64>) -> Self {
184        Self { input_tokens, output_tokens }
185    }
186
187    /// Billable input tokens.
188    #[must_use]
189    pub fn input_tokens(&self) -> Option<u64> {
190        self.input_tokens
191    }
192
193    /// Output tokens.
194    #[must_use]
195    pub fn output_tokens(&self) -> Option<u64> {
196        self.output_tokens
197    }
198}
199
200// ----------------------------------------------------------------- answers
201
202/// The answers of a response, looked up by question name.
203///
204/// The answers are kept in the order the server sent them, which is the order
205/// the questions were asked in. A lookup scans them, which for the handful of
206/// questions a call carries is faster than hashing the name. Should a document
207/// name one question twice - JSON allows it, the API does not do it - both
208/// answers are kept and a lookup finds the first.
209///
210/// The typed accessors ([`nouls`](Answers::nouls) and friends) filter as they
211/// iterate; nothing is copied or cached.
212#[derive(Debug, Clone, PartialEq, Default)]
213pub struct Answers {
214    entries: Vec<(Name, Answer)>,
215}
216
217impl Answers {
218    /// An empty set whose storage is sized for `capacity` answers.
219    pub(crate) fn with_capacity(capacity: usize) -> Self {
220        Self { entries: Vec::with_capacity(capacity) }
221    }
222
223    /// Appends one answer.
224    pub(crate) fn push(&mut self, name: Name, answer: Answer) {
225        self.entries.push((name, answer));
226    }
227
228    /// How many answers the storage holds without growing.
229    #[cfg(test)]
230    pub(crate) fn capacity(&self) -> usize {
231        self.entries.capacity()
232    }
233
234    /// How many answers there are.
235    #[must_use]
236    pub fn len(&self) -> usize {
237        self.entries.len()
238    }
239
240    /// Whether there are none.
241    #[must_use]
242    pub fn is_empty(&self) -> bool {
243        self.entries.is_empty()
244    }
245
246    /// The answer to the question called `name`.
247    #[must_use]
248    pub fn get(&self, name: &str) -> Option<&Answer> {
249        self.entries.iter().find(|(key, _)| key.as_str() == name).map(|(_, answer)| answer)
250    }
251
252    /// The answer to the yes/no question called `name`, if there is one and it
253    /// is a yes/no answer.
254    #[must_use]
255    pub fn noul(&self, name: &str) -> Option<&NoulAnswer> {
256        self.get(name).and_then(Answer::as_noul)
257    }
258
259    /// The answer to the choice question called `name`, if there is one and it
260    /// is a choice answer.
261    #[must_use]
262    pub fn choice(&self, name: &str) -> Option<&ChoiceAnswer> {
263        self.get(name).and_then(Answer::as_choice)
264    }
265
266    /// The answer to the score question called `name`, if there is one and it
267    /// is a score answer.
268    #[must_use]
269    pub fn score(&self, name: &str) -> Option<&ScoreAnswer> {
270        self.get(name).and_then(Answer::as_score)
271    }
272
273    /// Every answer with its question name, in the order received.
274    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &Answer)> + DoubleEndedIterator {
275        self.entries.iter().map(|(name, answer)| (name.as_str(), answer))
276    }
277
278    /// The question names, in the order received.
279    pub fn names(&self) -> impl ExactSizeIterator<Item = &str> + DoubleEndedIterator {
280        self.entries.iter().map(|(name, _)| name.as_str())
281    }
282
283    /// The yes/no answers, in the order received.
284    pub fn nouls(&self) -> impl DoubleEndedIterator<Item = (&str, &NoulAnswer)> {
285        self.iter().filter_map(|(name, answer)| answer.as_noul().map(|noul| (name, noul)))
286    }
287
288    /// The choice answers, in the order received.
289    pub fn choices(&self) -> impl DoubleEndedIterator<Item = (&str, &ChoiceAnswer)> {
290        self.iter().filter_map(|(name, answer)| answer.as_choice().map(|choice| (name, choice)))
291    }
292
293    /// The score answers, in the order received.
294    pub fn scores(&self) -> impl DoubleEndedIterator<Item = (&str, &ScoreAnswer)> {
295        self.iter().filter_map(|(name, answer)| answer.as_score().map(|score| (name, score)))
296    }
297}
298
299impl<S> FromIterator<(S, Answer)> for Answers
300where
301    S: Into<String>,
302{
303    fn from_iter<I>(iter: I) -> Self
304    where
305        I: IntoIterator<Item = (S, Answer)>,
306    {
307        Self {
308            entries: iter
309                .into_iter()
310                .map(|(name, answer)| (Name::from(name.into()), answer))
311                .collect(),
312        }
313    }
314}
315
316impl Serialize for Answers {
317    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
318    where
319        S: Serializer,
320    {
321        Pairs(&self.entries).serialize(serializer)
322    }
323}
324
325/// One answer, of whichever kind its question was.
326#[derive(Debug, Clone, PartialEq)]
327#[non_exhaustive]
328pub enum Answer {
329    /// The answer to a yes/no question.
330    Noul(NoulAnswer),
331    /// The answer to a choice question.
332    Choice(ChoiceAnswer),
333    /// The answer to a score question.
334    Score(ScoreAnswer),
335}
336
337impl Answer {
338    /// The yes/no answer, when this is one.
339    #[must_use]
340    pub fn as_noul(&self) -> Option<&NoulAnswer> {
341        match self {
342            Self::Noul(answer) => Some(answer),
343            Self::Choice(_) | Self::Score(_) => None,
344        }
345    }
346
347    /// The choice answer, when this is one.
348    #[must_use]
349    pub fn as_choice(&self) -> Option<&ChoiceAnswer> {
350        match self {
351            Self::Choice(answer) => Some(answer),
352            Self::Noul(_) | Self::Score(_) => None,
353        }
354    }
355
356    /// The score answer, when this is one.
357    #[must_use]
358    pub fn as_score(&self) -> Option<&ScoreAnswer> {
359        match self {
360            Self::Score(answer) => Some(answer),
361            Self::Noul(_) | Self::Choice(_) => None,
362        }
363    }
364}
365
366impl From<NoulAnswer> for Answer {
367    fn from(answer: NoulAnswer) -> Self {
368        Self::Noul(answer)
369    }
370}
371
372impl From<ChoiceAnswer> for Answer {
373    fn from(answer: ChoiceAnswer) -> Self {
374        Self::Choice(answer)
375    }
376}
377
378impl From<ScoreAnswer> for Answer {
379    fn from(answer: ScoreAnswer) -> Self {
380        Self::Score(answer)
381    }
382}
383
384impl Serialize for Answer {
385    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
386    where
387        S: Serializer,
388    {
389        match self {
390            Self::Noul(answer) => answer.serialize(serializer),
391            Self::Choice(answer) => answer.serialize(serializer),
392            Self::Score(answer) => answer.serialize(serializer),
393        }
394    }
395}
396
397/// A yes/no answer.
398///
399/// See the [noul primitive](https://docs.typesafe.ai/primitives/noul).
400#[derive(Debug, Clone, Copy, PartialEq)]
401pub struct NoulAnswer {
402    noul: f64,
403}
404
405impl NoulAnswer {
406    /// Builds an answer, for example to stand in for a real response in a
407    /// test.
408    #[must_use]
409    pub fn new(noul: f64) -> Self {
410        Self { noul }
411    }
412
413    /// The probability, from 0 to 1, that the answer is yes or the statement
414    /// is true. Near 0.5 means the model is unsure.
415    #[must_use]
416    pub fn noul(&self) -> f64 {
417        self.noul
418    }
419}
420
421impl Serialize for NoulAnswer {
422    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
423    where
424        S: Serializer,
425    {
426        let mut out = serializer.serialize_struct("NoulAnswer", 2)?;
427        out.serialize_field("type", "noul")?;
428        out.serialize_field("noul", &self.noul)?;
429        out.end()
430    }
431}
432
433/// The option a choice question picked, with how likely each option was.
434///
435/// See the [choice primitive](https://docs.typesafe.ai/primitives/choice).
436#[derive(Debug, Clone, PartialEq)]
437pub struct ChoiceAnswer {
438    choice: Name,
439    confidence: f64,
440    probabilities: Vec<(Name, f64)>,
441}
442
443impl ChoiceAnswer {
444    /// Builds an answer, for example to stand in for a real response in a
445    /// test. The probabilities keep the order they are given in.
446    #[must_use]
447    pub fn new<C, I, S>(choice: C, confidence: f64, probabilities: I) -> Self
448    where
449        C: Into<String>,
450        I: IntoIterator<Item = (S, f64)>,
451        S: Into<String>,
452    {
453        Self::from_parts(
454            Name::from(choice.into()),
455            confidence,
456            probabilities
457                .into_iter()
458                .map(|(name, probability)| (Name::from(name.into()), probability))
459                .collect(),
460        )
461    }
462
463    /// Assembles a decoded answer.
464    pub(crate) fn from_parts(
465        choice: Name,
466        confidence: f64,
467        probabilities: Vec<(Name, f64)>,
468    ) -> Self {
469        Self { choice, confidence, probabilities }
470    }
471
472    /// The option with the highest probability.
473    #[must_use]
474    pub fn choice(&self) -> &str {
475        self.choice.as_str()
476    }
477
478    /// How sure the model is of the pick, from 0 to 1.
479    #[must_use]
480    pub fn confidence(&self) -> f64 {
481        self.confidence
482    }
483
484    /// Every option with its probability, in the order received.
485    pub fn probabilities(
486        &self,
487    ) -> impl ExactSizeIterator<Item = (&str, f64)> + DoubleEndedIterator {
488        self.probabilities.iter().map(|(name, probability)| (name.as_str(), *probability))
489    }
490
491    /// The probability of the option called `name`.
492    #[must_use]
493    pub fn probability(&self, name: &str) -> Option<f64> {
494        self.probabilities
495            .iter()
496            .find(|(key, _)| key.as_str() == name)
497            .map(|(_, probability)| *probability)
498    }
499}
500
501impl Serialize for ChoiceAnswer {
502    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
503    where
504        S: Serializer,
505    {
506        let mut out = serializer.serialize_struct("ChoiceAnswer", 4)?;
507        out.serialize_field("type", "choice")?;
508        out.serialize_field("choice", &self.choice)?;
509        out.serialize_field("confidence", &self.confidence)?;
510        out.serialize_field("probabilities", &Pairs(&self.probabilities))?;
511        out.end()
512    }
513}
514
515/// A rating on the levels a score question defined, with the level
516/// descriptions it was rated against and how likely each level was.
517///
518/// See the [score primitive](https://docs.typesafe.ai/primitives/score).
519///
520/// # Storing a score
521///
522/// A score serializes with any serde format, but reading one back is
523/// supported through a JSON codec (sonic-rs, `serde_json`) only. Through a
524/// non-JSON serde format a score whose legend holds text fails to read back:
525/// each description is a [`Content`], which is read as JSON text in every
526/// format, and a plain description such as `low` is not JSON text.
527#[derive(Debug, Clone, PartialEq)]
528pub struct ScoreAnswer {
529    score: f64,
530    confidence: f64,
531    legend: Vec<(u32, Content<'static>)>,
532    probabilities: Vec<(u32, f64)>,
533}
534
535impl ScoreAnswer {
536    /// Builds an answer, for example to stand in for a real response in a
537    /// test. The legend and the probabilities are sorted by level.
538    #[must_use]
539    pub fn new<L, P>(score: f64, confidence: f64, legend: L, probabilities: P) -> Self
540    where
541        L: IntoIterator<Item = (u32, Content<'static>)>,
542        P: IntoIterator<Item = (u32, f64)>,
543    {
544        let mut sorted_legend: Vec<_> = legend.into_iter().collect();
545        sort_by_level(&mut sorted_legend);
546        let mut sorted_probabilities: Vec<_> = probabilities.into_iter().collect();
547        sort_by_level(&mut sorted_probabilities);
548        Self::from_sorted(score, confidence, sorted_legend, sorted_probabilities)
549    }
550
551    /// Assembles a decoded answer from maps the decoder built with
552    /// [`push_by_level`] and [`sort_by_level`].
553    pub(crate) fn from_sorted(
554        score: f64,
555        confidence: f64,
556        legend: Vec<(u32, Content<'static>)>,
557        probabilities: Vec<(u32, f64)>,
558    ) -> Self {
559        debug_assert!(legend.is_sorted_by_key(|(level, _)| *level), "the legend is sorted");
560        debug_assert!(
561            probabilities.is_sorted_by_key(|(level, _)| *level),
562            "the probabilities are sorted"
563        );
564        Self { score, confidence, legend, probabilities }
565    }
566
567    /// The expected score: the probability-weighted average of the levels. It
568    /// may fall between two levels.
569    #[must_use]
570    pub fn score(&self) -> f64 {
571        self.score
572    }
573
574    /// How sure the model is of the score, from 0 to 1.
575    #[must_use]
576    pub fn confidence(&self) -> f64 {
577        self.confidence
578    }
579
580    /// Every level with the description it was rated against, lowest level
581    /// first.
582    pub fn legend(
583        &self,
584    ) -> impl ExactSizeIterator<Item = (u32, &Content<'static>)> + DoubleEndedIterator {
585        self.legend.iter().map(|(level, description)| (*level, description))
586    }
587
588    /// The description of `level`.
589    #[must_use]
590    pub fn description(&self, level: u32) -> Option<&Content<'static>> {
591        self.legend.binary_search_by_key(&level, |(key, _)| *key).ok().map(|at| &self.legend[at].1)
592    }
593
594    /// Every level with its probability, lowest level first.
595    pub fn probabilities(&self) -> impl ExactSizeIterator<Item = (u32, f64)> + DoubleEndedIterator {
596        self.probabilities.iter().copied()
597    }
598
599    /// The probability of `level`.
600    #[must_use]
601    pub fn probability(&self, level: u32) -> Option<f64> {
602        self.probabilities
603            .binary_search_by_key(&level, |(key, _)| *key)
604            .ok()
605            .map(|at| self.probabilities[at].1)
606    }
607}
608
609impl Serialize for ScoreAnswer {
610    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
611    where
612        S: Serializer,
613    {
614        let mut out = serializer.serialize_struct("ScoreAnswer", 5)?;
615        out.serialize_field("type", "score")?;
616        out.serialize_field("score", &self.score)?;
617        out.serialize_field("confidence", &self.confidence)?;
618        out.serialize_field("legend", &Pairs(&self.legend))?;
619        out.serialize_field("probabilities", &Pairs(&self.probabilities))?;
620        out.end()
621    }
622}
623
624/// Writes a list of pairs as a JSON object. An integer key is written as the
625/// text of the integer, which is how JSON spells a score level.
626struct Pairs<'a, K, V>(&'a [(K, V)]);
627
628impl<K, V> Serialize for Pairs<'_, K, V>
629where
630    K: Serialize,
631    V: Serialize,
632{
633    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
634    where
635        S: Serializer,
636    {
637        let mut out = serializer.serialize_map(Some(self.0.len()))?;
638        for (key, value) in self.0 {
639            out.serialize_entry(key, value)?;
640        }
641        out.end()
642    }
643}
644
645/// Appends `value` and clears `in_order` when its level sorts before the
646/// level of the entry ahead of it; a list whose `in_order` was cleared is
647/// passed to [`sort_by_level`] once it is complete.
648///
649/// The number of levels and their order are chosen by whoever wrote the
650/// response, so the list is not kept sorted while it is built: moving the
651/// tail on every insert costs O(n^2) for levels that arrive in descending
652/// order, where one sort at the end costs O(n log n). Levels that arrive in
653/// order, as the API writes them, cost one comparison each and no sort.
654pub(crate) fn push_by_level<T>(
655    entries: &mut Vec<(u32, T)>,
656    in_order: &mut bool,
657    level: u32,
658    value: T,
659) {
660    *in_order &= entries.last().is_none_or(|(last, _)| *last <= level);
661    entries.push((level, value));
662}
663
664/// Sorts `entries` by level. The sort is stable, so a level named twice keeps
665/// both entries in the order they arrived.
666pub(crate) fn sort_by_level<T>(entries: &mut [(u32, T)]) {
667    entries.sort_by_key(|(level, _)| *level);
668}
669
670#[cfg(test)]
671#[path = "response_tests.rs"]
672mod tests;