use std::sync::Arc;
use synwire_core::BoxFuture;
use synwire_core::error::SynwireError;
use synwire_core::messages::Message;
use synwire_core::runnables::RunnableConfig;
use synwire_core::runnables::core::RunnableCore;
use super::traits::ChatMessageHistory;
pub type HistoryFactory = Arc<dyn Fn(&str) -> Arc<dyn ChatMessageHistory> + Send + Sync>;
pub struct RunnableWithMessageHistory {
inner: Arc<dyn RunnableCore>,
history_factory: HistoryFactory,
}
impl RunnableWithMessageHistory {
pub fn new(inner: Arc<dyn RunnableCore>, history_factory: HistoryFactory) -> Self {
Self {
inner,
history_factory,
}
}
}
impl RunnableCore for RunnableWithMessageHistory {
fn invoke<'a>(
&'a self,
input: serde_json::Value,
config: Option<&'a RunnableConfig>,
) -> BoxFuture<'a, Result<serde_json::Value, SynwireError>> {
Box::pin(async move {
let obj = input.as_object().ok_or_else(|| SynwireError::Prompt {
message: "RunnableWithMessageHistory input must be a JSON object".into(),
})?;
let session_id = obj
.get("session_id")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| SynwireError::Prompt {
message: "input must contain a \"session_id\" string field".into(),
})?;
let history = (self.history_factory)(session_id);
let history_msgs = history.get_messages().await?;
let input_messages: Vec<Message> = obj
.get("messages")
.map(|v| serde_json::from_value(v.clone()))
.transpose()
.map_err(|e| SynwireError::Prompt {
message: format!("failed to parse input messages: {e}"),
})?
.unwrap_or_default();
let mut all_messages = history_msgs;
all_messages.extend(input_messages.clone());
for msg in &input_messages {
history.add_message(msg.clone()).await?;
}
let combined_input = serde_json::json!({
"messages": all_messages,
});
let output = self.inner.invoke(combined_input, config).await?;
if let Some(output_messages) = output.get("messages") {
let out_msgs: Vec<Message> =
serde_json::from_value(output_messages.clone()).unwrap_or_default();
for msg in &out_msgs {
if msg.message_type() == "ai" {
history.add_message(msg.clone()).await?;
}
}
}
Ok(output)
})
}
#[allow(clippy::unnecessary_literal_bound)]
fn name(&self) -> &str {
"RunnableWithMessageHistory"
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::chat_history::InMemoryChatMessageHistory;
struct EchoRunnable;
impl RunnableCore for EchoRunnable {
fn invoke<'a>(
&'a self,
input: serde_json::Value,
_config: Option<&'a RunnableConfig>,
) -> BoxFuture<'a, Result<serde_json::Value, SynwireError>> {
Box::pin(async move { Ok(input) })
}
}
#[tokio::test]
async fn injects_history_into_input() {
let history = Arc::new(InMemoryChatMessageHistory::new());
history
.add_message(Message::human("Previous"))
.await
.unwrap();
let history_clone = history.clone();
let factory: HistoryFactory = Arc::new(move |_| history_clone.clone());
let runnable = RunnableWithMessageHistory::new(Arc::new(EchoRunnable), factory);
let input = serde_json::json!({
"session_id": "test",
"messages": [Message::human("Current")],
});
let output = runnable.invoke(input, None).await.unwrap();
let messages: Vec<Message> = serde_json::from_value(output["messages"].clone()).unwrap();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].content().as_text(), "Previous");
assert_eq!(messages[1].content().as_text(), "Current");
}
#[tokio::test]
async fn tracks_sessions_independently() {
let sessions: Arc<Mutex<HashMap<String, Arc<InMemoryChatMessageHistory>>>> =
Arc::new(Mutex::new(HashMap::new()));
let sessions_clone = sessions.clone();
let factory: HistoryFactory = Arc::new(move |session_id| {
let mut map = sessions_clone.lock().expect("lock");
map.entry(session_id.to_owned())
.or_insert_with(|| Arc::new(InMemoryChatMessageHistory::new()))
.clone()
});
let runnable = RunnableWithMessageHistory::new(Arc::new(EchoRunnable), factory);
let input_a = serde_json::json!({
"session_id": "A",
"messages": [Message::human("Hello A")],
});
let _output = runnable.invoke(input_a, None).await.unwrap();
let input_b = serde_json::json!({
"session_id": "B",
"messages": [Message::human("Hello B")],
});
let _output = runnable.invoke(input_b, None).await.unwrap();
let (history_a, history_b) = {
let map = sessions.lock().expect("lock");
(
Arc::clone(map.get("A").unwrap()),
Arc::clone(map.get("B").unwrap()),
)
};
let a_msgs = history_a.get_messages().await.unwrap();
let b_msgs = history_b.get_messages().await.unwrap();
assert_eq!(a_msgs.len(), 1);
assert_eq!(b_msgs.len(), 1);
assert_eq!(a_msgs[0].content().as_text(), "Hello A");
assert_eq!(b_msgs[0].content().as_text(), "Hello B");
}
use std::sync::Mutex;
}