semantic_operators/operator.rs
1//! Named typed questions and heterogeneous one-call application.
2use crate::{
3 Answer, Answers, Error, ErrorKind, Provider, Question, Questions, Request, Result, State,
4 TypedQuestion, ask,
5};
6
7/// A named question erased to its dynamic variant.
8#[derive(Clone, Debug)]
9pub struct NamedQuestion {
10 name: String,
11 question: Question,
12}
13impl NamedQuestion {
14 /// Bind a nonempty name to a question.
15 ///
16 /// # Errors
17 /// Returns `InvalidArgument` if the operator name is empty.
18 pub fn new(name: impl Into<String>, question: impl Into<Question>) -> Result<Self> {
19 let name = name.into();
20 if name.is_empty() {
21 return Err(Error::new(
22 ErrorKind::InvalidArgument,
23 "operator name must not be empty",
24 ));
25 }
26 Ok(Self {
27 name,
28 question: question.into(),
29 })
30 }
31 /// Operator identity.
32 pub fn name(&self) -> &str {
33 &self.name
34 }
35 /// Underlying semantic definition.
36 pub fn question(&self) -> &Question {
37 &self.question
38 }
39}
40/// A reusable typed question, independent of any inference provider.
41///
42/// This explicitly scripted provider demonstrates typed output without network
43/// access or semantic inference.
44///
45/// ```
46/// use semantic_operators::{
47/// AnswerValue, Boolean, FunctionProvider, IndexMap, Operator,
48/// ProviderFuture, Request, State, make_answer,
49/// };
50///
51/// fn scripted(request: &Request) -> ProviderFuture<'_> {
52/// Box::pin(async move {
53/// request.questions().iter().map(|(name, question)| {
54/// let probabilities = IndexMap::from([
55/// ("true".into(), 0.1), ("false".into(), 0.9),
56/// ]);
57/// Ok((name.clone(), make_answer(
58/// question, AnswerValue::Boolean(false), probabilities, None,
59/// )?))
60/// }).collect()
61/// })
62/// }
63///
64/// # fn main() -> semantic_operators::Result<()> {
65/// let operator = Operator::new("urgent", Boolean::new("Is this urgent?")?)?;
66/// let provider = FunctionProvider::new(scripted);
67/// let answer = futures_executor::block_on(operator.ask(&provider, &State::text("Fixture")))?;
68/// assert_eq!(answer.value(), Some(&false)); // False is a decided answer.
69/// # Ok(())
70/// # }
71/// ```
72#[derive(Clone, Debug)]
73pub struct Operator<Q: TypedQuestion> {
74 name: String,
75 question: Q,
76}
77impl<Q: TypedQuestion> Operator<Q> {
78 /// Create an operator with a nonempty name.
79 ///
80 /// # Errors
81 /// Returns `InvalidArgument` if the operator name is empty.
82 pub fn new(name: impl Into<String>, question: Q) -> Result<Self> {
83 let name = name.into();
84 if name.is_empty() {
85 return Err(Error::new(
86 ErrorKind::InvalidArgument,
87 "operator name must not be empty",
88 ));
89 }
90 Ok(Self { name, question })
91 }
92 /// Stable name used in batches and traces.
93 pub fn name(&self) -> &str {
94 &self.name
95 }
96 /// Typed question definition.
97 pub fn question(&self) -> &Q {
98 &self.question
99 }
100 /// Erase the type for a heterogeneous batch.
101 pub fn erased(&self) -> NamedQuestion {
102 NamedQuestion {
103 name: self.name.clone(),
104 question: self.question.clone().into(),
105 }
106 }
107 /// Ask about a state and retain a typed answer, including uncertainty.
108 ///
109 /// # Errors
110 /// Propagates request, provider, response-validation, and typed-conversion
111 /// errors. An undecided answer is successful and retains its evidence.
112 pub async fn ask<P: Provider + ?Sized>(
113 &self,
114 provider: &P,
115 state: &State,
116 ) -> Result<Answer<Q::Output>> {
117 let request = Request::new(state.clone(), questions(&[self.erased()])?)?;
118 self.ask_request(provider, &request).await
119 }
120 /// Ask about a request's state and assets, replacing its questions.
121 ///
122 /// # Errors
123 /// Propagates request/image validation, provider, response-validation, and
124 /// typed-conversion errors. The supplied request's questions are replaced.
125 pub async fn ask_request<P: Provider + ?Sized>(
126 &self,
127 provider: &P,
128 request: &Request,
129 ) -> Result<Answer<Q::Output>> {
130 let request = request.with_questions(questions(&[self.erased()])?)?;
131 let answers = ask(provider, &request).await?;
132 self.answer(&answers)
133 }
134 /// Read this operator's checked typed result from a heterogeneous batch.
135 ///
136 /// # Errors
137 /// Returns `InvalidArgument` if this operator is absent, or a normalization
138 /// or conversion error if the retained answer does not match its question/type.
139 pub fn answer(&self, answers: &Answers) -> Result<Answer<Q::Output>> {
140 answers
141 .get(&self.name)
142 .ok_or_else(|| {
143 Error::new(
144 ErrorKind::InvalidArgument,
145 "operator is absent from the answer batch",
146 )
147 })?
148 .revalidate(&self.question.clone().into())?
149 .typed()
150 }
151}
152/// Build ordered questions, rejecting empty batches and duplicate names.
153///
154/// # Errors
155/// Returns `InvalidArgument` for an empty operator list or duplicate names.
156pub fn questions(operators: &[NamedQuestion]) -> Result<Questions> {
157 if operators.is_empty() {
158 return Err(Error::new(
159 ErrorKind::InvalidArgument,
160 "operator batch must not be empty",
161 ));
162 }
163 let mut result = Questions::new();
164 for operator in operators {
165 if result
166 .insert(operator.name.clone(), operator.question.clone())
167 .is_some()
168 {
169 return Err(Error::new(
170 ErrorKind::InvalidArgument,
171 "operator names must be unique",
172 ));
173 }
174 }
175 Ok(result)
176}
177/// Ask all operators in one request, preserving its state and assets.
178///
179/// # Errors
180/// Returns `InvalidArgument` for an empty batch or duplicate operator names.
181/// Otherwise propagates checked request/image, provider, and response errors.
182pub async fn apply<P: Provider + ?Sized>(
183 provider: &P,
184 request: &Request,
185 operators: &[NamedQuestion],
186) -> Result<Answers> {
187 ask(provider, &request.with_questions(questions(operators)?)?).await
188}