use std::sync::Arc;
use serde_json::Value as JsonValue;
use dotenvy::dotenv;
use langgraph::prelude::*;
use langgraph_checkpoint::checkpoint::memory::InMemorySaver;
use langgraph_derive::{langgraph_state, tool};
use langgraph_prebuilt::{
invoke_llm, prepare_tools, tools_condition, BaseChatModel, Message, ToolError, ToolNode,
};
use langgraph_providers::openai::{OpenAIModel, OpenAIModelConfig};
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(|_| "mimo-v2.5-pro".to_string());
(api_key, api_base, model_name)
}
#[tool(
"human_assistance",
"Request assistance from a human. Use this when you need human review or correction of information."
)]
fn human_assistance(name: String, birthday: String) -> Result<String, ToolError> {
let human_response = interrupt(serde_json::json!({
"question": "Is this correct?",
"name": name,
"birthday": birthday,
}))?;
let (verified_name, verified_birthday, response) =
if let Some(correct) = human_response.get("correct").and_then(|v| v.as_str()) {
if correct.to_lowercase().starts_with('y') {
(name.clone(), birthday.clone(), "Correct".to_string())
} else {
let corrected_name = human_response
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(&name)
.to_string();
let corrected_birthday = human_response
.get("birthday")
.and_then(|v| v.as_str())
.unwrap_or(&birthday)
.to_string();
let msg = format!("Made a correction: {}", human_response);
(corrected_name, corrected_birthday, msg)
}
} else {
let corrected_name = human_response
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(&name)
.to_string();
let corrected_birthday = human_response
.get("birthday")
.and_then(|v| v.as_str())
.unwrap_or(&birthday)
.to_string();
let msg = format!("Made a correction: {}", human_response);
(corrected_name, corrected_birthday, msg)
};
let cmd = Command {
graph: None,
resume: None,
goto: vec![],
update: Some(serde_json::json!({
"name": verified_name,
"birthday": verified_birthday,
"messages": [Message::tool_result("__placeholder__", response)],
})),
};
Ok(serde_json::to_string(&cmd).unwrap_or_else(|_| "{}".to_string()))
}
#[tool("search", "Search for information about a topic.")]
fn search(query: String) -> Result<String, String> {
let results = match query.to_lowercase().as_str() {
q if q.contains("langgraph") => {
r#"[{"url": "https://blog.langchain.dev/langgraph-cloud/", "content": "LangGraph Platform was announced on June 27, 2024. LangGraph had been in development before this."}]"#
}
q if q.contains("rust") => {
r#"[{"url": "https://www.rust-lang.org/", "content": "Rust 1.0 was released on May 15, 2015."}]"#
}
_ => r#"[{"url": "https://example.com", "content": "No relevant results found."}]"#,
};
Ok(results.to_string())
}
#[langgraph_state]
#[derive(Debug)]
struct GraphState {
#[channel(messages)]
messages: Vec<Message>,
name: String,
birthday: String,
}
const SYSTEM_PROMPT: &str =
"You are a helpful assistant that researches information about entities. \
Use the search tool to find information, then use the human_assistance tool \
to get human review of your findings. When calling human_assistance, \
provide your suggested name and birthday in the tool arguments.";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("========================================");
println!(" Custom State + HITL Demo");
println!("========================================\n");
let prepared = prepare_tools(vec![
Arc::new(HumanAssistance::new()),
Arc::new(Search::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 checkpointer = Arc::new(InMemorySaver::new());
let app = graph.compile_builder().checkpointer(checkpointer).build()?;
let mut config = RunnableConfig::new();
config.insert(
"configurable".to_string(),
serde_json::json!({
"thread_id": "custom-state-demo-1"
}),
);
println!("--- Step 1: Initial query ---\n");
println!(
"User: Can you look up when LangGraph was released? \
When you have the answer, use the human_assistance tool for review.\n"
);
let input = serde_json::json!({
"messages": [{
"type": "human",
"content": "Can you look up when LangGraph was released? \
When you have the answer, use the human_assistance tool for review."
}]
});
let result = app.ainvoke(&input, &config).await?;
println!("--- Graph paused (interrupt occurred) ---\n");
print_messages(&result);
println!("\n--- Step 2: Check state with get_state ---\n");
let snapshot = app.get_state(&config)?;
println!("snapshot.next = {:?}", snapshot.next);
println!(
"snapshot.values.name = {}",
snapshot
.values
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("(empty)")
);
println!(
"snapshot.values.birthday = {}",
snapshot
.values
.get("birthday")
.and_then(|v| v.as_str())
.unwrap_or("(empty)")
);
println!("snapshot.interrupts = {:?}", snapshot.interrupts);
println!("\n--- Step 3: Resume with human correction ---\n");
println!("Human: The name is 'LangGraph' and the birthday is 'Jan 17, 2024'\n");
let resume_command = Command::resume(serde_json::json!({
"name": "LangGraph",
"birthday": "Jan 17, 2024",
}));
let result = app
.ainvoke(&serde_json::to_value(&resume_command)?, &config)
.await?;
println!("--- Result after resume ---\n");
print_messages(&result);
println!("\n--- Step 4: Verify custom state ---\n");
let snapshot = app.get_state(&config)?;
let name = snapshot
.values
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("(empty)");
let birthday = snapshot
.values
.get("birthday")
.and_then(|v| v.as_str())
.unwrap_or("(empty)");
println!("{{'name': '{}', 'birthday': '{}'}}", name, birthday);
println!("\n--- Step 5: Manually update state ---\n");
app.update_state(
&config,
&serde_json::json!({
"name": "LangGraph (library)"
}),
)?;
let snapshot = app.get_state(&config)?;
let name = snapshot
.values
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("(empty)");
let birthday = snapshot
.values
.get("birthday")
.and_then(|v| v.as_str())
.unwrap_or("(empty)");
println!("After update_state:");
println!("{{'name': '{}', 'birthday': '{}'}}", name, birthday);
println!("\n========================================");
println!(" Demo completed!");
println!("========================================");
Ok(())
}
fn print_messages(output: &JsonValue) {
if let Some(messages) = output.get("messages").and_then(|m| m.as_array()) {
for msg in messages {
if let Ok(m) = serde_json::from_value::<Message>(msg.clone()) {
println!("{}", m);
}
}
}
}