Skip to main content

semantic_operators/
provider.rs

1//! Async provider contracts and the checked request/response boundary.
2
3use crate::{Answer, Error, ErrorKind, IndexMap, Question, Result, State};
4use std::{future::Future, pin::Pin, sync::Arc};
5
6/// Questions in caller-supplied order, keyed by unique names.
7pub type Questions = IndexMap<String, Question>;
8/// Dynamic answers, returned in request order by [`ask`].
9pub type Answers = IndexMap<String, Answer>;
10/// Object-safe borrowed future. Work begins only when it is polled.
11pub type ProviderFuture<'a> = Pin<Box<dyn Future<Output = Result<Answers>> + Send + 'a>>;
12
13/// One state and its ordered questions, with optional request-owned assets.
14#[derive(Clone)]
15pub struct Request {
16    state: State,
17    questions: Questions,
18    #[cfg(feature = "images")]
19    assets: crate::image::Assets,
20}
21impl std::fmt::Debug for Request {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("Request")
24            .field("question_count", &self.questions.len())
25            .finish_non_exhaustive()
26    }
27}
28impl Request {
29    /// Construct a nonempty request, rejecting empty names before any provider call.
30    ///
31    /// # Errors
32    /// Returns `InvalidArgument` if questions are empty or any question name is
33    /// empty. Image references are checked later by [`Request::validate`], so
34    /// assets may be attached after construction.
35    pub fn new(state: State, questions: Questions) -> Result<Self> {
36        if questions.is_empty() || questions.keys().any(String::is_empty) {
37            return Err(Error::new(
38                ErrorKind::InvalidArgument,
39                "a request needs nonempty, uniquely named questions",
40            ));
41        }
42        Ok(Self {
43            state,
44            questions,
45            #[cfg(feature = "images")]
46            assets: Default::default(),
47        })
48    }
49    /// Input state. Debug formatting never prints this value.
50    pub fn state(&self) -> &State {
51        &self.state
52    }
53    /// Ordered questions.
54    pub fn questions(&self) -> &Questions {
55        &self.questions
56    }
57    /// Replace questions while preserving state and attached assets.
58    ///
59    /// # Errors
60    /// Returns `InvalidArgument` if the replacement map is empty or contains
61    /// an empty question name.
62    pub fn with_questions(&self, questions: Questions) -> Result<Self> {
63        let request = Self::new(self.state.clone(), questions)?;
64        #[cfg(feature = "images")]
65        let request = request.with_assets(self.assets.clone());
66        Ok(request)
67    }
68    /// Attach an explicit asset registry, validated before dispatch.
69    #[cfg(feature = "images")]
70    #[cfg_attr(docsrs, doc(cfg(feature = "images")))]
71    pub fn with_assets(mut self, assets: crate::image::Assets) -> Self {
72        self.assets = assets;
73        self
74    }
75    /// Borrow assets without opening files or resolving URLs.
76    #[cfg(feature = "images")]
77    #[cfg_attr(docsrs, doc(cfg(feature = "images")))]
78    pub fn assets(&self) -> &crate::image::Assets {
79        &self.assets
80    }
81    /// Validate assets before calling a provider.
82    ///
83    /// # Errors
84    /// With `images` enabled, returns `InvalidImage` for unknown image references
85    /// or excessive repeated image use. With that feature disabled, this check
86    /// always succeeds.
87    pub fn validate(&self) -> Result<()> {
88        #[cfg(feature = "images")]
89        self.assets.validate(&self.state, &self.questions)?;
90        Ok(())
91    }
92}
93
94/// A provider must be lazy, cancellation-safe, and free of implicit retries.
95/// Consumers should use [`ask`] to check response coverage and normalization.
96pub trait Provider: Send + Sync {
97    /// Answer the state and all its questions in one semantic invocation.
98    ///
99    /// # Errors
100    /// The returned future may fail with provider, timeout, unsupported-capability,
101    /// or input/response-validation errors. Implementations must preserve errors
102    /// as errors; uncertainty belongs in a successful answer.
103    fn ask<'a>(&'a self, request: &'a Request) -> ProviderFuture<'a>;
104}
105impl<P: Provider + ?Sized> Provider for Arc<P> {
106    fn ask<'a>(&'a self, request: &'a Request) -> ProviderFuture<'a> {
107        (**self).ask(request)
108    }
109}
110
111/// Call a provider and verify input assets, response names, and answer semantics.
112///
113/// # Errors
114/// Propagates request validation and provider errors. Returns `InvalidResponse`
115/// for missing/extra answers or candidates incompatible with their questions.
116pub async fn ask<P: Provider + ?Sized>(provider: &P, request: &Request) -> Result<Answers> {
117    request.validate()?;
118    validate_answers(request, provider.ask(request).await?)
119}
120/// Check response coverage and requested thresholds; return answers in request order.
121///
122/// # Errors
123/// Returns `InvalidResponse` if answer names do not exactly match the request
124/// or an answer fails normalization against its question.
125pub fn validate_answers(request: &Request, answers: Answers) -> Result<Answers> {
126    if answers.len() != request.questions.len()
127        || answers
128            .keys()
129            .any(|name| !request.questions.contains_key(name))
130    {
131        return Err(Error::new(
132            ErrorKind::InvalidResponse,
133            "provider must answer exactly the requested question names",
134        ));
135    }
136    request
137        .questions
138        .iter()
139        .map(|(name, question)| {
140            let answer = answers.get(name).ok_or_else(|| {
141                Error::new(
142                    ErrorKind::InvalidResponse,
143                    "provider omitted a requested answer",
144                )
145            })?;
146            Ok((name.clone(), answer.revalidate(question)?))
147        })
148        .collect()
149}
150
151/// An explicitly supplied async function implementing the provider contract.
152pub struct FunctionProvider<F> {
153    function: F,
154}
155impl<F> FunctionProvider<F>
156where
157    F: for<'a> Fn(&'a Request) -> ProviderFuture<'a> + Send + Sync,
158{
159    /// Wrap a local inference adapter or scripted function.
160    pub fn new(function: F) -> Self {
161        Self { function }
162    }
163}
164impl<F> Provider for FunctionProvider<F>
165where
166    F: for<'a> Fn(&'a Request) -> ProviderFuture<'a> + Send + Sync,
167{
168    fn ask<'a>(&'a self, request: &'a Request) -> ProviderFuture<'a> {
169        Box::pin(async move { (self.function)(request).await })
170    }
171}
172
173// Tokio has no public timer-enabled query. Its timer constructor panics when
174// a runtime exists without a timer driver. Catch only construction, before the
175// provider future is polled; caller configuration errors remain typed errors.
176#[cfg(feature = "tokio")]
177pub(crate) async fn deadline<F: Future>(
178    duration: std::time::Duration,
179    future: F,
180) -> Result<F::Output> {
181    if tokio::runtime::Handle::try_current().is_err() {
182        return Err(Error::new(
183            ErrorKind::Runtime,
184            "deadline requires a time-enabled Tokio runtime",
185        ));
186    }
187    let timer = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
188        tokio::time::timeout(duration, future)
189    }))
190    .map_err(|_| Error::new(ErrorKind::Runtime, "Tokio timer driver is not enabled"))?;
191    timer
192        .await
193        .map_err(|_| Error::new(ErrorKind::Timeout, "provider call exceeded its deadline"))
194}