versionhq 1.2.1.5__py3-none-any.whl → 1.2.1.6__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 CHANGED
@@ -28,9 +28,10 @@ from versionhq.memory.contextual_memory import ContextualMemory
28
28
  from versionhq.memory.model import ShortTermMemory,LongTermMemory, UserMemory, MemoryItem
29
29
 
30
30
  from versionhq.task.formation import form_agent_network
31
+ from versionhq.task_graph.draft import workflow
31
32
 
32
33
 
33
- __version__ = "1.2.1.5"
34
+ __version__ = "1.2.1.6"
34
35
  __all__ = [
35
36
  "Agent",
36
37
 
@@ -88,5 +89,6 @@ __all__ = [
88
89
  "UserMemory",
89
90
  "MemoryItem",
90
91
 
91
- "form_agent_network"
92
+ "form_agent_network",
93
+ "workflow",
92
94
  ]
@@ -39,7 +39,7 @@ def form_agent_network(
39
39
  pass
40
40
 
41
41
  case str():
42
- matched = [item for item in Formation._member_names_ if item == formation.upper()]
42
+ matched = [item for item in Formation.s_ if item == formation.upper()]
43
43
  if matched:
44
44
  formation = getattr(Formation, matched[0])
45
45
  else:
versionhq/task/model.py CHANGED
@@ -4,6 +4,7 @@ import datetime
4
4
  import uuid
5
5
  import inspect
6
6
  import enum
7
+ from textwrap import dedent
7
8
  from concurrent.futures import Future
8
9
  from hashlib import md5
9
10
  from typing import Any, Dict, List, Set, Optional, Callable, Type
@@ -288,7 +289,7 @@ class Task(BaseModel):
288
289
  should_evaluate: bool = Field(default=False, description="True to run the evaluation flow")
289
290
  eval_criteria: Optional[List[str]] = Field(default_factory=list, description="criteria to evaluate the outcome. i.e., fit to the brand tone")
290
291
 
291
- # recording
292
+ # recording !# REFINEME - eval_callbacks
292
293
  processed_agents: Set[str] = Field(default_factory=set, description="store roles of the agents that executed the task")
293
294
  tool_errors: int = 0
294
295
  delegations: int = 0
@@ -364,7 +365,7 @@ Ref. Output image: {output_formats_to_follow}
364
365
  else:
365
366
  output_prompt = "Return your response as a valid JSON serializable string, enclosed in double quotes. Do not use single quotes, trailing commas, or other non-standard JSON syntax."
366
367
 
367
- return output_prompt
368
+ return dedent(output_prompt)
368
369
 
369
370
 
370
371
  def _draft_context_prompt(self, context: Any) -> str:
@@ -374,7 +375,7 @@ Ref. Output image: {output_formats_to_follow}
374
375
 
375
376
  context_to_add = None
376
377
  if not context:
377
- Logger().log(level="error", color="red", message="Missing a context to add to the prompt. We'll return ''.")
378
+ # Logger().log(level="error", color="red", message="Missing a context to add to the prompt. We'll return ''.")
378
379
  return context_to_add
379
380
 
380
381
  match context:
@@ -403,7 +404,7 @@ Ref. Output image: {output_formats_to_follow}
403
404
  case _:
404
405
  pass
405
406
 
406
- return context_to_add
407
+ return dedent(context_to_add)
407
408
 
408
409
 
409
410
  def _prompt(self, model_provider: str = None, context: Optional[Any] = None) -> str:
@@ -593,7 +594,6 @@ Ref. Output image: {output_formats_to_follow}
593
594
  return self._execute_async(agent=agent, context=context)
594
595
 
595
596
 
596
-
597
597
  def _execute_sync(self, agent, context: Optional[Any] = None) -> TaskOutput:
598
598
  """Executes the task synchronously."""
599
599
  return self._execute_core(agent, context)
@@ -0,0 +1,28 @@
1
+ white = "#fafafa"
2
+ black = "#191b1b"
3
+ darkgrey = "#363636"
4
+ grey = "#414141"
5
+ primary = "#B9005B"
6
+ orange = "#ffb713"
7
+ lightgreen = "#b8fff5"
8
+ green = "#00d1b2"
9
+ darkgreen = "#169d87"
10
+ darkergreen = "#026b5d"
11
+
12
+
13
+ # $black: #191b1b;
14
+ # $true-white: #ffffff;
15
+ # $white: #fafafa;
16
+ # $grey: #414141;
17
+ # $light-grey: #f9f9f9;
18
+ # $border-color: #dedede;
19
+ # $primary: #B9005B;
20
+ # $primary-hovered: #9D034F;
21
+ # $gradient-main: linear-gradient(to right $black $primary);
22
+ # $linkedin-blue: #0077B5;
23
+ # $bulma-primary: #00d1b2;
24
+ # $bluma-primary-dark: #169d87;
25
+ # $bulma-grey-light: hsl(0 0%, 71%);
26
+ # $bulma-grey: hsl(0, 0%, 48%);
27
+ # $bulma-dark: hsl(0, 0%, 21%); //#363636
28
+ # $bulma-white-ter: #F5F5F5;
@@ -0,0 +1,101 @@
1
+ import sys
2
+ from typing import Type, Any
3
+ from pydantic import BaseModel
4
+ from pydantic._internal._model_construction import ModelMetaclass
5
+ from textwrap import dedent
6
+ if 'pydantic.main' not in sys.modules:
7
+ import pydantic.main
8
+
9
+ sys.modules['pydantic.main'].ModelMetaclass = ModelMetaclass
10
+
11
+ from versionhq.agent.model import Agent
12
+ from versionhq.task.model import ResponseField
13
+ from versionhq.task_graph.model import TaskGraph, Task, DependencyType, Node, TaskStatus
14
+ from versionhq._utils.logger import Logger
15
+
16
+
17
+ def workflow(final_output: Type[BaseModel], context: Any = None, human: bool = True) -> TaskGraph | None:
18
+ """
19
+ Generate a TaskGraph object to generate the givne final_output most resource-efficiently.
20
+ """
21
+
22
+ if not final_output or not isinstance(final_output, ModelMetaclass):
23
+ Logger().log(level="error", message="Missing an expected output in Pydantic model.", color="red")
24
+ return None
25
+
26
+ final_output_prompt = ", ".join([k for k in final_output.model_fields.keys()])
27
+
28
+ if not final_output_prompt:
29
+ Logger().log(level="error", message="Expected output is in invalid format.", color="red")
30
+ return None
31
+
32
+ context_prompt = f'We are designing a resource-efficient workflow using graph algorithm concepts to achieve the following goal: {final_output_prompt}.'
33
+
34
+ dep_type_prompt = ", ".join([k for k in DependencyType._member_map_.keys()])
35
+
36
+ graph_expert = Agent(
37
+ role="vhq-G Expert",
38
+ goal="design the most resource-efficient workflow graph to achieve the given goal",
39
+ knowledge_sources=[
40
+ "https://en.wikipedia.org/wiki/Graph_theory",
41
+ # "https://www.geeksforgeeks.org/graph-data-structure-and-algorithms/?ref=lbp",
42
+ "https://www.geeksforgeeks.org/graph-and-its-representations/",
43
+ ", ".join([k for k in DependencyType._member_map_.keys()]),
44
+ ],
45
+ llm="gemini-2.0",
46
+ use_memory=True,
47
+ maxit=1,
48
+ max_retry_limit=1,
49
+ )
50
+
51
+ task = Task(
52
+ description=f"Design a resource-efficient workflow to achieve the following goal: {final_output_prompt}. The workflow should consist of a list of tasks, each with the following information:\nname: A concise name of the task\ndescription: A concise description of the task.\nconnections: A list of target tasks that this task connects to.\ndependency_types: The type of dependency between this task and each of its connected task. Use the following dependency types: {dep_type_prompt}.\n\nPrioritize minimizing resource consumption (computation, memory, and data transfer) when defining tasks, connections, and dependencies. Consider how data is passed between tasks and aim to reduce unnecessary data duplication or transfer. Explain any design choices made to optimize resource usage.",
53
+ response_fields=[
54
+ ResponseField(title="tasks", data_type=list, items=dict, properties=[
55
+ ResponseField(title="name", data_type=str),
56
+ ResponseField(title="description", data_type=str),
57
+ ResponseField(title="connections", data_type=list, items=str),
58
+ ResponseField(title="dependency_types", data_type=list, items=str),
59
+ ]
60
+ )
61
+ ]
62
+ )
63
+ res = task.execute(agent=graph_expert, context=[context_prompt, context])
64
+
65
+ if not res:
66
+ return None
67
+
68
+ task_items = res.json_dict["tasks"]
69
+ tasks = [Task(name=item["name"], description=item["description"]) for item in task_items]
70
+ nodes = [Node(task=task) for task in tasks]
71
+ task_graph = TaskGraph(
72
+ nodes={node.identifier: node for node in nodes},
73
+ concl=final_output,
74
+ should_reform=True,
75
+ )
76
+
77
+ for res in task_items:
78
+ if res["connections"]:
79
+ dependency_types = [DependencyType[dt] if DependencyType[dt] else DependencyType.FINISH_TO_START for dt in res["dependency_types"]]
80
+
81
+ for i, target_task_name in enumerate(res["connections"]):
82
+ source = [v for k, v in task_graph.nodes.items() if v.task.name == res["name"]][0]
83
+ target = [v for k, v in task_graph.nodes.items() if v.task.name == target_task_name][0]
84
+ dependency_type = dependency_types[i]
85
+ task_graph.add_dependency(source_node_identifier=source.identifier, target_node_identifier=target.identifier, dependency_type=dependency_type)
86
+
87
+ ## test purpose
88
+ # task_graph.visualize()
89
+
90
+ # if human:
91
+ # print('Proceed? Y/n:')
92
+ # x = input()
93
+
94
+ # if x.lower() == "y":
95
+ # print("ok. generating agent network")
96
+
97
+ # else:
98
+ # request = input("request?")
99
+ # print('ok. regenerating the graph based on your input: ', request)
100
+
101
+ return task_graph
@@ -4,8 +4,9 @@ import networkx as nx
4
4
  import matplotlib.pyplot as plt
5
5
  from abc import ABC
6
6
  from typing import List, Any, Optional, Callable, Dict, Type, Tuple
7
+ from typing_extensions import Self
7
8
 
8
- from pydantic import BaseModel, InstanceOf, Field, UUID4, field_validator
9
+ from pydantic import BaseModel, InstanceOf, Field, UUID4, field_validator, model_validator
9
10
  from pydantic_core import PydanticCustomError
10
11
 
11
12
  from versionhq.task.model import Task, TaskOutput
@@ -36,32 +37,13 @@ class DependencyType(enum.Enum):
36
37
  START_TO_FINISH = "SF" # Task B finishes when Task A starts
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
55
-
56
-
57
-
58
40
  class Node(BaseModel):
59
41
  """
60
42
  A class to store a node object.
61
43
  """
44
+
62
45
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
63
46
  task: InstanceOf[Task] = Field(default=None)
64
- # trigger_event: TriggerEvent = Field(default=TriggerEvent.IMMEDIATE, description="store trigger event for starting the task execution")
65
47
  in_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
66
48
  out_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
67
49
  assigned_to: InstanceOf[Agent] = Field(default=None)
@@ -70,14 +52,14 @@ class Node(BaseModel):
70
52
 
71
53
  @field_validator("id", mode="before")
72
54
  @classmethod
73
- def _deny_id(cls, v: Optional[UUID4]) -> None:
55
+ def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
74
56
  if v:
75
- raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
57
+ raise PydanticCustomError("may_not_set_field", "This field is not to be set by client.", {})
58
+
76
59
 
77
60
  def is_independent(self) -> bool:
78
61
  return not self.in_degree_nodes and not self.out_degree_nodes
79
62
 
80
-
81
63
  def handle_task_execution(self, agent: Agent = None, context: str = None) -> TaskOutput | None:
82
64
  """
83
65
  Start task execution and update status accordingly.
@@ -109,11 +91,14 @@ class Node(BaseModel):
109
91
 
110
92
  @property
111
93
  def identifier(self) -> str:
112
- """Unique identifier for the node"""
94
+ """Unique identifier of the node"""
113
95
  return f"{str(self.id)}"
114
96
 
115
97
  def __str__(self):
116
- return self.identifier
98
+ if self.task:
99
+ return f"{self.identifier}: {self.task.name if self.task.name else self.task.description[0: 12]}"
100
+ else:
101
+ return self.identifier
117
102
 
118
103
 
119
104
  class Edge(BaseModel):
@@ -136,6 +121,17 @@ class Edge(BaseModel):
136
121
  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")
137
122
 
138
123
 
124
+ def _response_schema(self) -> Type[BaseModel]:
125
+ class EdgeResponseSchema(BaseModel):
126
+ wight: float
127
+ dependecy_type: str
128
+ required: bool
129
+ need_condition: bool
130
+ lag_in_sec: float
131
+
132
+ return EdgeResponseSchema
133
+
134
+
139
135
  def dependency_met(self) -> bool:
140
136
  """
141
137
  Defines if the dependency is ready to execute:
@@ -153,28 +149,28 @@ class Edge(BaseModel):
153
149
  match self.dependency_type:
154
150
  case DependencyType.FINISH_TO_START:
155
151
  """target starts after source finishes"""
156
- if self.source.status == TaskStatus.COMPLETED:
152
+ if not self.source or self.source.status == TaskStatus.COMPLETED:
157
153
  return self.condition(**self.conditon_kwargs) if self.condition else True
158
154
  else:
159
155
  return False
160
156
 
161
157
  case DependencyType.START_TO_START:
162
158
  """target starts when source starts"""
163
- if self.source.status != TaskStatus.NOT_STARTED:
159
+ if not self.source or self.source.status != TaskStatus.NOT_STARTED:
164
160
  return self.condition(**self.conditon_kwargs) if self.condition else True
165
161
  else:
166
162
  return False
167
163
 
168
164
  case DependencyType.FINISH_TO_FINISH:
169
165
  """target finish when source start"""
170
- if self.source.status != TaskStatus.COMPLETED:
166
+ if not self.source or self.source.status != TaskStatus.COMPLETED:
171
167
  return self.condition(**self.conditon_kwargs) if self.condition else True
172
168
  else:
173
169
  return False
174
170
 
175
171
  case DependencyType.START_TO_FINISH:
176
172
  """target finishes when source start"""
177
- if self.source.status == TaskStatus.IN_PROGRESS:
173
+ if not self.source or self.source.status == TaskStatus.IN_PROGRESS:
178
174
  return self.condition(**self.conditon_kwargs) if self.condition else True
179
175
  else:
180
176
  return False
@@ -190,10 +186,9 @@ class Edge(BaseModel):
190
186
  return None
191
187
 
192
188
  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")
189
+ Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll return None.", color="yellow")
194
190
  return None
195
191
 
196
-
197
192
  if self.lag:
198
193
  import time
199
194
  time.sleep(self.lag)
@@ -207,29 +202,42 @@ class Graph(ABC, BaseModel):
207
202
  """
208
203
  An abstract class to store G using NetworkX library.
209
204
  """
210
-
211
205
  directed: bool = Field(default=False, description="Whether the graph is directed")
212
206
  graph: Type[nx.Graph] = Field(default=None)
213
- nodes: Dict[str, InstanceOf[Node]] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
214
- edges: Dict[str, InstanceOf[Edge]] = Field(default_factory=dict)
207
+ nodes: Dict[str, Node] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
208
+ edges: Dict[str, Edge] = Field(default_factory=dict)
215
209
 
216
210
  def __init__(self, directed: bool = False, **kwargs):
217
211
  super().__init__(directed=directed, **kwargs)
218
- self.graph = nx.DiGraph() if self.directed else nx.Graph()
212
+ self.graph = nx.DiGraph(directed=True) if self.directed else nx.Graph()
213
+
219
214
 
220
215
  def _return_node_object(self, node_identifier) -> Node | None:
221
- 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
216
+ match = [v for k, v in self.nodes.items() if k == node_identifier]
217
+
218
+ if match:
219
+ node = match[0] if isinstance(match[0], Node) else match[0]["node"] if "node" in match[0] else None
220
+ return node
221
+ else:
222
+ return None
222
223
 
223
224
  def add_node(self, node: Node) -> None:
224
- self.graph.add_node(node.identifier, **node.model_dump())
225
+ if node.identifier in self.nodes.keys():
226
+ return
227
+ self.graph.add_node(node.identifier, node=node)
225
228
  self.nodes[node.identifier] = node
226
229
 
227
230
  def add_edge(self, source: str, target: str, edge: Edge) -> None:
228
- self.graph.add_edge(source, target, **edge.model_dump())
229
- edge.source = self._return_node_object(source)
230
- edge.source.out_degree_nodes.append(target)
231
- edge.target = self._return_node_object(target)
232
- edge.target.in_degree_nodes.append(source)
231
+ self.graph.add_edge(source, target, edge=edge)
232
+
233
+ source_node, target_node = self._return_node_object(source), self._return_node_object(target)
234
+
235
+ edge.source = source_node
236
+ source_node.out_degree_nodes.append(target_node)
237
+
238
+ edge.target = target_node
239
+ target_node.in_degree_nodes.append(source_node)
240
+
233
241
  self.edges[(source, target)] = edge
234
242
 
235
243
  def add_weighted_edges_from(self, edges):
@@ -244,11 +252,11 @@ class Graph(ABC, BaseModel):
244
252
  def get_out_degree(self, node: Node) -> int:
245
253
  return self.graph.out_degree(node)
246
254
 
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]
255
+ def find_start_nodes(self) -> List[Node]:
256
+ return [v for v in self.nodes.values() if v.in_degrees == 0 and v.out_degrees > 0]
249
257
 
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]
258
+ def find_end_nodes(self) -> List[Node]:
259
+ return [v for v in self.nodes.values() if v.out_degrees == 0 and v.in_degrees > 0]
252
260
 
