versionhq 1.1.8__py3-none-any.whl → 1.1.9.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 +3 -1
- versionhq/agent/model.py +1 -1
- versionhq/clients/workflow/model.py +8 -9
- versionhq/task/model.py +76 -74
- versionhq/team/model.py +134 -79
- versionhq/team/team_planner.py +58 -20
- versionhq/tool/__init__.py +53 -0
- versionhq/tool/composio.py +149 -0
- versionhq/tool/decorator.py +3 -1
- versionhq/tool/model.py +129 -104
- versionhq/tool/tool_handler.py +16 -21
- {versionhq-1.1.8.dist-info → versionhq-1.1.9.0.dist-info}/METADATA +2 -2
- {versionhq-1.1.8.dist-info → versionhq-1.1.9.0.dist-info}/RECORD +16 -15
- {versionhq-1.1.8.dist-info → versionhq-1.1.9.0.dist-info}/LICENSE +0 -0
- {versionhq-1.1.8.dist-info → versionhq-1.1.9.0.dist-info}/WHEEL +0 -0
- {versionhq-1.1.8.dist-info → versionhq-1.1.9.0.dist-info}/top_level.txt +0 -0
versionhq/__init__.py
CHANGED
@@ -15,9 +15,10 @@ from versionhq.llm.model import LLM
|
|
15
15
|
from versionhq.task.model import Task, TaskOutput
|
16
16
|
from versionhq.team.model import Team, TeamOutput
|
17
17
|
from versionhq.tool.model import Tool
|
18
|
+
from versionhq.tool.composio import Composio
|
18
19
|
|
19
20
|
|
20
|
-
__version__ = "1.1.
|
21
|
+
__version__ = "1.1.9.0"
|
21
22
|
__all__ = [
|
22
23
|
"Agent",
|
23
24
|
"Customer",
|
@@ -33,4 +34,5 @@ __all__ = [
|
|
33
34
|
"Team",
|
34
35
|
"TeamOutput",
|
35
36
|
"Tool",
|
37
|
+
"Composio"
|
36
38
|
]
|
versionhq/agent/model.py
CHANGED
@@ -15,7 +15,7 @@ from versionhq.llm.llm_vars import LLM_VARS
|
|
15
15
|
from versionhq.llm.model import LLM, DEFAULT_CONTEXT_WINDOW
|
16
16
|
from versionhq.task import TaskOutputFormat
|
17
17
|
from versionhq.task.model import ResponseField
|
18
|
-
from versionhq.tool.model import Tool,
|
18
|
+
from versionhq.tool.model import Tool, ToolSet
|
19
19
|
from versionhq.tool.tool_handler import ToolHandler
|
20
20
|
|
21
21
|
load_dotenv(override=True)
|
@@ -117,15 +117,6 @@ class MessagingWorkflow(ABC, BaseModel):
|
|
117
117
|
description="store metrics that used to predict and track the performance of this workflow."
|
118
118
|
)
|
119
119
|
|
120
|
-
|
121
|
-
@property
|
122
|
-
def name(self):
|
123
|
-
if self.customer.id:
|
124
|
-
return f"Workflow ID: {self.id} - on {self.product.id} for {self.customer.id}"
|
125
|
-
else:
|
126
|
-
return f"Workflow ID: {self.id} - on {self.product.id}"
|
127
|
-
|
128
|
-
|
129
120
|
@field_validator("id", mode="before")
|
130
121
|
@classmethod
|
131
122
|
def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
|
@@ -160,3 +151,11 @@ class MessagingWorkflow(ABC, BaseModel):
|
|
160
151
|
self.agents = agents
|
161
152
|
self.team = team
|
162
153
|
self.updated_at = datetime.datetime.now()
|
154
|
+
|
155
|
+
|
156
|
+
@property
|
157
|
+
def name(self):
|
158
|
+
if self.customer.id:
|
159
|
+
return f"Workflow ID: {self.id} - on {self.product.id} for {self.customer.id}"
|
160
|
+
else:
|
161
|
+
return f"Workflow ID: {self.id} - on {self.product.id}"
|
versionhq/task/model.py
CHANGED
@@ -12,7 +12,7 @@ from pydantic_core import PydanticCustomError
|
|
12
12
|
from versionhq._utils.process_config import process_config
|
13
13
|
from versionhq.task import TaskOutputFormat
|
14
14
|
from versionhq.task.log_handler import TaskOutputStorageHandler
|
15
|
-
from versionhq.tool.model import Tool,
|
15
|
+
from versionhq.tool.model import Tool, ToolSet
|
16
16
|
from versionhq._utils.logger import Logger
|
17
17
|
|
18
18
|
|
@@ -76,18 +76,6 @@ class TaskOutput(BaseModel):
|
|
76
76
|
def __str__(self) -> str:
|
77
77
|
return str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw
|
78
78
|
|
79
|
-
@property
|
80
|
-
def json(self) -> Optional[str]:
|
81
|
-
if self.output_format != TaskOutputFormat.JSON:
|
82
|
-
raise ValueError(
|
83
|
-
"""
|
84
|
-
Invalid output format requested.
|
85
|
-
If you would like to access the JSON output,
|
86
|
-
pleae make sure to set the output_json property for the task
|
87
|
-
"""
|
88
|
-
)
|
89
|
-
return json.dumps(self.json_dict)
|
90
|
-
|
91
79
|
|
92
80
|
def to_dict(self) -> Dict[str, Any]:
|
93
81
|
"""
|
@@ -112,6 +100,19 @@ class TaskOutput(BaseModel):
|
|
112
100
|
return json.dumps(self.json_dict) if self.json_dict else self.raw[0: 127]
|
113
101
|
|
114
102
|
|
103
|
+
@property
|
104
|
+
def json(self) -> Optional[str]:
|
105
|
+
if self.output_format != TaskOutputFormat.JSON:
|
106
|
+
raise ValueError(
|
107
|
+
"""
|
108
|
+
Invalid output format requested.
|
109
|
+
If you would like to access the JSON output,
|
110
|
+
pleae make sure to set the output_json property for the task
|
111
|
+
"""
|
112
|
+
)
|
113
|
+
return json.dumps(self.json_dict)
|
114
|
+
|
115
|
+
|
115
116
|
|
116
117
|
class Task(BaseModel):
|
117
118
|
"""
|
@@ -140,7 +141,7 @@ class Task(BaseModel):
|
|
140
141
|
|
141
142
|
# task setup
|
142
143
|
context: Optional[List["Task"]] = Field(default=None, description="other tasks whose outputs should be used as context")
|
143
|
-
tools_called: Optional[List[
|
144
|
+
tools_called: Optional[List[ToolSet]] = Field(default_factory=list, description="tools that the agent can use for this task")
|
144
145
|
take_tool_res_as_final: bool = Field(default=False, description="when set True, tools res will be stored in the `TaskOutput`")
|
145
146
|
allow_delegation: bool = Field(default=False, description="ask other agents for help and run the task instead")
|
146
147
|
|
@@ -157,63 +158,6 @@ class Task(BaseModel):
|
|
157
158
|
delegations: int = 0
|
158
159
|
|
159
160
|
|
160
|
-
@property
|
161
|
-
def output_prompt(self) -> str:
|
162
|
-
"""
|
163
|
-
Draft prompts on the output format by converting `output_field_list` to dictionary.
|
164
|
-
"""
|
165
|
-
|
166
|
-
output_prompt, output_formats_to_follow = "", dict()
|
167
|
-
for item in self.output_field_list:
|
168
|
-
output_formats_to_follow[item.title] = f"<Return your answer in {item.type.__name__}>"
|
169
|
-
|
170
|
-
output_prompt = f"""
|
171
|
-
Your outputs MUST adhere to the following format and should NOT include any irrelevant elements:
|
172
|
-
{output_formats_to_follow}
|
173
|
-
"""
|
174
|
-
return output_prompt
|
175
|
-
|
176
|
-
|
177
|
-
@property
|
178
|
-
def expected_output_formats(self) -> List[TaskOutputFormat]:
|
179
|
-
"""
|
180
|
-
Return output formats in list with the ENUM item.
|
181
|
-
`TaskOutputFormat.RAW` is set as default.
|
182
|
-
"""
|
183
|
-
outputs = [TaskOutputFormat.RAW,]
|
184
|
-
if self.expected_output_json:
|
185
|
-
outputs.append(TaskOutputFormat.JSON)
|
186
|
-
if self.expected_output_pydantic:
|
187
|
-
outputs.append(TaskOutputFormat.PYDANTIC)
|
188
|
-
return outputs
|
189
|
-
|
190
|
-
|
191
|
-
@property
|
192
|
-
def key(self) -> str:
|
193
|
-
output_format = (
|
194
|
-
TaskOutputFormat.JSON
|
195
|
-
if self.expected_output_json == True
|
196
|
-
else (
|
197
|
-
TaskOutputFormat.PYDANTIC
|
198
|
-
if self.expected_output_pydantic == True
|
199
|
-
else TaskOutputFormat.RAW
|
200
|
-
)
|
201
|
-
)
|
202
|
-
source = [self.description, output_format]
|
203
|
-
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
204
|
-
|
205
|
-
|
206
|
-
@property
|
207
|
-
def summary(self) -> str:
|
208
|
-
return f"""
|
209
|
-
Task ID: {str(self.id)}
|
210
|
-
"Description": {self.description}
|
211
|
-
"Prompt": {self.output_prompt}
|
212
|
-
"Tools": {", ".join([tool_called.tool.name for tool_called in self.tools_called])}
|
213
|
-
"""
|
214
|
-
|
215
|
-
|
216
|
-
# validators
|
217
161
|
@model_validator(mode="before")
|
218
162
|
@classmethod
|
219
163
|
def process_model_config(cls, values: Dict[str, Any]):
|
@@ -399,7 +343,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
399
343
|
"""
|
400
344
|
Run the core execution logic of the task.
|
401
345
|
To speed up the process, when the format is not expected to return, we will skip the conversion process.
|
402
|
-
When the task is allowed to delegate to another agent, we will select a responsible one in order of
|
346
|
+
When the task is allowed to delegate to another agent, we will select a responsible one in order of manager > peer_agent > anoymous agent.
|
403
347
|
"""
|
404
348
|
from versionhq.agent.model import Agent
|
405
349
|
from versionhq.team.model import Team
|
@@ -410,8 +354,9 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
410
354
|
agent_to_delegate = None
|
411
355
|
|
412
356
|
if hasattr(agent, "team") and isinstance(agent.team, Team):
|
413
|
-
if agent.team.
|
414
|
-
|
357
|
+
if agent.team.managers:
|
358
|
+
idling_manager_agents = [manager.agent for manager in agent.team.managers if manager.task is None]
|
359
|
+
agent_to_delegate = idling_manager_agents[0] if idling_manager_agents else agent.team.managers[0]
|
415
360
|
else:
|
416
361
|
peers = [member.agent for member in agent.team.members if member.is_manager == False and member.agent.id is not agent.id]
|
417
362
|
if len(peers) > 0:
|
@@ -466,6 +411,63 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
466
411
|
self._task_output_handler.update(task=self, task_index=task_index, was_replayed=was_replayed, inputs=inputs)
|
467
412
|
|
468
413
|
|
414
|
+
@property
|
415
|
+
def output_prompt(self) -> str:
|
416
|
+
"""
|
417
|
+
Draft prompts on the output format by converting `output_field_list` to dictionary.
|
418
|
+
"""
|
419
|
+
|
420
|
+
output_prompt, output_formats_to_follow = "", dict()
|
421
|
+
for item in self.output_field_list:
|
422
|
+
output_formats_to_follow[item.title] = f"<Return your answer in {item.type.__name__}>"
|
423
|
+
|
424
|
+
output_prompt = f"""
|
425
|
+
Your outputs MUST adhere to the following format and should NOT include any irrelevant elements:
|
426
|
+
{output_formats_to_follow}
|
427
|
+
"""
|
428
|
+
return output_prompt
|
429
|
+
|
430
|
+
|
431
|
+
@property
|
432
|
+
def expected_output_formats(self) -> List[TaskOutputFormat]:
|
433
|
+
"""
|
434
|
+
Return output formats in list with the ENUM item.
|
435
|
+
`TaskOutputFormat.RAW` is set as default.
|
436
|
+
"""
|
437
|
+
outputs = [TaskOutputFormat.RAW,]
|
438
|
+
if self.expected_output_json:
|
439
|
+
outputs.append(TaskOutputFormat.JSON)
|
440
|
+
if self.expected_output_pydantic:
|
441
|
+
outputs.append(TaskOutputFormat.PYDANTIC)
|
442
|
+
return outputs
|
443
|
+
|
444
|
+
|
445
|
+
@property
|
446
|
+
def key(self) -> str:
|
447
|
+
output_format = (
|
448
|
+
TaskOutputFormat.JSON
|
449
|
+
if self.expected_output_json == True
|
450
|
+
else (
|
451
|
+
TaskOutputFormat.PYDANTIC
|
452
|
+
if self.expected_output_pydantic == True
|
453
|
+
else TaskOutputFormat.RAW
|
454
|
+
)
|
455
|
+
)
|
456
|
+
source = [self.description, output_format]
|
457
|
+
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
458
|
+
|
459
|
+
|
460
|
+
@property
|
461
|
+
def summary(self) -> str:
|
462
|
+
return f"""
|
463
|
+
Task ID: {str(self.id)}
|
464
|
+
"Description": {self.description}
|
465
|
+
"Prompt": {self.output_prompt}
|
466
|
+
"Tools": {", ".join([tool_called.tool.name for tool_called in self.tools_called])}
|
467
|
+
"""
|
468
|
+
|
469
|
+
|
470
|
+
|
469
471
|
|
470
472
|
class ConditionalTask(Task):
|
471
473
|
"""
|
versionhq/team/model.py
CHANGED
@@ -113,6 +113,9 @@ class TeamMember(ABC, BaseModel):
|
|
113
113
|
is_manager: bool = Field(default=False)
|
114
114
|
task: Optional[Task] = Field(default=None)
|
115
115
|
|
116
|
+
def update(self, task: Task):
|
117
|
+
self.task = task
|
118
|
+
|
116
119
|
|
117
120
|
class Team(BaseModel):
|
118
121
|
"""
|
@@ -158,41 +161,6 @@ class Team(BaseModel):
|
|
158
161
|
return self.name if self.name is not None else self.id.__str__
|
159
162
|
|
160
163
|
|
161
|
-
@property
|
162
|
-
def key(self) -> str:
|
163
|
-
source = [str(member.agent.id.__str__) for member in self.members] + [str(task.id.__str__) for task in self.tasks]
|
164
|
-
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
165
|
-
|
166
|
-
|
167
|
-
@property
|
168
|
-
def manager_agent(self) -> Agent:
|
169
|
-
manager_agent = [member.agent for member in self.members if member.is_manager == True]
|
170
|
-
return manager_agent[0] if len(manager_agent) > 0 else None
|
171
|
-
|
172
|
-
|
173
|
-
@property
|
174
|
-
def manager_task(self) -> Task:
|
175
|
-
"""
|
176
|
-
Aside from the team task, return the task that the `manager_agent` needs to handle.
|
177
|
-
The task is set as second priority following to the team tasks.
|
178
|
-
"""
|
179
|
-
task = [member.task for member in self.members if member.is_manager == True]
|
180
|
-
return task[0] if len(task) > 0 else None
|
181
|
-
|
182
|
-
|
183
|
-
@property
|
184
|
-
def tasks(self):
|
185
|
-
"""
|
186
|
-
Return all the tasks that the team needs to handle in order of priority:
|
187
|
-
1. team tasks,
|
188
|
-
2. manager_task,
|
189
|
-
3. members' tasks
|
190
|
-
"""
|
191
|
-
sorted_member_tasks = [member.task for member in self.members if member.is_manager == True] + [member.task for member in self.members if member.is_manager == False]
|
192
|
-
return self.team_tasks + sorted_member_tasks if len(self.team_tasks) > 0 else sorted_member_tasks
|
193
|
-
|
194
|
-
|
195
|
-
# validators
|
196
164
|
@field_validator("id", mode="before")
|
197
165
|
@classmethod
|
198
166
|
def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
|
@@ -201,6 +169,21 @@ class Team(BaseModel):
|
|
201
169
|
raise PydanticCustomError("may_not_set_field", "The 'id' field cannot be set by the user.", {})
|
202
170
|
|
203
171
|
|
172
|
+
@model_validator(mode="after")
|
173
|
+
def assess_tasks(self):
|
174
|
+
"""
|
175
|
+
Validates if the model recognize all tasks that the team needs to handle.
|
176
|
+
"""
|
177
|
+
|
178
|
+
if self.tasks:
|
179
|
+
if all(task in self.tasks for task in self.team_tasks) == False:
|
180
|
+
raise PydanticCustomError("task_validation_error", "`team_tasks` needs to be recognized in the task.", {})
|
181
|
+
|
182
|
+
if len(self.tasks) != len(self.team_tasks) + len([member for member in self.members if member.task is not None]):
|
183
|
+
raise PydanticCustomError("task_validation_error", "Some tasks are missing.", {})
|
184
|
+
return self
|
185
|
+
|
186
|
+
|
204
187
|
@model_validator(mode="after")
|
205
188
|
def check_manager_llm(self):
|
206
189
|
"""
|
@@ -208,37 +191,29 @@ class Team(BaseModel):
|
|
208
191
|
"""
|
209
192
|
|
210
193
|
if self.process == TaskHandlingProcess.hierarchical:
|
211
|
-
if self.
|
194
|
+
if self.managers is None:
|
212
195
|
raise PydanticCustomError(
|
213
|
-
"
|
214
|
-
"Attribute `manager_llm` or `
|
196
|
+
"missing_manager_llm_or_manager",
|
197
|
+
"Attribute `manager_llm` or `manager` is required when using hierarchical process.",
|
215
198
|
{},
|
216
199
|
)
|
217
200
|
|
218
|
-
if (self.
|
219
|
-
|
220
|
-
|
221
|
-
raise PydanticCustomError(
|
222
|
-
"manager_agent_in_agents",
|
223
|
-
"Manager agent should not be included in agents list.",
|
224
|
-
{},
|
225
|
-
)
|
201
|
+
if self.managers and (self.manager_tasks is None or self.team_tasks is None):
|
202
|
+
raise PydanticCustomError("missing_manager_task", "manager needs to have at least one manager task or team task.", {})
|
203
|
+
|
226
204
|
return self
|
227
205
|
|
228
206
|
|
229
207
|
@model_validator(mode="after")
|
230
208
|
def validate_tasks(self):
|
231
209
|
"""
|
232
|
-
|
210
|
+
Sequential task processing without any team tasks require a task-agent pairing.
|
233
211
|
"""
|
234
|
-
|
212
|
+
|
213
|
+
if self.process == TaskHandlingProcess.sequential and self.team_tasks is None:
|
235
214
|
for member in self.members:
|
236
215
|
if member.task is None:
|
237
|
-
raise PydanticCustomError(
|
238
|
-
"missing_agent_in_task",
|
239
|
-
f"Sequential process error: Agent is missing in the task with the following description: {member.task.description}",
|
240
|
-
{},
|
241
|
-
)
|
216
|
+
raise PydanticCustomError("missing_agent_in_task", "Sequential process error: Agent is missing the task", {})
|
242
217
|
return self
|
243
218
|
|
244
219
|
@model_validator(mode="after")
|
@@ -249,34 +224,64 @@ class Team(BaseModel):
|
|
249
224
|
|
250
225
|
async_task_count = 0
|
251
226
|
for task in reversed(self.tasks):
|
252
|
-
if task
|
227
|
+
if not task:
|
228
|
+
break
|
229
|
+
elif task.async_execution:
|
253
230
|
async_task_count += 1
|
254
231
|
else:
|
255
232
|
break
|
256
233
|
|
257
234
|
if async_task_count > 1:
|
258
|
-
raise PydanticCustomError(
|
259
|
-
"async_task_count",
|
260
|
-
"The team must end with max. one asynchronous task.",
|
261
|
-
{},
|
262
|
-
)
|
235
|
+
raise PydanticCustomError("async_task_count", "The team must end with max. one asynchronous task.", {})
|
263
236
|
return self
|
264
237
|
|
238
|
+
|
265
239
|
def _get_responsible_agent(self, task: Task) -> Agent:
|
266
|
-
|
267
|
-
|
240
|
+
if task is None:
|
241
|
+
return None
|
242
|
+
else:
|
243
|
+
res = [member.agent for member in self.members if member.task and member.task.id == task.id]
|
244
|
+
return None if len(res) == 0 else res[0]
|
245
|
+
|
246
|
+
|
247
|
+
def _handle_team_planning(self) -> None:
|
248
|
+
"""
|
249
|
+
Form a team considering agents and tasks given, and update `self.members` field:
|
250
|
+
1. Idling managers to take the team tasks.
|
251
|
+
2. Idling members to take the remaining tasks starting from the team tasks to member tasks.
|
252
|
+
3. Create agents to handle the rest tasks.
|
253
|
+
"""
|
268
254
|
|
269
|
-
# setup team planner
|
270
|
-
def _handle_team_planning(self):
|
271
255
|
team_planner = TeamPlanner(tasks=self.tasks, planner_llm=self.planning_llm)
|
272
|
-
|
256
|
+
idling_managers: List[TeamMember] = [member for member in self.members if member.task is None and member.is_manager is True]
|
257
|
+
idling_members: List[TeamMember] = [member for member in self.members if member.task is None and member.is_manager is False]
|
258
|
+
unassigned_tasks: List[Task] = self.member_tasks_without_agent
|
259
|
+
new_team_members: List[TeamMember] = []
|
260
|
+
|
261
|
+
if self.team_tasks:
|
262
|
+
candidates = idling_managers + idling_members
|
263
|
+
if candidates:
|
264
|
+
i = 0
|
265
|
+
while i < len(candidates):
|
266
|
+
if self.team_tasks[i]:
|
267
|
+
candidates[i].task = self.team_tasks[i]
|
268
|
+
i += 1
|
269
|
+
|
270
|
+
if len(self.team_tasks) > i:
|
271
|
+
for item in self.team_tasks[i:]:
|
272
|
+
if item not in unassigned_tasks:
|
273
|
+
unassigned_tasks = [item, ] + unassigned_tasks
|
273
274
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
275
|
+
else:
|
276
|
+
for item in self.team_tasks:
|
277
|
+
if item not in unassigned_tasks:
|
278
|
+
unassigned_tasks = [item, ] + unassigned_tasks
|
279
|
+
|
280
|
+
if unassigned_tasks:
|
281
|
+
new_team_members = team_planner._handle_assign_agents(unassigned_tasks=unassigned_tasks)
|
282
|
+
|
283
|
+
if new_team_members:
|
284
|
+
self.members += new_team_members
|
280
285
|
|
281
286
|
|
282
287
|
# task execution
|
@@ -333,9 +338,11 @@ class Team(BaseModel):
|
|
333
338
|
token_sum = agent._token_process.get_summary()
|
334
339
|
total_usage_metrics.add_usage_metrics(token_sum)
|
335
340
|
|
336
|
-
if self.
|
337
|
-
|
338
|
-
|
341
|
+
if self.managers:
|
342
|
+
for manager in self.managers:
|
343
|
+
if hasattr(manager.agent, "_token_process"):
|
344
|
+
token_sum = manager.agent._token_process.get_summary()
|
345
|
+
total_usage_metrics.add_usage_metrics(token_sum)
|
339
346
|
|
340
347
|
self.usage_metrics = total_usage_metrics
|
341
348
|
return total_usage_metrics
|
@@ -366,7 +373,7 @@ class Team(BaseModel):
|
|
366
373
|
|
367
374
|
responsible_agent = self._get_responsible_agent(task)
|
368
375
|
if responsible_agent is None:
|
369
|
-
|
376
|
+
self._handle_team_planning()
|
370
377
|
|
371
378
|
if isinstance(task, ConditionalTask):
|
372
379
|
skipped_task_output = task._handle_conditional_task(task_outputs, futures, task_index, was_replayed)
|
@@ -377,13 +384,13 @@ class Team(BaseModel):
|
|
377
384
|
# self._log_task_start(task, responsible_agent)
|
378
385
|
|
379
386
|
if task.async_execution:
|
380
|
-
context = create_raw_outputs(tasks=[task, ],task_outputs=([last_sync_output,] if last_sync_output else []))
|
387
|
+
context = create_raw_outputs(tasks=[task, ], task_outputs=([last_sync_output,] if last_sync_output else []))
|
381
388
|
future = task.execute_async(agent=responsible_agent, context=context, tools=responsible_agent.tools)
|
382
389
|
futures.append((task, future, task_index))
|
383
390
|
else:
|
384
391
|
context = create_raw_outputs(tasks=[task,], task_outputs=([last_sync_output,] if last_sync_output else [] ))
|
385
392
|
task_output = task.execute_sync(agent=responsible_agent, context=context, tools=responsible_agent.tools)
|
386
|
-
if responsible_agent
|
393
|
+
if self.managers and responsible_agent in [manager.agent for manager in self.managers]:
|
387
394
|
lead_task_output = task_output
|
388
395
|
|
389
396
|
task_outputs.append(task_output)
|
@@ -400,15 +407,15 @@ class Team(BaseModel):
|
|
400
407
|
def kickoff(self, kwargs_before: Optional[Dict[str, str]] = None, kwargs_after: Optional[Dict[str, Any]] = None) -> TeamOutput:
|
401
408
|
"""
|
402
409
|
Kickoff the team:
|
403
|
-
0.
|
410
|
+
0. Assign an agent to a task - using conditions (manager prioritizes team_tasks) and planning_llm.
|
404
411
|
1. Address `before_kickoff_callbacks` if any.
|
405
|
-
2. Handle team members' tasks in accordance with the
|
412
|
+
2. Handle team members' tasks in accordance with the process.
|
406
413
|
3. Address `after_kickoff_callbacks` if any.
|
407
414
|
"""
|
408
415
|
|
409
416
|
metrics: List[UsageMetrics] = []
|
410
417
|
|
411
|
-
if
|
418
|
+
if self.team_tasks or self.member_tasks_without_agent:
|
412
419
|
self._handle_team_planning()
|
413
420
|
|
414
421
|
if kwargs_before is not None:
|
@@ -450,3 +457,51 @@ class Team(BaseModel):
|
|
450
457
|
self.usage_metrics.add_usage_metrics(metric)
|
451
458
|
|
452
459
|
return result
|
460
|
+
|
461
|
+
|
462
|
+
@property
|
463
|
+
def key(self) -> str:
|
464
|
+
source = [str(member.agent.id.__str__) for member in self.members] + [str(task.id.__str__) for task in self.tasks]
|
465
|
+
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
466
|
+
|
467
|
+
|
468
|
+
@property
|
469
|
+
def managers(self) -> List[TeamMember] | None:
|
470
|
+
managers = [member for member in self.members if member.is_manager == True]
|
471
|
+
return managers if len(managers) > 0 else None
|
472
|
+
|
473
|
+
|
474
|
+
@property
|
475
|
+
def manager_tasks(self) -> List[Task] | None:
|
476
|
+
"""
|
477
|
+
Tasks (incl. team tasks) handled by managers in the team.
|
478
|
+
"""
|
479
|
+
if self.managers:
|
480
|
+
tasks = [manager.task for manager in self.managers if manager.task is not None]
|
481
|
+
return tasks if len(tasks) > 0 else None
|
482
|
+
|
483
|
+
return None
|
484
|
+
|
485
|
+
|
486
|
+
@property
|
487
|
+
def tasks(self):
|
488
|
+
"""
|
489
|
+
Return all the tasks that the team needs to handle in order of priority:
|
490
|
+
1. team tasks, -> assigned to the member
|
491
|
+
2. manager_task,
|
492
|
+
3. members' tasks
|
493
|
+
"""
|
494
|
+
|
495
|
+
team_tasks = self.team_tasks
|
496
|
+
manager_tasks = [member.task for member in self.members if member.is_manager == True and member.task is not None and member.task not in team_tasks]
|
497
|
+
member_tasks = [member.task for member in self.members if member.is_manager == False and member.task is not None and member.task not in team_tasks]
|
498
|
+
|
499
|
+
return team_tasks + manager_tasks + member_tasks
|
500
|
+
|
501
|
+
|
502
|
+
@property
|
503
|
+
def member_tasks_without_agent(self) -> List[Task] | None:
|
504
|
+
if self.members:
|
505
|
+
return [member.task for member in self.members if member.agent is None]
|
506
|
+
|
507
|
+
return None
|
versionhq/team/team_planner.py
CHANGED
@@ -3,37 +3,80 @@ from dotenv import load_dotenv
|
|
3
3
|
from typing import Any, List, Optional
|
4
4
|
from pydantic import BaseModel, Field
|
5
5
|
|
6
|
-
from versionhq.agent.model import Agent
|
7
|
-
from versionhq.task.model import Task, ResponseField
|
8
|
-
|
9
6
|
load_dotenv(override=True)
|
10
7
|
|
11
8
|
|
12
9
|
class TeamPlanner:
|
13
10
|
"""
|
14
|
-
|
11
|
+
Assign agents to multiple tasks.
|
15
12
|
"""
|
16
13
|
|
14
|
+
from versionhq.task.model import Task, ResponseField, TaskOutput
|
15
|
+
from versionhq.agent.model import Agent
|
16
|
+
|
17
|
+
|
17
18
|
def __init__(self, tasks: List[Task], planner_llm: Optional[Any] = None):
|
18
19
|
self.tasks = tasks
|
19
|
-
self.planner_llm = (
|
20
|
-
|
20
|
+
self.planner_llm = planner_llm if planner_llm else os.environ.get("LITELLM_MODEL_NAME")
|
21
|
+
|
22
|
+
|
23
|
+
def _handle_assign_agents(self, unassigned_tasks: List[Task]) -> List[Any]:
|
24
|
+
"""
|
25
|
+
Build an agent and assign it a task, then return a list of TeamMember connecting the agent created and the task given.
|
26
|
+
"""
|
27
|
+
|
28
|
+
from versionhq.agent.model import Agent
|
29
|
+
from versionhq.task.model import Task, ResponseField
|
30
|
+
from versionhq.team.model import TeamMember
|
31
|
+
|
32
|
+
new_member_list: List[TeamMember] = []
|
33
|
+
agent_creator = Agent(
|
34
|
+
role="agent_creator",
|
35
|
+
goal="build an ai agent that can competitively handle the task given",
|
36
|
+
llm=self.planner_llm,
|
21
37
|
)
|
22
38
|
|
23
|
-
|
39
|
+
for unassgined_task in unassigned_tasks:
|
40
|
+
task = Task(
|
41
|
+
description=f"""
|
42
|
+
Based on the following task summary, draft a AI agent's role and goal in concise manner.
|
43
|
+
Task summary: {unassgined_task.summary}
|
44
|
+
""",
|
45
|
+
expected_output_json=True,
|
46
|
+
output_field_list=[
|
47
|
+
ResponseField(title="goal", type=str, required=True),
|
48
|
+
ResponseField(title="role", type=str, required=True),
|
49
|
+
],
|
50
|
+
)
|
51
|
+
res = task.execute_sync(agent=agent_creator)
|
52
|
+
agent = Agent(
|
53
|
+
role=res.json_dict["role"] if "role" in res.json_dict else res.raw,
|
54
|
+
goal=res.json_dict["goal"] if "goal" in res.json_dict else task.description
|
55
|
+
)
|
56
|
+
if agent.id:
|
57
|
+
team_member = TeamMember(agent=agent, task=unassgined_task, is_manager=False)
|
58
|
+
new_member_list.append(team_member)
|
59
|
+
|
60
|
+
return new_member_list
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
def _handle_task_planning(self, context: Optional[str] = None, tools: Optional[str] = None) -> TaskOutput:
|
24
65
|
"""
|
25
66
|
Handles the team planning by creating detailed step-by-step plans for each task.
|
26
67
|
"""
|
27
68
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
69
|
+
from versionhq.agent.model import Agent
|
70
|
+
from versionhq.task.model import Task, ResponseField
|
71
|
+
|
72
|
+
team_planner = Agent(
|
73
|
+
role="team planner",
|
74
|
+
goal="Plan extremely detailed, step-by-step plan based on the tasks and tools available to each agent so that they can perform the tasks in an exemplary manner and assign a task to each agent.",
|
32
75
|
llm=self.planner_llm,
|
33
76
|
)
|
34
77
|
|
35
78
|
task_summary_list = [task.summary for task in self.tasks]
|
36
|
-
|
79
|
+
task = Task(
|
37
80
|
description=f"""
|
38
81
|
Based on the following task summaries, create the most descriptive plan that the team can execute most efficiently. Take all the task summaries - task's description and tools available - into consideration. Your answer only contains a dictionary.
|
39
82
|
|
@@ -42,14 +85,9 @@ class TeamPlanner:
|
|
42
85
|
expected_output_json=False,
|
43
86
|
expected_output_pydantic=True,
|
44
87
|
output_field_list=[
|
45
|
-
ResponseField(title=
|
88
|
+
ResponseField(title="task", type=str, required=True)
|
46
89
|
for task in self.tasks
|
47
90
|
],
|
48
91
|
)
|
49
|
-
|
50
|
-
|
51
|
-
if isinstance(task_output.pydantic, BaseModel):
|
52
|
-
return task_output.pydantic
|
53
|
-
|
54
|
-
else:
|
55
|
-
return None
|
92
|
+
output = task.execute_sync(agent=team_planner, context=context, tools=tools)
|
93
|
+
return output
|