versionhq 1.2.0.4__py3-none-any.whl → 1.2.1.0__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
@@ -16,7 +16,7 @@ from versionhq.clients.workflow.model import MessagingWorkflow, MessagingCompone
16
16
  from versionhq.knowledge.model import Knowledge, KnowledgeStorage
17
17
  from versionhq.knowledge.source import PDFKnowledgeSource, CSVKnowledgeSource, JSONKnowledgeSource, TextFileKnowledgeSource, ExcelKnowledgeSource, StringKnowledgeSource
18
18
  from versionhq.knowledge.source_docling import DoclingSource
19
- from versionhq.network.model import TaskStatus, TaskGraph, Node, Edge, DependencyType
19
+ from versionhq.graph.model import TaskStatus, TaskGraph, Node, Edge, DependencyType
20
20
  from versionhq.task.model import Task, TaskOutput, ResponseField, TaskExecutionType
21
21
  from versionhq.task.evaluate import Evaluation, EvaluationItem
22
22
  from versionhq.team.model import Team, TeamOutput, Formation, Member, TaskHandlingProcess
@@ -30,7 +30,7 @@ from versionhq.memory.model import ShortTermMemory,LongTermMemory, UserMemory, M
30
30
  from versionhq.task.formation import form_agent_network
31
31
 
32
32
 
