Skip to main content

entelix_runnable/
parser.rs

1//! Output parsers — `Runnable`s that turn model output (`Message`) into
2//! strongly-typed Rust values.
3//!
4//! Surface:
5//! - [`JsonOutputParser<T>`] — direct serde-JSON deserializer.
6
7use std::marker::PhantomData;
8
9use entelix_core::ir::{ContentPart, Message};
10use entelix_core::{Error, ExecutionContext, Result};
11use serde::de::DeserializeOwned;
12
13use crate::runnable::Runnable;
14
15/// Parses an assistant `Message` as JSON into `T`.
16///
17/// The parser concatenates every [`ContentPart::Text`] in the message and
18/// runs `serde_json::from_str` on the result. Non-text parts are ignored;
19/// callers that need stricter handling can wrap this with a
20/// [`crate::RunnableLambda`] preprocessor.
21///
22/// ```ignore
23/// use entelix_runnable::JsonOutputParser;
24/// use serde::Deserialize;
25///
26/// #[derive(Deserialize)]
27/// struct Reply { answer: String }
28///
29/// let parser = JsonOutputParser::<Reply>::new();
30/// let chain = prompt.pipe(model).pipe(parser);
31/// ```
32pub struct JsonOutputParser<T> {
33    _phantom: PhantomData<fn() -> T>,
34}
35
36impl<T> JsonOutputParser<T> {
37    /// Build a fresh parser. Consumers usually let type inference at the
38    /// call site pin `T`.
39    pub const fn new() -> Self {
40        Self {
41            _phantom: PhantomData,
42        }
43    }
44}
45
46impl<T> Default for JsonOutputParser<T> {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl<T> Clone for JsonOutputParser<T> {
53    fn clone(&self) -> Self {
54        *self
55    }
56}
57
58impl<T> Copy for JsonOutputParser<T> {}
59
60#[async_trait::async_trait]
61impl<T> Runnable<Message, T> for JsonOutputParser<T>
62where
63    T: DeserializeOwned + Send + 'static,
64{
65    async fn invoke(&self, input: Message, _ctx: &ExecutionContext) -> Result<T> {
66        let mut text = String::new();
67        for part in &input.content {
68            if let ContentPart::Text { text: t, .. } = part {
69                text.push_str(t);
70            }
71        }
72        if text.is_empty() {
73            return Err(Error::invalid_request(
74                "JsonOutputParser: message contains no text parts",
75            ));
76        }
77        Ok(serde_json::from_str(&text)?)
78    }
79}