1use crate::errors::GraphError;
8use crate::state::StateSchema;
9use std::collections::HashMap;
10use std::marker::PhantomData;
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum EdgeTarget {
15 Fixed(String),
17
18 Conditional(String),
20}
21
22impl EdgeTarget {
23 pub fn to(node: impl Into<String>) -> Self {
25 Self::Fixed(node.into())
26 }
27
28 pub fn conditional(router: impl Into<String>) -> Self {
30 Self::Conditional(router.into())
31 }
32}
33
34#[derive(Debug, Clone)]
40pub enum GraphEdge {
41 Fixed {
42 source: String,
43 target: String,
44 },
45
46 Conditional {
47 source: String,
48 router_name: String,
49 targets: HashMap<String, String>,
50 default_target: Option<String>,
51 },
52
53 FanOut {
55 source: String,
56 targets: Vec<String>,
57 },
58
59 FanIn {
61 sources: Vec<String>,
62 target: String,
63 },
64}
65
66impl GraphEdge {
67 pub fn fixed(source: impl Into<String>, target: impl Into<String>) -> Self {
68 Self::Fixed {
69 source: source.into(),
70 target: target.into(),
71 }
72 }
73
74 pub fn conditional<R, T>(
75 source: impl Into<String>,
76 router_name: impl Into<String>,
77 targets: HashMap<R, T>,
78 default: Option<T>,
79 ) -> Self
80 where
81 R: Into<String>,
82 T: Into<String>,
83 {
84 Self::Conditional {
85 source: source.into(),
86 router_name: router_name.into(),
87 targets: targets
88 .into_iter()
89 .map(|(k, v)| (k.into(), v.into()))
90 .collect(),
91 default_target: default.map(|d| d.into()),
92 }
93 }
94
95 pub fn fan_out(source: impl Into<String>, targets: Vec<String>) -> Self {
96 Self::FanOut {
97 source: source.into(),
98 targets,
99 }
100 }
101
102 pub fn fan_in(sources: Vec<String>, target: impl Into<String>) -> Self {
103 Self::FanIn {
104 sources,
105 target: target.into(),
106 }
107 }
108
109 pub fn source(&self) -> &str {
110 match self {
111 Self::Fixed { source, .. } => source,
112 Self::Conditional { source, .. } => source,
113 Self::FanOut { source, .. } => source,
114 Self::FanIn { .. } => "__fanin__", }
116 }
117
118 pub fn fixed_target(&self) -> Option<&str> {
119 match self {
120 Self::Fixed { target, .. } => Some(target),
121 Self::Conditional { .. } => None,
122 Self::FanOut { .. } => None,
123 Self::FanIn { target, .. } => Some(target),
124 }
125 }
126
127 pub fn fan_out_targets(&self) -> Option<&Vec<String>> {
128 match self {
129 Self::FanOut { targets, .. } => Some(targets),
130 _ => None,
131 }
132 }
133
134 pub fn fan_in_sources(&self) -> Option<&Vec<String>> {
135 match self {
136 Self::FanIn { sources, .. } => Some(sources),
137 _ => None,
138 }
139 }
140}
141
142#[async_trait::async_trait]
147pub trait ConditionalEdge<S: StateSchema>: Send + Sync {
148 async fn route(&self, state: &S) -> Result<String, GraphError>;
152}
153
154pub struct FunctionRouter<S: StateSchema, F> {
156 func: F,
157 _marker: PhantomData<S>,
158}
159
160impl<S: StateSchema, F> FunctionRouter<S, F>
161where
162 F: Fn(&S) -> String + Send + Sync,
163{
164 pub fn new(func: F) -> Self {
165 Self {
166 func,
167 _marker: PhantomData,
168 }
169 }
170}
171
172#[async_trait::async_trait]
173impl<S: StateSchema, F> ConditionalEdge<S> for FunctionRouter<S, F>
174where
175 F: Fn(&S) -> String + Send + Sync,
176{
177 async fn route(&self, state: &S) -> Result<String, GraphError> {
178 Ok((self.func)(state))
179 }
180}
181
182pub struct AsyncFunctionRouter<S: StateSchema, F> {
184 func: F,
185 _marker: PhantomData<S>,
186}
187
188impl<S: StateSchema, F, Fut> AsyncFunctionRouter<S, F>
189where
190 F: Fn(&S) -> Fut + Send + Sync,
191 Fut: std::future::Future<Output = Result<String, GraphError>> + Send,
192{
193 pub fn new(func: F) -> Self {
194 Self {
195 func,
196 _marker: PhantomData,
197 }
198 }
199}
200
201#[async_trait::async_trait]
202impl<S: StateSchema, F, Fut> ConditionalEdge<S> for AsyncFunctionRouter<S, F>
203where
204 F: Fn(&S) -> Fut + Send + Sync,
205 Fut: std::future::Future<Output = Result<String, GraphError>> + Send,
206{
207 async fn route(&self, state: &S) -> Result<String, GraphError> {
208 (self.func)(state).await
209 }
210}
211
212pub const ROUTE_CONTINUE: &str = "continue";
214pub const ROUTE_END: &str = "end";
215pub const ROUTE_ERROR: &str = "error";
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::state::AgentState;
221
222 #[test]
223 fn test_fixed_edge() {
224 let edge = GraphEdge::fixed("start", "process");
225 assert_eq!(edge.source(), "start");
226 assert_eq!(edge.fixed_target(), Some("process"));
227 }
228
229 #[test]
230 fn test_conditional_edge() {
231 let targets = HashMap::from([("continue", "next_node"), ("end", "__end__")]);
232 let edge = GraphEdge::conditional("decision", "router", targets, None);
233 assert_eq!(edge.source(), "decision");
234 assert!(edge.fixed_target().is_none());
235 }
236
237 #[tokio::test]
238 async fn test_function_router() {
239 let router = FunctionRouter::new(|state: &AgentState| {
240 if state.output.is_some() {
241 ROUTE_END.to_string()
242 } else {
243 ROUTE_CONTINUE.to_string()
244 }
245 });
246
247 let state = AgentState::new("test".to_string());
248 let route = router.route(&state).await.unwrap();
249 assert_eq!(route, ROUTE_CONTINUE);
250 }
251}