Skip to main content

typesafe_jev/
lib.rs

1//! Typed client for [TypeSafe](https://typesafe.ai)'s System One API and its Jev model.
2//!
3//! Jev does not generate text. A request carries a `state` and a set of typed questions about it;
4//! the reply carries one calibrated answer per question. Every question in a request is evaluated
5//! against the same state in parallel, so a request with hundreds of questions takes about as
6//! long as one with a single question.
7//!
8//! ```no_run
9//! use typesafe_jev::{Choice, Client, Config, Noul, Questions, Score};
10//!
11//! # fn main() -> Result<(), typesafe_jev::Error> {
12//! let client = Client::from_env(Config::default())?; // reads TYPESAFE_API_KEY
13//!
14//! let ticket = "Hi, I've been trying to connect my Stripe account for 3 days and the \
15//!               integration keeps failing. I'm losing sales. Please help ASAP.";
16//! let questions = Questions::new()
17//!     .with("department", Choice::new("Which team should handle this?", [
18//!         ("billing", "Payment or subscription issues"),
19//!         ("technical", "Bugs or integration problems"),
20//!         ("sales", "Pricing or account questions"),
21//!     ]))
22//!     .with("frustration", Score::new("How frustrated the customer appears", [
23//!         "Calm, just stating facts",
24//!         "Frustrated but civil",
25//!         "Very angry, strong language",
26//!     ]))
27//!     .with("is_urgent", Noul::new("The message conveys urgency or time-sensitivity"));
28//!
29//! let response = client.ask(ticket, &questions)?;
30//!
31//! let department = response.choice("department").expect("asked as a choice");
32//! println!("{} (confidence {:.2})", department.choice, department.confidence); // technical (0.78)
33//! let frustration = response.score("frustration").expect("asked as a score");
34//! println!("level {:.1} of 2", frustration.score); // 1.0
35//! let urgent = response.noul("is_urgent").expect("asked as a noul");
36//! println!("urgent with p={:.2}", urgent.noul); // 1.00
37//! println!("{} input tokens, ~${:.4}", client.usage().input_tokens(), client.usage().cost_usd());
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! # Questions and answers
43//!
44//! The types follow TypeSafe's [API reference](https://docs.typesafe.ai/api) field for field, and
45//! serialize to exactly the documents it shows.
46//!
47//! | Question | Asks | Answer |
48//! | --- | --- | --- |
49//! | [`Noul`] | a yes/no question, with optional descriptions of a yes and a no | [`NoulAnswer`]: the probability of a yes |
50//! | [`Choice`] | for one option out of a set, each with an optional description | [`ChoiceAnswer`]: the option, the probability of each, a confidence |
51//! | [`Score`] | for a rating against ordered, described levels | [`ScoreAnswer`]: a position on the scale, the probability of each level, a confidence |
52//!
53//! [`Questions`] holds the questions of one request under ids you choose, in the order you add
54//! them, and a [`Response`] holds an [`Answer`] under each of those ids. The probabilities are
55//! calibrated: of the `noul` answers given as 0.9, about nine in ten are a yes.
56//!
57//! Instructions and descriptions are [`Content`]: text, or a JSON object or array when a question
58//! refers to data of its own. The `state` is any value that serializes to a JSON string, object
59//! or array: a `&str`, a `serde_json::Value`, or your own `#[derive(Serialize)]` type.
60//!
61//! A reply is read strictly: an answer without a field the reference requires is
62//! [`Error::Api`], not a default. Fields this version does not know are ignored, and the
63//! structs and enums are `#[non_exhaustive]`, so the API can grow without breaking callers.
64//!
65//! # Retries, concurrency and cost
66//!
67//! [`Client::ask`] retries connection failures and transient HTTP statuses with exponential
68//! backoff and jitter, honouring `Retry-After`. Rejected credentials come back at once as
69//! [`Error::Auth`]; a request too large for the model's context as [`Error::TokenLimit`], which
70//! the caller should split and retry; a body the API refuses as malformed (HTTP 422) as
71//! [`Error::InvalidRequest`]. A key or a [`Config`] that no request could be sent with is
72//! reported by [`Client::new`], as [`Error::Auth`] or [`Error::InvalidConfig`], not retried.
73//!
74//! Calls are blocking. The client is `Send + Sync` and shares one connection pool, so call it from
75//! as many threads as [`Config::pool_size`]. An [`AdaptiveLimiter`] caps the concurrency and halves
76//! it whenever the API throttles, growing it back as requests succeed.
77//!
78//! [`Usage`] counts requests, retries and tokens across threads; [`Usage::cost_usd`] prices the
79//! input tokens at Jev's list price, [`USD_PER_INPUT_MTOK`]. Each [`Response`] also carries the
80//! [`TokenUsage`] of its own request.
81//!
82//! # Testing without the network
83//!
84//! [`Client::with_transport`] takes any [`Transport`], including a closure from the request body
85//! to a [`Reply`]:
86//!
87//! ```
88//! use typesafe_jev::{Client, Config, Noul, Questions, Reply};
89//!
90//! let client = Client::with_transport(
91//!     |_body: &[u8]| Ok(Reply {
92//!         status: 200,
93//!         retry_after: None,
94//!         body: r#"{"model": "fake", "answers": {"q": {"type": "noul", "noul": 0.5}}}"#.into(),
95//!     }),
96//!     Config::default(),
97//! );
98//! let response = client.ask("any state", &Questions::new().with("q", Noul::new("Is the state empty?"))).unwrap();
99//! assert_eq!(response.noul("q").unwrap().noul, 0.5);
100//! ```
101
102#![warn(missing_docs, missing_debug_implementations)]
103
104mod answer;
105mod client;
106mod content;
107mod error;
108mod limiter;
109mod question;
110mod transport;
111mod usage;
112
113pub use answer::{Answer, ChoiceAnswer, NoulAnswer, Response, ScoreAnswer, TokenUsage};
114pub use client::{Client, Config};
115pub use content::Content;
116pub use error::Error;
117pub use limiter::AdaptiveLimiter;
118pub use question::{Choice, Noul, NoulCriteria, Question, Questions, Score};
119pub use transport::{Reply, Transport};
120pub use usage::Usage;
121
122/// TypeSafe's hosted System One endpoint.
123pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai/v1/systemone";
124/// The model name that follows TypeSafe's latest Jev release.
125pub const DEFAULT_MODEL: &str = "jev-latest";
126/// The environment variable [`Client::from_env`] reads.
127pub const API_KEY_VAR: &str = "TYPESAFE_API_KEY";
128/// Jev's list price per million input tokens, in dollars. Output tokens are free.
129pub const USD_PER_INPUT_MTOK: f64 = 0.042;
130
131/// The README's examples compile and run with the crate's own tests.
132#[cfg(doctest)]
133#[doc = include_str!("../README.md")]
134struct ReadmeDoctests;