praisonaiagents 0.0.57__py3-none-any.whl → 0.0.59__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,122 +12,300 @@ class LoopItems(BaseModel):
12
12
  items: List[Any]
13
13
 
14
14
  class Process:
15
+ DEFAULT_RETRY_LIMIT = 3 # Predefined retry limit in a common place
16
+
15
17
  def __init__(self, tasks: Dict[str, Task], agents: List[Agent], manager_llm: Optional[str] = None, verbose: bool = False, max_iter: int = 10):
18
+ logging.debug(f"=== Initializing Process ===")
19
+ logging.debug(f"Number of tasks: {len(tasks)}")
20
+ logging.debug(f"Number of agents: {len(agents)}")
21
+ logging.debug(f"Manager LLM: {manager_llm}")
22
+ logging.debug(f"Verbose mode: {verbose}")
23
+ logging.debug(f"Max iterations: {max_iter}")
24
+
16
25
  self.tasks = tasks
17
26
  self.agents = agents
18
27
  self.manager_llm = manager_llm
19
28
  self.verbose = verbose
20
29
  self.max_iter = max_iter
30
+ self.task_retry_counter: Dict[str, int] = {} # Initialize retry counter
31
+ self.workflow_finished = False # ADDED: Workflow finished flag
32
+
33
+ def _find_next_not_started_task(self) -> Optional[Task]:
34
+ """Fallback mechanism to find the next 'not started' task."""
35
+ fallback_attempts = 0
36
+ temp_current_task = None
37
+
38
+ # Clear previous task context before finding next task
39
+ for task in self.tasks.values():
40
+ if hasattr(task, 'description') and 'Input data from previous tasks:' in task.description:
41
+ task.description = task.description.split('Input data from previous tasks:')[0].strip()
42
+
43
+ while fallback_attempts < Process.DEFAULT_RETRY_LIMIT and not temp_current_task:
44
+ fallback_attempts += 1
45
+ logging.debug(f"Fallback attempt {fallback_attempts}: Trying to find next 'not started' task.")
46
+ for task_candidate in self.tasks.values():
47
+ if task_candidate.status == "not started":
48
+ # Check if there's a condition path to this task
49
+ current_conditions = task_candidate.condition or {}
50
+ leads_to_task = any(
51
+ task_value for task_value in current_conditions.values()
52
+ if isinstance(task_value, (list, str)) and task_value
53
+ )
54
+
55
+ if not leads_to_task and not task_candidate.next_tasks:
56
+ continue # Skip if no valid path exists
57
+
58
+ if self.task_retry_counter.get(task_candidate.id, 0) < Process.DEFAULT_RETRY_LIMIT:
59
+ self.task_retry_counter[task_candidate.id] = self.task_retry_counter.get(task_candidate.id, 0) + 1
60
+ temp_current_task = task_candidate
61
+ logging.debug(f"Fallback attempt {fallback_attempts}: Found 'not started' task: {temp_current_task.name}, retry count: {self.task_retry_counter[temp_current_task.id]}")
62
+ return temp_current_task # Return the found task immediately
63
+ else:
64
+ logging.debug(f"Max retries reached for task {task_candidate.name} in fallback mode, marking as failed.")
65
+ task_candidate.status = "failed"
66
+ if not temp_current_task:
67
+ logging.debug(f"Fallback attempt {fallback_attempts}: No 'not started' task found within retry limit.")
68
+ return None # Return None if no task found after all attempts
69
+
21
70
 
22
71
  async def aworkflow(self) -> AsyncGenerator[str, None]:
23
72
  """Async version of workflow method"""
73
+ logging.debug("=== Starting Async Workflow ===")
24
74
  current_iter = 0 # Track how many times we've looped
25
75
  # Build workflow relationships first
76
+ logging.debug("Building workflow relationships...")
26
77
  for task in self.tasks.values():
27
78
  if task.next_tasks:
28
79
  for next_task_name in task.next_tasks:
29
80
  next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
30
81
  if next_task:
31
82
  next_task.previous_tasks.append(task.name)
83
+ logging.debug(f"Added {task.name} as previous task for {next_task_name}")
32
84
 
33
85
  # Find start task
86
+ logging.debug("Finding start task...")
34
87
  start_task = None
35
88
  for task_id, task in self.tasks.items():
36
89
  if task.is_start:
37
90
  start_task = task
91
+ logging.debug(f"Found marked start task: {task.name} (id: {task_id})")
38
92
  break
39
-
93
+
40
94
  if not start_task:
41
95
  start_task = list(self.tasks.values())[0]
42
- logging.info("No start task marked, using first task")
43
-
96
+ logging.debug(f"No start task marked, using first task: {start_task.name}")
97
+
44
98
  current_task = start_task
45
99
  visited_tasks = set()
46
100
  loop_data = {} # Store loop-specific data
47
101
 
48
102
  # TODO: start task with loop feature is not available in aworkflow method
49
-
103
+
50
104
  while current_task:
51
105
  current_iter += 1
52
106
  if current_iter > self.max_iter:
53
107
  logging.info(f"Max iteration limit {self.max_iter} reached, ending workflow.")
54
108
  break
55
109
 
110
+ # ADDED: Check workflow finished flag at the start of each cycle
111
+ if self.workflow_finished:
112
+ logging.info("Workflow finished early as all tasks are completed.")
113
+ break
114
+
115
+ # Add task summary at start of each cycle
116
+ logging.debug(f"""
117
+ === Workflow Cycle {current_iter} Summary ===
118
+ Total tasks: {len(self.tasks)}
119
+ Outstanding tasks: {sum(1 for t in self.tasks.values() if t.status != "completed")}
120
+ Completed tasks: {sum(1 for t in self.tasks.values() if t.status == "completed")}
121
+ Tasks by status:
122
+ - Not started: {sum(1 for t in self.tasks.values() if t.status == "not started")}
123
+ - In progress: {sum(1 for t in self.tasks.values() if t.status == "in_progress")}
124
+ - Completed: {sum(1 for t in self.tasks.values() if t.status == "completed")}
125
+ Tasks by type:
126
+ - Loop tasks: {sum(1 for t in self.tasks.values() if t.task_type == "loop")}
127
+ - Decision tasks: {sum(1 for t in self.tasks.values() if t.task_type == "decision")}
128
+ - Regular tasks: {sum(1 for t in self.tasks.values() if t.task_type not in ["loop", "decision"])}
129
+ """)
130
+
131
+ # ADDED: Check if all tasks are completed and set workflow_finished flag
132
+ if all(task.status == "completed" for task in self.tasks.values()):
133
+ logging.info("All tasks are completed.")
134
+ self.workflow_finished = True
135
+ # The next iteration loop check will break the workflow
136
+
56
137
  task_id = current_task.id
