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
32use 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 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 #[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#[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 self.id == other.id && self.name == other.name
128 }
129}
130
131impl Workflow {
132 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, }
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 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 let id = create_uuid7();
189
190 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(), 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 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 break;
364 }
365
366 let mut ready_set = HashSet::with_capacity(ready_keys.len());
368
369 for key in ready_keys {
371 executed.insert(key.clone());
372 remaining.remove(&key);
373 ready_set.insert(key);
374 }
375
376 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 let json = serde_json::to_string(self).unwrap();
392 Ok(json)
394 }
395
396 pub fn from_json(json: &str) -> Result<Self, WorkflowError> {
397 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
414fn is_workflow_complete(workflow: &Arc<RwLock<Workflow>>) -> bool {
419 workflow.read().unwrap().is_complete()
420}
421
422fn 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
436fn get_ready_tasks(workflow: &Arc<RwLock<Workflow>>) -> Vec<Arc<RwLock<Task>>> {
441 workflow.read().unwrap().task_list.get_ready_tasks()
442}
443
444fn 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
462fn 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
472fn 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#[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 if structure_output.is_object() {
530 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
549fn 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
570fn 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); 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
659fn 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 let event_tracker = workflow.read().unwrap().event_tracker.clone();
672
673 for task in ready_tasks {
674 let (task_id, dependencies, agent_id, provider) = get_parameters_from_context(task.clone());
676
677 mark_task_as_running(task.clone(), &event_tracker);
680
681 let (context, parameter_context, global_context) =
687 build_task_context(workflow, &dependencies, &provider)?;
688
689 let agent = get_agent_for_task(workflow, &agent_id)?;
691
692 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
708async 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#[instrument(skip_all)]
733pub async fn execute_workflow(workflow: &Arc<RwLock<Workflow>>) -> Result<(), WorkflowError> {
734 debug!("Starting workflow execution");
738
739 while !is_workflow_complete(workflow) {
741 reset_failed_workflow_tasks(workflow)?;
744
745 let ready_tasks = get_ready_tasks(workflow);
748 debug!("Found {} ready tasks for execution", ready_tasks.len());
749
750 if ready_tasks.is_empty() {
752 if check_for_circular_dependencies(workflow) {
753 break;
754 }
755 continue;
756 }
757
758 let handles = spawn_task_executions(workflow, ready_tasks)?;
760
761 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 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 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 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, })
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 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 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 let (response_type, response_json_schema) = parse_response_to_json(py, &output_type)
976 .map_err(|e| WorkflowError::InvalidOutputType(e.to_string()))?;
977
978 task.prompt
980 .set_response_json_schema(response_json_schema, response_type);
981
982 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 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 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 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 let workflow_result = match Arc::try_unwrap(workflow) {
1052 Ok(rwlock) => {
1054 let workflow = rwlock
1056 .into_inner()
1057 .map_err(|_| WorkflowError::LockAcquireError)?;
1058
1059 let events = workflow
1061 .event_tracker
1062 .read()
1063 .unwrap()
1064 .events
1065 .read()
1066 .unwrap()
1067 .clone();
1068
1069 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 Err(arc) => {
1080 error!("Workflow still has other references, reading instead of consuming.");
1082 let workflow = arc
1083 .read()
1084 .map_err(|_| WorkflowError::ReadLockAcquireError)?;
1085
1086 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 workflow.task_list.rebuild_task_validators()?;
1144
1145 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); }
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}