langgraph_core_rs/runnable/into_node_fn.rs
1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use serde_json::Value as JsonValue;
6use langgraph_checkpoint::config::RunnableConfig;
7
8use super::base::{Runnable, RunnableError};
9use super::callable::RunnableCallable;
10
11/// Type alias for the boxed future returned by async node functions.
12///
13/// Use this in return-position of factory functions that produce node closures:
14/// ```ignore
15/// fn my_node(model: Arc<dyn BaseChatModel>) -> impl Fn(JsonValue, RunnableConfig) -> NodeFnFuture + Send + Sync + 'static {
16/// move |input, config| {
17/// let model = model.clone();
18/// Box::pin(async move { ... })
19/// }
20/// }
21/// ```
22pub type NodeFnFuture = Pin<Box<dyn Future<Output = Result<JsonValue, RunnableError>> + Send>>;
23
24/// Trait for converting closures into node Runnables.
25///
26/// This enables `add_node` and `add_conditional_edges` to accept both
27/// async and sync closures with a uniform API.
28///
29/// Implemented for:
30/// - Async closures: `Fn(JsonValue, RunnableConfig) -> impl Future<...>`
31/// - Via `SyncNodeFn` wrapper: sync closures
32/// - `Arc<dyn Runnable>`: pre-built runnables
33pub trait IntoNodeFunction {
34 fn into_runnable(self, name: &str) -> Arc<dyn Runnable>;
35}
36
37// Blanket impl for async closures
38impl<F, Fut> IntoNodeFunction for F
39where
40 F: Fn(JsonValue, RunnableConfig) -> Fut + Send + Sync + 'static,
41 Fut: Future<Output = Result<JsonValue, RunnableError>> + Send + 'static,
42{
43 fn into_runnable(self, name: &str) -> Arc<dyn Runnable> {
44 Arc::new(RunnableCallable::new(name, self))
45 }
46}
47
48/// Wrapper for sync node functions.
49///
50/// Use this to pass sync closures to `add_node`:
51/// ```ignore
52/// graph.add_node("my_node", SyncNodeFn(|input, _config| {
53/// Ok(json!({"result": 42}))
54/// }));
55/// ```
56pub struct SyncNodeFn<F>(pub F);
57
58impl<F> IntoNodeFunction for SyncNodeFn<F>
59where
60 F: Fn(&JsonValue, &RunnableConfig) -> Result<JsonValue, RunnableError>
61 + Send + Sync + 'static,
62{
63 fn into_runnable(self, name: &str) -> Arc<dyn Runnable> {
64 Arc::new(RunnableCallable::new_sync(name, self.0))
65 }
66}
67
68/// Implement `IntoNodeFunction` for pre-built runnables.
69impl IntoNodeFunction for Arc<dyn Runnable> {
70 fn into_runnable(self, _name: &str) -> Arc<dyn Runnable> {
71 self
72 }
73}
74
75/// Wrapper for single-argument sync node functions that ignore config.
76///
77/// ```ignore
78/// use langgraph::prelude::*;
79///
80/// graph.add_node("doubler", NodeFn1(|input| {
81/// let n = input.get("value").and_then(|v| v.as_i64()).unwrap_or(0);
82/// Ok(json!({"value": n * 2}))
83/// }));
84/// ```
85pub struct NodeFn1<F>(pub F);
86
87impl<F> IntoNodeFunction for NodeFn1<F>
88where
89 F: Fn(&JsonValue) -> Result<JsonValue, RunnableError> + Send + Sync + 'static,
90{
91 fn into_runnable(self, name: &str) -> Arc<dyn Runnable> {
92 let f = self.0;
93 Arc::new(RunnableCallable::new_sync(name, move |input: &JsonValue, _config: &RunnableConfig| {
94 f(input)
95 }))
96 }
97}
98
99/// Wrapper for routing functions used with `add_conditional_edges`.
100///
101/// Wraps a function `Fn(&JsonValue) -> String` so it can be used
102/// directly as the `path` argument to `add_conditional_edges`.
103///
104/// ```ignore
105/// use langgraph::prelude::*;
106/// use langgraph_prebuilt::tools_condition;
107///
108/// graph.add_conditional_edges(
109/// "agent",
110/// RoutingFn(tools_condition),
111/// Some(HashMap::from([
112/// ("tools".to_string(), "tools".to_string()),
113/// (END.to_string(), END.to_string()),
114/// ])),
115/// )?;
116/// ```
117pub struct RoutingFn<F>(pub F);
118
119impl<F> IntoNodeFunction for RoutingFn<F>
120where
121 F: Fn(&JsonValue) -> String + Send + Sync + 'static,
122{
123 fn into_runnable(self, name: &str) -> Arc<dyn Runnable> {
124 let f = self.0;
125 Arc::new(RunnableCallable::new_sync(name, move |input: &JsonValue, _config: &RunnableConfig| {
126 let route = f(input);
127 Ok(JsonValue::String(route))
128 }))
129 }
130}
131
132/// Convenience macro to wrap a sync closure for use with `add_node`.
133///
134/// ```ignore
135/// use langgraph::prelude::*;
136///
137/// graph.add_node("doubler", node_fn!(|input, _config| {
138/// let n = input.as_i64().unwrap_or(0);
139/// Ok(json!(n * 2))
140/// }));
141/// ```
142#[macro_export]
143macro_rules! node_fn {
144 ($f:expr) => {
145 $crate::runnable::SyncNodeFn($f)
146 };
147}
148
149/// Wrap a routing function for use with `add_conditional_edges`.
150///
151/// ```ignore
152/// use langgraph::prelude::*;
153///
154/// // Instead of: RoutingFn(tools_condition)
155/// graph.add_conditional_edges("agent", routing!(tools_condition), None)?;
156/// ```
157#[macro_export]
158macro_rules! routing {
159 ($f:expr) => {
160 $crate::runnable::RoutingFn($f)
161 };
162}
163
164/// Simplify `add_conditional_edges` with automatic route map construction.
165///
166/// Python:
167/// ```python
168/// graph.add_conditional_edges("chatbot", tools_condition)
169/// ```
170///
171/// Rust (before):
172/// ```ignore
173/// graph.add_conditional_edges(
174/// "chatbot",
175/// RoutingFn(tools_condition),
176/// Some({
177/// let mut map = HashMap::new();
178/// map.insert("tools".to_string(), "tools".to_string());
179/// map.insert(END.to_string(), END.to_string());
180/// map
181/// }),
182/// )?;
183/// ```
184///
185/// Rust (after):
186/// ```ignore
187/// conditional_edges!(graph, "chatbot", tools_condition, "tools" => "tools", END => END)?;
188/// ```
189#[macro_export]
190macro_rules! conditional_edges {
191 ($graph:expr, $source:expr, $route_fn:expr, $($key:expr => $val:expr),+ $(,)?) => {
192 $graph.add_conditional_edges(
193 $source,
194 $crate::runnable::RoutingFn($route_fn),
195 Some({
196 let mut map = std::collections::HashMap::new();
197 $(map.insert($key.to_string(), $val.to_string());)+
198 map
199 }),
200 )
201 };
202}