57
- logging.info(f"Executing workflow task: {current_task.name if current_task.name else task_id}")
58
-
138
+ logging.debug(f"""
139
+ === Task Execution Details ===
140
+ Current task: {current_task.name}
141
+ Type: {current_task.task_type}
142
+ Status: {current_task.status}
143
+ Previous tasks: {current_task.previous_tasks}
144
+ Next tasks: {current_task.next_tasks}
145
+ Context tasks: {[t.name for t in current_task.context] if current_task.context else []}
146
+ Description length: {len(current_task.description)}
147
+ """)
148
+
59
149
  # Add context from previous tasks to description
60
150
  if current_task.previous_tasks or current_task.context:
61
151
  context = "\nInput data from previous tasks:"
62
-
152
+
63
153
  # Add data from previous tasks in workflow
64
154
  for prev_name in current_task.previous_tasks:
65
155
  prev_task = next((t for t in self.tasks.values() if t.name == prev_name), None)
66
156
  if prev_task and prev_task.result:
67
157
  # Handle loop data
68
158
  if current_task.task_type == "loop":
69
- # # create a loop manager Agent
70
- # loop_manager = Agent(
71
- # name="Loop Manager",
72
- # role="Loop data processor",
73
- # goal="Process loop data and convert it to list format",
74
- # backstory="Expert at handling loop data and converting it to proper format",
75
- # llm=self.manager_llm,
76
- # verbose=self.verbose,
77
- # markdown=True
78
- # )
79
-
80
- # # get the loop data convert it to list using calling Agent class chat
81
- # loop_prompt = f"""
82
- # Process this data into a list format:
83
- # {prev_task.result.raw}
84
-
85
- # Return a JSON object with an 'items' array containing the items to process.
86
- # """
87
- # if current_task.async_execution:
88
- # loop_data_str = await loop_manager.achat(
89
- # prompt=loop_prompt,
90
- # output_json=LoopItems
91
- # )
92
- # else:
93
- # loop_data_str = loop_manager.chat(
94
- # prompt=loop_prompt,
95
- # output_json=LoopItems
96
- # )
97
-
98
- # try:
99
- # # The response will already be parsed into LoopItems model
100
- # loop_data[f"loop_{current_task.name}"] = {
101
- # "items": loop_data_str.items,
102
- # "index": 0,
103
- # "remaining": len(loop_data_str.items)
104
- # }
105
- # context += f"\nCurrent loop item: {loop_data_str.items[0]}"
106
- # except Exception as e:
107
- # display_error(f"Failed to process loop data: {e}")
108
- # context += f"\n{prev_name}: {prev_task.result.raw}"
109
159
  context += f"\n{prev_name}: {prev_task.result.raw}"
110
160
  else:
111
161
  context += f"\n{prev_name}: {prev_task.result.raw}"
112
-
162
+
113
163
  # Add data from context tasks
114
164
  if current_task.context:
115
165
  for ctx_task in current_task.context:
116
166
  if ctx_task.result and ctx_task.name != current_task.name:
117
167
  context += f"\n{ctx_task.name}: {ctx_task.result.raw}"
118
-
168
+
119
169
  # Update task description with context
120
170
  current_task.description = current_task.description + context
