Skip to main content

potato_workflow/workflow/
flow.rs

1use crate::tasklist::TaskList;
2
3use crate::{
4    events::{EventTracker, TaskEvent},
5    workflow::error::WorkflowError,
6};
7pub use potato_agent::agents::{
8    agent::{Agent, PyAgent},
9    task::{Task, TaskStatus, WorkflowTask},
10};
11use potato_agent::{AgentError, PyAgentResponse};
12use potato_state::block_on;
13use potato_type::prompt::{parse_response_to_json, MessageNum};
14use potato_type::Provider;
15use potato_util::utils::depythonize_object_to_value;
16use potato_util::{create_uuid7, utils::update_serde_map_with, PyHelperFuncs};
17use pyo3::prelude::*;
18use pythonize::pythonize;
19use serde::{
20    de::{self, MapAccess, Visitor},
21    ser::SerializeStruct,
22    Deserialize, Deserializer, Serialize, Serializer,
23};
24use serde_json::Map;
25use serde_json::Value;
26use std::collections::{HashMap, HashSet};
27use std::sync::Arc;
28use std::sync::RwLock;
29use tracing::instrument;
30use tracing::{debug, error, info, warn};
31
32/// Python workflows are a work in progress
33use pyo3::types::PyDict;
34
35pub type Context = (HashMap<String, Vec<MessageNum>>, Value, Option<Arc<Value>>);
36
37#[derive(Debug)]
38#[pyclass(skip_from_py_object)]
39pub struct WorkflowResult {
40    #[pyo3(get)]
41    pub tasks: HashMap<String, Py<WorkflowTask>>,
42
43    #[pyo3(get)]
44    pub events: Vec<TaskEvent>,
45
46    last_task_id: Option<String>,
47}
48
49impl WorkflowResult {
50    pub fn new(
51        py: Python,
52        tasks: HashMap<String, Task>,
53        output_types: &HashMap<String, Arc<Py<PyAny>>>,
54        events: Vec<TaskEvent>,
55        last_task_id: Option<String>,
56    ) -> Self {
57        let py_tasks = tasks
58            .into_iter()
59            .map(|(id, task)| {
60                let py_agent_response = if let Some(result) = task.result {
61                    let output_type = output_types.get(&id).map(|arc| arc.as_ref().clone_ref(py));
62                    Some(PyAgentResponse::new(result, output_type))
63                } else {
64                    None
65                };
66                let py_task = WorkflowTask {
67                    id: task.id.clone(),
68                    prompt: task.prompt,
69                    dependencies: task.dependencies,
70                    status: task.status,
71                    agent_id: task.agent_id,
72                    result: py_agent_response,
73                    max_retries: task.max_retries,
74                    retry_count: task.retry_count,
75                };
76                (id, Py::new(py, py_task).unwrap())
77            })
78            .collect::<HashMap<_, _>>();
79
80        Self {
81            tasks: py_tasks,
82            events,
83            last_task_id,
84        }
85    }
86}
87
88#[pymethods]
89impl WorkflowResult {
90    pub fn __str__(&self) -> String {
91        // serialize tasks to json
92        let json = serde_json::json!({
93            "tasks": serde_json::to_value(&self.tasks).unwrap_or(Value::Null),
94            "events": serde_json::to_value(&self.events).unwrap_or(Value::Null)
95        });
96
97        PyHelperFuncs::__str__(&json)
98    }
99
100    /// Get last task result
101    #[getter]
102    pub fn result<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, AgentError> {
103        if let Some(last_task_id) = &self.last_task_id {
104            if let Some(task) = self.tasks.get(last_task_id) {
105                let result = task.bind(py).getattr("result")?;
106                return Ok(result);
107            }
108        }
109        Ok(py.None().bind(py).clone())
110    }
111}
112
113/// Rust-specific implementation of a workflow
114#[derive(Debug, Clone)]
115pub struct Workflow {
116    pub id: String,
117    pub name: String,
118    pub task_list: TaskList,
119    pub agents: HashMap<String, Arc<Agent>>,
120    pub event_tracker: Arc<RwLock<EventTracker>>,
121    pub global_context: Option<Arc<Value>>,
122}
123
124impl PartialEq for Workflow {
125    fn eq(&self, other: &Self) -> bool {
126        // Compare by ID and name
127        self.id == other.id && self.name == other.name
128    }
129}
130
131impl Workflow {
132    /// Reload the agent clients by overwriting the existing clients
133    /// We are trying to solve the deserialization issue where GenAIClient requires
134    /// and async context to be created. During deserialization we don't have that context, so we default
135    /// to an Undefined client, but keep the other agent details. This means that if we try to run a workflow after deserialization
136    /// we will get an error when we try to execute a task with an Undefined client.
137    /// For this specific function, we can either make Arc<Agent> into RW compatible, or we can
138    /// rebuild the entire agents map with new Arcs. Given that the only mutation we need to do is to
139    /// rebuild the GenAIClient, we opt for the latter because we don't need to make everything else RW compatible.
140    /// This will incure a small startup cost, but it will be a one-time cost and 99% of the time unnoticed.
141    pub async fn reset_agents(&mut self) -> Result<(), WorkflowError> {
142        let mut agents_map = self.agents.clone();
143
144        for agent in self.agents.values_mut() {
145            agents_map.insert(agent.id.clone(), Arc::new(agent.rebuild_client().await?));
146        }
147        self.agents = agents_map;
148        Ok(())
149    }
150    pub fn new(name: &str) -> Self {
151        debug!("Creating new workflow: {}", name);
152        let id = create_uuid7();
153        Self {
154            id: id.clone(),
155            name: name.to_string(),
156            task_list: TaskList::new(),
157            agents: HashMap::new(),
158            event_tracker: Arc::new(RwLock::new(EventTracker::new(id))),
159            global_context: None, // Initialize with no global context
160        }
161    }
162    pub fn events(&self) -> Vec<TaskEvent> {
163        let tracker = self.event_tracker.read().unwrap();
164        let events = tracker.events.read().unwrap().clone();
165        events
166    }
167
168    pub fn total_duration(&self) -> i32 {
169        let tracker = self.event_tracker.read().unwrap();
170
171        if tracker.is_empty() {
172            0
173        } else {
174            //iter over each tracker event and get the details for each task event and get duration
175            let mut total_duration = chrono::Duration::zero();
176            for event in tracker.events.read().unwrap().iter() {
177                total_duration += event.details.duration.unwrap_or(chrono::Duration::zero());
178            }
179            total_duration.subsec_millis()
180        }
181    }
182
183    pub fn get_new_workflow(
184        &self,
185        global_context: Option<Arc<Value>>,
186    ) -> Result<Self, WorkflowError> {
187        // set new id for the new workflow
188        let id = create_uuid7();
189
190        // create deep copy of the tasklist so we don't clone the arc
191        let task_list = self.task_list.deep_clone()?;
192
193        Ok(Workflow {
194            id: id.clone(),
195            name: self.name.clone(),
196            task_list,
197            agents: self.agents.clone(), // Agents can be shared since they're read-only during execution
198            event_tracker: Arc::new(RwLock::new(EventTracker::new(id))),
199            global_context,
200        })
201    }
202
203    pub async fn run(
204        &self,
205        global_context: Option<Value>,
206    ) -> Result<Arc<RwLock<Workflow>>, WorkflowError> {
207        debug!("Running workflow: {}", self.name);
208
209        let global_context = global_context.map(Arc::new);
210        let run_workflow = Arc::new(RwLock::new(self.get_new_workflow(global_context)?));
211
212        execute_workflow(&run_workflow).await?;
213
214        Ok(run_workflow)
215    }
216
217    pub fn is_complete(&self) -> bool {
218        self.task_list.is_complete()
219    }
220
221    pub fn pending_count(&self) -> usize {
222        self.task_list.pending_count()
223    }
224
225    pub fn add_task(&mut self, task: Task) -> Result<(), WorkflowError> {
226        self.task_list.add_task(task)
227    }
228
229    pub fn add_tasks(&mut self, tasks: Vec<Task>) -> Result<(), WorkflowError> {
230        for task in tasks {
231            self.task_list.add_task(task)?;
232        }
233        Ok(())
234    }
235
236    pub fn add_agent(&mut self, agent: &Agent) {
237        self.agents
238            .insert(agent.id.clone(), Arc::new(agent.clone()));
239    }
240
241    pub fn add_agents(&mut self, agents: &[&Agent]) {
242        for agent in agents {
243            self.add_agent(agent);
244        }
245    }
246
247    pub async fn execute_task(&self, task: &str, context: &Value) -> Result<Value, WorkflowError> {
248        let task = self
249            .task_list
250            .get_task(task)
251            .ok_or_else(|| WorkflowError::TaskNotFound(task.to_string()))?;
252
253        let agent = {
254            let task_guard = task.read().map_err(|_| WorkflowError::TaskLockError)?;
255            self.agents
256                .get(&task_guard.agent_id)
257                .ok_or_else(|| WorkflowError::AgentNotFound(task_guard.agent_id.clone()))?
258                .clone()
259        };
260
261        let max_retries = {
262            let task_guard = task.read().map_err(|_| WorkflowError::TaskLockError)?;
263            task_guard.max_retries
264        };
265
266        for attempt in 0..=max_retries {
267            match agent.execute_task_with_context(&task, context).await {
268                Ok(response) => {
269                    let is_valid = validate_response_schema(&task, &response);
270
271                    if !is_valid {
272                        if attempt == max_retries {
273                            let (task_id, expected_schema, received_response) = {
274                                let task_guard = task.read().unwrap();
275                                (
276                                    task_guard.id.clone(),
277                                    task_guard
278                                        .prompt
279                                        .response_json_schema()
280                                        .map(|s| s.to_string())
281                                        .unwrap_or_else(|| "No schema".to_string()),
282                                    response
283                                        .response_value()
284                                        .map(|v| v.to_string())
285                                        .unwrap_or_else(|| "No response".to_string()),
286                                )
287                            };
288
289                            error!(
290                                "Task {} response validation failed after {} attempts",
291                                task_id,
292                                attempt + 1
293                            );
294
295                            return Err(WorkflowError::ResponseValidationFailed {
296                                task_id,
297                                expected_schema,
298                                received_response,
299                            });
300                        }
301                        warn!(
302                            "Task validation failed (attempt {}/{}), retrying...",
303                            attempt + 1,
304                            max_retries + 1
305                        );
306                        continue;
307                    }
308
309                    let value = match response.response_value() {
310                        Some(v) => v,
311                        None => Value::String(response.response_text()),
312                    };
313                    return Ok(value);
314                }
315                Err(e) => {
316                    let task_id = { task.read().unwrap().id.clone() };
317                    warn!(
318                        "Task {} execution failed (attempt {}/{}): {}",
319                        task_id,
320                        attempt + 1,
321                        max_retries + 1,
322                        e
323                    );
324
325                    if attempt == max_retries {
326                        error!("Task {} exceeded max retries ({})", task_id, max_retries);
327                        return Err(WorkflowError::MaxRetriesExceeded(task_id));
328                    }
329                }
330            }
331        }
332
333        unreachable!("Loop should always return via Ok or error")
334    }
335
336    pub fn execution_plan(&self) -> Result<HashMap<i32, HashSet<String>>, WorkflowError> {
337        let mut remaining: HashMap<String, HashSet<String>> = self
338            .task_list
339            .tasks
340            .iter()
341            .map(|(id, task)| {
342                (
343                    id.clone(),
344                    task.read().unwrap().dependencies.iter().cloned().collect(),
345                )
346            })
347            .collect();
348
349        let mut executed = HashSet::new();
350        let mut plan = HashMap::new();
351        let mut step = 1;
352
353        while !remaining.is_empty() {
354            // Find all tasks that can be executed in parallel - collect just the keys we need to remove
355            let ready_keys: Vec<String> = remaining
356                .iter()
357                .filter(|(_, deps)| deps.is_subset(&executed))
358                .map(|(id, _)| id.to_string())
359                .collect();
360
361            if ready_keys.is_empty() {
362                // Circular dependency detected
363                break;
364            }
365
366            // Create the set for the plan (reusing the already allocated Strings)
367            let mut ready_set = HashSet::with_capacity(ready_keys.len());
368
369            // Update tracking sets and build the ready set in one pass
370            for key in ready_keys {
371                executed.insert(key.clone());
372                remaining.remove(&key);
373                ready_set.insert(key);
374            }
375
376            // Add parallel tasks to the current step
377            plan.insert(step, ready_set);
378
379            step += 1;
380        }
381
382        Ok(plan)
383    }
384
385    pub fn __str__(&self) -> String {
386        PyHelperFuncs::__str__(&self.task_list)
387    }
388
389    pub fn serialize(&self) -> Result<String, serde_json::Error> {
390        // reset the workflow
391        let json = serde_json::to_string(self).unwrap();
392        // Add debug output to see what's being serialized
393        Ok(json)
394    }
395
396    pub fn from_json(json: &str) -> Result<Self, WorkflowError> {
397        // Deserialize the JSON string into a Workflow instance
398        Ok(serde_json::from_str(json)?)
399    }
400
401    pub fn task_names(&self) -> Vec<String> {
402        self.task_list
403            .tasks
404            .keys()
405            .cloned()
406            .collect::<Vec<String>>()
407    }
408
409    pub fn last_task_id(&self) -> Option<String> {
410        self.task_list.get_last_task_id()
411    }
412}
413
414/// Check if the workflow is complete
415/// # Arguments
416/// * `workflow` - A reference to the workflow instance
417/// # Returns true if the workflow is complete, false otherwise
418fn is_workflow_complete(workflow: &Arc<RwLock<Workflow>>) -> bool {
419    workflow.read().unwrap().is_complete()
420}
421
422/// Reset failed tasks in the workflow
423/// # Arguments
424/// * `workflow` - A reference to the workflow instance
425/// # Returns Ok(()) if successful, or an error if the reset fails
426fn reset_failed_workflow_tasks(workflow: &Arc<RwLock<Workflow>>) -> Result<(), WorkflowError> {
427    match workflow.write().unwrap().task_list.reset_failed_tasks() {
428        Ok(_) => Ok(()),
429        Err(e) => {
430            warn!("Failed to reset failed tasks: {}", e);
431            Err(e)
432        }
433    }
434}
435
436/// Get all ready tasks in the workflow
437/// # Arguments
438/// * `workflow` - A reference to the workflow instance
439/// # Returns a vector of tasks that are ready to be executed
440fn get_ready_tasks(workflow: &Arc<RwLock<Workflow>>) -> Vec<Arc<RwLock<Task>>> {
441    workflow.read().unwrap().task_list.get_ready_tasks()
442}
443
444/// Check for circular dependencies
445/// # Arguments
446/// * `workflow` - A reference to the workflow instance
447/// # Returns true if circular dependencies are detected, false otherwise
448fn check_for_circular_dependencies(workflow: &Arc<RwLock<Workflow>>) -> bool {
449    let pending_count = workflow.read().unwrap().pending_count();
450
451    if pending_count > 0 {
452        warn!(
453            "No ready tasks found but {} pending tasks remain. Possible circular dependency.",
454            pending_count
455        );
456        return true;
457    }
458
459    false
460}
461
462/// Mark a task as running
463/// # Arguments
464/// * `workflow` - A reference to the workflow instance
465/// # Returns nothing
466fn mark_task_as_running(task: Arc<RwLock<Task>>, event_tracker: &Arc<RwLock<EventTracker>>) {
467    let mut task = task.write().unwrap();
468    task.set_status(TaskStatus::Running);
469    event_tracker.write().unwrap().record_task_started(&task.id);
470}
471
472/// Get an agent for a task
473/// # Arguments
474/// * `workflow` - A reference to the workflow instance
475/// * `task` - A reference to the task for which the agent is needed
476fn get_agent_for_task(
477    workflow: &Arc<RwLock<Workflow>>,
478    agent_id: &str,
479) -> Result<Arc<Agent>, WorkflowError> {
480    let wf = workflow.read().unwrap();
481    match wf.agents.get(agent_id) {
482        Some(agent) => Ok(agent.clone()),
483        None => Err(WorkflowError::AgentNotFound(agent_id.to_string())),
484    }
485}
486
487/// Builds the context for a task from its dependencies
488/// For a given list of dependency task IDs (tasks current task depends on),
489/// for each dependency:
490/// 1. Retrieve the task from the workflow's task list
491/// 2. If the task has a result, convert the result to MessageNum format (convert from response struct to request struct)
492/// 2a. Inside of to_message_num, convert each request message to the target provider format if needed
493/// 3. Insert the converted messages into the context hashmap
494/// 4. if the result has a structured output, extract it and merge into parameter context
495/// This is helpful for when a user wants to pass structured output variables that can be bound in subsequent tasks
496/// 5. Return the dependency context, parameter context, and global context
497/// # Arguments
498/// * `workflow` - A reference to the workflow instance
499/// * `task` - A reference to the task for which the context is being built
500/// # Returns a HashMap containing the context messages for the task
501#[instrument(skip_all)]
502fn build_task_context(
503    workflow: &Arc<RwLock<Workflow>>,
504    task_dependencies: &Vec<String>,
505    provider: &Provider,
506) -> Result<Context, WorkflowError> {
507    let wf = workflow.read().unwrap();
508    let mut ctx = HashMap::new();
509    let mut param_ctx: Value = Value::Object(Map::new());
510
511    for dep_id in task_dependencies {
512        debug!("Building context for task dependency: {}", dep_id);
513        if let Some(dep) = wf.task_list.get_task(dep_id) {
514            if let Some(result) = &dep.read().unwrap().result {
515                let msg_to_insert = result.response.to_message_num(provider);
516
517                match msg_to_insert {
518                    Ok(message) => {
519                        ctx.insert(dep_id.clone(), message);
520                    }
521                    Err(e) => {
522                        warn!("Failed to convert response to message: {}", e);
523                    }
524                }
525
526                if let Some(structure_output) = result.response.extract_structured_data() {
527                    // Value should be a serde_json::Value Object type
528                    // validate that it's an object
529                    if structure_output.is_object() {
530                        // extract the Map from the Value
531                        update_serde_map_with(&mut param_ctx, &structure_output)?;
532                    }
533                }
534            }
535        }
536    }
537
538    debug!("Built context for task dependencies: {:?}", ctx);
539    let global_context = workflow
540        .read()
541        .unwrap()
542        .global_context
543        .as_ref()
544        .map(Arc::clone);
545
546    Ok((ctx, param_ctx, global_context))
547}
548
549/// Validate a task response against its JSON schema
550/// Only validates if a schema is defined for the task
551/// # Arguments
552/// * `task` - A reference to the task
553/// * `response` - A reference to the task response
554/// # Returns
555/// true if the response is invalid, false otherwise
556fn validate_response_schema(
557    task: &Arc<RwLock<Task>>,
558    response: &potato_agent::AgentResponse,
559) -> bool {
560    task.read()
561        .ok()
562        .and_then(|t| {
563            response
564                .response_value()
565                .map(|value| t.validate_output(&value).is_ok())
566        })
567        .unwrap_or(true)
568}
569
570/// Spawns an individual task execution
571/// # Arguments
572/// * `workflow` - A reference to the workflow instance
573/// * `task` - The task to be executed
574/// * `task_id` - The ID of the task
575/// * `agent` - An optional reference to the agent that will execute the task
576/// * `context` - A HashMap containing the context messages for the task
577/// # Returns a JoinHandle for the spawned task
578fn spawn_task_execution(
579    event_tracker: Arc<RwLock<EventTracker>>,
580    task: Arc<RwLock<Task>>,
581    task_id: String,
582    agent: Arc<Agent>,
583    context: HashMap<String, Vec<MessageNum>>,
584    parameter_context: Value,
585    global_context: Option<Arc<Value>>,
586) -> tokio::task::JoinHandle<()> {
587    tokio::spawn(async move {
588        let result = agent
589            .execute_task_with_context_message(&task, context, parameter_context, global_context)
590            .await;
591
592        match result {
593            Ok(response) => {
594                info!("Task {} completed successfully", task_id);
595
596                let is_valid = validate_response_schema(&task, &response); // No schema or no response_value = validation passes
597                if !is_valid {
598                    error!(
599                        "Task {} response validation against JSON schema failed",
600                        task_id
601                    );
602
603                    if let Ok(mut write_task) = task.write() {
604                        write_task.set_status(TaskStatus::Failed);
605
606                        if let Ok(tracker) = event_tracker.write() {
607                            tracker.record_task_failed(
608                                &write_task.id,
609                                "Response JSON schema validation failed",
610                                &write_task.prompt,
611                            );
612                        }
613                    }
614                    return;
615                }
616
617                if let Ok(mut write_task) = task.write() {
618                    write_task.set_status(TaskStatus::Completed);
619                    write_task.set_result(response.clone());
620
621                    if let Ok(tracker) = event_tracker.write() {
622                        tracker.record_task_completed(&write_task.id, &write_task.prompt, response);
623                    }
624                }
625            }
626            Err(e) => {
627                error!("Task {} failed: {}", task_id, e);
628
629                if let Ok(mut write_task) = task.write() {
630                    write_task.set_status(TaskStatus::Failed);
631
632                    if let Ok(tracker) = event_tracker.write() {
633                        tracker.record_task_failed(
634                            &write_task.id,
635                            &e.to_string(),
636                            &write_task.prompt,
637                        );
638                    }
639                }
640            }
641        }
642    })
643}
644
645fn get_parameters_from_context(task: Arc<RwLock<Task>>) -> (String, Vec<String>, String, Provider) {
646    let (task_id, dependencies, agent_id, provider) = {
647        let task_guard = task.read().unwrap();
648        (
649            task_guard.id.clone(),
650            task_guard.dependencies.clone(),
651            task_guard.agent_id.clone(),
652            task_guard.prompt.provider.clone(),
653        )
654    };
655
656    (task_id, dependencies, agent_id, provider)
657}
658
659/// Helper for spawning a task execution
660/// # Arguments
661/// * `workflow` - A reference to the workflow instance
662/// * `tasks` - A vector of tasks to be executed
663/// # Returns a vector of JoinHandles for the spawned tasks
664fn spawn_task_executions(
665    workflow: &Arc<RwLock<Workflow>>,
666    ready_tasks: Vec<Arc<RwLock<Task>>>,
667) -> Result<Vec<tokio::task::JoinHandle<()>>, WorkflowError> {
668    let mut handles = Vec::with_capacity(ready_tasks.len());
669
670    // Get the event tracker from the workflow
671    let event_tracker = workflow.read().unwrap().event_tracker.clone();
672
673    for task in ready_tasks {
674        // Get task parameters for active task
675        let (task_id, dependencies, agent_id, provider) = get_parameters_from_context(task.clone());
676
677        // Mark task as running
678        // This will also record the task started event
679        mark_task_as_running(task.clone(), &event_tracker);
680
681        // Build the context
682        // Here we:
683        // 1. Get the task dependencies and their results (these will be injected as assistant messages)
684        // We need to know the provider here so that we can convert  messages to a different provider type if needed
685        // 2. Parse dependent tasks for any structured outputs and return as a serde_json::Value (this will be task-level context)
686        let (context, parameter_context, global_context) =
687            build_task_context(workflow, &dependencies, &provider)?;
688
689        // Get/clone agent ARC
690        let agent = get_agent_for_task(workflow, &agent_id)?;
691
692        // Spawn task execution and push handle to future vector
693        let handle = spawn_task_execution(
694            event_tracker.clone(),
695            task.clone(),
696            task_id,
697            agent,
698            context,
699            parameter_context,
700            global_context,
701        );
702        handles.push(handle);
703    }
704
705    Ok(handles)
706}
707
708/// Wait for all spawned tasks to complete
709/// # Arguments
710/// * `handles` - A vector of JoinHandles for the spawned tasks
711/// # Returns nothing
712async fn await_task_completions(handles: Vec<tokio::task::JoinHandle<()>>) {
713    for handle in handles {
714        if let Err(e) = handle.await {
715            warn!("Task execution failed: {}", e);
716        }
717    }
718}
719
720/// Execute the workflow asynchronously
721/// This function will be called to start the workflow execution process and does the following:
722/// 1. Iterates over workflow tasks while the shared workflow is not complete.
723/// 2. Resets any failed tasks to allow them to be retried. This needs to happen before getting ready tasks.
724/// 3. Gets all ready tasks
725/// 4. For each ready task:
726/// ///    - Marks the task as running
727/// ///    - Checks previous tasks for injected context
728/// ///    - Gets the agent for the task  
729/// ///    - Spawn a new tokio task and execute task with agent
730/// ///    - Push task to the handles vector
731/// 4. Waits for all spawned tasks to complete
732#[instrument(skip_all)]
733pub async fn execute_workflow(workflow: &Arc<RwLock<Workflow>>) -> Result<(), WorkflowError> {
734    // Important to remember that the workflow is an Arc<RwLock<Workflow>> is a new clone of
735    // the loaded workflow. This allows us to mutate the workflow without affecting the original
736    // workflow instance.
737    debug!("Starting workflow execution");
738
739    // Run until workflow is complete
740    while !is_workflow_complete(workflow) {
741        // Reset any failed tasks
742        // This will return an error if any task exceeds its max retries (set at the task level)
743        reset_failed_workflow_tasks(workflow)?;
744
745        // Get tasks ready for execution
746        // This will return an Arc<RwLock<Task>>
747        let ready_tasks = get_ready_tasks(workflow);
748        debug!("Found {} ready tasks for execution", ready_tasks.len());
749
750        // Check for circular dependencies
751        if ready_tasks.is_empty() {
752            if check_for_circular_dependencies(workflow) {
753                break;
754            }
755            continue;
756        }
757
758        // Execute tasks asynchronously
759        let handles = spawn_task_executions(workflow, ready_tasks)?;
760
761        // Wait for all tasks to complete
762        await_task_completions(handles).await;
763    }
764
765    debug!("Workflow execution completed");
766    Ok(())
767}
768
769impl Serialize for Workflow {
770    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
771    where
772        S: Serializer,
773    {
774        let mut state = serializer.serialize_struct("Workflow", 4)?;
775
776        // set session to none
777        state.serialize_field("id", &self.id)?;
778        state.serialize_field("name", &self.name)?;
779        state.serialize_field("task_list", &self.task_list)?;
780        state.serialize_field("agents", &self.agents)?;
781
782        state.end()
783    }
784}
785
786impl<'de> Deserialize<'de> for Workflow {
787    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
788    where
789        D: Deserializer<'de>,
790    {
791        #[derive(Deserialize)]
792        #[serde(field_identifier, rename_all = "snake_case")]
793        enum Field {
794            Id,
795            Name,
796            TaskList,
797            Agents,
798        }
799
800        struct WorkflowVisitor;
801
802        impl<'de> Visitor<'de> for WorkflowVisitor {
803            type Value = Workflow;
804
805            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
806                formatter.write_str("struct Workflow")
807            }
808
809            fn visit_map<V>(self, mut map: V) -> Result<Workflow, V::Error>
810            where
811                V: MapAccess<'de>,
812            {
813                let mut id = None;
814                let mut name = None;
815                let mut task_list_data = None;
816                let mut agents: Option<HashMap<String, Agent>> = None;
817
818                while let Some(key) = map.next_key()? {
819                    match key {
820                        Field::Id => {
821                            let value: String = map.next_value().map_err(|e| {
822                                error!("Failed to deserialize field 'id': {e}");
823                                de::Error::custom(format!("Failed to deserialize field 'id': {e}"))
824                            })?;
825                            id = Some(value);
826                        }
827                        Field::TaskList => {
828                            // Deserialize as a generic Value first
829                            let value: TaskList = map.next_value().map_err(|e| {
830                                error!("Failed to deserialize field 'task_list': {e}");
831                                de::Error::custom(format!(
832                                    "Failed to deserialize field 'task_list': {e}",
833                                ))
834                            })?;
835
836                            task_list_data = Some(value);
837                        }
838                        Field::Name => {
839                            let value: String = map.next_value().map_err(|e| {
840                                error!("Failed to deserialize field 'name': {e}");
841                                de::Error::custom(format!(
842                                    "Failed to deserialize field 'name': {e}",
843                                ))
844                            })?;
845                            name = Some(value);
846                        }
847                        Field::Agents => {
848                            let value: HashMap<String, Agent> = map.next_value().map_err(|e| {
849                                error!("Failed to deserialize field 'agents': {e}");
850                                de::Error::custom(format!(
851                                    "Failed to deserialize field 'agents': {e}"
852                                ))
853                            })?;
854                            agents = Some(value);
855                        }
856                    }
857                }
858
859                let id = id.ok_or_else(|| de::Error::missing_field("id"))?;
860                let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
861                let task_list_data =
862                    task_list_data.ok_or_else(|| de::Error::missing_field("task_list"))?;
863                let agents = agents.ok_or_else(|| de::Error::missing_field("agents"))?;
864
865                let event_tracker = Arc::new(RwLock::new(EventTracker::new(create_uuid7())));
866
867                // convert agents to arc
868                let agents = agents
869                    .into_iter()
870                    .map(|(id, agent)| (id, Arc::new(agent)))
871                    .collect();
872
873                Ok(Workflow {
874                    id,
875                    name,
876                    task_list: task_list_data,
877                    agents,
878                    event_tracker,
879                    global_context: None, // Initialize with no global context
880                })
881            }
882        }
883
884        const FIELDS: &[&str] = &["id", "name", "task_list", "agents"];
885        deserializer.deserialize_struct("Workflow", FIELDS, WorkflowVisitor)
886    }
887}
888
889#[pyclass(skip_from_py_object, name = "Workflow")]
890#[derive(Debug, Clone)]
891pub struct PyWorkflow {
892    workflow: Workflow,
893
894    // allow adding output types for python tasks (py only)
895    // these are provided at runtime by the user and must match the response
896    // format of the prompt the task is associated with
897    output_types: HashMap<String, Arc<Py<PyAny>>>,
898}
899
900#[pymethods]
901impl PyWorkflow {
902    #[new]
903    #[pyo3(signature = (name))]
904    pub fn new(name: &str) -> Result<Self, WorkflowError> {
905        debug!("Creating new workflow: {}", name);
906        Ok(Self {
907            workflow: Workflow::new(name),
908            output_types: HashMap::new(),
909        })
910    }
911
912    #[getter]
913    pub fn name(&self) -> String {
914        self.workflow.name.clone()
915    }
916
917    #[getter]
918    pub fn task_list(&self) -> TaskList {
919        self.workflow.task_list.clone()
920    }
921
922    #[getter]
923    pub fn is_workflow(&self) -> bool {
924        true
925    }
926
927    #[getter]
928    pub fn __workflow__(&self) -> Result<String, WorkflowError> {
929        self.model_dump_json()
930    }
931
932    #[getter]
933    pub fn agents(&self) -> Result<HashMap<String, PyAgent>, WorkflowError> {
934        self.workflow
935            .agents
936            .iter()
937            .map(|(id, agent)| {
938                Ok((
939                    id.clone(),
940                    PyAgent {
941                        agent: agent.clone(),
942                    },
943                ))
944            })
945            .collect::<Result<HashMap<_, _>, _>>()
946    }
947
948    #[pyo3(signature = (task_output_types))]
949    pub fn add_task_output_types<'py>(
950        &mut self,
951        task_output_types: Bound<'py, PyDict>,
952    ) -> PyResult<()> {
953        let converted: HashMap<String, Arc<Py<PyAny>>> = task_output_types
954            .iter()
955            .map(|(k, v)| -> PyResult<(String, Arc<Py<PyAny>>)> {
956                // Explicitly return a Result from the closure
957                let key = k.extract::<String>()?;
958                let value = v.clone().unbind();
959                Ok((key, Arc::new(value)))
960            })
961            .collect::<PyResult<_>>()?;
962        self.output_types.extend(converted);
963        Ok(())
964    }
965
966    #[pyo3(signature = (task, output_type = None))]
967    pub fn add_task(
968        &mut self,
969        py: Python<'_>,
970        mut task: Task,
971        output_type: Option<Bound<'_, PyAny>>,
972    ) -> Result<(), WorkflowError> {
973        if let Some(output_type) = output_type {
974            // Parse and set the response format
975            let (response_type, response_json_schema) = parse_response_to_json(py, &output_type)
976                .map_err(|e| WorkflowError::InvalidOutputType(e.to_string()))?;
977
978            // update the task prompt with the response schema
979            task.prompt
980                .set_response_json_schema(response_json_schema, response_type);
981
982            // Store the output type for later use
983            self.output_types
984                .insert(task.id.clone(), Arc::new(output_type.unbind()));
985        }
986
987        self.workflow.task_list.add_task(task)?;
988        Ok(())
989    }
990
991    pub fn add_tasks(&mut self, tasks: Vec<Task>) -> Result<(), WorkflowError> {
992        for task in tasks {
993            self.workflow.task_list.add_task(task)?;
994        }
995        Ok(())
996    }
997
998    pub fn add_agent(&mut self, agent: &Bound<'_, PyAgent>) {
999        // extract the arc rust agent from the python agent
1000        let agent = agent.extract::<PyAgent>().unwrap().agent.clone();
1001        self.workflow.agents.insert(agent.id.clone(), agent);
1002    }
1003
1004    pub fn add_agents(&mut self, agents: Vec<Bound<'_, PyAgent>>) {
1005        for agent in agents {
1006            self.add_agent(&agent);
1007        }
1008    }
1009
1010    pub fn is_complete(&self) -> bool {
1011        self.workflow.task_list.is_complete()
1012    }
1013
1014    pub fn pending_count(&self) -> usize {
1015        self.workflow.task_list.pending_count()
1016    }
1017
1018    pub fn execution_plan<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, WorkflowError> {
1019        let plan = self.workflow.execution_plan()?;
1020        debug!("Execution plan: {:?}", plan);
1021
1022        // turn hashmap into a to json
1023        let json = serde_json::to_value(plan).map_err(|e| {
1024            error!("Failed to serialize execution plan to JSON: {}", e);
1025            e
1026        })?;
1027
1028        Ok(pythonize(py, &json)?)
1029    }
1030
1031    #[pyo3(signature = (global_context=None))]
1032    pub fn run(
1033        &self,
1034        py: Python,
1035        global_context: Option<Bound<'_, PyAny>>,
1036    ) -> Result<WorkflowResult, WorkflowError> {
1037        debug!("Running workflow: {}", self.workflow.name);
1038
1039        // Convert the global context from PyDict to serde_json::Value if provided
1040        let global_context = if let Some(context) = global_context {
1041            let json_value = depythonize_object_to_value(py, &context)?;
1042            Some(json_value)
1043        } else {
1044            None
1045        };
1046
1047        let workflow: Arc<RwLock<Workflow>> =
1048            block_on(async { self.workflow.run(global_context).await })?;
1049
1050        // Try to get exclusive ownership of the workflow by unwrapping the Arc if there's only one reference
1051        let workflow_result = match Arc::try_unwrap(workflow) {
1052            // If we have exclusive ownership, we can consume the RwLock
1053            Ok(rwlock) => {
1054                // Unwrap the RwLock to get the Workflow
1055                let workflow = rwlock
1056                    .into_inner()
1057                    .map_err(|_| WorkflowError::LockAcquireError)?;
1058
1059                // Get the events before creating WorkflowResult
1060                let events = workflow
1061                    .event_tracker
1062                    .read()
1063                    .unwrap()
1064                    .events
1065                    .read()
1066                    .unwrap()
1067                    .clone();
1068
1069                // Move the tasks out of the workflow
1070                WorkflowResult::new(
1071                    py,
1072                    workflow.task_list.tasks(),
1073                    &self.output_types,
1074                    events,
1075                    workflow.task_list.get_last_task_id(),
1076                )
1077            }
1078            // If there are other references, we need to clone
1079            Err(arc) => {
1080                // Just read the workflow
1081                error!("Workflow still has other references, reading instead of consuming.");
1082                let workflow = arc
1083                    .read()
1084                    .map_err(|_| WorkflowError::ReadLockAcquireError)?;
1085
1086                // Get the events before creating WorkflowResult
1087                let events = workflow
1088                    .event_tracker
1089                    .read()
1090                    .unwrap()
1091                    .events
1092                    .read()
1093                    .unwrap()
1094                    .clone();
1095
1096                WorkflowResult::new(
1097                    py,
1098                    workflow.task_list.tasks(),
1099                    &self.output_types,
1100                    events,
1101                    workflow.task_list.get_last_task_id(),
1102                )
1103            }
1104        };
1105
1106        info!("Workflow execution completed successfully.");
1107        Ok(workflow_result)
1108    }
1109
1110    #[pyo3(signature = (task_id, context=None))]
1111    pub fn execute_task<'py>(
1112        &self,
1113        py: Python<'py>,
1114        task_id: String,
1115        context: Option<Bound<'py, PyAny>>,
1116    ) -> Result<Bound<'py, PyAny>, WorkflowError> {
1117        let context_value = if let Some(ctx) = context {
1118            depythonize_object_to_value(py, &ctx)?
1119        } else {
1120            Value::Null
1121        };
1122
1123        let response_value =
1124            block_on(async { self.workflow.execute_task(&task_id, &context_value).await })?;
1125        let py_response = pythonize(py, &response_value)?;
1126
1127        Ok(py_response)
1128    }
1129
1130    pub fn model_dump_json(&self) -> Result<String, WorkflowError> {
1131        Ok(self.workflow.serialize()?)
1132    }
1133
1134    #[staticmethod]
1135    #[pyo3(signature = (json_string, output_types=None))]
1136    pub fn model_validate_json(
1137        json_string: String,
1138        output_types: Option<Bound<'_, PyDict>>,
1139    ) -> Result<Self, WorkflowError> {
1140        let mut workflow: Workflow = Workflow::from_json(&json_string)?;
1141
1142        // rebuild tasklist schema validators
1143        workflow.task_list.rebuild_task_validators()?;
1144
1145        // reload agents to ensure clients are rebuilt
1146        // This is necessary because during deserialization the GenAIClient
1147        block_on(async { workflow.reset_agents().await })?;
1148        let output_types = match output_types {
1149            Some(output_types) => output_types
1150                .iter()
1151                .map(|(k, v)| -> PyResult<(String, Arc<Py<PyAny>>)> {
1152                    let key = k.extract::<String>()?;
1153                    let value = v.clone().unbind();
1154                    Ok((key, Arc::new(value)))
1155                })
1156                .collect::<PyResult<HashMap<String, Arc<Py<PyAny>>>>>()?,
1157            None => HashMap::new(),
1158        };
1159
1160        let py_workflow = PyWorkflow {
1161            workflow,
1162            output_types,
1163        };
1164
1165        Ok(py_workflow)
1166    }
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171    use super::*;
1172    use potato_type::openai::v1::chat::request::{
1173        ChatMessage as OpenAIChatMessage, ContentPart, TextContentPart,
1174    };
1175    use potato_type::prompt::Prompt;
1176    use potato_type::prompt::ResponseType;
1177
1178    fn create_openai_chat_message_num() -> MessageNum {
1179        let text_part = TextContentPart::new("What company is this logo from?".to_string());
1180        let text_content_part = ContentPart::Text(text_part);
1181        let text_message = OpenAIChatMessage {
1182            role: "user".to_string(),
1183            content: vec![text_content_part],
1184            name: None,
1185        };
1186        MessageNum::OpenAIMessageV1(text_message)
1187    }
1188
1189    #[test]
1190    fn test_workflow_creation() {
1191        let workflow = Workflow::new("Test Workflow");
1192        assert_eq!(workflow.name, "Test Workflow");
1193        assert_eq!(workflow.id.len(), 36); // UUID7 length
1194    }
1195
1196    #[test]
1197    fn test_task_list_add_and_get() {
1198        let mut task_list = TaskList::new();
1199
1200        let prompt = Prompt::new_rs(
1201            vec![create_openai_chat_message_num()],
1202            "gpt-4o",
1203            potato_type::Provider::OpenAI,
1204            vec![],
1205            None,
1206            None,
1207            ResponseType::Null,
1208        )
1209        .unwrap();
1210
1211        let task = Task::new("task1", prompt, "task1", None, None).unwrap();
1212        task_list.add_task(task.clone()).unwrap();
1213        assert_eq!(
1214            task_list.get_task(&task.id).unwrap().read().unwrap().id,
1215            task.id
1216        );
1217        task_list.reset_failed_tasks().unwrap();
1218    }
1219}