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 {
43 source: String,
45 target: String,
47 },
48
49 Conditional {
51 source: String,
53 router_name: String,
55 targets: HashMap<String, String>,
57 default_target: Option<String>,
59 },
60
61 FanOut {
63 source: String,
65 targets: Vec<String>,
67 },
68
69 FanIn {
71 sources: Vec<String>,
73 target: String,
75 },
76}
77
78impl GraphEdge {
79 pub fn fixed(source: impl Into<String>, target: impl Into<String>) -> Self {
81 Self::Fixed {
82 source: source.into(),
83 target: target.into(),
84 }
85 }
86
87 pub fn conditional<R, T>(
89 source: impl Into<String>,
90 router_name: impl Into<String>,
91 targets: HashMap<R, T>,
92 default: Option<T>,
93 ) -> Self
94 where
95 R: Into<String>,
96 T: Into<String>,
97 {
98 Self::Conditional {
99 source: source.into(),
100 router_name: router_name.into(),
101 targets: targets
102 .into_iter()
103 .map(|(k, v)| (k.into(), v.into()))
104 .collect(),
105 default_target: default.map(|d| d.into()),
106 }
107 }
108
109 pub fn fan_out(source: impl Into<String>, targets: Vec<String>) -> Self {
111 Self::FanOut {
112 source: source.into(),
113 targets,
114 }
115 }
116
117 pub fn fan_in(sources: Vec<String>, target: impl Into<String>) -> Self {
119 Self::FanIn {
120 sources,
121 target: target.into(),
122 }
123 }
124
125 pub fn source(&self) -> &str {
127 match self {
128 Self::Fixed { source, .. } => source,
129 Self::Conditional { source, .. } => source,
130 Self::FanOut { source, .. } => source,
131 Self::FanIn { .. } => "__fanin__", }
133 }
134
135 pub fn fixed_target(&self) -> Option<&str> {
137 match self {
138 Self::Fixed { target, .. } => Some(target),
139 Self::Conditional { .. } => None,
140 Self::FanOut { .. } => None,
141 Self::FanIn { target, .. } => Some(target),
142 }
143 }
144
145 pub fn fan_out_targets(&self) -> Option<&Vec<String>> {
147 match self {
148 Self::FanOut { targets, .. } => Some(targets),
149 _ => None,
150 }
151 }
152
153 pub fn fan_in_sources(&self) -> Option<&Vec<String>> {
155 match self {
156 Self::FanIn { sources, .. } => Some(sources),
157 _ => None,
158 }
159 }
160}
161
162#[async_trait::async_trait]
167pub trait ConditionalEdge<S: StateSchema>: Send + Sync {
168 async fn route(&self, state: &S) -> Result<String, GraphError>;
172}
173
174pub struct FunctionRouter<S: StateSchema, F> {
176 func: F,
177 _marker: PhantomData<S>,
178}
179
180impl<S: StateSchema, F> FunctionRouter<S, F>
181where
182 F: Fn(&S) -> String + Send + Sync,
183{
184 pub fn new(func: F) -> Self {
186 Self {
187 func,
188 _marker: PhantomData,
189 }
190 }
191}
192
193#[async_trait::async_trait]
194impl<S: StateSchema, F> ConditionalEdge<S> for FunctionRouter<S, F>
195where
196 F: Fn(&S) -> String + Send + Sync,
197{
198 async fn route(&self, state: &S) -> Result<String, GraphError> {
199 Ok((self.func)(state))
200 }
201}
202
203pub struct AsyncFunctionRouter<S: StateSchema, F> {
205 func: F,
206 _marker: PhantomData<S>,
207}
208
209impl<S: StateSchema, F, Fut> AsyncFunctionRouter<S, F>
210where
211 F: Fn(&S) -> Fut + Send + Sync,
212 Fut: std::future::Future<Output = Result<String, GraphError>> + Send,
213{
214 pub fn new(func: F) -> Self {
216 Self {
217 func,
218 _marker: PhantomData,
219 }
220 }
221}
222
223#[async_trait::async_trait]
224impl<S: StateSchema, F, Fut> ConditionalEdge<S> for AsyncFunctionRouter<S, F>
225where
226 F: Fn(&S) -> Fut + Send + Sync,
227 Fut: std::future::Future<Output = Result<String, GraphError>> + Send,
228{
229 async fn route(&self, state: &S) -> Result<String, GraphError> {
230 (self.func)(state).await
231 }
232}
233
234pub const ROUTE_CONTINUE: &str = "continue";
236pub const ROUTE_END: &str = "end";
238pub const ROUTE_ERROR: &str = "error";
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::state::AgentState;
245
246 #[test]
247 fn test_fixed_edge() {
248 let edge = GraphEdge::fixed("start", "process");
249 assert_eq!(edge.source(), "start");
250 assert_eq!(edge.fixed_target(), Some("process"));
251 }
252
253 #[test]
254 fn test_conditional_edge() {
255 let targets = HashMap::from([("continue", "next_node"), ("end", "__end__")]);
256 let edge = GraphEdge::conditional("decision", "router", targets, None);
257 assert_eq!(edge.source(), "decision");
258 assert!(edge.fixed_target().is_none());
259 }
260
261 #[tokio::test]
262 async fn test_function_router() {
263 let router = FunctionRouter::new(|state: &AgentState| {
264 if state.output.is_some() {
265 ROUTE_END.to_string()
266 } else {
267 ROUTE_CONTINUE.to_string()
268 }
269 });
270
271 let state = AgentState::new("test".to_string());
272 let route = router.route(&state).await.unwrap();
273 assert_eq!(route, ROUTE_CONTINUE);
274 }
275}