1mod memory;
2#[cfg(feature = "redis-store")]
3mod redis_store;
4
5use std::{env, sync::Arc};
6
7use anyhow::Result;
8use async_trait::async_trait;
9use greentic_types::{TenantCtx, session::SessionKey};
10use serde::{Deserialize, Serialize};
11use time::OffsetDateTime;
12#[cfg(not(feature = "redis-store"))]
13use tracing::warn;
14
15pub use memory::MemorySessionStore;
16#[cfg(feature = "redis-store")]
17pub use redis_store::RedisSessionStore;
18
19pub type SharedSessionStore = Arc<dyn SessionStore>;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct ConversationScope {
25 pub env: String,
26 pub tenant: String,
27 pub platform: String,
28 pub chat_id: String,
29 pub user_id: String,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub thread_id: Option<String>,
32}
33
34impl ConversationScope {
35 pub fn new(
36 env: impl Into<String>,
37 tenant: impl Into<String>,
38 platform: impl Into<String>,
39 chat_id: impl Into<String>,
40 user_id: impl Into<String>,
41 thread_id: Option<String>,
42 ) -> Self {
43 Self {
44 env: env.into(),
45 tenant: tenant.into(),
46 platform: platform.into(),
47 chat_id: chat_id.into(),
48 user_id: user_id.into(),
49 thread_id,
50 }
51 }
52
53 pub fn cache_key(&self) -> String {
54 format!(
55 "{}:{}:{}:{}:{}:{}",
56 self.env,
57 self.tenant,
58 self.platform,
59 self.chat_id,
60 self.user_id,
61 self.thread_id.as_deref().unwrap_or_default()
62 )
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SessionSnapshot {
69 pub tenant_ctx: TenantCtx,
70 pub flow_id: String,
71 pub cursor_node: String,
72 pub context_json: String,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SessionRecord {
78 pub key: SessionKey,
79 pub scope: ConversationScope,
80 pub snapshot: SessionSnapshot,
81 pub updated_unix_ms: i128,
82}
83
84impl SessionRecord {
85 pub fn new(key: SessionKey, scope: ConversationScope, snapshot: SessionSnapshot) -> Self {
86 Self {
87 key,
88 scope,
89 snapshot,
90 updated_unix_ms: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000,
91 }
92 }
93}
94
95#[async_trait]
96pub trait SessionStore: Send + Sync {
97 async fn save(&self, record: SessionRecord) -> Result<()>;
98 async fn get(&self, key: &SessionKey) -> Result<Option<SessionRecord>>;
99 async fn find_by_scope(&self, scope: &ConversationScope) -> Result<Option<SessionRecord>>;
100 async fn delete(&self, key: &SessionKey) -> Result<()>;
101}
102
103pub fn shared_memory_store() -> SharedSessionStore {
105 Arc::new(MemorySessionStore::new())
106}
107
108pub async fn store_from_env() -> Result<SharedSessionStore> {
113 match env::var("SESSION_REDIS_URL") {
114 Ok(url) => {
115 let namespace = env::var("SESSION_NAMESPACE").unwrap_or_else(|_| "gsm".into());
116 build_redis_store(&url, &namespace).await
117 }
118 Err(_) => Ok(shared_memory_store()),
119 }
120}
121
122#[cfg(feature = "redis-store")]
123async fn build_redis_store(url: &str, namespace: &str) -> Result<SharedSessionStore> {
124 let store = RedisSessionStore::connect(url, namespace).await?;
125 Ok(Arc::new(store))
126}
127
128#[cfg(not(feature = "redis-store"))]
129async fn build_redis_store(_url: &str, _namespace: &str) -> Result<SharedSessionStore> {
130 warn!("redis-store feature disabled; using in-memory session store");
131 Ok(shared_memory_store())
132}