versionhq 1.1.13.0__py3-none-any.whl → 1.2.0.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 +18 -3
- versionhq/agent/model.py +91 -39
- versionhq/llm/llm_vars.py +0 -1
- versionhq/llm/model.py +1 -1
- versionhq/network/__init__.py +0 -0
- versionhq/network/model.py +500 -0
- versionhq/task/evaluate.py +1 -1
- versionhq/task/formation.py +1 -1
- versionhq/task/model.py +120 -86
- versionhq/task/structured_response.py +16 -14
- versionhq/team/model.py +11 -10
- versionhq/team/team_planner.py +2 -4
- {versionhq-1.1.13.0.dist-info → versionhq-1.2.0.1.dist-info}/METADATA +149 -35
- {versionhq-1.1.13.0.dist-info → versionhq-1.2.0.1.dist-info}/RECORD +17 -15
- {versionhq-1.1.13.0.dist-info → versionhq-1.2.0.1.dist-info}/LICENSE +0 -0
- {versionhq-1.1.13.0.dist-info → versionhq-1.2.0.1.dist-info}/WHEEL +0 -0
- {versionhq-1.1.13.0.dist-info → versionhq-1.2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,500 @@
|
|
1
|
+
import enum
|
2
|
+
import uuid
|
3
|
+
from abc import ABC
|
4
|
+
from typing import List, Any, Optional, Callable, Dict, Type, Tuple
|
5
|
+
|
6
|
+
from pydantic import BaseModel, InstanceOf, Field, UUID4, PrivateAttr, field_validator, model_validator
|
7
|
+
from pydantic_core import PydanticCustomError
|
8
|
+
|
9
|
+
from versionhq.task.model import Task, TaskOutput
|
10
|
+
from versionhq.agent.model import Agent
|
11
|
+
from versionhq._utils.logger import Logger
|
12
|
+
|
13
|
+
try:
|
14
|
+
import networkx as ntx
|
15
|
+
except ImportError:
|
16
|
+
try:
|
17
|
+
import os
|
18
|
+
os.system("uv add networkx --optional networkx")
|
19
|
+
import networkx as ntx
|
20
|
+
except:
|
21
|
+
raise ImportError("networkx is not installed. Please install it with: uv add networkx --optional networkx")
|
22
|
+
|
23
|
+
|
24
|
+
try:
|
25
|
+
import matplotlib.pyplot as plt
|
26
|
+
except ImportError:
|
27
|
+
try:
|
28
|
+
import os
|
29
|
+
os.system("uv add matplotlib --optional matplotlib")
|
30
|
+
import matplotlib.pyplot as plt
|
31
|
+
except:
|
32
|
+
raise ImportError("matplotlib is not installed. Please install it with: uv add matplotlib --optional matplotlib")
|
33
|
+
|
34
|
+
import networkx as nx
|
35
|
+
import matplotlib.pyplot as plt
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
class TaskStatus(enum.Enum):
|
40
|
+
"""
|
41
|
+
Enum to track the task execution status
|
42
|
+
"""
|
43
|
+
NOT_STARTED = 1
|
44
|
+
IN_PROGRESS = 2
|
45
|
+
WAITING = 3 # waiting for its dependant tasks to complete. resumption set as AUTO.
|
46
|
+
COMPLETED = 4
|
47
|
+
DELAYED = 5 # task in progress - but taking longer than expected duration
|
48
|
+
ON_HOLD = 6 # intentionally paused due to external factors and decisions. resumption set as DECISION.
|
49
|
+
ERROR = 7 # tried task execute but returned error. resupmtion follows edge weights and agent settings
|
50
|
+
|
51
|
+
|
52
|
+
class DependencyType(enum.Enum):
|
53
|
+
"""
|
54
|
+
Concise enumeration of the edge type.
|
55
|
+
"""
|
56
|
+
FINISH_TO_START = "FS" # Task B starts after Task A finishes
|
57
|
+
START_TO_START = "SS" # Task B starts when Task A starts
|
58
|
+
FINISH_TO_FINISH = "FF" # Task B finishes when Task A finishes
|
59
|
+
START_TO_FINISH = "SF" # Task B finishes when Task A starts
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
class TriggerEvent(enum.Enum):
|
64
|
+
"""
|
65
|
+
Concise enumeration of key trigger events for task execution.
|
66
|
+
"""
|
67
|
+
IMMEDIATE = 0 # execute immediately
|
68
|
+
DEPENDENCIES_MET = 1 # All/required dependencies are satisfied
|
69
|
+
RESOURCES_AVAILABLE = 2 # Necessary resources are available
|
70
|
+
SCHEDULED_TIME = 3 # Scheduled start time or time window reached
|
71
|
+
EXTERNAL_EVENT = 4 # Triggered by an external event/message
|
72
|
+
DATA_AVAILABLE = 5 # Required data is available both internal/external
|
73
|
+
APPROVAL_RECEIVED = 6 # Necessary approvals have been granted
|
74
|
+
STATUS_CHANGED = 7 # Relevant task/system status has changed
|
75
|
+
RULE_MET = 8 # A predefined rule or condition has been met
|
76
|
+
MANUAL_TRIGGER = 9 # Manually initiated by a user
|
77
|
+
ERROR_HANDLED = 10 # A previous error/exception has been handled
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
class Node(BaseModel):
|
82
|
+
"""
|
83
|
+
A class to store a node object.
|
84
|
+
"""
|
85
|
+
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
86
|
+
task: InstanceOf[Task] = Field(default=None)
|
87
|
+
# trigger_event: TriggerEvent = Field(default=TriggerEvent.IMMEDIATE, description="store trigger event for starting the task execution")
|
88
|
+
in_degree_nodes: List[Any] = Field(default=None, description="list of Node objects")
|
89
|
+
out_degree_nodes: List[Any] = Field(default=None, description="list of Node objects")
|
90
|
+
assigned_to: InstanceOf[Agent] = Field(default=None)
|
91
|
+
status: TaskStatus = Field(default=TaskStatus.NOT_STARTED)
|
92
|
+
|
93
|
+
|
94
|
+
@field_validator("id", mode="before")
|
95
|
+
@classmethod
|
96
|
+
def _deny_id(cls, v: Optional[UUID4]) -> None:
|
97
|
+
if v:
|
98
|
+
raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
|
99
|
+
|
100
|
+
def is_independent(self) -> bool:
|
101
|
+
return not self.in_degree_nodes and not self.out_degree_nodes
|
102
|
+
|
103
|
+
@property
|
104
|
+
def in_degrees(self) -> int:
|
105
|
+
return len(self.in_degree_nodes) if self.in_degree_nodes else 0
|
106
|
+
|
107
|
+
@property
|
108
|
+
def out_degrees(self) -> int:
|
109
|
+
return len(self.out_degree_nodes) if self.out_degree_nodes else 0
|
110
|
+
|
111
|
+
@property
|
112
|
+
def degrees(self) -> int:
|
113
|
+
return self.in_degrees + self.out_degrees
|
114
|
+
|
115
|
+
@property
|
116
|
+
def identifier(self) -> str:
|
117
|
+
"""Unique identifier for the node"""
|
118
|
+
return f"{str(self.id)}"
|
119
|
+
|
120
|
+
def handle_task_execution(self, agent: Agent = None, context: str = None) -> TaskOutput | None:
|
121
|
+
"""
|
122
|
+
Start task execution and update status accordingly.
|
123
|
+
"""
|
124
|
+
|
125
|
+
self.status = TaskStatus.IN_PROGRESS
|
126
|
+
|
127
|
+
if not self.task:
|
128
|
+
Logger(verbose=True).log(level="error", message="Missing a task to execute. We'll return None.", color="red")
|
129
|
+
self.status = TaskStatus.ERROR
|
130
|
+
return None
|
131
|
+
|
132
|
+
res = self.task.execute(agent=agent, context=context)
|
133
|
+
self.status = TaskStatus.COMPLETED if res else TaskStatus.ERROR
|
134
|
+
return res
|
135
|
+
|
136
|
+
def __str__(self):
|
137
|
+
return self.identifier
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
class Edge(BaseModel):
|
142
|
+
"""
|
143
|
+
A class to store an edge object that connects source and target nodes.
|
144
|
+
"""
|
145
|
+
source: Node = Field(default=None)
|
146
|
+
target: Node = Field(default=None)
|
147
|
+
|
148
|
+
description: Optional[str] = Field(default=None)
|
149
|
+
weight: Optional[float | int] = Field(default=1, description="est. duration for the task execution or respective weight of the target node (1 low - 10 high priority)")
|
150
|
+
|
151
|
+
dependency_type: DependencyType = Field(default=DependencyType.FINISH_TO_START)
|
152
|
+
required: bool = Field(default=True, description="whether to consider the source's status")
|
153
|
+
condition: Optional[Callable] = Field(default=None, description="conditional function to start executing the dependency")
|
154
|
+
condition_kwargs: Optional[Dict[str, Any]] = Field(default_factory=dict)
|
155
|
+
|
156
|
+
lag: Optional[float | int] = Field(default=None, description="lag time (sec) from the dependency met to the task execution")
|
157
|
+
data_transfer: bool = Field(default=True, description="whether the data transfer is required. by default transfer plane text output from in-degree nodes as context")
|
158
|
+
|
159
|
+
|
160
|
+
def dependency_met(self) -> bool:
|
161
|
+
"""
|
162
|
+
Defines if the dependency is ready to execute:
|
163
|
+
|
164
|
+
required - condition - Dependency Met? - Dependent Task Can Start?
|
165
|
+
True Not Given Predecessor task finished Yes
|
166
|
+
True Given Predecessor task finished and condition True Yes
|
167
|
+
False Not Given Always (regardless of predecessor status) Yes
|
168
|
+
False Given Condition True (predecessor status irrelevant) Yes
|
169
|
+
"""
|
170
|
+
|
171
|
+
if not self.required:
|
172
|
+
return self.condition(**self.condition_kwargs) if self.condition else True
|
173
|
+
|
174
|
+
match self.dependency_type:
|
175
|
+
case DependencyType.FINISH_TO_START:
|
176
|
+
"""target starts after source finishes"""
|
177
|
+
if self.source.status == TaskStatus.COMPLETED:
|
178
|
+
return self.condition(**self.conditon_kwargs) if self.condtion else True
|
179
|
+
else:
|
180
|
+
return False
|
181
|
+
|
182
|
+
case DependencyType.START_TO_START:
|
183
|
+
"""target starts when source starts"""
|
184
|
+
if self.source.status != TaskStatus.NOT_STARTED:
|
185
|
+
return self.condition(**self.conditon_kwargs) if self.condtion else True
|
186
|
+
else:
|
187
|
+
return False
|
188
|
+
|
189
|
+
case DependencyType.FINISH_TO_FINISH:
|
190
|
+
"""target finish when source start"""
|
191
|
+
if self.source.status != TaskStatus.COMPLETED:
|
192
|
+
return self.condition(**self.conditon_kwargs) if self.condtion else True
|
193
|
+
else:
|
194
|
+
return False
|
195
|
+
|
196
|
+
case DependencyType.START_TO_FINISH:
|
197
|
+
"""target finishes when source start"""
|
198
|
+
if self.source.status == TaskStatus.IN_PROGRESS:
|
199
|
+
return self.condition(**self.conditon_kwargs) if self.condtion else True
|
200
|
+
else:
|
201
|
+
return False
|
202
|
+
|
203
|
+
|
204
|
+
def activate(self) -> TaskOutput | None:
|
205
|
+
"""
|
206
|
+
Activates the edge to initiate task execution of the target node.
|
207
|
+
"""
|
208
|
+
|
209
|
+
if not self.dependency_met():
|
210
|
+
Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll return None.", color="yellow")
|
211
|
+
return None
|
212
|
+
|
213
|
+
if not self.source or not self.target:
|
214
|
+
Logger(verbose=True).log(level="warning", message="Cannot find source or target nodes. We'll return None.", color="yellow")
|
215
|
+
return None
|
216
|
+
|
217
|
+
if self.lag:
|
218
|
+
import time
|
219
|
+
time.sleep(self.lag)
|
220
|
+
|
221
|
+
context = self.source.task.task_output.raw if self.data_transfer else None
|
222
|
+
res = self.target.handle_task_execution(context=context)
|
223
|
+
return res
|
224
|
+
|
225
|
+
|
226
|
+
class Graph(ABC, BaseModel):
|
227
|
+
"""
|
228
|
+
An abstract class to store G using NetworkX library.
|
229
|
+
"""
|
230
|
+
|
231
|
+
_logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
|
232
|
+
directed: bool = Field(default=False, description="Whether the graph is directed")
|
233
|
+
graph: Type[nx.Graph] = Field(default=None)
|
234
|
+
nodes: Dict[str, InstanceOf[Node]] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
|
235
|
+
edges: Dict[str, InstanceOf[Edge]] = Field(default_factory=dict)
|
236
|
+
|
237
|
+
def __init__(self, directed: bool = False, **kwargs):
|
238
|
+
super().__init__(directed=directed, **kwargs)
|
239
|
+
self.graph = nx.DiGraph() if self.directed else nx.Graph()
|
240
|
+
|
241
|
+
def _return_node_object(self, node_identifier) -> Node | None:
|
242
|
+
return [v for k, v in self.nodes.items() if k == node_identifier][0] if [v for k, v in self.nodes.items() if k == node_identifier] else None
|
243
|
+
|
244
|
+
def add_node(self, node: Node) -> None:
|
245
|
+
self.graph.add_node(node.identifier, **node.model_dump())
|
246
|
+
self.nodes[node.identifier] = node
|
247
|
+
|
248
|
+
def add_edge(self, source: str, target: str, edge: Edge) -> None:
|
249
|
+
self.graph.add_edge(source, target, **edge.model_dump())
|
250
|
+
edge.source = self._return_node_object(source)
|
251
|
+
edge.target = self._return_node_object(target)
|
252
|
+
self.edges[(source, target)] = edge
|
253
|
+
|
254
|
+
def add_weighted_edges_from(self, edges):
|
255
|
+
self.graph.add_weighted_edges_from(edges)
|
256
|
+
|
257
|
+
def get_neighbors(self, node: Node) -> List[Node]:
|
258
|
+
return list(self.graph.neighbors(node))
|
259
|
+
|
260
|
+
def get_in_degree(self, node: Node) -> int:
|
261
|
+
return self.graph.in_degree(node)
|
262
|
+
|
263
|
+
def get_out_degree(self, node: Node) -> int:
|
264
|
+
return self.graph.out_degree(node)
|
265
|
+
|
266
|
+
def find_path(self, source: Optional[str] | None, target: str, weight: Optional[Any] | None) -> Any:
|
267
|
+
try:
|
268
|
+
return nx.shortest_path(self.graph, source=source, target=target, weight=weight)
|
269
|
+
except nx.NetworkXNoPath:
|
270
|
+
return None
|
271
|
+
|
272
|
+
def find_all_paths(self, source: str, target: str) -> List[Any]:
|
273
|
+
return list(nx.all_simple_paths(self.graph, source=source, target=target))
|
274
|
+
|
275
|
+
def find_critical_path(self) -> tuple[List[Any], int, Dict[str, int]]:
|
276
|
+
"""
|
277
|
+
Finds the critical path in the graph.
|
278
|
+
Returns:
|
279
|
+
A tuple containing:
|
280
|
+
- The critical path (a list of task names).
|
281
|
+
- The duration of the critical path.
|
282
|
+
- A dictionary of all paths and their durations.
|
283
|
+
"""
|
284
|
+
|
285
|
+
all_paths = {}
|
286
|
+
for start_node in (v for k, v in self.nodes.items() if v.in_degrees == 0): # Start from nodes with 0 in-degree
|
287
|
+
for end_node in (v for k, v in self.nodes.items() if v.out_degrees == 0): # End at nodes with 0 out-degree
|
288
|
+
for edge in nx.all_simple_paths(self.graph, source=start_node.identifier, target=end_node.identifier):
|
289
|
+
edge_weight = sum(self.edges.get(item).weight if self.edges.get(item) else 0 for item in edge)
|
290
|
+
all_paths[tuple(edge)] = edge_weight
|
291
|
+
|
292
|
+
if not all_paths:
|
293
|
+
return [], 0, all_paths
|
294
|
+
|
295
|
+
critical_path = max(all_paths, key=all_paths.get)
|
296
|
+
critical_duration = all_paths[critical_path]
|
297
|
+
|
298
|
+
return list(critical_path), critical_duration, all_paths
|
299
|
+
|
300
|
+
|
301
|
+
def is_circled(self, node: Node) -> bool:
|
302
|
+
"""Check if there's a path from the node to itself and return bool."""
|
303
|
+
try:
|
304
|
+
path = nx.shortest_path(self.graph, source=node, target=node)
|
305
|
+
return True if path else False
|
306
|
+
except nx.NetworkXNoPath:
|
307
|
+
return False
|
308
|
+
|
309
|
+
|
310
|
+
def visualize(self, title: str = "Task Graph", pos: Any = None, **graph_config):
|
311
|
+
pos = pos if pos else nx.spring_layout(self.graph, seed=42)
|
312
|
+
nx.draw(
|
313
|
+
self.graph,
|
314
|
+
pos,
|
315
|
+
with_labels=True, node_size=700, node_color="skyblue", font_size=10, font_color="black", arrowstyle='-|>', arrowsize=20, arrows=True,
|
316
|
+
**graph_config
|
317
|
+
)
|
318
|
+
edge_labels = {}
|
319
|
+
for u, v, data in self.graph.edges(data=True):
|
320
|
+
edge = self.edges.get((u,v))
|
321
|
+
if edge:
|
322
|
+
label_parts = []
|
323
|
+
if edge.type:
|
324
|
+
label_parts.append(f"Type: {edge.type}")
|
325
|
+
if edge.duration is not None:
|
326
|
+
label_parts.append(f"Duration: {edge.duration}")
|
327
|
+
if edge.lag is not None:
|
328
|
+
label_parts.append(f"Lag: {edge.lag}")
|
329
|
+
edge_labels[(u, v)] = "\n".join(label_parts) # Combine labels with newlines
|
330
|
+
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
|
331
|
+
plt.title(title)
|
332
|
+
plt.show()
|
333
|
+
|
334
|
+
|
335
|
+
class TaskGraph(Graph):
|
336
|
+
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
337
|
+
should_reform: bool = Field(default=False)
|
338
|
+
status: Dict[str, TaskStatus] = Field(default_factory=dict, description="store identifier (str) and TaskStatus of all task_nodes")
|
339
|
+
outputs: Dict[str, TaskOutput] = Field(default_factory=dict, description="store identifire and TaskOutput")
|
340
|
+
|
341
|
+
def add_task(self, task: Node | Task) -> Node:
|
342
|
+
"""Convert `task` to a Node object and add it to G"""
|
343
|
+
task_node = task if isinstance(task, Node) else Node(task=task)
|
344
|
+
self.add_node(task_node)
|
345
|
+
self.status[task_node.identifier] = TaskStatus.NOT_STARTED
|
346
|
+
return task_node
|
347
|
+
|
348
|
+
|
349
|
+
def add_dependency(
|
350
|
+
self, source_task_node_identifier: str, target_task_node_identifier: str, **edge_attributes
|
351
|
+
) -> None:
|
352
|
+
"""
|
353
|
+
Add an edge that connect task 1 (source) and task 2 (target) using task_node.name as an identifier
|
354
|
+
"""
|
355
|
+
|
356
|
+
if not edge_attributes:
|
357
|
+
self._logger.log(level="error", message="Edge attributes are missing.", color="red")
|
358
|
+
|
359
|
+
edge = Edge()
|
360
|
+
for k in Edge.model_fields.keys():
|
361
|
+
v = edge_attributes.get(k, None)
|
362
|
+
if v:
|
363
|
+
setattr(edge, k, v)
|
364
|
+
else:
|
365
|
+
pass
|
366
|
+
|
367
|
+
self.add_edge(source_task_node_identifier, target_task_node_identifier, edge)
|
368
|
+
|
369
|
+
|
370
|
+
def set_task_status(self, identifier: str, status: TaskStatus) -> None:
|
371
|
+
if identifier in self.status:
|
372
|
+
self.status[identifier] = status
|
373
|
+
else:
|
374
|
+
self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
|
375
|
+
pass
|
376
|
+
|
377
|
+
def get_task_status(self, identifier):
|
378
|
+
if identifier in self.status:
|
379
|
+
return self.status[identifier]
|
380
|
+
else:
|
381
|
+
self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
|
382
|
+
return None
|
383
|
+
|
384
|
+
|
385
|
+
def visualize(self, layout: str = None):
|
386
|
+
try:
|
387
|
+
pos = nx.drawing.nx_agraph.graphviz_layout(self.graph, prog='dot') # 'dot', 'neato', 'fdp', 'sfdp'
|
388
|
+
except ImportError:
|
389
|
+
pos = nx.spring_layout(self.graph, seed=42) # REFINEME - layout
|
390
|
+
|
391
|
+
node_colors = list()
|
392
|
+
for k, v in self.graph.nodes.items():
|
393
|
+
status = self.get_task_status(identifier=k)
|
394
|
+
if status == TaskStatus.NOT_STARTED:
|
395
|
+
node_colors.append("skyblue")
|
396
|
+
elif status == TaskStatus.IN_PROGRESS:
|
397
|
+
node_colors.append("lightgreen")
|
398
|
+
elif status == TaskStatus.BLOCKED:
|
399
|
+
node_colors.append("lightcoral")
|
400
|
+
elif status == TaskStatus.COMPLETED:
|
401
|
+
node_colors.append("black")
|
402
|
+
elif status == TaskStatus.DELAYED:
|
403
|
+
node_colors.append("orange")
|
404
|
+
elif status == TaskStatus.ON_HOLD:
|
405
|
+
node_colors.append("yellow")
|
406
|
+
else:
|
407
|
+
node_colors.append("grey")
|
408
|
+
|
409
|
+
critical_paths, duration, paths = self.find_critical_path()
|
410
|
+
edge_colors = ['red' if (u, v) in zip(critical_paths, critical_paths[1:]) else 'black' for u, v in self.graph.edges()]
|
411
|
+
edge_widths = []
|
412
|
+
|
413
|
+
for k, v in self.edges.items():
|
414
|
+
# edge_weights = nx.get_edge_attributes(self.graph, 'weight')
|
415
|
+
# edge_colors.append(plt.cm.viridis(v.weight / max(edge_weights.values())))
|
416
|
+
edge_widths.append(v.weight * 0.5)
|
417
|
+
|
418
|
+
nx.draw(
|
419
|
+
self.graph, pos,
|
420
|
+
with_labels=True,
|
421
|
+
node_size=700,
|
422
|
+
node_color=node_colors,
|
423
|
+
font_size=10,
|
424
|
+
font_color="black",
|
425
|
+
edge_color=edge_colors,
|
426
|
+
width=edge_widths,
|
427
|
+
arrows=True,
|
428
|
+
arrowsize=20,
|
429
|
+
arrowstyle='-|>'
|
430
|
+
)
|
431
|
+
|
432
|
+
edge_labels = nx.get_edge_attributes(G=self.graph, name="edges")
|
433
|
+
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
|
434
|
+
|
435
|
+
plt.title("Project Network Diagram")
|
436
|
+
self._save()
|
437
|
+
plt.show()
|
438
|
+
|
439
|
+
|
440
|
+
def _save(self, abs_file_path: str = None) -> None:
|
441
|
+
"""
|
442
|
+
Save the graph image in the local directory.
|
443
|
+
"""
|
444
|
+
|
445
|
+
try:
|
446
|
+
import os
|
447
|
+
project_root = os.path.abspath(os.getcwd())
|
448
|
+
abs_file_path = abs_file_path if abs_file_path else f"{project_root}/uploads"
|
449
|
+
|
450
|
+
os.makedirs(abs_file_path, exist_ok=True)
|
451
|
+
|
452
|
+
plt.savefig(f"{abs_file_path}/{str(self.id)}.png")
|
453
|
+
|
454
|
+
except Exception as e:
|
455
|
+
self._logger.log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
|
456
|
+
|
457
|
+
|
458
|
+
def activate(self, target_node_identifier: Optional[str] = None) -> Tuple[TaskOutput | None, Dict[str, TaskOutput]]:
|
459
|
+
"""
|
460
|
+
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.
|
461
|
+
Then returns tuple of the last task output and all task outputs (self.outputs)
|
462
|
+
"""
|
463
|
+
if target_node_identifier:
|
464
|
+
if not [k for k in self.nodes.keys() if k == target_node_identifier]:
|
465
|
+
self._logger.log(level="error", message=f"The node {str(target_node_identifier)} is not in the graph.", color="red")
|
466
|
+
return None
|
467
|
+
|
468
|
+
# find a shortest path to each in-degree node of the node and see if dependency met.
|
469
|
+
node = self._return_node_object(target_node_identifier)
|
470
|
+
sources = node.in_degrees
|
471
|
+
edge_status = []
|
472
|
+
res = None
|
473
|
+
|
474
|
+
for item in sources:
|
475
|
+
edge = self.find_path(source=item, target=target_node_identifier)
|
476
|
+
edge_status.append(dict(edge=edge if edge else None, dep_met=edge.dependency_met() if edge else False))
|
477
|
+
|
478
|
+
if len([item for item in edge_status if item["dep_met"] == True]) == len(sources):
|
479
|
+
res = node.handle_task_execution()
|
480
|
+
self.outputs.update({ target_node_identifier: res })
|
481
|
+
self.status.update({ target_node_identifier: edge.target.status })
|
482
|
+
|
483
|
+
return res, self.outputs
|
484
|
+
|
485
|
+
else:
|
486
|
+
if not self.edges or self.nodes:
|
487
|
+
self._logger.log(level="error", message="Needs at least 2 nodes and 1 edge to activate the graph. We'll return None.", color="red")
|
488
|
+
return None
|
489
|
+
|
490
|
+
for edge in self.edges:
|
491
|
+
res = edge.activate()
|
492
|
+
if not res:
|
493
|
+
break
|
494
|
+
|
495
|
+
node_identifier = edge.target.identifier
|
496
|
+
self.outputs.update({ node_identifier: res })
|
497
|
+
self.status.update({ node_identifier: edge.target.status })
|
498
|
+
|
499
|
+
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
|
500
|
+
return last_task_output, self.outputs
|
versionhq/task/evaluate.py
CHANGED
@@ -72,7 +72,7 @@ class EvaluationItem(BaseModel):
|
|
72
72
|
|
73
73
|
class Evaluation(BaseModel):
|
74
74
|
items: List[EvaluationItem] = []
|
75
|
-
latency:
|
75
|
+
latency: float = Field(default=None, description="job execution latency in seconds")
|
76
76
|
tokens: int = Field(default=None, description="tokens consumed")
|
77
77
|
eval_by: Any = Field(default=None, description="stores agent object that evaluates the outcome")
|
78
78
|
|
versionhq/task/formation.py
CHANGED
@@ -79,7 +79,7 @@ def form_agent_network(
|
|
79
79
|
if agents:
|
80
80
|
vhq_task.description += "Consider adding following agents in the formation: " + ", ".join([agent.role for agent in agents if isinstance(agent, Agent)])
|
81
81
|
|
82
|
-
res = vhq_task.
|
82
|
+
res = vhq_task.execute(agent=vhq_formation_planner, context=context)
|
83
83
|
_formation = Formation.SUPERVISING
|
84
84
|
|
85
85
|
if res.pydantic:
|