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(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
101        let merged = self.merged_config(config);
102        let result = self
103            .bound
104            .invoke_any(Box::new(input) as Box<dyn Any + Send>, Some(merged))
105            .await?;
106        result.downcast::<O>().map(|b| *b).map_err(|_| {
107            LcelError::TypeMismatch(format!(
108                "binding output downcast: expected {}",
109                std::any::type_name::<O>()
110            ))
111        })
112    }
113
114    async fn batch(
115        &self,
116        inputs: Vec<I>,
117        config: Option<RunnableConfig>,
118    ) -> Result<Vec<O>, LcelError> {
119        let merged = self.merged_config(config);
120        let boxed_inputs: Vec<Box<dyn Any + Send>> = inputs
121            .into_iter()
122            .map(|i| Box::new(i) as Box<dyn Any + Send>)
123            .collect();
124        let results = self.bound.batch_any(boxed_inputs, Some(merged)).await?;
125        results
126            .into_iter()
127            .map(|boxed| {
128                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
129                    LcelError::TypeMismatch(format!(
130                        "binding batch downcast: expected {}",
131                        std::any::type_name::<O>()
132                    ))
133                })
134            })
135            .collect()
136    }
137
138    async fn stream(
139        &self,
140        input: I,
141        config: Option<RunnableConfig>,
142    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
143        let merged = self.merged_config(config);
144        let stream = self
145            .bound
146            .stream_any(Box::new(input) as Box<dyn Any + Send>, Some(merged))
147            .await?;
148        let output_stream = stream.map(|result| {
149            result.and_then(|boxed| {
150                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
151                    LcelError::TypeMismatch(format!(
152                        "binding stream downcast: expected {}",
153                        std::any::type_name::<O>()
154                    ))
155                })
156            })
157        });
158        Ok(Box::pin(output_stream))
159    }
160
161    async fn transform(
162        &self,
163        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
164        config: Option<RunnableConfig>,
165    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
166        let merged = self.merged_config(config);
167        let any_input: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
168            Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
169
170        let output_stream = self.bound.transform_any(any_input, Some(merged)).await?;
171
172        let typed_output = output_stream.map(|result| {
173            result.and_then(|boxed| {
174                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
175                    LcelError::TypeMismatch(format!(
176                        "binding transform downcast: expected {}",
177                        std::any::type_name::<O>()
178                    ))
179                })
180            })
181        });
182        Ok(Box::pin(typed_output))
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use futures_util::StreamExt;
190
191    struct EchoRunnable;
192
193    #[async_trait]
194    impl Runnable<String, String> for EchoRunnable {
195        type Error = std::convert::Infallible;
196
197        async fn invoke(
198            &self,
199            input: String,
200            config: Option<RunnableConfig>,
201        ) -> Result<String, Self::Error> {
202            let tags = config.map(|c| c.tags.join(",")).unwrap_or_default();
203            if tags.is_empty() {
204                Ok(input)
205            } else {
206                Ok(format!("[{}] {}", tags, input))
207            }
208        }
209    }
210
211    #[tokio::test]
212    async fn binding_with_config() {
213        let binding = RunnableBinding::new(EchoRunnable)
214            .with_config(RunnableConfig::default().with_tag("prod"));
215
216        let result = binding.invoke("hello".to_string(), None).await.unwrap();
217        assert_eq!(result, "[prod] hello");
218    }
219
220    #[tokio::test]
221    async fn binding_with_kwargs() {
222        let binding =
223            RunnableBinding::new(EchoRunnable).bind("stop", Value::String("\n".to_string()));
224
225        // kwargs are stored in metadata
226        let result = binding.invoke("test".to_string(), None).await.unwrap();
227        assert_eq!(result, "test"); // EchoRunnable doesn't use metadata
228    }
229
230    #[tokio::test]
231    async fn binding_stream_works() {
232        let binding = RunnableBinding::new(EchoRunnable)
233            .with_config(RunnableConfig::default().with_tag("stream"));
234
235        let mut stream = binding.stream("hello".to_string(), None).await.unwrap();
236        let result = stream.next().await.unwrap().unwrap();
237        assert_eq!(result, "[stream] hello");
238    }
239}