253
261
  def find_critical_end_node(self) -> Node | None:
254
262
  """
@@ -310,51 +318,26 @@ class Graph(ABC, BaseModel):
310
318
  return False
311
319
 
312
320
 
313
- def visualize(self, title: str = "Task Graph", pos: Any = None, **graph_config):
314
- pos = pos if pos else nx.spring_layout(self.graph, seed=42)
315
- nx.draw(
316
- self.graph,
317
- pos,
318
- with_labels=True, node_size=700, node_color="skyblue", font_size=10, font_color="black", arrowstyle='-|>', arrowsize=20, arrows=True,
319
- **graph_config
320
- )
321
- edge_labels = {}
322
- for u, v, data in self.graph.edges(data=True):
323
- edge = self.edges.get((u,v))
324
- if edge:
325
- label_parts = []
326
- if edge.type:
327
- label_parts.append(f"Type: {edge.type}")
328
- if edge.duration is not None:
329
- label_parts.append(f"Duration: {edge.duration}")
330
- if edge.lag is not None:
331
- label_parts.append(f"Lag: {edge.lag}")
332
- edge_labels[(u, v)] = "\n".join(label_parts) # Combine labels with newlines
333
- nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
334
- plt.title(title)
335
- plt.show()
336
-
337
-
338
321
  class TaskGraph(Graph):
339
322
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
340
323
  should_reform: bool = Field(default=False)
341
- status: Dict[str, TaskStatus] = Field(default_factory=dict, description="store identifier (str) and TaskStatus of all task_nodes")
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")
324
+ outputs: Dict[str, TaskOutput] = Field(default_factory=dict, description="stores node identifier and TaskOutput")
325
+ concl_template: Optional[Dict[str, Any] | Type[BaseModel]] = Field(default=None, description="stores a format of `concl` either in Pydantic model or JSON dict")
326
+ concl: Any = Field(default=None, description="store the final result of the entire task graph")
344
327
 
345
328
 
346
- def _save(self, abs_file_path: str = None) -> None:
329
+ def _save(self, title: str, abs_file_path: str = None) -> None:
347
330
  """
