use crate::{client::LlmConfig, error::Result, types::PipelineContext, PipelineError};
#[derive(Clone)]
pub struct Stage {
pub name: String,
pub prompt_template: String,
pub system_prompt: Option<String>,
pub model: String,
pub config: LlmConfig,
pub enabled: bool,
}
impl Stage {
pub fn new(name: impl Into<String>, prompt_template: impl Into<String>) -> Self {
Self {
name: name.into(),
prompt_template: prompt_template.into(),
system_prompt: None,
model: "llama3.2:3b".to_string(),
config: LlmConfig::default(),
enabled: true,
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = model.into();
self
}
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn with_thinking(mut self, enabled: bool) -> Self {
self.config.thinking = enabled;
self
}
pub fn with_temperature(mut self, temp: f64) -> Self {
self.config.temperature = temp;
self
}
pub fn with_max_tokens(mut self, tokens: u32) -> Self {
self.config.max_tokens = tokens;
self
}
pub fn with_json_mode(mut self, enabled: bool) -> Self {
self.config.json_mode = enabled;
self
}
pub fn with_config(mut self, config: LlmConfig) -> Self {
self.config = config;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn render_prompt(&self, input: &str, context: &PipelineContext) -> String {
let mut rendered = self.prompt_template.clone();
rendered = rendered.replace("{input}", input);
for (key, value) in &context.data {
let placeholder = format!("{{{}}}", key);
rendered = rendered.replace(&placeholder, value);
}
rendered
}
pub fn render_system_prompt(&self, context: &PipelineContext) -> Option<String> {
self.system_prompt.as_ref().map(|sp| {
let mut rendered = sp.clone();
for (key, value) in &context.data {
let placeholder = format!("{{{}}}", key);
rendered = rendered.replace(&placeholder, value);
}
rendered
})
}
}
pub struct StageBuilder {
stage: Stage,
}
impl StageBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
stage: Stage {
name: name.into(),
prompt_template: String::new(),
system_prompt: None,
model: "llama3.2:3b".to_string(),
config: LlmConfig::default(),
enabled: true,
},
}
}
pub fn prompt(mut self, template: impl Into<String>) -> Self {
self.stage.prompt_template = template.into();
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.stage.system_prompt = Some(prompt.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.stage.model = model.into();
self
}
pub fn thinking(mut self, enabled: bool) -> Self {
self.stage.config.thinking = enabled;
self
}
pub fn temperature(mut self, temp: f64) -> Self {
self.stage.config.temperature = temp;
self
}
pub fn max_tokens(mut self, tokens: u32) -> Self {
self.stage.config.max_tokens = tokens;
self
}
pub fn json_mode(mut self, enabled: bool) -> Self {
self.stage.config.json_mode = enabled;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.stage.enabled = enabled;
self
}
pub fn build(self) -> Result<Stage> {
if self.stage.prompt_template.is_empty() {
return Err(PipelineError::InvalidConfig(
"Stage prompt template cannot be empty".to_string(),
));
}
Ok(self.stage)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stage_creation() {
let stage = Stage::new("test", "Test prompt: {input}")
.with_model("test-model")
.with_thinking(true)
.with_temperature(0.8);
assert_eq!(stage.name, "test");
assert_eq!(stage.model, "test-model");
assert!(stage.config.thinking);
assert_eq!(stage.config.temperature, 0.8);
assert!(stage.enabled);
}
#[test]
fn test_stage_disabled() {
let stage = Stage::new("test", "prompt").disabled();
assert!(!stage.enabled);
}
#[test]
fn test_prompt_rendering() {
let context = PipelineContext::new()
.insert("name", "Alice")
.insert("age", "30");
let stage = Stage::new("test", "Hello {name}, you are {age}. Input: {input}");
let rendered = stage.render_prompt("test input", &context);
assert_eq!(rendered, "Hello Alice, you are 30. Input: test input");
}
#[test]
fn test_prompt_rendering_no_context() {
let context = PipelineContext::new();
let stage = Stage::new("test", "Process: {input}");
let rendered = stage.render_prompt("my data", &context);
assert_eq!(rendered, "Process: my data");
}
#[test]
fn test_system_prompt_rendering() {
let context = PipelineContext::new().insert("domain", "medical");
let stage = Stage::new("test", "{input}").with_system_prompt("You are a {domain} expert.");
let rendered = stage.render_system_prompt(&context);
assert_eq!(rendered, Some("You are a medical expert.".to_string()));
}
#[test]
fn test_system_prompt_none() {
let stage = Stage::new("test", "{input}");
let rendered = stage.render_system_prompt(&PipelineContext::new());
assert!(rendered.is_none());
}
#[test]
fn test_stage_builder_success() {
let stage = StageBuilder::new("builder-test")
.prompt("Do something with {input}")
.model("my-model")
.thinking(true)
.temperature(0.5)
.max_tokens(1024)
.json_mode(true)
.build();
assert!(stage.is_ok());
let stage = stage.unwrap();
assert_eq!(stage.name, "builder-test");
assert_eq!(stage.model, "my-model");
assert!(stage.config.thinking);
assert_eq!(stage.config.temperature, 0.5);
assert_eq!(stage.config.max_tokens, 1024);
assert!(stage.config.json_mode);
}
#[test]
fn test_stage_builder_empty_prompt_fails() {
let result = StageBuilder::new("test").model("model").build();
assert!(result.is_err());
}
}