versionhq 1.2.0.4__py3-none-any.whl → 1.2.1.1__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.
@@ -5,7 +5,7 @@ from pydantic import BaseModel
5
5
 
6
6
  from versionhq.task.model import Task
7
7
  from versionhq.agent.model import Agent
8
- from versionhq.team.model import Team, Member, Formation
8
+ from versionhq.agent_network.model import AgentNetwork, Member, Formation
9
9
  from versionhq.agent.inhouse_agents import vhq_formation_planner
10
10
  from versionhq._utils import Logger
11
11
 
@@ -16,7 +16,7 @@ def form_agent_network(
16
16
  agents: List[Agent] = None,
17
17
  context: str = None,
18
18
  formation: Type[Formation] = None
19
- ) -> Team | None:
19
+ ) -> AgentNetwork | None:
20
20
  """
21
21
  Make a formation of agents from the given task description, expected outcome, agents (optional), and context (optional).
22
22
  """
@@ -68,7 +68,7 @@ def form_agent_network(
68
68
 
69
69
  vhq_task = Task(
70
70
  description=f"""
71
- Create a team of specialized agents designed to automate the following task and deliver the expected outcome. Consider the necessary roles for each agent with a clear task description. If you think we neeed a leader to handle the automation, return a leader_agent role as well, but if not, leave the a leader_agent role blank.
71
+ Create a team of specialized agents designed to automate the following task and deliver the expected outcome. Consider the necessary roles for each agent with a clear task description. If you think we neeed a leader to handle the automation, return a leader_agent role as well, but if not, leave the a leader_agent role blank. When you have a leader_agent, the formation must be SUPERVISING or HYBRID.
72
72
  Task: {str(task)}
73
73
  Expected outcome: {str(expected_outcome)}
74
74
  Formation: {prompt_formation}
@@ -91,7 +91,7 @@ def form_agent_network(
91
91
  created_agents = [Agent(role=item, goal=item) for item in res.pydantic.agent_roles]
92
92
  created_tasks = [Task(description=item) for item in res.pydantic.task_descriptions]
93
93
 
94
- team_tasks = []
94
+ network_tasks = []
95
95
  members = []
96
96
  leader = str(res.pydantic.leader_agent)
97
97
 
@@ -106,11 +106,11 @@ def form_agent_network(
106
106
 
107
107
 
108
108
  if len(created_agents) < len(created_tasks):
109
- team_tasks.extend(created_tasks[len(created_agents):len(created_tasks)])
109
+ network_tasks.extend(created_tasks[len(created_agents):len(created_tasks)])
110
110
 
111
111
  members.sort(key=lambda x: x.is_manager == False)
112
- team = Team( members=members, formation=_formation, team_tasks=team_tasks, planner_llm=vhq_formation_planner.llm)
113
- return team
112
+ network = AgentNetwork(members=members, formation=_formation, network_tasks=network_tasks)
113
+ return network
114
114
 
115
115
  else:
116
116
  res = res.json_dict
@@ -122,7 +122,7 @@ def form_agent_network(
122
122
  created_agents = [Agent(role=item, goal=item) for item in res["agent_roles"]]
123
123
  created_tasks = [Task(description=item) for item in res["task_descriptions"]]
124
124
 
125
- team_tasks = []
125
+ network_tasks = []
126
126
  members = []
127
127
  leader = str(res["leader_agent"])
128
128
 
@@ -136,11 +136,11 @@ def form_agent_network(
136
136
  members.append(member)
137
137
 
138
138
  if len(created_agents) < len(created_tasks):
139
- team_tasks.extend(created_tasks[len(created_agents):len(created_tasks)])
139
+ network_tasks.extend(created_tasks[len(created_agents):len(created_tasks)])
140
140
 
141
141
  members.sort(key=lambda x: x.is_manager == False)
142
- team = Team( members=members, formation=_formation, team_tasks=team_tasks, planner_llm=vhq_formation_planner.llm)
143
- return team
142
+ network = AgentNetwork(members=members, formation=_formation, network_tasks=network_tasks)
143
+ return network
144
144
 
145
145
 
146
146
  except Exception as e:
versionhq/task/model.py CHANGED
@@ -149,9 +149,16 @@ class ResponseField(BaseModel):
149
149
  return value
150
150
 
151
151
 
152
+ def _annotate(self, value: Any) -> Annotated:
153
+ """
154
+ Address Pydantic's `create_model`
155
+ """
156
+ return Annotated[self.type, value] if isinstance(value, self.type) else Annotated[str, str(value)]
157
+
158
+
152
159
  def create_pydantic_model(self, result: Dict, base_model: InstanceOf[BaseModel] | Any) -> Any:
153
160
  """
154
- Create a Pydantic model from the given result
161
+ Create a Pydantic model from the given result.
155
162
  """
156
163
  for k, v in result.items():
157
164
  if k is not self.title:
@@ -164,14 +171,6 @@ class ResponseField(BaseModel):
164
171
  return base_model
165
172
 
166
173
 
167
- def _annotate(self, value: Any) -> Annotated:
168
- """
169
- Address Pydantic's `create_model`
170
- """
171
- return Annotated[self.type, value] if isinstance(value, self.type) else Annotated[str, str(value)]
172
-
173
-
174
-
175
174
  class TaskOutput(BaseModel):
176
175
  """
177
176
  A class to store the final output of the given task in raw (string), json_dict, and pydantic class formats.
@@ -614,7 +613,7 @@ Ref. Output image: {output_formats_to_follow}
614
613
  """
615
614
 
616
615
  from versionhq.agent.model import Agent
617
- from versionhq.team.model import Team
616
+ from versionhq.agent_network.model import AgentNetwork
618
617
 
619
618
  self.prompt_context = context
620
619
  task_output: InstanceOf[TaskOutput] = None
@@ -627,15 +626,15 @@ Ref. Output image: {output_formats_to_follow}
627
626
  if isinstance(item, ToolSet) or isinstance(item, Tool) or type(item) == Tool:
628
627
  task_tools.append(item)
629
628
 
630
- if self.allow_delegation:
629
+ if self.allow_delegation == True:
631
630
  agent_to_delegate = None
632
631
 
633
- if hasattr(agent, "team") and isinstance(agent.team, Team):
634
- if agent.team.managers:
635
- idling_manager_agents = [manager.agent for manager in agent.team.managers if manager.is_idling]
636
- agent_to_delegate = idling_manager_agents[0] if idling_manager_agents else agent.team.managers[0]
632
+ if hasattr(agent, "network") and isinstance(agent.network, AgentNetwork):
633
+ if agent.network.managers:
634
+ idling_manager_agents = [manager.agent for manager in agent.network.managers if manager.is_idling]
635
+ agent_to_delegate = idling_manager_agents[0] if idling_manager_agents else agent.network.managers[0]
637
636
  else:
638
- peers = [member.agent for member in agent.team.members if member.is_manager == False and member.agent.id is not agent.id]
637
+ peers = [member.agent for member in agent.network.members if member.is_manager == False and member.agent.id is not agent.id]
639
638
  if len(peers) > 0:
640
639
  agent_to_delegate = peers[0]
641
640
  else:
@@ -5,7 +5,7 @@ import matplotlib.pyplot as plt
5
5
  from abc import ABC
6
6
  from typing import List, Any, Optional, Callable, Dict, Type, Tuple
7
7
 
8
- from pydantic import BaseModel, InstanceOf, Field, UUID4, PrivateAttr, field_validator, model_validator
8
+ from pydantic import BaseModel, InstanceOf, Field, UUID4, field_validator
9
9
  from pydantic_core import PydanticCustomError
10
10
 
11
11
  from versionhq.task.model import Task, TaskOutput
@@ -37,21 +37,21 @@ class DependencyType(enum.Enum):
37
37
 
38
38
 
39
39
 
40
- class TriggerEvent(enum.Enum):
41
- """
42
- Concise enumeration of key trigger events for task execution.
43
- """
44
- IMMEDIATE = 0 # execute immediately
45
- DEPENDENCIES_MET = 1 # All/required dependencies are satisfied
46
- RESOURCES_AVAILABLE = 2 # Necessary resources are available
47
- SCHEDULED_TIME = 3 # Scheduled start time or time window reached
48
- EXTERNAL_EVENT = 4 # Triggered by an external event/message
49
- DATA_AVAILABLE = 5 # Required data is available both internal/external
50
- APPROVAL_RECEIVED = 6 # Necessary approvals have been granted
51
- STATUS_CHANGED = 7 # Relevant task/system status has changed
52
- RULE_MET = 8 # A predefined rule or condition has been met
53
- MANUAL_TRIGGER = 9 # Manually initiated by a user
54
- ERROR_HANDLED = 10 # A previous error/exception has been handled
40
+ # class TriggerEvent(enum.Enum):
41
+ # """
42
+ # Concise enumeration of key trigger events for task execution.
43
+ # """
44
+ # IMMEDIATE = 0 # execute immediately
45
+ # DEPENDENCIES_MET = 1 # All/required dependencies are satisfied
46
+ # RESOURCES_AVAILABLE = 2 # Necessary resources are available
47
+ # SCHEDULED_TIME = 3 # Scheduled start time or time window reached
48
+ # EXTERNAL_EVENT = 4 # Triggered by an external event/message
49
+ # DATA_AVAILABLE = 5 # Required data is available both internal/external
50
+ # APPROVAL_RECEIVED = 6 # Necessary approvals have been granted
51
+ # STATUS_CHANGED = 7 # Relevant task/system status has changed
52
+ # RULE_MET = 8 # A predefined rule or condition has been met
53
+ # MANUAL_TRIGGER = 9 # Manually initiated by a user
54
+ # ERROR_HANDLED = 10 # A previous error/exception has been handled
55
55
 
56
56
 
57
57
 
@@ -62,8 +62,8 @@ class Node(BaseModel):
62
62
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
63
63
  task: InstanceOf[Task] = Field(default=None)
64
64
  # trigger_event: TriggerEvent = Field(default=TriggerEvent.IMMEDIATE, description="store trigger event for starting the task execution")
65
- in_degree_nodes: List[Any] = Field(default=None, description="list of Node objects")
66
- out_degree_nodes: List[Any] = Field(default=None, description="list of Node objects")
65
+ in_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
66
+ out_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
67
67
  assigned_to: InstanceOf[Agent] = Field(default=None)
68
68
  status: TaskStatus = Field(default=TaskStatus.NOT_STARTED)
69
69
 
@@ -77,22 +77,6 @@ class Node(BaseModel):
77
77
  def is_independent(self) -> bool:
78
78
  return not self.in_degree_nodes and not self.out_degree_nodes
79
79
 
80
- @property
81
- def in_degrees(self) -> int:
82
- return len(self.in_degree_nodes) if self.in_degree_nodes else 0
83
-
84
- @property
85
- def out_degrees(self) -> int:
86
- return len(self.out_degree_nodes) if self.out_degree_nodes else 0
87
-
88
- @property
89
- def degrees(self) -> int:
90
- return self.in_degrees + self.out_degrees
91
-
92
- @property
93
- def identifier(self) -> str:
94
- """Unique identifier for the node"""
95
- return f"{str(self.id)}"
96
80
 
97
81
  def handle_task_execution(self, agent: Agent = None, context: str = None) -> TaskOutput | None:
98
82
  """
@@ -110,15 +94,33 @@ class Node(BaseModel):
110
94
  self.status = TaskStatus.COMPLETED if res else TaskStatus.ERROR
111
95
  return res
112
96
 
97
+
98
+ @property
99
+ def in_degrees(self) -> int:
100
+ return len(self.in_degree_nodes) if self.in_degree_nodes else 0
101
+
102
+ @property
103
+ def out_degrees(self) -> int:
104
+ return len(self.out_degree_nodes) if self.out_degree_nodes else 0
105
+
106
+ @property
107
+ def degrees(self) -> int:
108
+ return self.in_degrees + self.out_degrees
109
+
110
+ @property
111
+ def identifier(self) -> str:
112
+ """Unique identifier for the node"""
113
+ return f"{str(self.id)}"
114
+
113
115
  def __str__(self):
114
116
  return self.identifier
115
117
 
116
118
 
117
-
118
119
  class Edge(BaseModel):
119
120
  """
120
121
  A class to store an edge object that connects source and target nodes.
121
122
  """
123
+
122
124
  source: Node = Field(default=None)
123
125
  target: Node = Field(default=None)
124
126
 
@@ -152,28 +154,28 @@ class Edge(BaseModel):
152
154
  case DependencyType.FINISH_TO_START:
153
155
  """target starts after source finishes"""
154
156
  if self.source.status == TaskStatus.COMPLETED:
155
- return self.condition(**self.conditon_kwargs) if self.condtion else True
157
+ return self.condition(**self.conditon_kwargs) if self.condition else True
156
158
  else:
157
159
  return False
158
160
 
159
161
  case DependencyType.START_TO_START:
160
162
  """target starts when source starts"""
161
163
  if self.source.status != TaskStatus.NOT_STARTED:
162
- return self.condition(**self.conditon_kwargs) if self.condtion else True
164
+ return self.condition(**self.conditon_kwargs) if self.condition else True
163
165
  else:
164
166
  return False
165
167
 
166
168
  case DependencyType.FINISH_TO_FINISH:
167
169
  """target finish when source start"""
168
170
  if self.source.status != TaskStatus.COMPLETED:
169
- return self.condition(**self.conditon_kwargs) if self.condtion else True
171
+ return self.condition(**self.conditon_kwargs) if self.condition else True
170
172
  else:
171
173
  return False
172
174
 
173
175
  case DependencyType.START_TO_FINISH:
174
176
  """target finishes when source start"""
175
177
  if self.source.status == TaskStatus.IN_PROGRESS:
176
- return self.condition(**self.conditon_kwargs) if self.condtion else True
178
+ return self.condition(**self.conditon_kwargs) if self.condition else True
177
179
  else:
178
180
  return False
179
181
 
@@ -183,19 +185,20 @@ class Edge(BaseModel):
183
185
  Activates the edge to initiate task execution of the target node.
184
186
  """
185
187
 
186
- if not self.dependency_met():
187
- Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll return None.", color="yellow")
188
- return None
189
-
190
188
  if not self.source or not self.target:
191
189
  Logger(verbose=True).log(level="warning", message="Cannot find source or target nodes. We'll return None.", color="yellow")
192
190
  return None
193
191
 
192
+ if not self.dependency_met():
193
+ Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll see the source node status.", color="yellow")
194
+ return None
195
+
196
+
194
197
  if self.lag:
195
198
  import time
196
199
  time.sleep(self.lag)
197
200
 
198
- context = self.source.task.task_output.raw if self.data_transfer else None
201
+ context = self.source.task.output.raw if self.data_transfer else None
199
202
  res = self.target.handle_task_execution(context=context)
200
203
  return res
201
204
 
@@ -205,8 +208,6 @@ class Graph(ABC, BaseModel):
205
208
  An abstract class to store G using NetworkX library.
206
209
  """
207
210
 
208
-
209
- _logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
210
211
  directed: bool = Field(default=False, description="Whether the graph is directed")
211
212
  graph: Type[nx.Graph] = Field(default=None)
212
213
  nodes: Dict[str, InstanceOf[Node]] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
@@ -226,7 +227,9 @@ class Graph(ABC, BaseModel):
226
227
  def add_edge(self, source: str, target: str, edge: Edge) -> None:
227
228
  self.graph.add_edge(source, target, **edge.model_dump())
228
229
  edge.source = self._return_node_object(source)
230
+ edge.source.out_degree_nodes.append(target)
229
231
  edge.target = self._return_node_object(target)
232
+ edge.target.in_degree_nodes.append(source)
230
233
  self.edges[(source, target)] = edge
231
234
 
232
235
  def add_weighted_edges_from(self, edges):
@@ -241,6 +244,28 @@ class Graph(ABC, BaseModel):
241
244
  def get_out_degree(self, node: Node) -> int:
242
245
  return self.graph.out_degree(node)
243
246
 
247
+ def find_start_nodes(self) -> Tuple[Node]:
248
+ return [v for k, v in self.nodes.items() if v.in_degrees == 0 and v.out_degrees > 0]
249
+
250
+ def find_end_nodes(self) -> Tuple[Node]:
251
+ return [v for k, v in self.nodes.items() if v.out_degrees == 0 and v.in_degrees > 0]
252
+
253
+ def find_critical_end_node(self) -> Node | None:
254
+ """
255
+ Find a critical end node from all the end nodes to lead a conclusion of the entire graph.
256
+ """
257
+ end_nodes = self.find_end_nodes()
258
+ if not end_nodes:
259
+ return None
260
+
261
+ if len(end_nodes) == 1:
262
+ return end_nodes[0]
263
+
264
+ edges = [v for k, v in self.edges if isinstance(v, Edge) and v.source in end_nodes]
265
+ critical_edge = max(edges, key=lambda item: item['weight']) if edges else None
266
+ return critical_edge.target if critical_edge else None
267
+
268
+
244
269
  def find_path(self, source: Optional[str] | None, target: str, weight: Optional[Any] | None) -> Any:
245
270
  try:
246
271
  return nx.shortest_path(self.graph, source=source, target=target, weight=weight)
@@ -255,14 +280,14 @@ class Graph(ABC, BaseModel):
255
280
  Finds the critical path in the graph.
256
281
  Returns:
257
282
  A tuple containing:
258
- - The critical path (a list of task names).
283
+ - The critical path (a list of edge identifiers).
259
284
  - The duration of the critical path.
260
285
  - A dictionary of all paths and their durations.
261
286
  """
262
287
 
263
288
  all_paths = {}
264
- for start_node in (v for k, v in self.nodes.items() if v.in_degrees == 0): # Start from nodes with 0 in-degree
265
- for end_node in (v for k, v in self.nodes.items() if v.out_degrees == 0): # End at nodes with 0 out-degree
289
+ for start_node in self.find_start_nodes():
290
+ for end_node in self.find_end_nodes(): # End at nodes with 0 out-degree
266
291
  for edge in nx.all_simple_paths(self.graph, source=start_node.identifier, target=end_node.identifier):
267
292
  edge_weight = sum(self.edges.get(item).weight if self.edges.get(item) else 0 for item in edge)
268
293
  all_paths[tuple(edge)] = edge_weight
@@ -314,7 +339,26 @@ class TaskGraph(Graph):
314
339
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
315
340
  should_reform: bool = Field(default=False)
316
341
  status: Dict[str, TaskStatus] = Field(default_factory=dict, description="store identifier (str) and TaskStatus of all task_nodes")
317
- outputs: Dict[str, TaskOutput] = Field(default_factory=dict, description="store identifire and TaskOutput")
342
+ outputs: Dict[str, TaskOutput] = Field(default_factory=dict, description="store node identifire and TaskOutput")
343
+ conclusion: Any = Field(default=None, description="store the final result of the entire task graph. critical path target/end node")
344
+
345
+
346
+ def _save(self, abs_file_path: str = None) -> None:
347
+ """
348
+ Save the graph image in the local directory.
349
+ """
350
+
351
+ try:
352
+ import os
353
+ project_root = os.path.abspath(os.getcwd())
354
+ abs_file_path = abs_file_path if abs_file_path else f"{project_root}/uploads"
355
+
356
+ os.makedirs(abs_file_path, exist_ok=True)
357
+ plt.savefig(f"{abs_file_path}/{str(self.id)}.png")
358
+
359
+ except Exception as e:
360
+ Logger().log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
361
+
318
362
 
319
363
  def add_task(self, task: Node | Task) -> Node:
320
364
  """Convert `task` to a Node object and add it to G"""
@@ -332,7 +376,7 @@ class TaskGraph(Graph):
332
376
  """
333
377
 
334
378
  if not edge_attributes:
335
- self._logger.log(level="error", message="Edge attributes are missing.", color="red")
379
+ Logger(verbose=True).log(level="error", message="Edge attributes are missing.", color="red")
336
380
 
337
381
  edge = Edge()
338
382
  for k in Edge.model_fields.keys():
@@ -349,14 +393,15 @@ class TaskGraph(Graph):
349
393
  if identifier in self.status:
350
394
  self.status[identifier] = status
351
395
  else:
352
- self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
396
+ Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
353
397
  pass
354
398
 
399
+
355
400
  def get_task_status(self, identifier):
356
401
  if identifier in self.status:
357
402
  return self.status[identifier]
358
403
  else:
359
- self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
404
+ Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
360
405
  return None
361
406
 
362
407
 
@@ -415,23 +460,6 @@ class TaskGraph(Graph):
415
460
  plt.show()
416
461
 
417
462
 
418
- def _save(self, abs_file_path: str = None) -> None:
419
- """
420
- Save the graph image in the local directory.
421
- """
422
-
423
- try:
424
- import os
425
- project_root = os.path.abspath(os.getcwd())
426
- abs_file_path = abs_file_path if abs_file_path else f"{project_root}/uploads"
427
-
428
- os.makedirs(abs_file_path, exist_ok=True)
429
- plt.savefig(f"{abs_file_path}/{str(self.id)}.png")
430
-
431
- except Exception as e:
432
- self._logger.log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
433
-
434
-
435
463
  def activate(self, target_node_identifier: Optional[str] = None) -> Tuple[TaskOutput | None, Dict[str, TaskOutput]]:
436
464
  """
437
465
  Starts to execute all nodes in the graph or a specific node if the target is given, following the given conditons of the edge obeject.
@@ -439,7 +467,7 @@ class TaskGraph(Graph):
439
467
  """
440
468
  if target_node_identifier:
441
469
  if not [k for k in self.nodes.keys() if k == target_node_identifier]:
442
- self._logger.log(level="error", message=f"The node {str(target_node_identifier)} is not in the graph.", color="red")
470
+ Logger().log(level="error", message=f"The node {str(target_node_identifier)} is not in the graph.", color="red")
443
471
  return None
444
472
 
445
473
  # find a shortest path to each in-degree node of the node and see if dependency met.
@@ -460,18 +488,62 @@ class TaskGraph(Graph):
460
488
  return res, self.outputs
461
489
 
462
490
  else:
463
- if not self.edges or self.nodes:
464
- self._logger.log(level="error", message="Needs at least 2 nodes and 1 edge to activate the graph. We'll return None.", color="red")
491
+ if not self.edges or not self.nodes:
492
+ Logger().log(level="error", message="TaskGraph needs at least 2 nodes and 1 edge to activate. We'll return None.", color="red")
465
493
  return None
466
494
 
467
- for edge in self.edges:
468
- res = edge.activate()
469
- if not res:
470
- break
495
+ start_nodes = self.find_start_nodes()
496
+ end_nodes = self.find_end_nodes()
497
+ critical_end_node = self.find_critical_end_node()
498
+ critical_path, _, _ = self.find_critical_path()
499
+ res = None
471
500
 
472
- node_identifier = edge.target.identifier
473
- self.outputs.update({ node_identifier: res })
474
- self.status.update({ node_identifier: edge.target.status })
501
+ # When all nodes are completed, return the output of the critical end node or end node.
502
+ if end_nodes and len([node for node in end_nodes if node.status == TaskStatus.COMPLETED]) == len(end_nodes):
503
+ if critical_end_node:
504
+ return critical_end_node.task.output, self.outputs
475
505
 
476
- last_task_output = [v for v in self.outputs.values()][len([v for v in self.outputs.values()]) - 1] if [v for v in self.outputs.values()] else None
477
- return last_task_output, self.outputs
506
+ else:
507
+ return [v.task.output.raw for k, v in end_nodes.items()][0], self.outputs
508
+
509
+ # Else, execute nodes connected with the critical_path
510
+ elif critical_path:
511
+ for item in critical_path:
512
+ edge = [v for k, v in self.edges.items() if item in k]
513
+
514
+ if edge:
515
+ edge = edge[0]
516
+
517
+ if edge.target.status == TaskStatus.COMPLETED:
518
+ res = edge.target.output
519
+
520
+ else:
521
+ res = edge.activate()
522
+ node_identifier = edge.target.identifier
523
+ self.outputs.update({ node_identifier: res })
524
+ self.status.update({ node_identifier: edge.target.status })
525
+
526
+ if not res and start_nodes:
527
+ for node in start_nodes:
528
+ res = node.handle_task_execution()
529
+ self.outputs.update({ node.identifier: res })
530
+ self.status.update({ node.identifier: node.status })
531
+
532
+ # if no critical paths in the graph, simply start from the start nodes.
533
+ elif start_nodes:
534
+ for node in start_nodes:
535
+ res = node.handle_task_execution()
536
+ self.outputs.update({ node.identifier: res })
537
+ self.status.update({ node.identifier: node.status })
538
+
539
+
540
+ # if none of above is applicable, try to activate all the edges.
541
+ else:
542
+ for k, edge in self.edges.items():
543
+ res = edge.activate()
544
+ node_identifier = edge.target.identifier
545
+ self.outputs.update({ node_identifier: res })
546
+ self.status.update({ node_identifier: edge.target.status })
547
+
548
+ # last_task_output = [v for v in self.outputs.values()][len([v for v in self.outputs.values()]) - 1] if [v for v in self.outputs.values()] else None
549
+ return res, self.outputs