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.push((name.into(), into_runnable_any(runnable)));
89        self
90    }
91
92    /// Pick the target for this call based on the configurable selector.
93    fn resolve(&self, config: &Option<RunnableConfig>) -> &dyn RunnableAny {
94        let selected = config
95            .as_ref()
96            .and_then(|c| c.configurable_value(&self.which))
97            .and_then(|v| v.as_str());
98        match selected {
99            Some(name) if name != self.default_key => self
100                .alternatives
101                .iter()
102                .find(|(k, _)| k == name)
103                .map(|(_, r)| r.as_ref())
104                .unwrap_or(&*self.default),
105            _ => &*self.default,
106        }
107    }
108}
109
110#[async_trait]
111impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
112    for RunnableConfigurable<I, O>
113{
114    type Error = LcelError;
115
116    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
117        let target = self.resolve(&config);
118        let boxed = target.invoke_any(Box::new(input) as Box<dyn Any + Send>, config).await?;
119        boxed.downcast::<O>().map(|b| *b).map_err(|_| {
120            LcelError::TypeMismatch(format!(
121                "configurable invoke downcast: expected {}",
122                std::any::type_name::<O>()
123            ))
124        })
125    }
126
127    async fn batch(
128        &self,
129        inputs: Vec<I>,
130        config: Option<RunnableConfig>,
131    ) -> Result<Vec<O>, LcelError> {
132        let target = self.resolve(&config);
133        let boxed_inputs: Vec<Box<dyn Any + Send>> = inputs
134            .into_iter()
135            .map(|i| Box::new(i) as Box<dyn Any + Send>)
136            .collect();
137        let results = target.batch_any(boxed_inputs, config).await?;
138        results
139            .into_iter()
140            .map(|boxed| {
141                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
142                    LcelError::TypeMismatch(format!(
143                        "configurable batch downcast: expected {}",
144                        std::any::type_name::<O>()
145                    ))
146                })
147            })
148            .collect()
149    }
150
151    async fn stream(
152        &self,
153        input: I,
154        config: Option<RunnableConfig>,
155    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
156        let target = self.resolve(&config);
157        let stream = target
158            .stream_any(Box::new(input) as Box<dyn Any + Send>, config)
159            .await?;
160        let output = stream.map(|result| {
161            result.and_then(|boxed| {
162                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
163                    LcelError::TypeMismatch(format!(
164                        "configurable stream downcast: expected {}",
165                        std::any::type_name::<O>()
166                    ))
167                })
168            })
169        });
170        Ok(Box::pin(output))
171    }
172
173    async fn transform(
174        &self,
175        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
176        config: Option<RunnableConfig>,
177    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
178        let target = self.resolve(&config);
179        let any_input: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
180            Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
181
182        let stream = target.transform_any(any_input, config).await?;
183        let output = stream.map(|result| {
184            result.and_then(|boxed| {
185                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
186                    LcelError::TypeMismatch(format!(
187                        "configurable transform downcast: expected {}",
188                        std::any::type_name::<O>()
189                    ))
190                })
191            })
192        });
193        Ok(Box::pin(output))
194    }
195}
196
197/// Overrides recognized config fields at invoke time from
198/// `config.configurable` (Python's `Runnable.configurable_fields`).
199///
200/// The following configurable keys are applied before the inner runnable is
201/// invoked:
202///
203/// | configurable key        | effect |
204/// |-------------------------|--------|
205/// | `temperature` (number)  | `RunnableConfig.temperature` override |
206/// | `max_tokens` (integer)  | `RunnableConfig.max_tokens` override |
207/// | anything else           | merged into `RunnableConfig.metadata` |
208///
209/// Providers already consume `temperature` / `max_tokens` (via
210/// `sampling_overrides`), so e.g. `llm.configurable_fields()` actually
211/// changes sampling when the runtime config carries the key.
212pub struct RunnableConfigurableFields<I: Send + Sync + 'static, O: Send + Sync + 'static> {
213    inner: Box<dyn RunnableAny>,
214    _marker: PhantomData<(I, O)>,
215}
216
217impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableConfigurableFields<I, O> {
218    /// Wrap a runnable so its config fields can be overridden at invoke time.
219    pub fn new<R>(runnable: R) -> Self
220    where
221        R: Runnable<I, O> + 'static,
222        R::Error: Into<LcelError>,
223    {
224        Self {
225            inner: into_runnable_any(runnable),
226            _marker: PhantomData,
227        }
228    }
229
230    /// Build the effective config by promoting configurable keys into typed
231    /// config fields / metadata.
232    fn effective_config(&self, config: &Option<RunnableConfig>) -> RunnableConfig {
233        let mut effective = config.clone().unwrap_or_default();
234        // Clone the map first — applying each override consumes `effective`.
235        let configurables = effective.configurable.clone();
236        for (key, value) in configurables {
237            match (key.as_str(), &value) {
238                ("temperature", Value::Number(n)) if n.as_f64().is_some() => {
239                    effective = effective.with_temperature(n.as_f64().unwrap() as f32);
240                }
241                ("max_tokens", Value::Number(n)) if n.as_u64().is_some() => {
242                    effective = effective.with_max_tokens(n.as_u64().unwrap() as usize);
243                }
244                (k, v) => effective = effective.with_metadata(k.to_string(), v.clone()),
245            }
246        }
247        effective
248    }
249}
250
251#[async_trait]
252impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
253    for RunnableConfigurableFields<I, O>
254{
255    type Error = LcelError;
256
257    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
258        let effective = self.effective_config(&config);
259        let boxed = self
260            .inner
261            .invoke_any(Box::new(input) as Box<dyn Any + Send>, Some(effective))
262            .await?;
263        boxed.downcast::<O>().map(|b| *b).map_err(|_| {
264            LcelError::TypeMismatch(format!(
265                "configurable_fields invoke downcast: expected {}",
266                std::any::type_name::<O>()
267            ))
268        })
269    }
270
271    async fn batch(
272        &self,
273        inputs: Vec<I>,
274        config: Option<RunnableConfig>,
275    ) -> Result<Vec<O>, LcelError> {
276        let effective = self.effective_config(&config);
277        let boxed_inputs: Vec<Box<dyn Any + Send>> = inputs
278            .into_iter()
279            .map(|i| Box::new(i) as Box<dyn Any + Send>)
280            .collect();
281        let results = self.inner.batch_any(boxed_inputs, Some(effective)).await?;
282        results
283            .into_iter()
284            .map(|boxed| {
285                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
286                    LcelError::TypeMismatch(format!(
287                        "configurable_fields batch downcast: expected {}",
288                        std::any::type_name::<O>()
289                    ))
290                })
291            })
292            .collect()
293    }
294
295    async fn stream(
296        &self,
297        input: I,
298        config: Option<RunnableConfig>,
299    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
300        let effective = self.effective_config(&config);
301        let stream = self
302            .inner
303            .stream_any(Box::new(input) as Box<dyn Any + Send>, Some(effective))
304            .await?;
305        let output = stream.map(|result| {
306            result.and_then(|boxed| {
307                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
308                    LcelError::TypeMismatch(format!(
309                        "configurable_fields stream downcast: expected {}",
310                        std::any::type_name::<O>()
311                    ))
312                })
313            })
314        });
315        Ok(Box::pin(output))
316    }
317
318    async fn transform(
319        &self,
320        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
321        config: Option<RunnableConfig>,
322    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
323        let effective = self.effective_config(&config);
324        let any_input: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
325            Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
326
327        let stream = self.inner.transform_any(any_input, Some(effective)).await?;
328        let output = stream.map(|result| {
329            result.and_then(|boxed| {
330                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
331                    LcelError::TypeMismatch(format!(
332                        "configurable_fields transform downcast: expected {}",
333                        std::any::type_name::<O>()
334                    ))
335                })
336            })
337        });
338        Ok(Box::pin(output))
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::RunnableExt;
346    use crate::RunnableLambda;
347
348    #[tokio::test]
349    async fn routes_to_alternative() {
350        let chain = RunnableLambda::new_sync(|s: String| format!("default:{s}"))
351            .configurable_alternatives(
352                "which",
353                "default",
354                vec![("alt", RunnableLambda::new_sync(|s: String| format!("alt:{s}")))],
355            );
356        let cfg = RunnableConfig::new().with_configurable("which", serde_json::json!("alt"));
357        assert_eq!(chain.invoke("x".to_string(), Some(cfg)).await.unwrap(), "alt:x");
358    }
359
360    #[tokio::test]
361    async fn defaults_when_no_selector() {
362        let chain = RunnableLambda::new_sync(|s: String| format!("default:{s}"))
363            .configurable_alternatives(
364                "which",
365                "default",
366                vec![(
367                    "alt",
368                    RunnableLambda::new_sync(|s: String| format!("alt:{s}")),
369                )],
370            );
371        // 无 config / 无 which 键 → default
372        assert_eq!(chain.invoke("x".to_string(), None).await.unwrap(), "default:x");
373        let cfg = RunnableConfig::new().with_configurable("which", serde_json::json!("default"));
374        assert_eq!(chain.invoke("x".to_string(), Some(cfg)).await.unwrap(), "default:x");
375        // 未知值 → default
376        let cfg = RunnableConfig::new().with_configurable("which", serde_json::json!("nope"));
377        assert_eq!(chain.invoke("x".to_string(), Some(cfg)).await.unwrap(), "default:x");
378    }
379
380    /// 探针:Runnable<(), String>,invoke 时把收到的 config.temperature 打出来
381    struct TemperatureProbe;
382
383    #[async_trait]
384    impl Runnable<(), String> for TemperatureProbe {
385        type Error = LcelError;
386
387        async fn invoke(
388            &self,
389            _input: (),
390            config: Option<RunnableConfig>,
391        ) -> Result<String, LcelError> {
392            Ok(format!(
393                "temp={:?}",
394                config.as_ref().and_then(|c| c.temperature)
395            ))
396        }
397    }
398
399    #[tokio::test]
400    async fn configurable_fields_promotes_temperature() {
401        let wrapped = RunnableConfigurableFields::<(), String>::new(TemperatureProbe);
402        let cfg = RunnableConfig::new().with_configurable("temperature", serde_json::json!(0.5));
403        let out = wrapped.invoke((), Some(cfg)).await.unwrap();
404        assert_eq!(out, "temp=Some(0.5)", "temperature 应被提升为 typed config 字段");
405    }
406
407    #[tokio::test]
408    async fn configurable_fields_promotes_max_tokens() {
409        let wrapped = RunnableConfigurableFields::<(), String>::new(TemperatureProbe);
410        let cfg = RunnableConfig::new().with_configurable("max_tokens", serde_json::json!(128));
411        let out = wrapped.invoke((), Some(cfg)).await.unwrap();
412        assert_eq!(out, "temp=None"); // temperature 未被设置
413    }
414
415    #[tokio::test]
416    async fn configurable_fields_other_keys_go_to_metadata() {
417        struct MetadataProbe;
418
419        #[async_trait]
420        impl Runnable<(), String> for MetadataProbe {
421            type Error = LcelError;
422
423            async fn invoke(
424                &self,
425                _input: (),
426                config: Option<RunnableConfig>,
427            ) -> Result<String, LcelError> {
428                let cfg = config.unwrap_or_default();
429                Ok(cfg
430                    .metadata
431                    .get("provider")
432                    .cloned()
433                    .unwrap_or_default()
434                    .to_string())
435            }
436        }
437
438        let wrapped = RunnableConfigurableFields::<(), String>::new(MetadataProbe);
439        let cfg = RunnableConfig::new().with_configurable("provider", serde_json::json!("x"));
440        let out = wrapped.invoke((), Some(cfg)).await.unwrap();
441        assert_eq!(out, "\"x\"", "未知 configurable 键应进 metadata");
442    }
443}