Gestion de l'état

L'état de Session dans adk-rust permet aux Agent de stocker et de récupérer des données qui persistent au fil des tours de conversation. L'état est organisé à l'aide de préfixes de clés qui déterminent la portée et la durée de vie des données.

Aperçu

L'état est stocké sous forme de paires clé-valeur où :

  • Les clés sont des chaînes de caractères avec des préfixes optionnels
  • Les valeurs sont des valeurs JSON (serde_json::Value)

Le système de préfixes permet différents niveaux de portée :

  • Portée Session : Par défaut, liée à une seule Session
  • Portée Utilisateur : Partagée entre toutes les Session pour un utilisateur
  • Portée Application : Partagée entre tous les utilisateurs d'une application
  • Temporaire : Effacée après chaque invocation

Trait State

Le trait State définit l'interface pour l'accès à l'état :

use serde_json::Value;
use std::collections::HashMap;

pub trait State: Send + Sync {
    /// Get a value by key
    fn get(&self, key: &str) -> Option<Value>;
    
    /// Set a value
    fn set(&mut self, key: String, value: Value);
    
    /// Get all state as a map
    fn all(&self) -> HashMap<String, Value>;
}

Il existe également un trait ReadonlyState pour l'accès en lecture seule :

pub trait ReadonlyState: Send + Sync {
    fn get(&self, key: &str) -> Option<Value>;
    fn all(&self) -> HashMap<String, Value>;
}

Préfixes de clé d'état

adk-rust utilise trois préfixes de clé pour contrôler la portée de l'état :

PréfixeConstantePortée
app:KEY_PREFIX_APPPartagée entre tous les utilisateurs et Sessions
user:KEY_PREFIX_USERPartagée entre toutes les Sessions pour un utilisateur
temp:KEY_PREFIX_TEMPEffacée après chaque invocation
(aucun)-Portée Session (par défaut)

app: - État de l'application

État partagé entre tous les utilisateurs et Sessions d'une application.

use adk_session::KEY_PREFIX_APP;

// KEY_PREFIX_APP = "app:"
let key = format!("{}settings", KEY_PREFIX_APP);  // "app:settings"

Cas d'utilisation :

  • Configuration de l'application
  • Ressources partagées
  • Compteurs ou statistiques globaux

user: - État de l'utilisateur

État partagé entre toutes les Sessions pour un utilisateur spécifique.

use adk_session::KEY_PREFIX_USER;

// KEY_PREFIX_USER = "user:"
let key = format!("{}preferences", KEY_PREFIX_USER);  // "user:preferences"

Cas d'utilisation :

  • Préférences de l'utilisateur
  • Données de profil utilisateur
  • Contexte utilisateur inter-Session

temp: - État temporaire

État qui est effacé après chaque invocation. Non persisté.

use adk_session::KEY_PREFIX_TEMP;

// KEY_PREFIX_TEMP = "temp:"
let key = format!("{}current_step", KEY_PREFIX_TEMP);  // "temp:current_step"

Cas d'utilisation :

  • Résultats de calcul intermédiaires
  • Contexte d'opération actuel
  • Données qui ne devraient pas persister

Aucun préfixe - État de Session

Les clés sans préfixe ont une portée Session (comportement par défaut).

let key = "conversation_topic";  // Session-scoped

Cas d'utilisation :

  • Contexte de conversation
  • Données spécifiques à la Session
  • État tour par tour

Définition de l'état initial

L'état peut être initialisé lors de la création d'une Session :

use adk_session::{InMemorySessionService, SessionService, CreateRequest, KEY_PREFIX_APP, KEY_PREFIX_USER};
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut initial_state = HashMap::new();

    // App-scoped state
    initial_state.insert(
        format!("{}version", KEY_PREFIX_APP),
        json!("1.0.0")
    );

    // User-scoped state
    initial_state.insert(
        format!("{}name", KEY_PREFIX_USER),
        json!("Alice")
    );

    // Session-scoped state
    initial_state.insert(
        "topic".to_string(),
        json!("Getting started")
    );

    let service = InMemorySessionService::new();
    let session = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "user_123".to_string(),
        session_id: None,
        state: initial_state,
    }).await?;
    
    Ok(())
}

Lecture de l'état

Accédez à l'état via la méthode state() de la session :

let state = session.state();

// Get a specific key
if let Some(value) = state.get("topic") {
    println!("Topic: {}", value);
}

// Get app-scoped state
if let Some(version) = state.get("app:version") {
    println!("App version: {}", version);
}

// Get all state
let all_state = state.all();
for (key, value) in all_state {
    println!("{}: {}", key, value);
}

Mises à jour de l'état via les événements

L'état est généralement mis à jour via les actions d'événement. Lorsqu'un événement est ajouté à une session, son state_delta est appliqué :

