use async_trait::async_trait;
use futures_util::StreamExt;
use lc_core::language_models::LLMResult;
use lc_core::{BaseChatModel, Runnable};
use lc_memory::{BaseMemory, ConversationBufferMemory};
use lc_schema::Message;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::base::{BaseChain, ChainError, ChainResult, ChainStream, StreamToken};
pub struct ConversationChain<M: BaseChatModel> {
llm: M,
memory: Arc<Mutex<dyn BaseMemory>>,
system_prompt: Option<String>,
input_key: String,
output_key: String,
name: String,
verbose: bool,
}
impl<M: BaseChatModel + 'static> ConversationChain<M> {
pub fn new(llm: M, memory: ConversationBufferMemory) -> Self {
Self::from_memory(llm, Arc::new(Mutex::new(memory.with_return_messages(true))))
}
pub fn from_memory(llm: M, memory: Arc<Mutex<dyn BaseMemory>>) -> Self {
Self {
llm,
memory,
system_prompt: None,
input_key: "input".to_string(),
output_key: "output".to_string(),
name: "conversation_chain".to_string(),
verbose: false,
}
}
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
self.input_key = key.into();
self
}
pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
self.output_key = key.into();
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn memory(&self) -> &Arc<Mutex<dyn BaseMemory>> {
&self.memory
}
pub fn builder(llm: M) -> ConversationChainBuilder<M> {
ConversationChainBuilder::new(llm)
}
pub async fn clear_memory(&self) -> Result<(), ChainError> {
let mut memory = self.memory.lock().await;
memory
.clear()
.await
.map_err(|e| ChainError::ExecutionError(format!("Failed to clear memory: {}", e)))?;
Ok(())
}
pub async fn predict(&self, input: impl Into<String>) -> Result<String, ChainError> {
let inputs = HashMap::from([(self.input_key.clone(), Value::String(input.into()))]);
let result = self.invoke(inputs).await?;
result
.get(&self.output_key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| ChainError::OutputError("Missing output".to_string()))
}
pub fn prepare_messages(&self, input: &str, history_messages: &[Message]) -> Vec<Message> {
let mut messages = Vec::new();
if let Some(system_prompt) = &self.system_prompt {
messages.push(Message::system(system_prompt));
}
for msg in history_messages {
messages.push(msg.clone());
}
messages.push(Message::human(input));
messages
}
async fn load_history(&self, input: &str) -> Result<Vec<Message>, ChainError> {
let memory = self.memory.lock().await;
let inputs = HashMap::from([(self.input_key.clone(), input.to_string())]);
let vars = memory
.load_memory_variables(&inputs)
.await
.map_err(|e| ChainError::ExecutionError(format!("Failed to load memory: {}", e)))?;
Ok(crate::base::variables_to_messages(&vars))
}
async fn save_context(&self, input: &str, output: &str) -> Result<(), ChainError> {
let mut memory = self.memory.lock().await;
let inputs = HashMap::from([(self.input_key.clone(), input.to_string())]);
let outputs = HashMap::from([(self.output_key.clone(), output.to_string())]);
memory
.save_context(&inputs, &outputs)
.await
.map_err(|e| ChainError::ExecutionError(format!("Failed to save context: {}", e)))?;
Ok(())
}
}
#[async_trait]
impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for ConversationChain<M>
where
<M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
{
fn input_keys(&self) -> Vec<&str> {
vec![&self.input_key]
}
fn output_keys(&self) -> Vec<&str> {
vec![&self.output_key]
}
async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
self.validate_inputs(&inputs)?;
let input = inputs
.get(&self.input_key)
.and_then(|v| v.as_str())
.ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
if self.verbose {
println!("\n=== ConversationChain execution ===");
println!("User input: {}", input);
}
let history_messages = self.load_history(input).await?;
if self.verbose && !history_messages.is_empty() {
println!("History message count: {}", history_messages.len());
}
let messages = self.prepare_messages(input, &history_messages);
if self.verbose {
println!("Total message count: {}", messages.len());
}
let result = self
.llm
.invoke(messages, None)
.await
.map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
let output = result.content;
if self.verbose {
println!("AI response: {}", output);
}
self.save_context(input, &output).await?;
if self.verbose {
println!("=== ConversationChain complete ===\n");
}
let mut result = HashMap::new();
result.insert(self.output_key.clone(), Value::String(output));
Ok(result)
}
async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
self.validate_inputs(&inputs)?;
let input = inputs
.get(&self.input_key)
.and_then(|v| v.as_str())
.ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
let history_messages = self.load_history(input).await?;
let messages = self.prepare_messages(input, &history_messages);
let llm_stream = self
.llm
.stream_chat(messages, None)
.await
.map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
let memory = self.memory.clone();
let input_key = self.input_key.clone();
let output_key = self.output_key.clone();
let input_str = input.to_string();
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
let stream = llm_stream.map(move |result| match result {
Ok(token) => {
let _ = tx.send(token.clone());
Ok(StreamToken {
token,
is_final: false,
})
}
Err(e) => Err(ChainError::StreamError(format!(
"Stream token error: {}",
e
))),
});
let finalizer_stream = async move {
let mut output = String::new();
let mut rx = rx;
while let Some(token) = rx.recv().await {
output.push_str(&token);
}
if !output.is_empty() {
let mut mem = memory.lock().await;
let ctx_inputs = HashMap::from([(input_key.clone(), input_str.clone())]);
let ctx_outputs = HashMap::from([(output_key.clone(), output)]);
if let Err(e) = mem.save_context(&ctx_inputs, &ctx_outputs).await {
log::error!("[ConversationChain] failed to save context: {}", e);
}
}
};
let final_stream = stream.chain(futures_util::stream::once(async move {
finalizer_stream.await;
Ok(StreamToken {
token: String::new(),
is_final: true,
})
}));
Ok(Box::pin(final_stream))
}
fn name(&self) -> &str {
&self.name
}
}
pub struct ConversationChainBuilder<M: BaseChatModel> {
llm: M,
memory: Option<Arc<Mutex<dyn BaseMemory>>>,
system_prompt: Option<String>,
input_key: Option<String>,
output_key: Option<String>,
name: Option<String>,
verbose: Option<bool>,
}
impl<M: BaseChatModel + 'static> ConversationChainBuilder<M> {
pub fn new(llm: M) -> Self {
Self {
llm,
memory: None,
system_prompt: None,
input_key: None,
output_key: None,
name: None,
verbose: None,
}
}
pub fn memory<Mem: BaseMemory + 'static>(mut self, memory: Mem) -> Self {
self.memory = Some(Arc::new(Mutex::new(memory)));
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn input_key(mut self, key: impl Into<String>) -> Self {
self.input_key = Some(key.into());
self
}
pub fn output_key(mut self, key: impl Into<String>) -> Self {
self.output_key = Some(key.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = Some(verbose);
self
}
pub fn build(self) -> ConversationChain<M> {
let mut chain = match self.memory {
Some(memory) => ConversationChain::from_memory(self.llm, memory),
None => ConversationChain::new(self.llm, ConversationBufferMemory::new()),
};
if let Some(prompt) = self.system_prompt {
chain = chain.with_system_prompt(prompt);
}
if let Some(key) = self.input_key {
chain = chain.with_input_key(key);
}
if let Some(key) = self.output_key {
chain = chain.with_output_key(key);
}
if let Some(name) = self.name {
chain = chain.with_name(name);
}
if let Some(verbose) = self.verbose {
chain = chain.with_verbose(verbose);
}
chain
}
}