lc_core/runnables/
pick.rs1use super::error::LcelError;
18use super::ext::RunnableExt;
19use super::lambda::RunnableLambda;
20use super::runnable_trait::Runnable;
21use super::sequence::RunnableSequence;
22use serde_json::Value;
23use std::collections::HashMap;
24
25pub trait RunnablePick<I: Send + Sync + 'static>: Sized {
27 fn pick<K>(
41 self,
42 keys: impl IntoIterator<Item = K>,
43 ) -> RunnableSequence<I, HashMap<String, Value>>
44 where
45 K: Into<String>;
46
47 fn pluck(self, key: impl Into<String>) -> RunnableSequence<I, Value>;
58}
59
60impl<I, R> RunnablePick<I> for R
61where
62 I: Send + Sync + 'static,
63 R: Runnable<I, HashMap<String, Value>> + Sized + 'static,
64 R::Error: Into<LcelError>,
65{
66 fn pick<K>(
67 self,
68 keys: impl IntoIterator<Item = K>,
69 ) -> RunnableSequence<I, HashMap<String, Value>>
70 where
71 K: Into<String>,
72 {
73 let keys: Vec<String> = keys.into_iter().map(Into::into).collect();
74 let filter = RunnableLambda::new_sync(move |m: HashMap<String, Value>| {
75 keys.iter()
76 .filter_map(|k| m.get(k).cloned().map(|v| (k.clone(), v)))
77 .collect::<HashMap<String, Value>>()
78 });
79 self.pipe(filter)
80 }
81
82 fn pluck(self, key: impl Into<String>) -> RunnableSequence<I, Value> {
83 let key = key.into();
84 let extract = RunnableLambda::new_sync(move |m: HashMap<String, Value>| {
85 m.get(&key).cloned().unwrap_or(Value::Null)
86 });
87 self.pipe(extract)
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::Runnable;
95 use crate::RunnableLambda;
96 use crate::RunnableParallel;
97
98 fn parallel() -> RunnableParallel<String> {
99 RunnableParallel::<String>::new()
100 .with("len", RunnableLambda::new_sync(|s: String| s.len() as i64))
101 .with(
102 "upper",
103 RunnableLambda::new_sync(|s: String| s.to_uppercase()),
104 )
105 }
106
107 #[tokio::test]
108 async fn pick_keeps_only_selected_keys() {
109 let chain = parallel().pick(["len"]);
110 let out = chain.invoke("hello".to_string(), None).await.unwrap();
111 assert_eq!(out.len(), 1);
112 assert!(out.contains_key("len"));
113 assert!(!out.contains_key("upper"));
114 }
115
116 #[tokio::test]
117 async fn pick_multiple_keys_and_missing() {
118 let chain = parallel().pick(["len", "nope"]);
119 let out = chain.invoke("hello".to_string(), None).await.unwrap();
120 assert_eq!(out.len(), 1);
122 assert!(out.contains_key("len"));
123 }
124
125 #[tokio::test]
126 async fn pluck_returns_single_value() {
127 let chain = parallel().pluck("upper");
128 let out = chain.invoke("hello".to_string(), None).await.unwrap();
129 assert_eq!(out, Value::String("HELLO".to_string()));
130 }
131
132 #[tokio::test]
133 async fn pluck_missing_key_yields_null() {
134 let chain = parallel().pluck("missing");
135 let out = chain.invoke("hello".to_string(), None).await.unwrap();
136 assert_eq!(out, Value::Null);
137 }
138
139 #[tokio::test]
140 async fn pick_works_on_sequence_ending_in_map() {
141 let seq = RunnableLambda::new_sync(|s: String| s.to_uppercase())
143 .pipe(RunnableLambda::new_sync(|s: String| {
144 let mut m = HashMap::new();
145 m.insert("up".to_string(), Value::String(s));
146 m.insert("drop".to_string(), Value::Bool(true));
147 m
148 }))
149 .pick(["up"]);
150 let out = seq.invoke("hi".to_string(), None).await.unwrap();
151 assert_eq!(out.len(), 1);
152 assert_eq!(out["up"], Value::String("HI".to_string()));
153 }
154}