versionhq 1.1.9.0__py3-none-any.whl → 1.1.9.2__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 +1 -1
- versionhq/_utils/cache_handler.py +2 -2
- versionhq/_utils/logger.py +12 -19
- versionhq/_utils/rpm_controller.py +6 -5
- versionhq/_utils/usage_metrics.py +1 -1
- versionhq/agent/model.py +59 -46
- versionhq/agent/parser.py +1 -12
- versionhq/clients/customer/__init__.py +5 -0
- versionhq/clients/customer/model.py +27 -31
- versionhq/clients/product/model.py +9 -8
- versionhq/clients/workflow/model.py +18 -24
- versionhq/task/model.py +56 -37
- versionhq/team/model.py +8 -9
- versionhq/tool/composio.py +14 -2
- versionhq/tool/model.py +70 -62
- versionhq/tool/tool_handler.py +17 -15
- {versionhq-1.1.9.0.dist-info → versionhq-1.1.9.2.dist-info}/METADATA +1 -1
- {versionhq-1.1.9.0.dist-info → versionhq-1.1.9.2.dist-info}/RECORD +21 -21
- {versionhq-1.1.9.0.dist-info → versionhq-1.1.9.2.dist-info}/LICENSE +0 -0
- {versionhq-1.1.9.0.dist-info → versionhq-1.1.9.2.dist-info}/WHEEL +0 -0
- {versionhq-1.1.9.0.dist-info → versionhq-1.1.9.2.dist-info}/top_level.txt +0 -0
versionhq/task/model.py
CHANGED
@@ -4,7 +4,7 @@ import uuid
|
|
4
4
|
from concurrent.futures import Future
|
5
5
|
from hashlib import md5
|
6
6
|
from typing import Any, Dict, List, Set, Optional, Tuple, Callable, Type
|
7
|
-
from typing_extensions import Annotated
|
7
|
+
from typing_extensions import Annotated, Self
|
8
8
|
|
9
9
|
from pydantic import UUID4, BaseModel, Field, PrivateAttr, field_validator, model_validator, create_model, InstanceOf
|
10
10
|
from pydantic_core import PydanticCustomError
|
@@ -72,6 +72,7 @@ class TaskOutput(BaseModel):
|
|
72
72
|
raw: str = Field(default="", description="Raw output of the task")
|
73
73
|
json_dict: Dict[str, Any] = Field(default=None, description="`raw` converted to dictionary")
|
74
74
|
pydantic: Optional[Any] = Field(default=None, description="`raw` converted to the abs. pydantic model")
|
75
|
+
tool_output: Optional[Any] = Field(default=None, description="store tool result when the task takes tool output as its final output")
|
75
76
|
|
76
77
|
def __str__(self) -> str:
|
77
78
|
return str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw
|
@@ -125,6 +126,7 @@ class Task(BaseModel):
|
|
125
126
|
_original_description: str = PrivateAttr(default=None)
|
126
127
|
_logger: Logger = PrivateAttr()
|
127
128
|
_task_output_handler = TaskOutputStorageHandler()
|
129
|
+
config: Optional[Dict[str, Any]] = Field(default=None, description="configuration for the agent")
|
128
130
|
|
129
131
|
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="unique identifier for the object, not set by user")
|
130
132
|
name: Optional[str] = Field(default=None)
|
@@ -134,33 +136,35 @@ class Task(BaseModel):
|
|
134
136
|
expected_output_json: bool = Field(default=True)
|
135
137
|
expected_output_pydantic: bool = Field(default=False)
|
136
138
|
output_field_list: List[ResponseField] = Field(
|
137
|
-
|
139
|
+
default_factory=list,
|
138
140
|
description="provide output key and data type. this will be cascaded to the agent via task.prompt()"
|
139
141
|
)
|
140
142
|
output: Optional[TaskOutput] = Field(default=None, description="store the final task output in TaskOutput class")
|
141
143
|
|
142
144
|
# task setup
|
143
145
|
context: Optional[List["Task"]] = Field(default=None, description="other tasks whose outputs should be used as context")
|
144
|
-
|
146
|
+
prompt_context: Optional[str] = Field(default=None)
|
147
|
+
|
148
|
+
# tool usage
|
149
|
+
tools: Optional[List[ToolSet | Tool | Any]] = Field(default_factory=list, description="tools that the agent can use aside from their tools")
|
150
|
+
can_use_agent_tools: bool = Field(default=False, description="whether the agent can use their own tools when executing the task")
|
145
151
|
take_tool_res_as_final: bool = Field(default=False, description="when set True, tools res will be stored in the `TaskOutput`")
|
146
|
-
allow_delegation: bool = Field(default=False, description="ask other agents for help and run the task instead")
|
147
152
|
|
148
|
-
|
153
|
+
# execution rules
|
154
|
+
allow_delegation: bool = Field(default=False, description="ask other agents for help and run the task instead")
|
149
155
|
async_execution: bool = Field(default=False,description="whether the task should be executed asynchronously or not")
|
150
|
-
config: Optional[Dict[str, Any]] = Field(default=None, description="configuration for the agent")
|
151
156
|
callback: Optional[Any] = Field(default=None, description="callback to be executed after the task is completed.")
|
152
157
|
callback_kwargs: Optional[Dict[str, Any]] = Field(default_factory=dict, description="kwargs for the callback when the callback is callable")
|
153
158
|
|
154
159
|
# recording
|
155
160
|
processed_by_agents: Set[str] = Field(default_factory=set, description="store responsible agents' roles")
|
156
|
-
used_tools: int = 0
|
157
161
|
tools_errors: int = 0
|
158
162
|
delegations: int = 0
|
159
163
|
|
160
164
|
|
161
165
|
@model_validator(mode="before")
|
162
166
|
@classmethod
|
163
|
-
def process_model_config(cls, values: Dict[str, Any]):
|
167
|
+
def process_model_config(cls, values: Dict[str, Any]) -> None:
|
164
168
|
return process_config(values_to_update=values, model_class=cls)
|
165
169
|
|
166
170
|
|
@@ -172,7 +176,7 @@ class Task(BaseModel):
|
|
172
176
|
|
173
177
|
|
174
178
|
@model_validator(mode="after")
|
175
|
-
def validate_required_fields(self):
|
179
|
+
def validate_required_fields(self) -> Self:
|
176
180
|
required_fields = ["description",]
|
177
181
|
for field in required_fields:
|
178
182
|
if getattr(self, field) is None:
|
@@ -181,7 +185,7 @@ class Task(BaseModel):
|
|
181
185
|
|
182
186
|
|
183
187
|
@model_validator(mode="after")
|
184
|
-
def set_attributes_based_on_config(self) ->
|
188
|
+
def set_attributes_based_on_config(self) -> Self:
|
185
189
|
"""
|
186
190
|
Set attributes based on the agent configuration.
|
187
191
|
"""
|
@@ -192,12 +196,21 @@ class Task(BaseModel):
|
|
192
196
|
return self
|
193
197
|
|
194
198
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
199
|
+
@model_validator(mode="after")
|
200
|
+
def set_up_tools(self) -> Self:
|
201
|
+
if not self.tools:
|
202
|
+
pass
|
203
|
+
else:
|
204
|
+
tool_list = []
|
205
|
+
for item in self.tools:
|
206
|
+
if isinstance(item, Tool) or isinstance(item, ToolSet):
|
207
|
+
tool_list.append(item)
|
208
|
+
elif (isinstance(item, dict) and "function" not in item) or isinstance(item, str):
|
209
|
+
pass
|
210
|
+
else:
|
211
|
+
tool_list.append(item) # address custom tool
|
212
|
+
self.tools = tool_list
|
213
|
+
return self
|
201
214
|
|
202
215
|
|
203
216
|
@model_validator(mode="after")
|
@@ -261,7 +274,6 @@ class Task(BaseModel):
|
|
261
274
|
return output_json_dict
|
262
275
|
|
263
276
|
|
264
|
-
|
265
277
|
def create_pydantic_output(self, output_json_dict: Dict[str, Any], raw_result: Any = None) -> Optional[Any]:
|
266
278
|
"""
|
267
279
|
Create pydantic output from the `raw` result.
|
@@ -302,11 +314,10 @@ class Task(BaseModel):
|
|
302
314
|
"""
|
303
315
|
if inputs:
|
304
316
|
self.description = self._original_description.format(**inputs)
|
305
|
-
# self.expected_output = self._original_expected_output.format(**inputs)
|
306
317
|
|
307
318
|
|
308
319
|
# task execution
|
309
|
-
def execute_sync(self, agent, context: Optional[str] = None
|
320
|
+
def execute_sync(self, agent, context: Optional[str] = None) -> TaskOutput:
|
310
321
|
"""
|
311
322
|
Execute the task synchronously.
|
312
323
|
When the task has context, make sure we have executed all the tasks in the context first.
|
@@ -320,26 +331,26 @@ class Task(BaseModel):
|
|
320
331
|
return self._execute_core(agent, context)
|
321
332
|
|
322
333
|
|
323
|
-
def execute_async(self, agent, context: Optional[str] = None
|
334
|
+
def execute_async(self, agent, context: Optional[str] = None) -> Future[TaskOutput]:
|
324
335
|
"""
|
325
336
|
Execute the task asynchronously.
|
326
337
|
"""
|
327
338
|
|
328
339
|
future: Future[TaskOutput] = Future()
|
329
|
-
threading.Thread(daemon=True, target=self._execute_task_async, args=(agent, context,
|
340
|
+
threading.Thread(daemon=True, target=self._execute_task_async, args=(agent, context, future)).start()
|
330
341
|
return future
|
331
342
|
|
332
343
|
|
333
|
-
def _execute_task_async(self, agent, context: Optional[str],
|
344
|
+
def _execute_task_async(self, agent, context: Optional[str], future: Future[TaskOutput]) -> None:
|
334
345
|
"""
|
335
346
|
Execute the task asynchronously with context handling.
|
336
347
|
"""
|
337
348
|
|
338
|
-
result = self._execute_core(agent, context
|
349
|
+
result = self._execute_core(agent, context)
|
339
350
|
future.set_result(result)
|
340
351
|
|
341
352
|
|
342
|
-
def _execute_core(self, agent, context: Optional[str]
|
353
|
+
def _execute_core(self, agent, context: Optional[str]) -> TaskOutput:
|
343
354
|
"""
|
344
355
|
Run the core execution logic of the task.
|
345
356
|
To speed up the process, when the format is not expected to return, we will skip the conversion process.
|
@@ -349,13 +360,14 @@ class Task(BaseModel):
|
|
349
360
|
from versionhq.team.model import Team
|
350
361
|
|
351
362
|
self.prompt_context = context
|
363
|
+
task_output: InstanceOf[TaskOutput] = None
|
352
364
|
|
353
365
|
if self.allow_delegation:
|
354
366
|
agent_to_delegate = None
|
355
367
|
|
356
368
|
if hasattr(agent, "team") and isinstance(agent.team, Team):
|
357
369
|
if agent.team.managers:
|
358
|
-
idling_manager_agents = [manager.agent for manager in agent.team.managers if manager.
|
370
|
+
idling_manager_agents = [manager.agent for manager in agent.team.managers if manager.is_idling]
|
359
371
|
agent_to_delegate = idling_manager_agents[0] if idling_manager_agents else agent.team.managers[0]
|
360
372
|
else:
|
361
373
|
peers = [member.agent for member in agent.team.members if member.is_manager == False and member.agent.id is not agent.id]
|
@@ -367,20 +379,26 @@ class Task(BaseModel):
|
|
367
379
|
agent = agent_to_delegate
|
368
380
|
self.delegations += 1
|
369
381
|
|
370
|
-
|
382
|
+
if self.take_tool_res_as_final is True:
|
383
|
+
output = agent.execute_task(task=self, context=context)
|
384
|
+
task_output = TaskOutput(task_id=self.id, tool_output=output)
|
371
385
|
|
372
|
-
|
373
|
-
output_json_dict =
|
386
|
+
else:
|
387
|
+
output_raw, output_json_dict, output_pydantic = agent.execute_task(task=self, context=context), None, None
|
374
388
|
|
375
|
-
|
376
|
-
|
389
|
+
if self.expected_output_json:
|
390
|
+
output_json_dict = self.create_json_output(raw_result=output_raw)
|
391
|
+
|
392
|
+
if self.expected_output_pydantic:
|
393
|
+
output_pydantic = self.create_pydantic_output(output_json_dict=output_json_dict)
|
394
|
+
|
395
|
+
task_output = TaskOutput(
|
396
|
+
task_id=self.id,
|
397
|
+
raw=output_raw,
|
398
|
+
pydantic=output_pydantic,
|
399
|
+
json_dict=output_json_dict
|
400
|
+
)
|
377
401
|
|
378
|
-
task_output = TaskOutput(
|
379
|
-
task_id=self.id,
|
380
|
-
raw=output_raw,
|
381
|
-
pydantic=output_pydantic,
|
382
|
-
json_dict=output_json_dict
|
383
|
-
)
|
384
402
|
self.output = task_output
|
385
403
|
self.processed_by_agents.add(agent.role)
|
386
404
|
|
@@ -400,6 +418,7 @@ class Task(BaseModel):
|
|
400
418
|
# else pydantic_output.model_dump_json() if pydantic_output else result
|
401
419
|
# )
|
402
420
|
# self._save_file(content)
|
421
|
+
|
403
422
|
return task_output
|
404
423
|
|
405
424
|
|
@@ -463,7 +482,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
|
|
463
482
|
Task ID: {str(self.id)}
|
464
483
|
"Description": {self.description}
|
465
484
|
"Prompt": {self.output_prompt}
|
466
|
-
"Tools": {", ".join([
|
485
|
+
"Tools": {", ".join([tool.name for tool in self.tools])}
|
467
486
|
"""
|
468
487
|
|
469
488
|
|
versionhq/team/model.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import uuid
|
2
2
|
import warnings
|
3
3
|
import json
|
4
|
-
from abc import ABC
|
5
4
|
from enum import Enum
|
6
5
|
from dotenv import load_dotenv
|
7
6
|
from concurrent.futures import Future
|
@@ -108,13 +107,14 @@ class TeamOutput(BaseModel):
|
|
108
107
|
return res
|
109
108
|
|
110
109
|
|
111
|
-
class TeamMember(
|
110
|
+
class TeamMember(BaseModel):
|
112
111
|
agent: Agent | None = Field(default=None, description="store the agent to be a member")
|
113
112
|
is_manager: bool = Field(default=False)
|
114
113
|
task: Optional[Task] = Field(default=None)
|
115
114
|
|
116
|
-
|
117
|
-
|
115
|
+
@property
|
116
|
+
def is_idling(self):
|
117
|
+
return bool(self.task is None)
|
118
118
|
|
119
119
|
|
120
120
|
class Team(BaseModel):
|
@@ -253,8 +253,8 @@ class Team(BaseModel):
|
|
253
253
|
"""
|
254
254
|
|
255
255
|
team_planner = TeamPlanner(tasks=self.tasks, planner_llm=self.planning_llm)
|
256
|
-
idling_managers: List[TeamMember] = [member for member in self.members if member.
|
257
|
-
idling_members: List[TeamMember] = [member for member in self.members if member.
|
256
|
+
idling_managers: List[TeamMember] = [member for member in self.members if member.is_idling and member.is_manager is True]
|
257
|
+
idling_members: List[TeamMember] = [member for member in self.members if member.is_idling and member.is_manager is False]
|
258
258
|
unassigned_tasks: List[Task] = self.member_tasks_without_agent
|
259
259
|
new_team_members: List[TeamMember] = []
|
260
260
|
|
@@ -380,16 +380,15 @@ class Team(BaseModel):
|
|
380
380
|
if skipped_task_output:
|
381
381
|
continue
|
382
382
|
|
383
|
-
# self._prepare_agent_tools(task)
|
384
383
|
# self._log_task_start(task, responsible_agent)
|
385
384
|
|
386
385
|
if task.async_execution:
|
387
386
|
context = create_raw_outputs(tasks=[task, ], task_outputs=([last_sync_output,] if last_sync_output else []))
|
388
|
-
future = task.execute_async(agent=responsible_agent, context=context
|
387
|
+
future = task.execute_async(agent=responsible_agent, context=context)
|
389
388
|
futures.append((task, future, task_index))
|
390
389
|
else:
|
391
390
|
context = create_raw_outputs(tasks=[task,], task_outputs=([last_sync_output,] if last_sync_output else [] ))
|
392
|
-
task_output = task.execute_sync(agent=responsible_agent, context=context
|
391
|
+
task_output = task.execute_sync(agent=responsible_agent, context=context)
|
393
392
|
if self.managers and responsible_agent in [manager.agent for manager in self.managers]:
|
394
393
|
lead_task_output = task_output
|
395
394
|
|
versionhq/tool/composio.py
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
import os
|
2
2
|
import uuid
|
3
|
+
from abc import ABC
|
3
4
|
from dotenv import load_dotenv
|
4
5
|
from typing import Any, Callable, Type, get_args, get_origin, Optional, Tuple
|
5
6
|
|
6
|
-
from pydantic import BaseModel, Field, model_validator, field_validator, UUID4
|
7
|
+
from pydantic import BaseModel, Field, model_validator, field_validator, UUID4, PrivateAttr
|
7
8
|
from pydantic_core import PydanticCustomError
|
8
9
|
|
9
10
|
from composio import ComposioToolSet, Action, App, action
|
10
11
|
|
11
12
|
from versionhq.tool import ComposioAppName, ComposioAuthScheme, composio_app_set, COMPOSIO_STATUS
|
13
|
+
from versionhq._utils.logger import Logger
|
14
|
+
from versionhq._utils.cache_handler import CacheHandler
|
12
15
|
|
13
16
|
load_dotenv(override=True)
|
14
17
|
|
@@ -16,11 +19,14 @@ DEFAULT_REDIRECT_URL = os.environ.get("DEFAULT_REDIRECT_URL", None)
|
|
16
19
|
DEFAULT_USER_ID = os.environ.get("DEFAULT_USER_ID", None)
|
17
20
|
|
18
21
|
|
19
|
-
class Composio(BaseModel):
|
22
|
+
class Composio(ABC, BaseModel):
|
20
23
|
"""
|
21
24
|
Class to handle composio tools.
|
22
25
|
"""
|
23
26
|
|
27
|
+
_logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
|
28
|
+
_cache: CacheHandler
|
29
|
+
|
24
30
|
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
25
31
|
app_name: str = Field(default=ComposioAppName.HUBSPOT)
|
26
32
|
user_id: str = Field(default=DEFAULT_USER_ID)
|
@@ -125,6 +131,12 @@ class Composio(BaseModel):
|
|
125
131
|
if connected_account.status is not COMPOSIO_STATUS.FAILED:
|
126
132
|
setattr(self.toolset, "entity_id", self.user_id)
|
127
133
|
|
134
|
+
else:
|
135
|
+
self._logger.log(level="error", message="The account is not valid.", color="red")
|
136
|
+
|
137
|
+
else:
|
138
|
+
self._logger.log(level="error", message="Connection to composio failed.", color="red")
|
139
|
+
|
128
140
|
return connected_account, connected_account.status if connected_account else connection_request.connectionStatus
|
129
141
|
|
130
142
|
|
versionhq/tool/model.py
CHANGED
@@ -1,20 +1,21 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
from inspect import signature
|
3
3
|
from typing import Any, Dict, Callable, Type, Optional, get_args, get_origin
|
4
|
+
from typing_extensions import Self
|
4
5
|
from pydantic import InstanceOf, BaseModel, ConfigDict, Field, field_validator, model_validator
|
5
6
|
|
6
7
|
from versionhq._utils.cache_handler import CacheHandler
|
7
8
|
|
8
9
|
|
9
10
|
class BaseTool(ABC, BaseModel):
|
11
|
+
"""
|
12
|
+
Abstract class for Tool class.
|
13
|
+
"""
|
10
14
|
|
11
15
|
class _ArgsSchemaPlaceholder(BaseModel):
|
12
16
|
pass
|
13
17
|
|
14
|
-
name: str = Field(default=None)
|
15
|
-
goal: str = Field(default=None)
|
16
18
|
args_schema: Type[BaseModel] = Field(default_factory=_ArgsSchemaPlaceholder)
|
17
|
-
cache_function: Callable = lambda _args=None, _result=None: True
|
18
19
|
|
19
20
|
|
20
21
|
@field_validator("args_schema", mode="before")
|
@@ -26,11 +27,7 @@ class BaseTool(ABC, BaseModel):
|
|
26
27
|
return type(
|
27
28
|
f"{cls.__name__}Schema",
|
28
29
|
(BaseModel,),
|
29
|
-
{
|
30
|
-
"__annotations__": {
|
31
|
-
k: v for k, v in cls._run.__annotations__.items() if k != "return"
|
32
|
-
},
|
33
|
-
},
|
30
|
+
{ "__annotations__": { k: v for k, v in cls._run.__annotations__.items() if k != "return" }},
|
34
31
|
)
|
35
32
|
|
36
33
|
|
@@ -39,8 +36,36 @@ class BaseTool(ABC, BaseModel):
|
|
39
36
|
"""any handling"""
|
40
37
|
|
41
38
|
|
42
|
-
|
43
|
-
|
39
|
+
|
40
|
+
class Tool(BaseTool):
|
41
|
+
name: str = Field(default=None)
|
42
|
+
goal: str = Field(default=None)
|
43
|
+
function: Callable = Field(default=None)
|
44
|
+
tool_handler: Optional[Dict[str, Any] | Any] = Field(default=None, description="store tool_handler to record the usage of this tool")
|
45
|
+
should_cache: bool = Field(default=True, description="whether the tool usage should be cached")
|
46
|
+
cache_function: Callable = lambda _args=None, _result=None: True
|
47
|
+
cache_handler: Optional[InstanceOf[CacheHandler]] = Field(default=None)
|
48
|
+
|
49
|
+
|
50
|
+
@model_validator(mode="after")
|
51
|
+
def set_up_tool_handler(self) -> Self:
|
52
|
+
from versionhq.tool.tool_handler import ToolHandler
|
53
|
+
|
54
|
+
if self.tool_handler and not isinstance(self.tool_handler, ToolHandler):
|
55
|
+
ToolHandler(**self.tool_handler)
|
56
|
+
|
57
|
+
else:
|
58
|
+
self.tool_handler = ToolHandler(cache_handler=self.cache_handler, should_cache=self.should_cache)
|
59
|
+
|
60
|
+
return self
|
61
|
+
|
62
|
+
|
63
|
+
@model_validator(mode="after")
|
64
|
+
def set_up_function(self) -> Self:
|
65
|
+
if self.function is None:
|
66
|
+
self.function = self._run
|
67
|
+
self._set_args_schema_from_func()
|
68
|
+
return self
|
44
69
|
|
45
70
|
|
46
71
|
@staticmethod
|
@@ -85,88 +110,71 @@ class BaseTool(ABC, BaseModel):
|
|
85
110
|
self.args_schema = type(
|
86
111
|
class_name,
|
87
112
|
(BaseModel,),
|
88
|
-
{ "__annotations__": {
|
113
|
+
{ "__annotations__": {
|
114
|
+
k: v for k, v in self._run.__annotations__.items() if k != "return"
|
115
|
+
} },
|
89
116
|
)
|
90
117
|
return self
|
91
118
|
|
92
119
|
|
93
|
-
@property
|
94
|
-
def description(self) -> str:
|
95
|
-
args_schema = {
|
96
|
-
name: {
|
97
|
-
"description": field.description,
|
98
|
-
"type": self._get_arg_annotations(field.annotation),
|
99
|
-
}
|
100
|
-
for name, field in self.args_schema.model_fields.items()
|
101
|
-
}
|
102
|
-
|
103
|
-
return f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nGoal: {self.goal}"
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
class Tool(BaseTool):
|
108
|
-
|
109
|
-
function: Callable = Field(default=None)
|
110
|
-
tool_handler: Optional[Dict[str, Any]] = Field(
|
111
|
-
default=None,
|
112
|
-
description="store tool_handler to record the usage of this tool. to avoid circular import, set as Dict format",
|
113
|
-
)
|
114
|
-
|
115
|
-
@model_validator(mode="after")
|
116
|
-
def set_up_tool_handler(self):
|
117
|
-
from versionhq.tool.tool_handler import ToolHandler
|
118
|
-
|
119
|
-
if self.tool_handler:
|
120
|
-
ToolHandler(**self.tool_handler)
|
121
|
-
return self
|
122
|
-
|
123
|
-
@model_validator(mode="after")
|
124
|
-
def set_up_function(self):
|
125
|
-
if self.function is None:
|
126
|
-
self.function = self._run
|
127
|
-
self._set_args_schema_from_func()
|
128
|
-
return self
|
129
|
-
|
130
|
-
|
131
120
|
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
132
|
-
return self.
|
121
|
+
return self.run(*args, **kwargs)
|
133
122
|
|
134
123
|
|
135
124
|
def run(self, *args, **kwargs) -> Any:
|
136
125
|
"""
|
137
|
-
Use tool
|
126
|
+
Use tool and record its usage if should_cache is True.
|
138
127
|
"""
|
139
128
|
from versionhq.tool.tool_handler import ToolHandler
|
140
129
|
|
141
130
|
result = None
|
131
|
+
tool_set = ToolSet(tool=self, kwargs={})
|
142
132
|
|
143
|
-
if self.function
|
133
|
+
if self.function:
|
144
134
|
result = self.function(*args, **kwargs)
|
145
135
|
|
146
136
|
else:
|
147
137
|
acceptable_args = self.args_schema.model_json_schema()["properties"].keys()
|
148
138
|
acceptable_kwargs = { k: v for k, v in kwargs.items() if k in acceptable_args }
|
149
|
-
|
139
|
+
tool_set = ToolSet(tool=self, kwargs=acceptable_kwargs)
|
150
140
|
|
151
141
|
if self.tool_handler:
|
152
|
-
if self.tool_handler.has_called_before(
|
142
|
+
if self.tool_handler.has_called_before(tool_set):
|
153
143
|
self.tool_handler.error = "Agent execution error"
|
154
144
|
|
155
145
|
elif self.tool_handler.cache:
|
156
|
-
result = self.tools_handler.cache.read(tool=
|
146
|
+
result = self.tools_handler.cache.read(tool=tool_set.tool.name, input=tool_set.kwargs)
|
157
147
|
if result is None:
|
158
|
-
parsed_kwargs = self._parse_args(
|
159
|
-
result = self.function(**parsed_kwargs)
|
148
|
+
parsed_kwargs = self._parse_args(raw_args=acceptable_kwargs)
|
149
|
+
result = self.function(**parsed_kwargs) if self.function else None
|
160
150
|
|
161
151
|
else:
|
162
|
-
tool_handler = ToolHandler(last_used_tool=
|
152
|
+
tool_handler = ToolHandler(last_used_tool=tool_set, cache_handler=self.cache_handler, should_cache=self.should_cache)
|
163
153
|
self.tool_handler = tool_handler
|
164
|
-
parsed_kwargs = self._parse_args(
|
165
|
-
result = self.function(**parsed_kwargs)
|
154
|
+
parsed_kwargs = self._parse_args(raw_args=acceptable_kwargs)
|
155
|
+
result = self.function(**parsed_kwargs) if self.function else None
|
156
|
+
|
157
|
+
|
158
|
+
if self.should_cache is True:
|
159
|
+
self.tool_handler.record_last_tool_used(tool_set, result, self.should_cache)
|
166
160
|
|
167
161
|
return result
|
168
162
|
|
169
163
|
|
164
|
+
@property
|
165
|
+
def description(self) -> str:
|
166
|
+
args_schema = {
|
167
|
+
name: {
|
168
|
+
"description": field.description,
|
169
|
+
"type": self._get_arg_annotations(field.annotation),
|
170
|
+
}
|
171
|
+
for name, field in self.args_schema.model_fields.items()
|
172
|
+
}
|
173
|
+
|
174
|
+
return f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nGoal: {self.goal}"
|
175
|
+
|
176
|
+
|
177
|
+
|
170
178
|
# @classmethod
|
171
179
|
# def from_composio(
|
172
180
|
# cls, func: Callable = None, tool_name: str = "Composio tool"
|
@@ -204,12 +212,12 @@ class ToolSet(BaseModel):
|
|
204
212
|
"""
|
205
213
|
Store the tool called and any kwargs used.
|
206
214
|
"""
|
207
|
-
tool: InstanceOf[Tool] = Field(..., description="store the tool instance to be called.")
|
215
|
+
tool: InstanceOf[Tool] | Any = Field(..., description="store the tool instance to be called.")
|
208
216
|
kwargs: Optional[Dict[str, Any]] = Field(..., description="kwargs passed to the tool")
|
209
217
|
|
210
218
|
|
211
219
|
class InstructorToolSet(BaseModel):
|
212
|
-
tool: InstanceOf[Tool] = Field(..., description="store the tool instance to be called.")
|
220
|
+
tool: InstanceOf[Tool] | Any = Field(..., description="store the tool instance to be called.")
|
213
221
|
kwargs: Optional[Dict[str, Any]] = Field(..., description="kwargs passed to the tool")
|
214
222
|
|
215
223
|
|
versionhq/tool/tool_handler.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Optional, Any
|
2
2
|
from pydantic import InstanceOf
|
3
|
-
|
3
|
+
|
4
|
+
from versionhq.tool.model import ToolSet, InstructorToolSet
|
4
5
|
from versionhq._utils.cache_handler import CacheHandler
|
5
6
|
|
6
7
|
|
@@ -12,32 +13,33 @@ class ToolHandler:
|
|
12
13
|
last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet]
|
13
14
|
cache: Optional[CacheHandler]
|
14
15
|
error: Optional[str]
|
16
|
+
should_cache: bool
|
15
17
|
|
16
18
|
def __init__(
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
self,
|
20
|
+
last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet] = None,
|
21
|
+
cache_handler: Optional[CacheHandler] = None,
|
22
|
+
should_cache: bool = True
|
20
23
|
):
|
21
24
|
self.cache = cache_handler
|
22
25
|
self.last_used_tool = last_used_tool
|
26
|
+
self.should_cache = should_cache
|
23
27
|
|
24
28
|
|
25
29
|
def record_last_tool_used(
|
26
|
-
self,
|
27
|
-
last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet],
|
28
|
-
output: str,
|
29
|
-
should_cache: bool = True,
|
30
|
+
self, last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet], output: str, should_cache: bool = True
|
30
31
|
) -> Any:
|
32
|
+
from versionhq.tool.model import CacheTool
|
31
33
|
|
32
34
|
self.last_used_tool = last_used_tool
|
33
35
|
|
34
|
-
if self.cache and should_cache and last_used_tool.
|
35
|
-
self.cache.add(last_used_tool.
|
36
|
+
if self.cache and should_cache and last_used_tool.tool.name != CacheTool().name:
|
37
|
+
self.cache.add(tool=last_used_tool.tool.name, input=last_used_tool.kwargs, output=output)
|
36
38
|
|
37
39
|
|
38
|
-
def has_called_before(self,
|
39
|
-
if
|
40
|
+
def has_called_before(self, tool_set: ToolSet = None) -> bool:
|
41
|
+
if tool_set is None or not self.last_used_tool:
|
40
42
|
return False
|
41
43
|
|
42
|
-
if
|
43
|
-
return bool((
|
44
|
+
if tool_set := self.last_used_tool:
|
45
|
+
return bool((tool_set.tool.name == self.last_used_tool.tool.name) and (tool_set.kwargs == self.last_used_tool.kwargs))
|