348
331
  Save the graph image in the local directory.
349
332
  """
350
-
351
333
  try:
352
334
  import os
353
335
  project_root = os.path.abspath(os.getcwd())
354
- abs_file_path = abs_file_path if abs_file_path else f"{project_root}/uploads"
336
+ abs_file_path = abs_file_path if abs_file_path else f"{project_root}/.diagrams"
337
+ title = title if title else f"vhq-Diagram-{str(self.id)}"
355
338
 
356
339
  os.makedirs(abs_file_path, exist_ok=True)
357
- plt.savefig(f"{abs_file_path}/{str(self.id)}.png")
340
+ plt.savefig(f"{abs_file_path}/{title}.png")
358
341
 
359
342
  except Exception as e:
360
343
  Logger().log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
@@ -362,21 +345,38 @@ class TaskGraph(Graph):
362
345
 
363
346
  def add_task(self, task: Node | Task) -> Node:
364
347
  """Convert `task` to a Node object and add it to G"""
365
- task_node = task if isinstance(task, Node) else Node(task=task)
366
- self.add_node(task_node)
367
- self.status[task_node.identifier] = TaskStatus.NOT_STARTED
368
- return task_node
369
348
 
349
+ if isinstance(task, Node) and task.identifier in self.nodes.keys():
350
+ return task
351
+
352
+ elif isinstance(task, Task):
353
+ match = []
354
+ for v in self.nodes.values():
355
+ if type(v) == dict and v["node"] and v["node"].task == task:
356
+ match.append(v["node"])
357
+ elif v.task == task:
358
+ match.append(v)
370
359
 
371
- def add_dependency(
372
- self, source_task_node_identifier: str, target_task_node_identifier: str, **edge_attributes
373
- ) -> None:
360
+ if match:
361
+ return match[0]
362
+ else:
363
+ node = Node(task=task)
364
+ self.add_node(node)
365
+ return node
366
+
367
+ else:
368
+ task_node = task if isinstance(task, Node) else Node(task=task)
369
+ self.add_node(task_node)
370
+ return task_node
371
+
372
+
373
+ def add_dependency(self, source_node_identifier: str, target_node_identifier: str, **edge_attributes) -> None:
374
374
  """
