feat: llm graph nodes support skills
This commit is contained in:
@@ -119,6 +119,7 @@ impl GraphValidator {
|
||||
self.validate_approval_routes(graph, &mut result);
|
||||
self.validate_rag_nodes(graph, &mut result);
|
||||
self.validate_llm_nodes(graph, &mut result);
|
||||
self.validate_llm_skills(graph, &mut result);
|
||||
self.validate_max_concurrency(graph, &mut result);
|
||||
self.validate_map_branches(graph, &mut result);
|
||||
self.validate_parallel_user_interaction(graph, &mut result);
|
||||
@@ -189,6 +190,39 @@ impl GraphValidator {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_llm_skills(&self, graph: &Graph, result: &mut ValidationResult) {
|
||||
for (node_id, node) in &graph.nodes {
|
||||
let NodeType::Llm(llm) = &node.node_type else {
|
||||
continue;
|
||||
};
|
||||
let Some(node_skills) = &llm.enabled_skills else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for name in node_skills {
|
||||
if name.trim().is_empty() {
|
||||
result.error(ValidationError::with_node(
|
||||
node_id,
|
||||
"llm node 'enabled_skills' contains an empty skill name",
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if let Some(graph_skills) = &graph.enabled_skills
|
||||
&& !graph_skills.iter().any(|g| g == name)
|
||||
{
|
||||
result.error(ValidationError::with_node(
|
||||
node_id,
|
||||
format!(
|
||||
"llm node 'enabled_skills' references '{name}' which is not in \
|
||||
graph-level 'enabled_skills' ({})",
|
||||
graph_skills.join(", ")
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_node_references(&self, graph: &Graph, result: &mut ValidationResult) {
|
||||
for (node_id, node) in &graph.nodes {
|
||||
for (target, label) in declared_targets(node) {
|
||||
@@ -847,6 +881,8 @@ mod tests {
|
||||
top_p: None,
|
||||
global_tools: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
skills_enabled: None,
|
||||
enabled_skills: None,
|
||||
conversation_starters: Vec::new(),
|
||||
variables: Vec::new(),
|
||||
settings: GraphSettings::default(),
|
||||
@@ -946,6 +982,8 @@ mod tests {
|
||||
state_updates: None,
|
||||
output_schema: None,
|
||||
timeout: None,
|
||||
skills_enabled: None,
|
||||
enabled_skills: None,
|
||||
}),
|
||||
next: next.map(NextTargets::from),
|
||||
}
|
||||
@@ -967,6 +1005,99 @@ mod tests {
|
||||
assert!(result.errors.iter().any(|e| e.message.contains("ghost")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_node_skill_in_graph_set_passes() {
|
||||
let mut graph = graph_with(
|
||||
vec![("l", llm_node("l", None, Some("end"))), ("end", end_node("end"))],
|
||||
"l",
|
||||
);
|
||||
graph.enabled_skills = Some(vec!["code-review".into(), "git-master".into()]);
|
||||
if let NodeType::Llm(ref mut n) = graph.nodes.get_mut("l").unwrap().node_type {
|
||||
n.enabled_skills = Some(vec!["code-review".into()]);
|
||||
}
|
||||
|
||||
let result = validator().validate(&graph);
|
||||
|
||||
assert!(
|
||||
!result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| e.message.contains("enabled_skills")),
|
||||
"unexpected enabled_skills error: {:?}",
|
||||
result.errors
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_node_skill_not_in_graph_set_errors() {
|
||||
let mut graph = graph_with(
|
||||
vec![("l", llm_node("l", None, Some("end"))), ("end", end_node("end"))],
|
||||
"l",
|
||||
);
|
||||
graph.enabled_skills = Some(vec!["code-review".into()]);
|
||||
if let NodeType::Llm(ref mut n) = graph.nodes.get_mut("l").unwrap().node_type {
|
||||
n.enabled_skills = Some(vec!["git-master".into()]);
|
||||
}
|
||||
|
||||
let result = validator().validate(&graph);
|
||||
|
||||
assert!(!result.is_valid());
|
||||
assert!(
|
||||
result.errors.iter().any(|e| e
|
||||
.message
|
||||
.contains("'git-master'")
|
||||
&& e.message.contains("graph-level")),
|
||||
"expected git-master subset error, got: {:?}",
|
||||
result.errors
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_node_empty_skill_name_errors() {
|
||||
let mut graph = graph_with(
|
||||
vec![("l", llm_node("l", None, Some("end"))), ("end", end_node("end"))],
|
||||
"l",
|
||||
);
|
||||
graph.enabled_skills = Some(vec!["code-review".into()]);
|
||||
if let NodeType::Llm(ref mut n) = graph.nodes.get_mut("l").unwrap().node_type {
|
||||
n.enabled_skills = Some(vec!["".into()]);
|
||||
}
|
||||
|
||||
let result = validator().validate(&graph);
|
||||
|
||||
assert!(!result.is_valid());
|
||||
assert!(
|
||||
result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| e.message.contains("empty skill name")),
|
||||
"expected empty-skill-name error, got: {:?}",
|
||||
result.errors
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_node_skill_when_no_graph_set_is_permitted_by_validator() {
|
||||
let mut graph = graph_with(
|
||||
vec![("l", llm_node("l", None, Some("end"))), ("end", end_node("end"))],
|
||||
"l",
|
||||
);
|
||||
if let NodeType::Llm(ref mut n) = graph.nodes.get_mut("l").unwrap().node_type {
|
||||
n.enabled_skills = Some(vec!["anything".into()]);
|
||||
}
|
||||
|
||||
let result = validator().validate(&graph);
|
||||
|
||||
assert!(
|
||||
!result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| e.message.contains("enabled_skills")),
|
||||
"validator should not block when graph.enabled_skills is None: {:?}",
|
||||
result.errors
|
||||
);
|
||||
}
|
||||
|
||||
fn agent_ctx(tools: &[&str], mcp: &[&str]) -> AgentValidationContext {
|
||||
AgentValidationContext {
|
||||
tool_names: tools.iter().map(|s| s.to_string()).collect(),
|
||||
|
||||
Reference in New Issue
Block a user