use adk_session::{Event, EventActions};
use serde_json::json;
use std::collections::HashMap;

let mut state_delta = HashMap::new();
state_delta.insert("counter".to_string(), json!(42));
state_delta.insert("user:last_seen".to_string(), json!("2024-01-15"));

let mut event = Event::new("invocation_123");
event.actions = EventActions {
    state_delta,
    ..Default::default()
};

// Lorsque cet événement est ajouté, l'état est mis à jour
service.append_event(session.id(), event).await?;

Comportement de la portée de l'état

Le service de session gère automatiquement la portée de l'état :

À la Création de Session

  1. Extrait les clés préfixées par app: → Stocke dans l'état de l'application
  2. Extrait les clés préfixées par user: → Stocke dans l'état de l'utilisateur
  3. Les clés restantes (sauf temp:) → Stocke dans l'état de la session
  4. Fusionne toutes les portées pour la session renvoyée

À la Récupération de Session

  1. Charge l'état de l'application
  2. Charge l'état de l'utilisateur
  3. Charge l'état de la session
  4. Fusionne toutes les portées (application → utilisateur → session)

À l'Ajout d'Événement

  1. Extrait le delta d'état de l'événement
  2. Filtre les clés temp: (non persistées)
  3. Applique les deltas app: à l'état de l'application
  4. Applique les deltas user: à l'état de l'utilisateur
  5. Applique les deltas restants à l'état de la session

Exemple Complet

use adk_session::{
    InMemorySessionService, SessionService, CreateRequest, GetRequest,
    KEY_PREFIX_APP, KEY_PREFIX_USER,
};
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let service = InMemorySessionService::new();
    
    // Crée la première session avec un état initial
    let mut state1 = HashMap::new();
    state1.insert(format!("{}theme", KEY_PREFIX_APP), json!("dark"));
    state1.insert(format!("{}language", KEY_PREFIX_USER), json!("en"));
    state1.insert("context".to_string(), json!("session1"));
    
    let session1 = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "alice".to_string(),
        session_id: Some("s1".to_string()),
        state: state1,
    }).await?;
    
    // Crée une deuxième session pour le même utilisateur
    let mut state2 = HashMap::new();
    state2.insert("context".to_string(), json!("session2"));
    
    let session2 = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "alice".to_string(),
        session_id: Some("s2".to_string()),
        state: state2,
    }).await?;
    
    // La Session 2 hérite de l'état de l'application et de l'utilisateur
    let s2_state = session2.state();
    
    // L'état de l'application est partagé
    assert_eq!(s2_state.get("app:theme"), Some(json!("dark")));
    
    // L'état de l'utilisateur est partagé
    assert_eq!(s2_state.get("user:language"), Some(json!("en")));
    
    // L'état de la session est séparé
    assert_eq!(s2_state.get("context"), Some(json!("session2")));
    
    println!("La portée de l'état fonctionne correctement !");
    Ok(())
}

Templating d'instructions avec l'état

Les valeurs d'état peuvent être injectées dans les instructions de l'agent en utilisant la syntaxe {key} :

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("personalized_assistant")
    .instruction("You are helping {user:name} with {topic}. Their preferred language is {user:language}.")
    .model(Arc::new(model))
    .build()?;

Lorsque l'agent s'exécute, {user:name}, {topic} et {user:language} sont remplacés par les valeurs de l'état de la session.

Bonnes Pratiques

1. Utiliser des Portées Appropriées

// ✅ Bien : Préférences utilisateur dans la portée utilisateur
"user:theme"
"user:timezone"

// ✅ Bien : Contexte spécifique à la session sans préfixe
"current_task"
"conversation_summary"

// ✅ Bien : Paramètres globaux de l'application dans la portée de l'application
"app:model_version"
"app:feature_flags"

// ❌ Mauvais : Données utilisateur dans la portée de la session (perdues entre les sessions)
"user_preferences"  // Devrait être "user:preferences"

2. Utiliser un État Temporaire pour les Données Intermédiaires

// ✅ Bien : Résultats intermédiaires dans la portée temporaire
"temp:search_results"
"temp:current_step"

// ❌ Mauvais : Données intermédiaires persistées inutilement
"search_results"  // Seront sauvegardées dans la base de données

3. Maintenir la Cohérence des Clés d'État

// ✅ Bien : Convention de nommage cohérente
"user:preferences.theme"
"user:preferences.language"

// ❌ Mauvais : Nommage incohérent
"user:theme"
"userLanguage"
"user-timezone"

Liens Utiles

  • Sessions - Vue d'ensemble de la gestion des sessions
  • Événements - Structure des événements et state_delta
  • LlmAgent - Modélisation des instructions

Précédent: ← Sessions | Suivant: Callbacks →