agentforge_core/
multiagent.rs1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::AgentFile;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentNode {
9 pub id: String,
11 pub role: String,
13 pub agent: AgentFile,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GraphEdge {
20 pub from: String,
22 pub to: String,
24 pub output_field: Option<String>,
26 pub input_key: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AgentGraph {
33 pub id: Uuid,
34 pub name: String,
35 pub description: Option<String>,
36 pub nodes: Vec<AgentNode>,
37 pub edges: Vec<GraphEdge>,
38}
39
40impl AgentGraph {
41 pub fn topological_order(&self) -> Result<Vec<&AgentNode>, String> {
44 use std::collections::{HashMap, VecDeque};
45
46 let mut in_degree: HashMap<&str, usize> = HashMap::new();
47 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
48
49 for node in &self.nodes {
50 in_degree.entry(node.id.as_str()).or_insert(0);
51 adj.entry(node.id.as_str()).or_default();
52 }
53
54 for edge in &self.edges {
55 *in_degree.entry(edge.to.as_str()).or_insert(0) += 1;
56 adj.entry(edge.from.as_str())
57 .or_default()
58 .push(edge.to.as_str());
59 }
60
61 let mut queue: VecDeque<&str> = in_degree
62 .iter()
63 .filter(|(_, &d)| d == 0)
64 .map(|(&id, _)| id)
65 .collect();
66
67 let node_by_id: HashMap<&str, &AgentNode> =
68 self.nodes.iter().map(|n| (n.id.as_str(), n)).collect();
69
70 let mut result = Vec::new();
71 while let Some(id) = queue.pop_front() {
72 if let Some(node) = node_by_id.get(id) {
73 result.push(*node);
74 }
75 if let Some(neighbors) = adj.get(id) {
76 for &neighbor in neighbors {
77 let deg = in_degree.get_mut(neighbor).unwrap();
78 *deg -= 1;
79 if *deg == 0 {
80 queue.push_back(neighbor);
81 }
82 }
83 }
84 }
85
86 if result.len() != self.nodes.len() {
87 return Err("AgentGraph contains a cycle".to_string());
88 }
89
90 Ok(result)
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct MultiAgentScorecard {
97 pub graph_id: Uuid,
98 pub node_scores: std::collections::HashMap<String, f64>,
100 pub composite_score: f64,
102 pub pass_rate: f64,
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn make_graph(edges: &[(&str, &str)]) -> AgentGraph {
110 use crate::{AgentFile, ModelConfig, ModelProvider};
111 let node_ids: std::collections::HashSet<&str> =
112 edges.iter().flat_map(|(a, b)| [*a, *b]).collect();
113
114 let nodes = node_ids
115 .into_iter()
116 .map(|id| AgentNode {
117 id: id.to_string(),
118 role: id.to_string(),
119 agent: AgentFile {
120 agentforge_schema_version: "1".into(),
121 name: id.to_string(),
122 version: "0.1.0".into(),
123 model: ModelConfig {
124 provider: ModelProvider::Openai,
125 model_id: "gpt-4o".into(),
126 temperature: None,
127 max_tokens: None,
128 top_p: None,
129 },
130 system_prompt: String::new(),
131 tools: vec![],
132 output_schema: None,
133 constraints: vec![],
134 eval_hints: None,
135 metadata: None,
136 },
137 })
138 .collect();
139
140 let graph_edges = edges
141 .iter()
142 .map(|(from, to)| GraphEdge {
143 from: from.to_string(),
144 to: to.to_string(),
145 output_field: None,
146 input_key: None,
147 })
148 .collect();
149
150 AgentGraph {
151 id: Uuid::new_v4(),
152 name: "test".into(),
153 description: None,
154 nodes,
155 edges: graph_edges,
156 }
157 }
158
159 #[test]
160 fn topological_order_linear() {
161 let g = make_graph(&[("a", "b"), ("b", "c")]);
162 let order = g.topological_order().unwrap();
163 let ids: Vec<&str> = order.iter().map(|n| n.id.as_str()).collect();
164 assert!(ids.iter().position(|&x| x == "a") < ids.iter().position(|&x| x == "b"));
166 assert!(ids.iter().position(|&x| x == "b") < ids.iter().position(|&x| x == "c"));
167 }
168}