375
375
  Add an edge that connect task 1 (source) and task 2 (target) using task_node.name as an identifier
376
376
  """
377
377
 
378
378
  if not edge_attributes:
379
- Logger(verbose=True).log(level="error", message="Edge attributes are missing.", color="red")
379
+ Logger().log(level="error", message="Edge attributes are missing.", color="red")
380
380
 
381
381
  edge = Edge()
382
382
  for k in Edge.model_fields.keys():
@@ -386,78 +386,97 @@ class TaskGraph(Graph):
386
386
  else:
387
387
  pass
388
388
 
389
- self.add_edge(source_task_node_identifier, target_task_node_identifier, edge)
390
-
391
-
392
- def set_task_status(self, identifier: str, status: TaskStatus) -> None:
393
- if identifier in self.status:
394
- self.status[identifier] = status
395
- else:
396
- Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
397
- pass
389
+ self.add_edge(source_node_identifier, target_node_identifier, edge)
398
390
 
399
391
 
400
- def get_task_status(self, identifier):
401
- if identifier in self.status:
402
- return self.status[identifier]
403
- else:
404
- Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
392
+ def get_task_status(self, identifier: str) -> TaskStatus | None:
393
+ """Retrieves the latest status of the given node"""
394
+ if not identifier or identifier not in self.nodes.keys():
395
+ Logger().log(level="error", message=f"Task node: {identifier} is not in the graph.", color="red")
405
396
  return None
406
397
 
398
+ return self._return_node_object(identifier).status
399
+
407
400
 
408
401
  def visualize(self, layout: str = None):
402
+ from matplotlib.lines import Line2D
403
+ from versionhq.task_graph.colors import white, black, darkgrey, grey, primary, orange, lightgreen, green, darkgreen, darkergreen
404
+
409
405
  try:
410
406
  pos = nx.drawing.nx_agraph.graphviz_layout(self.graph, prog='dot') # 'dot', 'neato', 'fdp', 'sfdp'
411
407
  except ImportError:
412
408
  pos = nx.spring_layout(self.graph, seed=42) # REFINEME - layout
413
409
 
414
- node_colors = list()
415
- for k, v in self.graph.nodes.items():
410
+ node_colors, legend_elements = list(), list()
411
+ for k, v in self.nodes.items():
416
412
  status = self.get_task_status(identifier=k)
417
- if status == TaskStatus.NOT_STARTED:
418
- node_colors.append("skyblue")
419
- elif status == TaskStatus.IN_PROGRESS:
420
- node_colors.append("lightgreen")
421
- elif status == TaskStatus.BLOCKED:
422
- node_colors.append("lightcoral")
423
- elif status == TaskStatus.COMPLETED:
424
- node_colors.append("black")
425
- elif status == TaskStatus.DELAYED:
426
- node_colors.append("orange")
427
- elif status == TaskStatus.ON_HOLD:
428
- node_colors.append("yellow")
429
- else:
430
- node_colors.append("grey")
413
+ node = v if isinstance(v, Node) else v["node"] if hasattr(v, "node") else None
414
+ if node:
415
+ task_name = node.task.name if node.task and node.task.name else node.task.description if node.task else None
416
+ legend_label = f"ID {str(node.identifier)[0: 6]}...: {task_name}" if task_name else f"ID {str(node.identifier)[0: 6]}..."
417
+ match status:
418
+ case TaskStatus.NOT_STARTED:
419
+ node_colors.append(green)
420
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=green))
421
+
422
+ case TaskStatus.IN_PROGRESS:
423
+ node_colors.append(darkgreen)
424
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=darkgreen))
425
+
426
+ case TaskStatus.WAITING:
427
+ node_colors.append(lightgreen)
428
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=lightgreen))
429
+
430
+ case TaskStatus.COMPLETED:
431
+ node_colors.append(darkergreen)
432
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=darkergreen))
433
+
434
+ case TaskStatus.DELAYED:
435
+ node_colors.append(orange)
436
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=orange))
437
+
438
+ case TaskStatus.ON_HOLD:
439
+ node_colors.append(grey)
440
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=grey))
441
+
442
+ case TaskStatus.ERROR:
443
+ node_colors.append(primary)
444
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=primary))
445
+
446
+ case _:
447
+ node_colors.append(white)
448
+ legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=white))
431
449
 
432
450
  critical_paths, duration, paths = self.find_critical_path()
433
- edge_colors = ['red' if (u, v) in zip(critical_paths, critical_paths[1:]) else 'black' for u, v in self.graph.edges()]
434
- edge_widths = []
451
+ edge_colors = [black if (u, v) in zip(critical_paths, critical_paths[1:]) else darkgrey for u, v in self.graph.edges()]
452
+ edge_widths = [v.weight * 0.5 for v in self.edges.values()]
435
453
 
436
- for k, v in self.edges.items():
437
- # edge_weights = nx.get_edge_attributes(self.graph, 'weight')
438
- # edge_colors.append(plt.cm.viridis(v.weight / max(edge_weights.values())))
439
- edge_widths.append(v.weight * 0.5)
454
+ # for k, v in self.edges.items():
455
+ # # edge_weights = nx.get_edge_attributes(self.graph, 'weight')
456
+ # # edge_colors.append(plt.cm.viridis(v.weight / max(edge_weights.values())))
457
+ # edge_widths.append(v.weight * 0.5)
440
458
 
441
459
  nx.draw(
442
460
  self.graph, pos,
443
461
  with_labels=True,
444
- node_size=700,
462
+ node_size=600,
445
463
  node_color=node_colors,
446
- font_size=10,
447
- font_color="black",
464
+ font_size=8,
465
+ font_color=darkgrey,
448
466
  edge_color=edge_colors,
449
467
  width=edge_widths,
450
468
  arrows=True,
451
- arrowsize=20,
469
+ arrowsize=15,
452
470
  arrowstyle='-|>'
453
471
  )
454
472
 
455
473
  edge_labels = nx.get_edge_attributes(G=self.graph, name="edges")
456
474
  nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
457
475
 
458
- plt.title("Project Network Diagram")
459
- self._save()
460
- plt.show()
476
+ plt.legend(handles=legend_elements, loc='lower right')
477
+ plt.title(f"vhq-Diagram {str(self.id)}")
478
+ self._save(title=f"vhq-Diagram {str(self.id)}")
479
+ plt.show(block=False)
461
480
 
462
481
 
463
482
  def activate(self, target_node_identifier: Optional[str] = None) -> Tuple[TaskOutput | None, Dict[str, TaskOutput]]:
@@ -465,6 +484,9 @@ class TaskGraph(Graph):
465
484
  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.
466
485
  Then returns tuple of the last task output and all task outputs (self.outputs)
467
486
  """
