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.
- praisonaiagents/process/process.py +680 -175
- praisonaiagents/task/task.py +18 -16
- praisonaiagents/tools/csv_tools.py +54 -23
- praisonaiagents/tools/train/data/generatecot.py +13 -6
- {praisonaiagents-0.0.57.dist-info → praisonaiagents-0.0.59.dist-info}/METADATA +1 -1
- {praisonaiagents-0.0.57.dist-info → praisonaiagents-0.0.59.dist-info}/RECORD +8 -8
- {praisonaiagents-0.0.57.dist-info → praisonaiagents-0.0.59.dist-info}/WHEEL +0 -0
- {praisonaiagents-0.0.57.dist-info → praisonaiagents-0.0.59.dist-info}/top_level.txt +0 -0
@@ -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.
|
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.
|
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
|
-
#
|
123
|
-
|
124
|
-
|
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
|
-
|
129
|
-
self.tasks[task_id].
|
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
|
-
#
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
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.
|
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
|
-
#
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
483
|
-
self.tasks[task_id].
|
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
|
-
#
|
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
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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")
|