refactor: migrated the next_node and apply_state_updates logic for LLM nodes into the LlmExecutor
This commit is contained in:
+3
-32
@@ -7,11 +7,11 @@
|
|||||||
//! template as the graph's return value.
|
//! template as the graph's return value.
|
||||||
|
|
||||||
use super::agent::AgentNodeExecutor;
|
use super::agent::AgentNodeExecutor;
|
||||||
use super::llm::{self, LlmNodeExecutor};
|
use super::llm::LlmNodeExecutor;
|
||||||
use super::parser::GraphParser;
|
use super::parser::GraphParser;
|
||||||
use super::script::ScriptExecutor;
|
use super::script::ScriptExecutor;
|
||||||
use super::state::StateManager;
|
use super::state::StateManager;
|
||||||
use super::types::{EndNode, Graph, LlmNode, Node, NodeType};
|
use super::types::{EndNode, Graph, Node, NodeType};
|
||||||
use super::user_interaction::{ApprovalNodeExecutor, InputNodeExecutor};
|
use super::user_interaction::{ApprovalNodeExecutor, InputNodeExecutor};
|
||||||
use super::validator::GraphValidator;
|
use super::validator::GraphValidator;
|
||||||
use crate::config::RequestContext;
|
use crate::config::RequestContext;
|
||||||
@@ -220,42 +220,13 @@ async fn step(
|
|||||||
Ok(StepResult::Continue(next))
|
Ok(StepResult::Continue(next))
|
||||||
}
|
}
|
||||||
NodeType::Llm(llm_node) => {
|
NodeType::Llm(llm_node) => {
|
||||||
let result = LlmNodeExecutor::execute(llm_node, state, ctx).await;
|
let next = LlmNodeExecutor::execute(llm_node, node.next.as_deref(), state, ctx).await?;
|
||||||
let (output, failed) = match result {
|
|
||||||
Ok(out) => (out, false),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("[graph:{}] llm node '{}' failed: {e}", graph_name, current);
|
|
||||||
(format!("LLM node failed: {e}"), true)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
apply_state_updates_with_llm_output(llm_node, state, &output);
|
|
||||||
let next = next_for_llm_node(node, failed, llm_node.fallback.as_deref())?;
|
|
||||||
Ok(StepResult::Continue(next))
|
Ok(StepResult::Continue(next))
|
||||||
}
|
}
|
||||||
NodeType::End(end_node) => Ok(StepResult::End(resolve_end_output(end_node, state))),
|
NodeType::End(end_node) => Ok(StepResult::End(resolve_end_output(end_node, state))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_for_llm_node(node: &Node, failed: bool, fallback: Option<&str>) -> Result<String> {
|
|
||||||
if failed && let Some(fb) = fallback {
|
|
||||||
return Ok(fb.to_string());
|
|
||||||
}
|
|
||||||
node.next.clone().ok_or_else(|| {
|
|
||||||
anyhow!(
|
|
||||||
"llm node '{}' has no `next` set; llm nodes need static routing",
|
|
||||||
node.id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_state_updates_with_llm_output(
|
|
||||||
node: &super::types::LlmNode,
|
|
||||||
state: &mut StateManager,
|
|
||||||
output: &str,
|
|
||||||
) {
|
|
||||||
crate::graph::llm::apply_state_updates_with_output(node, state, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply the end node's `state_updates`, then interpolate its `output`
|
/// Apply the end node's `state_updates`, then interpolate its `output`
|
||||||
/// template against the resulting state. Both use lenient interpolation
|
/// template against the resulting state. Both use lenient interpolation
|
||||||
/// so the graph still produces a result even when some keys are absent.
|
/// so the graph still produces a result even when some keys are absent.
|
||||||
|
|||||||
+61
-32
@@ -13,7 +13,7 @@ use super::state::StateManager;
|
|||||||
use super::types::LlmNode;
|
use super::types::LlmNode;
|
||||||
use crate::config::RequestContext;
|
use crate::config::RequestContext;
|
||||||
use crate::utils::dimmed_text;
|
use crate::utils::dimmed_text;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
const OUTPUT_KEY: &str = "output";
|
const OUTPUT_KEY: &str = "output";
|
||||||
@@ -21,48 +21,77 @@ const OUTPUT_KEY: &str = "output";
|
|||||||
pub struct LlmNodeExecutor;
|
pub struct LlmNodeExecutor;
|
||||||
|
|
||||||
impl LlmNodeExecutor {
|
impl LlmNodeExecutor {
|
||||||
/// Interpolate the node's templates, run the LLM call, then return
|
/// Run the LLM call and resolve routing. Returns the next node ID
|
||||||
/// the model's final response. State updates are applied by the
|
/// to visit. Handles the tolerant-fail contract internally:
|
||||||
/// graph executor (which knows whether to use the success path or
|
/// success → `node_next`; failure with `fallback` → `fallback`;
|
||||||
/// the failure path).
|
/// failure without `fallback` → `node_next`. State updates are
|
||||||
|
/// applied in both success and failure paths, with `{{output}}`
|
||||||
|
/// resolving to the LLM's response or an error description.
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
node: &LlmNode,
|
node: &LlmNode,
|
||||||
|
node_next: Option<&str>,
|
||||||
state_manager: &mut StateManager,
|
state_manager: &mut StateManager,
|
||||||
_parent_ctx: &mut RequestContext,
|
parent_ctx: &mut RequestContext,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let _instructions = state_manager
|
let result = run(node, state_manager, parent_ctx).await;
|
||||||
.interpolate(&node.instructions)
|
let (output, failed) = match result {
|
||||||
.context("Failed to interpolate llm node instructions")?;
|
Ok(out) => (out, false),
|
||||||
let _prompt = state_manager
|
Err(e) => {
|
||||||
.interpolate(&node.prompt)
|
warn!("llm node failed: {e}");
|
||||||
.context("Failed to interpolate llm node prompt")?;
|
(format!("LLM node failed: {e}"), true)
|
||||||
|
}
|
||||||
eprintln!(
|
};
|
||||||
"{}",
|
apply_state_updates_with_output(node, state_manager, &output);
|
||||||
dimmed_text(&format!(
|
next_for_llm_node(node_next, failed, node.fallback.as_deref())
|
||||||
"▸ llm call: model={} tools={}",
|
|
||||||
node.model.as_deref().unwrap_or("<active>"),
|
|
||||||
describe_tools_filter(node.tools.as_deref())
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
bail!(
|
|
||||||
"llm node execution body not yet implemented — see \
|
|
||||||
docs/implementation/graph-agents/10.5-llm-nodes.md \
|
|
||||||
(steps 3 & 5 of the implementation order)"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
node: &LlmNode,
|
||||||
|
state_manager: &mut StateManager,
|
||||||
|
_parent_ctx: &mut RequestContext,
|
||||||
|
) -> Result<String> {
|
||||||
|
let _instructions = state_manager
|
||||||
|
.interpolate(&node.instructions)
|
||||||
|
.context("Failed to interpolate llm node instructions")?;
|
||||||
|
let _prompt = state_manager
|
||||||
|
.interpolate(&node.prompt)
|
||||||
|
.context("Failed to interpolate llm node prompt")?;
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
dimmed_text(&format!(
|
||||||
|
"▸ llm call: model={} tools={}",
|
||||||
|
node.model.as_deref().unwrap_or("<active>"),
|
||||||
|
describe_tools_filter(node.tools.as_deref())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
bail!(
|
||||||
|
"llm node execution body not yet implemented — see \
|
||||||
|
docs/implementation/graph-agents/10.5-llm-nodes.md \
|
||||||
|
(steps 3 & 5 of the implementation order)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_for_llm_node(
|
||||||
|
node_next: Option<&str>,
|
||||||
|
failed: bool,
|
||||||
|
fallback: Option<&str>,
|
||||||
|
) -> Result<String> {
|
||||||
|
if failed && let Some(fb) = fallback {
|
||||||
|
return Ok(fb.to_string());
|
||||||
|
}
|
||||||
|
node_next
|
||||||
|
.map(String::from)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("llm node has no `next` set; llm nodes need static routing"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Expose the LLM call's final output as `{{output}}` for the duration
|
/// Expose the LLM call's final output as `{{output}}` for the duration
|
||||||
/// of `state_updates` evaluation, then restore the prior value (or set
|
/// of `state_updates` evaluation, then restore the prior value (or set
|
||||||
/// it to `Null` if there wasn't one). Same pattern as
|
/// it to `Null` if there wasn't one). Same pattern as
|
||||||
/// `AgentNodeExecutor`'s `{{output}}` scoping.
|
/// `AgentNodeExecutor`'s `{{output}}` scoping.
|
||||||
pub fn apply_state_updates_with_output(
|
fn apply_state_updates_with_output(node: &LlmNode, state_manager: &mut StateManager, output: &str) {
|
||||||
node: &LlmNode,
|
|
||||||
state_manager: &mut StateManager,
|
|
||||||
output: &str,
|
|
||||||
) {
|
|
||||||
let Some(updates) = &node.state_updates else {
|
let Some(updates) = &node.state_updates else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user