121
-
122
- # Execute task using existing run_task method
123
- yield task_id
124
- visited_tasks.add(task_id)
125
-
171
+
172
+ # Skip execution for loop tasks, only process their subtasks
173
+ if current_task.task_type == "loop":
174
+ logging.debug(f"""
175
+ === Loop Task Details ===
176
+ Name: {current_task.name}
177
+ ID: {current_task.id}
178
+ Status: {current_task.status}
179
+ Next tasks: {current_task.next_tasks}
180
+ Condition: {current_task.condition}
181
+ Subtasks created: {getattr(current_task, '_subtasks_created', False)}
182
+ Input file: {getattr(current_task, 'input_file', None)}
183
+ """)
184
+
185
+ # Check if subtasks are created and completed
186
+ if getattr(current_task, "_subtasks_created", False):
187
+ subtasks = [
188
+ t for t in self.tasks.values()
189
+ if t.name.startswith(current_task.name + "_")
190
+ ]
191
+ logging.debug(f"""
192
+ === Subtask Status Check ===
193
+ Total subtasks: {len(subtasks)}
194
+ Completed: {sum(1 for st in subtasks if st.status == "completed")}
195
+ Pending: {sum(1 for st in subtasks if st.status != "completed")}
196
+ """)
197
+
198
+ # Log detailed subtask info
199
+ for st in subtasks:
200
+ logging.debug(f"""
201
+ Subtask: {st.name}
202
+ - Status: {st.status}
203
+ - Next tasks: {st.next_tasks}
204
+ - Condition: {st.condition}
205
+ """)
206
+
207
+ if subtasks and all(st.status == "completed" for st in subtasks):
208
+ logging.debug(f"=== All {len(subtasks)} subtasks completed for {current_task.name} ===")
209
+
210
+ # Mark loop task completed and move to next task
211
+ current_task.status = "completed"
212
+ logging.debug(f"Loop {current_task.name} marked as completed")
213
+
214
+ # Set result for loop task when all subtasks complete
215
+ if not current_task.result:
216
+ # Get result from last completed subtask
217
+ last_subtask = next((t for t in reversed(subtasks) if t.status == "completed"), None)
218
+ if last_subtask and last_subtask.result:
219
+ current_task.result = last_subtask.result
220
+
221
+ # Route to next task based on condition
222
+ if current_task.condition:
223
+ # Get decision from result if available
224
+ decision_str = None
225
+ if current_task.result:
226
+ if current_task.result.pydantic and hasattr(current_task.result.pydantic, "decision"):
227
+ decision_str = current_task.result.pydantic.decision.lower()
228
+ elif current_task.result.raw:
229
+ decision_str = current_task.result.raw.lower()
230
+
231
+ # For loop tasks, use "done" to follow condition path
232
+ if current_task.task_type == "loop" and all(t.status == "completed" for t in subtasks):
233
+ decision_str = "done"
234
+
235
+ target_tasks = current_task.condition.get(decision_str, []) if decision_str else []
236
+ task_value = target_tasks[0] if isinstance(target_tasks, list) else target_tasks
237
+ next_task = next((t for t in self.tasks.values() if t.name == task_value), None)
238
+ if next_task:
239
+ next_task.status = "not started" # Reset status to allow execution
240
+ logging.debug(f"Routing to {next_task.name} based on decision: {decision_str}")
241
+ self.workflow_finished = False
242
+ current_task = next_task
243
+ # Ensure the task is yielded for execution
244
+ if current_task.id not in visited_tasks:
245
+ yield current_task.id
246
+ visited_tasks.add(current_task.id)
247
+ else:
248
+ # End workflow if no valid next task found
249
+ logging.info(f"No valid next task found for decision: {decision_str}")
250
+ self.workflow_finished = True
251
+ current_task = None
252
+ break
253
+ else:
254
+ logging.debug(f"No subtasks created yet for {current_task.name}")
255
+ # Create subtasks if needed
256
+ if current_task.input_file:
257
+ self._create_loop_subtasks(current_task)
258
+ current_task._subtasks_created = True
259
+ logging.debug(f"Created subtasks from {current_task.input_file}")
260
+ else:
261
+ # No input file, mark as done
262
+ current_task.status = "completed"
263
+ logging.debug(f"No input file, marking {current_task.name} as completed")
264
+ if current_task.next_tasks:
265
+ next_task_name = current_task.next_tasks[0]
266
+ next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
267
+ current_task = next_task
268
+ else:
269
+ current_task = None
270
+ else:
271
+ # Execute non-loop task
272
+ logging.debug(f"=== Executing non-loop task: {current_task.name} (id: {task_id}) ===")
273
+ logging.debug(f"Task status: {current_task.status}")
274
+ logging.debug(f"Task next_tasks: {current_task.next_tasks}")
275
+ yield task_id
276
+ visited_tasks.add(task_id)
277
+
278
+ # Only end workflow if no next_tasks AND no conditions
279
+ if not current_task.next_tasks and not current_task.condition and not any(
280
+ t.task_type == "loop" and current_task.name.startswith(t.name + "_")
281
+ for t in self.tasks.values()
282
+ ):
283
+ logging.info(f"Task {current_task.name} has no next tasks, ending workflow")
284
+ self.workflow_finished = True
285
+ current_task = None
286
+ break
287
+
126
288
  # Reset completed task to "not started" so it can run again
127
289
  if self.tasks[task_id].status == "completed":
128
- logging.debug(f"Task {task_id} was completed, resetting to 'not started' for next iteration.")
129
- self.tasks[task_id].status = "not started"
130
-
290
+ # Never reset loop tasks, decision tasks, or their subtasks if rerun is False
291
+ subtask_name = self.tasks[task_id].name
292
+ task_to_check = self.tasks[task_id]
293
+ logging.debug(f"=== Checking reset for completed task: {subtask_name} ===")
294
+ logging.debug(f"Task type: {task_to_check.task_type}")
295
+ logging.debug(f"Task status before reset check: {task_to_check.status}")
296
+ logging.debug(f"Task rerun: {getattr(task_to_check, 'rerun', True)}") # default to True if not set
297
+
298
+ if (getattr(task_to_check, 'rerun', True) and # Corrected condition - reset only if rerun is True (or default True)
299
+ task_to_check.task_type != "loop" and # Removed "decision" from exclusion
300
+ not any(t.task_type == "loop" and subtask_name.startswith(t.name + "_")
301
+ for t in self.tasks.values())):
302
+ logging.debug(f"=== Resetting non-loop, non-decision task {subtask_name} to 'not started' ===")
303
+ self.tasks[task_id].status = "not started"
304
+ logging.debug(f"Task status after reset: {self.tasks[task_id].status}")
305
+ else:
306
+ logging.debug(f"=== Skipping reset for loop/decision/subtask or rerun=False: {subtask_name} ===")
307
+ logging.debug(f"Keeping status as: {self.tasks[task_id].status}")
308
+
131
309
  # Handle loop progression
132
310
  if current_task.task_type == "loop":
133
311
  loop_key = f"loop_{current_task.name}"
@@ -135,7 +313,7 @@ class Process:
135
313
  loop_info = loop_data[loop_key]
136
314
  loop_info["index"] += 1
137
315
  has_more = loop_info["remaining"] > 0
138
-
316
+
139
317
  # Update result to trigger correct condition
140
318
  if current_task.result:
141
319
  result = current_task.result.raw
@@ -144,41 +322,90 @@ class Process:
144
322
  else:
145
323
  result += "\ndone"
146
324
  current_task.result.raw = result
147
-
325
+
148
326
  # Determine next task based on result
149
327
  next_task = None
150
328
  if current_task and current_task.result:
151
329
  if current_task.task_type in ["decision", "loop"]:
152
- # MINIMAL CHANGE: use pydantic decision if present
330
+ # Get decision from pydantic or raw response
153
331
  decision_str = current_task.result.raw.lower()
154
332
  if current_task.result.pydantic and hasattr(current_task.result.pydantic, "decision"):
155
333
  decision_str = current_task.result.pydantic.decision.lower()
156
334
 