487
+
488
+ Logger().log(color="blue", message=f"Start to activate the graph: {str(self.id)}", level="info")
489
+
468
490
  if target_node_identifier:
469
491
  if not [k for k in self.nodes.keys() if k == target_node_identifier]:
470
492
  Logger().log(level="error", message=f"The node {str(target_node_identifier)} is not in the graph.", color="red")
@@ -483,7 +505,6 @@ class TaskGraph(Graph):
483
505
  if len([item for item in edge_status if item["dep_met"] == True]) == len(sources):
484
506
  res = node.handle_task_execution()
485
507
  self.outputs.update({ target_node_identifier: res })
486
- self.status.update({ target_node_identifier: edge.target.status })
487
508
 
488
509
  return res, self.outputs
489
510
 
@@ -502,15 +523,13 @@ class TaskGraph(Graph):
502
523
  if end_nodes and len([node for node in end_nodes if node.status == TaskStatus.COMPLETED]) == len(end_nodes):
503
524
  if critical_end_node:
504
525
  return critical_end_node.task.output, self.outputs
505
-
506
526
  else:
507
- return [v.task.output.raw for k, v in end_nodes.items()][0], self.outputs
527
+ return [v.task.output for k, v in end_nodes.items()][0], self.outputs
508
528
 
509
529
  # Else, execute nodes connected with the critical_path
