versionhq 1.1.4.4__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.
@@ -0,0 +1,102 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from typing import Any, Callable, Type, get_args, get_origin
4
+ from composio import ComposioToolSet, Action, App
5
+
6
+
7
+ load_dotenv(override=True)
8
+ DEFAULT_REDIRECT_URL = os.environ.get("DEFAULT_REDIRECT_URL")
9
+ DEFAULT_APP_NAME = "hubspot"
10
+ DEFAULT_USER_ID = os.environ.get("DEFAULT_USER_ID")
11
+
12
+
13
+ def connect(app_name: str = DEFAULT_APP_NAME, user_id: str = DEFAULT_USER_ID, redirect_url: str = DEFAULT_REDIRECT_URL, *args, **kwargs):
14
+ """
15
+ Connect with the data pipelines or destination services.
16
+ """
17
+
18
+ composio_toolset = ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"))
19
+
20
+
21
+ if not user_id:
22
+ return None
23
+
24
+ auth_scheme = "OAUTH2"
25
+ connection_request = composio_toolset.initiate_connection(
26
+ app=app_name,
27
+ redirect_url = redirect_url, # user comes here after oauth flow
28
+ entity_id=user_id,
29
+ auth_scheme=auth_scheme,
30
+ )
31
+
32
+ print(connection_request.connectedAccountId,connection_request.connectionStatus)
33
+ print(connection_request.redirectUrl)
34
+
35
+
36
+
37
+ # connection_request_id = "connection_request_id" # replace connection_request_id from earlier response
38
+ # # validate the connection is active
39
+ # connected_account = composio_toolset.get_connected_account(id=connection_request_id)
40
+ # print(connected_account.status) # should be
41
+
42
+
43
+ # @action(toolname="hubspot")
44
+ # def deploy_on_hubspot(param1: str, param2: str, execute_request: Callable) -> str:
45
+ # """
46
+ # Deploy the messaging workflow on the third-party service using Composio.
47
+ # List of the services: https://composio.dev/tools?category=marketing
48
+
49
+ # my custom action description which will be passed to llm
50
+
51
+ # :param param1: param1 description which will be passed to llm
52
+ # :param param2: param2 description which will be passed to llm
53
+ # :return info: return description
54
+ # """
55
+
56
+ # response = execute_request(
57
+ # "/my_action_endpoint",
58
+ # "GET",
59
+ # {} # body can be added here
60
+ # ) # execute requests by appending credentials to the request
61
+ # return str(response) # complete auth dict is available for local use if needed
62
+
63
+
64
+ # toolset = ComposioToolSet(entity_id=D)
65
+ # tools = composio_toolset.get_tools(actions=[deploy_on_hubspot])
66
+
67
+
68
+ # rag_tools = composio_toolset.get_tools(
69
+ # apps=[App.RAGTOOL],
70
+ # actions=[
71
+ # Action.FILETOOL_LIST_FILES,
72
+ # Action.FILETOOL_CHANGE_WORKING_DIRECTORY,
73
+ # Action.FILETOOL_FIND_FILE,
74
+ # ],
75
+ # )
76
+
77
+ # rag_query_tools = composio_toolset.get_tools(
78
+ # apps=[App.RAGTOOL],
79
+ # )
80
+
81
+ # # can pass multiple actions
82
+ # tools = composio_toolset.get_tools(
83
+ # actions=[Action.GITHUB_CREATE_AN_ISSUE]
84
+ # )
85
+
86
+ # rag_tools = composio_toolset.get_tools(
87
+ # apps=[App.RAGTOOL],
88
+ # actions=[
89
+ # Action.FILETOOL_LIST_FILES,
90
+ # Action.FILETOOL_CHANGE_WORKING_DIRECTORY,
91
+ # Action.FILETOOL_FIND_FILE,
92
+ # ],
93
+ # )
94
+
95
+ # rag_query_tools = composio_toolset.get_tools(
96
+ # apps=[App.RAGTOOL],
97
+ # )
98
+
99
+
100
+ if __name__ == "__main__":
101
+ connect()
102
+
@@ -0,0 +1,40 @@
1
+ from typing import Callable
2
+ from pydantic import BaseModel
3
+ from versionhq.tool.model import Tool
4
+
5
+
6
+ def tool(*args):
7
+ """
8
+ Decorator to create a tool from a function.
9
+ """
10
+
11
+ def create_tool(tool_name: str) -> Callable:
12
+
13
+ def _make_tool(f: Callable) -> Tool:
14
+ if f.__doc__ is None:
15
+ raise ValueError("Function must have a docstring")
16
+ if f.__annotations__ is None:
17
+ raise ValueError("Function must have type annotations")
18
+
19
+ class_name = "".join(tool_name.split()).title()
20
+ args_schema = type(
21
+ class_name,
22
+ (BaseModel,),
23
+ {
24
+ "__annotations__": {
25
+ k: v for k, v in f.__annotations__.items() if k != "return"
26
+ },
27
+ },
28
+ )
29
+ return Tool(name=tool_name, func=f, args_schema=args_schema)
30
+
31
+ return _make_tool
32
+
33
+ if len(args) == 1 and callable(args[0]):
34
+ return create_tool(args[0].__name__)(args[0])
35
+
36
+ elif len(args) == 1 and isinstance(args[0], str):
37
+ return create_tool(args[0])
38
+
39
+ else:
40
+ raise ValueError("Invalid arguments")
@@ -0,0 +1,220 @@
1
+ from abc import ABC
2
+ from inspect import signature
3
+ from typing import Any, Dict, Callable, Type, Optional, get_args, get_origin
4
+ from pydantic import (
5
+ InstanceOf,
6
+ BaseModel,
7
+ ConfigDict,
8
+ Field,
9
+ create_model,
10
+ field_validator,
11
+ model_validator,
12
+ )
13
+
14
+ from versionhq._utils.cache_handler import CacheHandler
15
+
16
+
17
+ class Tool(ABC, BaseModel):
18
+ """
19
+ The function that will be executed when the tool is called.
20
+ """
21
+
22
+ class ArgsSchema(BaseModel):
23
+ pass
24
+
25
+ model_config = ConfigDict()
26
+ name: str = Field(default=None)
27
+ func: Callable = Field(default=None)
28
+ cache_function: Callable = lambda _args=None, _result=None: True
29
+ args_schema: Type[BaseModel] = Field(default_factory=ArgsSchema)
30
+ tool_handler: Optional[Dict[str, Any]] = Field(
31
+ default=None,
32
+ description="store tool_handler to record the usage of this tool. to avoid circular import, set as Dict format",
33
+ )
34
+
35
+ @property
36
+ def description(self):
37
+ args_schema = {
38
+ name: {
39
+ "description": field.description,
40
+ "type": Tool._get_arg_annotations(field.annotation),
41
+ }
42
+ for name, field in self.args_schema.model_fields.items()
43
+ }
44
+ return f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nTool Description: {self.description}"
45
+
46
+ @field_validator("args_schema", mode="before")
47
+ @classmethod
48
+ def _default_args_schema(cls, v: Type[BaseModel]) -> Type[BaseModel]:
49
+ if not isinstance(v, cls.ArgsSchema):
50
+ return v
51
+
52
+ return type(
53
+ f"{cls.__name__}Schema",
54
+ (BaseModel,),
55
+ {
56
+ "__annotations__": {
57
+ k: v for k, v in cls._run.__annotations__.items() if k != "return"
58
+ },
59
+ },
60
+ )
61
+
62
+ @model_validator(mode="after")
63
+ def set_up_tool_handler_instance(self):
64
+ from versionhq.tool.tool_handler import ToolHandler
65
+
66
+ if self.tool_handler:
67
+ ToolHandler(**self.tool_handler)
68
+
69
+ return self
70
+
71
+ @staticmethod
72
+ def _get_arg_annotations(annotation: type[Any] | None) -> str:
73
+ if annotation is None:
74
+ return "None"
75
+
76
+ origin = get_origin(annotation)
77
+ args = get_args(annotation)
78
+
79
+ if origin is None:
80
+ return (
81
+ annotation.__name__
82
+ if hasattr(annotation, "__name__")
83
+ else str(annotation)
84
+ )
85
+
86
+ if args:
87
+ args_str = ", ".join(Tool._get_arg_annotations(arg) for arg in args)
88
+ return f"{origin.__name__}[{args_str}]"
89
+
90
+ return origin.__name__
91
+
92
+ def _set_args_schema(self):
93
+ if self.args_schema is None:
94
+ class_name = f"{self.__class__.__name__}Schema"
95
+
96
+ self.args_schema = type(
97
+ class_name,
98
+ (BaseModel,),
99
+ {
100
+ "__annotations__": {
101
+ k: v
102
+ for k, v in self._run.__annotations__.items()
103
+ if k != "return"
104
+ },
105
+ },
106
+ )
107
+
108
+ @classmethod
109
+ def from_composio(
110
+ cls, func: Callable = None, tool_name: str = "Composio tool"
111
+ ) -> "Tool":
112
+ """
113
+ Create a Pydantic BaseModel instance from Composio tools, ensuring the Tool instance has a func to be executed.
114
+ Refer to the `args_schema` from the func signature if any. Else, create an `args_schema`.
115
+ """
116
+
117
+ if not func:
118
+ raise ValueError("Params must have a callable 'func' attribute.")
119
+
120
+ # args_schema = getattr(tool, "args_schema", None)
121
+ args_fields = {}
122
+ func_signature = signature(func)
123
+ annotations = func_signature.parameters
124
+ for name, param in annotations.items():
125
+ if name != "self":
126
+ param_annotation = (
127
+ param.annotation if param.annotation != param.empty else Any
128
+ )
129
+ field_info = Field(default=..., description="")
130
+ args_fields[name] = (param_annotation, field_info)
131
+
132
+ args_schema = (
133
+ create_model(f"{tool_name}Input", **args_fields)
134
+ if args_fields
135
+ else create_model(f"{tool_name}Input", __base__=BaseModel)
136
+ )
137
+
138
+ return cls(name=tool_name, func=func, args_schema=args_schema)
139
+
140
+ def run(self, *args, **kwargs) -> Any:
141
+ """
142
+ Use the tool.
143
+ When the tool has a func, execute the func and return any response from the func.
144
+ Else,
145
+
146
+ The response will be cascaded to the Task and stored in the TaskOutput.
147
+ """
148
+
149
+ print(f"Using the tool: {self.name}")
150
+ result = None
151
+
152
+ if self.func is not None:
153
+ result = self.func(
154
+ *args, **kwargs
155
+ ) #! REFINEME - format - json dict, pydantic, raw
156
+
157
+ else:
158
+ acceptable_args = self.args_schema.model_json_schema()["properties"].keys()
159
+ arguments = {k: v for k, v in kwargs.items() if k in acceptable_args}
160
+ tool_called = ToolCalled(tool=self, arguments=arguments)
161
+
162
+ if self.tool_handler:
163
+ if self.tool_handler.has_called_before(tool_called):
164
+ self.tool_handler.error = "Agent execution error."
165
+
166
+ elif self.tool_handler.cache:
167
+ result = self.tools_handler.cache.read(
168
+ tool=tool_called.tool.name, input=tool_called.arguments
169
+ )
170
+ # from_cache = result is not None
171
+ if result is None:
172
+ result = self.invoke(input=arguments)
173
+
174
+ else:
175
+ from versionhq.tool.tool_handler import ToolHandler
176
+
177
+ tool_handler = ToolHandler(
178
+ last_used_tool=tool_called, cache=CacheHandler()
179
+ )
180
+ self.tool_handler = tool_handler
181
+ result = self.invoke(input=arguments)
182
+
183
+ return result
184
+
185
+
186
+ class ToolCalled(BaseModel):
187
+ """
188
+ Store the tool called and any kwargs used.
189
+ """
190
+
191
+ tool: InstanceOf[Tool] = Field(
192
+ ..., description="store the tool instance to be called."
193
+ )
194
+ arguments: Optional[Dict[str, Any]] = Field(
195
+ ..., description="kwargs passed to the tool"
196
+ )
197
+
198
+
199
+ class InstructorToolCalled(BaseModel):
200
+ tool: InstanceOf[Tool] = Field(
201
+ ..., description="store the tool instance to be called."
202
+ )
203
+ arguments: Optional[Dict[str, Any]] = Field(
204
+ ..., description="kwargs passed to the tool"
205
+ )
206
+
207
+
208
+ class CacheTool(BaseModel):
209
+ """
210
+ Default tools to hit the cache.
211
+ """
212
+
213
+ name: str = "Hit Cache"
214
+ cache_handler: CacheHandler = Field(default_factory=CacheHandler)
215
+
216
+ def hit_cache(self, key):
217
+ split = key.split("tool:")
218
+ tool = split[1].split("|input:")[0].strip()
219
+ tool_input = split[1].split("|input:")[1].strip()
220
+ return self.cache_handler.read(tool, tool_input)
@@ -0,0 +1,47 @@
1
+ from typing import Any, Optional, Union
2
+ from versionhq.tool.model import ToolCalled, InstructorToolCalled, CacheTool
3
+ from versionhq._utils.cache_handler import CacheHandler
4
+
5
+
6
+ class ToolHandler:
7
+ """
8
+ Record the tool usage by ToolCalled instance with cache and error recording.
9
+ Use as a callback function.
10
+ """
11
+
12
+ last_used_tool: ToolCalled = {}
13
+ cache: Optional[CacheHandler]
14
+ error: Optional[str]
15
+
16
+ def __init__(self, cache_handler: Optional[CacheHandler] = None):
17
+ """
18
+ Initialize the callback handler.
19
+ """
20
+
21
+ self.cache = cache_handler
22
+ self.last_used_tool = {}
23
+
24
+ def record_last_tool_used(
25
+ self,
26
+ last_used_tool: Union[ToolCalled, InstructorToolCalled],
27
+ output: str,
28
+ should_cache: bool = True,
29
+ ) -> Any:
30
+ self.last_used_tool = last_used_tool
31
+
32
+ if self.cache and should_cache and last_used_tool.tool_name != CacheTool().name:
33
+ self.cache.add(
34
+ last_used_tool.tool_name,
35
+ input.last_used_tool.arguments,
36
+ output=output,
37
+ )
38
+
39
+ def has_called_before(self, tool_called: ToolCalled = None) -> bool:
40
+ if tool_called is None or not self.last_used_tool:
41
+ return False
42
+
43
+ if tool_called := self.last_used_tool:
44
+ return bool(
45
+ (tool_called.tool.name == self.last_used_tool.tool.name)
46
+ and (tool_called.arguments == self.last_used_tool.arguments)
47
+ )
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Version IO Sdn. Bhd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.