157
- # Check conditions
158
- for condition, tasks in current_task.condition.items():
159
- if condition.lower() == decision_str:
160
- # Handle both list and direct string values
161
- task_value = tasks[0] if isinstance(tasks, list) else tasks
162
- if not task_value or task_value == "exit":
163
- logging.info("Workflow exit condition met, ending workflow")
164
- current_task = None
165
- break
166
- next_task_name = task_value
167
- next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
168
- # For loops, allow revisiting the same task
169
- if next_task and next_task.id == current_task.id:
170
- visited_tasks.discard(current_task.id)
335
+ # Check if task has conditions and next_tasks
336
+ if current_task.condition:
337
+ # Get target task based on decision
338
+ target_tasks = current_task.condition.get(decision_str, [])
339
+ # Handle all forms of exit conditions
340
+ if not target_tasks or target_tasks == "exit" or (isinstance(target_tasks, list) and (not target_tasks or target_tasks[0] == "exit")):
341
+ logging.info(f"Workflow exit condition met on decision: {decision_str}")
342
+ self.workflow_finished = True
343
+ current_task = None
171
344
  break
172
-
345
+ else:
346
+ # Find the target task by name
347
+ task_value = target_tasks[0] if isinstance(target_tasks, list) else target_tasks
348
+ next_task = next((t for t in self.tasks.values() if t.name == task_value), None)
349
+ if next_task:
350
+ next_task.status = "not started" # Reset status to allow execution
351
+ logging.debug(f"Routing to {next_task.name} based on decision: {decision_str}")
352
+ # Don't mark workflow as finished when following condition path
353
+ self.workflow_finished = False
354
+
355
+ # If no condition-based routing, use next_tasks
173
356
  if not next_task and current_task and current_task.next_tasks:
174
357
  next_task_name = current_task.next_tasks[0]
175
358
  next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
176
-
359
+ if next_task:
360
+ # Reset the next task to allow re-execution
361
+ next_task.status = "not started"
362
+ # Don't mark workflow as finished if we're in a task loop
363
+ if (next_task.previous_tasks and current_task.name in next_task.previous_tasks and
364
+ next_task.next_tasks and
365
+ next_task.next_tasks[0] in self.tasks and
366
+ next_task.name in self.tasks[next_task.next_tasks[0]].previous_tasks):
367
+ self.workflow_finished = False
368
+ logging.debug(f"Following next_tasks to {next_task.name}")
369
+
177
370
  current_task = next_task
178
371
  if not current_task:
372
+ current_task = self._find_next_not_started_task() # General fallback if no next task in workflow
373
+
374
+
375
+ if not current_task:
376
+ # Add final workflow summary
377
+ logging.debug(f"""
378
+ === Final Workflow Summary ===
379
+ Total tasks processed: {len(self.tasks)}
380
+ Final status:
381
+ - Completed tasks: {sum(1 for t in self.tasks.values() if t.status == "completed")}
382
+ - Outstanding tasks: {sum(1 for t in self.tasks.values() if t.status != "completed")}
383
+ Tasks by status:
384
+ - Not started: {sum(1 for t in self.tasks.values() if t.status == "not started")}
385
+ - In progress: {sum(1 for t in self.tasks.values() if t.status == "in_progress")}
386
+ - Completed: {sum(1 for t in self.tasks.values() if t.status == "completed")}
387
+ - Failed: {sum(1 for t in self.tasks.values() if t.status == "failed")}
388
+ Tasks by type:
389
+ - Loop tasks: {sum(1 for t in self.tasks.values() if t.task_type == "loop")}
390
+ - Decision tasks: {sum(1 for t in self.tasks.values() if t.task_type == "decision")}
391
+ - Regular tasks: {sum(1 for t in self.tasks.values() if t.task_type not in ["loop", "decision"])}
392
+ Total iterations: {current_iter}
393
+ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
394
+ """)
395
+
179
396
  logging.info("Workflow execution completed")
180
397
  break
181
398
 
399
+ # Add completion logging
400
+ logging.debug(f"""
401
+ === Task Completion ===
402
+ Task: {current_task.name}
403
+ Final status: {current_task.status}
404
+ Next task: {next_task.name if next_task else None}
405
+ Iteration: {current_iter}/{self.max_iter}
406
+ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
407
+ """)
408
+
182
409
  async def asequential(self) -> AsyncGenerator[str, None]:
183
410
  """Async version of sequential method"""
184
411
  for task_id in self.tasks:
@@ -308,7 +535,7 @@ Provide a JSON with the structure:
308
535
  self.tasks[manager_task.id].status = "completed"
309
536
  if self.verbose >= 1:
310
537
  logging.info("All tasks completed under manager supervision.")
311
- logging.info("Hierarchical task execution finished")
538
+ logging.info("Hierarchical task execution finished")
312
539
 
313
540
  def workflow(self):
314
541
  """Synchronous version of workflow method"""
@@ -327,12 +554,12 @@ Provide a JSON with the structure:
327
554
  if task.is_start:
328
555
  start_task = task
329
556
  break
330
-
557
+
331
558
  if not start_task:
332
559
  start_task = list(self.tasks.values())[0]
333
560
  logging.info("No start task marked, using first task")
334
561
 
335
- # If loop type and no input_file, default to tasks.csv
562
+ # If loop type and no input_file, default to tasks.csv
336
563
  if start_task and start_task.task_type == "loop" and not start_task.input_file:
337
564
  start_task.input_file = "tasks.csv"
338
565
 
@@ -341,35 +568,61 @@ Provide a JSON with the structure:
341
568
  try:
342
569
  file_ext = os.path.splitext(start_task.input_file)[1].lower()
343
570
  new_tasks = []
344
-
571
+
345
572
  if file_ext == ".csv":
346
- # existing CSV reading logic
347
573
  with open(start_task.input_file, "r", encoding="utf-8") as f:
348
- # Try as simple CSV first
349
- reader = csv.reader(f)
574
+ reader = csv.reader(f, quotechar='"', escapechar='\\') # Handle quoted/escaped fields
350
575
  previous_task = None
576
+ task_count = 0
577
+
351
578
  for i, row in enumerate(reader):
