1use async_trait::async_trait;
5use futures_util::{stream, Stream, StreamExt};
6use lc_callbacks::{CallbackManager, RunTree, RunType};
7use lc_core::runnables::RunnableConfig;
8use lc_schema::Message;
9use lc_shared::document::Document;
10use serde_json::{json, Value};
11use std::collections::HashMap;
12use std::future::Future;
13use std::pin::Pin;
14use std::sync::Arc;
15
16#[derive(Debug, thiserror::Error)]
18#[non_exhaustive]
19pub enum ChainError {
20 #[error("Missing input: {0}")]
22 MissingInput(String),
23
24 #[error("Input error: {0}")]
26 InputError(String),
27
28 #[error("Output error: {0}")]
30 OutputError(String),
31
32 #[error("Execution error: {0}")]
34 ExecutionError(String),
35
36 #[error("Stream error: {0}")]
38 StreamError(String),
39
40 #[error("Chain error: {0}")]
42 Other(String),
43
44 #[error("{context}: {source}")]
52 Nested {
53 context: String,
55 #[source]
57 source: Box<dyn std::error::Error + Send + Sync>,
58 },
59}
60
61pub type ChainResult = HashMap<String, Value>;
63
64#[derive(Debug, Clone)]
66pub struct StreamToken {
67 pub token: String,
69 pub is_final: bool,
71}
72
73pub type ChainStream = Pin<Box<dyn Stream<Item = Result<StreamToken, ChainError>> + Send>>;
75
76pub(crate) fn variables_to_messages(vars: &HashMap<String, Value>) -> Vec<Message> {
86 lc_memory::memory_variables_to_messages(vars)
89}
90
91pub(crate) fn documents_from_input(value: Option<&Value>) -> Result<Vec<Document>, ChainError> {
99 let arr = value
100 .and_then(|v| v.as_array())
101 .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
102
103 let mut docs = Vec::with_capacity(arr.len());
104 let mut failed = 0usize;
105 for item in arr {
106 match serde_json::from_value::<Document>(item.clone()) {
107 Ok(doc) => docs.push(doc),
108 Err(_) => failed += 1,
109 }
110 }
111 if failed > 0 {
112 return Err(ChainError::InputError(format!(
113 "document deserialization failed: {failed} of {} document(s) lost",
114 arr.len()
115 )));
116 }
117 Ok(docs)
118}
119
120pub(crate) fn documents_to_values(documents: &[Document]) -> Result<Vec<Value>, ChainError> {
123 documents
124 .iter()
125 .map(|doc| {
126 serde_json::to_value(doc)
127 .map_err(|e| ChainError::Other(format!("failed to serialize document: {e}")))
128 })
129 .collect()
130}
131
132#[async_trait]
136pub trait BaseChain: Send + Sync {
137 fn input_keys(&self) -> Vec<&str>;
139
140 fn output_keys(&self) -> Vec<&str>;
142
143 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError>;
151
152 async fn invoke_with_config(
162 &self,
163 inputs: HashMap<String, Value>,
164 config: Option<RunnableConfig>,
165 ) -> Result<ChainResult, ChainError> {
166 run_chain_with_callbacks(self.name(), inputs, config, |inputs| async move {
167 self.invoke(inputs).await
168 })
169 .await
170 }
171
172 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
184 let result = self.invoke(inputs).await?;
186 let output_text = result
189 .values()
190 .next()
191 .and_then(|v| v.as_str())
192 .ok_or_else(|| {
193 ChainError::OutputError("chain produced no string output to stream".to_string())
194 })?
195 .to_string();
196 let stream = futures_util::stream::once(async move {
197 Ok(StreamToken {
198 token: output_text,
199 is_final: true,
200 })
201 });
202 Ok(Box::pin(stream))
203 }
204
205 async fn stream_with_config(
211 &self,
212 inputs: HashMap<String, Value>,
213 config: Option<RunnableConfig>,
214 ) -> Result<ChainStream, ChainError> {
215 stream_chain_with_callbacks(self.name(), inputs, config, |inputs| async move {
216 self.stream(inputs).await
217 })
218 .await
219 }
220
221 fn validate_inputs(&self, inputs: &HashMap<String, Value>) -> Result<(), ChainError> {
223 for key in self.input_keys() {
224 if !inputs.contains_key(key) {
225 return Err(ChainError::MissingInput(key.to_string()));
226 }
227 }
228 Ok(())
229 }
230
231 fn name(&self) -> &str {
233 "chain"
234 }
235}
236
237pub(crate) async fn run_chain_with_callbacks<F, Fut>(
245 name: &str,
246 inputs: HashMap<String, Value>,
247 config: Option<RunnableConfig>,
248 body: F,
249) -> Result<ChainResult, ChainError>
250where
251 F: FnOnce(HashMap<String, Value>) -> Fut,
252 Fut: Future<Output = Result<ChainResult, ChainError>> + Send,
253{
254 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
255 let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
256
257 if let Some(ref cb) = callbacks {
258 cb.dispatch_chain_start(&run, &run.inputs).await;
259 }
260
261 let result = body(inputs).await;
262
263 match result {
264 Ok(output) => {
265 run.end(json!({ "output": output }));
266 if let Some(ref cb) = callbacks {
267 cb.dispatch_chain_end(&run, &json!({ "output": output }))
268 .await;
269 }
270 Ok(output)
271 }
272 Err(e) => {
273 let msg = e.to_string();
274 run.end_with_error(msg.clone());
275 if let Some(ref cb) = callbacks {
276 cb.dispatch_chain_error(&run, &msg).await;
277 }
278 Err(e)
279 }
280 }
281}
282
283pub(crate) async fn stream_chain_with_callbacks<F, Fut>(
290 name: &str,
291 inputs: HashMap<String, Value>,
292 config: Option<RunnableConfig>,
293 body: F,
294) -> Result<ChainStream, ChainError>
295where
296 F: FnOnce(HashMap<String, Value>) -> Fut,
297 Fut: Future<Output = Result<ChainStream, ChainError>> + Send,
298{
299 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
300 let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
301
302 if let Some(ref cb) = callbacks {
303 cb.dispatch_chain_start(&run, &run.inputs).await;
304 }
305
306 let stream = match body(inputs).await {
307 Ok(s) => s,
308 Err(e) => {
309 let msg = e.to_string();
310 run.end_with_error(msg.clone());
311 if let Some(ref cb) = callbacks {
312 cb.dispatch_chain_error(&run, &msg).await;
313 }
314 return Err(e);
315 }
316 };
317
318 Ok(Box::pin(end_stream_on_completion(stream, run, callbacks)))
319}
320
321fn end_stream_on_completion(
324 inner: ChainStream,
325 run: RunTree,
326 callbacks: Option<Arc<CallbackManager>>,
327) -> impl Stream<Item = Result<StreamToken, ChainError>> + Send {
328 stream::unfold(Some((inner, run, callbacks)), |state| async move {
329 let (mut inner, run, callbacks) = match state {
330 Some(s) => s,
331 None => return None,
332 };
333 match inner.next().await {
334 Some(Ok(token)) => Some((Ok(token), Some((inner, run, callbacks)))),
335 Some(Err(e)) => {
336 let msg = e.to_string();
337 let mut run = run;
338 run.end_with_error(msg.clone());
339 if let Some(cb) = callbacks {
340 cb.dispatch_chain_error(&run, &msg).await;
341 }
342 Some((Err(e), None))
343 }
344 None => {
345 let mut run = run;
346 run.end(json!({ "output": null }));
347 if let Some(cb) = callbacks {
348 cb.dispatch_chain_end(&run, &json!({ "output": null }))
349 .await;
350 }
351 None
352 }
353 }
354 })
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use std::error::Error;
361
362 #[test]
363 fn test_chain_error_display() {
364 let error = ChainError::MissingInput("test".to_string());
365 assert!(error.to_string().contains("Missing input"));
366
367 let error = ChainError::ExecutionError("test".to_string());
368 assert!(error.to_string().contains("Execution error"));
369 }
370
371 #[test]
372 fn test_chain_error_all_variants() {
373 let err = ChainError::MissingInput("key".to_string());
374 assert!(err.to_string().contains("key"));
375
376 let err = ChainError::OutputError("bad".to_string());
377 assert!(err.to_string().contains("bad"));
378
379 let err = ChainError::ExecutionError("fail".to_string());
380 assert!(err.to_string().contains("fail"));
381
382 let err = ChainError::StreamError("broken".to_string());
383 assert!(err.to_string().contains("broken"));
384
385 let err = ChainError::Other("misc".to_string());
386 assert!(err.to_string().contains("misc"));
387 }
388
389 #[test]
393 fn test_chain_error_nested_preserves_source() {
394 let inner = ChainError::MissingInput("text".to_string());
395 let nested = ChainError::Nested {
396 context: "Step 0 (echo) execution failed".to_string(),
397 source: Box::new(inner),
398 };
399 assert!(nested
400 .to_string()
401 .contains("Step 0 (echo) execution failed"));
402 assert!(nested.to_string().contains("Missing input"));
403
404 let source = nested.source().expect("Nested must carry a source");
405 let downcast = source.downcast_ref::<ChainError>();
406 assert!(
407 matches!(downcast, Some(ChainError::MissingInput(k)) if k == "text"),
408 "source should downcast back to the original variant, got {downcast:?}"
409 );
410 }
411
412 #[test]
413 fn test_stream_token_debug() {
414 let token = StreamToken {
415 token: "hello".to_string(),
416 is_final: false,
417 };
418 assert!(format!("{:?}", token).contains("hello"));
419 }
420
421 #[tokio::test]
425 async fn test_default_stream_errors_on_non_string_output() {
426 struct NonStringChain;
427 #[async_trait]
428 impl BaseChain for NonStringChain {
429 fn input_keys(&self) -> Vec<&str> {
430 vec![]
431 }
432 fn output_keys(&self) -> Vec<&str> {
433 vec!["count"]
434 }
435 async fn invoke(
436 &self,
437 _inputs: HashMap<String, Value>,
438 ) -> Result<ChainResult, ChainError> {
439 let mut result = HashMap::new();
440 result.insert("count".to_string(), json!(3));
441 Ok(result)
442 }
443 }
444
445 let chain = NonStringChain;
446 let err = match chain.stream(HashMap::new()).await {
447 Ok(_) => panic!("expected an OutputError"),
448 Err(e) => e,
449 };
450 assert!(
451 matches!(err, ChainError::OutputError(_)),
452 "expected OutputError, got {err:?}"
453 );
454 }
455
456 #[test]
457 fn test_validate_inputs_pass() {
458 struct PassthroughChain;
459 #[async_trait]
460 impl BaseChain for PassthroughChain {
461 fn input_keys(&self) -> Vec<&str> {
462 vec!["input"]
463 }
464 fn output_keys(&self) -> Vec<&str> {
465 vec!["output"]
466 }
467 async fn invoke(
468 &self,
469 inputs: HashMap<String, Value>,
470 ) -> Result<ChainResult, ChainError> {
471 Ok(inputs)
472 }
473 }
474
475 let chain = PassthroughChain;
476 let mut inputs = HashMap::new();
477 inputs.insert("input".to_string(), Value::String("test".to_string()));
478 assert!(chain.validate_inputs(&inputs).is_ok());
479 }
480
481 #[test]
482 fn test_validate_inputs_missing_key() {
483 struct PassthroughChain;
484 #[async_trait]
485 impl BaseChain for PassthroughChain {
486 fn input_keys(&self) -> Vec<&str> {
487 vec!["input"]
488 }
489 fn output_keys(&self) -> Vec<&str> {
490 vec!["output"]
491 }
492 async fn invoke(
493 &self,
494 _inputs: HashMap<String, Value>,
495 ) -> Result<ChainResult, ChainError> {
496 Ok(HashMap::new())
497 }
498 }
499
500 let chain = PassthroughChain;
501 let inputs = HashMap::new();
502 assert!(chain.validate_inputs(&inputs).is_err());
503 }
504
505 #[test]
506 fn test_default_chain_name() {
507 struct MyChain;
508 #[async_trait]
509 impl BaseChain for MyChain {
510 fn input_keys(&self) -> Vec<&str> {
511 vec![]
512 }
513 fn output_keys(&self) -> Vec<&str> {
514 vec![]
515 }
516 async fn invoke(
517 &self,
518 _inputs: HashMap<String, Value>,
519 ) -> Result<ChainResult, ChainError> {
520 Ok(HashMap::new())
521 }
522 }
523 let chain = MyChain;
524 assert_eq!(chain.name(), "chain");
525 }
526}