Handle tool calls in workflow context using an iterative ReAct loop. When an agent node returns `tool_calls_required`, this function: 1. Executes the requested tools via Python 2. Sends tool results back to the LLM WITH tool definitions 3. If the LLM requests more tools, repeats from step 1 4. Exits when the LLM returns a final answer (no tool calls) or max_iterations is reached This enables mul
(
mut context: graphbit_core::types::WorkflowContext,
workflow: &graphbit_core::workflow::Workflow,
guardrail_enforcer: Option<&Enforcer>,
)
| 1795 | /// When `guardrail_enforcer` is `Some`, decodes tool-call parameters before execution only; |
| 1796 | /// after tool execution we do nothing (no encode of tool results). |
| 1797 | async fn handle_tool_calls_in_context( |
| 1798 | mut context: graphbit_core::types::WorkflowContext, |
| 1799 | workflow: &graphbit_core::workflow::Workflow, |
| 1800 | guardrail_enforcer: Option<&Enforcer>, |
| 1801 | ) -> Result<(graphbit_core::types::WorkflowContext, Vec<String>), graphbit_core::errors::GraphBitError> { |
| 1802 | use crate::workflow::node::execute_production_tool_calls; |
| 1803 | use graphbit_core::llm::{LlmMessage, LlmProvider, LlmRequest, LlmTool, LlmToolCall}; |
| 1804 | |
| 1805 | // Check each node output for tool_calls_required responses |
| 1806 | let node_outputs = context.node_outputs.clone(); |
| 1807 | let mut nodes_with_tool_calls: Vec<String> = Vec::new(); |
| 1808 | |
| 1809 | for (node_id, output) in node_outputs { |
| 1810 | if let Ok(response_obj) = serde_json::from_value::<serde_json::Value>(output.clone()) { |
| 1811 | if let Some(response_type) = response_obj.get("type").and_then(|v| v.as_str()) { |
| 1812 | if response_type == "tool_calls_required" { |
| 1813 | // Extract initial tool calls and original prompt |
| 1814 | if let (Some(initial_tool_calls), Some(original_prompt)) = ( |
| 1815 | response_obj.get("tool_calls"), |
| 1816 | response_obj.get("original_prompt").and_then(|v| v.as_str()), |
| 1817 | ) { |
| 1818 | // Get the node from the workflow |
| 1819 | let node = match workflow |
| 1820 | .graph |
| 1821 | .get_nodes() |
| 1822 | .iter() |
| 1823 | .find(|(id, _)| id.to_string() == node_id) |
| 1824 | .map(|(_, node)| node.clone()) |
| 1825 | { |
| 1826 | Some(n) => n, |
| 1827 | None => continue, |
| 1828 | }; |
| 1829 | |
| 1830 | // Only handle agent nodes |
| 1831 | if !matches!( |
| 1832 | node.node_type, |
| 1833 | graphbit_core::graph::NodeType::Agent { .. } |
| 1834 | ) { |
| 1835 | continue; |
| 1836 | } |
| 1837 | |
| 1838 | nodes_with_tool_calls.push(node.id.to_string()); |
| 1839 | |
| 1840 | // Get node name for metadata storage |
| 1841 | let node_name = workflow |
| 1842 | .graph |
| 1843 | .get_nodes() |
| 1844 | .iter() |
| 1845 | .find(|(id, _)| **id == node.id) |
| 1846 | .map(|(_, n)| n.name.clone()) |
| 1847 | .unwrap_or_else(|| "unknown".to_string()); |
| 1848 | |
| 1849 | // Extract available tool names for this node |
| 1850 | let node_tools = node |
| 1851 | .config |
| 1852 | .get("tools") |
| 1853 | .and_then(|v| v.as_array()) |
| 1854 | .map(|arr| { |
nothing calls this directly
no test coverage detected