510
530
  elif critical_path:
511
531
  for item in critical_path:
512
532
  edge = [v for k, v in self.edges.items() if item in k]
513
-
514
533
  if edge:
515
534
  edge = edge[0]
516
535
 
@@ -521,29 +540,24 @@ class TaskGraph(Graph):
521
540
  res = edge.activate()
522
541
  node_identifier = edge.target.identifier
523
542
  self.outputs.update({ node_identifier: res })
524
- self.status.update({ node_identifier: edge.target.status })
525
543
 
526
544
  if not res and start_nodes:
527
545
  for node in start_nodes:
528
546
  res = node.handle_task_execution()
529
547
  self.outputs.update({ node.identifier: res })
530
- self.status.update({ node.identifier: node.status })
531
548
 
532
- # if no critical paths in the graph, simply start from the start nodes.
549
+ # If no critical paths in the graph, simply start from the start nodes.
533
550
  elif start_nodes:
534
551
  for node in start_nodes:
535
552
  res = node.handle_task_execution()
536
553
  self.outputs.update({ node.identifier: res })
537
- self.status.update({ node.identifier: node.status })
538
-
539
554
 
540
- # if none of above is applicable, try to activate all the edges.
555
+ # If none of above is applicable, try to activate all the edges.
541
556
  else:
