Skip to main content

lc_core/runnables/
binding.rs

1// lc-core/src/runnables/binding.rs
2//! RunnableBinding - bind runtime parameters and config to a Runnable.
3//!
4//! `RunnableBinding` wraps a `Runnable` and attaches runtime kwargs
5//! and/or a `RunnableConfig`, allowing pre-configuration of steps
6//! in an LCEL pipeline.
7
8use super::any::{into_runnable_any, RunnableAny};
9use super::config::RunnableConfig;
10use super::error::LcelError;
11use super::runnable_trait::Runnable;
12use async_trait::async_trait;
13use futures_util::{Stream, StreamExt};
14use serde_json::Value;
15use std::any::Any;
16use std::collections::HashMap;
17use std::pin::Pin;
18
19/// A `Runnable` that binds runtime parameters and config to an inner runnable.
20///
21/// # Example
22///
23/// ```rust,ignore
24/// let chain = llm
25///     .pipe(parser)
26///     .bind("stop", json!("\n"))
27///     .with_config(RunnableConfig::new().with_tag("production"));
28/// ```
29pub struct RunnableBinding<I: Send + Sync + 'static, O: Send + Sync + 'static> {
30    bound: Box<dyn RunnableAny>,
31    kwargs: HashMap<String, Value>,
32    config: RunnableConfig,
33    _marker: std::marker::PhantomData<(I, O)>,
34}
35
36impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug for RunnableBinding<I, O> {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.debug_struct("RunnableBinding")
39            .field("kwargs", &self.kwargs)
40            .field("input", &std::any::type_name::<I>())
41            .field("output", &std::any::type_name::<O>())
42            .finish()
43    }
44}
45
46impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableBinding<I, O> {
47    /// Create a new binding wrapping the given runnable.
48    pub fn new<R>(runnable: R) -> Self
49    where
50        R: Runnable<I, O> + 'static,
51        R::Error: Into<LcelError>,
52    {
53        Self {
54            bound: into_runnable_any(runnable),
55            kwargs: HashMap::new(),
56            config: RunnableConfig::default(),
57            _marker: std::marker::PhantomData,
58        }
59    }
60
61    /// Bind a runtime keyword argument.
62    ///
63    /// The kwargs are merged into the `RunnableConfig` metadata
64    /// when the runnable is invoked.
65    pub fn bind(mut self, key: impl Into<String>, value: Value) -> Self {
66        self.kwargs.insert(key.into(), value);
67        self
68    }
69
70    /// Set the execution config.
71    ///
72    /// This config is merged with any config passed at invocation time.
73    pub fn with_config(mut self, config: RunnableConfig) -> Self {
74        self.config = config;
75        self
76    }
77
78    /// Merge bound kwargs and config into the invocation config.
79    fn merged_config(&self, invocation_config: Option<RunnableConfig>) -> RunnableConfig {
80        let mut base = self.config.clone();
81
82        // Add kwargs as metadata
83        for (key, value) in &self.kwargs {
84            base = base.with_metadata(key.clone(), value.clone());
85        }
86
87        // Merge with invocation config
88        if let Some(inv) = invocation_config {
89            base.merge(inv)
90        } else {
91            base
92        }
93    }
94}
95
96#[async_trait]
97impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O> for RunnableBinding<I, O> {
98    type Error = LcelError;
99
100    async fn invoke(
101        &self,
102        input: I,
103        config: Option<RunnableConfig>,
104    ) -> Result<O, LcelError> {
105        let merged = self.merged_config(config);
106        let result = self
107            .bound
108            .invoke_any(Box::new(input) as Box<dyn Any + Send>, Some(merged))
109            .await?;
110        result
111            .downcast::<O>()
112            .map(|b| *b)
113            .map_err(|_| LcelError::TypeMismatch(format!(
114                "binding output downcast: expected {}",
115                std::any::type_name::<O>()
116            )))
117    }
118
119    async fn batch(
120        &self,
121        inputs: Vec<I>,
122        config: Option<RunnableConfig>,
123    ) -> Result<Vec<O>, LcelError> {
124        let merged = self.merged_config(config);
125        let boxed_inputs: Vec<Box<dyn Any + Send>> =
126            inputs.into_iter().map(|i| Box::new(i) as Box<dyn Any + Send>).collect();
127        let results = self.bound.batch_any(boxed_inputs, Some(merged)).await?;
128        results
129            .into_iter()
130            .map(|boxed| {
131                boxed
132                    .downcast::<O>()
133                    .map(|b| *b)
134                    .map_err(|_| LcelError::TypeMismatch(format!(
135                        "binding batch downcast: expected {}",
136                        std::any::type_name::<O>()
137                    )))
138            })
139            .collect()
140    }
141
142    async fn stream(
143        &self,
144        input: I,
145        config: Option<RunnableConfig>,
146    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
147        let merged = self.merged_config(config);
148        let stream = self
149            .bound
150            .stream_any(Box::new(input) as Box<dyn Any + Send>, Some(merged))
151            .await?;
152        let output_stream = stream.map(|result| {
153            result.and_then(|boxed| {
154                boxed
155                    .downcast::<O>()
156                    .map(|b| *b)
157                    .map_err(|_| LcelError::TypeMismatch(format!(
158                        "binding stream downcast: expected {}",
159                        std::any::type_name::<O>()
160                    )))
161            })
162        });
163        Ok(Box::pin(output_stream))
164    }
165
166    async fn transform(
167        &self,
168        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
169        config: Option<RunnableConfig>,
170    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
171        let merged = self.merged_config(config);
172        let any_input: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
173            Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
174
175        let output_stream = self.bound.transform_any(any_input, Some(merged)).await?;
176
177        let typed_output = output_stream.map(|result| {
178            result.and_then(|boxed| {
179                boxed
180                    .downcast::<O>()
181                    .map(|b| *b)
182                    .map_err(|_| LcelError::TypeMismatch(format!(
183                        "binding transform downcast: expected {}",
184                        std::any::type_name::<O>()
185                    )))
186            })
187        });
188        Ok(Box::pin(typed_output))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use futures_util::StreamExt;
196
197    struct EchoRunnable;
198
199    #[async_trait]
200    impl Runnable<String, String> for EchoRunnable {
201        type Error = std::convert::Infallible;
202
203        async fn invoke(
204            &self,
205            input: String,
206            config: Option<RunnableConfig>,
207        ) -> Result<String, Self::Error> {
208            let tags = config
209                .map(|c| c.tags.join(","))
210                .unwrap_or_default();
211            if tags.is_empty() {
212                Ok(input)
213            } else {
214                Ok(format!("[{}] {}", tags, input))
215            }
216        }
217    }
218
219    #[tokio::test]
220    async fn binding_with_config() {
221        let binding = RunnableBinding::new(EchoRunnable)
222            .with_config(RunnableConfig::default().with_tag("prod"));
223
224        let result = binding.invoke("hello".to_string(), None).await.unwrap();
225        assert_eq!(result, "[prod] hello");
226    }
227
228    #[tokio::test]
229    async fn binding_with_kwargs() {
230        let binding = RunnableBinding::new(EchoRunnable)
231            .bind("stop", Value::String("\n".to_string()));
232
233        // kwargs are stored in metadata
234        let result = binding.invoke("test".to_string(), None).await.unwrap();
235        assert_eq!(result, "test"); // EchoRunnable doesn't use metadata
236    }
237
238    #[tokio::test]
239    async fn binding_stream_works() {
240        let binding = RunnableBinding::new(EchoRunnable)
241            .with_config(RunnableConfig::default().with_tag("stream"));
242
243        let mut stream = binding.stream("hello".to_string(), None).await.unwrap();
244        let result = stream.next().await.unwrap().unwrap();
245        assert_eq!(result, "[stream] hello");
246    }
247}