352
- if row: # Skip empty rows
353
- task_desc = row[0] # Take first column
354
- row_task = Task(
355
- description=f"{start_task.description}\n{task_desc}" if start_task.description else task_desc,
356
- agent=start_task.agent,
357
- name=f"{start_task.name}_{i+1}" if start_task.name else task_desc,
358
- expected_output=getattr(start_task, 'expected_output', None),
359
- is_start=(i == 0),
360
- task_type="task",
361
- condition={
362
- "complete": ["next"],
363
- "retry": ["current"]
364
- }
365
- )
366
- self.tasks[row_task.id] = row_task
367
- new_tasks.append(row_task)
368
-
369
- if previous_task:
370
- previous_task.next_tasks = [row_task.name]
371
- previous_task.condition["complete"] = [row_task.name]
372
- previous_task = row_task
579
+ if not row: # Skip truly empty rows
580
+ continue
581
+
582
+ # Properly handle Q&A pairs with potential commas
583
+ task_desc = row[0].strip() if row else ""
584
+ if len(row) > 1:
585
+ # Preserve all fields in case of multiple commas
586
+ question = row[0].strip()
587
+ answer = ",".join(field.strip() for field in row[1:])
588
+ task_desc = f"Question: {question}\nAnswer: {answer}"
589
+
590
+ if not task_desc: # Skip rows with empty content
591
+ continue
592
+
593
+ task_count += 1
594
+ logging.debug(f"Processing CSV row {i+1}: {task_desc}")
595
+
596
+ # Inherit next_tasks from parent loop task
597
+ inherited_next_tasks = start_task.next_tasks if start_task.next_tasks else []
598
+
599
+ row_task = Task(
600
+ description=f"{start_task.description}\n{task_desc}" if start_task.description else task_desc,
601
+ agent=start_task.agent,
602
+ name=f"{start_task.name}_{task_count}" if start_task.name else task_desc,
603
+ expected_output=getattr(start_task, 'expected_output', None),
604
+ is_start=(task_count == 1),
605
+ task_type="decision", # Change to decision type
606
+ next_tasks=inherited_next_tasks, # Inherit parent's next tasks
607
+ condition={
608
+ "done": inherited_next_tasks if inherited_next_tasks else ["next"], # Use full inherited_next_tasks
609
+ "retry": ["current"],
610
+ "exit": [] # Empty list for exit condition
611
+ }
612
+ )
613
+ self.tasks[row_task.id] = row_task
614
+ new_tasks.append(row_task)
615
+
616
+ if previous_task:
617
+ previous_task.next_tasks = [row_task.name]
618
+ previous_task.condition["done"] = [row_task.name] # Use "done" consistently
619
+ previous_task = row_task
620
+
621
+ # For the last task in the loop, ensure it points to parent's next tasks
622
+ if task_count > 0 and not row_task.next_tasks:
623
+ row_task.next_tasks = inherited_next_tasks
624
+
625
+ logging.info(f"Processed {task_count} rows from CSV file")
373
626
  else:
374
627
  # If not CSV, read lines
375
628
  with open(start_task.input_file, "r", encoding="utf-8") as f:
@@ -390,7 +643,7 @@ Provide a JSON with the structure:
390
643
  )
391
644
  self.tasks[row_task.id] = row_task
392
645
  new_tasks.append(row_task)
393
-
646
+
394
647
  if previous_task:
395
648
  previous_task.next_tasks = [row_task.name]
396
649
  previous_task.condition["complete"] = [row_task.name]
@@ -402,86 +655,289 @@ Provide a JSON with the structure:
402
655
  except Exception as e:
403
656
  logging.error(f"Failed to read file tasks: {e}")
404
657
 
405
- # end of the new block
658
+ # end of start task handling
406
659
  current_task = start_task
407
660
  visited_tasks = set()
408
661
  loop_data = {} # Store loop-specific data
409
-
662
+
410
663
  while current_task:
411
664
  current_iter += 1
412
665
  if current_iter > self.max_iter:
413
666
  logging.info(f"Max iteration limit {self.max_iter} reached, ending workflow.")
414
667
  break
415
668
 
669
+ # ADDED: Check workflow finished flag at the start of each cycle
670
+ if self.workflow_finished:
671
+ logging.info("Workflow finished early as all tasks are completed.")
672
+ break
673
+
674
+ # Add task summary at start of each cycle
675
+ logging.debug(f"""
676
+ === Workflow Cycle {current_iter} Summary ===
677
+ Total tasks: {len(self.tasks)}
678
+ Outstanding tasks: {sum(1 for t in self.tasks.values() if t.status != "completed")}
679
+ Completed tasks: {sum(1 for t in self.tasks.values() if t.status == "completed")}
680
+ Tasks by status:
681
+ - Not started: {sum(1 for t in self.tasks.values() if t.status == "not started")}
682
+ - In progress: {sum(1 for t in self.tasks.values() if t.status == "in_progress")}
683
+ - Completed: {sum(1 for t in self.tasks.values() if t.status == "completed")}
684
+ Tasks by type:
685
+ - Loop tasks: {sum(1 for t in self.tasks.values() if t.task_type == "loop")}
686
+ - Decision tasks: {sum(1 for t in self.tasks.values() if t.task_type == "decision")}
687
+ - Regular tasks: {sum(1 for t in self.tasks.values() if t.task_type not in ["loop", "decision"])}
688
+ """)
689
+
690
+ # ADDED: Check if all tasks are completed and set workflow_finished flag
691
+ if all(task.status == "completed" for task in self.tasks.values()):
692
+ logging.info("All tasks are completed.")
693
+ self.workflow_finished = True
694
+ # The next iteration loop check will break the workflow
695
+
696
+
697
+ # Handle loop task file reading at runtime
698
+ if (current_task.task_type == "loop" and
699
+ current_task is not start_task and
700
+ getattr(current_task, "_subtasks_created", False) is not True):
701
+
702
+ if not current_task.input_file:
703
+ current_task.input_file = "tasks.csv"
704
+
705
+ if getattr(current_task, "input_file", None):
706
+ try:
707
+ file_ext = os.path.splitext(current_task.input_file)[1].lower()
708
+ new_tasks = []
709
+
710
+ if file_ext == ".csv":
711
+ with open(current_task.input_file, "r", encoding="utf-8") as f:
712
+ reader = csv.reader(f)
713
+ previous_task = None
714
+ for i, row in enumerate(reader):
715
+ if row: # Skip empty rows
716
+ task_desc = row[0] # Take first column
717
+ row_task = Task(
718
+ description=f"{current_task.description}\n{task_desc}" if current_task.description else task_desc,
719
+ agent=current_task.agent,
720
+ name=f"{current_task.name}_{i+1}" if current_task.name else task_desc,
721
+ expected_output=getattr(current_task, 'expected_output', None),
722
+ is_start=(i == 0),
723
+ task_type="task",
724
+ condition={
725
+ "complete": ["next"],
726
+ "retry": ["current"]
727
+ }
728
+ )
729
+ self.tasks[row_task.id] = row_task
730
+ new_tasks.append(row_task)
731
+
732
+ if previous_task:
733
+ previous_task.next_tasks = [row_task.name]
734
+ previous_task.condition["complete"] = [row_task.name]
735
+ previous_task = row_task
736
+ else:
737
+ with open(current_task.input_file, "r", encoding="utf-8") as f:
738
+ lines = f.read().splitlines()
739
+ previous_task = None
740
+ for i, line in enumerate(lines):
741
+ row_task = Task(
742
+ description=f"{current_task.description}\n{line.strip()}" if current_task.description else line.strip(),
743
+ agent=current_task.agent,
744
+ name=f"{current_task.name}_{i+1}" if current_task.name else line.strip(),
745
+ expected_output=getattr(current_task, 'expected_output', None),
746
+ is_start=(i == 0),
747
+ task_type="task",
748
+ condition={
749
+ "complete": ["next"],
750
+ "retry": ["current"]
751
+ }
752
+ )
753
+ self.tasks[row_task.id] = row_task
754
+ new_tasks.append(row_task)
755
+
756
+ if previous_task:
757
+ previous_task.next_tasks = [row_task.name]
758
+ previous_task.condition["complete"] = [row_task.name]
759
+ previous_task = row_task
760
+
761
+ if new_tasks:
762
+ current_task.next_tasks = [new_tasks[0].name]
763
+ current_task._subtasks_created = True
764
+ logging.info(f"Created {len(new_tasks)} tasks from: {current_task.input_file} for loop task {current_task.name}")
765
+ except Exception as e:
766
+ logging.error(f"Failed to read file tasks for loop task {current_task.name}: {e}")
767
+
416
768
  task_id = current_task.id