33
- __version__ = "1.2.0.4"
33
+ __version__ = "1.2.1.0"
34
34
  __all__ = [
35
35
  "Agent",
36
36
 
@@ -38,7 +38,7 @@ class Printer:
38
38
  class Logger(BaseModel):
39
39
  """
40
40
  Control CLI messages.
41
- Color: red = error, yellow = warning, blue = info (from vhq), green = info (from third party)
41
+ Color: red = error, yellow = warning, blue = info (from vhq), green = info (from third parties)
42
42
  """
43
43
 
44
44
  verbose: bool = Field(default=True)
@@ -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, PrivateAttr, field_validator
9
9
  from pydantic_core import PydanticCustomError
10
10
 
11
11
  from versionhq.task.model import Task, TaskOutput
@@ -13,6 +13,26 @@ from versionhq.agent.model import Agent
13
13
  from versionhq._utils.logger import Logger
14
14
 
15
15
 
16
+ def gen_network():
17
+ goal = "make a promo plan to increase gross sales of Temu, an ecommerce site with affordable items."
18
+ agent = Agent(
19
+ role="Network Generator", goal="draft a best graph with nodes (tasks) and edges", llm="gemini-2.0", maxit=1,
20
+ knowledge_sources=["https://en.wikipedia.org/wiki/Graph_theory", "https://www.temu.com", ]
21
+ )
22
+ class Outcome(BaseModel):
23
+ task_descriptions: list[str]
24
+
25
+
26
+ task = Task(
27
+ description=" 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. ",
28
+ pydantic_output=Outcome
29
+ )
30
+ task = Task(
31
+ description="Form best network with nodes and edges for the task described as following: Draft a promo plan for the client.",
32
+ pydantic_output=Edge
33
+ )
34
+ res = task.execute(agent=agent)
35
+
16
36
  class TaskStatus(enum.Enum):
17
37
  """
18
38
  Enum to track the task execution status
@@ -37,21 +57,21 @@ class DependencyType(enum.Enum):
37
57
 
38
58
 
39
59
 
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
60
+ # class TriggerEvent(enum.Enum):
61
+ # """
62
+ # Concise enumeration of key trigger events for task execution.
63
+ # """
64
+ # IMMEDIATE = 0 # execute immediately
65
+ # DEPENDENCIES_MET = 1 # All/required dependencies are satisfied
66
+ # RESOURCES_AVAILABLE = 2 # Necessary resources are available
67
+ # SCHEDULED_TIME = 3 # Scheduled start time or time window reached
68
+ # EXTERNAL_EVENT = 4 # Triggered by an external event/message
69
+ # DATA_AVAILABLE = 5 # Required data is available both internal/external
70
+ # APPROVAL_RECEIVED = 6 # Necessary approvals have been granted
71
+ # STATUS_CHANGED = 7 # Relevant task/system status has changed
72
+ # RULE_MET = 8 # A predefined rule or condition has been met
73
+ # MANUAL_TRIGGER = 9 # Manually initiated by a user
74
+ # ERROR_HANDLED = 10 # A previous error/exception has been handled
55
75
 
56
76
 
57
77
 
@@ -62,8 +82,8 @@ class Node(BaseModel):
62
82
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
63
83
  task: InstanceOf[Task] = Field(default=None)
64
84
  # 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")
85
+ in_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
86
+ out_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
67
87
  assigned_to: InstanceOf[Agent] = Field(default=None)
68
88
  status: TaskStatus = Field(default=TaskStatus.NOT_STARTED)
69
89
 
@@ -77,22 +97,6 @@ class Node(BaseModel):
77
97
  def is_independent(self) -> bool:
78
98
  return not self.in_degree_nodes and not self.out_degree_nodes
79
99
 
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
100
 
97
101
  def handle_task_execution(self, agent: Agent = None, context: str = None) -> TaskOutput | None:
98
102
  """
@@ -110,15 +114,33 @@ class Node(BaseModel):
110
114
  self.status = TaskStatus.COMPLETED if res else TaskStatus.ERROR
111
115
  return res
112
116
 
117
+
118
+ @property
119
+ def in_degrees(self) -> int:
120
+ return len(self.in_degree_nodes) if self.in_degree_nodes else 0
121
+
122
+ @property
123
+ def out_degrees(self) -> int:
124
+ return len(self.out_degree_nodes) if self.out_degree_nodes else 0
125
+
126
+ @property
127
+ def degrees(self) -> int:
128
+ return self.in_degrees + self.out_degrees
129
+
130
+ @property
131
+ def identifier(self) -> str:
132
+ """Unique identifier for the node"""
133
+ return f"{str(self.id)}"
134
+
113
135
  def __str__(self):
114
136
  return self.identifier
115
137
 
116
138
 
117
-
118
139
  class Edge(BaseModel):
119
140
  """
120
141
  A class to store an edge object that connects source and target nodes.
121
142
  """
143
+
122
144
  source: Node = Field(default=None)
123
145
  target: Node = Field(default=None)
124
146
 
@@ -152,28 +174,28 @@ class Edge(BaseModel):
152
174
  case DependencyType.FINISH_TO_START:
153
175
  """target starts after source finishes"""
154
176
  if self.source.status == TaskStatus.COMPLETED:
155
- return self.condition(**self.conditon_kwargs) if self.condtion else True
177
+ return self.condition(**self.conditon_kwargs) if self.condition else True
156
178
  else:
157
179
  return False
158
180
 
159
181
  case DependencyType.START_TO_START:
160
182
  """target starts when source starts"""
161
183
  if self.source.status != TaskStatus.NOT_STARTED:
162
- return self.condition(**self.conditon_kwargs) if self.condtion else True
184
+ return self.condition(**self.conditon_kwargs) if self.condition else True
163
185
  else:
164
186
  return False
165
187
 
166
188
  case DependencyType.FINISH_TO_FINISH:
167
189
  """target finish when source start"""
168
190
  if self.source.status != TaskStatus.COMPLETED:
169
- return self.condition(**self.conditon_kwargs) if self.condtion else True
191
+ return self.condition(**self.conditon_kwargs) if self.condition else True
170
192
  else:
171
193
  return False
172
194
 
173
195
  case DependencyType.START_TO_FINISH:
174
196
  """target finishes when source start"""
175
197
  if self.source.status == TaskStatus.IN_PROGRESS:
176
- return self.condition(**self.conditon_kwargs) if self.condtion else True
198
+ return self.condition(**self.conditon_kwargs) if self.condition else True
177
199
  else:
178
200
  return False
179
201
 
@@ -183,19 +205,20 @@ class Edge(BaseModel):
183
205
  Activates the edge to initiate task execution of the target node.
184
206
  """
185
207
 
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
208
  if not self.source or not self.target:
191
209
  Logger(verbose=True).log(level="warning", message="Cannot find source or target nodes. We'll return None.", color="yellow")
192
210
  return None
193
211
 
212
+ if not self.dependency_met():
213
+ Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll see the source node status.", color="yellow")
214
+ return None
215
+
216
+
194
217
  if self.lag:
195
218
  import time
196
219
  time.sleep(self.lag)
197
220
 
198
- context = self.source.task.task_output.raw if self.data_transfer else None
221
+ context = self.source.task.output.raw if self.data_transfer else None
199
222
  res = self.target.handle_task_execution(context=context)
200
223
  return res
201
224
 
@@ -205,8 +228,6 @@ class Graph(ABC, BaseModel):
205
228
  An abstract class to store G using NetworkX library.
206
229
  """
207
230
 
208
-
209
- _logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
210
231
  directed: bool = Field(default=False, description="Whether the graph is directed")
211
232
  graph: Type[nx.Graph] = Field(default=None)
212
233
  nodes: Dict[str, InstanceOf[Node]] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
@@ -226,7 +247,9 @@ class Graph(ABC, BaseModel):
226
247
  def add_edge(self, source: str, target: str, edge: Edge) -> None:
227
248
  self.graph.add_edge(source, target, **edge.model_dump())
228
249
  edge.source = self._return_node_object(source)
250
+ edge.source.out_degree_nodes.append(target)
229
251
  edge.target = self._return_node_object(target)
252
+ edge.target.in_degree_nodes.append(source)
230
253
  self.edges[(source, target)] = edge
231
254
 
232
255
  def add_weighted_edges_from(self, edges):
@@ -241,6 +264,28 @@ class Graph(ABC, BaseModel):
241
264
  def get_out_degree(self, node: Node) -> int:
242
265
  return self.graph.out_degree(node)
243
266
 
267
+ def find_start_nodes(self) -> Tuple[Node]:
268
+ return [v for k, v in self.nodes.items() if v.in_degrees == 0 and v.out_degrees > 0]
269
+
270
+ def find_end_nodes(self) -> Tuple[Node]:
271
+ return [v for k, v in self.nodes.items() if v.out_degrees == 0 and v.in_degrees > 0]
272
+
273
+ def find_critical_end_node(self) -> Node | None:
274
+ """
275
+ Find a critical end node from all the end nodes to lead a conclusion of the entire graph.
276
+ """
277
+ end_nodes = self.find_end_nodes()
278
+ if not end_nodes:
279
+ return None
280
+
281
+ if len(end_nodes) == 1:
282
+ return end_nodes[0]
283
+
284
+ edges = [v for k, v in self.edges if isinstance(v, Edge) and v.source in end_nodes]
285
+ critical_edge = max(edges, key=lambda item: item['weight']) if edges else None
286
+ return critical_edge.target if critical_edge else None
287
+
288
+
244
289
  def find_path(self, source: Optional[str] | None, target: str, weight: Optional[Any] | None) -> Any:
245
290
  try:
246
291
  return nx.shortest_path(self.graph, source=source, target=target, weight=weight)
@@ -255,14 +300,14 @@ class Graph(ABC, BaseModel):
255
300
  Finds the critical path in the graph.
256
301
  Returns:
257
302
  A tuple containing:
258
- - The critical path (a list of task names).
303
+ - The critical path (a list of edge identifiers).
259
304
  - The duration of the critical path.
260
305
  - A dictionary of all paths and their durations.
261
306
  """
262
307
 
263
308
  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
309
+ for start_node in self.find_start_nodes():
310
+ for end_node in self.find_end_nodes(): # End at nodes with 0 out-degree
266
311
  for edge in nx.all_simple_paths(self.graph, source=start_node.identifier, target=end_node.identifier):
267
312
  edge_weight = sum(self.edges.get(item).weight if self.edges.get(item) else 0 for item in edge)
268
313
  all_paths[tuple(edge)] = edge_weight
@@ -314,7 +359,26 @@ class TaskGraph(Graph):
314
359
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
315
360
  should_reform: bool = Field(default=False)
316
361
  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")
362
+ outputs: Dict[str, TaskOutput] = Field(default_factory=dict, description="store node identifire and TaskOutput")
363
+ conclusion: Any = Field(default=None, description="store the final result of the entire task graph. critical path target/end node")
364
+
365
+
366
+ def _save(self, abs_file_path: str = None) -> None:
367
+ """
368
+ Save the graph image in the local directory.
369
+ """
370
+
371
+ try:
372
+ import os
373
+ project_root = os.path.abspath(os.getcwd())
374
+ abs_file_path = abs_file_path if abs_file_path else f"{project_root}/uploads"
375
+
376
+ os.makedirs(abs_file_path, exist_ok=True)
377
+ plt.savefig(f"{abs_file_path}/{str(self.id)}.png")
378
+
379
+ except Exception as e:
380
+ Logger().log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
381
+
318
382
 
319
383
  def add_task(self, task: Node | Task) -> Node:
320
384
  """Convert `task` to a Node object and add it to G"""
@@ -332,7 +396,7 @@ class TaskGraph(Graph):
332
396
  """
333
397
 
334
398
  if not edge_attributes:
335
- self._logger.log(level="error", message="Edge attributes are missing.", color="red")
399
+ Logger(verbose=True).log(level="error", message="Edge attributes are missing.", color="red")
336
400
 
337
401
  edge = Edge()
338
402
  for k in Edge.model_fields.keys():
@@ -349,14 +413,15 @@ class TaskGraph(Graph):
349
413
  if identifier in self.status:
350
414
  self.status[identifier] = status
351
415
  else:
352
- self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
416
+ Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
353
417
  pass
354
418
 
419
+
355
420
  def get_task_status(self, identifier):
356
421
  if identifier in self.status:
357
422
  return self.status[identifier]
358
423
  else:
359
- self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
424
+ Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
360
425
  return None
361
426
 
362
427
 
@@ -415,23 +480,6 @@ class TaskGraph(Graph):
415
480
  plt.show()
416
481
 
417
482
 
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
483
  def activate(self, target_node_identifier: Optional[str] = None) -> Tuple[TaskOutput | None, Dict[str, TaskOutput]]:
436
484
  """
437
485
  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 +487,7 @@ class TaskGraph(Graph):
439
487
  """
440
488
  if target_node_identifier:
441
489
  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")
490
+ Logger().log(level="error", message=f"The node {str(target_node_identifier)} is not in the graph.", color="red")
443
491
  return None
444
492
 
445
493
  # find a shortest path to each in-degree node of the node and see if dependency met.
@@ -460,18 +508,62 @@ class TaskGraph(Graph):
460
508
  return res, self.outputs
461
509
 
462
510
  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")
511
+ if not self.edges or not self.nodes:
512
+ Logger().log(level="error", message="TaskGraph needs at least 2 nodes and 1 edge to activate. We'll return None.", color="red")
465
513
  return None
466
514
 
467
- for edge in self.edges:
468
- res = edge.activate()
469
- if not res:
470
- break
515
+ start_nodes = self.find_start_nodes()
516
+ end_nodes = self.find_end_nodes()
517
+ critical_end_node = self.find_critical_end_node()
518
+ critical_path, _, _ = self.find_critical_path()
519
+ res = None
520
+
521
+ # When all nodes are completed, return the output of the critical end node or end node.
522
+ if end_nodes and len([node for node in end_nodes if node.status == TaskStatus.COMPLETED]) == len(end_nodes):
523
+ if critical_end_node:
524
+ return critical_end_node.task.output, self.outputs
525
+
526
+ else:
527
+ return [v.task.output.raw for k, v in end_nodes.items()][0], self.outputs
528
+
529
+ # Else, execute nodes connected with the critical_path
530
+ elif critical_path:
531
+ for item in critical_path:
532
+ edge = [v for k, v in self.edges.items() if item in k]
533
+
534
+ if edge:
535
+ edge = edge[0]
536
+
537
+ if edge.target.status == TaskStatus.COMPLETED:
538
+ res = edge.target.output
471
539
 
472
- node_identifier = edge.target.identifier
473
- self.outputs.update({ node_identifier: res })
474
- self.status.update({ node_identifier: edge.target.status })
540
+ else:
541
+ res = edge.activate()
542
+ node_identifier = edge.target.identifier
543
+ self.outputs.update({ node_identifier: res })
544
+ self.status.update({ node_identifier: edge.target.status })
475
545
 
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
546
+ if not res and start_nodes:
547
+ for node in start_nodes:
548
+ res = node.handle_task_execution()
549
+ self.outputs.update({ node.identifier: res })
550
+ self.status.update({ node.identifier: node.status })
551
+
552
+ # if no critical paths in the graph, simply start from the start nodes.
553
+ elif start_nodes:
554
+ for node in start_nodes:
555
+ res = node.handle_task_execution()
556
+ self.outputs.update({ node.identifier: res })
557
+ self.status.update({ node.identifier: node.status })
558
+
559
+
560
+ # if none of above is applicable, try to activate all the edges.
561
+ else:
562
+ for k, edge in self.edges.items():
563
+ res = edge.activate()
564
+ node_identifier = edge.target.identifier
565
+ self.outputs.update({ node_identifier: res })
566
+ self.status.update({ node_identifier: edge.target.status })
567
+
568
+ # 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
569
+ return res, self.outputs
@@ -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}
@@ -109,7 +109,7 @@ def form_agent_network(
109
109
  team_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)
112
+ team = Team(members=members, formation=_formation, team_tasks=team_tasks, planner_llm=vhq_formation_planner.llm)
113
113
  return team
114
114
 
115
115
  else:
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.
versionhq/team/model.py CHANGED
@@ -27,33 +27,25 @@ def match_type(self, obj):
27
27
 
28
28
  GenerateSchema.match_type = match_type
29
29
  warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
30
- load_dotenv(override=True)
31
-
32
- # agentops = None
33
- # if os.environ.get("AGENTOPS_API_KEY"):
34
- # try:
35
- # import agentops # type: ignore
36
- # except ImportError:
37
- # pass
38
-
39
30
 
40
31
 
41
32
  class Formation(str, Enum):
42
33
  UNDEFINED = 0
43
34
  SOLO = 1
44
35
  SUPERVISING = 2
45
- NETWORK = 3
36
+ SQUAD = 3
46
37
  RANDOM = 4
47
38
  HYBRID = 10
48
39
 
49
40
 
50
41
  class TaskHandlingProcess(str, Enum):
51
42
  """
52
- Class representing the different processes that can be used to tackle multiple tasks.
43
+ A class representing task handling processes to tackle multiple tasks.
44
+ When the team has multiple tasks that connect with edges, follow the edge conditions.
53
45
  """
54
- sequential = "sequential"
55
- hierarchical = "hierarchical"
56
- consensual = "consensual"
46
+ SEQUENT = 1
47
+ HIERARCHY = 2
48
+ CONSENSUAL = 3 # either from managers or peers or (human) - most likely controlled by edge
57
49
 
58
50
 
59
51
  class TeamOutput(TaskOutput):
@@ -63,19 +55,16 @@ class TeamOutput(TaskOutput):
63
55
 
64
56
  team_id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="store the team ID that generate the TeamOutput")
65
57
  task_description: str = Field(default=None, description="store initial request (task description) from the client")
66
- task_outputs: list[TaskOutput] = Field(default=list, description="store outputs of all tasks that the team has executed")
67
- token_usage: UsageMetrics = Field(default=dict, description="processed token summary")
68
-
58
+ task_outputs: list[TaskOutput] = Field(default=list, description="store TaskOutput objects of all tasks that the team has executed")
59
+ # token_usage: UsageMetrics = Field(default=dict, description="processed token summary")
69
60
 
70
61
  def return_all_task_outputs(self) -> List[Dict[str, Any]]:
71
62
  res = [output.json_dict for output in self.task_outputs]
72
63
  return res
73
64
 
74
-
75
65
  def __str__(self):
76
66
  return (str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw)
77
67
 
78
-
79
68
  def __getitem__(self, key):
80
69
  if self.pydantic and hasattr(self.pydantic, key):
81
70
  return getattr(self.pydantic, key)
@@ -88,7 +77,7 @@ class TeamOutput(TaskOutput):
88
77
 
89
78
  class Member(BaseModel):
90
79
  """
91
- A class to store a member in the network and connect the agent as a member with tasks and sharable settings.
80
+ A class to store a member in the team and connect the agent as a member with tasks and memory/knowledge share settings.
92
81
  """
93
82
  agent: Agent | None = Field(default=None)
94
83
  is_manager: bool = Field(default=False)
@@ -103,7 +92,8 @@ class Member(BaseModel):
103
92
 
104
93
  class Team(BaseModel):
105
94
  """
106
- A class to store agent network that shares knowledge, memory and tools among the members.
95
+ A class to store a team with members and tasks.
96
+ Tasks can be 1. multiple individual tasks, 2. multiple dependant tasks connected via Graph, and 3. hybrid.
107
97
  """
108
98
 
109
99
  __hash__ = object.__hash__
@@ -123,7 +113,7 @@ class Team(BaseModel):
123
113
 
124
114
  # task execution rules
125
115
  prompt_file: str = Field(default="", description="absolute path to the prompt json file")
126
- process: TaskHandlingProcess = Field(default=TaskHandlingProcess.sequential)
116
+ process: TaskHandlingProcess = Field(default=TaskHandlingProcess.SEQUENT)
127
117
 
128
118
  # callbacks
129
119
  pre_launch_callbacks: List[Callable[[Optional[Dict[str, Any]]], Optional[Dict[str, Any]]]] = Field(
@@ -174,21 +164,19 @@ class Team(BaseModel):
174
164
 
175
165
 
176
166
  @model_validator(mode="after")
177
- def check_manager_llm(self):
167
+ def check_manager(self):
178
168
  """
179
169
  Check if the team has a manager
180
170
  """
181
-
182
- if self.process == TaskHandlingProcess.hierarchical or self.formation == Formation.SUPERVISING:
171
+ if self.process == TaskHandlingProcess.HIERARCHY or self.formation == Formation.SUPERVISING:
183
172
  if not self.managers:
184
173
  self._logger.log(level="error", message="The process or formation created needs at least 1 manager agent.", color="red")
185
- raise PydanticCustomError(
186
- "missing_manager_llm_or_manager","Attribute `manager_llm` or `manager` is required when using hierarchical process.", {})
174
+ raise PydanticCustomError("missing_manager", "`manager` is required when using hierarchical process.", {})
187
175
 
188
- ## comment out for the formation flexibilities
189
- # if self.managers and (self.manager_tasks is None or self.team_tasks is None):
190
- # self._logger.log(level="error", message="The manager is idling. At least 1 task needs to be assigned to the manager.", color="red")
191
- # raise PydanticCustomError("missing_manager_task", "manager needs to have at least one manager task or team task.", {})
176
+ ## comment out for the formation flexibilities
177
+ # if self.managers and (self.manager_tasks is None or self.team_tasks is None):
178
+ # self._logger.log(level="error", message="The manager is idling. At least 1 task needs to be assigned to the manager.", color="red")
179
+ # raise PydanticCustomError("missing_manager_task", "manager needs to have at least one manager task or team task.", {})
192
180
 
193
181
  return self
194
182
 
@@ -198,7 +186,7 @@ class Team(BaseModel):
198
186
  """
199
187
  Sequential task processing without any team tasks require a task-agent pairing.
200
188
  """
201
- if self.process == TaskHandlingProcess.sequential and self.team_tasks is None:
189
+ if self.process == TaskHandlingProcess.SEQUENT and self.team_tasks is None:
202
190
  for task in self.tasks:
203
191
  if not [member.task == task for member in self.members]:
204
192
  self._logger.log(level="error", message=f"The following task needs a dedicated agent to be assinged: {task.description}", color="red")
@@ -278,31 +266,6 @@ class Team(BaseModel):
278
266
  return task_outputs
279
267
 
280
268
 
281
- def _create_team_output(self, task_outputs: List[TaskOutput], lead_task_output: TaskOutput = None) -> TeamOutput:
282
- """
283
- Take the output of the first task or the lead task output as the team output `raw` value.
284
- Note that `tasks` are already sorted by the importance.
285
- """
286
-
287
- if not task_outputs:
288
- self._logger.log(level="error", message="Missing task outcomes. Failed to launch the task.", color="red")
289
- raise ValueError("Failed to launch tasks")
290
-
291
- final_task_output = lead_task_output if lead_task_output is not None else task_outputs[0] #! REFINEME
292
- # final_string_output = final_task_output.raw
293
- # self._finish_execution(final_string_output)
294
- token_usage = self._calculate_usage_metrics()
295
-
296
- return TeamOutput(
297
- team_id=self.id,
298
- raw=final_task_output.raw,
299
- json_dict=final_task_output.json_dict,
300
- pydantic=final_task_output.pydantic,
301
- task_outputs=task_outputs,
302
- token_usage=token_usage,
303
- )
304
-
305
-
306
269
  def _calculate_usage_metrics(self) -> UsageMetrics:
307
270
  """
308
271
  Calculate and return the usage metrics that consumed by the team.
@@ -371,14 +334,29 @@ class Team(BaseModel):
371
334
  lead_task_output = task_output
372
335
 
373
336
  task_outputs.append(task_output)
374
- # self._process_task_result(task, task_output)
375
337
  task._store_execution_log(task_index, was_replayed, self._inputs)
376
338
 
377
339
 
378
340
  if futures:
379
341
  task_outputs = self._process_async_tasks(futures, was_replayed)
380
342
 
381
- return self._create_team_output(task_outputs, lead_task_output)
343
+ if not task_outputs:
344
+ self._logger.log(level="error", message="Missing task outcomes. Failed to launch the task.", color="red")
345
+ raise ValueError("Failed to launch tasks")
346
+
347
+ final_task_output = lead_task_output if lead_task_output is not None else task_outputs[0] #! REFINEME
348
+
349
+ # token_usage = self._calculate_usage_metrics() #! combining with Eval
350
+
351
+ return TeamOutput(
352
+ team_id=self.id,
353
+ raw=final_task_output.raw,
354
+ json_dict=final_task_output.json_dict,
355
+ pydantic=final_task_output.pydantic,
356
+ task_outputs=task_outputs,
357
+ # token_usage=token_usage,
358
+ )
359
+
382
360
 
383
361
 
384
362
  def launch(self, kwargs_pre: Optional[Dict[str, str]] = None, kwargs_post: Optional[Dict[str, Any]] = None) -> TeamOutput:
@@ -412,7 +390,7 @@ class Team(BaseModel):
412
390
  agent.callbacks.append(self.step_callback)
413
391
 
414
392
  if self.process is None:
415
- self.process = TaskHandlingProcess.sequential
393
+ self.process = TaskHandlingProcess.SEQUENT
416
394
 
417
395
  result = self._execute_tasks(self.tasks)
418
396
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: versionhq
3
- Version: 1.2.0.4
3
+ Version: 1.2.1.0
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
@@ -81,10 +81,10 @@ Requires-Dist: pygraphviz>=1.14; extra == "pygraphviz"
81
81
 
82
82
  # Overview
83
83
 
84
- [![DL](https://img.shields.io/badge/Download-15K+-red)](https://clickpy.clickhouse.com/dashboard/versionhq)
84
+ [![DL](https://img.shields.io/badge/Download-17K+-red)](https://clickpy.clickhouse.com/dashboard/versionhq)
85
85
  ![MIT license](https://img.shields.io/badge/License-MIT-green)
86
86
  [![Publisher](https://github.com/versionHQ/multi-agent-system/actions/workflows/publish.yml/badge.svg)](https://github.com/versionHQ/multi-agent-system/actions/workflows/publish.yml)
87
- ![PyPI](https://img.shields.io/badge/PyPI-v1.2.0+-blue)
87
+ ![PyPI](https://img.shields.io/badge/PyPI-v1.2.1+-blue)
88
88
  ![python ver](https://img.shields.io/badge/Python-3.11/3.12-purple)
89
89
  ![pyenv ver](https://img.shields.io/badge/pyenv-2.5.0-orange)
90
90
 
@@ -106,9 +106,10 @@ A Python framework for agentic orchestration that handles complex task automatio
106
106
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
107
107
 
108
108
  - [Key Features](#key-features)
109
- - [Agent formation](#agent-formation)
109
+ - [Agent Network](#agent-network)
110
110
  - [Graph Theory Concept](#graph-theory-concept)
111
- - [Agent optimization](#agent-optimization)
111
+ - [Task Graph](#task-graph)
112
+ - [Optimization](#optimization)
112
113
  - [Quick Start](#quick-start)
113
114
  - [Package installation](#package-installation)
114
115
  - [Forming a agent network](#forming-a-agent-network)
@@ -139,14 +140,14 @@ A Python framework for agentic orchestration that handles complex task automatio
139
140
  Agents are model-agnostic, and will improve task output, while oprimizing token cost and job latency, by sharing their memory, knowledge base, and RAG tools with other agents in the network.
140
141
 
141
142
 
142
- ### Agent formation
143
+ ### Agent Network
143
144
 
144
145
  Agents adapt their formation based on task complexity.
145
146
 
146
147
  You can specify a desired formation or allow the agents to determine it autonomously (default).
147
148
 
148
149
 
149
- | | **Solo Agent** | **Supervising** | **Network** | **Random** |
150
+ | | **Solo Agent** | **Supervising** | **Squad** | **Random** |
150
151
  | :--- | :--- | :--- | :--- | :--- |
151
152
  | **Formation** | <img src="https://res.cloudinary.com/dfeirxlea/image/upload/v1738818211/pj_m_agents/rbgxttfoeqqis1ettlfz.png" alt="solo" width="200"> | <img src="https://res.cloudinary.com/dfeirxlea/image/upload/v1738818211/pj_m_agents/zhungor3elxzer5dum10.png" alt="solo" width="200"> | <img src="https://res.cloudinary.com/dfeirxlea/image/upload/v1738818211/pj_m_agents/dnusl7iy7kiwkxwlpmg8.png" alt="solo" width="200"> | <img src="https://res.cloudinary.com/dfeirxlea/image/upload/v1738818211/pj_m_agents/sndpczatfzbrosxz9ama.png" alt="solo" width="200"> |
152
153
  | **Usage** | <ul><li>A single agent with tools, knowledge, and memory.</li><li>When self-learning mode is on - it will turn into **Random** formation.</li></ul> | <ul><li>Leader agent gives directions, while sharing its knowledge and memory.</li><li>Subordinates can be solo agents or networks.</li></ul> | <ul><li>Share tasks, knowledge, and memory among network members.</li></ul> | <ul><li>A single agent handles tasks, asking help from other agents without sharing its memory or knowledge.</li></ul> |
@@ -192,7 +193,19 @@ task_graph.visualize()
192
193
 
193
194
  <hr />
194
195
 
195
- ### Agent optimization
196
+ ### Task Graph
197
+
198
+ A `TaskGraph` represents tasks as `nodes` and their execution dependencies as `edges`, automating rule-based execution.
199
+
200
+ `Agent Networks` can handle `TaskGraph` objects by optimizing their formations.
201
+
202
+ The following example demonstrates a simple concept of a `supervising` agent network handling a task graph with three tasks and one critical edge.
203
+
204
+ <img src="https://res.cloudinary.com/dfeirxlea/image/upload/v1739337639/pj_m_home/zfg4ccw1m1ww1tpnb0pa.png">
205
+
206
+ <hr />
207
+
208
+ ### Optimization
196
209
 
197
210
  Agents are model-agnostic and can handle multiple tasks, leveraging their own and their peers' knowledge sources, memories, and tools.
198
211
 
@@ -1,7 +1,7 @@
1
- versionhq/__init__.py,sha256=9ZKO5IoKeFB9BaNAMZKDRLKf1xGAA_VSpgnMm1xc4GE,2783
1
+ versionhq/__init__.py,sha256=OvDVdnABXYcdjKCYL58tOw2duy59rQ2lxc7e1AoxUjc,2781
2
2
  versionhq/_utils/__init__.py,sha256=dzoZr4cBlh-2QZuPzTdehPUCe9lP1dmRtauD7qTjUaA,158
3
3
  versionhq/_utils/i18n.py,sha256=TwA_PnYfDLA6VqlUDPuybdV9lgi3Frh_ASsb_X8jJo8,1483
4
- versionhq/_utils/logger.py,sha256=j9SlQPIefdVUlwpGfJY83E2BUt1ejWgZ2M2I8aMyQ3c,1579
4
+ versionhq/_utils/logger.py,sha256=2qkODR6y8ApOoMjQZVecTboXvCLrMy2v_mTSOnNMKFY,1581
5
5
  versionhq/_utils/process_config.py,sha256=jbPGXK2Kb4iyCugJ3FwRJuU0wL5Trq2x4xFQz2uOyFY,746
6
6
  versionhq/_utils/usage_metrics.py,sha256=NXF18dn5NNvGK7EsQ4AAghpR8ppYOjMx6ABenLLHnmM,1066
7
7
  versionhq/_utils/vars.py,sha256=bZ5Dx_bFKlt3hi4-NNGXqdk7B23If_WaTIju2fiTyPQ,57
@@ -20,6 +20,8 @@ versionhq/clients/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
20
20
  versionhq/clients/product/model.py,sha256=3w__pug9XRe4LIm9wX8C8WKqi40r081Eb1q2vWk9UaU,3694
21
21
  versionhq/clients/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  versionhq/clients/workflow/model.py,sha256=FNftenLLoha0bkivrjId32awLHAkBwIT8iNljdic_bw,6003
23
+ versionhq/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ versionhq/graph/model.py,sha256=k4NbNn0D3r3ujfkQHOsfiiEJRxZx7iK5DczBnOyn1ik,23912
23
25
  versionhq/knowledge/__init__.py,sha256=qW7IgssTA4_bFFV9ziOcYRfGjlq1c8bkb-HnfWknpuQ,567
24
26
  versionhq/knowledge/_utils.py,sha256=YWRF8U533cfZes_gZqUvdj-K24MD2ri1R0gjc_aPYyc,402
25
27
  versionhq/knowledge/embedding.py,sha256=KfHc__1THxb5jrg1EMrF-v944RDuIr2hE0l-MtM3Bp0,6826
@@ -33,8 +35,6 @@ versionhq/llm/model.py,sha256=wlzDUMEyIOm808d1vzqu9gmbB4ch-s_EUvwFR60gR80,17177
33
35
  versionhq/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
36
  versionhq/memory/contextual_memory.py,sha256=tCsOOAUnfrOL7YiakqGoi3uShzzS870TmGnlGd3z_A4,3556
35
37
  versionhq/memory/model.py,sha256=4wow2O3UuMZ0AbC2NyxddGZac3-_GjNZbK9wsA015NA,8145
36
- versionhq/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- versionhq/network/model.py,sha256=fKKMTviIS2uOwLYT_bGN_2-4R0gmnPB8x-dd05k5jFE,19700
38
38
  versionhq/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  versionhq/storage/base.py,sha256=p-Jas0fXQan_qotnRD6seQxrT2lj-uw9-SmHQhdppcs,355
40
40
  versionhq/storage/ltm_sqlite_storage.py,sha256=wdUiuwHfJocdk0UGqyrdU4S5Nae1rgsoRNu3LWmGFcI,3951
@@ -44,14 +44,14 @@ 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=iHhW2webMirYC78G8kHpaMTYjCdq7c3yFD2u2Egd8Eo,6350
47
+ versionhq/task/formation.py,sha256=8YQxGZ7cs7Q-a7UnwheHxXei9LSt1AWpCQmQiN1ZkgI,6423
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=zAnZ-_5YKZRs43gpM2AL9IVtttq5GqGfqfCMj_Oyz9g,30624
50
+ versionhq/task/model.py,sha256=aXsFRhpmIyfoYpF8BU2cBU4nh4OsaSxc9D98DJ9auqE,30624
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/team/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- versionhq/team/model.py,sha256=kxrjF3RFGO9l3IsJIffI1nqgl_Vh5RLuhqrwIqbEoBY,19214
54
+ versionhq/team/model.py,sha256=88l2lxlAZGIauRJr607IMbV7jP3FIvveVHrT7O4AVpk,18614
55
55
  versionhq/team/team_planner.py,sha256=Zc3zvhPR7T0O8RQoq9CKOrvrll4klemY1rONLFeJ96M,3663
56
56
  versionhq/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  versionhq/tool/cache_handler.py,sha256=iL8FH7X0G-cdT0uhJwzuhLDaadTXOdfybZcDy151-es,1085
@@ -60,8 +60,8 @@ versionhq/tool/composio_tool_vars.py,sha256=FvBuEXsOQUYnN7RTFxT20kAkiEYkxWKkiVtg
60
60
  versionhq/tool/decorator.py,sha256=C4ZM7Xi2gwtEMaSeRo-geo_g_MAkY77WkSLkAuY0AyI,1205
61
61
  versionhq/tool/model.py,sha256=PO4zNWBZcJhYVur381YL1dy6zqurio2jWjtbxOxZMGI,12194
62
62
  versionhq/tool/tool_handler.py,sha256=2m41K8qo5bGCCbwMFferEjT-XZ-mE9F0mDUOBkgivOI,1416
63
- versionhq-1.2.0.4.dist-info/LICENSE,sha256=cRoGGdM73IiDs6nDWKqPlgSv7aR4n-qBXYnJlCMHCeE,1082
64
- versionhq-1.2.0.4.dist-info/METADATA,sha256=d0YGN_ulUD-0CLAOAGfyae2FLmY32CxT2RQEQzgjK20,21366
65
- versionhq-1.2.0.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
66
- versionhq-1.2.0.4.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
67
- versionhq-1.2.0.4.dist-info/RECORD,,
63
+ versionhq-1.2.1.0.dist-info/LICENSE,sha256=cRoGGdM73IiDs6nDWKqPlgSv7aR4n-qBXYnJlCMHCeE,1082
64
+ versionhq-1.2.1.0.dist-info/METADATA,sha256=Ilxj9ZEYzoFzEnzdDE6umFKruSRN3pmcm_D2LvpVi8A,21854
65
+ versionhq-1.2.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
66
+ versionhq-1.2.1.0.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
67
+ versionhq-1.2.1.0.dist-info/RECORD,,
File without changes