Agents de graphe
Crรฉez des workflows complexes et avec รฉtat en utilisant une orchestration de style LangGraph avec une intรฉgration native de ADK-Rust.
Vue dโensemble
GraphAgent vous permet de dรฉfinir des workflows sous forme de graphes orientรฉs avec des nลuds et des arรชtes, en prenant en charge :
- AgentNode : Encapsulez des agents LLM comme nลuds du graphe avec des mappeurs dโentrรฉe/sortie personnalisรฉs
- Workflows cycliques : Prise en charge native des boucles et du raisonnement itรฉratif (pattern ReAct)
- Routage conditionnel : Routage dynamique des arรชtes en fonction de lโรฉtat
- Gestion de lโรฉtat : รtat typรฉ avec des rรฉducteurs (รฉcrasement, ajout, somme, personnalisรฉ)
- Checkpointing : รtat persistant pour la tolรฉrance aux pannes et lโintervention humaine dans la boucle
- Streaming : Plusieurs modes de flux (values, updates, messages, debug)
Le crate adk-graph fournit une orchestration de workflow de style LangGraph pour construire des workflows dโagents complexes et avec รฉtat. Il apporte des capacitรฉs de workflow basรฉes sur des graphes ร lโรฉcosystรจme ADK-Rust tout en conservant une compatibilitรฉ totale avec le systรจme dโagents de ADK.
Avantages clรฉs :
- Conception visuelle du workflow : Dรฉfinissez une logique complexe sous forme de graphes intuitifs de nลuds et dโarรชtes
- Exรฉcution parallรจle : Plusieurs nลuds peuvent sโexรฉcuter simultanรฉment pour de meilleures performances
- Persistance de lโรฉtat : Checkpointing intรฉgrรฉ pour la tolรฉrance aux pannes et lโintervention humaine dans la boucle
- Intรฉgration LLM : Prise en charge native de lโencapsulation dโagents ADK en tant que nลuds du graphe
- Routage flexible : Arรชtes statiques, routage conditionnel et prise de dรฉcision dynamique
Ce que vous allez construire
Dans ce guide, vous allez crรฉer un pipeline de traitement de texte qui exรฉcute la traduction et le rรฉsumรฉ en parallรจle :
โโโโโโโโโโโโโโโโโโโโโโโ
User Input โ โ
โโโโโโโโโโโโโโโโโถ โ START โ
โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ TRANSLATOR โ โ SUMMARIZER โ
โ โ โ โ
โ ๐ซ๐ท French โ โ ๐ One sentence โ
โ Translation โ โ Summary โ
โโโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโโฌโโโโโโโโโ
โ โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ COMBINE โ
โ โ
โ ๐ Merge Results โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ END โ
โ โ
โ โ
Complete โ
โโโโโโโโโโโโโโโโโโโโโโโ
Concepts clรฉs :
- Nลuds - Unitรฉs de traitement qui effectuent le travail (agents LLM, fonctions ou logique personnalisรฉe)
- Arรชtes - Flux de contrรดle entre les nลuds (connexions statiques ou routage conditionnel)
- รtat - Donnรฉes partagรฉes qui circulent dans le graphe et persistent entre les nลuds
- Exรฉcution parallรจle - Plusieurs nลuds peuvent sโexรฉcuter simultanรฉment pour de meilleures performances
Comprendre les composants principaux
๐ง Nลuds : les travailleurs Les nลuds sont lโendroit oรน le travail rรฉel se produit. Chaque nลud peut :
- AgentNode : Encapsuler un agent LLM pour traiter le langage naturel
- Nลud de fonction : Exรฉcuter du code Rust personnalisรฉ pour le traitement des donnรฉes
- Nลuds intรฉgrรฉs : Utiliser une logique prรฉdรฉfinie comme des compteurs ou des validateurs
Considรฉrez les nลuds comme des travailleurs spรฉcialisรฉs dans une chaรฎne de montage - chacun a une tรขche et une expertise spรฉcifiques.
๐ Arรชtes : le contrรดle du flux Les arรชtes dรฉterminent comment lโexรฉcution progresse dans votre graphe :
- Arรชtes statiques : Connexions directes (
A โ B โ C) - Arรชtes conditionnelles : Routage dynamique basรฉ sur lโรฉtat (
if sentiment == "positive" โ positive_handler) - Arรชtes parallรจles : Plusieurs chemins ร partir dโun nลud (
START โ [translator, summarizer])
Les arรชtes sont comme des feux de circulation et des panneaux routiers qui dirigent le flux de travail.
๐พ รtat : la mรฉmoire partagรฉe Lโรฉtat est un magasin clรฉ-valeur que tous les nลuds peuvent lire et รฉcrire :
- Donnรฉes dโentrรฉe : Informations initiales injectรฉes dans le graphe
- Rรฉsultats intermรฉdiaires : La sortie dโun nลud devient lโentrรฉe dโun autre
- Sortie finale : Le rรฉsultat achevรฉ aprรจs tout le traitement
Lโรฉtat agit comme un tableau blanc partagรฉ oรน les nลuds peuvent laisser des informations ร utiliser par dโautres.
โก Exรฉcution parallรจle : le coup de pouce en vitesse Lorsque plusieurs arรชtes quittent un nลud, les nลuds cibles sโexรฉcutent simultanรฉment :
- Traitement plus rapide : Les tรขches indรฉpendantes sโexรฉcutent en mรชme temps
- Efficacitรฉ des ressources : Meilleure utilisation du CPU et des entrรฉes/sorties
- Scalabilitรฉ : Gรฉrer des flux de travail plus complexes sans ralentissement linรฉaire
Cโest comme avoir plusieurs travailleurs qui sโattaquent simultanรฉment ร diffรฉrentes parties dโun travail au lieu dโattendre leur tour.
Dรฉmarrage rapide
1. Crรฉez votre projet
cargo new graph_demo
cd graph_demo
Ajoutez les dรฉpendances ร Cargo.toml :
[dependencies]
adk-graph = { version = "2.0.0", features = ["sqlite"] }
adk-agent = "2.0.0"
adk-model = "2.0.0"
adk-core = "2.0.0"
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
serde_json = "1.0"
Crรฉez .env avec votre clรฉ API :
echo 'GOOGLE_API_KEY=your-api-key' > .env
2. Exemple de traitement parallรจle
Voici un exemple complet fonctionnel qui traite du texte en parallรจle :
use adk_agent::LlmAgentBuilder;
use adk_graph::{
agent::GraphAgent,
edge::{END, START},
node::{AgentNode, ExecutionConfig, NodeOutput},
state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create specialized LLM agents
let translator_agent = Arc::new(
LlmAgentBuilder::new("translator")
.description("Translates text to French")
.model(model.clone())
.instruction("Translate the input text to French. Only output the translation.")
.build()?,
);
let summarizer_agent = Arc::new(
LlmAgentBuilder::new("summarizer")
.description("Summarizes text")
.model(model.clone())
.instruction("Summarize the input text in one sentence.")
.build()?,
);
// Wrap agents as graph nodes with input/output mappers
let translator_node = AgentNode::new(translator_agent)
.with_input_mapper(|state| {
let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
if !text.is_empty() {
updates.insert("translation".to_string(), json!(text));
}
}
}
updates
});
let summarizer_node = AgentNode::new(summarizer_agent)
.with_input_mapper(|state| {
let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
if !text.is_empty() {
updates.insert("summary".to_string(), json!(text));
}
}
}
updates
});
// Build the graph with parallel execution
let agent = GraphAgent::builder("text_processor")
.description("Processes text with translation and summarization in parallel")
.channels(&["input", "translation", "summary", "result"])
.node(translator_node)
.node(summarizer_node)
.node_fn("combine", |ctx| async move {
let translation = ctx.get("translation").and_then(|v| v.as_str()).unwrap_or("N/A");
let summary = ctx.get("summary").and_then(|v| v.as_str()).unwrap_or("N/A");
let result = format!(
"=== Processing Complete ===\n\n\
French Translation:\n{}\n\n\
Summary:\n{}",
translation, summary
);
Ok(NodeOutput::new().with_update("result", json!(result)))
})
// Parallel execution: both nodes start simultaneously
.edge(START, "translator")
.edge(START, "summarizer")
.edge("translator", "combine")
.edge("summarizer", "combine")
.edge("combine", END)
.build()?;
// Execute the graph
let mut input = State::new();
input.insert("input".to_string(), json!("AI is transforming how we work and live."));
let result = agent.invoke(input, ExecutionConfig::new("thread-1")).await?;
println!("{}", result.get("result").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}
Sortie de lโexemple :
=== Processing Complete ===
French Translation:
L'IA transforme notre faรงon de travailler et de vivre.
Summary:
AI is revolutionizing work and daily life through technological transformation.
Comment fonctionne lโexรฉcution du graphe
Vue dโensemble
Les agents de graphe sโexรฉcutent en super-รฉtapes - tous les nลuds prรชts sโexรฉcutent en parallรจle, puis le graphe attend que tous soient terminรฉs avant lโรฉtape suivante :
Step 1: START โโโฌโโโถ translator (running)
โโโโถ summarizer (running)
โณ Wait for both to complete...
Step 2: translator โโโฌโโโถ combine (running)
summarizer โโโ
โณ Wait for combine to complete...
Step 3: combine โโโถ END โ
Flux de lโรฉtat ร travers les nลuds
Chaque nลud peut lire depuis et รฉcrire dans lโรฉtat partagรฉ :
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STEP 1: Initial state โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ State: { "input": "AI is transforming how we work" } โ
โ โ
โ โ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ translator โ โ summarizer โ โ
โ โ reads "input" โ โ reads "input" โ โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STEP 2: After parallel execution โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ State: { โ
โ "input": "AI is transforming how we work", โ
โ "translation": "L'IA transforme notre faรงon de travailler", โ
โ "summary": "AI is revolutionizing work through technology" โ
โ } โ
โ โ
โ โ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ combine โ โ
โ โ reads "translation" + "summary" โ โ
โ โ writes "result" โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STEP 3: Final state โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ State: { โ
โ "input": "AI is transforming how we work", โ
โ "translation": "L'IA transforme notre faรงon de travailler", โ
โ "summary": "AI is revolutionizing work through technology", โ
โ "result": "=== Processing Complete ===\n\nFrench..." โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Ce qui le rend possible
| Composant | Rรดle |
|---|---|
AgentNode | Enveloppe les agents LLM avec des mappeurs d'entrรฉe/sortie |
input_mapper | Transforme l'รฉtat โ entrรฉe de l'agent Content |
output_mapper | Transforme les รฉvรฉnements dโagent โ mises ร jour de lโรฉtat |
channels | Dรฉclare les champs dโรฉtat que le graphe utilisera |
edge() | Dรฉfinit le flux dโexรฉcution entre les nลuds |
ExecutionConfig | Fournit lโID de fil pour le point de contrรดle |
Routage conditionnel avec classification LLM
Construisez des systรจmes de routage intelligents oรน LLMs dรฉcident du chemin dโexรฉcution :
Visuel : routage basรฉ sur le sentiment
โโโโโโโโโโโโโโโโโโโโโโโ
User Feedback โ โ
โโโโโโโโโโโโโโโโโถ โ CLASSIFIER โ
โ ๐ง Analyze tone โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ POSITIVE โ โ NEGATIVE โ โ NEUTRAL โ
โ โ โ โ โ โ
โ ๐ Thank you! โ โ ๐ Apologize โ โ ๐ Ask more โ
โ Celebrate โ โ Help fix โ โ questions โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
Code dโexemple complet
use adk_agent::LlmAgentBuilder;
use adk_graph::{
edge::{END, Router, START},
graph::StateGraph,
node::{AgentNode, ExecutionConfig},
state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create classifier agent
let classifier_agent = Arc::new(
LlmAgentBuilder::new("classifier")
.description("Classifies text sentiment")
.model(model.clone())
.instruction(
"You are a sentiment classifier. Analyze the input text and respond with \
ONLY one word: 'positive', 'negative', or 'neutral'. Nothing else.",
)
.build()?,
);
// Create response agents for each sentiment
let positive_agent = Arc::new(
LlmAgentBuilder::new("positive")
.description("Handles positive feedback")
.model(model.clone())
.instruction(
"You are a customer success specialist. The customer has positive feedback. \
Express gratitude, reinforce the positive experience, and suggest ways to \
share their experience. Be warm and appreciative. Keep response under 3 sentences.",
)
.build()?,
);
let negative_agent = Arc::new(
LlmAgentBuilder::new("negative")
.description("Handles negative feedback")
.model(model.clone())
.instruction(
"You are a customer support specialist. The customer has a complaint. \
Acknowledge their frustration, apologize sincerely, and offer help. \
Be empathetic. Keep response under 3 sentences.",
)
.build()?,
);
let neutral_agent = Arc::new(
LlmAgentBuilder::new("neutral")
.description("Handles neutral feedback")
.model(model.clone())
.instruction(
"You are a customer service representative. The customer has neutral feedback. \
Ask clarifying questions to better understand their needs. Be helpful and curious. \
Keep response under 3 sentences.",
)
.build()?,
);
// Create AgentNodes with mappers
let classifier_node = AgentNode::new(classifier_agent)
.with_input_mapper(|state| {
let text = state.get("feedback").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("")
.to_lowercase()
.trim()
.to_string();
let sentiment = if text.contains("positive") { "positive" }
else if text.contains("negative") { "negative" }
else { "neutral" };
updates.insert("sentiment".to_string(), json!(sentiment));
}
}
updates
});
// Response nodes (similar pattern for each)
let positive_node = AgentNode::new(positive_agent)
.with_input_mapper(|state| {
let text = state.get("feedback").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
updates.insert("response".to_string(), json!(text));
}
}
updates
});
// Build graph with conditional routing
let graph = StateGraph::with_channels(&["feedback", "sentiment", "response"])
.add_node(classifier_node)
.add_node(positive_node)
// ... add negative_node and neutral_node similarly
.add_edge(START, "classifier")
.add_conditional_edges(
"classifier",
Router::by_field("sentiment"), // Route based on sentiment field
[
("positive", "positive"),
("negative", "negative"),
("neutral", "neutral"),
],
)
.add_edge("positive", END)
.add_edge("negative", END)
.add_edge("neutral", END)
.compile()?;
// Test with different feedback
let mut input = State::new();
input.insert("feedback".to_string(), json!("Your product is amazing! I love it!"));
let result = graph.invoke(input, ExecutionConfig::new("feedback-1")).await?;
println!("Sentiment: {}", result.get("sentiment").and_then(|v| v.as_str()).unwrap_or(""));
println!("Response: {}", result.get("response").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}
Flux dโexemple :
Input: "Your product is amazing! I love it!"
โ
Classifier: "positive"
โ
Positive Agent: "Thank you so much for the wonderful feedback!
We're thrilled you love our product.
Would you consider leaving a review to help others?"
Modรจle ReAct : raisonner + agir
Construisez des agents capables dโutiliser des outils de maniรจre itรฉrative pour rรฉsoudre des problรจmes complexes :
Visuel : cycle ReAct
โโโโโโโโโโโโโโโโโโโโโโโ
User Question โ โ
โโโโโโโโโโโโโโโโโถ โ REASONER โ
โ ๐ง Think + Act โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Has tool calls? โ
โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ YES โ โ NO โ
โ โ โ โ
โ ๐ Loop back โ โ โ
Final answer โ
โ to reasoner โ โ END โ
โโโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ REASONER โ
โ ๐ง Think + Act โ
โ (next iteration) โ
โโโโโโโโโโโโโโโโโโโโโโโ
Exemple complet ReAct
use adk_agent::LlmAgentBuilder;
use adk_core::{Part, Tool};
use adk_graph::{
edge::{END, START},
graph::StateGraph,
node::{AgentNode, ExecutionConfig, NodeOutput},
state::State,
};
use adk_model::GeminiModel;
use adk_tool::FunctionTool;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create tools
let weather_tool = Arc::new(FunctionTool::new(
"get_weather",
"Get the current weather for a location. Takes a 'location' parameter (city name).",
|_ctx, args| async move {
let location = args.get("location").and_then(|v| v.as_str()).unwrap_or("unknown");
Ok(json!({
"location": location,
"temperature": "72ยฐF",
"condition": "Sunny",
"humidity": "45%"
}))
},
)) as Arc<dyn Tool>;
let calculator_tool = Arc::new(FunctionTool::new(
"calculator",
"Perform mathematical calculations. Takes an 'expression' parameter (string).",
|_ctx, args| async move {
let expr = args.get("expression").and_then(|v| v.as_str()).unwrap_or("0");
let result = match expr {
"2 + 2" => "4",
"10 * 5" => "50",
"100 / 4" => "25",
"15 - 7" => "8",
_ => "Unable to evaluate",
};
Ok(json!({ "result": result, "expression": expr }))
},
)) as Arc<dyn Tool>;
// Create reasoner agent with tools
let reasoner_agent = Arc::new(
LlmAgentBuilder::new("reasoner")
.description("Reasoning agent with tools")
.model(model.clone())
.instruction(
"You are a helpful assistant with access to tools. Use tools when needed to answer questions. \
When you have enough information, provide a final answer without using more tools.",
)
.tool(weather_tool)
.tool(calculator_tool)
.build()?,
);
// Create reasoner node that detects tool usage
let reasoner_node = AgentNode::new(reasoner_agent)
.with_input_mapper(|state| {
let question = state.get("question").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(question)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
let mut has_tool_calls = false;
let mut response = String::new();
for event in events {
if let Some(content) = event.content() {
for part in &content.parts {
match part {
Part::FunctionCall { .. } => {
has_tool_calls = true;
}
Part::Text { text } => {
response.push_str(text);
}
_ => {}
}
}
}
}
updates.insert("has_tool_calls".to_string(), json!(has_tool_calls));
updates.insert("response".to_string(), json!(response));
updates
});
// Build ReAct graph with cycle
let graph = StateGraph::with_channels(&["question", "has_tool_calls", "response", "iteration"])
.add_node(reasoner_node)
.add_node_fn("counter", |ctx| async move {
let i = ctx.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(NodeOutput::new().with_update("iteration", json!(i + 1)))
})
.add_edge(START, "counter")
.add_edge("counter", "reasoner")
.add_conditional_edges(
"reasoner",
|state| {
let has_tools = state.get("has_tool_calls").and_then(|v| v.as_bool()).unwrap_or(false);
let iteration = state.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
// Safety limit
if iteration >= 5 { return END.to_string(); }
if has_tools {
"counter".to_string() // Loop back for more reasoning
} else {
END.to_string() // Done - final answer
}
},
[("counter", "counter"), (END, END)],
)
.compile()?
.with_recursion_limit(10);
// Test the ReAct agent
let mut input = State::new();
input.insert("question".to_string(), json!("What's the weather in Paris and what's 15 + 25?"));
let result = graph.invoke(input, ExecutionConfig::new("react-1")).await?;
println!("Final answer: {}", result.get("response").and_then(|v| v.as_str()).unwrap_or(""));
println!("Iterations: {}", result.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0));
Ok(())
}
Flux dโexemple :
Question: "What's the weather in Paris and what's 15 + 25?"
Iteration 1:
Reasoner: "I need to get weather info and do math"
โ Calls get_weather(location="Paris") and calculator(expression="15 + 25")
โ has_tool_calls = true โ Loop back
Iteration 2:
Reasoner: "Based on the results: Paris is 72ยฐF and sunny, 15 + 25 = 40"
โ No tool calls โ has_tool_calls = false โ END
Final Answer: "The weather in Paris is 72ยฐF and sunny with 45% humidity.
And 15 + 25 equals 40."
AgentNode
Enveloppe nโimporte quel ADK Agent (gรฉnรฉralement LlmAgent) en tant que nลud de graphe :
Ce que voit lโagent
Un agent ร lโintรฉrieur dโun graphe sโexรฉcute sous un contexte dรฉrivรฉ de lโinvocation qui a dรฉmarrรฉ le graphe, de sorte quโil se comporte de la mรชme maniรจre quโen dehors de celui-ci. Lorsquโun Runner invoque un GraphAgent, lโidentitรฉ et les services de lโappelant sont automatiquement transmis :
| Transmis | Remarque |
|---|---|
app_name, user_id, session_id | Ceux de lโappelant, pas un appel synthรฉtique |
| Les scopes et les mรฉtadonnรฉes de requรชte | Ainsi, les vรฉrifications de scope voient les autorisations de lโappelant |
| Service secret, mรฉmoire, artefacts, รฉtat partagรฉ | Disponible exactement comme en dehors du graphe |
| Annulation | Runner::interrupt atteint un agent exรฉcutรฉ en tant que nลud |
RunConfig | Hรฉritรฉ de l'appelant |
branch | Dรฉrivรฉ, comme {caller_branch}.{agent_name}, donc les รฉvรฉnements d'un nลud sont attribuables |
Un graphe invoquรฉ directement โ graph.invoke(state, ExecutionConfig::new("thread")) โ
nโa aucune invocation ร hรฉriter. Cโest le mode autonome : le nลud reรงoit
user_id = "graph_user", app_name = "graph_app", la branche main, aucun secret, et aucune
mรฉmoire. Cโest un mode volontaire pour exรฉcuter un graphe en dehors dโun Runner, pas un
repli ร utiliser en production.
Pour faire le pont manuellement โ par exemple lorsquโon pilote un graphe depuis son propre exรฉcuteur โ passez lโinvocation explicitement :
let config = ExecutionConfig::new(ctx.session_id()).with_parent_context(ctx.clone());
Note : le nลud sโexรฉcute toujours dans sa propre session de graphe en mรฉmoire, donc lโhistorique de conversation de lโagent ร lโintรฉrieur dโun nลud est limitรฉ au nลud et nโest pas ajoutรฉ ร la session de lโappelant.
let node = AgentNode::new(llm_agent)
.with_input_mapper(|state| {
// Transform graph state to agent input Content
let text = state.get("input").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(text)
})
.with_output_mapper(|events| {
// Transform agent events to state updates
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
updates.insert("output".to_string(), json!(text));
}
}
updates
});
Nลuds de fonction
Fonctions async simples qui traitent lโรฉtat :
.node_fn("process", |ctx| async move {
let input = ctx.state.get("input").unwrap();
let output = process_data(input).await?;
Ok(NodeOutput::new().with_update("output", output))
})
Types dโarรชtes
Arรชtes statiques
Connexions directes entre les nลuds :
.edge(START, "first_node")
.edge("first_node", "second_node")
.edge("second_node", END)
Arรชtes conditionnelles
Routage dynamique basรฉ sur lโรฉtat :
.conditional_edge(
"router",
|state| {
match state.get("next").and_then(|v| v.as_str()) {
Some("research") => "research_node".to_string(),
Some("write") => "write_node".to_string(),
_ => END.to_string(),
}
},
[
("research_node", "research_node"),
("write_node", "write_node"),
(END, END),
],
)
Assistants de routage
Utilisez les routeurs intรฉgrรฉs pour les schรฉmas courants :
use adk_graph::edge::Router;
// Route based on a state field value
.conditional_edge("classifier", Router::by_field("sentiment"), [
("positive", "positive_handler"),
("negative", "negative_handler"),
("neutral", "neutral_handler"),
])
// Route based on boolean field
.conditional_edge("check", Router::by_bool("approved"), [
("true", "execute"),
("false", "reject"),
])
// Limit iterations
.conditional_edge("loop", Router::max_iterations("count", 5), [
("continue", "process"),
("done", END),
])
Exรฉcution parallรจle
Plusieurs arรชtes ร partir dโun mรชme nลud sโexรฉcutent en parallรจle :
let agent = GraphAgent::builder("parallel_processor")
.channels(&["input", "translation", "summary", "analysis"])
.node(translator_node)
.node(summarizer_node)
.node(analyzer_node)
.node(combiner_node)
// All three start simultaneously
.edge(START, "translator")
.edge(START, "summarizer")
.edge(START, "analyzer")
// Wait for all to complete before combining
.edge("translator", "combiner")
.edge("summarizer", "combiner")
.edge("analyzer", "combiner")
.edge("combiner", END)
.build()?;
Graphes cycliques (modรจle ReAct)
Construisez des agents de raisonnement itรฉratif avec des cycles :
use adk_core::Part;
// Create agent with tools
let reasoner = Arc::new(
LlmAgentBuilder::new("reasoner")
.model(model)
.instruction("Use tools to answer questions. Provide final answer when done.")
.tool(search_tool)
.tool(calculator_tool)
.build()?
);
let reasoner_node = AgentNode::new(reasoner)
.with_input_mapper(|state| {
let question = state.get("question").and_then(|v| v.as_str()).unwrap_or("");
adk_core::Content::new("user").with_text(question)
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
let mut has_tool_calls = false;
let mut response = String::new();
for event in events {
if let Some(content) = event.content() {
for part in &content.parts {
match part {
Part::FunctionCall { name, .. } => {
has_tool_calls = true;
}
Part::Text { text } => {
response.push_str(text);
}
_ => {}
}
}
}
}
updates.insert("has_tool_calls".to_string(), json!(has_tool_calls));
updates.insert("response".to_string(), json!(response));
updates
});
// Build graph with cycle
let react_agent = StateGraph::with_channels(&["question", "has_tool_calls", "response", "iteration"])
.add_node(reasoner_node)
.add_node_fn("counter", |ctx| async move {
let i = ctx.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(NodeOutput::new().with_update("iteration", json!(i + 1)))
})
.add_edge(START, "counter")
.add_edge("counter", "reasoner")
.add_conditional_edges(
"reasoner",
|state| {
let has_tools = state.get("has_tool_calls").and_then(|v| v.as_bool()).unwrap_or(false);
let iteration = state.get("iteration").and_then(|v| v.as_i64()).unwrap_or(0);
// Safety limit
if iteration >= 5 { return END.to_string(); }
if has_tools {
"counter".to_string() // Loop back
} else {
END.to_string() // Done
}
},
[("counter", "counter"), (END, END)],
)
.compile()?
.with_recursion_limit(10);
Superviseur multi-agent
Acheminez les tรขches vers des agents spรฉcialisรฉs :
// Create supervisor agent
let supervisor = Arc::new(
LlmAgentBuilder::new("supervisor")
.model(model.clone())
.instruction("Route tasks to: researcher, writer, or coder. Reply with agent name only.")
.build()?
);
let supervisor_node = AgentNode::new(supervisor)
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("")
.to_lowercase();
let next = if text.contains("researcher") { "researcher" }
else if text.contains("writer") { "writer" }
else if text.contains("coder") { "coder" }
else { "done" };
updates.insert("next_agent".to_string(), json!(next));
}
}
updates
});
// Build supervisor graph
let graph = StateGraph::with_channels(&["task", "next_agent", "research", "content", "code"])
.add_node(supervisor_node)
.add_node(researcher_node)
.add_node(writer_node)
.add_node(coder_node)
.add_edge(START, "supervisor")
.add_conditional_edges(
"supervisor",
Router::by_field("next_agent"),
[
("researcher", "researcher"),
("writer", "writer"),
("coder", "coder"),
("done", END),
],
)
// Agents report back to supervisor
.add_edge("researcher", "supervisor")
.add_edge("writer", "supervisor")
.add_edge("coder", "supervisor")
.compile()?;
Gestion de lโรฉtat
Schรฉma dโรฉtat avec rรฉducteurs
Contrรดlez la faรงon dont les mises ร jour de lโรฉtat sont fusionnรฉes :
let schema = StateSchema::builder()
.channel("current_step") // Overwrite (default)
.list_channel("messages") // Append to list
.channel_with_reducer("count", Reducer::Sum) // Sum values
.channel_with_reducer("data", Reducer::Custom(Arc::new(|old, new| {
// Custom merge logic
merge_json(old, new)
})))
.build();
let agent = GraphAgent::builder("stateful")
.state_schema(schema)
// ... nodes and edges
.build()?;
Types de rรฉducteurs
| Rรฉducteur | Comportement |
|---|---|
Overwrite | Remplacer lโancienne valeur par la nouvelle (par dรฉfaut) |
Append | Ajouter ร la liste |
Sum | Ajouter des valeurs numรฉriques |
Custom | Fonction de fusion personnalisรฉe |
Point de contrรดle
Active lโรฉtat persistant pour la tolรฉrance aux pannes et lโintervention humaine dans la boucle :
En mรฉmoire (dรฉveloppement)
use adk_graph::checkpoint::MemoryCheckpointer;
let checkpointer = Arc::new(MemoryCheckpointer::new());
let graph = StateGraph::with_channels(&["task", "result"])
// ... nodes and edges
.compile()?
.with_checkpointer_arc(checkpointer.clone());
SQLite (production)
use adk_graph::checkpoint::SqliteCheckpointer;
let checkpointer = SqliteCheckpointer::new("checkpoints.db").await?;
let graph = StateGraph::with_channels(&["task", "result"])
// ... nodes and edges
.compile()?
.with_checkpointer(checkpointer);
Ce quโenregistre un point de contrรดle
Un point de contrรดle stocke lโรฉtat accumulรฉ, le numรฉro dโรฉtape et la frontiรจre โ les nลuds qui doivent encore sโexรฉcuter. Il est รฉcrit aprรจs lโavancement de la frontiรจre, de sorte que la reprise ne rรฉexรฉcute jamais un nลud dรฉjร terminรฉ et ne rรฉapplique jamais deux fois ses mises ร jour. Une exรฉcution qui se termine enregistre un point de contrรดle avec une frontiรจre vide, donc reprendre un fil dโexรฉcution terminรฉ renvoie lโรฉtat final plutรดt que de redรฉmarrer le graphe.
Deux cas enregistrent dรฉlibรฉrรฉment le point de contrรดle de la frontiรจre qui รฉtait en train de sโexรฉcuter plutรดt que la suivante, parce que le nลud interrompu nโa pas encore produit ses mises ร jour et doit sโexรฉcuter ร nouveau lors de la reprise :
| Situation | Frontier enregistrรฉe |
|---|---|
| Super-step terminรฉ | Les prochains nลuds ร exรฉcuter |
| Exรฉcution terminรฉe | Vide |
| Interruption dรฉclenchรฉe (en mode bloquant ou en streaming) | Les nลuds en cours dโexรฉcution |
Les exรฉcutions diffusรฉes effectuent un point de contrรดle selon le mรชme calendrier que les exรฉcutions bloquantes, y compris lorsquโune interruption met fin au flux, de sorte quโune pause avec intervention humaine peut รชtre reprise dans lโun ou lโautre mode dโexรฉcution.
Historique des points de contrรดle (voyage dans le temps)
Lecture seule.
TimeTravelHandle::state_history(from, to)renvoie lโรฉtat qui a รฉtรฉ enregistrรฉ ร chaque รฉtape pointรฉe par un point de contrรดle. Il nโexรฉcute rien โ aucun nลud nโest lancรฉ, aucun รฉvรฉnement nโest rรฉgรฉnรฉrรฉ, et aucun effet de bord nโest rรฉpรฉtรฉ. Pour rรฉexรฉcuter ร partir dโun point dans lโhistorique, utilisezfork_atpour crรฉer une branche ร partir de ce point de contrรดle et invoquer le graphe sur le thread bifurquรฉ. La mรฉthode sโappelait auparavantreplayet รฉtait documentรฉe comme rรฉexรฉcutant le graphe, ce quโelle nโa jamais fait.
Les points de contrรดle permettent aussi une reprise durable โ si lโexรฉcution dโun graphe plante ou si le processus redรฉmarre, lโexรฉcution reprend ร partir du dernier point de contrรดle persistant au lieu de repartir de zรฉro. Utilisez SqliteCheckpointer ou PostgresCheckpointer pour une persistance ร lโรฉpreuve des plantages.
// List all checkpoints for a thread
let checkpoints = checkpointer.list("thread-id").await?;
for cp in checkpoints {
println!("Step {}: {:?}", cp.step, cp.state.get("status"));
}
// Load a specific checkpoint
if let Some(checkpoint) = checkpointer.load_by_id(&checkpoint_id).await? {
println!("State at step {}: {:?}", checkpoint.step, checkpoint.state);
}
Intervention humaine dans la boucle
Interrompez lโexรฉcution pour obtenir lโapprobation humaine ร lโaide dโinterruptions dynamiques :
use adk_graph::{error::GraphError, node::NodeOutput};
// Planner agent assesses risk
let planner_node = AgentNode::new(planner_agent)
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect::<Vec<_>>()
.join("");
// Extract risk level from LLM response
let risk = if text.to_lowercase().contains("risk: high") { "high" }
else if text.to_lowercase().contains("risk: medium") { "medium" }
else { "low" };
updates.insert("plan".to_string(), json!(text));
updates.insert("risk_level".to_string(), json!(risk));
}
}
updates
});
// Review node with dynamic interrupt
let graph = StateGraph::with_channels(&["task", "plan", "risk_level", "approved", "result"])
.add_node(planner_node)
.add_node(executor_node)
.add_node_fn("review", |ctx| async move {
let risk = ctx.get("risk_level").and_then(|v| v.as_str()).unwrap_or("low");
let approved = ctx.get("approved").and_then(|v| v.as_bool());
// Already approved - continue
if approved == Some(true) {
return Ok(NodeOutput::new());
}
// High/medium risk - interrupt for approval
if risk == "high" || risk == "medium" {
return Ok(NodeOutput::interrupt_with_data(
&format!("{} RISK: Human approval required", risk.to_uppercase()),
json!({
"plan": ctx.get("plan"),
"risk_level": risk,
"action": "Set 'approved' to true to continue"
})
));
}
// Low risk - auto-approve
Ok(NodeOutput::new().with_update("approved", json!(true)))
})
.add_edge(START, "planner")
.add_edge("planner", "review")
.add_edge("review", "executor")
.add_edge("executor", END)
.compile()?
.with_checkpointer_arc(checkpointer.clone());
// Execute - may pause for approval
let thread_id = "task-001";
let result = graph.invoke(input, ExecutionConfig::new(thread_id)).await;
match result {
Err(GraphError::Interrupted(interrupt)) => {
println!("*** EXECUTION PAUSED ***");
println!("Reason: {}", interrupt.interrupt);
println!("Plan awaiting approval: {:?}", interrupt.state.get("plan"));
// Human reviews and approves...
// Update state with approval
graph.update_state(thread_id, [("approved".to_string(), json!(true))]).await?;
// Resume execution
let final_result = graph.invoke(State::new(), ExecutionConfig::new(thread_id)).await?;
println!("Final result: {:?}", final_result.get("result"));
}
Ok(result) => {
println!("Completed without interrupt: {:?}", result);
}
Err(e) => {
println!("Error: {}", e);
}
}
Interruptions statiques
Utilisez interrupt_before ou interrupt_after pour les points dโarrรชt obligatoires :
let graph = StateGraph::with_channels(&["task", "plan", "result"])
.add_node(planner_node)
.add_node(executor_node)
.add_edge(START, "planner")
.add_edge("planner", "executor")
.add_edge("executor", END)
.compile()?
.with_interrupt_before(&["executor"]); // Always pause before execution
Exรฉcution en streaming
Diffusez les รฉvรฉnements pendant lโexรฉcution du graphe :
use futures::StreamExt;
use adk_graph::stream::StreamMode;
let stream = agent.stream(input, config, StreamMode::Updates);
while let Some(event) = stream.next().await {
match event? {
StreamEvent::NodeStart(name) => println!("Starting: {}", name),
StreamEvent::Updates { node, updates } => {
println!("{} updated state: {:?}", node, updates);
}
StreamEvent::NodeEnd(name) => println!("Completed: {}", name),
StreamEvent::Done(state) => println!("Final state: {:?}", state),
_ => {}
}
}
Modes de diffusion
| Mode de diffusion | Fonctionnement |
|---|---|
Values | Diffuser l'รฉtat complet aprรจs chaque nลud |
Updates | Diffuser uniquement les changements d'รฉtat |
Messages | Diffuser les mises ร jour de type message |
Debug | Diffuser tous les รฉvรฉnements internes |
Messages lit les jetons de Node::execute_stream au fur et ร mesure de leur production.
Chaque nลud sโexรฉcute une fois par super-รฉtape dans ce mode : le nลud signale ses mises ร jour dโรฉtat sur le flux sous forme dโรฉvรฉnement StreamEvent::Updates, et lโexรฉcuteur applique celles-ci au lieu de rรฉexรฉcuter le nลud une seconde fois pour les recueillir. Cela est particuliรจrement important pour AgentNode, oรน une seconde exรฉcution signifierait un second appel facturรฉ au modรจle par nลud.
Important : un
Nodepersonnalisรฉ qui redรฉfinitexecute_streamdoit รฉmettre un รฉvรฉnementStreamEvent::Updatesportant ses mises ร jour dโรฉtat. Sans cela, le nลud diffuse des รฉvรฉnements mais nโapporte aucun รฉtat en modeMessages. Leexecute_streampar dรฉfaut, qui enveloppeexecute, le fait pour vous.
Les politiques de dรฉlai dโexpiration sโappliquent ร lโexรฉcution diffusรฉe elle-mรชme. Pour un flux, idle_timeout signifie quโaucun รฉvรฉnement nโa รฉtรฉ produit dans la limite impartie.
Intรฉgration ADK
GraphAgent implรฉmente le trait ADK Agent, donc il fonctionne avec :
- Runner : Utilisez-le avec
adk-runnerpour une exรฉcution standard - Callbacks : Prise en charge complรจte des callbacks avant/aprรจs
- Sessions : Fonctionne avec
adk-sessionpour lโhistorique de conversation - Streaming : Retourne ADK
EventStream
use adk_runner::Runner;
let graph_agent = GraphAgent::builder("workflow")
.before_agent_callback(|ctx| async {
println!("Starting graph execution for session: {}", ctx.session_id());
Ok(())
})
.after_agent_callback(|ctx, event| async {
if let Some(content) = event.content() {
println!("Graph completed with content");
}
Ok(())
})
// ... graph definition
.build()?;
// GraphAgent implements Agent trait - use with Launcher or Runner
// See adk-runner README for Runner configuration
Exemples
Exemples de graphes validรฉs dans ce dรฉpรดt :
cargo run --manifest-path examples/tier_examples/standard/Cargo.toml --bin 11-standard-graph
cargo run --manifest-path examples/tier_examples/standard/Cargo.toml --bin 12-standard-sequential
cargo run --manifest-path examples/competitive_graph_resume/Cargo.toml
LโADK-Rust Playground intรฉgrรฉ ร ce site comprend la galerie complรจte des graphes avec une vรฉritable intรฉgration LLM.
Comparaison avec LangGraph
| Fonctionnalitรฉ | LangGraph | adk-graph |
|---|---|---|
| Gestion d'รฉtat | TypedDict + Reducers | StateSchema + Reducers |
| Modรจle d'exรฉcution | super-รฉtapes Pregel | super-รฉtapes Pregel |
| Point de contrรดle | Mรฉmoire, SQLite, Postgres | Mรฉmoire, SQLite |
| Humain dans la boucle | interrupt_before/after | interrupt_before/after + dynamique |
| Flux continu | 5 modes | 5 modes |
| Cycles | Prise en charge native | Prise en charge native |
| Sรฉcuritรฉ des types | typage Python | systรจme de types Rust |
| LLM Intรฉgration | LangChain | AgentNode + ADK agents |
Prรฉcรฉdent: โ Systรจmes multi-agents | Suivant: Agents en temps rรฉel โ