1use 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
21pub 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 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 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 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
200pub 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 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 fn effective_config(&self, config: &Option<RunnableConfig>) -> RunnableConfig {
236 let mut effective = config.clone().unwrap_or_default();
237 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 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 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 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"); }
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}