use async_trait::async_trait;
use futures::TryStreamExt;
use mongodb::{
bson::{doc, from_document, to_document, DateTime, Document as BsonDocument},
options::{FindOptions, IndexOptions},
Client, Collection, IndexModel,
};
use tokio::sync::OnceCell;
use crate::schemas::{
memory::{BaseMemory, MemoryError},
messages::Message,
};
pub const DEFAULT_MONGO_HISTORY_DATABASE: &str = "chat_history";
pub const DEFAULT_MONGO_HISTORY_COLLECTION: &str = "message_store";
const SESSION_ID: &str = "session_id";
const MESSAGE: &str = "message";
const CREATED_AT: &str = "created_at";
const INDEX_NAME: &str = "session_id_1__id_1";
pub struct MongoMessageHistory {
collection: Collection<BsonDocument>,
session_id: String,
create_index: bool,
index_ready: OnceCell<()>,
}
impl MongoMessageHistory {
pub async fn from_uri(uri: &str, session_id: impl Into<String>) -> Result<Self, MemoryError> {
let client = Client::with_uri_str(uri).await?;
let collection = client
.database(DEFAULT_MONGO_HISTORY_DATABASE)
.collection(DEFAULT_MONGO_HISTORY_COLLECTION);
Ok(Self::new(collection, session_id))
}
pub fn new(collection: Collection<BsonDocument>, session_id: impl Into<String>) -> Self {
Self {
collection,
session_id: session_id.into(),
create_index: true,
index_ready: OnceCell::new(),
}
}
pub fn with_collection(mut self, database: &str, collection: &str) -> Self {
self.collection = self
.collection
.client()
.database(database)
.collection(collection);
self.index_ready = OnceCell::new();
self
}
pub fn without_index(mut self) -> Self {
self.create_index = false;
self
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn collection(&self) -> &Collection<BsonDocument> {
&self.collection
}
pub(crate) fn session_filter(&self) -> BsonDocument {
doc! { SESSION_ID: &self.session_id }
}
pub(crate) fn index_model() -> IndexModel {
IndexModel::builder()
.keys(doc! { SESSION_ID: 1, "_id": 1 })
.options(IndexOptions::builder().name(INDEX_NAME.to_string()).build())
.build()
}
pub(crate) fn find_options() -> FindOptions {
FindOptions::builder()
.sort(doc! { "_id": 1 })
.projection(doc! { MESSAGE: 1 })
.build()
}
pub(crate) fn to_stored(&self, message: &Message) -> Result<BsonDocument, MemoryError> {
let message = to_document(message)
.map_err(|e| MemoryError::Other(format!("can't convert the message to BSON: {e}")))?;
Ok(doc! {
SESSION_ID: &self.session_id,
MESSAGE: message,
CREATED_AT: DateTime::now(),
})
}
pub(crate) fn from_stored(stored: &BsonDocument) -> Result<Message, MemoryError> {
let message = stored.get_document(MESSAGE).map_err(|e| {
MemoryError::Other(format!(
"stored chat message has no `{MESSAGE}` document: {e}"
))
})?;
from_document(message.clone())
.map_err(|e| MemoryError::Other(format!("invalid stored chat message: {e}")))
}
async fn ensure_index(&self) -> Result<(), MemoryError> {
if !self.create_index {
return Ok(());
}
self.index_ready
.get_or_try_init(|| async {
self.collection.create_index(Self::index_model()).await?;
Ok::<_, MemoryError>(())
})
.await?;
Ok(())
}
}
#[async_trait]
impl BaseMemory for MongoMessageHistory {
async fn messages(&self) -> Result<Vec<Message>, MemoryError> {
let stored: Vec<BsonDocument> = self
.collection
.find(self.session_filter())
.with_options(Self::find_options())
.await?
.try_collect()
.await?;
stored.iter().map(Self::from_stored).collect()
}
async fn add_message(&mut self, message: Message) -> Result<(), MemoryError> {
self.ensure_index().await?;
self.collection
.insert_one(self.to_stored(&message)?)
.await?;
Ok(())
}
async fn clear(&mut self) -> Result<(), MemoryError> {
self.collection.delete_many(self.session_filter()).await?;
Ok(())
}
}
impl From<MongoMessageHistory> for std::sync::Arc<tokio::sync::Mutex<dyn BaseMemory>> {
fn from(memory: MongoMessageHistory) -> Self {
std::sync::Arc::new(tokio::sync::Mutex::new(memory))
}
}
#[cfg(test)]
mod tests;