lc_core/runnables/
binding.rs1use 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
19pub 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 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 pub fn bind(mut self, key: impl Into<String>, value: Value) -> Self {
66 self.kwargs.insert(key.into(), value);
67 self
68 }
69
70 pub fn with_config(mut self, config: RunnableConfig) -> Self {
74 self.config = config;
75 self
76 }
77
78 fn merged_config(&self, invocation_config: Option<RunnableConfig>) -> RunnableConfig {
80 let mut base = self.config.clone();
81
82 for (key, value) in &self.kwargs {
84 base = base.with_metadata(key.clone(), value.clone());
85 }
86
87 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 let result = binding.invoke("test".to_string(), None).await.unwrap();
227 assert_eq!(result, "test"); }
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}