use std::fmt;
use regex::Regex;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PipelineId(String);
impl PipelineId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
}
impl fmt::Display for PipelineId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MergeStrategy {
Concat { separator: String },
First,
Fastest { n: usize },
Custom { aggregator: String },
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum ExitCondition {
ToolCalled { tool_name: String },
OutputContains {
pattern: String,
#[allow(dead_code)]
compiled: Regex,
},
MaxIterations,
}
impl ExitCondition {
pub fn output_contains(pattern: impl Into<String>) -> Result<Self, String> {
let pattern = pattern.into();
let compiled =
Regex::new(&pattern).map_err(|e| format!("invalid regex '{pattern}': {e}"))?;
Ok(Self::OutputContains { pattern, compiled })
}
}
impl Serialize for ExitCondition {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
#[serde(tag = "type")]
enum Helper<'a> {
ToolCalled { tool_name: &'a str },
OutputContains { pattern: &'a str },
MaxIterations,
}
match self {
Self::ToolCalled { tool_name } => {
Helper::ToolCalled { tool_name }.serialize(serializer)
}
Self::OutputContains { pattern, .. } => {
Helper::OutputContains { pattern }.serialize(serializer)
}
Self::MaxIterations => Helper::MaxIterations.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for ExitCondition {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(tag = "type")]
enum Helper {
ToolCalled { tool_name: String },
OutputContains { pattern: String },
MaxIterations,
}
let h = Helper::deserialize(deserializer)?;
match h {
Helper::ToolCalled { tool_name } => Ok(Self::ToolCalled { tool_name }),
Helper::OutputContains { pattern } => {
let compiled = Regex::new(&pattern).map_err(serde::de::Error::custom)?;
Ok(Self::OutputContains { pattern, compiled })
}
Helper::MaxIterations => Ok(Self::MaxIterations),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Pipeline {
Sequential {
id: PipelineId,
name: String,
steps: Vec<String>,
pass_context: bool,
},
Parallel {
id: PipelineId,
name: String,
branches: Vec<String>,
merge_strategy: MergeStrategy,
},
Loop {
id: PipelineId,
name: String,
body: String,
exit_condition: ExitCondition,
max_iterations: usize,
},
}
impl Pipeline {
pub fn sequential(name: impl Into<String>, steps: Vec<String>) -> Self {
Self::Sequential {
id: PipelineId::generate(),
name: name.into(),
steps,
pass_context: false,
}
}
pub fn sequential_with_context(name: impl Into<String>, steps: Vec<String>) -> Self {
Self::Sequential {
id: PipelineId::generate(),
name: name.into(),
steps,
pass_context: true,
}
}
pub fn parallel(
name: impl Into<String>,
branches: Vec<String>,
merge_strategy: MergeStrategy,
) -> Self {
Self::Parallel {
id: PipelineId::generate(),
name: name.into(),
branches,
merge_strategy,
}
}
pub fn loop_(
name: impl Into<String>,
body: impl Into<String>,
exit_condition: ExitCondition,
) -> Self {
Self::Loop {
id: PipelineId::generate(),
name: name.into(),
body: body.into(),
exit_condition,
max_iterations: 10,
}
}
pub fn loop_with_max(
name: impl Into<String>,
body: impl Into<String>,
exit_condition: ExitCondition,
max_iterations: usize,
) -> Self {
Self::Loop {
id: PipelineId::generate(),
name: name.into(),
body: body.into(),
exit_condition,
max_iterations,
}
}
#[must_use]
pub fn with_id(mut self, id: PipelineId) -> Self {
match &mut self {
Self::Sequential { id: i, .. }
| Self::Parallel { id: i, .. }
| Self::Loop { id: i, .. } => *i = id,
}
self
}
pub fn id(&self) -> &PipelineId {
match self {
Self::Sequential { id, .. } | Self::Parallel { id, .. } | Self::Loop { id, .. } => id,
}
}
pub fn name(&self) -> &str {
match self {
Self::Sequential { name, .. }
| Self::Parallel { name, .. }
| Self::Loop { name, .. } => name,
}
}
}
#[cfg(test)]
#[path = "types_tests.rs"]
mod tests;