versionhq 1.1.12.5__py3-none-any.whl → 1.1.13.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 +21 -3
- versionhq/_utils/usage_metrics.py +6 -6
- versionhq/agent/inhouse_agents.py +1 -1
- versionhq/agent/model.py +90 -38
- versionhq/llm/llm_vars.py +5 -1
- versionhq/llm/model.py +1 -1
- versionhq/network/__init__.py +0 -0
- versionhq/network/model.py +394 -0
- versionhq/task/evaluate.py +5 -6
- versionhq/task/formation.py +64 -30
- versionhq/task/model.py +2 -5
- versionhq/team/model.py +95 -92
- versionhq/team/team_planner.py +5 -6
- {versionhq-1.1.12.5.dist-info → versionhq-1.1.13.1.dist-info}/METADATA +232 -130
- {versionhq-1.1.12.5.dist-info → versionhq-1.1.13.1.dist-info}/RECORD +18 -16
- {versionhq-1.1.12.5.dist-info → versionhq-1.1.13.1.dist-info}/LICENSE +0 -0
- {versionhq-1.1.12.5.dist-info → versionhq-1.1.13.1.dist-info}/WHEEL +0 -0
- {versionhq-1.1.12.5.dist-info → versionhq-1.1.13.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,394 @@
|
|
1
|
+
import enum
|
2
|
+
import uuid
|
3
|
+
from abc import ABC
|
4
|
+
from typing import List, Any, Optional, Callable, Dict, Type
|
5
|
+
|
6
|
+
from pydantic import BaseModel, InstanceOf, Field, UUID4, PrivateAttr, field_validator
|
7
|
+
from pydantic_core import PydanticCustomError
|
8
|
+
|
9
|
+
from versionhq.task.model import Task
|
10
|
+
from versionhq.agent.model import Agent
|
11
|
+
from versionhq._utils.logger import Logger
|
12
|
+
|
13
|
+
|
14
|
+
try:
|
15
|
+
import networkx as ntx
|
16
|
+
except ImportError:
|
17
|
+
try:
|
18
|
+
import os
|
19
|
+
os.system("uv add networkx --optional networkx")
|
20
|
+
import networkx as ntx
|
21
|
+
except:
|
22
|
+
raise ImportError("networkx is not installed. Please install it with: uv add networkx --optional networkx")
|
23
|
+
|
24
|
+
|
25
|
+
try:
|
26
|
+
import matplotlib.pyplot as plt
|
27
|
+
except ImportError:
|
28
|
+
try:
|
29
|
+
import os
|
30
|
+
os.system("uv add matplotlib --optional matplotlib")
|
31
|
+
import matplotlib.pyplot as plt
|
32
|
+
except:
|
33
|
+
raise ImportError("matplotlib is not installed. Please install it with: uv add matplotlib --optional matplotlib")
|
34
|
+
|
35
|
+
import networkx as nx
|
36
|
+
import matplotlib.pyplot as plt
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
class TaskStatus(enum.Enum):
|
41
|
+
"""
|
42
|
+
Enum to track the task execution status
|
43
|
+
"""
|
44
|
+
NOT_STARTED = 1
|
45
|
+
IN_PROGRESS = 2
|
46
|
+
BLOCKED = 3 # task is waiting for its dependant tasks to complete. resumption set as AUTO.
|
47
|
+
COMPLETED = 4
|
48
|
+
DELAYED = 5 # task has begun - but is taking longer than expected duration and behind schedule.
|
49
|
+
ON_HOLD = 6 # task is temporarily & intentionally paused due to external factors and/or decisions. resumption set as DECISION.
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
class DependencyType(enum.Enum):
|
54
|
+
"""
|
55
|
+
Concise enumeration of the edge type.
|
56
|
+
"""
|
57
|
+
|
58
|
+
FINISH_TO_START = "FS" # Task B starts after Task A finishes
|
59
|
+
START_TO_START = "SS" # Task B starts when Task A starts
|
60
|
+
FINISH_TO_FINISH = "FF" # Task B finishes when Task A finishes
|
61
|
+
START_TO_FINISH = "SF" # Task B finishes when Task A starts
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
class TriggerEvent(enum.Enum):
|
66
|
+
"""
|
67
|
+
Concise enumeration of key trigger events for task execution.
|
68
|
+
"""
|
69
|
+
IMMEDIATE = 0 # execute immediately
|
70
|
+
DEPENDENCIES_MET = 1 # All/required dependencies are satisfied
|
71
|
+
RESOURCES_AVAILABLE = 2 # Necessary resources are available
|
72
|
+
SCHEDULED_TIME = 3 # Scheduled start time or time window reached
|
73
|
+
EXTERNAL_EVENT = 4 # Triggered by an external event/message
|
74
|
+
DATA_AVAILABLE = 5 # Required data is available both internal/external
|
75
|
+
APPROVAL_RECEIVED = 6 # Necessary approvals have been granted
|
76
|
+
STATUS_CHANGED = 7 # Relevant task/system status has changed
|
77
|
+
RULE_MET = 8 # A predefined rule or condition has been met
|
78
|
+
MANUAL_TRIGGER = 9 # Manually initiated by a user
|
79
|
+
ERROR_HANDLED = 10 # A previous error/exception has been handled
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
class Node(BaseModel):
|
84
|
+
"""
|
85
|
+
A class to store a node object.
|
86
|
+
"""
|
87
|
+
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
88
|
+
task: InstanceOf[Task] = Field(default=None)
|
89
|
+
trigger_event: TriggerEvent = Field(default=TriggerEvent.IMMEDIATE, description="store trigger event to execute the task")
|
90
|
+
in_degree_nodes: List[Any] = Field(default=None, description="list of Node objects")
|
91
|
+
out_degree_nodes: List[Any] = Field(default=None, description="list of Node objects")
|
92
|
+
assigned_to: InstanceOf[Agent] = Field(default=None)
|
93
|
+
status: TaskStatus = Field(default=TaskStatus.NOT_STARTED)
|
94
|
+
|
95
|
+
|
96
|
+
@field_validator("id", mode="before")
|
97
|
+
@classmethod
|
98
|
+
def _deny_id(cls, v: Optional[UUID4]) -> None:
|
99
|
+
if v:
|
100
|
+
raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
|
101
|
+
|
102
|
+
def is_independent(self) -> bool:
|
103
|
+
return not self.in_degree_nodes and not self.out_degree_nodes
|
104
|
+
|
105
|
+
@property
|
106
|
+
def in_degrees(self) -> int:
|
107
|
+
return len(self.in_degree_nodes) if self.in_degree_nodes else 0
|
108
|
+
|
109
|
+
@property
|
110
|
+
def out_degrees(self) -> int:
|
111
|
+
return len(self.out_degree_nodes) if self.out_degree_nodes else 0
|
112
|
+
|
113
|
+
@property
|
114
|
+
def degrees(self) -> int:
|
115
|
+
return self.in_degrees + self.out_degrees
|
116
|
+
|
117
|
+
@property
|
118
|
+
def identifier(self) -> str:
|
119
|
+
"""Unique identifier for the node"""
|
120
|
+
return f"{str(self.id)}"
|
121
|
+
|
122
|
+
def __str__(self):
|
123
|
+
return self.identifier
|
124
|
+
|
125
|
+
|
126
|
+
class Edge(BaseModel):
|
127
|
+
"""
|
128
|
+
A class to store an edge object that connects multiple nodes as dependencies.
|
129
|
+
"""
|
130
|
+
description: Optional[str] = Field(default=None)
|
131
|
+
|
132
|
+
type: DependencyType = Field(default=DependencyType.FINISH_TO_START)
|
133
|
+
weight: Optional[float] = Field(default=None, description="duration or weight of the dependency: 1 light 10 heavy")
|
134
|
+
lag: Optional[float] = Field(default=None, description="lag time for the dependency to be executed")
|
135
|
+
constraint: Optional[str] = Field(default=None, description="constraint to consider executing the dependency")
|
136
|
+
priority: Optional[int] = Field(default=None, description="priority of the dependency if multiple depencencies are given")
|
137
|
+
|
138
|
+
data_transfer: bool = Field(True, description="whether the data transfer is required")
|
139
|
+
data_format: Optional[str] = Field(default=None, description="Format of data transfer")
|
140
|
+
|
141
|
+
# execution logic
|
142
|
+
required: bool = Field(True, description="whether the execution of the dependency is required - False = conditional, optional task")
|
143
|
+
condition: Optional[Callable] = Field(default=None, description="conditional function to start executing the dependency")
|
144
|
+
|
145
|
+
|
146
|
+
def is_dependency_met(self, predecessor_node: Node = None) -> bool:
|
147
|
+
"""
|
148
|
+
Define if the dependency is ready to execute:
|
149
|
+
|
150
|
+
required condition Dependency Met? Dependent Task Can Start?
|
151
|
+
True Not Given Predecessor task finished Yes (if other deps met)
|
152
|
+
True Given Predecessor task finished and condition True Yes (if other deps met)
|
153
|
+
False Not Given Always (regardless of predecessor status) Yes (if other deps met)
|
154
|
+
False Given Condition True (predecessor status irrelevant) Yes (if other deps met)
|
155
|
+
"""
|
156
|
+
|
157
|
+
if self.required:
|
158
|
+
if predecessor_node.status == TaskStatus.COMPLETED:
|
159
|
+
return self.condition() if self.condtion else True
|
160
|
+
else:
|
161
|
+
return False
|
162
|
+
else:
|
163
|
+
return self.condition() if self.condition else True
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
class Graph(ABC, BaseModel):
|
168
|
+
"""
|
169
|
+
An abstract class to store G using NetworkX library.
|
170
|
+
"""
|
171
|
+
|
172
|
+
_logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
|
173
|
+
directed: bool = Field(default=False, description="Whether the graph is directed")
|
174
|
+
graph: Type[nx.Graph] = Field(default=None)
|
175
|
+
nodes: Dict[str, InstanceOf[Node]] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
|
176
|
+
edges: Dict[str, InstanceOf[Edge]] = Field(default_factory=dict)
|
177
|
+
|
178
|
+
def __init__(self, directed: bool = False, **kwargs):
|
179
|
+
|
180
|
+
super().__init__(directed=directed, **kwargs)
|
181
|
+
self.graph = nx.DiGraph() if self.directed else nx.Graph()
|
182
|
+
|
183
|
+
def add_node(self, node: Node) -> None:
|
184
|
+
self.graph.add_node(node.identifier, **node.model_dump())
|
185
|
+
self.nodes[node.identifier] = node
|
186
|
+
|
187
|
+
def add_edge(self, source: str, target: str, edge: Edge) -> None:
|
188
|
+
self.graph.add_edge(source, target, **edge.model_dump())
|
189
|
+
self.edges[(source, target)] = edge
|
190
|
+
|
191
|
+
def add_weighted_edges_from(self, edges):
|
192
|
+
self.graph.add_weighted_edges_from(edges)
|
193
|
+
|
194
|
+
def get_neighbors(self, node: Node) -> List[Node]:
|
195
|
+
return list(self.graph.neighbors(node))
|
196
|
+
|
197
|
+
def get_in_degree(self, node: Node) -> int:
|
198
|
+
return self.graph.in_degree(node)
|
199
|
+
|
200
|
+
def get_out_degree(self, node: Node) -> int:
|
201
|
+
return self.graph.out_degree(node)
|
202
|
+
|
203
|
+
def find_path(self, source: str, target: str, weight: Any) -> Any:
|
204
|
+
try:
|
205
|
+
return nx.shortest_path(self.graph, source=source, target=target, weight=weight)
|
206
|
+
except nx.NetworkXNoPath:
|
207
|
+
return None
|
208
|
+
|
209
|
+
def find_all_paths(self, source: str, target: str) -> List[Any]:
|
210
|
+
return list(nx.all_simple_paths(self.graph, source=source, target=target))
|
211
|
+
|
212
|
+
|
213
|
+
def find_critical_path(self) -> tuple[List[Any], int, Dict[str, int]]:
|
214
|
+
"""
|
215
|
+
Finds the critical path in the graph.
|
216
|
+
Returns:
|
217
|
+
A tuple containing:
|
218
|
+
- The critical path (a list of task names).
|
219
|
+
- The duration of the critical path.
|
220
|
+
- A dictionary of all paths and their durations.
|
221
|
+
"""
|
222
|
+
|
223
|
+
all_paths = {}
|
224
|
+
for start_node in (v for k, v in self.nodes.items() if v.in_degrees == 0): # Start from nodes with 0 in-degree
|
225
|
+
for end_node in (v for k, v in self.nodes.items() if v.out_degrees == 0): # End at nodes with 0 out-degree
|
226
|
+
for edge in nx.all_simple_paths(self.graph, source=start_node.identifier, target=end_node.identifier):
|
227
|
+
edge_weight = sum(self.edges.get(item).weight if self.edges.get(item) else 0 for item in edge)
|
228
|
+
all_paths[tuple(edge)] = edge_weight
|
229
|
+
|
230
|
+
if not all_paths:
|
231
|
+
return [], 0, all_paths
|
232
|
+
|
233
|
+
critical_path = max(all_paths, key=all_paths.get)
|
234
|
+
critical_duration = all_paths[critical_path]
|
235
|
+
|
236
|
+
return list(critical_path), critical_duration, all_paths
|
237
|
+
|
238
|
+
|
239
|
+
def is_circled(self, node: Node) -> bool:
|
240
|
+
"""Check if there's a path from the node to itself and return bool."""
|
241
|
+
try:
|
242
|
+
path = nx.shortest_path(self.graph, source=node, target=node)
|
243
|
+
return True if path else False
|
244
|
+
except nx.NetworkXNoPath:
|
245
|
+
return False
|
246
|
+
|
247
|
+
|
248
|
+
def visualize(self, title: str = "Graph Visualization", pos: Any = None, **graph_config):
|
249
|
+
pos = pos if pos else nx.spring_layout(self.graph, seed=42)
|
250
|
+
nx.draw(
|
251
|
+
self.graph,
|
252
|
+
pos,
|
253
|
+
with_labels=True, node_size=700, node_color="skyblue", font_size=10, font_color="black", arrowstyle='-|>', arrowsize=20, arrows=True,
|
254
|
+
**graph_config
|
255
|
+
)
|
256
|
+
edge_labels = {}
|
257
|
+
for u, v, data in self.graph.edges(data=True):
|
258
|
+
edge = self.edges.get((u,v))
|
259
|
+
if edge:
|
260
|
+
label_parts = []
|
261
|
+
if edge.type:
|
262
|
+
label_parts.append(f"Type: {edge.type}")
|
263
|
+
if edge.duration is not None:
|
264
|
+
label_parts.append(f"Duration: {edge.duration}")
|
265
|
+
if edge.lag is not None:
|
266
|
+
label_parts.append(f"Lag: {edge.lag}")
|
267
|
+
edge_labels[(u, v)] = "\n".join(label_parts) # Combine labels with newlines
|
268
|
+
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
|
269
|
+
plt.title(title)
|
270
|
+
plt.show()
|
271
|
+
|
272
|
+
|
273
|
+
|
274
|
+
class TaskGraph(Graph):
|
275
|
+
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
276
|
+
should_reform: bool = Field(default=False)
|
277
|
+
status: Dict[str, TaskStatus] = Field(default_factory=dict, description="store identifier (str) and TaskStatus of all task_nodes")
|
278
|
+
|
279
|
+
|
280
|
+
def add_task(self, task: Node | Task) -> Node:
|
281
|
+
"""Convert `task` to a Node object and add it to G"""
|
282
|
+
task_node = task if isinstance(task, Node) else Node(task=task)
|
283
|
+
self.add_node(task_node)
|
284
|
+
self.status[task_node.identifier] = TaskStatus.NOT_STARTED
|
285
|
+
return task_node
|
286
|
+
|
287
|
+
|
288
|
+
def add_dependency(
|
289
|
+
self, source_task_node_identifier: str, target_task_node_identifier: str, **edge_attributes
|
290
|
+
) -> None:
|
291
|
+
"""
|
292
|
+
Add an edge that connect task 1 (source) and task 2 (target) using task_node.name as an identifier
|
293
|
+
"""
|
294
|
+
|
295
|
+
if not edge_attributes:
|
296
|
+
self._logger.log(level="error", message="Edge attributes are missing.", color="red")
|
297
|
+
|
298
|
+
edge = Edge()
|
299
|
+
for k in Edge.model_fields.keys():
|
300
|
+
v = edge_attributes.get(k, None)
|
301
|
+
if v:
|
302
|
+
setattr(edge, k, v)
|
303
|
+
else:
|
304
|
+
pass
|
305
|
+
|
306
|
+
self.add_edge(source_task_node_identifier, target_task_node_identifier, edge)
|
307
|
+
|
308
|
+
|
309
|
+
def set_task_status(self, identifier: str, status: TaskStatus) -> None:
|
310
|
+
if identifier in self.status:
|
311
|
+
self.status[identifier] = status
|
312
|
+
else:
|
313
|
+
self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
|
314
|
+
pass
|
315
|
+
|
316
|
+
def get_task_status(self, identifier):
|
317
|
+
if identifier in self.status:
|
318
|
+
return self.status[identifier]
|
319
|
+
else:
|
320
|
+
self._logger.log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
|
321
|
+
return None
|
322
|
+
|
323
|
+
|
324
|
+
def visualize(self, layout: str = None):
|
325
|
+
try:
|
326
|
+
pos = nx.drawing.nx_agraph.graphviz_layout(self.graph, prog='dot') # 'dot', 'neato', 'fdp', 'sfdp'
|
327
|
+
except ImportError:
|
328
|
+
pos = nx.spring_layout(self.graph, seed=42) # REFINEME - layout
|
329
|
+
|
330
|
+
node_colors = list()
|
331
|
+
for k, v in self.graph.nodes.items():
|
332
|
+
status = self.get_task_status(identifier=k)
|
333
|
+
if status == TaskStatus.NOT_STARTED:
|
334
|
+
node_colors.append("skyblue")
|
335
|
+
elif status == TaskStatus.IN_PROGRESS:
|
336
|
+
node_colors.append("lightgreen")
|
337
|
+
elif status == TaskStatus.BLOCKED:
|
338
|
+
node_colors.append("lightcoral")
|
339
|
+
elif status == TaskStatus.COMPLETED:
|
340
|
+
node_colors.append("black")
|
341
|
+
elif status == TaskStatus.DELAYED:
|
342
|
+
node_colors.append("orange")
|
343
|
+
elif status == TaskStatus.ON_HOLD:
|
344
|
+
node_colors.append("yellow")
|
345
|
+
else:
|
346
|
+
node_colors.append("grey")
|
347
|
+
|
348
|
+
critical_paths, duration, paths = self.find_critical_path()
|
349
|
+
edge_colors = ['red' if (u, v) in zip(critical_paths, critical_paths[1:]) else 'black' for u, v in self.graph.edges()]
|
350
|
+
edge_widths = []
|
351
|
+
|
352
|
+
for k, v in self.edges.items():
|
353
|
+
# edge_weights = nx.get_edge_attributes(self.graph, 'weight')
|
354
|
+
# edge_colors.append(plt.cm.viridis(v.weight / max(edge_weights.values())))
|
355
|
+
edge_widths.append(v.weight * 0.5) # Width proportional to weight (adjust scaling as needed)
|
356
|
+
|
357
|
+
nx.draw(
|
358
|
+
self.graph, pos,
|
359
|
+
with_labels=True,
|
360
|
+
node_size=700,
|
361
|
+
node_color=node_colors,
|
362
|
+
font_size=10,
|
363
|
+
font_color="black",
|
364
|
+
edge_color=edge_colors,
|
365
|
+
width=edge_widths,
|
366
|
+
arrows=True,
|
367
|
+
arrowsize=20,
|
368
|
+
arrowstyle='-|>'
|
369
|
+
)
|
370
|
+
|
371
|
+
edge_labels = nx.get_edge_attributes(G=self.graph, name="edges")
|
372
|
+
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
|
373
|
+
|
374
|
+
plt.title("Project Network Diagram")
|
375
|
+
self._save()
|
376
|
+
plt.show()
|
377
|
+
|
378
|
+
|
379
|
+
def _save(self, abs_file_path: str = None) -> None:
|
380
|
+
"""
|
381
|
+
Save the graph image in the local directory.
|
382
|
+
"""
|
383
|
+
|
384
|
+
try:
|
385
|
+
import os
|
386
|
+
project_root = os.path.abspath(os.getcwd())
|
387
|
+
abs_file_path = abs_file_path if abs_file_path else f"{project_root}/uploads"
|
388
|
+
|
389
|
+
os.makedirs(abs_file_path, exist_ok=True)
|
390
|
+
|
391
|
+
plt.savefig(f"{abs_file_path}/{str(self.id)}.png")
|
392
|
+
|
393
|
+
except Exception as e:
|
394
|
+
self._logger.log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
|
versionhq/task/evaluate.py
CHANGED
@@ -70,17 +70,16 @@ class EvaluationItem(BaseModel):
|
|
70
70
|
else: return None
|
71
71
|
|
72
72
|
|
73
|
-
|
74
73
|
class Evaluation(BaseModel):
|
75
74
|
items: List[EvaluationItem] = []
|
76
|
-
latency: int = Field(default=None, description="seconds")
|
75
|
+
latency: int = Field(default=None, description="job execution latency in seconds")
|
77
76
|
tokens: int = Field(default=None, description="tokens consumed")
|
78
|
-
|
77
|
+
eval_by: Any = Field(default=None, description="stores agent object that evaluates the outcome")
|
79
78
|
|
80
79
|
@model_validator(mode="after")
|
81
|
-
def
|
80
|
+
def set_up_evaluator(self) -> Self:
|
82
81
|
from versionhq.agent.inhouse_agents import vhq_task_evaluator
|
83
|
-
self.
|
82
|
+
self.eval_by = vhq_task_evaluator
|
84
83
|
return self
|
85
84
|
|
86
85
|
|
@@ -88,7 +87,7 @@ class Evaluation(BaseModel):
|
|
88
87
|
"""
|
89
88
|
Create and store evaluation results in the memory metadata
|
90
89
|
"""
|
91
|
-
eval_by = self.
|
90
|
+
eval_by = self.eval_by.role if self.eval_by else None
|
92
91
|
score = self.aggregate_score
|
93
92
|
eval_criteria = ", ".join([item.criteria for item in self.items]) if self.items else None
|
94
93
|
suggestion = self.suggestion_summary
|
versionhq/task/formation.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
from typing import List
|
1
|
+
from typing import List, Type
|
2
2
|
from enum import Enum
|
3
3
|
|
4
4
|
from pydantic import BaseModel
|
5
5
|
|
6
6
|
from versionhq.task.model import Task
|
7
7
|
from versionhq.agent.model import Agent
|
8
|
-
from versionhq.team.model import Team,
|
8
|
+
from versionhq.team.model import Team, Member, Formation
|
9
9
|
from versionhq.agent.inhouse_agents import vhq_formation_planner
|
10
10
|
from versionhq._utils import Logger
|
11
11
|
|
@@ -15,10 +15,10 @@ def form_agent_network(
|
|
15
15
|
expected_outcome: str,
|
16
16
|
agents: List[Agent] = None,
|
17
17
|
context: str = None,
|
18
|
-
formation: Formation = None
|
18
|
+
formation: Type[Formation] = None
|
19
19
|
) -> Team | None:
|
20
20
|
"""
|
21
|
-
Make a formation of agents from the given task description,
|
21
|
+
Make a formation of agents from the given task description, expected outcome, agents (optional), and context (optional).
|
22
22
|
"""
|
23
23
|
|
24
24
|
if not task:
|
@@ -29,8 +29,37 @@ def form_agent_network(
|
|
29
29
|
Logger(verbose=True).log(level="error", message="Missing expected outcome.", color="red")
|
30
30
|
return None
|
31
31
|
|
32
|
+
if formation:
|
33
|
+
try:
|
34
|
+
match formation:
|
35
|
+
case Formation():
|
36
|
+
if formation == Formation.UNDEFINED:
|
37
|
+
formation = None
|
38
|
+
else:
|
39
|
+
pass
|
40
|
+
|
41
|
+
case str():
|
42
|
+
matched = [item for item in Formation._member_names_ if item == formation.upper()]
|
43
|
+
if matched:
|
44
|
+
formation = getattr(Formation, matched[0])
|
45
|
+
else:
|
46
|
+
# Formation._generate_next_value_(name=f"CUSTOM_{formation.upper()}", start=100, count=6, last_values=Formation.HYBRID.name)
|
47
|
+
Logger(verbose=True).log(level="warning", message=f"The formation {formation} is invalid. We'll recreate a valid formation.", color="yellow")
|
48
|
+
formation = None
|
49
|
+
|
50
|
+
case int() | float():
|
51
|
+
formation = Formation(int(formation))
|
52
|
+
|
53
|
+
case _:
|
54
|
+
Logger(verbose=True).log(level="warning", message=f"The formation {formation} is invalid. We'll recreate a valid formation.", color="yellow")
|
55
|
+
formation = None
|
56
|
+
|
57
|
+
except Exception as e:
|
58
|
+
Logger(verbose=True).log(level="warning", message=f"The formation {formation} is invalid: {str(e)}. We'll recreate a formation.", color="yellow")
|
59
|
+
formation = None
|
32
60
|
|
33
61
|
try:
|
62
|
+
prompt_formation = formation.name if formation and isinstance(formation, Formation) else f"Select the best formation to effectively execute the tasks from the given Enum sets: {str(Formation.__dict__)}."
|
34
63
|
class Outcome(BaseModel):
|
35
64
|
formation: Enum
|
36
65
|
agent_roles: list[str]
|
@@ -42,73 +71,78 @@ def form_agent_network(
|
|
42
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.
|
43
72
|
Task: {str(task)}
|
44
73
|
Expected outcome: {str(expected_outcome)}
|
74
|
+
Formation: {prompt_formation}
|
45
75
|
""",
|
46
76
|
pydantic_output=Outcome
|
47
77
|
)
|
48
78
|
|
49
|
-
if formation:
|
50
|
-
vhq_task.description += f"Select 1 formation you think the best from the given Enum sets: {str(Formation.__dict__)}"
|
51
|
-
|
52
79
|
if agents:
|
53
80
|
vhq_task.description += "Consider adding following agents in the formation: " + ", ".join([agent.role for agent in agents if isinstance(agent, Agent)])
|
54
81
|
|
55
82
|
res = vhq_task.execute_sync(agent=vhq_formation_planner, context=context)
|
56
|
-
|
83
|
+
_formation = Formation.SUPERVISING
|
57
84
|
|
58
85
|
if res.pydantic:
|
59
86
|
formation_keys = [k for k, v in Formation._member_map_.items() if k == res.pydantic.formation.upper()]
|
60
87
|
|
61
88
|
if formation_keys:
|
62
|
-
|
89
|
+
_formation = Formation[formation_keys[0]]
|
63
90
|
|
64
91
|
created_agents = [Agent(role=item, goal=item) for item in res.pydantic.agent_roles]
|
65
92
|
created_tasks = [Task(description=item) for item in res.pydantic.task_descriptions]
|
93
|
+
|
66
94
|
team_tasks = []
|
67
95
|
members = []
|
68
96
|
leader = str(res.pydantic.leader_agent)
|
69
97
|
|
70
98
|
for i in range(len(created_agents)):
|
71
|
-
is_manager = bool(created_agents[i].role.lower()
|
72
|
-
member =
|
99
|
+
is_manager = bool(created_agents[i].role.lower() == leader.lower())
|
100
|
+
member = Member(agent=created_agents[i], is_manager=is_manager)
|
101
|
+
|
102
|
+
if len(created_tasks) >= i and created_tasks[i]:
|
103
|
+
member.tasks.append(created_tasks[i])
|
104
|
+
|
105
|
+
members.append(member)
|
73
106
|
|
74
|
-
if len(created_tasks) >= i:
|
75
|
-
member.task = created_tasks[i]
|
76
|
-
members.append(member)
|
77
107
|
|
78
108
|
if len(created_agents) < len(created_tasks):
|
79
|
-
team_tasks.extend(created_tasks[len(created_agents)
|
109
|
+
team_tasks.extend(created_tasks[len(created_agents):len(created_tasks)])
|
80
110
|
|
81
111
|
members.sort(key=lambda x: x.is_manager == False)
|
82
|
-
team = Team(members=members, formation=
|
112
|
+
team = Team( members=members, formation=_formation, team_tasks=team_tasks, planner_llm=vhq_formation_planner.llm)
|
83
113
|
return team
|
84
114
|
|
85
115
|
else:
|
86
|
-
|
116
|
+
res = res.json_dict
|
117
|
+
formation_keys = [k for k, v in Formation._member_map_.items() if k == res["formation"].upper()]
|
87
118
|
|
88
119
|
if formation_keys:
|
89
|
-
|
120
|
+
_formation = Formation[formation_keys[0]]
|
121
|
+
|
122
|
+
created_agents = [Agent(role=item, goal=item) for item in res["agent_roles"]]
|
123
|
+
created_tasks = [Task(description=item) for item in res["task_descriptions"]]
|
90
124
|
|
91
|
-
created_agents = [Agent(role=item, goal=item) for item in res.json_dict["agent_roles"]]
|
92
|
-
created_tasks = [Task(description=item) for item in res.json_dict["task_descriptions"]]
|
93
125
|
team_tasks = []
|
94
126
|
members = []
|
95
|
-
leader = str(res
|
127
|
+
leader = str(res["leader_agent"])
|
96
128
|
|
97
129
|
for i in range(len(created_agents)):
|
98
|
-
is_manager = bool(created_agents[i].role.lower()
|
99
|
-
member =
|
130
|
+
is_manager = bool(created_agents[i].role.lower() == leader.lower())
|
131
|
+
member = Member(agent=created_agents[i], is_manager=is_manager)
|
132
|
+
|
133
|
+
if len(created_tasks) >= i and created_tasks[i]:
|
134
|
+
member.tasks.append(created_tasks[i])
|
100
135
|
|
101
|
-
|
102
|
-
member.task = created_tasks[i]
|
103
|
-
members.append(member)
|
136
|
+
members.append(member)
|
104
137
|
|
105
138
|
if len(created_agents) < len(created_tasks):
|
106
|
-
team_tasks.extend(created_tasks[len(created_agents)
|
139
|
+
team_tasks.extend(created_tasks[len(created_agents):len(created_tasks)])
|
107
140
|
|
108
|
-
members.sort(key=lambda x: x.is_manager ==
|
109
|
-
team = Team(members=members, formation=
|
141
|
+
members.sort(key=lambda x: x.is_manager == False)
|
142
|
+
team = Team( members=members, formation=_formation, team_tasks=team_tasks, planner_llm=vhq_formation_planner.llm)
|
110
143
|
return team
|
111
144
|
|
145
|
+
|
112
146
|
except Exception as e:
|
113
|
-
Logger(verbose=True).log(level="error", message=f"Failed to create
|
147
|
+
Logger(verbose=True).log(level="error", message=f"Failed to create a agent network - return None. You can try with solo agent. Error: {str(e)}", color="red")
|
114
148
|
return None
|
versionhq/task/model.py
CHANGED
@@ -203,7 +203,7 @@ class TaskOutput(BaseModel):
|
|
203
203
|
description=EVALUATE.format(task_description=task.description, task_output=self.raw, eval_criteria=str(item)),
|
204
204
|
pydantic_output=EvaluationItem
|
205
205
|
)
|
206
|
-
res = task_eval.execute_sync(agent=self.evaluation.
|
206
|
+
res = task_eval.execute_sync(agent=self.evaluation.eval_by)
|
207
207
|
|
208
208
|
if res.pydantic:
|
209
209
|
item = EvaluationItem(score=res.pydantic.score, suggestion=res.pydantic.suggestion, criteria=res.pydantic.criteria)
|
@@ -241,10 +241,7 @@ class TaskOutput(BaseModel):
|
|
241
241
|
|
242
242
|
class Task(BaseModel):
|
243
243
|
"""
|
244
|
-
|
245
|
-
Each task must have a description.
|
246
|
-
Default response is JSON string that strictly follows `response_fields` - and will be stored in TaskOuput.raw / json_dict.
|
247
|
-
When `pydantic_output` is provided, we prioritize them and store raw (json string), json_dict, pydantic in the TaskOutput class.
|
244
|
+
A class that stores independent task information.
|
248
245
|
"""
|
249
246
|
|
250
247
|
__hash__ = object.__hash__
|