use std::sync::Arc;
use adk_core::{AdkError, Tool, ToolContext};
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::error::GraphError;
use crate::graph::CompiledGraph;
use crate::interrupt::GraphInterruptPayload;
use crate::node::ExecutionConfig;
enum Target {
Node(String),
Graph,
}
pub struct NodeTool {
graph: Arc<CompiledGraph>,
target: Target,
name: String,
description: String,
parameters_schema: Option<Value>,
}
impl NodeTool {
pub fn for_node(graph: Arc<CompiledGraph>, node: impl Into<String>) -> Self {
let node = node.into();
Self {
graph,
name: node.clone(),
description: format!("Runs the '{node}' graph node."),
target: Target::Node(node),
parameters_schema: None,
}
}
pub fn for_graph(graph: Arc<CompiledGraph>) -> Self {
Self {
graph,
target: Target::Graph,
name: "run_graph".to_string(),
description: "Runs a graph workflow to completion.".to_string(),
parameters_schema: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_parameters_schema(mut self, schema: Value) -> Self {
self.parameters_schema = Some(schema);
self
}
fn derived_schema(&self) -> Value {
let mut properties = serde_json::Map::new();
for channel in self.graph.state_channels() {
properties.insert(channel, json!({ "description": "A graph state channel." }));
}
json!({ "type": "object", "properties": properties })
}
}
#[async_trait]
impl Tool for NodeTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn parameters_schema(&self) -> Option<Value> {
if let Some(schema) = &self.parameters_schema {
return Some(schema.clone());
}
match self.target {
Target::Node(_) => Some(json!({ "type": "object" })),
Target::Graph => Some(self.derived_schema()),
}
}
fn is_long_running(&self) -> bool {
true
}
async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> adk_core::Result<Value> {
let mut input = crate::state::State::new();
if let Value::Object(map) = args {
for (key, value) in map {
input.insert(key, value);
}
}
let config = ExecutionConfig::new(ctx.session_id());
match &self.target {
Target::Node(node) => {
let node_impl = self
.graph
.node(node)
.ok_or_else(|| AdkError::tool(format!("no graph node named '{node}'")))?;
let node_ctx = crate::node::NodeContext::new(input, config, 0);
let output = node_impl
.execute(&node_ctx)
.await
.map_err(|error| AdkError::tool(error.to_string()))?;
if let Some(interrupt) = output.interrupt {
return Ok(interrupt_value(&interrupt, ctx.session_id(), ""));
}
Ok(Value::Object(output.updates.into_iter().collect()))
}
Target::Graph => match self.graph.invoke(input, config).await {
Ok(state) => Ok(Value::Object(state.into_iter().collect())),
Err(GraphError::Interrupted(interrupted)) => Ok(interrupt_value(
&interrupted.interrupt,
&interrupted.thread_id,
&interrupted.checkpoint_id,
)),
Err(error) => Err(AdkError::tool(error.to_string())),
},
}
}
}
fn interrupt_value(
interrupt: &crate::interrupt::Interrupt,
thread_id: &str,
checkpoint_id: &str,
) -> Value {
let payload = GraphInterruptPayload::new(interrupt, thread_id, checkpoint_id);
json!({
"status": "interrupted",
"interrupt": serde_json::to_value(&payload).unwrap_or(Value::Null),
})
}