use std::future::Future;
use std::sync::Arc;
use serde_json::{Map, Value};
use crate::config::Config;
use crate::error::{Error, Result};
use crate::instrumentation::{self, current_workflow, with_workflow};
#[derive(Debug, Clone)]
pub struct Workflow {
id: String,
name: String,
metadata: Option<Value>,
config: Arc<Config>,
}
pub async fn workflow<T, F, Fut>(
name: &str,
id: Option<&str>,
metadata: Option<Value>,
body: F,
) -> Result<T>
where
F: FnOnce(Workflow) -> Fut,
Fut: Future<Output = Result<T>>,
{
Workflow::new(name, id, metadata, crate::config())?
.run(body)
.await
}
impl Workflow {
pub fn new(
name: &str,
id: Option<&str>,
metadata: Option<Value>,
config: Arc<Config>,
) -> Result<Workflow> {
let id = id
.map(str::to_string)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
Ok(Workflow {
name: normalize(name, "name")?,
id: normalize(&id, "id")?,
metadata,
config,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
fn context(&self) -> Map<String, Value> {
let mut context = Map::new();
context.insert("workflow_id".into(), self.id.clone().into());
context.insert("workflow_name".into(), self.name.clone().into());
if let Some(m) = &self.metadata {
context.insert("workflow_metadata".into(), m.clone());
}
context
}
pub async fn run<T, F, Fut>(&self, body: F) -> Result<T>
where
F: FnOnce(Workflow) -> Fut,
Fut: Future<Output = Result<T>>,
{
let mut context = self.context();
if let Some(current) = current_workflow()
.filter(|c| c.get("workflow_id").and_then(Value::as_str) != Some(&self.id))
{
if let Some(parent) = current.get("workflow_id") {
context.insert("workflow_parent_id".into(), parent.clone());
}
if let Some(step) = current.get("workflow_step_id") {
context.insert("workflow_parent_step_id".into(), step.clone());
}
}
let config = self.config.clone();
let this = self.clone();
Box::pin(with_workflow(context, async move {
instrumentation::instrument(
&config,
"workflow.rust_llm",
Map::new(),
Box::pin(body(this)),
)
.await
}))
.await
}
pub async fn step<T>(
&self,
name: &str,
id: Option<&str>,
body: impl Future<Output = Result<T>>,
) -> Result<T> {
let step_id = id
.map(str::to_string)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let current = current_workflow();
let mut context = current
.clone()
.filter(|c| c.get("workflow_id").and_then(Value::as_str) == Some(&self.id))
.unwrap_or_else(|| self.context());
let parent = current
.filter(|c| c.get("workflow_id").and_then(Value::as_str) == Some(&self.id))
.and_then(|c| c.get("workflow_step_id").cloned());
context.remove("workflow_step_parent_id");
context.insert(
"workflow_step_id".into(),
normalize(&step_id, "step id")?.into(),
);
context.insert(
"workflow_step_name".into(),
normalize(name, "step name")?.into(),
);
if let Some(parent) = parent {
context.insert("workflow_step_parent_id".into(), parent);
}
let config = self.config.clone();
Box::pin(with_workflow(context, async move {
instrumentation::instrument(
&config,
"workflow_step.rust_llm",
Map::new(),
Box::pin(body),
)
.await
}))
.await
}
}
fn normalize(value: &str, attribute: &str) -> Result<String> {
if value.is_empty() {
return Err(Error::Argument(format!(
"workflow {attribute} cannot be empty"
)));
}
Ok(value.to_string())
}