system2 0.1.0

An (unofficial) idiomatic Rust SDK for TypeSafe's System One model, Jev.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
# system2

[![crates.io](https://img.shields.io/crates/v/system2.svg)](https://crates.io/crates/system2) [![docs.rs](https://img.shields.io/docsrs/system2)](https://docs.rs/system2) [![CI](https://github.com/jhartquist/system2/actions/workflows/ci.yml/badge.svg)](https://github.com/jhartquist/system2/actions/workflows/ci.yml)

An (unofficial) idiomatic Rust SDK for [TypeSafe](https://typesafe.ai/)'s System One model,
[Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev).

Daniel Kahneman's *Thinking, Fast and Slow* popularized two modes of thinking:

- **System 1** makes quick, intuitive judgments.
- **System 2** reasons slowly and deliberately.

> the goal is to make System 1 tasks extremely reliable and get System 2 by composing with code
>
> \- [Diogo Almeida]https://x.com/CompleteSkeptic/status/2099981459541143995, CEO of TypeSafe

TypeSafe calls Jev a [System One](https://docs.typesafe.ai/concepts/system-one) model: it answers
narrow questions about an input, called the `state`. Unlike an LLM asked for structured output, Jev
doesn't generate text or produce any chain-of-thought: it answers with calibrated probabilities.
`system2` brings those judgments into Rust. A question or a state can be as simple as a string, or
it can be built from your own enums and structs, and even images. Either way, an answer hands back
the values you passed in, with their probabilities, so you can compose them into decisions
with code.

## Why system2

- **Flexible**: strings, JSON, images, your own enums, or any `Serialize` or `Display` type.
- **Ergonomic**: macros and derives for static questions, options, and levels, with helpful
  compiler errors.
- **Verifiable**: static questions checked at compile time, dynamic ones on creation, and every
  answer on arrival.
- **Composable**: nest tuples, arrays, `Vec`s, and structs of questions in a single request.
- **Testable**: cache and replay recorded answers, and unit test your decisions with no network.
- **Observable**: OpenTelemetry-compatible `tracing` spans with latency, tokens, model, retries,
  and request ID.
- **Resilient**: retries with backoff, timeouts, and a concurrency limit built in.

## Install

[Sign up](https://console.typesafe.ai/home) for TypeSafe, create an
[API key](https://console.typesafe.ai/keys), then:

```sh
cargo add system2
cargo add tokio --features macros,rt-multi-thread
export TYPESAFE_API_KEY=...
```

`system2` is young, so expect breaking changes before 1.0.

## Quick start

Jev has four kinds of question: a [`noul`][Noul] asks yes or no, a [`choice`][Choice] picks one
of your options, a [`score`][Score] picks a level on your ordered scale, and a [`bbox`][Bbox] finds
a region of an image. This example asks two nouls in one request, then decides based on the
probability of yes:

```rust,no_run
use system2::prelude::*;

#[tokio::main]
async fn main() -> system2::Result<()> {
    let ticket = "I was charged twice. Fix it today.";

    let (angry, refund) = (
        noul!("Is the customer angry?"),
        noul!("Does the customer ask for a refund?"),
    )
        .ask(ticket)
        .await?;

    if refund.probability() > 0.9 {
        println!("open a refund");
    } else if angry.probability() > 0.8 {
        println!("send to a person");
    } else {
        println!("add to the queue");
    }
    Ok(())
}
```

Jev answered 0.67 for the refund, under the 0.9 the code wants, and 0.85 for angry, over 0.8, so
this prints `send to a person`.

[`.ask`][Ask::ask] takes any question or set of questions, and a state, and returns the answers in
the shape of the questions. There is no client to set up. The first `.ask` makes a global client
from `TYPESAFE_API_KEY`, and every later `.ask` shares it. `.ask` consumes what it asks, so clone a
question to ask it again.

The prelude has the macros and the traits. The snippets below are fragments, so `ticket` is whatever
state each one needs. Import each type they use by name, as in `use system2::Noul;`.

## Questions and state

A question and a state can each be any `serde::Serialize` value. Each must serialize to text, an
object, or an array, and an empty one fails before any request is sent. Here the question
carries a struct of your own, and the state is JSON. Add serde with
`cargo add serde --features derive` and `cargo add serde_json`:

```rust,ignore
use serde::Serialize;
use serde_json::json;
use system2::Noul;

#[derive(Serialize)]
struct Policy {
    name: String,
    text: String,
}

let policy = Policy {
    name: "passwords".to_string(),
    text: "A ticket never contains a password.".to_string(),
};
let question = json!({ "question": "Does the ticket break this policy?", "policy": policy });
let ticket = json!({ "plan": "pro", "message": "I was charged twice. Fix it today." });

let broken = Noul::new(question)?.ask(&ticket).await?;
```

A noul can also say what a yes and a no mean. These are its criteria, and you can give one or
both:

```rust
use system2::Noul;
use system2::prelude::*;

const REFUND: Noul = noul!("Does the customer ask for a refund?", {
    "true": "The customer asks for money back.",
    "false": "The customer asks for no money back.",
});
```

A [`Choice`][Choice] asks Jev to pick one of your options, and its answer gives each option a
probability. An option can be any value with `Display`, which gives the option its name.

```rust,ignore
let teams: Vec<String> = load_teams(); // ["billing", "engineering", "support"]

let team = Choice::new("Which team should handle this ticket?", teams)?
    .ask(&ticket)
    .await?;

println!("{}", team.most_likely()); // billing
```

It also takes your own types. The enum is the list of options, its doc comments describe them to
Jev, and the answer is one of your variants. Each option's name is its variant in snake case, such
as `billing`:

```rust,ignore
#[derive(Serialize)]
struct Ticket { id: u64, plan: &'static str, message: &'static str }

#[derive(Debug, Clone, Copy, Options)]
enum Team {
    /// Payments, refunds, and invoices.
    Billing,
    /// Bugs and outages.
    Engineering,
    /// Anything else.
    Support,
}

let team = choice!("Which team should handle this ticket?", Team).ask(&ticket).await?;

println!("{:?}", team.most_likely()); // Billing
```

To keep your doc comments for your own docs, give Jev its text with an attribute instead.
`description` replaces the doc comment, and `rename` changes the name Jev sees:

```rust
use system2::prelude::*;

#[derive(Debug, Clone, Copy, Options)]
enum Team {
    #[system2(rename = "payments", description = "Payments, refunds, and invoices.")]
    Billing,
    #[system2(description = "Bugs and outages.")]
    Engineering,
    #[system2(description = "Anything else.")]
    Support,
}
```

Options can also be written inline: as names, as names with descriptions, or as tuples with a
value of your own that the answer hands back:

```rust
use system2::Choice;
use system2::prelude::*;

const LANGUAGE: Choice<&str> = choice!("In which language is the ticket?", ["en", "es"]);

const NEXT: Choice<&str> = choice!("What should support do next?", {
    "reply": "Send an answer to the customer.",
    "escalate": "Send the ticket to a lead.",
});

const DAYS: Choice<u32> = choice!(
    "When should support answer?",
    [
        ("today", "Before the end of the day.", 1),
        ("this_week", "Before the end of the week.", 7),
    ]
);
```

For values from data, [`Choice::with_values`][Choice::with_values] takes your values and a closure
that gives each one its name, or its name and a description. The answer hands back the value Jev
picked, with no lookup:

```rust,ignore
#[derive(Deserialize)]
struct Person {
    name: String,
    skills: String,
}

let people: Vec<Person> = load_people();
let owner = Choice::with_values("Who should answer this ticket?", people, |person| {
    (person.name.clone(), person.skills.clone())
})?
.ask(&ticket)
.await?;

let person: Person = owner.into_most_likely();
```

An [`Image`][Image] goes anywhere text does: in a `{placeholder}` of a question, in `json!`, in a
struct, or as an option. Here a customer sends a screenshot, and the code compares it to a picture
of a known bug that ships with the app:

```rust,ignore
const KNOWN_BUG: Image = image!("known_bug.png");

let screenshot = Image::new(bytes)?; // the bytes of the customer's upload

let same = noul!("Does {screenshot} show the same error as {KNOWN_BUG}?")
    .ask("The app shows this error when I pay.")
    .await?;

if same.is_likely() {
    let dialog = bbox!("The error dialog.").ask(&screenshot).await?;
    println!("the dialog is at {:?}", dialog.to_pixels(&screenshot));
}
```

`image!` uses `include_bytes!` under the hood and checks the image when the crate builds, so a
missing or bad image is a compile error. `Image::new` checks bytes that arrive at run time.
`is_likely` is true at a probability of 0.5 or more. A [`bbox`][Bbox] asks where something is,
and `to_pixels` turns its answer into a box in the pixels of the image. Ask a bbox about one image
at a time, because its answer does not say which image holds the box.

## Validation

Each question type has two ways in. A constructor, such as [`Noul::new`][Noul::new] or
[`Noul::with_criteria`][Noul::with_criteria], checks its input at run time and returns a
`Result<Noul, InvalidNoul>`. A macro, such as [`noul!`][noul!], is a shortcut for the same rules,
checked by the compiler, so it gives you a `Noul` with nothing to unwrap. It can still take
run-time values in its placeholders, as in `noul!("Is the ticket about {product}?")`, because the
text around them already proves the question valid. Anything the compiler cannot see, such as a
whole question or the options of a Choice from data, goes through a constructor.

A mistake in a macro is a compile error at the bad token:

```text
error: each option must have a different name, but two options have the name `go`
 --> src/main.rs:4:55
  |
4 |     let _ = choice!("Which language?", ["go", "rust", "go"]);
  |                                                       ^^^^
```

A constructor gives the same message at run time, before any request is sent:

```rust
use system2::Choice;

let teams = vec!["billing", "support", "billing"]; // from a bad config file

if let Err(error) = Choice::new("Which team should handle this ticket?", teams) {
    println!("{error}");
    // each option must have a different name, but two options have the name `billing`
}
```

The answer is checked too. Jev is built to answer inside your options, and the SDK still checks
each response against its question at the boundary, so a bug or a proxy in between gives you an
error instead of a bad value. That is a check on shape, not on truth: a valid answer can still be
a wrong judgment, which is why every answer carries its probabilities.

## Question sets

A set is a group of questions that one request asks. There are three ways to build one:

- A tuple, an array, or a `Vec` of questions. The answers come back in the same shape.
- [`questions!`][questions!], which names each answer, inline.
- `#[derive(Answers)]`, which names them in a struct of your own, to return from a function.

The first example was a tuple of two questions. Sets nest, so a tuple can hold a `Vec` with one
question for each item in your data:

```rust,ignore
let products = load_products(); // ["the mobile app", "the web app"]

let (angry, about) = (
    noul!("Is the customer angry?"),
    products
        .iter()
        .map(|product| noul!("Is the ticket about {product}?"))
        .collect::<Vec<_>>(),
)
    .ask(&ticket)
    .await?;
```

The answers come back in the shape of the questions: `angry` is one answer, and `about` is a
`Vec` with an answer for each product. An array works the same way and gives an array back. An
`Option` of a set asks it only when it is `Some`, as in `paid.then_some(REFUND)`. An iterator of
questions has `.ask` too, and an empty set sends no request at all.

When a set grows, give its answers names with `questions!`. This one asks three things about a
ticket in one request, then decides. The score's levels come from an enum, lowest first. Jev
knows a level only by its text, so each doc comment is the text of its level, and an enum with
no doc comments uses the names of its variants, such as `low`:

```rust,ignore
#[derive(Debug, Levels)]
enum Urgency { Low, Medium, High }

let triage = questions! {
    angry: noul!("Is the customer angry?"),
    team: choice!("Which team should handle this ticket?", Team),
    urgency: score!("How urgent is this ticket?", Urgency),
}
.ask(&ticket)
.await?;

let (&team, team_probability) = triage.team.top(1).next().expect("a Choice has an option");
let urgent = triage.urgency.score() >= 1.5;
let angry = triage.angry.probability() > 0.8;

// act only when Jev gives the team it picked at least 0.9
let decision = if team_probability < 0.9 {
    "ask a person".to_string()
} else if urgent || angry {
    format!("page the {team:?} lead")
} else {
    format!("add to the {team:?} queue")
};
```

`top(1)` gives the option Jev picked, with its probability. A score's `score()` is the mean
level, from 0 to the index of the last level, so here it runs from 0 (low) to 2 (high), and
`most_likely()` is the level with the highest probability. Levels can be inline too, as text or
with your own values, as in `score!("How long is the fix?", [("A day.", 1), ("A week.", 5)])`.

To return the answers from a function, put them in a struct of your own. `#[derive(Answers)]`
takes a question for each field, and [`Questions::new()`][Questions] asks them all, knowing which
from the return type:

```rust,ignore
#[derive(Answers)]
struct Triage {
    #[system2(question = noul!("Is the customer angry?"))]
    angry: NoulAnswer,
    #[system2(question = choice!("Which team should handle this ticket?", Team))]
    team: ChoiceAnswer<Team>,
    #[system2(question = score!("How urgent is this ticket?", Urgency))]
    urgency: ScoreAnswer<Urgency>,
}

async fn triage(ticket: &Ticket) -> system2::Result<Triage> {
    Ok(Questions::new().ask(ticket).await?)
}
```

Five tickets, one request each:

| Ticket | Message | Decision |
| --- | --- | --- |
| 1 | We were charged twice this month. Please fix it today. | page the Billing lead |
| 2 | How do I change my avatar? | add to the Support queue |
| 3 | The export page has shown a 500 error since this morning. Our whole team is blocked. | page the Engineering lead |
| 4 | hello?? | add to the Support queue |
| 5 | The invoice page crashes when I try to pay. | ask a person |

The full program is [`examples/triage`][triage], with the recording behind these numbers. Ticket 5
could be billing or engineering. Jev gave engineering 0.80 and billing 0.20, the code wants 0.90
before it acts, so a person gets it. Each number in that code is yours: the answers keep their
probabilities, so every threshold stays in your code where you can see it.

One answer can also pick the next question:

```rust,ignore
if let Team::Billing = team {
    let refund = noul!("Does the customer ask for a refund?").ask(&ticket).await?;
    // ...
}
```

## Testing

Record the answers once, then replay them with no API key and no network:

```sh
SYSTEM2_CACHE=triage.jsonl cargo run                     # asks Jev, and records
SYSTEM2_CACHE=triage.jsonl SYSTEM2_OFFLINE=1 cargo run   # replays, sends nothing
```

In `examples/triage`, change `TEAM_THRESHOLD` from 0.9 to 0.8 and replay: ticket 5 pages the
engineering lead. Change it back, and it goes to a person. Trying both cost zero requests. Offline,
a request that changed since the recording (the question, the state, or the model) is an error, not
a quiet call to the API. A replayed answer counts no tokens and has no request ID. The file holds
each response and a hash of its request, not the request itself. Treat it like the rest of your test
data.

The same file makes a test repeatable. Write the test as usual:

```rust,ignore
#[tokio::test]
async fn a_double_charge_goes_to_billing() -> system2::Result<()> {
    let team = choice!("Which team should handle this ticket?", Team)
        .ask("I was charged twice.")
        .await?;
    assert!(matches!(team.most_likely(), Team::Billing));
    Ok(())
}
```

Record it once with your key, commit the file, and run it offline in CI:

```sh
SYSTEM2_CACHE=tests/jev.jsonl cargo test
SYSTEM2_CACHE=tests/jev.jsonl SYSTEM2_OFFLINE=1 cargo test
```

On a client of your own, `Client::from_env()?.cache("tests/jev.jsonl")?.offline(true)` does the same
in code. Two live runs of one request can give different probabilities: in the recordings behind
this page, the probability that ticket 1 is urgent was 0.82 in one run and 0.83 in the next. The
cache is what makes runs the same. A request matches only when its body matches byte for byte, so
use a `BTreeMap`, not a `HashMap`, in a state: a `HashMap` writes its keys in a different order each
run.

### Choosing thresholds

The right number is yours to find. TypeSafe trains Jev so that its probabilities are
[calibrated](https://docs.typesafe.ai/concepts/system-one): across many answers, the stated
probability should match how often such answers are right. It is no guarantee about any one
answer, and the numbers on this page are examples, not advice.

1. Pin the model with `.model("jev-1.14.0")`, so the answers do not move while you choose.
2. Record the answers for tickets you have already labelled.
3. Replay them at each candidate threshold, and count each kind of mistake. A replay costs no
   requests, so trying many thresholds is free.
4. Pick the threshold whose mistakes cost you least, and check it again on each new model
   version before you move the pin.

Once you have a number, pin down what your code does at it. A decision is a function of answers,
so test it like any other function. `answer` makes a real answer from the values you give it,
with no key, no network, and no runtime:

```rust,ignore
const REFUND: Noul = noul!("Does the customer ask for a refund?");

fn refund_now(refund: &NoulAnswer) -> bool {
    refund.probability() > 0.9
}

#[test]
fn a_refund_needs_more_than_0_9() {
    assert!(!refund_now(&REFUND.answer(0.90).unwrap()));
    assert!(refund_now(&REFUND.answer(0.91).unwrap()));
}
```

`answer` goes through the same checks as a response from Jev, so a test cannot build an answer that
Jev could never give: `REFUND.answer(1.2)` is an error. Each question type has an `answer`.

## Concurrency and retries

Ask about a whole queue at once. By default the client keeps at most 8 requests in flight, gives
each attempt 10 s, and retries a 408, a 429, a 5xx, a timeout, or a failed connection twice, with
backoff.

To change a setting (`model`, `max_concurrent_requests`, `timeout`, `max_retries`), build a
[`Client`][Client] of your own. [`set_global`][Client::set_global] makes it the client that every
`.ask` uses, so the rest of your code does not change. `client.ask(question, state)` uses one client
without making it global.

```rust,ignore
use tokio::task::JoinSet;

let client = Client::from_env()?.max_concurrent_requests(2).set_global();

let mut asking = JoinSet::new();
for ticket in &tickets { // tickets: Vec<Ticket>
    asking.spawn(noul!("Is the ticket urgent?").tag(ticket.id).ask(ticket));
}
for answered in asking.join_all().await {
    let (urgent, id) = answered?;
    println!("ticket {id}: urgent {:.2}", urgent.probability());
}

let usage = client.usage();
println!("{} tokens in, {} out", usage.input_tokens, usage.output_tokens);
```

```text
ticket 2: urgent 0.08
ticket 4: urgent 0.20
ticket 3: urgent 0.90
ticket 5: urgent 0.71
ticket 1: urgent 0.83
1507 tokens in, 105 out
```

The answers arrive in the order they finish, so `.tag` carries each ticket's ID back next to its
answer. The tag is not sent to Jev. `.ask` serializes the state before it returns, so a spawned
task does not borrow the ticket.

[`Client::from_env`][Client::from_env] reads these variables:

| Variable | Value |
| --- | --- |
| `TYPESAFE_API_KEY` | The API key. |
| `TYPESAFE_BASE_URL` | The base URL. The default is `https://api.typesafe.ai`. |
| `TYPESAFE_DEFAULT_MODEL` | The model. The default is `jev-latest`. |
| `SYSTEM2_CACHE` | The path of a cache file. |
| `SYSTEM2_OFFLINE` | `1` answers only from the cache. |

## OpenRouter

[OpenRouter](https://openrouter.ai/) also serves Jev, with the same API. Point the client at it
with the two variables from OpenRouter's guide, and the rest of your code stays the same:

```sh
export TYPESAFE_BASE_URL=https://openrouter.ai/api
export TYPESAFE_API_KEY=sk-or-...   # your OpenRouter key
```

Or set both in code, with the key from OpenRouter's own variable:

```rust,ignore
let key = std::env::var("OPENROUTER_API_KEY").expect("set OPENROUTER_API_KEY");
let client = Client::new()
    .base_url("https://openrouter.ai/api")
    .api_key(key)
    .set_global();
```

A few things differ on OpenRouter:

- A pinned model has no patch number: `jev-1.14` on OpenRouter, and `jev-1.14.0` on TypeSafe.
  `jev-latest` works on both.
- Long context is always on, so `long_context(true)` changes nothing.
- The request ID in an error or a receipt is OpenRouter's generation ID, and the span's
  `gen_ai.provider.name` is `openrouter`.

## Tracing

Each request is a `tracing` span, so it goes to the logs and dashboards you already have. Its fields
have the model that answered, the tokens, the latency, and the request ID, and the `gen_ai.*` names
follow OpenTelemetry's conventions for generative AI. Each attempt is a `DEBUG` event in the span,
or a `WARN` event with the error and the wait when a retry follows. The span's parent is the span
that is current when you call `.ask`. The five requests of the queue took 73 to 184 ms.

The crate installs no subscriber. This output is from
`tracing_subscriber::fmt().with_span_events(FmtSpan::CLOSE)`, which prints each span when it
closes (some fields left out here):

```text
INFO ask{gen_ai.request.model="jev-latest" gen_ai.response.model="jev-1.14.0"
         gen_ai.usage.input_tokens=293 gen_ai.usage.output_tokens=21
         system2.cached=false system2.latency_ms=184
         system2.request_id="req_01a1ce54e294763290b45e66f1f22bf1"}
```

To read the same facts in code, call [`.ask_with_receipt`][Ask::ask_with_receipt] in place of
`.ask`. It returns the answers and a [`Receipt`][Receipt], with the model that answered, the
tokens, the latency, whether the answer came from the cache, and the request ID:

```rust,ignore
let (urgent, receipt) = noul!("Is the ticket urgent?").ask_with_receipt(&ticket).await?;
println!("{} tokens, {:?}", receipt.usage.input_tokens, receipt.latency);
```

Each answer is `Serialize`, with its probabilities, so you can log it or keep it as a label. An
option serializes as its name, so your values need no `Serialize`. `trace_bodies(true)` on a
client also writes the body of each request in a `TRACE` event. A body holds the state, which is
often private, so it is off by default.

In the span, the request asked for `jev-latest`, and `jev-1.14.0` answered. `jev-latest` is the
default, and TypeSafe moves it to each new version. `.model("jev-1.14.0")` on the client pins
one, so the model changes only when your code does.

## Errors

Each error that a call returns converts into one [`system2::Error`][Error], so `?` works anywhere
that returns `system2::Result`. Here one `?` takes an `InvalidChoice` and the next an `AskError`:

```rust
use system2::prelude::*;
use system2::{AskError, Choice, Error};

async fn route(ticket: &str, teams: Vec<String>) -> system2::Result<String> {
    let question = Choice::new("Which team should handle this ticket?", teams)?;
    let team = question.ask(ticket).await?;
    Ok(team.most_likely().clone())
}
```

To handle one failure, match through it:

```rust,ignore
match route(ticket, teams).await {
    Ok(team) => println!("{team}"),
    Err(Error::Question(error)) => eprintln!("fix the team list: {error}"),
    Err(Error::Ask(AskError::Offline)) => eprintln!("no recorded answer for this ticket"),
    Err(error) => eprintln!("{error}"),
}
```

`Error::Question` holds a [`QuestionError`][QuestionError], from whichever constructor refused
the question. Each message says what failed, and the fix when there is one. `AskError::Http` and
`AskError::InvalidResponse` carry the request ID, when the API sent one, for TypeSafe support.

## Blocking API

[`system2::blocking`][blocking] asks with no `async` and needs no Tokio in your crate. Import
`system2::blocking::prelude::*` in place of `system2::prelude::*`, and drop each `.await`:

```rust,no_run
use system2::blocking::prelude::*;

fn main() -> system2::Result<()> {
    let urgent = noul!("Is the ticket urgent?").ask("I cannot log in.")?;
    println!("urgent: {:.2}", urgent.probability());
    Ok(())
}
```

The questions, sets, and answers are the same in both modes, and both use the same global
client. `blocking::Client::from(client)` wraps a `Client` with your settings. A blocking `ask`
inside an async runtime panics, so pick one mode for each program.

## Limits

TypeSafe's API sets most of these, and the crate checks each one as early as it can.

| Limit | Value | Checked by |
| --- | --- | --- |
| Options | 1 to 255 for a Choice, each with a different, nonempty name. | The compiler, or a constructor |
| Levels | 1 to 10 for a Score, each different. | The compiler, or a constructor |
| Tuples | 1 to 12 items in a tuple, each a question or a set. Arrays and `Vec`s have no limit. | The compiler |
| Tokens | About 32k for the state and the longest question, and 64k for the request. `long_context(true)` raises each to about 262k. | `AskError::TokenLimit` |
| Images | PNG or JPEG, at most 16 MiB, each side 280 to 1568 px. At most 32 MiB of images in a request. | `InvalidImage`, or `AskError::ByteLimit` |
| Bbox | Jev 1.14 or a later model. TypeSafe can change images and Bbox in a new release of Jev. | `AskError::BboxModel` |
| Precision | The API rounds each probability and each edge to 0.01. | |

## Crate features

- `image`: `Image::new` and `image!` also take WebP and GIF, turn each image upright, and scale
  it to fit the limits. `Image::crop` cuts a region from the original, and `Image` converts to
  and from `image::DynamicImage`. Without it, an image must be a PNG or JPEG that already fits.

The minimum supported Rust version is 1.88. TLS is rustls only, and the crate has no `unsafe`
code.

## The main types

- Questions: [`Noul`][Noul], [`Choice`][Choice], [`Score`][Score], and [`Bbox`][Bbox]. Their
  answers are [`NoulAnswer`][NoulAnswer], [`ChoiceAnswer`][ChoiceAnswer],
  [`ScoreAnswer`][ScoreAnswer], and [`BboxAnswer`][BboxAnswer].
- Sets: tuples, arrays, `Vec`, `Option`, [`questions!`][questions!], and
  [`Questions`][Questions] for a struct that derives [`Answers`][Answers].
- [`Client`][Client] holds the key, the model, the cache, and the other settings of a request.
- [`Image`][Image] is an image for a state, a question, or an option.
- [`Receipt`][Receipt] has the model, the tokens, and the latency of one request.
- [`Error`][Error] covers every error, and [`AskError`][AskError] tells why a request failed.

## Examples

Each example is a crate of its own in [`examples/`][examples]. Run one with `cargo run -p <name>`,
or replay its recording with no key, as CI does:

```sh
SYSTEM2_CACHE=examples/recordings/triage.jsonl SYSTEM2_OFFLINE=1 cargo run -p triage
```

- [`quick_start`][quick_start]: asks one yes-or-no question about a short function, and reads the
  probability of yes.
- [`triage`][triage]: the triage on this page, five tickets with three questions each.
- [`find_in_image`][find_in_image]: finds an object in a photo, and writes a crop of it. It needs
  `--features image`.
- [`citation_check`][citation_check]: a port of a [TypeSafe
  cookbook](https://docs.typesafe.ai/cookbooks). It checks whether its source supports each
  citation, and sends the uncertain ones to a person.

## License

Licensed under either of
[Apache License, Version 2.0](https://github.com/jhartquist/system2/blob/main/LICENSE-APACHE) or
[MIT license](https://github.com/jhartquist/system2/blob/main/LICENSE-MIT) at your option.

Issues are welcome, but pull requests are not accepted for now.

[Noul]: https://docs.rs/system2/latest/system2/struct.Noul.html
[NoulAnswer]: https://docs.rs/system2/latest/system2/struct.NoulAnswer.html
[Ask::ask]: https://docs.rs/system2/latest/system2/trait.Ask.html#method.ask
[Ask::ask_with_receipt]: https://docs.rs/system2/latest/system2/trait.Ask.html#method.ask_with_receipt
[Choice]: https://docs.rs/system2/latest/system2/struct.Choice.html
[Choice::with_values]: https://docs.rs/system2/latest/system2/struct.Choice.html#method.with_values
[ChoiceAnswer]: https://docs.rs/system2/latest/system2/struct.ChoiceAnswer.html
[Score]: https://docs.rs/system2/latest/system2/struct.Score.html
[ScoreAnswer]: https://docs.rs/system2/latest/system2/struct.ScoreAnswer.html
[Bbox]: https://docs.rs/system2/latest/system2/struct.Bbox.html
[BboxAnswer]: https://docs.rs/system2/latest/system2/struct.BboxAnswer.html
[Image]: https://docs.rs/system2/latest/system2/struct.Image.html
[Noul::new]: https://docs.rs/system2/latest/system2/struct.Noul.html#method.new
[Noul::with_criteria]: https://docs.rs/system2/latest/system2/struct.Noul.html#method.with_criteria
[noul!]: https://docs.rs/system2/latest/system2/macro.noul.html
[questions!]: https://docs.rs/system2/latest/system2/macro.questions.html
[Questions]: https://docs.rs/system2/latest/system2/struct.Questions.html
[Answers]: https://docs.rs/system2/latest/system2/derive.Answers.html
[Client]: https://docs.rs/system2/latest/system2/struct.Client.html
[Client::from_env]: https://docs.rs/system2/latest/system2/struct.Client.html#method.from_env
[Client::set_global]: https://docs.rs/system2/latest/system2/struct.Client.html#method.set_global
[Receipt]: https://docs.rs/system2/latest/system2/struct.Receipt.html
[Error]: https://docs.rs/system2/latest/system2/enum.Error.html
[AskError]: https://docs.rs/system2/latest/system2/enum.AskError.html
[QuestionError]: https://docs.rs/system2/latest/system2/enum.QuestionError.html
[blocking]: https://docs.rs/system2/latest/system2/blocking/index.html
[examples]: https://github.com/jhartquist/system2/tree/main/examples
[quick_start]: https://github.com/jhartquist/system2/tree/main/examples/quick_start
[triage]: https://github.com/jhartquist/system2/tree/main/examples/triage
[find_in_image]: https://github.com/jhartquist/system2/tree/main/examples/find_in_image
[citation_check]: https://github.com/jhartquist/system2/tree/main/examples/citation_check