use super::agent::LLMAgent;
use super::types::{LLMError, LLMResult};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
enum PipelineStep {
Agent {
agent: Arc<LLMAgent>,
prompt_template: Option<String>,
session_id: Option<String>,
},
Transform(Arc<dyn Fn(String) -> String + Send + Sync>),
AsyncTransform(
Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync>,
),
Filter(Arc<dyn Fn(&str) -> bool + Send + Sync>),
Branch {
condition: Arc<dyn Fn(&str) -> bool + Send + Sync>,
if_true: Box<PipelineStep>,
if_false: Box<PipelineStep>,
},
TryRecover {
step: Box<PipelineStep>,
default: String,
},
Retry {
step: Box<PipelineStep>,
max_retries: usize,
},
Identity,
}
pub struct Pipeline {
steps: Vec<PipelineStep>,
}
impl Default for Pipeline {
fn default() -> Self {
Self::new()
}
}
impl Pipeline {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn from_agent(agent: Arc<LLMAgent>) -> Self {
Self::new().with_agent(agent)
}
pub fn with_agent(mut self, agent: Arc<LLMAgent>) -> Self {
self.steps.push(PipelineStep::Agent {
agent,
prompt_template: None,
session_id: None,
});
self
}
pub fn with_agent_template(
mut self,
agent: Arc<LLMAgent>,
template: impl Into<String>,
) -> Self {
self.steps.push(PipelineStep::Agent {
agent,
prompt_template: Some(template.into()),
session_id: None,
});
self
}
pub fn with_agent_session(
mut self,
agent: Arc<LLMAgent>,
session_id: impl Into<String>,
) -> Self {
self.steps.push(PipelineStep::Agent {
agent,
prompt_template: None,
session_id: Some(session_id.into()),
});
self
}
pub fn then(self, agent: Arc<LLMAgent>) -> Self {
self.with_agent(agent)
}
pub fn then_with_template(self, agent: Arc<LLMAgent>, template: impl Into<String>) -> Self {
self.with_agent_template(agent, template)
}
pub fn map<F>(mut self, f: F) -> Self
where
F: Fn(String) -> String + Send + Sync + 'static,
{
self.steps.push(PipelineStep::Transform(Arc::new(f)));
self
}
pub fn map_async<F, Fut>(mut self, f: F) -> Self
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = String> + Send + 'static,
{
self.steps
.push(PipelineStep::AsyncTransform(Arc::new(move |s| {
Box::pin(f(s))
})));
self
}
pub fn filter<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.steps.push(PipelineStep::Filter(Arc::new(f)));
self
}
pub fn branch<F>(mut self, condition: F, if_true: Pipeline, if_false: Pipeline) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
let true_step = if if_true.steps.is_empty() {
PipelineStep::Identity
} else if if_true.steps.len() == 1 {
if_true.steps.into_iter().next().unwrap()
} else {
PipelineStep::Identity };
let false_step = if if_false.steps.is_empty() {
PipelineStep::Identity
} else if if_false.steps.len() == 1 {
if_false.steps.into_iter().next().unwrap()
} else {
PipelineStep::Identity
};
self.steps.push(PipelineStep::Branch {
condition: Arc::new(condition),
if_true: Box::new(true_step),
if_false: Box::new(false_step),
});
self
}
pub fn try_or_default(mut self, default: impl Into<String>) -> Self {
if let Some(last_step) = self.steps.pop() {
self.steps.push(PipelineStep::TryRecover {
step: Box::new(last_step),
default: default.into(),
});
}
self
}
pub fn retry(mut self, max_retries: usize) -> Self {
if let Some(last_step) = self.steps.pop() {
self.steps.push(PipelineStep::Retry {
step: Box::new(last_step),
max_retries,
});
}
self
}
pub async fn run(&self, input: impl Into<String>) -> LLMResult<String> {
let mut current = input.into();
for step in &self.steps {
current = self.execute_step(step, current).await?;
}
Ok(current)
}
fn execute_step<'a>(
&'a self,
step: &'a PipelineStep,
input: String,
) -> Pin<Box<dyn Future<Output = LLMResult<String>> + Send + 'a>> {
Box::pin(async move {
match step {
PipelineStep::Agent {
agent,
prompt_template,
session_id,
} => {
let prompt = if let Some(template) = prompt_template {
template.replace("{input}", &input)
} else {
input
};
if let Some(sid) = session_id {
let _ = agent.get_or_create_session(sid).await;
agent.chat_with_session(sid, &prompt).await
} else {
agent.ask(&prompt).await
}
}
PipelineStep::Transform(f) => Ok(f(input)),
PipelineStep::AsyncTransform(f) => Ok(f(input).await),
PipelineStep::Filter(f) => {
if f(&input) {
Ok(input)
} else {
Err(LLMError::Other("Filtered out".to_string()))
}
}
PipelineStep::Branch {
condition,
if_true,
if_false,
} => {
if condition(&input) {
self.execute_step(if_true, input).await
} else {
self.execute_step(if_false, input).await
}
}
PipelineStep::TryRecover { step, default } => {
match self.execute_step(step, input).await {
Ok(result) => Ok(result),
Err(_) => Ok(default.clone()),
}
}
PipelineStep::Retry { step, max_retries } => {
let mut last_error = None;
for _ in 0..=*max_retries {
match self.execute_step(step, input.clone()).await {
Ok(result) => return Ok(result),
Err(e) => last_error = Some(e),
}
}
Err(last_error
.unwrap_or_else(|| LLMError::Other("Retry exhausted".to_string())))
}
PipelineStep::Identity => Ok(input),
}
})
}
}
#[macro_export]
macro_rules! pipeline {
() => {
$crate::llm::pipeline::Pipeline::new()
};
(agent => $agent:expr $(, $($rest:tt)*)?) => {
$crate::llm::pipeline::Pipeline::new()
.with_agent($agent)
$($(. $rest)*)?
};
(map => $f:expr $(, $($rest:tt)*)?) => {
$crate::llm::pipeline::Pipeline::new()
.map($f)
$($(. $rest)*)?
};
}
pub fn agent_pipe(agents: Vec<Arc<LLMAgent>>) -> Pipeline {
let mut pipeline = Pipeline::new();
for agent in agents {
pipeline = pipeline.with_agent(agent);
}
pipeline
}
pub fn agent_pipe_with_templates(agents: Vec<(Arc<LLMAgent>, impl Into<String>)>) -> Pipeline {
let mut pipeline = Pipeline::new();
for (agent, template) in agents {
pipeline = pipeline.with_agent_template(agent, template);
}
pipeline
}
pub async fn quick_ask(agent: &LLMAgent, question: impl Into<String>) -> LLMResult<String> {
agent.ask(question).await
}
pub async fn ask_with_template(
agent: &LLMAgent,
template: &str,
input: impl Into<String>,
) -> LLMResult<String> {
let prompt = template.replace("{input}", &input.into());
agent.ask(&prompt).await
}
pub async fn batch_ask(
agent: &LLMAgent,
questions: Vec<impl Into<String>>,
) -> Vec<LLMResult<String>> {
let mut results = Vec::new();
for question in questions {
results.push(agent.ask(question).await);
}
results
}
pub struct StreamPipeline {
agent: Arc<LLMAgent>,
pre_transform: Option<Arc<dyn Fn(String) -> String + Send + Sync>>,
post_transform: Option<Arc<dyn Fn(String) -> String + Send + Sync>>,
prompt_template: Option<String>,
}
impl StreamPipeline {
pub fn new(agent: Arc<LLMAgent>) -> Self {
Self {
agent,
pre_transform: None,
post_transform: None,
prompt_template: None,
}
}
pub fn pre_process<F>(mut self, f: F) -> Self
where
F: Fn(String) -> String + Send + Sync + 'static,
{
self.pre_transform = Some(Arc::new(f));
self
}
pub fn post_process<F>(mut self, f: F) -> Self
where
F: Fn(String) -> String + Send + Sync + 'static,
{
self.post_transform = Some(Arc::new(f));
self
}
pub fn with_template(mut self, template: impl Into<String>) -> Self {
self.prompt_template = Some(template.into());
self
}
pub async fn run_stream(
&self,
input: impl Into<String>,
) -> LLMResult<super::agent::TextStream> {
let mut input = input.into();
if let Some(ref pre) = self.pre_transform {
input = pre(input);
}
let prompt = if let Some(ref template) = self.prompt_template {
template.replace("{input}", &input)
} else {
input
};
self.agent.ask_stream(&prompt).await
}
pub async fn run(&self, input: impl Into<String>) -> LLMResult<String> {
use futures::StreamExt;
let mut stream = self.run_stream(input).await?;
let mut result = String::new();
while let Some(chunk) = stream.next().await {
match chunk {
Ok(text) => result.push_str(&text),
Err(e) => return Err(e),
}
}
if let Some(ref post) = self.post_transform {
result = post(result);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_transform() {
let pipeline = Pipeline::new()
.map(|s| s.to_uppercase())
.map(|s| format!("Hello, {}!", s));
assert!(!pipeline.steps.is_empty());
}
#[test]
fn test_pipeline_builder() {
let pipeline = Pipeline::new()
.map(|s| s.trim().to_string())
.map(|s| s.to_lowercase());
assert_eq!(pipeline.steps.len(), 2);
}
}