417
- logging.info(f"Executing workflow task: {current_task.name if current_task.name else task_id}")
418
-
769
+ logging.debug(f"""
770
+ === Task Execution Details ===
771
+ Current task: {current_task.name}
772
+ Type: {current_task.task_type}
773
+ Status: {current_task.status}
774
+ Previous tasks: {current_task.previous_tasks}
775
+ Next tasks: {current_task.next_tasks}
776
+ Context tasks: {[t.name for t in current_task.context] if current_task.context else []}
777
+ Description length: {len(current_task.description)}
778
+ """)
779
+
419
780
  # Add context from previous tasks to description
420
781
  if current_task.previous_tasks or current_task.context:
421
782
  context = "\nInput data from previous tasks:"
422
-
783
+
423
784
  # Add data from previous tasks in workflow
424
785
  for prev_name in current_task.previous_tasks:
425
786
  prev_task = next((t for t in self.tasks.values() if t.name == prev_name), None)
426
787
  if prev_task and prev_task.result:
427
788
  # Handle loop data
428
789
  if current_task.task_type == "loop":
429
- # # create a loop manager Agent
430
- # loop_manager = Agent(
431
- # name="Loop Manager",
432
- # role="Loop data processor",
433
- # goal="Process loop data and convert it to list format",
434
- # backstory="Expert at handling loop data and converting it to proper format",
435
- # llm=self.manager_llm,
436
- # verbose=self.verbose,
437
- # markdown=True
438
- # )
439
-
440
- # # get the loop data convert it to list using calling Agent class chat
441
- # loop_prompt = f"""
442
- # Process this data into a list format:
443
- # {prev_task.result.raw}
444
-
445
- # Return a JSON object with an 'items' array containing the items to process.
446
- # """
447
- # loop_data_str = loop_manager.chat(
448
- # prompt=loop_prompt,
449
- # output_json=LoopItems
450
- # )
451
-
452
- # try:
453
- # # The response will already be parsed into LoopItems model
454
- # loop_data[f"loop_{current_task.name}"] = {
455
- # "items": loop_data_str.items,
456
- # "index": 0,
457
- # "remaining": len(loop_data_str.items)
458
- # }
459
- # context += f"\nCurrent loop item: {loop_data_str.items[0]}"
460
- # except Exception as e:
461
- # display_error(f"Failed to process loop data: {e}")
462
- # context += f"\n{prev_name}: {prev_task.result.raw}"
463
790
  context += f"\n{prev_name}: {prev_task.result.raw}"
464
791
  else:
465
792
  context += f"\n{prev_name}: {prev_task.result.raw}"
466
-
793
+
467
794
  # Add data from context tasks
468
795
  if current_task.context:
469
796
  for ctx_task in current_task.context:
470
797
  if ctx_task.result and ctx_task.name != current_task.name:
471
798
  context += f"\n{ctx_task.name}: {ctx_task.result.raw}"
472
-
799
+
473
800
  # Update task description with context
474
801
  current_task.description = current_task.description + context
