Skip to main content

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}