Skip to main content

lc_core/runnables/
configurable.rs

1// lc-core/src/runnables/configurable.rs
2//! Configurable runnables — runtime selection, the Rust counterpart of
3//! Python LCEL's `Runnable.configurable_alternatives(...)` and
4//! `Runnable.configurable_fields(...)`.
5//!
6//! Both read the `configurable` map of `RunnableConfig` (Python's
7//! `config["configurable"]`) at invoke time, so the same chain can route
8//! differently per call without rebuilding the pipeline.
9
10use super::any::{into_runnable_any, RunnableAny};
11use super::config::RunnableConfig;
12use super::error::LcelError;
13use super::runnable_trait::Runnable;
14use async_trait::async_trait;
15use futures_util::{Stream, StreamExt};
16use serde_json::Value;
17use std::any::Any;
18use std::marker::PhantomData;
19use std::pin::Pin;
20
21/// Routes between a default runnable and named alternatives at invoke time.
22///
23/// The selector key (`which`) is read from `config.configurable`; the value
24/// must be a string naming either the `default_key` (→ default) or one of
25/// the alternatives. An unknown value falls back to the default.
26///
27/// # Example
28///
29/// ```rust,ignore
30/// let chain = llm.configurable_alternatives(
31///     "provider", "default",
32///     vec![("anthropic", anthropic_llm), ("ollama", ollama_llm)],
33/// );
34/// // config = RunnableConfig::new().with_configurable("provider", json!("anthropic"))
35/// ```
36pub struct RunnableConfigurable<I: Send + Sync + 'static, O: Send + Sync + 'static> {
37    default: Box<dyn RunnableAny>,
38    alternatives: Vec<(String, Box<dyn RunnableAny>)>,
39    which: String,
40    default_key: String,
41    _marker: PhantomData<(I, O)>,
42}
43
44impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug
45    for RunnableConfigurable<I, O>
46{
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        let names: Vec<&str> = self.alternatives.iter().map(|(k, _)| k.as_str()).collect();
49        f.debug_struct("RunnableConfigurable")
50            .field("which", &self.which)
51            .field("default_key", &self.default_key)
52            .field("alternatives", &names)
53            .field("input", &std::any::type_name::<I>())
54            .field("output", &std::any::type_name::<O>())
55            .finish()
56    }
57}
58
59impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableConfigurable<I, O> {
60    /// Build a configurable router.
61    ///
62    /// * `default` — runnable used when the selector is absent, equals
63    ///   `default_key`, or names an unknown alternative.
64    /// * `which` — key read from `config.configurable`.
65    /// * `default_key` — config value that routes to `default`.
66    /// * `alternatives` — `(option name, runnable)` pairs; the config value
67    ///   must match one of these names to be selected.
68    pub fn new<R>(default: R, which: impl Into<String>, default_key: impl Into<String>) -> Self
69    where
70        R: Runnable<I, O> + 'static,
71        R::Error: Into<LcelError>,
72    {
73        Self {
74            default: into_runnable_any(default),
75            alternatives: Vec::new(),
76            which: which.into(),
77            default_key: default_key.into(),
78            _marker: PhantomData,
79        }
80    }
81
82    /// Add an alternative branch reachable via `config.configurable[which] == name`.
83    pub fn with_alternative<R>(mut self, name: impl Into<String>, runnable: R) -> Self
84    where
85        R: Runnable<I, O> + 'static,
86        R::Error: Into<LcelError>,
87    {
88        self.alternatives
89            .push((name.into(), into_runnable_any(runnable)));
90        self
91    }
92
93    /// Pick the target for this call based on the configurable selector.
94    fn resolve(&self, config: &Option<RunnableConfig>) -> &dyn RunnableAny {
95        let selected = config
96            .as_ref()
97            .and_then(|c| c.configurable_value(&self.which))
98            .and_then(|v| v.as_str());
99        match selected {
100            Some(name) if name != self.default_key => self
101                .alternatives
102                .iter()
103                .find(|(k, _)| k == name)
104                .map(|(_, r)| r.as_ref())
105                .unwrap_or(&*self.default),
106            _ => &*self.default,
107        }
108    }
109}
110
111#[async_trait]
112impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
113    for RunnableConfigurable<I, O>
114{
115    type Error = LcelError;
116
117    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
118        let target = self.resolve(&config);
119        let boxed = target
120            .invoke_any(Box::new(input) as Box<dyn Any + Send>, config)
121            .await?;
122        boxed.downcast::<O>().map(|b| *b).map_err(|_| {
123            LcelError::TypeMismatch(format!(
124                "configurable invoke downcast: expected {}",
125                std::any::type_name::<O>()
126            ))
127        })
128    }
129
130    async fn batch(
131        &self,
132        inputs: Vec<I>,
133        config: Option<RunnableConfig>,
134    ) -> Result<Vec<O>, LcelError> {
135        let target = self.resolve(&config);
136        let boxed_inputs: Vec<Box<dyn Any + Send>> = inputs
137            .into_iter()
138            .map(|i| Box::new(i) as Box<dyn Any + Send>)
139            .collect();
140        let results = target.batch_any(boxed_inputs, config).await?;
141        results
142            .into_iter()
143            .map(|boxed| {
144                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
145                    LcelError::TypeMismatch(format!(
146                        "configurable batch downcast: expected {}",
147                        std::any::type_name::<O>()
148                    ))
149                })
150            })
151            .collect()
152    }
153
154    async fn stream(
155        &self,
156        input: I,
157        config: Option<RunnableConfig>,
158    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
159        let target = self.resolve(&config);
160        let stream = target
161            .stream_any(Box::new(input) as Box<dyn Any + Send>, config)
162            .await?;
163        let output = stream.map(|result| {
164            result.and_then(|boxed| {
165                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
166                    LcelError::TypeMismatch(format!(
167                        "configurable stream downcast: expected {}",
168                        std::any::type_name::<O>()
169                    ))
170                })
171            })
172        });
173        Ok(Box::pin(output))
174    }
175
176    async fn transform(
177        &self,
178        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
179        config: Option<RunnableConfig>,
180    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send + '_>>, LcelError> {
181        let target = self.resolve(&config);
182        let any_input: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
183            Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
184
185        let stream = target.transform_any(any_input, config).await?;
186        let output = stream.map(|result| {
187            result.and_then(|boxed| {
188                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
189                    LcelError::TypeMismatch(format!(
190                        "configurable transform downcast: expected {}",
191                        std::any::type_name::<O>()
192                    ))
193                })
194            })
195        });
196        Ok(Box::pin(output))
197    }
198}
199
200/// Overrides recognized config fields at invoke time from
201/// `config.configurable` (Python's `Runnable.configurable_fields`).
202///
203/// The following configurable keys are applied before the inner runnable is
204/// invoked:
205///
206/// | configurable key        | effect |
207/// |-------------------------|--------|
208/// | `temperature` (number)  | `RunnableConfig.temperature` override |
209/// | `max_tokens` (integer)  | `RunnableConfig.max_tokens` override |
210/// | anything else           | merged into `RunnableConfig.metadata` |
211///
212/// Providers already consume `temperature` / `max_tokens` (via
213/// `sampling_overrides`), so e.g. `llm.configurable_fields()` actually
214/// changes sampling when the runtime config carries the key.
215pub struct RunnableConfigurableFields<I: Send + Sync + 'static, O: Send + Sync + 'static> {
216    inner: Box<dyn RunnableAny>,
217    _marker: PhantomData<(I, O)>,
218}
219
220impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableConfigurableFields<I, O> {
221    /// Wrap a runnable so its config fields can be overridden at invoke time.
222    pub fn new<R>(runnable: R) -> Self
223    where
224        R: Runnable<I, O> + 'static,
225        R::Error: Into<LcelError>,
226    {
227        Self {
228            inner: into_runnable_any(runnable),
229            _marker: PhantomData,
230        }
231    }
232
233    /// Build the effective config by promoting configurable keys into typed
234    /// config fields / metadata.
235    fn effective_config(&self, config: &Option<RunnableConfig>) -> RunnableConfig {
236        let mut effective = config.clone().unwrap_or_default();
237        // Clone the map first — applying each override consumes `effective`.
238        let configurables = effective.configurable.clone();
239        for (key, value) in configurables {
240            match (key.as_str(), &value) {
241                ("temperature", Value::Number(n)) => match n.as_f64() {
242                    Some(f) => effective = effective.with_temperature(f as f32),
243                    None => effective = effective.with_metadata(key.to_string(), value.clone()),
244                },
245                ("max_tokens", Value::Number(n)) => match n.as_u64() {
246                    Some(u) => effective = effective.with_max_tokens(u as usize),
247                    None => effective = effective.with_metadata(key.to_string(), value.clone()),
248                },
249                (k, v) => effective = effective.with_metadata(k.to_string(), v.clone()),
250            }
251        }
252        effective
253    }
254}
255
256#[async_trait]
257impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
258    for RunnableConfigurableFields<I, O>
259{
260    type Error = LcelError;
261
262    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
263        let effective = self.effective_config(&config);
264        let boxed = self
265            .inner
266            .invoke_any(Box::new(input) as Box<dyn Any + Send>, Some(effective))
267            .await?;
268        boxed.downcast::<O>().map(|b| *b).map_err(|_| {
269            LcelError::TypeMismatch(format!(
270                "configurable_fields invoke downcast: expected {}",
271                std::any::type_name::<O>()
272            ))
273        })
274    }
275
276    async fn batch(
277        &self,
278        inputs: Vec<I>,
279        config: Option<RunnableConfig>,
280    ) -> Result<Vec<O>, LcelError> {
281        let effective = self.effective_config(&config);
282        let boxed_inputs: Vec<Box<dyn Any + Send>> = inputs
283            .into_iter()
284            .map(|i| Box::new(i) as Box<dyn Any + Send>)
285            .collect();
286        let results = self.inner.batch_any(boxed_inputs, Some(effective)).await?;
287        results
288            .into_iter()
289            .map(|boxed| {
290                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
291                    LcelError::TypeMismatch(format!(
292                        "configurable_fields batch downcast: expected {}",
293                        std::any::type_name::<O>()
294                    ))
295                })
296            })
297            .collect()
298    }
299
300    async fn stream(
301        &self,
302        input: I,
303        config: Option<RunnableConfig>,
304    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
305        let effective = self.effective_config(&config);
306        let stream = self
307            .inner
308            .stream_any(Box::new(input) as Box<dyn Any + Send>, Some(effective))
309            .await?;
310        let output = stream.map(|result| {
311            result.and_then(|boxed| {
312                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
313                    LcelError::TypeMismatch(format!(
314                        "configurable_fields stream downcast: expected {}",
315                        std::any::type_name::<O>()
316                    ))
317                })
318            })
319        });
320        Ok(Box::pin(output))
321    }
322
323    async fn transform(
324        &self,
325        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
326        config: Option<RunnableConfig>,
327    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send + '_>>, LcelError> {
328        let effective = self.effective_config(&config);
329        let any_input: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
330            Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
331
332        let stream = self.inner.transform_any(any_input, Some(effective)).await?;
333        let output = stream.map(|result| {
334            result.and_then(|boxed| {
335                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
336                    LcelError::TypeMismatch(format!(
337                        "configurable_fields transform downcast: expected {}",
338                        std::any::type_name::<O>()
339                    ))
340                })
341            })
342        });
343        Ok(Box::pin(output))
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::RunnableExt;
351    use crate::RunnableLambda;
352
353    #[tokio::test]
354    async fn routes_to_alternative() {
355        let chain = RunnableLambda::new_sync(|s: String| format!("default:{s}"))
356            .configurable_alternatives(
357                "which",
358                "default",
359                vec![(
360                    "alt",
361                    RunnableLambda::new_sync(|s: String| format!("alt:{s}")),
362                )],
363            );
364        let cfg = RunnableConfig::new().with_configurable("which", serde_json::json!("alt"));
365        assert_eq!(
366            chain.invoke("x".to_string(), Some(cfg)).await.unwrap(),
367            "alt:x"
368        );
369    }
370
371    #[tokio::test]
372    async fn defaults_when_no_selector() {
373        let chain = RunnableLambda::new_sync(|s: String| format!("default:{s}"))
374            .configurable_alternatives(
375                "which",
376                "default",
377                vec![(
378                    "alt",
379                    RunnableLambda::new_sync(|s: String| format!("alt:{s}")),
380                )],
381            );
382        // 无 config / 无 which 键 → default
383        assert_eq!(
384            chain.invoke("x".to_string(), None).await.unwrap(),
385            "default:x"
386        );
387        let cfg = RunnableConfig::new().with_configurable("which", serde_json::json!("default"));
388        assert_eq!(
389            chain.invoke("x".to_string(), Some(cfg)).await.unwrap(),
390            "default:x"
391        );
392        // 未知值 → default
393        let cfg = RunnableConfig::new().with_configurable("which", serde_json::json!("nope"));
394        assert_eq!(
395            chain.invoke("x".to_string(), Some(cfg)).await.unwrap(),
396            "default:x"
397        );
398    }
399
400    /// 探针:Runnable<(), String>,invoke 时把收到的 config.temperature 打出来
401    struct TemperatureProbe;
402
403    #[async_trait]
404    impl Runnable<(), String> for TemperatureProbe {
405        type Error = LcelError;
406
407        async fn invoke(
408            &self,
409            _input: (),
410            config: Option<RunnableConfig>,
411        ) -> Result<String, LcelError> {
412            Ok(format!(
413                "temp={:?}",
414                config.as_ref().and_then(|c| c.temperature)
415            ))
416        }
417    }
418
419    #[tokio::test]
420    async fn configurable_fields_promotes_temperature() {
421        let wrapped = RunnableConfigurableFields::<(), String>::new(TemperatureProbe);
422        let cfg = RunnableConfig::new().with_configurable("temperature", serde_json::json!(0.5));
423        let out = wrapped.invoke((), Some(cfg)).await.unwrap();
424        assert_eq!(
425            out, "temp=Some(0.5)",
426            "temperature 应被提升为 typed config 字段"
427        );
428    }
429
430    #[tokio::test]
431    async fn configurable_fields_promotes_max_tokens() {
432        let wrapped = RunnableConfigurableFields::<(), String>::new(TemperatureProbe);
433        let cfg = RunnableConfig::new().with_configurable("max_tokens", serde_json::json!(128));
434        let out = wrapped.invoke((), Some(cfg)).await.unwrap();
435        assert_eq!(out, "temp=None"); // temperature 未被设置
436    }
437
438    #[tokio::test]
439    async fn configurable_fields_other_keys_go_to_metadata() {
440        struct MetadataProbe;
441
442        #[async_trait]
443        impl Runnable<(), String> for MetadataProbe {
444            type Error = LcelError;
445
446            async fn invoke(
447                &self,
448                _input: (),
449                config: Option<RunnableConfig>,
450            ) -> Result<String, LcelError> {
451                let cfg = config.unwrap_or_default();
452                Ok(cfg
453                    .metadata
454                    .get("provider")
455                    .cloned()
456                    .unwrap_or_default()
457                    .to_string())
458            }
459        }
460
461        let wrapped = RunnableConfigurableFields::<(), String>::new(MetadataProbe);
462        let cfg = RunnableConfig::new().with_configurable("provider", serde_json::json!("x"));
463        let out = wrapped.invoke((), Some(cfg)).await.unwrap();
464        assert_eq!(out, "\"x\"", "未知 configurable 键应进 metadata");
465    }
466}