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.
- versionhq/__init__.py +9 -9
- versionhq/_utils/logger.py +1 -1
- versionhq/agent/inhouse_agents.py +7 -0
- versionhq/agent/model.py +2 -2
- versionhq/{team → agent_network}/model.py +113 -115
- versionhq/clients/workflow/model.py +10 -10
- versionhq/knowledge/source.py +2 -2
- versionhq/knowledge/source_docling.py +1 -1
- versionhq/memory/model.py +1 -1
- versionhq/task/formation.py +11 -11
- versionhq/task/model.py +15 -16
- versionhq/{network → task_graph}/model.py +154 -82
- {versionhq-1.2.0.4.dist-info → versionhq-1.2.1.1.dist-info}/METADATA +54 -32
- {versionhq-1.2.0.4.dist-info → versionhq-1.2.1.1.dist-info}/RECORD +19 -20
- versionhq/team/team_planner.py +0 -92
- /versionhq/{network → agent_network}/__init__.py +0 -0
- /versionhq/{team → task_graph}/__init__.py +0 -0
- {versionhq-1.2.0.4.dist-info → versionhq-1.2.1.1.dist-info}/LICENSE +0 -0
- {versionhq-1.2.0.4.dist-info → versionhq-1.2.1.1.dist-info}/WHEEL +0 -0
- {versionhq-1.2.0.4.dist-info → versionhq-1.2.1.1.dist-info}/top_level.txt +0 -0
versionhq/task/formation.py
CHANGED
@@ -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.
|
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
|
-
) ->
|
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
|
-
|
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
|
-
|
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
|
-
|
113
|
-
return
|
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
|
-
|
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
|
-
|
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
|
-
|
143
|
-
return
|
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.
|
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, "
|
634
|
-
if agent.
|
635
|
-
idling_manager_agents = [manager.agent for manager in agent.
|
636
|
-
agent_to_delegate = idling_manager_agents[0] if idling_manager_agents else agent.
|
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.
|
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,
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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(
|
66
|
-
out_degree_nodes: List[Any] = Field(
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
265
|
-
for end_node in
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
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
|
-
|
473
|
-
|
474
|
-
|
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
|
-
|
477
|
-
|
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
|