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

ComposantRรดle
AgentNodeEnveloppe les agents LLM avec des mappeurs d'entrรฉe/sortie
input_mapperTransforme l'รฉtat โ†’ entrรฉe de l'agent Content
output_mapperTransforme les รฉvรฉnements dโ€™agent โ†’ mises ร  jour de lโ€™รฉtat
channelsDรฉclare les champs dโ€™รฉtat que le graphe utilisera
edge()Dรฉfinit le flux dโ€™exรฉcution entre les nล“uds
ExecutionConfigFournit 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 :

TransmisRemarque
app_name, user_id, session_idCeux de lโ€™appelant, pas un appel synthรฉtique
Les scopes et les mรฉtadonnรฉes de requรชteAinsi, 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
AnnulationRunner::interrupt atteint un agent exรฉcutรฉ en tant que nล“ud
RunConfigHรฉritรฉ de l'appelant
branchDรฉ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รฉducteurComportement
OverwriteRemplacer lโ€™ancienne valeur par la nouvelle (par dรฉfaut)
AppendAjouter ร  la liste
SumAjouter des valeurs numรฉriques
CustomFonction 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 :

SituationFrontier enregistrรฉe
Super-step terminรฉLes prochains nล“uds ร  exรฉcuter
Exรฉcution terminรฉeVide
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, utilisez fork_at pour crรฉer une branche ร  partir de ce point de contrรดle et invoquer le graphe sur le thread bifurquรฉ. La mรฉthode sโ€™appelait auparavant replay et รฉ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 diffusionFonctionnement
ValuesDiffuser l'รฉtat complet aprรจs chaque nล“ud
UpdatesDiffuser uniquement les changements d'รฉtat
MessagesDiffuser les mises ร  jour de type message
DebugDiffuser 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 Node personnalisรฉ qui redรฉfinit execute_stream doit รฉmettre un รฉvรฉnement StreamEvent::Updates portant ses mises ร  jour dโ€™รฉtat. Sans cela, le nล“ud diffuse des รฉvรฉnements mais nโ€™apporte aucun รฉtat en mode Messages. Le execute_stream par dรฉfaut, qui enveloppe execute, 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-runner pour une exรฉcution standard
  • Callbacks : Prise en charge complรจte des callbacks avant/aprรจs
  • Sessions : Fonctionne avec adk-session pour 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รฉLangGraphadk-graph
Gestion d'รฉtatTypedDict + ReducersStateSchema + Reducers
Modรจle d'exรฉcutionsuper-รฉtapes Pregelsuper-รฉtapes Pregel
Point de contrรดleMรฉmoire, SQLite, PostgresMรฉmoire, SQLite
Humain dans la boucleinterrupt_before/afterinterrupt_before/after + dynamique
Flux continu5 modes5 modes
CyclesPrise en charge nativePrise en charge native
Sรฉcuritรฉ des typestypage Pythonsystรจme de types Rust
LLM IntรฉgrationLangChainAgentNode + ADK agents

Prรฉcรฉdent: โ† Systรจmes multi-agents | Suivant: Agents en temps rรฉel โ†’

Agents de graphe - Documentation ADK-Rust | ADK-Rust