use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::AgentFile;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentNode {
pub id: String,
pub role: String,
pub agent: AgentFile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
pub from: String,
pub to: String,
pub output_field: Option<String>,
pub input_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentGraph {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub nodes: Vec<AgentNode>,
pub edges: Vec<GraphEdge>,
}
impl AgentGraph {
pub fn topological_order(&self) -> Result<Vec<&AgentNode>, String> {
use std::collections::{HashMap, VecDeque};
let mut in_degree: HashMap<&str, usize> = HashMap::new();
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
for node in &self.nodes {
in_degree.entry(node.id.as_str()).or_insert(0);
adj.entry(node.id.as_str()).or_default();
}
for edge in &self.edges {
*in_degree.entry(edge.to.as_str()).or_insert(0) += 1;
adj.entry(edge.from.as_str())
.or_default()
.push(edge.to.as_str());
}
let mut queue: VecDeque<&str> = in_degree
.iter()
.filter(|(_, &d)| d == 0)
.map(|(&id, _)| id)
.collect();
let node_by_id: HashMap<&str, &AgentNode> =
self.nodes.iter().map(|n| (n.id.as_str(), n)).collect();
let mut result = Vec::new();
while let Some(id) = queue.pop_front() {
if let Some(node) = node_by_id.get(id) {
result.push(*node);
}
if let Some(neighbors) = adj.get(id) {
for &neighbor in neighbors {
let deg = in_degree.get_mut(neighbor).unwrap();
*deg -= 1;
if *deg == 0 {
queue.push_back(neighbor);
}
}
}
}
if result.len() != self.nodes.len() {
return Err("AgentGraph contains a cycle".to_string());
}
Ok(result)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiAgentScorecard {
pub graph_id: Uuid,
pub node_scores: std::collections::HashMap<String, f64>,
pub composite_score: f64,
pub pass_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_graph(edges: &[(&str, &str)]) -> AgentGraph {
use crate::{AgentFile, ModelConfig, ModelProvider};
let node_ids: std::collections::HashSet<&str> =
edges.iter().flat_map(|(a, b)| [*a, *b]).collect();
let nodes = node_ids
.into_iter()
.map(|id| AgentNode {
id: id.to_string(),
role: id.to_string(),
agent: AgentFile {
agentforge_schema_version: "1".into(),
name: id.to_string(),
version: "0.1.0".into(),
model: ModelConfig {
provider: ModelProvider::Openai,
model_id: "gpt-4o".into(),
temperature: None,
max_tokens: None,
top_p: None,
},
system_prompt: String::new(),
tools: vec![],
output_schema: None,
constraints: vec![],
eval_hints: None,
metadata: None,
},
})
.collect();
let graph_edges = edges
.iter()
.map(|(from, to)| GraphEdge {
from: from.to_string(),
to: to.to_string(),
output_field: None,
input_key: None,
})
.collect();
AgentGraph {
id: Uuid::new_v4(),
name: "test".into(),
description: None,
nodes,
edges: graph_edges,
}
}
#[test]
fn topological_order_linear() {
let g = make_graph(&[("a", "b"), ("b", "c")]);
let order = g.topological_order().unwrap();
let ids: Vec<&str> = order.iter().map(|n| n.id.as_str()).collect();
assert!(ids.iter().position(|&x| x == "a") < ids.iter().position(|&x| x == "b"));
assert!(ids.iter().position(|&x| x == "b") < ids.iter().position(|&x| x == "c"));
}
}