versionhq 1.1.7.9__tar.gz → 1.1.8.1__tar.gz
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-1.1.7.9 → versionhq-1.1.8.1}/PKG-INFO +1 -1
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/pyproject.toml +1 -1
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/__init__.py +1 -1
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/storage/task_output_storage.py +19 -22
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/task/model.py +9 -12
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/team/model.py +115 -55
- versionhq-1.1.8.1/src/versionhq/team/team_planner.py +93 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/tool/model.py +0 -1
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq.egg-info/PKG-INFO +1 -1
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/task/task_test.py +7 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/team/team_test.py +159 -30
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/uv.lock +1 -1
- versionhq-1.1.7.9/src/versionhq/team/team_planner.py +0 -55
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.github/workflows/publish.yml +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.github/workflows/publish_testpypi.yml +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.github/workflows/run_tests.yml +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.github/workflows/security_check.yml +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.gitignore +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.pre-commit-config.yaml +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/.python-version +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/LICENSE +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/README.md +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/SECURITY.md +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/db/preprocess.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/requirements-dev.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/requirements.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/runtime.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/setup.cfg +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/cache_handler.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/i18n.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/logger.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/process_config.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/rpm_controller.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/_utils/usage_metrics.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/agent/TEMPLATES/Backstory.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/agent/TEMPLATES/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/agent/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/agent/model.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/agent/parser.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/cli/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/customer/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/customer/model.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/product/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/product/model.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/workflow/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/clients/workflow/model.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/llm/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/llm/llm_vars.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/llm/model.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/storage/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/task/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/task/formatter.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/task/log_handler.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/team/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/tool/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/tool/decorator.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq/tool/tool_handler.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq.egg-info/SOURCES.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq.egg-info/dependency_links.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq.egg-info/requires.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/src/versionhq.egg-info/top_level.txt +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/agent/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/agent/agent_test.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/cli/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/clients/workflow_test.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/conftest.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/task/__init__.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/team/Prompts/Demo_test.py +0 -0
- {versionhq-1.1.7.9 → versionhq-1.1.8.1}/tests/team/__init__.py +0 -0
@@ -15,7 +15,7 @@ exclude = ["test*", "__pycache__"]
|
|
15
15
|
|
16
16
|
[project]
|
17
17
|
name = "versionhq"
|
18
|
-
version = "1.1.
|
18
|
+
version = "1.1.8.1"
|
19
19
|
authors = [{ name = "Kuriko Iwai", email = "kuriko@versi0n.io" }]
|
20
20
|
description = "LLM orchestration frameworks for model-agnostic AI agents that handle complex outbound workflows"
|
21
21
|
readme = "README.md"
|
@@ -11,11 +11,9 @@ from versionhq._utils.logger import Logger
|
|
11
11
|
|
12
12
|
load_dotenv(override=True)
|
13
13
|
|
14
|
-
|
15
14
|
def fetch_db_storage_path():
|
16
|
-
|
17
|
-
|
18
|
-
data_dir = Path(appdirs.user_data_dir(project_directory_name, app_author))
|
15
|
+
directory_name = Path.cwd().name
|
16
|
+
data_dir = Path(appdirs.user_data_dir(appname=directory_name, appauthor="Version IO Sdn Bhd.", version=None, roaming=False))
|
19
17
|
data_dir.mkdir(parents=True, exist_ok=True)
|
20
18
|
return data_dir
|
21
19
|
|
@@ -39,25 +37,25 @@ class TaskOutputSQLiteStorage:
|
|
39
37
|
Initializes the SQLite database and creates LTM table.
|
40
38
|
"""
|
41
39
|
|
42
|
-
try:
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
"""
|
47
|
-
CREATE TABLE IF NOT EXISTS task_outputs (
|
48
|
-
task_id TEXT PRIMARY KEY,
|
49
|
-
output JSON,
|
50
|
-
task_index INTEGER,
|
51
|
-
inputs JSON,
|
52
|
-
was_replayed BOOLEAN,
|
53
|
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
54
|
-
)
|
40
|
+
# try:
|
41
|
+
with sqlite3.connect(self.db_path) as conn:
|
42
|
+
cursor = conn.cursor()
|
43
|
+
cursor.execute(
|
55
44
|
"""
|
45
|
+
CREATE TABLE IF NOT EXISTS task_outputs (
|
46
|
+
task_id TEXT PRIMARY KEY,
|
47
|
+
output JSON,
|
48
|
+
task_index INTEGER,
|
49
|
+
inputs JSON,
|
50
|
+
was_replayed BOOLEAN,
|
51
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
56
52
|
)
|
57
|
-
|
53
|
+
"""
|
54
|
+
)
|
55
|
+
conn.commit()
|
58
56
|
|
59
|
-
except sqlite3.Error as e:
|
60
|
-
|
57
|
+
# except sqlite3.Error as e:
|
58
|
+
# self._logger.log(level="error", message=f"DATABASE INITIALIZATION ERROR: {e}", color="red")
|
61
59
|
|
62
60
|
|
63
61
|
def add(self, task, output: Dict[str, Any], task_index: int, was_replayed: bool = False, inputs: Dict[str, Any] = {}):
|
@@ -81,13 +79,12 @@ class TaskOutputSQLiteStorage:
|
|
81
79
|
try:
|
82
80
|
with sqlite3.connect(self.db_path) as conn:
|
83
81
|
cursor = conn.cursor()
|
84
|
-
|
85
82
|
fields, values = [], []
|
86
83
|
for k, v in kwargs.items():
|
87
84
|
fields.append(f"{k} = ?")
|
88
85
|
values.append(json.dumps(v) if isinstance(v, dict) else v)
|
89
86
|
|
90
|
-
query = f"UPDATE latest_kickoff_task_outputs SET {', '.join(fields)} WHERE task_index = ?"
|
87
|
+
query = f"UPDATE latest_kickoff_task_outputs SET {', '.join(fields)} WHERE task_index = ?"
|
91
88
|
values.append(task_index)
|
92
89
|
cursor.execute(query, tuple(values))
|
93
90
|
conn.commit()
|
@@ -206,10 +206,10 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
206
206
|
@property
|
207
207
|
def summary(self) -> str:
|
208
208
|
return f"""
|
209
|
-
Task: {self.id}
|
210
|
-
"
|
211
|
-
"
|
212
|
-
"
|
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
213
|
"""
|
214
214
|
|
215
215
|
|
@@ -382,11 +382,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
382
382
|
"""
|
383
383
|
|
384
384
|
future: Future[TaskOutput] = Future()
|
385
|
-
threading.Thread(
|
386
|
-
daemon=True,
|
387
|
-
target=self._execute_task_async,
|
388
|
-
args=(agent, context, tools, future),
|
389
|
-
).start()
|
385
|
+
threading.Thread(daemon=True, target=self._execute_task_async, args=(agent, context, tools, future)).start()
|
390
386
|
return future
|
391
387
|
|
392
388
|
|
@@ -403,7 +399,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
403
399
|
"""
|
404
400
|
Run the core execution logic of the task.
|
405
401
|
To speed up the process, when the format is not expected to return, we will skip the conversion process.
|
406
|
-
When the task is allowed to delegate to another agent, we will select a responsible one in order of
|
402
|
+
When the task is allowed to delegate to another agent, we will select a responsible one in order of manager > peer_agent > anoymous agent.
|
407
403
|
"""
|
408
404
|
from versionhq.agent.model import Agent
|
409
405
|
from versionhq.team.model import Team
|
@@ -414,8 +410,9 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
414
410
|
agent_to_delegate = None
|
415
411
|
|
416
412
|
if hasattr(agent, "team") and isinstance(agent.team, Team):
|
417
|
-
if agent.team.
|
418
|
-
|
413
|
+
if agent.team.managers:
|
414
|
+
idling_manager_agents = [manager.agent for manager in agent.team.managers if manager.task is None]
|
415
|
+
agent_to_delegate = idling_manager_agents[0] if idling_manager_agents else agent.team.managers[0]
|
419
416
|
else:
|
420
417
|
peers = [member.agent for member in agent.team.members if member.is_manager == False and member.agent.id is not agent.id]
|
421
418
|
if len(peers) > 0:
|
@@ -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
|
"""
|
@@ -123,7 +126,7 @@ class Team(BaseModel):
|
|
123
126
|
__hash__ = object.__hash__
|
124
127
|
_execution_span: Any = PrivateAttr()
|
125
128
|
_logger: Logger = PrivateAttr()
|
126
|
-
|
129
|
+
_inputs: Optional[Dict[str, Any]] = PrivateAttr(default=None)
|
127
130
|
|
128
131
|
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
129
132
|
name: Optional[str] = Field(default=None)
|
@@ -165,34 +168,47 @@ class Team(BaseModel):
|
|
165
168
|
|
166
169
|
|
167
170
|
@property
|
168
|
-
def
|
169
|
-
|
170
|
-
return
|
171
|
+
def managers(self) -> List[TeamMember] | None:
|
172
|
+
managers = [member for member in self.members if member.is_manager == True]
|
173
|
+
return managers if len(managers) > 0 else None
|
171
174
|
|
172
175
|
|
173
176
|
@property
|
174
|
-
def
|
177
|
+
def manager_tasks(self) -> List[Task] | None:
|
175
178
|
"""
|
176
|
-
|
177
|
-
The task is set as second priority following to the team tasks.
|
179
|
+
Tasks (incl. team tasks) handled by managers in the team.
|
178
180
|
"""
|
179
|
-
|
180
|
-
|
181
|
+
if self.managers:
|
182
|
+
tasks = [manager.task for manager in self.managers if manager.task is not None]
|
183
|
+
return tasks if len(tasks) > 0 else None
|
184
|
+
|
185
|
+
return None
|
181
186
|
|
182
187
|
|
183
188
|
@property
|
184
189
|
def tasks(self):
|
185
190
|
"""
|
186
191
|
Return all the tasks that the team needs to handle in order of priority:
|
187
|
-
1. team tasks,
|
192
|
+
1. team tasks, -> assigned to the member
|
188
193
|
2. manager_task,
|
189
194
|
3. members' tasks
|
190
195
|
"""
|
191
|
-
|
192
|
-
|
196
|
+
|
197
|
+
team_tasks = self.team_tasks
|
198
|
+
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]
|
199
|
+
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]
|
200
|
+
|
201
|
+
return team_tasks + manager_tasks + member_tasks
|
202
|
+
|
203
|
+
|
204
|
+
@property
|
205
|
+
def member_tasks_without_agent(self) -> List[Task] | None:
|
206
|
+
if self.members:
|
207
|
+
return [member.task for member in self.members if member.agent is None]
|
208
|
+
|
209
|
+
return None
|
193
210
|
|
194
211
|
|
195
|
-
# validators
|
196
212
|
@field_validator("id", mode="before")
|
197
213
|
@classmethod
|
198
214
|
def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
|
@@ -201,6 +217,21 @@ class Team(BaseModel):
|
|
201
217
|
raise PydanticCustomError("may_not_set_field", "The 'id' field cannot be set by the user.", {})
|
202
218
|
|
203
219
|
|
220
|
+
@model_validator(mode="after")
|
221
|
+
def validate_tasks(self):
|
222
|
+
"""
|
223
|
+
Validates if the model recognize all tasks that the team needs to handle.
|
224
|
+
"""
|
225
|
+
|
226
|
+
if self.tasks:
|
227
|
+
if all(task in self.tasks for task in self.team_tasks) == False:
|
228
|
+
raise PydanticCustomError("task_validation_error", "`team_tasks` needs to be recognized in the task.", {})
|
229
|
+
|
230
|
+
if len(self.tasks) != len(self.team_tasks) + len([member for member in self.members if member.task is not None]):
|
231
|
+
raise PydanticCustomError("task_validation_error", "Some tasks are missing.", {})
|
232
|
+
return self
|
233
|
+
|
234
|
+
|
204
235
|
@model_validator(mode="after")
|
205
236
|
def check_manager_llm(self):
|
206
237
|
"""
|
@@ -208,37 +239,29 @@ class Team(BaseModel):
|
|
208
239
|
"""
|
209
240
|
|
210
241
|
if self.process == TaskHandlingProcess.hierarchical:
|
211
|
-
if self.
|
242
|
+
if self.managers is None:
|
212
243
|
raise PydanticCustomError(
|
213
|
-
"
|
214
|
-
"Attribute `manager_llm` or `
|
244
|
+
"missing_manager_llm_or_manager",
|
245
|
+
"Attribute `manager_llm` or `manager` is required when using hierarchical process.",
|
215
246
|
{},
|
216
247
|
)
|
217
248
|
|
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
|
-
)
|
249
|
+
if self.managers and (self.manager_tasks is None or self.team_tasks is None):
|
250
|
+
raise PydanticCustomError("missing_manager_task", "manager needs to have at least one manager task or team task.", {})
|
251
|
+
|
226
252
|
return self
|
227
253
|
|
228
254
|
|
229
255
|
@model_validator(mode="after")
|
230
256
|
def validate_tasks(self):
|
231
257
|
"""
|
232
|
-
|
258
|
+
Sequential task processing without any team tasks require a task-agent pairing.
|
233
259
|
"""
|
234
|
-
|
260
|
+
|
261
|
+
if self.process == TaskHandlingProcess.sequential and self.team_tasks is None:
|
235
262
|
for member in self.members:
|
236
263
|
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
|
-
)
|
264
|
+
raise PydanticCustomError("missing_agent_in_task", f"Sequential process error: Agent is missing the task", {})
|
242
265
|
return self
|
243
266
|
|
244
267
|
@model_validator(mode="after")
|
@@ -249,7 +272,9 @@ class Team(BaseModel):
|
|
249
272
|
|
250
273
|
async_task_count = 0
|
251
274
|
for task in reversed(self.tasks):
|
252
|
-
if task
|
275
|
+
if not task:
|
276
|
+
break
|
277
|
+
elif task.async_execution:
|
253
278
|
async_task_count += 1
|
254
279
|
else:
|
255
280
|
break
|
@@ -262,21 +287,54 @@ class Team(BaseModel):
|
|
262
287
|
)
|
263
288
|
return self
|
264
289
|
|
290
|
+
|
265
291
|
def _get_responsible_agent(self, task: Task) -> Agent:
|
266
|
-
|
267
|
-
|
292
|
+
if task is None:
|
293
|
+
return None
|
294
|
+
else:
|
295
|
+
res = [member.agent for member in self.members if member.task and member.task.id == task.id]
|
296
|
+
return None if len(res) == 0 else res[0]
|
297
|
+
|
298
|
+
|
299
|
+
def _handle_team_planning(self) -> None:
|
300
|
+
"""
|
301
|
+
Form a team considering agents and tasks given, and update `self.members` field:
|
302
|
+
1. Idling managers to take the team tasks.
|
303
|
+
2. Idling members to take the remaining tasks starting from the team tasks to member tasks.
|
304
|
+
3. Create agents to handle the rest tasks.
|
305
|
+
"""
|
268
306
|
|
269
|
-
# setup team planner
|
270
|
-
def _handle_team_planning(self):
|
271
307
|
team_planner = TeamPlanner(tasks=self.tasks, planner_llm=self.planning_llm)
|
272
|
-
|
308
|
+
idling_managers: List[TeamMember] = [member for member in self.members if member.task is None and member.is_manager is True]
|
309
|
+
idling_members: List[TeamMember] = [member for member in self.members if member.task is None and member.is_manager is False]
|
310
|
+
unassigned_tasks: List[Task] = self.member_tasks_without_agent
|
311
|
+
new_team_members: List[TeamMember] = []
|
312
|
+
|
313
|
+
|
314
|
+
if self.team_tasks:
|
315
|
+
candidates = idling_managers + idling_members
|
316
|
+
if candidates:
|
317
|
+
i = 0
|
318
|
+
while i < len(candidates):
|
319
|
+
if self.team_tasks[i]:
|
320
|
+
candidates[i].task = self.team_tasks[i]
|
321
|
+
i += 1
|
322
|
+
|
323
|
+
if len(self.team_tasks) > i:
|
324
|
+
for item in self.team_tasks[i:]:
|
325
|
+
if item not in unassigned_tasks:
|
326
|
+
unassigned_tasks = [item, ] + unassigned_tasks
|
273
327
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
328
|
+
else:
|
329
|
+
for item in self.team_tasks:
|
330
|
+
if item not in unassigned_tasks:
|
331
|
+
unassigned_tasks = [item, ] + unassigned_tasks
|
332
|
+
|
333
|
+
if unassigned_tasks:
|
334
|
+
new_team_members = team_planner._handle_assign_agents(unassigned_tasks=unassigned_tasks)
|
335
|
+
|
336
|
+
if new_team_members:
|
337
|
+
self.members += new_team_members
|
280
338
|
|
281
339
|
|
282
340
|
# task execution
|
@@ -333,9 +391,11 @@ class Team(BaseModel):
|
|
333
391
|
token_sum = agent._token_process.get_summary()
|
334
392
|
total_usage_metrics.add_usage_metrics(token_sum)
|
335
393
|
|
336
|
-
if self.
|
337
|
-
|
338
|
-
|
394
|
+
if self.managers:
|
395
|
+
for manager in self.managers:
|
396
|
+
if hasattr(manager.agent, "_token_process"):
|
397
|
+
token_sum = manager.agent._token_process.get_summary()
|
398
|
+
total_usage_metrics.add_usage_metrics(token_sum)
|
339
399
|
|
340
400
|
self.usage_metrics = total_usage_metrics
|
341
401
|
return total_usage_metrics
|
@@ -366,7 +426,7 @@ class Team(BaseModel):
|
|
366
426
|
|
367
427
|
responsible_agent = self._get_responsible_agent(task)
|
368
428
|
if responsible_agent is None:
|
369
|
-
|
429
|
+
self._handle_team_planning()
|
370
430
|
|
371
431
|
if isinstance(task, ConditionalTask):
|
372
432
|
skipped_task_output = task._handle_conditional_task(task_outputs, futures, task_index, was_replayed)
|
@@ -377,19 +437,18 @@ class Team(BaseModel):
|
|
377
437
|
# self._log_task_start(task, responsible_agent)
|
378
438
|
|
379
439
|
if task.async_execution:
|
380
|
-
context = create_raw_outputs(tasks=[task, ],task_outputs=([last_sync_output,] if last_sync_output else []))
|
440
|
+
context = create_raw_outputs(tasks=[task, ], task_outputs=([last_sync_output,] if last_sync_output else []))
|
381
441
|
future = task.execute_async(agent=responsible_agent, context=context, tools=responsible_agent.tools)
|
382
442
|
futures.append((task, future, task_index))
|
383
443
|
else:
|
384
444
|
context = create_raw_outputs(tasks=[task,], task_outputs=([last_sync_output,] if last_sync_output else [] ))
|
385
|
-
task_output = task.execute_sync(agent=responsible_agent, context=context, tools=responsible_agent.tools
|
386
|
-
|
387
|
-
if responsible_agent is self.manager_agent:
|
445
|
+
task_output = task.execute_sync(agent=responsible_agent, context=context, tools=responsible_agent.tools)
|
446
|
+
if self.managers and responsible_agent in [manager.agent for manager in self.managers]:
|
388
447
|
lead_task_output = task_output
|
389
448
|
|
390
449
|
task_outputs.append(task_output)
|
391
450
|
# self._process_task_result(task, task_output)
|
392
|
-
task._store_execution_log(task_index, was_replayed)
|
451
|
+
task._store_execution_log(task_index, was_replayed, self._inputs)
|
393
452
|
|
394
453
|
|
395
454
|
if futures:
|
@@ -401,15 +460,16 @@ class Team(BaseModel):
|
|
401
460
|
def kickoff(self, kwargs_before: Optional[Dict[str, str]] = None, kwargs_after: Optional[Dict[str, Any]] = None) -> TeamOutput:
|
402
461
|
"""
|
403
462
|
Kickoff the team:
|
404
|
-
0.
|
463
|
+
0. Assign an agent to a task - using conditions (manager prioritizes team_tasks) and planning_llm.
|
405
464
|
1. Address `before_kickoff_callbacks` if any.
|
406
|
-
2. Handle team members' tasks in accordance with the
|
465
|
+
2. Handle team members' tasks in accordance with the process.
|
407
466
|
3. Address `after_kickoff_callbacks` if any.
|
408
467
|
"""
|
409
468
|
|
410
469
|
metrics: List[UsageMetrics] = []
|
411
470
|
|
412
|
-
|
471
|
+
|
472
|
+
if self.team_tasks or self.member_tasks_without_agent:
|
413
473
|
self._handle_team_planning()
|
414
474
|
|
415
475
|
if kwargs_before is not None:
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import os
|
2
|
+
from dotenv import load_dotenv
|
3
|
+
from typing import Any, List, Optional
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
load_dotenv(override=True)
|
7
|
+
|
8
|
+
|
9
|
+
class TeamPlanner:
|
10
|
+
"""
|
11
|
+
Assign agents to multiple tasks.
|
12
|
+
"""
|
13
|
+
|
14
|
+
from versionhq.task.model import Task, ResponseField, TaskOutput
|
15
|
+
from versionhq.agent.model import Agent
|
16
|
+
|
17
|
+
|
18
|
+
def __init__(self, tasks: List[Task], planner_llm: Optional[Any] = None):
|
19
|
+
self.tasks = tasks
|
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,
|
37
|
+
)
|
38
|
+
|
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:
|
65
|
+
"""
|
66
|
+
Handles the team planning by creating detailed step-by-step plans for each task.
|
67
|
+
"""
|
68
|
+
|
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.",
|
75
|
+
llm=self.planner_llm,
|
76
|
+
)
|
77
|
+
|
78
|
+
task_summary_list = [task.summary for task in self.tasks]
|
79
|
+
task = Task(
|
80
|
+
description=f"""
|
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.
|
82
|
+
|
83
|
+
Task summaries: {" ".join(task_summary_list)}
|
84
|
+
""",
|
85
|
+
expected_output_json=False,
|
86
|
+
expected_output_pydantic=True,
|
87
|
+
output_field_list=[
|
88
|
+
ResponseField(title="task", type=str, required=True)
|
89
|
+
for task in self.tasks
|
90
|
+
],
|
91
|
+
)
|
92
|
+
output = task.execute_sync(agent=team_planner, context=context, tools=tools)
|
93
|
+
return output
|
@@ -277,4 +277,11 @@ def test_conditional_task():
|
|
277
277
|
assert conditional_res.task_id is conditional_task.id
|
278
278
|
|
279
279
|
|
280
|
+
def test_store_task_log():
|
281
|
+
task = Task(
|
282
|
+
description="Analyze the client's business model.",
|
283
|
+
output_field_list=[ResponseField(title="task_1", type=str, required=True),],
|
284
|
+
)
|
285
|
+
assert task._task_output_handler.load() is not None
|
286
|
+
|
280
287
|
# tools, token usage
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import os
|
2
|
-
from pydantic import BaseModel
|
3
2
|
|
4
3
|
from versionhq.agent.model import Agent
|
5
4
|
from versionhq.task.model import Task, ResponseField, TaskOutput
|
@@ -61,8 +60,8 @@ def test_form_team():
|
|
61
60
|
assert team.id is not None
|
62
61
|
assert team.key is not None
|
63
62
|
assert isinstance(team.key, str)
|
64
|
-
assert team.
|
65
|
-
assert
|
63
|
+
assert team.managers is not None
|
64
|
+
assert task_1 in team.manager_tasks
|
66
65
|
assert len(team.tasks) == 2
|
67
66
|
for item in team.tasks:
|
68
67
|
assert item.id is task_1.id or item.id is task_2.id
|
@@ -120,35 +119,24 @@ def test_form_team_without_leader():
|
|
120
119
|
assert team.id is not None
|
121
120
|
assert team.key is not None
|
122
121
|
assert isinstance(team.key, str)
|
123
|
-
assert team.
|
124
|
-
assert team.
|
122
|
+
assert team.managers is None
|
123
|
+
assert team.manager_tasks is None
|
125
124
|
assert len(team.tasks) == 2
|
126
125
|
for item in team.tasks:
|
127
126
|
assert item.id is task_1.id or item.id is task_2.id
|
128
127
|
|
129
128
|
|
130
|
-
def
|
131
|
-
agent_a = Agent(
|
132
|
-
|
133
|
-
goal="My amazing goals",
|
134
|
-
llm=MODEL_NAME
|
135
|
-
)
|
136
|
-
|
137
|
-
agent_b = Agent(
|
138
|
-
role="agent b",
|
139
|
-
goal="My amazing goals",
|
140
|
-
llm=MODEL_NAME
|
141
|
-
)
|
142
|
-
|
129
|
+
def test_kickoff_without_leader():
|
130
|
+
agent_a = Agent(role="agent a", goal="My amazing goals", llm=MODEL_NAME)
|
131
|
+
agent_b = Agent(role="agent b", goal="My amazing goals", llm=MODEL_NAME)
|
143
132
|
task_1 = Task(
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
133
|
+
description="Analyze the client's business model.",
|
134
|
+
expected_output_json=True,
|
135
|
+
output_field_list=[
|
136
|
+
ResponseField(title="test1", type=str, required=True),
|
137
|
+
ResponseField(title="test2", type=list, required=True),
|
138
|
+
],
|
150
139
|
)
|
151
|
-
|
152
140
|
task_2 = Task(
|
153
141
|
description="Define the cohort.",
|
154
142
|
expected_output_json=True,
|
@@ -157,7 +145,6 @@ def test_kickoff_team_without_leader():
|
|
157
145
|
ResponseField(title="test2", type=list, required=True),
|
158
146
|
],
|
159
147
|
)
|
160
|
-
|
161
148
|
team = Team(
|
162
149
|
members=[
|
163
150
|
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
@@ -185,7 +172,6 @@ def test_kickoff_team_without_leader():
|
|
185
172
|
assert res.token_usage.total_tokens == 0 # as we dont set token usage on agent
|
186
173
|
|
187
174
|
|
188
|
-
|
189
175
|
def team_kickoff_with_task_callback():
|
190
176
|
"""
|
191
177
|
Each task has callback with callback kwargs.
|
@@ -236,7 +222,6 @@ def team_kickoff_with_task_callback():
|
|
236
222
|
assert "pytest" in demo_list[1]
|
237
223
|
|
238
224
|
|
239
|
-
|
240
225
|
def test_delegate_in_team():
|
241
226
|
"""
|
242
227
|
When the agent belongs to the team, the team manager or peers are prioritized to delegete the task.
|
@@ -279,7 +264,151 @@ def test_delegate_in_team():
|
|
279
264
|
assert "agent b" in task_1.processed_by_agents
|
280
265
|
|
281
266
|
|
267
|
+
def test_kickoff_with_leader():
|
268
|
+
agent_a = Agent(role="agent a", goal="My amazing goals", llm=MODEL_NAME)
|
269
|
+
agent_b = Agent(role="agent b", goal="My amazing goals", llm=MODEL_NAME)
|
270
|
+
task_1 = Task(
|
271
|
+
description="Analyze the client's business model.",
|
272
|
+
output_field_list=[ResponseField(title="task_1", type=str, required=True),],
|
273
|
+
)
|
274
|
+
task_2 = Task(
|
275
|
+
description="Define the cohort timeframe.",
|
276
|
+
output_field_list=[
|
277
|
+
ResponseField(title="task_2_1", type=int, required=True),
|
278
|
+
ResponseField(title="task_2_2", type=list, required=True),
|
279
|
+
],
|
280
|
+
)
|
281
|
+
team = Team(
|
282
|
+
members=[
|
283
|
+
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
284
|
+
TeamMember(agent=agent_b, is_manager=True, task=task_2),
|
285
|
+
],
|
286
|
+
)
|
287
|
+
res = team.kickoff()
|
288
|
+
|
289
|
+
assert isinstance(res, TeamOutput)
|
290
|
+
assert res.team_id is team.id
|
291
|
+
assert res.raw is not None
|
292
|
+
assert res.json_dict is not None
|
293
|
+
assert team.managers[0].agent.id is agent_b.id
|
294
|
+
assert len(res.task_output_list) == 2
|
295
|
+
assert [item.raw is not None for item in res.task_output_list]
|
296
|
+
assert len(team.tasks) == 2
|
297
|
+
assert team.tasks[0].output.raw == res.raw
|
298
|
+
|
299
|
+
|
300
|
+
def test_hierarchial_process():
|
301
|
+
"""
|
302
|
+
Manager to handle the top priority task first.
|
303
|
+
"""
|
304
|
+
|
305
|
+
agent_a = Agent(role="agent a", goal="My amazing goals", llm=MODEL_NAME)
|
306
|
+
agent_b = Agent(role="agent b", goal="My amazing goals", llm=MODEL_NAME)
|
307
|
+
agent_c = Agent(role="agent c", goal="My amazing goals", llm=MODEL_NAME)
|
308
|
+
task_1 = Task(
|
309
|
+
description="Analyze the client's business model.",
|
310
|
+
output_field_list=[ResponseField(title="task_1", type=str, required=True),],
|
311
|
+
)
|
312
|
+
task_2 = Task(
|
313
|
+
description="Define the cohort timeframe.",
|
314
|
+
output_field_list=[
|
315
|
+
ResponseField(title="task_2_1", type=int, required=True),
|
316
|
+
ResponseField(title="task_2_2", type=list, required=True),
|
317
|
+
],
|
318
|
+
)
|
319
|
+
team = Team(
|
320
|
+
members=[
|
321
|
+
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
322
|
+
TeamMember(agent=agent_b, is_manager=True, task=task_2),
|
323
|
+
TeamMember(agent=agent_c, is_manager=False)
|
324
|
+
],
|
325
|
+
process=TaskHandlingProcess.hierarchical
|
326
|
+
)
|
327
|
+
res = team.kickoff()
|
328
|
+
|
329
|
+
assert isinstance(res, TeamOutput)
|
330
|
+
assert res.team_id is team.id
|
331
|
+
assert res.raw is not None
|
332
|
+
assert res.json_dict is not None
|
333
|
+
assert team.managers[0].agent.id is agent_b.id
|
334
|
+
assert len(res.task_output_list) == 2
|
335
|
+
assert [item.raw is not None for item in res.task_output_list]
|
336
|
+
assert len(team.tasks) == 2
|
337
|
+
assert team.tasks[0].output.raw == res.raw
|
338
|
+
|
339
|
+
|
340
|
+
def test_handle_team_task():
|
341
|
+
"""
|
342
|
+
Make the best team formation with agents and tasks given.
|
343
|
+
"""
|
344
|
+
|
345
|
+
agent_a = Agent(role="agent a", goal="My amazing goals", llm=MODEL_NAME)
|
346
|
+
agent_b = Agent(role="agent b", goal="My amazing goals", llm=MODEL_NAME)
|
347
|
+
agent_c = Agent(role="agent c", goal="My amazing goals", llm=MODEL_NAME)
|
348
|
+
team_task = Task(
|
349
|
+
description="Define outbound strategies.",
|
350
|
+
output_field_list=[ResponseField(title="team_task_1", type=str, required=True),],
|
351
|
+
)
|
352
|
+
task_1 = Task(
|
353
|
+
description="Analyze the client's business model.",
|
354
|
+
output_field_list=[ResponseField(title="task_1", type=str, required=True),],
|
355
|
+
)
|
356
|
+
task_2 = Task(
|
357
|
+
description="Define the cohort timeframe.",
|
358
|
+
output_field_list=[
|
359
|
+
ResponseField(title="task_2_1", type=int, required=True),
|
360
|
+
ResponseField(title="task_2_2", type=list, required=True),
|
361
|
+
],
|
362
|
+
)
|
363
|
+
team_solo = Team(
|
364
|
+
members=[
|
365
|
+
TeamMember(agent=agent_c, is_manager=False)
|
366
|
+
],
|
367
|
+
team_tasks=[team_task, task_1, task_2, ]
|
368
|
+
)
|
369
|
+
team_flat = Team(
|
370
|
+
members=[
|
371
|
+
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
372
|
+
TeamMember(agent=agent_c, is_manager=False)
|
373
|
+
],
|
374
|
+
team_tasks=[team_task, task_2,]
|
375
|
+
)
|
376
|
+
team_leader = Team(
|
377
|
+
members=[
|
378
|
+
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
379
|
+
TeamMember(agent=agent_b, is_manager=True, task=task_2),
|
380
|
+
TeamMember(agent=agent_c, is_manager=False)
|
381
|
+
],
|
382
|
+
team_tasks=[team_task, ]
|
383
|
+
)
|
384
|
+
team_dual_leaders = Team(
|
385
|
+
members=[
|
386
|
+
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
387
|
+
TeamMember(agent=agent_b, is_manager=True, task=task_2),
|
388
|
+
TeamMember(agent=agent_c, is_manager=True)
|
389
|
+
],
|
390
|
+
team_tasks=[team_task, ]
|
391
|
+
)
|
392
|
+
team_leader_without_task = Team(
|
393
|
+
members=[
|
394
|
+
TeamMember(agent=agent_a, is_manager=False, task=task_1),
|
395
|
+
TeamMember(agent=agent_b, is_manager=False, task=task_2),
|
396
|
+
TeamMember(agent=agent_c, is_manager=True)
|
397
|
+
],
|
398
|
+
team_tasks=[team_task,]
|
399
|
+
)
|
400
|
+
teams = [team_solo, team_flat, team_leader, team_dual_leaders, team_leader_without_task]
|
401
|
+
|
402
|
+
for team in teams:
|
403
|
+
res = team.kickoff()
|
404
|
+
assert team._get_responsible_agent(task=team_task) is not None
|
405
|
+
assert isinstance(res, TeamOutput)
|
406
|
+
assert res.team_id is team.id
|
407
|
+
assert team.tasks[0].id is team_task.id
|
408
|
+
assert res.raw is not None
|
409
|
+
assert len(team.members) == 3
|
410
|
+
assert len(team.tasks) == 3
|
411
|
+
|
282
412
|
|
283
413
|
if __name__ == "__main__":
|
284
|
-
|
285
|
-
# kickoff with teamleader, async, task handling process
|
414
|
+
test_handle_team_task()
|
@@ -1,55 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
from dotenv import load_dotenv
|
3
|
-
from typing import Any, List, Optional
|
4
|
-
from pydantic import BaseModel, Field
|
5
|
-
|
6
|
-
from versionhq.agent.model import Agent
|
7
|
-
from versionhq.task.model import Task, ResponseField
|
8
|
-
|
9
|
-
load_dotenv(override=True)
|
10
|
-
|
11
|
-
|
12
|
-
class TeamPlanner:
|
13
|
-
"""
|
14
|
-
(Optional) Plan how the team should handle multiple tasks using LLM.
|
15
|
-
"""
|
16
|
-
|
17
|
-
def __init__(self, tasks: List[Task], planner_llm: Optional[Any] = None):
|
18
|
-
self.tasks = tasks
|
19
|
-
self.planner_llm = (
|
20
|
-
planner_llm if planner_llm != None else os.environ.get("LITELLM_MODEL_NAME")
|
21
|
-
)
|
22
|
-
|
23
|
-
def _handle_task_planning(self) -> BaseModel:
|
24
|
-
"""
|
25
|
-
Handles the team planning by creating detailed step-by-step plans for each task.
|
26
|
-
"""
|
27
|
-
|
28
|
-
planning_agent = Agent(
|
29
|
-
role="Task Execution Planner",
|
30
|
-
goal="Your goal is to create an 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",
|
31
|
-
backstory="You have a strong ability to design efficient organizational structures and task processes, minimizing unnecessary steps.",
|
32
|
-
llm=self.planner_llm,
|
33
|
-
)
|
34
|
-
|
35
|
-
task_summary_list = [task.summary for task in self.tasks]
|
36
|
-
task_to_handle = Task(
|
37
|
-
description=f"""
|
38
|
-
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
|
-
|
40
|
-
Task summaries: {" ".join(task_summary_list)}
|
41
|
-
""",
|
42
|
-
expected_output_json=False,
|
43
|
-
expected_output_pydantic=True,
|
44
|
-
output_field_list=[
|
45
|
-
ResponseField(title=f"{task.id}", type=str, required=True)
|
46
|
-
for task in self.tasks
|
47
|
-
],
|
48
|
-
)
|
49
|
-
task_output = task_to_handle.execute_sync(agent=planning_agent)
|
50
|
-
|
51
|
-
if isinstance(task_output.pydantic, BaseModel):
|
52
|
-
return task_output.pydantic
|
53
|
-
|
54
|
-
else:
|
55
|
-
return None
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|