إدارة الحالة

تسمح حالة الجلسة (Session state) في adk-rust لـ Agents بتخزين واسترداد البيانات التي تستمر عبر دورات المحادثة. يتم تنظيم الحالة باستخدام بادئات المفاتيح (key prefixes) التي تحدد نطاق وعمر البيانات.

نظرة عامة

يتم تخزين الحالة كأزواج مفتاح-قيمة (key-value pairs) حيث:

  • المفاتيح هي سلاسل (strings) ذات بادئات اختيارية
  • القيم هي JSON values (serde_json::Value)

يتيح نظام البادئات مستويات مختلفة من النطاق (scoping levels):

  • نطاق الجلسة (Session-scoped): الافتراضي، مرتبط بجلسة واحدة
  • نطاق المستخدم (User-scoped): مشترك بين جميع الجلسات لمستخدم واحد
  • نطاق التطبيق (App-scoped): مشترك بين جميع مستخدمي التطبيق
  • مؤقت (Temporary): يتم مسحه بعد كل استدعاء

السمة State

تحدد السمة State الواجهة للوصول إلى الحالة:

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>;
}

توجد أيضًا سمة ReadonlyState للوصول للقراءة فقط:

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

بادئات مفاتيح الحالة

يستخدم adk-rust ثلاث بادئات مفاتيح للتحكم في تحديد نطاق الحالة:

PrefixConstantScope
app:KEY_PREFIX_APPمشترك بين جميع المستخدمين والجلسات
user:KEY_PREFIX_USERمشترك بين جميع الجلسات لمستخدم واحد
temp:KEY_PREFIX_TEMPيتم مسحه بعد كل استدعاء
(none)-نطاق الجلسة (افتراضي)

app: - حالة التطبيق

الحالة المشتركة بين جميع المستخدمين والجلسات لتطبيق ما.

use adk_session::KEY_PREFIX_APP;

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

حالات الاستخدام:

  • تهيئة التطبيق
  • الموارد المشتركة
  • العدادات أو الإحصائيات العامة

user: - حالة المستخدم

الحالة المشتركة بين جميع الجلسات لمستخدم معين.

use adk_session::KEY_PREFIX_USER;

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

حالات الاستخدام:

  • تفضيلات المستخدم
  • بيانات ملف تعريف المستخدم
  • سياق المستخدم عبر الجلسات

temp: - الحالة المؤقتة

الحالة التي يتم مسحها بعد كل استدعاء. لا يتم الاحتفاظ بها.

use adk_session::KEY_PREFIX_TEMP;

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

حالات الاستخدام:

  • نتائج الحسابات الوسيطة
  • سياق العملية الحالية
  • البيانات التي لا ينبغي الاحتفاظ بها

لا بادئة - حالة الجلسة

المفاتيح بدون بادئة تكون ذات نطاق جلسة (السلوك الافتراضي).

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

حالات الاستخدام:

  • سياق المحادثة
  • بيانات خاصة بالجلسة
  • الحالة خطوة بخطوة

تعيين الحالة الأولية

يمكن تهيئة الحالة عند إنشاء جلسة:

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(())
}

قراءة الحالة

يمكن الوصول إلى الحالة عبر أسلوب state() الخاص بـ Session:

let state = session.state();

// الحصول على مفتاح محدد
if let Some(value) = state.get("topic") {
    println!("Topic: {}", value);
}

// الحصول على الحالة ذات النطاق التطبيقي
if let Some(version) = state.get("app:version") {
    println!("App version: {}", version);
}

// الحصول على جميع الحالات
let all_state = state.all();
for (key, value) in all_state {
    println!("{}: {}", key, value);
}

تحديثات الحالة عبر الأحداث

عادةً ما يتم تحديث الحالة من خلال إجراءات الحدث. عند إضافة حدث إلى Session، يتم تطبيق state_delta الخاص به:

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()
};

// عند إضافة هذا الحدث، يتم تحديث الحالة
service.append_event(session.id(), event).await?;

سلوك نطاق الحالة

تتعامل خدمة Session مع نطاق الحالة تلقائيًا:

عند إنشاء Session

  1. استخراج المفاتيح التي تبدأ بـ app: → تخزينها في حالة التطبيق (app state)
  2. استخراج المفاتيح التي تبدأ بـ user: → تخزينها في حالة المستخدم (user state)
  3. المفاتيح المتبقية (باستثناء temp:) → تخزينها في حالة Session (session state)
  4. دمج جميع النطاقات لـ Session المُعادة

عند استرجاع Session

  1. تحميل حالة التطبيق (app state) للتطبيق
  2. تحميل حالة المستخدم (user state) للمستخدم
  3. تحميل حالة Session (session state)
  4. دمج جميع النطاقات (التطبيق ← المستخدم ← Session)

عند إضافة حدث

  1. استخراج دلتا الحالة (state delta) من الحدث
  2. تصفية المفاتيح التي تبدأ بـ temp: (لا يتم الاحتفاظ بها)
  3. تطبيق دلتا app: على حالة التطبيق (app state)
  4. تطبيق دلتا user: على حالة المستخدم (user state)
  5. تطبيق الدلتا المتبقية على حالة Session (session state)

مثال كامل

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();
    
    // إنشاء أول Session بحالة أولية
    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?;
    
    // إنشاء Session ثانية لنفس المستخدم
    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?;
    
    // Session 2 ترث حالة التطبيق والمستخدم
    let s2_state = session2.state();
    
    // حالة التطبيق مشتركة
    assert_eq!(s2_state.get("app:theme"), Some(json!("dark")));
    
    // حالة المستخدم مشتركة
    assert_eq!(s2_state.get("user:language"), Some(json!("en")));
    
    // حالة Session منفصلة
    assert_eq!(s2_state.get("context"), Some(json!("session2")));
    
    println!("State scoping works correctly!");
    Ok(())
}

قالب التعليمات مع الحالة

يمكن حقن قيم الحالة في تعليمات Agent باستخدام صيغة {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()?;

عندما يتم تشغيل Agent، يتم استبدال {user:name} و{topic} و{user:language} بالقيم من حالة Session.

أفضل الممارسات

1. استخدام النطاقات المناسبة

// ✅ جيد: تفضيلات المستخدم في نطاق المستخدم
"user:theme"
"user:timezone"

// ✅ جيد: سياق خاص بالجلسة بدون بادئة
"current_task"
"conversation_summary"

// ✅ جيد: إعدادات على مستوى التطبيق في نطاق التطبيق
"app:model_version"
"app:feature_flags"

// ❌ سيء: بيانات المستخدم في نطاق الجلسة (تُفقد بين الجلسات)
"user_preferences"  // يجب أن يكون "user:preferences"

2. استخدام الحالة المؤقتة للبيانات الوسيطة

// ✅ جيد: النتائج الوسيطة في النطاق المؤقت
"temp:search_results"
"temp:current_step"

// ❌ سيء: بيانات وسيطة يتم الاحتفاظ بها دون داعٍ
"search_results"  // سيتم حفظها في قاعدة البيانات

3. الحفاظ على اتساق مفاتيح الحالة

// ✅ جيد: اصطلاح تسمية متسق
"user:preferences.theme"
"user:preferences.language"

// ❌ سيء: تسمية غير متسقة
"user:theme"
"userLanguage"
"user-timezone"

ذات صلة

  • Sessions - نظرة عامة على إدارة الجلسات
  • Events - بنية الأحداث وstate_delta
  • LlmAgent - قوالب التعليمات

السابق: ← Sessions | التالي: Callbacks →