542
557
  for k, edge in self.edges.items():
543
558
  res = edge.activate()
544
559
  node_identifier = edge.target.identifier
545
560
  self.outputs.update({ node_identifier: res })
546
- self.status.update({ node_identifier: edge.target.status })
547
561
 
548
562
  # 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
563
  return res, self.outputs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: versionhq
3
- Version: 1.2.1.5
3
+ Version: 1.2.1.6
4
4
  Summary: An agentic orchestration framework for building agent networks that handle task automation.
5
5
  Author-email: Kuriko Iwai <kuriko@versi0n.io>
6
6
  License: MIT License
@@ -89,7 +89,7 @@ Requires-Dist: pygraphviz>=1.14; extra == "pygraphviz"
89
89
  ![pyenv ver](https://img.shields.io/badge/pyenv-2.5.0-orange)
90
90
 
91
91
 
92
- Agentic orchestration framework for multi-agent networks and task graph for complex task automation.
92
+ Agentic orchestration framework for multi-agent networks and task graphs for complex task automation.
93
93
 
94
94
  **Visit:**
95
95
 
@@ -361,7 +361,7 @@ Tasks can be delegated to a manager, peers within the agent network, or a comple
361
361
 
362
362
  **Workflow, Task Graph**
363
363
 
364
- * [NetworkX](https://networkx.org/documentation/stable/reference/introduction.html): A Python package to analyze, create, and manipulate complex graph networks.
364
+ * [NetworkX](https://networkx.org/documentation/stable/reference/introduction.html): A Python package to analyze, create, and manipulate complex graph networks. Ref. [Gallary](https://networkx.org/documentation/latest/auto_examples/index.html)
365
365
  * [Matplotlib](https://matplotlib.org/stable/index.html): For graph visualization.
366
366
  * [Graphviz](https://graphviz.org/about/): For graph visualization.
367
367
 
@@ -397,7 +397,7 @@ Tasks can be delegated to a manager, peers within the agent network, or a comple
397
397
  .github
398
398
  └── workflows/ # Github actions
399
399
 
400
- docs/ # Documentation built by MkDocs
400
+ docs/ # Documentation
401
401
  mkdocs.yml # MkDocs config
402
402
 
403
403
  src/
@@ -413,12 +413,13 @@ src/
413
413
  │ └── llm/
414
414
  │ └── ...
415
415
 
416
- └── uploads/ [.gitignore] # Local directory to store uploaded files such as graphviz diagrams generatd by `Network` class
416
+ └── .diagrams/ [.gitignore] # Local directory to store graph diagrams
417
417
 
418
- └── .logs/ [.gitignore] # Local directory to store error/warning logs for debugging
418
+ └── .logs/ [.gitignore] # Local directory to store error/warning logs for debugging
419
419
 
420
420
 
421
421
  pyproject.toml # Project config
422
+ .env.sample # sample .env file
422
423
 
423
424
  ```
424
425
 
@@ -475,16 +476,7 @@ pyproject.toml # Project config
475
476
 
476
477
  ### Adding env secrets to .env file
477
478
 
478
- Create `.env` file in the project root and add following:
479
-
480
- ```
481
- OPENAI_API_KEY=your-openai-api-key
482
- GEMINI_API_KEY=your-gemini-api-key
483
- LITELLM_API_KEY=your-litellm-api-key
484
- COMPOSIO_API_KEY=your-composio-api-key
485
- COMPOSIO_CLI_KEY=your-composio-cli-key
486
- [OTHER_LLM_INTERFACE_PROVIDER_OF_YOUR_CHOICE]_API_KEY=your-api-key
487
- ```
479
+ Create `.env` file in the project root and add secret vars following `.env.sample` file.
488
480
 
489
481
 
490
482
  <hr />
@@ -1,4 +1,4 @@
1
- versionhq/__init__.py,sha256=zh62wJK4H9QFpeR2SWXHuFDB8fvp-sN1ygezYC4pQGU,2817
1
+ versionhq/__init__.py,sha256=Qi68iCRR29ZUfpZOVR_1Bcag4DdJi3xJsYJQOsZCe3U,2882
2
2
  versionhq/_utils/__init__.py,sha256=dzoZr4cBlh-2QZuPzTdehPUCe9lP1dmRtauD7qTjUaA,158
3
3
  versionhq/_utils/i18n.py,sha256=TwA_PnYfDLA6VqlUDPuybdV9lgi3Frh_ASsb_X8jJo8,1483
4
4
  versionhq/_utils/logger.py,sha256=zgogTwAY-ujDLrdryAKhdtoaNe1nOFajmEN0V8aMR34,3155
@@ -44,14 +44,16 @@ versionhq/storage/task_output_storage.py,sha256=E1t_Fkt78dPYIOl3MP7LfQ8oGtjlzxBu
44
44
  versionhq/storage/utils.py,sha256=ByYXPoEIGJYLUqz-DWjbCAnneNrH1otiYbp12SCILpM,747
45
45
  versionhq/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  versionhq/task/evaluate.py,sha256=WdUgjbZL62XrxyWe5MTz29scfzwmuAHGxJ7GvAB8Fmk,3954
47
- versionhq/task/formation.py,sha256=qkAJ1ToeOHQFR3hqC8nJyU6msftPgm8yEPTrFfz77OA,6906
47
+ versionhq/task/formation.py,sha256=WH604q9bRmWH7KQCrk2qKJwisCopYX5CjJvsj4TgFjI,6894
48
48
  versionhq/task/formatter.py,sha256=N8Kmk9vtrMtBdgJ8J7RmlKNMdZWSmV8O1bDexmCWgU0,643
49
49
  versionhq/task/log_handler.py,sha256=LT7YnO7gcPR9IZS7eRvMjnHh8crMBFtqduxd8dxIbkk,1680
50
- versionhq/task/model.py,sha256=y4scd9c_RFF32OmJqBL3BBxVPykR8fu2DWk_tIqNgXk,28429
50
+ versionhq/task/model.py,sha256=54IxQvDbC2hhCDcAvA-MZuoNyT0k1UsYm8Kq7M3cMaE,28503
51
51
  versionhq/task/structured_response.py,sha256=4q-hQPu7oMMHHXEzh9YW4SJ7N5eCZ7OfZ65juyl_jCI,5000
52
52
  versionhq/task/TEMPLATES/Description.py,sha256=V-4kh8xpQTKOcDMi2xnuP-fcNk6kuoz1_5tYBlDLQWQ,420
53
53
  versionhq/task_graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- versionhq/task_graph/model.py,sha256=TsszoTJFeJ4TfSdLAjcGa9QSQyf9Uy3WmechTw0qL3k,22832
54
+ versionhq/task_graph/colors.py,sha256=naJCx4Vho4iuJtbW8USUXb-M5uYvd5ds2p8qbjUfRus,669
55
+ versionhq/task_graph/draft.py,sha256=nALpwFIDEEhij64ezMRKyNhmyyiXIhzA-6wpcU35xIs,4772
56
+ versionhq/task_graph/model.py,sha256=ZXpuG6f1OZBrGV0Ai1kBuRDNSlwj17WH5GAuWvTWjFQ,23545
55
57
  versionhq/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
58
  versionhq/tool/cache_handler.py,sha256=iL8FH7X0G-cdT0uhJwzuhLDaadTXOdfybZcDy151-es,1085
57
59
  versionhq/tool/composio_tool.py,sha256=38mEiVvTkuw1BLD233Bl1Gwxbpss1yfQiZLTWwX6BdA,8648
@@ -59,8 +61,8 @@ versionhq/tool/composio_tool_vars.py,sha256=FvBuEXsOQUYnN7RTFxT20kAkiEYkxWKkiVtg
59
61
  versionhq/tool/decorator.py,sha256=C4ZM7Xi2gwtEMaSeRo-geo_g_MAkY77WkSLkAuY0AyI,1205
60
62
  versionhq/tool/model.py,sha256=PO4zNWBZcJhYVur381YL1dy6zqurio2jWjtbxOxZMGI,12194
61
63
  versionhq/tool/tool_handler.py,sha256=2m41K8qo5bGCCbwMFferEjT-XZ-mE9F0mDUOBkgivOI,1416
62
- versionhq-1.2.1.5.dist-info/LICENSE,sha256=cRoGGdM73IiDs6nDWKqPlgSv7aR4n-qBXYnJlCMHCeE,1082
63
- versionhq-1.2.1.5.dist-info/METADATA,sha256=NdmB2VpDlH4dUaxzey8BSIjMlMzB1vj4tTrpYF00LdE,22395
64
- versionhq-1.2.1.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
65
- versionhq-1.2.1.5.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
66
- versionhq-1.2.1.5.dist-info/RECORD,,
64
+ versionhq-1.2.1.6.dist-info/LICENSE,sha256=cRoGGdM73IiDs6nDWKqPlgSv7aR4n-qBXYnJlCMHCeE,1082
65
+ versionhq-1.2.1.6.dist-info/METADATA,sha256=i-9oGWPZtHYMDxHIgNofj54Lg2E2nryyD4jKFFm6T9Q,22204
66
+ versionhq-1.2.1.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
67
+ versionhq-1.2.1.6.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
68
+ versionhq-1.2.1.6.dist-info/RECORD,,