Skip to content

Rust SDK

The Rust SDK gives your Rust code the ten functions as methods on one engine; each call gives back a plain Rust value, such as true or false from decide, or an Err when the answer is not sure or ThinkThen can't judge the text.

src/main.rs
use thinkthen::{Decide, Engine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let refund = Decide::new("Does the customer request a refund?")?;
let is_refund: bool = engine
.run(&refund, "Please refund my order.")?
.into_answer()?;
println!("{is_refund}");
Ok(())
}

Output

true

true means yes: the message asks for a refund.

Add the crate (Rust 1.95.0 or later):

Terminal window
cargo add thinkthen@0.2.0 --no-default-features

The engine is inside the crate, so cargo run is all it needs. Engine::from_env() uses the key from the Quickstart.

Set a low bar and a high bar with Threshold::band. Between them, decide is not sure, and into_answer() returns Err(NoAnswer::Unsure) with the probability. When ThinkThen can't judge the message, it returns Err(NoAnswer::Failed) with the error instead. Neither one can pass for false.

src/main.rs
use thinkthen::{Decide, Engine, NoAnswer, Threshold};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let refund = Decide::new("Does the customer request a refund?")?
.threshold(Threshold::band(0.2, 0.8)?);
let message = "I was charged twice for one invoice.";
match engine.run(&refund, message)?.into_answer() {
Ok(is_refund) => println!("{is_refund}"),
Err(NoAnswer::Unsure { probability }) => println!(
"Not sure ({}): ask a person.",
probability.get()
),
Err(NoAnswer::Failed(error)) => {
println!("{}: {error}", error.code())
}
}
Ok(())
}

Output

Not sure (0.7): ask a person.

A double charge might be a refund request, or might not, so the answer is not sure. Send it to a person.

probability.get() is the chance of yes. error.code() names the error, such as record.missing; Troubleshooting lists each one. With ?, not sure and a failure both stop the function. answer() borrows the answer instead of taking it.

run itself returns Err only when the call can't go ahead, such as blank text (record.empty) or a cancelled call.

Each function reads the same support-inbox messages as the function pages.

Yes or no about one message
src/main.rs
use thinkthen::{Decide, Engine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let refund = Decide::new("Does the customer request a refund?")?;
// into_answer() fails on not sure or a failed record, so neither
// reads as false.
let is_refund: bool = engine
.run(&refund, "Please refund my order.")?
.into_answer()?;
println!("{is_refund}");
Ok(())
}

Output

true

Yes: the message asks for a refund. Decide takes .threshold(...) (one bar with Threshold::cut, or a low and a high bar with Threshold::band), .yes_means(...) and .no_means(...). decide shows every option.

The one choice that fits
src/main.rs
use thinkthen::{Choose, Engine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let about = Choose::new(
"What is the message about?",
["billing", "shipping", "other"],
)?;
let topic: String = engine
.run(&about, "Please refund my order.")?
.into_answer()?;
println!("{topic}");
Ok(())
}

Output

billing

A refund is about billing. Choose takes .descriptions(...), each choice paired with what it means, and .threshold(...), which can make choose not sure. choose shows every option.

Only the messages that answer yes
src/main.rs
use thinkthen::{Engine, Filter};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let messages = vec![
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
];
let refund = Filter::new("Does the customer request a refund?")?;
// Only the messages that answer yes.
// A failed message arrives as an Err in its place.
for refund_request in engine.filter(&refund, messages, |message| *message)?
{
println!("{}", refund_request?);
}
Ok(())
}

Output

Please refund my order.
I was charged twice for one invoice.

The parcel question is left out. engine.filter gives back your own items, and leaves out the ones it is not sure about. The closure picks the text to judge from each item. A message that fails arrives as an Err in its place, holding the item, and reading goes on. engine.filter_items gives every item with its outcome instead. filter shows every option.

Every label that fits
src/main.rs
use thinkthen::{Engine, Tag};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let appear = Tag::new("Which topics appear?", ["billing", "shipping"])?;
let topics: Vec<String> = engine
.run(&appear, "I was charged twice and my parcel is late again.")?
.into_answer()?;
println!("{topics:?}");
Ok(())
}

Output

["billing", "shipping"]

The message is about both billing and shipping. The answer is a Vec<String>, empty when no label fits. tag shows every option.