475
-
476
- # Execute task using existing run_task method
477
- yield task_id
478
- visited_tasks.add(task_id)
479
-
480
- # Reset completed task to "not started" so it can run again: Only for workflow because some tasks may be revisited
802
+
803
+ # Skip execution for loop tasks, only process their subtasks
804
+ if current_task.task_type == "loop":
805
+ logging.debug(f"""
806
+ === Loop Task Details ===
807
+ Name: {current_task.name}
808
+ ID: {current_task.id}
809
+ Status: {current_task.status}
810
+ Next tasks: {current_task.next_tasks}
811
+ Condition: {current_task.condition}
812
+ Subtasks created: {getattr(current_task, '_subtasks_created', False)}
813
+ Input file: {getattr(current_task, 'input_file', None)}
814
+ """)
815
+
816
+ # Check if subtasks are created and completed
817
+ if getattr(current_task, "_subtasks_created", False):
818
+ subtasks = [
819
+ t for t in self.tasks.values()
820
+ if t.name.startswith(current_task.name + "_")
821
+ ]
822
+
823
+ logging.debug(f"""
824
+ === Subtask Status Check ===
825
+ Total subtasks: {len(subtasks)}
826
+ Completed: {sum(1 for st in subtasks if st.status == "completed")}
827
+ Pending: {sum(1 for st in subtasks if st.status != "completed")}
828
+ """)
829
+
830
+ for st in subtasks:
831
+ logging.debug(f"""
832
+ Subtask: {st.name}
833
+ - Status: {st.status}
834
+ - Next tasks: {st.next_tasks}
835
+ - Condition: {st.condition}
836
+ """)
837
+
838
+ if subtasks and all(st.status == "completed" for st in subtasks):
839
+ logging.debug(f"=== All {len(subtasks)} subtasks completed for {current_task.name} ===")
840
+
841
+ # Mark loop task completed and move to next task
842
+ current_task.status = "completed"
843
+ logging.debug(f"Loop {current_task.name} marked as completed")
844
+
845
+ # Set result for loop task when all subtasks complete
846
+ if not current_task.result:
847
+ # Get result from last completed subtask
848
+ last_subtask = next((t for t in reversed(subtasks) if t.status == "completed"), None)
849
+ if last_subtask and last_subtask.result:
850
+ current_task.result = last_subtask.result
851
+
852
+ # Route to next task based on condition
853
+ if current_task.condition:
854
+ # Get decision from result if available
855
+ decision_str = None
856
+ if current_task.result:
857
+ if current_task.result.pydantic and hasattr(current_task.result.pydantic, "decision"):
858
+ decision_str = current_task.result.pydantic.decision.lower()
859
+ elif current_task.result.raw:
860
+ decision_str = current_task.result.raw.lower()
861
+
862
+ # For loop tasks, use "done" to follow condition path
863
+ if current_task.task_type == "loop" and all(t.status == "completed" for t in subtasks):
864
+ decision_str = "done"
865
+
866
+ target_tasks = current_task.condition.get(decision_str, []) if decision_str else []
867
+ task_value = target_tasks[0] if isinstance(target_tasks, list) else target_tasks
868
+ next_task = next((t for t in self.tasks.values() if t.name == task_value), None)
869
+ if next_task:
870
+ next_task.status = "not started" # Reset status to allow execution
871
+ logging.debug(f"Routing to {next_task.name} based on decision: {decision_str}")
872
+ self.workflow_finished = False
873
+ current_task = next_task
874
+ # Ensure the task is yielded for execution
875
+ if current_task.id not in visited_tasks:
876
+ yield current_task.id
877
+ visited_tasks.add(current_task.id)
878
+ else:
879
+ # End workflow if no valid next task found
880
+ logging.info(f"No valid next task found for decision: {decision_str}")
881
+ self.workflow_finished = True
882
+ current_task = None
883
+ break
884
+ else:
885
+ logging.debug(f"No subtasks created yet for {current_task.name}")
886
+ # Create subtasks if needed
887
+ if current_task.input_file:
888
+ self._create_loop_subtasks(current_task)
889
+ current_task._subtasks_created = True
890
+ logging.debug(f"Created subtasks from {current_task.input_file}")
891
+ else:
892
+ # No input file, mark as done
893
+ current_task.status = "completed"
894
+ logging.debug(f"No input file, marking {current_task.name} as completed")
895
+ if current_task.next_tasks:
896
+ next_task_name = current_task.next_tasks[0]
897
+ next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
898
+ current_task = next_task
899
+ else:
900
+ current_task = None
901
+ else:
902
+ # Execute non-loop task
903
+ logging.debug(f"=== Executing non-loop task: {current_task.name} (id: {task_id}) ===")
904
+ logging.debug(f"Task status: {current_task.status}")
905
+ logging.debug(f"Task next_tasks: {current_task.next_tasks}")
906
+ yield task_id
907
+ visited_tasks.add(task_id)
908
+
909
+ # Only end workflow if no next_tasks AND no conditions
910
+ if not current_task.next_tasks and not current_task.condition and not any(
911
+ t.task_type == "loop" and current_task.name.startswith(t.name + "_")
912
+ for t in self.tasks.values()
913
+ ):
914
+ logging.info(f"Task {current_task.name} has no next tasks, ending workflow")
915
+ self.workflow_finished = True
916
+ current_task = None
917
+ break
918
+
919
+ # Reset completed task to "not started" so it can run again
481
920
  if self.tasks[task_id].status == "completed":
482
- logging.debug(f"Task {task_id} was completed, resetting to 'not started' for next iteration.")
483
- self.tasks[task_id].status = "not started"
484
-
921
+ # Never reset loop tasks, decision tasks, or their subtasks if rerun is False
922
+ subtask_name = self.tasks[task_id].name
923
+ task_to_check = self.tasks[task_id]
924
+ logging.debug(f"=== Checking reset for completed task: {subtask_name} ===")
925
+ logging.debug(f"Task type: {task_to_check.task_type}")
926
+ logging.debug(f"Task status before reset check: {task_to_check.status}")
927
+ logging.debug(f"Task rerun: {getattr(task_to_check, 'rerun', True)}") # default to True if not set
928
+
929
+ if (getattr(task_to_check, 'rerun', True) and # Corrected condition - reset only if rerun is True (or default True)
930
+ task_to_check.task_type != "loop" and # Removed "decision" from exclusion
931
+ not any(t.task_type == "loop" and subtask_name.startswith(t.name + "_")
932
+ for t in self.tasks.values())):
933
+ logging.debug(f"=== Resetting non-loop, non-decision task {subtask_name} to 'not started' ===")
934
+ self.tasks[task_id].status = "not started"
935
+ logging.debug(f"Task status after reset: {self.tasks[task_id].status}")
936
+ else:
937
+ logging.debug(f"=== Skipping reset for loop/decision/subtask or rerun=False: {subtask_name} ===")
938
+ logging.debug(f"Keeping status as: {self.tasks[task_id].status}")
939
+
940
+
485
941
  # Handle loop progression
486
942
  if current_task.task_type == "loop":
