use dotenvy::dotenv;
use langgraph::prelude::*;
use langgraph::sqlite::SqliteSaver;
use langgraph::{langgraph_state, tool};
use langgraph::prebuilt::{
invoke_llm, prepare_tools, tools_condition, BaseChatModel, Message, ToolError, ToolNode,
};
use langgraph::providers::openai::{OpenAIModel, OpenAIModelConfig};
use serde_json::Value as JsonValue;
use std::sync::Arc;
fn load_openai_config() -> (String, Option<String>, String) {
dotenv().ok();
let api_key =
std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set in .env or environment");
let api_base = std::env::var("OPENAI_API_BASE").ok();
let model_name = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "Pro/deepseek-ai/DeepSeek-V3.2".to_string());
(api_key, api_base, model_name)
}
#[tool(
"human_assistance",
"Request assistance from a human. Use this when you need human expertise or approval."
)]
fn human_assistance(query: String) -> Result<String, ToolError> {
let human_response = interrupt(serde_json::json!({
"query": query,
"message": "Please provide your response to the human assistance request."
}))?;
if let Some(data) = human_response.get("data").and_then(|v| v.as_str()) {
Ok(data.to_string())
} else {
Ok(human_response.to_string())
}
}
#[tool("get_weather", "Get the current weather for a given location.")]
fn get_weather(location: String) -> Result<String, String> {
Ok(format!(
"Weather for {}: sunny, 22°C, humidity 45%, wind 10km/h",
location
))
}
#[langgraph_state]
#[derive(Debug)]
struct GraphState {
#[channel(messages)]
messages: Vec<Message>,
}
const SYSTEM_PROMPT: &str = "You are a helpful assistant with access to tools. \
Use the human_assistance tool when the user needs expert guidance. \
IMPORTANT: After receiving the result from the human_assistance tool, you MUST immediately synthesize the expert's advice and present it to the user. Do NOT ask clarifying questions or call the human_assistance tool multiple times for the same user request. \
Use the get_weather tool for weather queries.";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("========================================");
println!(" Robust Human-in-the-Loop Demo");
println!("========================================\n");
let prepared = prepare_tools(vec![
Arc::new(HumanAssistance::new()),
Arc::new(GetWeather::new()),
]);
let (api_key, api_base, model_name) = load_openai_config();
let model = OpenAIModel::new(OpenAIModelConfig {
model: model_name,
api_key,
api_base,
temperature: Some(0.7),
..Default::default()
});
let model_with_tools: Arc<dyn BaseChatModel> = model.bind_tools(prepared.tool_defs).into();
let channels = GraphState::create_channels();
let mut graph = StateGraph::new(channels);
let model_clone = model_with_tools.clone();
graph.add_node(
"chatbot",
move |input: JsonValue, _config: RunnableConfig| {
let model = model_clone.clone();
async move { invoke_llm(model.as_ref(), &input, SYSTEM_PROMPT) }
},
)?;
let tools_node: Arc<dyn Runnable> = Arc::new(ToolNode::new(prepared.tools.clone()));
graph.add_node("tools", tools_node)?;
graph.add_edge(START, "chatbot")?;
conditional_edges!(graph, "chatbot", tools_condition, "tools" => "tools", END => END)?;
graph.add_edge("tools", "chatbot")?;
let saver = SqliteSaver::from_conn_string("sqlite:checkpoints.db").await?;
saver.setup().await?;
let checkpointer = Arc::new(saver);
let app = graph.compile_builder().checkpointer(checkpointer).build()?;
let thread_id = uuid::Uuid::new_v4().to_string();
let mut config = RunnableConfig::new();
config.insert(
"configurable".to_string(),
serde_json::json!({
"thread_id": thread_id
}),
);
let mut current_input = serde_json::json!({
"messages": [{
"type": "human",
"content": "I need some expert guidance for building an AI agent. Could you request assistance for me?"
}]
});
let mut step_count = 1;
let mut seen_messages = 0;
loop {
println!("----------------------------------------");
println!("▶ Execution Step {}", step_count);
println!("----------------------------------------");
let result = app.ainvoke(¤t_input, &config).await?;
let messages_array = result.get("messages").and_then(|m| m.as_array()).unwrap();
for msg_val in messages_array.iter().skip(seen_messages) {
if let Ok(m) = serde_json::from_value::<Message>(msg_val.clone()) {
println!("{}", m);
}
}
seen_messages = messages_array.len();
let last_msg_val = messages_array.last().unwrap();
let last_msg: Message = serde_json::from_value(last_msg_val.clone())?;
match last_msg {
Message::Ai { tool_calls, .. } => {
if tool_calls.is_empty() {
let has_used_tool = messages_array.iter().any(|m| {
matches!(serde_json::from_value::<Message>(m.clone()), Ok(Message::Tool { .. }))
});
if has_used_tool {
println!("\n[System 🤖] -> Expert advice synthesized and presented to the user. Demo completed! 🎉");
break;
} else {
println!("\n[System 🤖] -> The model asked a clarifying question without calling the tool.");
println!("[System 🤖] -> Automatically simulating the user to provide a follow-up answer...");
current_input = serde_json::json!({
"messages": [{
"type": "human",
"content": "I am building a customer support chatbot in Rust. I just need you to ping the human expert for their architectural recommendation. Please invoke the tool now."
}]
});
}
} else {
println!("\n[System 🤖] -> The model invoked a tool! Graph execution is paused (Interrupt).");
println!("[System 🤖] -> Automatically simulating the human expert to inject the response via Command::resume...");
let resume_command = Command::resume(serde_json::json!({
"data": "Expert advice: Use LangGraph-Rust for state machine orchestration and keep your tools modular!"
}));
current_input = serde_json::to_value(&resume_command)?;
}
},
Message::Tool { .. } => {
current_input = serde_json::json!(null);
},
_ => {
println!("\n[System 🤖] -> Unexpected graph state encountered. Terminating.");
break;
}
}
step_count += 1;
if step_count > 10 {
println!("\n[System 🤖] -> Maximum iteration limit (10) reached. Terminating to prevent infinite loop.");
break;
}
println!("\n"); }
Ok(())
}