A number from 0 to 1 on your scale
src/main.rs
use thinkthen::{Engine, Score};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let how_urgent =
Score::new("How urgent is the request?", ["routine", "urgent"])?;
// 0 is routine, 1 is urgent.
let urgency: f64 = engine
.run(&how_urgent, "Please refund my order.")?
.into_answer()?;
println!("{urgency}");
Ok(())
}

Output

0.05

The levels run lowest first, so a plain refund request scores near routine. score shows every option.

Every message, highest first
src/main.rs
use thinkthen::{Engine, Rank};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let messages = vec![
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
];
let by_urgency =
Rank::new("How urgent is the request?", ["routine", "urgent"])?;
// Rank 1 is the most urgent.
for ranked in engine.rows(&by_urgency, messages)? {
let ranked = ranked?;
let rank = ranked.answer()?;
let (_, message) = ranked
.record()
.expect("a rank row keeps its record")
.original();
println!("{rank} {}", String::from_utf8_lossy(message));
}
Ok(())
}

Output

1 I was charged twice for one invoice.
2 Where is my parcel?
3 Please refund my order.

The double charge is the most urgent. Each answer is a NonZeroUsize rank, starting at 1, and record() holds the message it belongs to. Rank takes .top(...) to keep only the first few. rank shows every option.

The line that answers the question
src/main.rs
use thinkthen::{Engine, Find};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let message = "Morgan works for Example Group.\nPlease refund my order.";
let requested = Find::new("Which line states the requested action?")?;
// None when no line answers the question.
match engine.run(&requested, message)?.into_answer()? {
Some(action) => println!("{}", action.text()),
None => println!("no line answers"),
}
Ok(())
}

Output

Please refund my order.

The second line states the action. The answer is Some(Found), with text(), start() and end(), or None when no line answers. Offsets count UTF-8 bytes, as Rust strings do, so &message[action.start()..action.end()] is the same text. find shows every option.

Each name in the text and its kind
src/main.rs
use thinkthen::{Engine, Recognize};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let people_and_groups = Recognize::new(["person", "organization"])?;
let names = engine
.run(&people_and_groups, "Morgan works for Example Group.")?
.into_answer()?;
for name in &names {
println!("{}: {}", name.kind(), name.text());
}
Ok(())
}

Output

person: Morgan
organization: Example Group

Each Name has text(), kind(), start(), end() and probability(). recognize shows every option.

Names linked by the relations you allow
src/main.rs
use thinkthen::{Engine, Relate, RelationRule};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let message = "Morgan works for Example Group.\nPlease refund my order.";
let works_for = Relate::new([RelationRule::new(
"works_for",
"person",
"organization",
)?])?;
let relations = engine.run(&works_for, message)?.into_answer()?;
for relation in &relations {
println!(
"{} --{}--> {}",
relation.source().text(),
relation.relation(),
relation.target().text()
);
}
Ok(())
}

Output

Morgan --works_for--> Example Group

RelationRule::new takes the relation, then the kind of name at each end. .reads(...) words the relation, and .either(true) makes it work in both directions. relate shows every option.

Several questions for each message
src/main.rs
use thinkthen::{AnnotatableQuestion, Annotate, Choose, Decide, Engine, Score};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let engine = Engine::from_env()?;
let messages = vec![
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
];
let triage = Annotate::new([
(
"refund",
AnnotatableQuestion::from(Decide::new(
"Does the customer request a refund?",
)?),
),
(
"topic",
Choose::new(
"What is the message about?",
["billing", "shipping", "other"],
)?
.into(),
),
(
"urgency",
Score::new("How urgent is the request?", ["routine", "urgent"])?
.into(),
),
])?;
for annotated in engine.rows(&triage, messages)? {
// Each answer prints bare, or as unsure or failed with its code.
let answers = annotated?.into_answer()?;
println!(
"{} {} {}",
answers["refund"], answers["topic"], answers["urgency"]
);
}
Ok(())
}

Output

true billing 0.05
false shipping 0.14
true billing 0.45

Each answer is read by name. It prints as its answer, or as unsure or failed: with the error code; answers.answer::<Decide>("refund") takes it as a Result, as decide does. annotate shows every option.

engine.rows(&question, messages) takes any owned iterator and gives one result per message, in order. With the stream feature, rows.into_stream() is a Stream for async code; the crate brings no async runtime. Each question takes .field("/text".parse()?) to judge one field of a JSON message. Dropping the results, or calling cancel(), stops the call. The Rust API reference lists every type and method.