Get complete workflow execution metadata Returns the full workflow-level schema containing: - workflow_id, workflow_name - start_time, end_time, duration_ms - user_input, final_output (from first/last nodes) - workflow_state: completed/failed/cancelled/paused - nodes: array of per-node metadata objects (each with executions array) - total_usage: aggregated token usage across all nodes - total_too
(&self, py: Python<'_>)
| 225 | /// # Returns |
| 226 | /// Dictionary with the complete workflow-level metadata |
| 227 | fn get_all_node_response_metadata(&self, py: Python<'_>) -> PyResult<PyObject> { |
| 228 | // Collect node metadata entries (by node ID only, skip name duplicates) |
| 229 | let mut nodes: Vec<serde_json::Value> = Vec::new(); |
| 230 | let mut seen_node_ids: std::collections::HashSet<String> = std::collections::HashSet::new(); |
| 231 | |
| 232 | for (k, v) in self.inner.metadata.iter() { |
| 233 | if let Some(node_id) = k.strip_prefix("node_response_") { |
| 234 | // Skip if this is a name-based duplicate (node names are typically not UUIDs) |
| 235 | // We include if the node_id is a UUID format or if the value has a node_id field matching |
| 236 | if let Some(stored_node_id) = v.get("node_id").and_then(|v| v.as_str()) { |
| 237 | if seen_node_ids.contains(stored_node_id) { |
| 238 | continue; |
| 239 | } |
| 240 | seen_node_ids.insert(stored_node_id.to_string()); |
| 241 | } else if seen_node_ids.contains(node_id) { |
| 242 | continue; |
| 243 | } else { |
| 244 | seen_node_ids.insert(node_id.to_string()); |
| 245 | } |
| 246 | // Sanitize node: expose parameters_masked as parameters and output_masked as output |
| 247 | // so the returned metadata does not leak real PII (executions, tool_calls, initial_response.tool_calls). |
| 248 | let mut node = v.clone(); |
| 249 | sanitize_node_metadata(&mut node); |
| 250 | nodes.push(node); |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | // Sort nodes chronologically by start_time |
| 255 | nodes.sort_by(|a, b| { |
| 256 | let a_start = a.get("start_time").and_then(|v| v.as_str()).unwrap_or(""); |
| 257 | let b_start = b.get("start_time").and_then(|v| v.as_str()).unwrap_or(""); |
| 258 | a_start.cmp(b_start) |
| 259 | }); |
| 260 | |
| 261 | // Aggregate total_usage and total_tool_calls across all nodes |
| 262 | let mut total_prompt_tokens: u64 = 0; |
| 263 | let mut total_completion_tokens: u64 = 0; |
| 264 | let mut total_tokens: u64 = 0; |
| 265 | let mut total_cached_tokens: u64 = 0; |
| 266 | let mut total_cache_creation_tokens: u64 = 0; |
| 267 | let mut total_tool_calls: u64 = 0; |
| 268 | |
| 269 | for node in &nodes { |
| 270 | if let Some(usage) = node.get("total_usage") { |
| 271 | total_prompt_tokens += usage |
| 272 | .get("prompt_tokens") |
| 273 | .and_then(|v| v.as_u64()) |
| 274 | .unwrap_or(0); |
| 275 | total_completion_tokens += usage |
| 276 | .get("completion_tokens") |
| 277 | .and_then(|v| v.as_u64()) |
| 278 | .unwrap_or(0); |
| 279 | total_tokens += usage |
| 280 | .get("total_tokens") |
| 281 | .and_then(|v| v.as_u64()) |
| 282 | .unwrap_or(0); |
| 283 | if let Some(details) = usage.get("prompt_tokens_details") { |
| 284 | total_cached_tokens += details |
no test coverage detected