use dashmap::DashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
use uuid::Uuid;
use crate::types::ChatMessage;
#[derive(Clone)]
pub struct SessionStore {
inner: Arc<DashMap<String, Vec<ChatMessage>>>,
reasoning: Arc<DashMap<String, String>>,
turn_reasoning: Arc<DashMap<u64, String>>,
}
impl SessionStore {
pub fn new() -> Self {
Self {
inner: Arc::new(DashMap::new()),
reasoning: Arc::new(DashMap::new()),
turn_reasoning: Arc::new(DashMap::new()),
}
}
pub fn store_reasoning(&self, call_id: String, reasoning: String) {
if !reasoning.is_empty() {
self.reasoning.insert(call_id, reasoning);
}
}
pub fn get_reasoning(&self, call_id: &str) -> Option<String> {
self.reasoning.get(call_id).map(|v| v.clone())
}
pub fn store_turn_reasoning(&self, _prior: &[ChatMessage], assistant: &ChatMessage, reasoning: String) {
if !reasoning.is_empty() {
let content = assistant.content.as_deref().unwrap_or("");
if !content.is_empty() {
let key = Self::content_key(content);
self.turn_reasoning.insert(key, reasoning.clone());
}
if let Some(tcs) = &assistant.tool_calls {
for tc in tcs {
if let Some(id) = tc.get("id").and_then(|v| v.as_str()) {
if !id.is_empty() {
self.store_reasoning(id.to_string(), reasoning.clone());
}
}
}
}
}
}
pub fn get_turn_reasoning(&self, _prior: &[ChatMessage], assistant: &ChatMessage) -> Option<String> {
let content = assistant.content.as_deref().unwrap_or("");
if content.is_empty() {
return None;
}
let key = Self::content_key(content);
self.turn_reasoning.get(&key).map(|v| v.clone())
}
fn content_key(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
pub fn get_history(&self, response_id: &str) -> Vec<ChatMessage> {
self.inner
.get(response_id)
.map(|v| v.clone())
.unwrap_or_default()
}
pub fn new_id(&self) -> String {
format!("resp_{}", Uuid::new_v4().simple())
}
pub fn save_with_id(&self, id: String, messages: Vec<ChatMessage>) {
self.inner.insert(id, messages);
}
pub fn save(&self, messages: Vec<ChatMessage>) -> String {
let id = self.new_id();
self.inner.insert(id.clone(), messages);
id
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ChatMessage;
fn msg(role: &str, content: Option<&str>) -> ChatMessage {
ChatMessage {
role: role.into(),
content: content.map(Into::into),
reasoning_content: None,
tool_calls: None,
tool_call_id: None,
name: None,
}
}
#[test]
fn test_store_and_get_reasoning() {
let store = SessionStore::new();
store.store_reasoning("call_1".into(), "think".into());
assert_eq!(store.get_reasoning("call_1"), Some("think".into()));
}
#[test]
fn test_get_reasoning_missing() {
let store = SessionStore::new();
assert_eq!(store.get_reasoning("nonexistent"), None);
}
#[test]
fn test_empty_reasoning_not_stored() {
let store = SessionStore::new();
store.store_reasoning("call_e".into(), "".into());
assert_eq!(store.get_reasoning("call_e"), None);
}
#[test]
fn test_turn_reasoning_by_content() {
let store = SessionStore::new();
let assistant = msg("assistant", Some("hello world"));
store.store_turn_reasoning(&[], &assistant, "deep thought".into());
assert_eq!(
store.get_turn_reasoning(&[], &assistant),
Some("deep thought".into())
);
}
#[test]
fn test_turn_reasoning_empty_content() {
let store = SessionStore::new();
let assistant = msg("assistant", Some(""));
store.store_turn_reasoning(&[], &assistant, "reason".into());
assert_eq!(store.get_turn_reasoning(&[], &assistant), None);
}
#[test]
fn test_turn_reasoning_also_stores_call_ids() {
let store = SessionStore::new();
let mut assistant = msg("assistant", Some("hi"));
assistant.tool_calls = Some(vec![serde_json::json!({
"id": "call_123",
"type": "function",
"function": {"name": "exec", "arguments": "{}"}
})]);
store.store_turn_reasoning(&[], &assistant, "reason_tc".into());
assert_eq!(store.get_reasoning("call_123"), Some("reason_tc".into()));
}
#[test]
fn test_history_save_and_get() {
let store = SessionStore::new();
let msgs = vec![msg("user", Some("hi")), msg("assistant", Some("hey"))];
let id = store.save(msgs.clone());
let got = store.get_history(&id);
assert_eq!(got.len(), 2);
assert_eq!(got[0].content.as_deref(), Some("hi"));
let id2 = store.new_id();
store.save_with_id(id2.clone(), vec![msg("user", Some("q"))]);
assert_eq!(store.get_history(&id2).len(), 1);
}
#[test]
fn test_content_key_deterministic() {
let a = SessionStore::content_key("same text");
let b = SessionStore::content_key("same text");
assert_eq!(a, b);
let c = SessionStore::content_key("different");
assert_ne!(a, c);
}
}