pub struct Client { /* private fields */ }Expand description
A connection to the System One API. One shared pool; call ask from as many
threads as Config::pool_size allows.
Implementations§
Source§impl Client
impl Client
Sourcepub fn new(api_key: &str, cfg: Config) -> Result<Self, Error>
pub fn new(api_key: &str, cfg: Config) -> Result<Self, Error>
A client that sends api_key as a bearer token to Config::base_url over HTTPS.
Whitespace around the key, such as the newline at the end of a key file, is dropped.
Nothing is sent until the first ask: a key the API rejects is reported
then, as Error::Auth.
§Errors
What no request could be sent with is reported here rather than retried later:
Error::Auth for a blank key or one that cannot go in a header, and
Error::InvalidConfig for a Config::base_url that is not an absolute http or
https URL or a Config::user_agent that is not a valid header value.
Sourcepub fn from_env(cfg: Config) -> Result<Self, Error>
pub fn from_env(cfg: Config) -> Result<Self, Error>
A client that reads its API key from the API_KEY_VAR environment variable.
§Errors
Error::Auth if the variable is unset or blank; otherwise as Client::new.
Examples found in repository?
20fn main() -> Result<(), Error> {
21 let client = Client::from_env(Config::default())?;
22
23 let ticket = Ticket {
24 customer_plan: "business",
25 message: "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. \
26 I'm losing sales. Please help ASAP.",
27 };
28 let questions = Questions::new()
29 .with(
30 "department",
31 Choice::new(
32 "Which team should handle the `message`?",
33 [
34 ("billing", "Payment or subscription issues"),
35 ("technical", "Bugs or integration problems"),
36 ("sales", "Pricing or account questions"),
37 ],
38 ),
39 )
40 .with(
41 "frustration",
42 Score::new(
43 "How frustrated the customer appears",
44 ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"],
45 ),
46 )
47 .with("is_urgent", Noul::new("The message conveys urgency or time-sensitivity"));
48
49 let response = match client.ask(&ticket, &questions) {
50 Ok(response) => response,
51 // Too much state for one request: a real caller would split it and ask again.
52 Err(Error::TokenLimit(message)) => {
53 eprintln!("request too large: {message}");
54 return Ok(());
55 }
56 Err(other) => return Err(other),
57 };
58
59 if let Some(department) = response.choice("department") {
60 println!("department: {} (confidence {:.2})", department.choice, department.confidence);
61 for (option, probability) in &department.probabilities {
62 println!(" {option}: {probability:.2}");
63 }
64 }
65 if let Some(frustration) = response.score("frustration") {
66 println!("frustration: {:.2} of {} (confidence {:.2})", frustration.score, frustration.legend.len() - 1, frustration.confidence);
67 }
68 if let Some(urgent) = response.noul("is_urgent") {
69 println!("urgent: p={:.2}", urgent.noul);
70 }
71
72 let usage = client.usage();
73 println!(
74 "{}: {} request(s), {} retries, {} input tokens, ~${:.6}",
75 response.model,
76 usage.requests(),
77 usage.retries(),
78 usage.input_tokens(),
79 usage.cost_usd()
80 );
81 Ok(())
82}Sourcepub fn with_transport(transport: impl Transport + 'static, cfg: Config) -> Self
pub fn with_transport(transport: impl Transport + 'static, cfg: Config) -> Self
A client over any Transport, for tests and local fakes. Config::base_url,
Config::user_agent and the timeouts are the transport’s business and are ignored.
Sourcepub fn set_debug_reporter(
&mut self,
reporter: impl Fn(&str) + Send + Sync + 'static,
)
pub fn set_debug_reporter( &mut self, reporter: impl Fn(&str) + Send + Sync + 'static, )
Receives one line per failed attempt (the status or connection error, and any
Retry-After) before the client retries. Nothing is reported without a reporter, and
the reporter changes neither the retry policy nor the request.
Sourcepub fn usage(&self) -> &Usage
pub fn usage(&self) -> &Usage
Requests, retries and tokens so far.
Examples found in repository?
20fn main() -> Result<(), Error> {
21 let client = Client::from_env(Config::default())?;
22
23 let ticket = Ticket {
24 customer_plan: "business",
25 message: "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. \
26 I'm losing sales. Please help ASAP.",
27 };
28 let questions = Questions::new()
29 .with(
30 "department",
31 Choice::new(
32 "Which team should handle the `message`?",
33 [
34 ("billing", "Payment or subscription issues"),
35 ("technical", "Bugs or integration problems"),
36 ("sales", "Pricing or account questions"),
37 ],
38 ),
39 )
40 .with(
41 "frustration",
42 Score::new(
43 "How frustrated the customer appears",
44 ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"],
45 ),
46 )
47 .with("is_urgent", Noul::new("The message conveys urgency or time-sensitivity"));
48
49 let response = match client.ask(&ticket, &questions) {
50 Ok(response) => response,
51 // Too much state for one request: a real caller would split it and ask again.
52 Err(Error::TokenLimit(message)) => {
53 eprintln!("request too large: {message}");
54 return Ok(());
55 }
56 Err(other) => return Err(other),
57 };
58
59 if let Some(department) = response.choice("department") {
60 println!("department: {} (confidence {:.2})", department.choice, department.confidence);
61 for (option, probability) in &department.probabilities {
62 println!(" {option}: {probability:.2}");
63 }
64 }
65 if let Some(frustration) = response.score("frustration") {
66 println!("frustration: {:.2} of {} (confidence {:.2})", frustration.score, frustration.legend.len() - 1, frustration.confidence);
67 }
68 if let Some(urgent) = response.noul("is_urgent") {
69 println!("urgent: p={:.2}", urgent.noul);
70 }
71
72 let usage = client.usage();
73 println!(
74 "{}: {} request(s), {} retries, {} input tokens, ~${:.6}",
75 response.model,
76 usage.requests(),
77 usage.retries(),
78 usage.input_tokens(),
79 usage.cost_usd()
80 );
81 Ok(())
82}Sourcepub fn limiter(&self) -> &AdaptiveLimiter
pub fn limiter(&self) -> &AdaptiveLimiter
The concurrency gate, for inspection.
Sourcepub fn ask<S: Serialize + ?Sized>(
&self,
state: &S,
questions: &Questions,
) -> Result<Response, Error>
pub fn ask<S: Serialize + ?Sized>( &self, state: &S, questions: &Questions, ) -> Result<Response, Error>
Evaluates questions against state and returns the Response: one
Answer per question, under the id the question had.
state is what the questions are about, and anything that serializes to a JSON string,
object or array: a &str, a serde_json::Value, or a type of your own that derives
Serialize. Jev reads text only. A question can name a field of the state in backticks.
Transient failures are retried with exponential backoff up to Config::max_retries
times, honouring Retry-After (capped at 30 seconds) and the shared AdaptiveLimiter.
A Retry-After given as a date rather than as seconds is ignored in favour of the backoff.
The call blocks the calling thread; run it from a thread pool for concurrency.
§Errors
Error::Auth for HTTP 401 and 403, Error::TokenLimit for a request larger than the
model’s context, and Error::InvalidRequest for a state that does not serialize or a
body the API refuses as malformed (HTTP 422), all without a retry. Error::Api for any
other status that is not worth retrying, for a reply that is not the documented JSON, and
once the retries are spent; the message then ends with the last failure.
Examples found in repository?
20fn main() -> Result<(), Error> {
21 let client = Client::from_env(Config::default())?;
22
23 let ticket = Ticket {
24 customer_plan: "business",
25 message: "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. \
26 I'm losing sales. Please help ASAP.",
27 };
28 let questions = Questions::new()
29 .with(
30 "department",
31 Choice::new(
32 "Which team should handle the `message`?",
33 [
34 ("billing", "Payment or subscription issues"),
35 ("technical", "Bugs or integration problems"),
36 ("sales", "Pricing or account questions"),
37 ],
38 ),
39 )
40 .with(
41 "frustration",
42 Score::new(
43 "How frustrated the customer appears",
44 ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"],
45 ),
46 )
47 .with("is_urgent", Noul::new("The message conveys urgency or time-sensitivity"));
48
49 let response = match client.ask(&ticket, &questions) {
50 Ok(response) => response,
51 // Too much state for one request: a real caller would split it and ask again.
52 Err(Error::TokenLimit(message)) => {
53 eprintln!("request too large: {message}");
54 return Ok(());
55 }
56 Err(other) => return Err(other),
57 };
58
59 if let Some(department) = response.choice("department") {
60 println!("department: {} (confidence {:.2})", department.choice, department.confidence);
61 for (option, probability) in &department.probabilities {
62 println!(" {option}: {probability:.2}");
63 }
64 }
65 if let Some(frustration) = response.score("frustration") {
66 println!("frustration: {:.2} of {} (confidence {:.2})", frustration.score, frustration.legend.len() - 1, frustration.confidence);
67 }
68 if let Some(urgent) = response.noul("is_urgent") {
69 println!("urgent: p={:.2}", urgent.noul);
70 }
71
72 let usage = client.usage();
73 println!(
74 "{}: {} request(s), {} retries, {} input tokens, ~${:.6}",
75 response.model,
76 usage.requests(),
77 usage.retries(),
78 usage.input_tokens(),
79 usage.cost_usd()
80 );
81 Ok(())
82}