use async_trait::async_trait;
use futures_util::Stream;
use lc_core::runnables::RunnableConfig;
use serde_json::Value;
use std::collections::HashMap;
use std::pin::Pin;
#[derive(Debug, thiserror::Error)]
pub enum ChainError {
#[error("Missing input: {0}")]
MissingInput(String),
#[error("Output error: {0}")]
OutputError(String),
#[error("Execution error: {0}")]
ExecutionError(String),
#[error("Stream error: {0}")]
StreamError(String),
#[error("Chain error: {0}")]
Other(String),
}
pub type ChainResult = HashMap<String, Value>;
#[derive(Debug, Clone)]
pub struct StreamToken {
pub token: String,
pub is_final: bool,
}
pub type ChainStream = Pin<Box<dyn Stream<Item = Result<StreamToken, ChainError>> + Send>>;
#[async_trait]
pub trait BaseChain: Send + Sync {
fn input_keys(&self) -> Vec<&str>;
fn output_keys(&self) -> Vec<&str>;
async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError>;
async fn invoke_with_config(
&self,
inputs: HashMap<String, Value>,
config: Option<RunnableConfig>,
) -> Result<ChainResult, ChainError> {
let _ = config; self.invoke(inputs).await
}
async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
let result = self.invoke(inputs).await?;
let output_text = result
.values()
.next()
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let stream = futures_util::stream::once(async move {
Ok(StreamToken {
token: output_text,
is_final: true,
})
});
Ok(Box::pin(stream))
}
async fn stream_with_config(
&self,
inputs: HashMap<String, Value>,
config: Option<RunnableConfig>,
) -> Result<ChainStream, ChainError> {
let _ = config;
self.stream(inputs).await
}
fn validate_inputs(&self, inputs: &HashMap<String, Value>) -> Result<(), ChainError> {
for key in self.input_keys() {
if !inputs.contains_key(key) {
return Err(ChainError::MissingInput(key.to_string()));
}
}
Ok(())
}
fn name(&self) -> &str {
"chain"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_error_display() {
let error = ChainError::MissingInput("test".to_string());
assert!(error.to_string().contains("Missing input"));
let error = ChainError::ExecutionError("test".to_string());
assert!(error.to_string().contains("Execution error"));
}
#[test]
fn test_chain_error_all_variants() {
let err = ChainError::MissingInput("key".to_string());
assert!(err.to_string().contains("key"));
let err = ChainError::OutputError("bad".to_string());
assert!(err.to_string().contains("bad"));
let err = ChainError::ExecutionError("fail".to_string());
assert!(err.to_string().contains("fail"));
let err = ChainError::StreamError("broken".to_string());
assert!(err.to_string().contains("broken"));
let err = ChainError::Other("misc".to_string());
assert!(err.to_string().contains("misc"));
}
#[test]
fn test_stream_token_debug() {
let token = StreamToken {
token: "hello".to_string(),
is_final: false,
};
assert!(format!("{:?}", token).contains("hello"));
}
#[test]
fn test_validate_inputs_pass() {
struct PassthroughChain;
#[async_trait]
impl BaseChain for PassthroughChain {
fn input_keys(&self) -> Vec<&str> { vec!["input"] }
fn output_keys(&self) -> Vec<&str> { vec!["output"] }
async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
Ok(inputs)
}
}
let chain = PassthroughChain;
let mut inputs = HashMap::new();
inputs.insert("input".to_string(), Value::String("test".to_string()));
assert!(chain.validate_inputs(&inputs).is_ok());
}
#[test]
fn test_validate_inputs_missing_key() {
struct PassthroughChain;
#[async_trait]
impl BaseChain for PassthroughChain {
fn input_keys(&self) -> Vec<&str> { vec!["input"] }
fn output_keys(&self) -> Vec<&str> { vec!["output"] }
async fn invoke(&self, _inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
Ok(HashMap::new())
}
}
let chain = PassthroughChain;
let inputs = HashMap::new();
assert!(chain.validate_inputs(&inputs).is_err());
}
#[test]
fn test_default_chain_name() {
struct MyChain;
#[async_trait]
impl BaseChain for MyChain {
fn input_keys(&self) -> Vec<&str> { vec![] }
fn output_keys(&self) -> Vec<&str> { vec![] }
async fn invoke(&self, _inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
Ok(HashMap::new())
}
}
let chain = MyChain;
assert_eq!(chain.name(), "chain");
}
}