487
943
  loop_key = f"loop_{current_task.name}"
@@ -489,7 +945,7 @@ Provide a JSON with the structure:
489
945
  loop_info = loop_data[loop_key]
490
946
  loop_info["index"] += 1
491
947
  has_more = loop_info["remaining"] > 0
492
-
948
+
493
949
  # Update result to trigger correct condition
494
950
  if current_task.result:
495
951
  result = current_task.result.raw
@@ -498,41 +954,90 @@ Provide a JSON with the structure:
498
954
  else:
499
955
  result += "\ndone"
500
956
  current_task.result.raw = result
501
-
957
+
502
958
  # Determine next task based on result
503
959
  next_task = None
504
960
  if current_task and current_task.result:
505
961
  if current_task.task_type in ["decision", "loop"]:
506
- # MINIMAL CHANGE: use pydantic decision if present
962
+ # Get decision from pydantic or raw response
507
963
  decision_str = current_task.result.raw.lower()
508
964
  if current_task.result.pydantic and hasattr(current_task.result.pydantic, "decision"):
509
965
  decision_str = current_task.result.pydantic.decision.lower()
510
966
 
511
- # Check conditions
512
- for condition, tasks in current_task.condition.items():
513
- if condition.lower() == decision_str:
514
- # Handle both list and direct string values
515
- task_value = tasks[0] if isinstance(tasks, list) else tasks
516
- if not task_value or task_value == "exit":
517
- logging.info("Workflow exit condition met, ending workflow")
518
- current_task = None
519
- break
520
- next_task_name = task_value
521
- next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
522
- # For loops, allow revisiting the same task
523
- if next_task and next_task.id == current_task.id:
524
- visited_tasks.discard(current_task.id)
967
+ # Check if task has conditions and next_tasks
968
+ if current_task.condition:
969
+ # Get target task based on decision
970
+ target_tasks = current_task.condition.get(decision_str, [])
971
+ # Handle all forms of exit conditions
972
+ if not target_tasks or target_tasks == "exit" or (isinstance(target_tasks, list) and (not target_tasks or target_tasks[0] == "exit")):
973
+ logging.info(f"Workflow exit condition met on decision: {decision_str}")
974
+ self.workflow_finished = True
975
+ current_task = None
525
976
  break
526
-
977
+ else:
978
+ # Find the target task by name
979
+ task_value = target_tasks[0] if isinstance(target_tasks, list) else target_tasks
980
+ next_task = next((t for t in self.tasks.values() if t.name == task_value), None)
981
+ if next_task:
982
+ next_task.status = "not started" # Reset status to allow execution
983
+ logging.debug(f"Routing to {next_task.name} based on decision: {decision_str}")
984
+ # Don't mark workflow as finished when following condition path
985
+ self.workflow_finished = False
986
+
987
+ # If no condition-based routing, use next_tasks
527
988
  if not next_task and current_task and current_task.next_tasks:
528
989
  next_task_name = current_task.next_tasks[0]
529
990
  next_task = next((t for t in self.tasks.values() if t.name == next_task_name), None)
530
-
991
+ if next_task:
992
+ # Reset the next task to allow re-execution
993
+ next_task.status = "not started"
994
+ # Don't mark workflow as finished if we're in a task loop
995
+ if (next_task.previous_tasks and current_task.name in next_task.previous_tasks and
996
+ next_task.next_tasks and
997
+ next_task.next_tasks[0] in self.tasks and
998
+ next_task.name in self.tasks[next_task.next_tasks[0]].previous_tasks):
999
+ self.workflow_finished = False
1000
+ logging.debug(f"Following next_tasks to {next_task.name}")
1001
+
531
1002
  current_task = next_task
532
1003
  if not current_task:
1004
+ current_task = self._find_next_not_started_task() # General fallback if no next task in workflow
1005
+
1006
+
1007
+ if not current_task:
1008
+ # Add final workflow summary
1009
+ logging.debug(f"""
1010
+ === Final Workflow Summary ===
1011
+ Total tasks processed: {len(self.tasks)}
1012
+ Final status:
1013
+ - Completed tasks: {sum(1 for t in self.tasks.values() if t.status == "completed")}
1014
+ - Outstanding tasks: {sum(1 for t in self.tasks.values() if t.status != "completed")}
1015
+ Tasks by status:
1016
+ - Not started: {sum(1 for t in self.tasks.values() if t.status == "not started")}
1017
+ - In progress: {sum(1 for t in self.tasks.values() if t.status == "in_progress")}
1018
+ - Completed: {sum(1 for t in self.tasks.values() if t.status == "completed")}
1019
+ - Failed: {sum(1 for t in self.tasks.values() if t.status == "failed")}
1020
+ Tasks by type:
1021
+ - Loop tasks: {sum(1 for t in self.tasks.values() if t.task_type == "loop")}
1022
+ - Decision tasks: {sum(1 for t in self.tasks.values() if t.task_type == "decision")}
1023
+ - Regular tasks: {sum(1 for t in self.tasks.values() if t.task_type not in ["loop", "decision"])}
1024
+ Total iterations: {current_iter}
1025
+ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
1026
+ """)
1027
+
533
1028
  logging.info("Workflow execution completed")
534
1029
  break
535
1030
 
1031
+ # Add completion logging
1032
+ logging.debug(f"""
1033
+ === Task Completion ===
1034
+ Task: {current_task.name}
1035
+ Final status: {current_task.status}
1036
+ Next task: {next_task.name if next_task else None}
1037
+ Iteration: {current_iter}/{self.max_iter}
1038
+ Workflow Finished: {self.workflow_finished} # ADDED: Workflow Finished Status
1039
+ """)
1040
+
536
1041
  def sequential(self):
537
1042
  """Synchronous version of sequential method"""
538
1043
  for task_id in self.tasks:
@@ -651,4 +1156,4 @@ Provide a JSON with the structure:
651
1156
  self.tasks[manager_task.id].status = "completed"
652
1157
  if self.verbose >= 1:
653
1158
  logging.info("All tasks completed under manager supervision.")
654
- logging.info("Hierarchical task execution finished")
1159
+ logging.info("Hierarchical task execution finished")