versionhq 1.1.9.1__py3-none-any.whl → 1.1.9.3__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 CHANGED
@@ -15,10 +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
+ from versionhq.tool.composio_tool import ComposioHandler
19
19
 
20
20
 
21
- __version__ = "1.1.9.1"
21
+ __version__ = "1.1.9.3"
22
22
  __all__ = [
23
23
  "Agent",
24
24
  "Customer",
@@ -34,5 +34,5 @@ __all__ = [
34
34
  "Team",
35
35
  "TeamOutput",
36
36
  "Tool",
37
- "Composio"
37
+ "ComposioHandler"
38
38
  ]
@@ -10,23 +10,21 @@ class Printer:
10
10
  self._print_purple(content)
11
11
  elif color == "red":
12
12
  self._print_red(content)
13
- elif color == "bold_green":
14
- self._print_bold_green(content)
15
- elif color == "bold_purple":
16
- self._print_bold_purple(content)
17
- elif color == "bold_blue":
18
- self._print_bold_blue(content)
13
+ elif color == "green":
14
+ self._print_green(content)
15
+ elif color == "purple":
16
+ self._print_purple(content)
17
+ elif color == "blue":
18
+ self._print_blue(content)
19
19
  elif color == "yellow":
20
20
  self._print_yellow(content)
21
- elif color == "bold_yellow":
22
- self._print_bold_yellow(content)
23
21
  else:
24
22
  print(content)
25
23
 
26
- def _print_bold_purple(self, content):
24
+ def _print_purple(self, content):
27
25
  print("\033[1m\033[95m {}\033[00m".format(content))
28
26
 
29
- def _print_bold_green(self, content):
27
+ def _print_green(self, content):
30
28
  print("\033[1m\033[92m {}\033[00m".format(content))
31
29
 
32
30
  def _print_purple(self, content):
@@ -35,23 +33,18 @@ class Printer:
35
33
  def _print_red(self, content):
36
34
  print("\033[91m {}\033[00m".format(content))
37
35
 
38
- def _print_bold_blue(self, content):
36
+ def _print_blue(self, content):
39
37
  print("\033[1m\033[94m {}\033[00m".format(content))
40
38
 
41
39
  def _print_yellow(self, content):
42
- print("\033[93m {}\033[00m".format(content))
43
-
44
- def _print_bold_yellow(self, content):
45
40
  print("\033[1m\033[93m {}\033[00m".format(content))
46
41
 
47
42
 
48
43
  class Logger(BaseModel):
49
- verbose: bool = Field(default=False)
44
+ verbose: bool = Field(default=True)
50
45
  _printer: Printer = PrivateAttr(default_factory=Printer)
51
46
 
52
- def log(self, level, message, color="bold_yellow"):
47
+ def log(self, level, message, color="yellow"):
53
48
  if self.verbose:
54
49
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
55
- self._printer.print(
56
- f"\n[{timestamp}][{level.upper()}]: {message}", color=color
57
- )
50
+ self._printer.print(f"\n[{timestamp}][{level.upper()}]: {message}", color=color)
versionhq/agent/model.py CHANGED
@@ -6,11 +6,9 @@ from dotenv import load_dotenv
6
6
  from pydantic import UUID4, BaseModel, Field, InstanceOf, PrivateAttr, model_validator, field_validator
7
7
  from pydantic_core import PydanticCustomError
8
8
 
9
- from versionhq._utils.cache_handler import CacheHandler
10
9
  from versionhq._utils.logger import Logger
11
10
  from versionhq._utils.rpm_controller import RPMController
12
11
  from versionhq._utils.usage_metrics import UsageMetrics
13
- from versionhq.agent.parser import AgentAction
14
12
  from versionhq.llm.llm_vars import LLM_VARS
15
13
  from versionhq.llm.model import LLM, DEFAULT_CONTEXT_WINDOW
16
14
  from versionhq.task import TaskOutputFormat
@@ -122,10 +120,7 @@ class Agent(BaseModel):
122
120
 
123
121
  # config, cache, error handling
124
122
  config: Optional[Dict[str, Any]] = Field(default=None, exclude=True, description="Configuration for the agent")
125
- cache: bool = Field(default=True, description="Whether the agent should use a cache for tool usage.")
126
- cache_handler: InstanceOf[CacheHandler] = Field(default=None, description="An instance of the CacheHandler class.")
127
123
  formatting_errors: int = Field(default=0, description="Number of formatting errors.")
128
- verbose: bool = Field(default=True, description="Verbose mode for the Agent Execution")
129
124
  agent_ops_agent_name: str = None
130
125
  agent_ops_agent_id: str = None
131
126
 
@@ -0,0 +1,5 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Status(str, Enum):
5
+ ON_WORKFLOW = "on_workflow"
@@ -1,41 +1,53 @@
1
1
  import uuid
2
- from abc import ABC
2
+ from abc import ABC, abstractmethod
3
3
  from typing import Any, Dict, List, Callable, Type, Optional, get_args, get_origin
4
4
  from pydantic import UUID4, InstanceOf, BaseModel, ConfigDict, Field, create_model, field_validator, model_validator
5
5
  from pydantic_core import PydanticCustomError
6
6
 
7
7
  from versionhq.clients.product.model import Product, ProductProvider
8
+ from versionhq.clients.customer import Status
8
9
 
9
10
 
10
- class Customer(ABC, BaseModel):
11
+ class BaseCustomer(ABC, BaseModel):
11
12
  """
12
- Store the minimal information on the customer.
13
+ Abstract base class for the base customer
13
14
  """
14
15
 
15
16
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
16
17
  name: Optional[str] = Field(default=None, description="customer's name if any")
17
- product_list: Optional[List[Product]] = Field(default=list, description="store products that the customer is associated with")
18
+ products: Optional[List[Product]] = Field(default=list, description="store products that the customer is associated with")
18
19
  analysis: str = Field(default=None, description="store the latest analysis results on the customer")
19
- on_workflow: bool = Field(default=False, description="`True` if they are on some messaging workflows")
20
- on: Optional[str] = Field(default=None, description="destination service for this customer if any")
20
+ status: str = Field(default=Status.ON_WORKFLOW)
21
21
 
22
22
 
23
23
  @field_validator("id", mode="before")
24
24
  @classmethod
25
25
  def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
26
26
  if v:
27
- raise PydanticCustomError(
28
- "may_not_set_field", "This field is not to be set by the user.", {}
29
- )
27
+ raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
28
+
30
29
 
31
30
  def customer_to(self) -> List[ProductProvider]:
32
31
  """
33
32
  Return list of ProductProvider if the customer has `product_list`
34
33
  """
35
34
 
36
- res = list
37
- if self.product_list:
38
- for item in self.product_list:
35
+ res = []
36
+ if self.products:
37
+ for item in self.products:
39
38
  if item.provider not in res:
40
39
  res.appned(item.provider)
41
40
  return res
41
+
42
+
43
+ @abstractmethod
44
+ def _deploy(self, *args, **kwargs) -> Any:
45
+ """Any method to deploy targeting the customer"""
46
+
47
+
48
+ class Customer(BaseCustomer):
49
+ id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
50
+ name: Optional[str] = Field(default=None, description="customer's name if any")
51
+ products: Optional[List[Product]] = Field(default=list, description="store products that the customer is associated with")
52
+ analysis: str = Field(default=None, description="store the latest analysis results on the customer")
53
+ status: str = Field(default=Status.ON_WORKFLOW)
@@ -1,22 +1,23 @@
1
1
  import uuid
2
+ from abc import ABC, abstractmethod
2
3
  from typing import Any, Dict, List, Callable, Type, Optional, get_args, get_origin
4
+
3
5
  from pydantic import UUID4, InstanceOf, BaseModel, ConfigDict, Field, create_model, field_validator, model_validator
4
6
  from pydantic_core import PydanticCustomError
5
7
 
8
+ from versionhq.tool import ComposioAppName
9
+
6
10
 
7
- class ProductProvider(BaseModel):
11
+ class ProductProvider(ABC, BaseModel):
8
12
  """
9
- Store the minimal client information.
10
- `data_pipeline` and `destinations` are for composio plug-in.
11
- (!REFINEME) Create an Enum list for the options.
12
- (!REFINEME) Create an Enum list for regions.
13
+ Abstract class for the product provider entity.
13
14
  """
14
15
 
15
16
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
16
- name: Optional[str] = Field(default=None, description="client name")
17
+ name: Optional[str] = Field(default=None)
17
18
  region: Optional[str] = Field(default=None, description="region of client's main business operation")
18
- data_pipeline: Optional[List[str]] = Field(default=None, description="store the data pipelines that the client is using")
19
- destinations: Optional[List[str]] = Field(default=None,description="store the destination services that the client is using")
19
+ data_pipelines: Optional[List[ComposioAppName | str]] = Field(default_factory=list)
20
+ destination_services: Optional[List[ComposioAppName | str]] = Field(default=None)
20
21
 
21
22
  @field_validator("id", mode="before")
22
23
  @classmethod
@@ -10,6 +10,7 @@ from versionhq.clients.product.model import Product
10
10
  from versionhq.clients.customer.model import Customer
11
11
  from versionhq.agent.model import Agent
12
12
  from versionhq.team.model import Team
13
+ from versionhq.tool import ComposioAppName
13
14
 
14
15
 
15
16
  class ScoreFormat:
@@ -57,14 +58,12 @@ class Score:
57
58
  class MessagingComponent(ABC, BaseModel):
58
59
  layer_id: int = Field(default=0, description="add id of the layer: 0, 1, 2")
59
60
  message: str = Field(default=None, max_length=1024, description="text message content to be sent")
60
- interval: Optional[str] = Field(
61
- default=None, description="interval to move on to the next layer. if this is the last layer, set as `None`"
62
- )
63
- score: float | InstanceOf[Score] = Field(default=None)
64
- condition: str = Field(default=None, max_length=128, description="condition to execute the next messaging component")
61
+ score: InstanceOf[Score] = Field(default=None)
62
+ condition: str = Field(default=None, max_length=128, description="condition to execute the next component")
63
+ interval: Optional[str] = Field(default=None, description="ideal interval to set to assess the condition")
65
64
 
66
65
 
67
- def store_scoring_result(self, scoring_subject: str, score_raw: int | Score | ScoreFormat = None) -> Self:
66
+ def store_scoring_result(self, subject: str, score_raw: int | Score | ScoreFormat = None) -> Self:
68
67
  """
69
68
  Set up the `score` field
70
69
  """
@@ -74,12 +73,12 @@ class MessagingComponent(ABC, BaseModel):
74
73
 
75
74
  elif isinstance(score_raw, ScoreFormat):
76
75
  score_instance = Score()
77
- setattr(score_instance, scoring_subject, score_raw)
76
+ setattr(score_instance, subject, score_raw)
78
77
  setattr(self, "score", score_instance)
79
78
 
80
79
  elif isinstance(score_raw, int) or isinstance(score_raw, float):
81
80
  score_instance, score_format_instance = Score(), ScoreFormat(rate=score_raw, weight=1)
82
- setattr(score_instance, "kwargs", { scoring_subject: score_format_instance })
81
+ setattr(score_instance, "kwargs", { subject: score_format_instance })
83
82
  setattr(self, "score", score_instance)
84
83
 
85
84
  else:
@@ -100,23 +99,17 @@ class MessagingWorkflow(ABC, BaseModel):
100
99
  model_config = ConfigDict()
101
100
 
102
101
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
103
- components: List[MessagingComponent] = Field(default_factory=list, description="store messaging components in the workflow")
102
+ messaging_components: List[MessagingComponent] = Field(default_factory=list, description="store messaging components in the workflow")
104
103
 
105
104
  # responsible tean or agents
106
- team: Optional[Team] = Field(default=None, description="store `Team` instance responsibile for autopiloting this workflow")
107
- agents: Optional[List[Agent]] = Field(
108
- default=None, description="store `Agent` instances responsible for autopiloting this workflow. if the team exsits, this field remains as `None`")
105
+ team: Optional[Team] = Field(default=None, description="store a responsibile team to autopilot the workflow")
106
+ agents: Optional[List[Agent]] = Field(default=None, description="store responsible agents. None when the team exists")
109
107
 
110
108
  # metrics
111
- destination: Optional[str | None] = Field(default=None, description="destination service to launch this workflow")
109
+ destination: Optional[ComposioAppName | str] = Field(default=None, description="destination service to launch the workflow")
112
110
  product: InstanceOf[Product] = Field(default=None)
113
111
  customer: InstanceOf[Customer] = Field(default=None)
114
-
115
- metrics: List[Dict[str, Any]] | List[str] = Field(
116
- default=None,
117
- max_length=256,
118
- description="store metrics that used to predict and track the performance of this workflow."
119
- )
112
+ performance_metrics: List[Dict[str, Any]] | List[str] = Field(default=None, max_length=256, description="performance metrics to track")
120
113
 
121
114
  @field_validator("id", mode="before")
122
115
  @classmethod
@@ -132,11 +125,11 @@ class MessagingWorkflow(ABC, BaseModel):
132
125
  Prioritize customer's destination to the product provider's destination list.
133
126
  """
134
127
  if self.destination is None:
135
- if self.customer is not None:
136
- self.destination = self.customer.on
128
+ # if self.customer is not None:
129
+ # self.destination = self.customer.on
137
130
 
138
- elif self.product.provider is not None and self.product.provider.destinations:
139
- self.destination = self.product.provider.destinations[0]
131
+ if self.product.provider is not None and self.product.provider.destination_services:
132
+ self.destination = self.product.provider.destination_services[0]
140
133
 
141
134
  return self
142
135
 
@@ -40,10 +40,11 @@ composio_app_set = [
40
40
  (ComposioAppName.LINKEDIN, ComposioAuthScheme.OAUTH2),
41
41
  ]
42
42
 
43
- class COMPOSIO_STATUS(str, Enum):
44
- INITIATED = "initiated"
45
- ACTIVE = "active"
46
- FAILED = "failed"
43
+ class ComposioStatus(str, Enum):
44
+ INITIATED = "INITIATED"
45
+ ACTIVE = "ACTIVE"
46
+ FAILED = "FAILED"
47
+
47
48
 
48
49
 
49
50
 
@@ -51,3 +52,5 @@ class ComposioAction(str, Enum):
51
52
  """
52
53
  Enum to store composio's action that can be called via `Actions.xxx`
53
54
  """
55
+ # HUBSPOT_INITIATE_DATA_IMPORT_PROCESS = "hubspot_initate_date_import_process"
56
+ HUBSPOT_CREATE_PIPELINE_STAGE = "hubspot_create_pipeline_stage"
@@ -0,0 +1,191 @@
1
+ import os
2
+ import uuid
3
+ from abc import ABC
4
+ from dotenv import load_dotenv
5
+ from typing import Any, Callable, Type, get_args, get_origin, Optional, Tuple, Dict
6
+ from typing_extensions import Self
7
+
8
+ from pydantic import BaseModel, Field, model_validator, field_validator, UUID4, PrivateAttr
9
+ from pydantic_core import PydanticCustomError
10
+
11
+ from composio import ComposioToolSet
12
+ from composio_langchain import action
13
+
14
+ from versionhq.tool import ComposioAppName, ComposioAuthScheme, composio_app_set, ComposioStatus, ComposioAction
15
+ from versionhq._utils.logger import Logger
16
+ from versionhq._utils.cache_handler import CacheHandler
17
+
18
+ load_dotenv(override=True)
19
+
20
+ DEFAULT_REDIRECT_URL = os.environ.get("DEFAULT_REDIRECT_URL", None)
21
+ DEFAULT_USER_ID = os.environ.get("DEFAULT_USER_ID", None)
22
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", None)
23
+
24
+
25
+ class ComposioHandler(ABC, BaseModel):
26
+ """
27
+ A class to handle connecting account with Composio and executing actions using Composio ecosystem.
28
+ `connected_account_id` is set up per `app_name` to call the actions on the given app. i.e., salesforce
29
+ """
30
+
31
+ _logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
32
+ _cache: CacheHandler = PrivateAttr(default_factory=lambda: CacheHandler())
33
+
34
+ id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
35
+ app_name: str = Field(default=ComposioAppName.HUBSPOT, max_length=128, description="app name defined by composio")
36
+ user_id: str = Field(default=DEFAULT_USER_ID, description="composio entity id")
37
+ auth_scheme: str = Field(default=ComposioAuthScheme.OAUTH2)
38
+ redirect_url: str = Field(default=DEFAULT_REDIRECT_URL, description="redirect url after successful oauth2 connection")
39
+ connected_account_id: str = Field(
40
+ default=None,
41
+ description="store the client id generated by composio after auth validation. use the id to connect with a given app and execute composio actions"
42
+ )
43
+ tools: Any = Field(default=None, descritpion="retrieved composio tools")
44
+
45
+ @field_validator("id", mode="before")
46
+ @classmethod
47
+ def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
48
+ if v:
49
+ raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
50
+
51
+
52
+ @model_validator(mode="after")
53
+ def validate_app_name(self):
54
+ if self.app_name not in ComposioAppName:
55
+ raise PydanticCustomError("no_app_name", f"The given app name {self.app_name} is not valid.", {})
56
+
57
+ return self
58
+
59
+
60
+ @model_validator(mode="after")
61
+ def validate_auth_scheme(self):
62
+ """
63
+ Raise error when the client uses auth scheme unavailable for the app.
64
+ """
65
+ app_set = next(filter(lambda tup: self.app_name in tup, composio_app_set), None)
66
+ if not app_set:
67
+ raise PydanticCustomError("no_app_set", f"The app set of {self.app_name} is missing.", {})
68
+
69
+ else:
70
+ acceptable_auth_scheme = next(filter(lambda item: self.auth_scheme in item, app_set), None)
71
+ if acceptable_auth_scheme is None:
72
+ raise PydanticCustomError("invalid_auth_scheme", f"The app {self.app_name} must have different auth_scheme.", {})
73
+
74
+ return self
75
+
76
+
77
+ def _setup_langchain_toolset(self, metadata: Dict[str, Any] = dict()):
78
+ """
79
+ Composio toolset on LangChain for action execution using LLM.
80
+ """
81
+ from composio_langchain import ComposioToolSet
82
+ return ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"), metadata={**metadata})
83
+
84
+
85
+ def _connect(
86
+ self, token: Optional[str] = None, api_key: Optional[str] = None, connected_account_id: str = None
87
+ ) -> Tuple[Self | str | Any]:
88
+ """
89
+ Send connection request to Composio, retrieve `connected_account_id`, and proceed with OAuth process of the given app to activate the connection.
90
+ """
91
+
92
+ connection_request, connected_account = None, None
93
+ connected_account_id = connected_account_id or self.connected_account_id
94
+ if connected_account_id:
95
+ connected_account = self.toolset.get_connected_account(id=connected_account_id)
96
+
97
+ if connected_account and connected_account.status == ComposioStatus.ACTIVE.value:
98
+ return self, ComposioStatus.ACTIVE.value
99
+
100
+ if not self.user_id:
101
+ raise PydanticCustomError("entity_id_missing", "Need entity_id to connect with the tool", {})
102
+
103
+ if self.auth_scheme == ComposioAuthScheme.API_KEY:
104
+ collected_from_user = {}
105
+ collected_from_user["api_key"] = api_key
106
+ connection_request = self.toolset.initiate_connection(
107
+ connected_account_params = collected_from_user,
108
+ app=self.app_name,
109
+ entity_id=self.user_id,
110
+ auth_scheme=self.auth_scheme,
111
+ )
112
+
113
+ if self.auth_scheme == ComposioAuthScheme.BEARER_TOKEN:
114
+ collected_from_user = {}
115
+ collected_from_user["token"] = token
116
+ connection_request = self.toolset.initiate_connection(
117
+ connected_account_params = collected_from_user,
118
+ app=self.app_name,
119
+ entity_id=self.user_id,
120
+ auth_scheme=self.auth_scheme,
121
+ )
122
+
123
+ if self.auth_scheme == ComposioAuthScheme.OAUTH2:
124
+ connection_request = self.toolset.initiate_connection(
125
+ app=self.app_name,
126
+ redirect_url = self.redirect_url, # clients will be redirected to this url after successful auth.
127
+ entity_id=self.user_id,
128
+ auth_scheme=self.auth_scheme,
129
+ )
130
+
131
+ if connection_request.connectionStatus == ComposioStatus.FAILED.value:
132
+ self._logger.log(level="error", message="Connection to composio failed.", color="red")
133
+ raise PydanticCustomError("connection_failed", "Connection to composio has failed", {})
134
+
135
+
136
+ connected_account = self.toolset.get_connected_account(id=connection_request.connectedAccountId)
137
+ # Note: connected_account.id === connection_request.connectedAccountId === self.connected_account_id
138
+
139
+ if connected_account.status == ComposioStatus.ACTIVE.value:
140
+ setattr(self.toolset, "entity_id", self.user_id)
141
+ self.connected_account_id = connection_request.connectedAccountId
142
+
143
+ elif connected_account.status == ComposioStatus.INITIATED.value:
144
+ setattr(self.toolset, "entity_id", self.user_id)
145
+ self.connected_account_id = connection_request.connectedAccountId
146
+
147
+ if connection_request.redirectUrl:
148
+ import webbrowser
149
+ webbrowser.open(connection_request.redirectUrl)
150
+
151
+ else:
152
+ self._logger.log(level="error", message="The account is invalid.", color="red")
153
+ raise PydanticCustomError("connection_failed", "Connection to composio has failed", {})
154
+
155
+ return self, connected_account.status if connected_account else connection_request.connectionStatus
156
+
157
+
158
+ def execute_composio_action_with_langchain(self, action_name: str | ComposioAction, task_in_natural_language: str) -> Tuple[Self, str]:
159
+ """
160
+ Execute Composio's Action using Langchain's agent ecosystem.
161
+ """
162
+ from langchain import hub
163
+ from langchain_openai import ChatOpenAI
164
+ from langchain.agents import create_openai_functions_agent, AgentExecutor
165
+ from composio_langchain import Action
166
+
167
+ action_name = action_name.value if isinstance(action_name, ComposioAction) else action_name
168
+ action = Action(action_name)
169
+ metadata = { action: { "OPENAI_API_KEY": OPENAI_API_KEY } }
170
+ toolset = self._setup_langchain_toolset(metadata=metadata)
171
+ tools = toolset.get_tools(actions=[action_name,], entity_id=self.user_id)
172
+ if not tools:
173
+ self._logger.log(level="error", message=f"Tools related to {action_name} are not found on Langchain", color="red")
174
+ raise PydanticCustomError("tool_not_found", "Tools not found on Langchain", {})
175
+
176
+ self.tools = tools
177
+ llm = ChatOpenAI()
178
+ prompt = hub.pull("hwchase17/openai-functions-agent")
179
+ agent = create_openai_functions_agent(llm, tools, prompt)
180
+ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
181
+ result = agent_executor.invoke(dict(input=task_in_natural_language))
182
+ return self, result["output"]
183
+
184
+
185
+ @property
186
+ def toolset(self) -> ComposioToolSet:
187
+ return ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"))
188
+
189
+
190
+ def __name__(self):
191
+ return self.app_name
versionhq/tool/model.py CHANGED
@@ -41,18 +41,22 @@ class Tool(BaseTool):
41
41
  name: str = Field(default=None)
42
42
  goal: str = Field(default=None)
43
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")
44
46
  cache_function: Callable = lambda _args=None, _result=None: True
45
- tool_handler: Optional[Dict[str, Any]] = Field(
46
- default=None,
47
- description="store tool_handler to record the usage of this tool. to avoid circular import, set as Dict format",
48
- )
47
+ cache_handler: Optional[InstanceOf[CacheHandler]] = Field(default=None)
48
+
49
49
 
50
50
  @model_validator(mode="after")
51
51
  def set_up_tool_handler(self) -> Self:
52
52
  from versionhq.tool.tool_handler import ToolHandler
53
53
 
54
- if self.tool_handler:
54
+ if self.tool_handler and not isinstance(self.tool_handler, ToolHandler):
55
55
  ToolHandler(**self.tool_handler)
56
+
57
+ else:
58
+ self.tool_handler = ToolHandler(cache_handler=self.cache_handler, should_cache=self.should_cache)
59
+
56
60
  return self
57
61
 
58
62
 
@@ -119,33 +123,40 @@ class Tool(BaseTool):
119
123
 
120
124
  def run(self, *args, **kwargs) -> Any:
121
125
  """
122
- Use tool
126
+ Use tool and record its usage if should_cache is True.
123
127
  """
124
128
  from versionhq.tool.tool_handler import ToolHandler
125
129
 
130
+ result = None
131
+ tool_set = ToolSet(tool=self, kwargs={})
132
+
126
133
  if self.function:
127
- return self.function(*args, **kwargs)
134
+ result = self.function(*args, **kwargs)
128
135
 
129
- result = None
130
- acceptable_args = self.args_schema.model_json_schema()["properties"].keys()
131
- acceptable_kwargs = { k: v for k, v in kwargs.items() if k in acceptable_args }
132
- tool_called = ToolSet(tool=self, kwargs=acceptable_kwargs)
136
+ else:
137
+ acceptable_args = self.args_schema.model_json_schema()["properties"].keys()
138
+ acceptable_kwargs = { k: v for k, v in kwargs.items() if k in acceptable_args }
139
+ tool_set = ToolSet(tool=self, kwargs=acceptable_kwargs)
133
140
 
134
- if self.tool_handler:
135
- if self.tool_handler.has_called_before(tool_called):
136
- self.tool_handler.error = "Agent execution error"
141
+ if self.tool_handler:
142
+ if self.tool_handler.has_called_before(tool_set):
143
+ self.tool_handler.error = "Agent execution error"
137
144
 
138
- elif self.tool_handler.cache:
139
- result = self.tools_handler.cache.read(tool=tool_called.tool.name, input=tool_called.kwargs)
140
- if result is None:
141
- parsed_kwargs = self._parse_args(raw_args=acceptable_kwargs)
142
- result = self.function(**parsed_kwargs) if self.function else None
145
+ elif self.tool_handler.cache:
146
+ result = self.tools_handler.cache.read(tool=tool_set.tool.name, input=tool_set.kwargs)
147
+ if result is None:
148
+ parsed_kwargs = self._parse_args(raw_args=acceptable_kwargs)
149
+ result = self.function(**parsed_kwargs) if self.function else None
143
150
 
144
- else:
145
- tool_handler = ToolHandler(last_used_tool=tool_called, cache_handler=CacheHandler())
146
- self.tool_handler = tool_handler
147
- parsed_kwargs = self._parse_args(raw_args=acceptable_kwargs)
148
- result = self.function(**parsed_kwargs) if self.function else None
151
+ else:
152
+ tool_handler = ToolHandler(last_used_tool=tool_set, cache_handler=self.cache_handler, should_cache=self.should_cache)
153
+ self.tool_handler = tool_handler
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)
149
160
 
150
161
  return result
151
162
 
@@ -13,33 +13,33 @@ class ToolHandler:
13
13
  last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet]
14
14
  cache: Optional[CacheHandler]
15
15
  error: Optional[str]
16
+ should_cache: bool
16
17
 
17
18
  def __init__(
18
19
  self,
19
20
  last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet] = None,
20
- cache_handler: Optional[CacheHandler] = None
21
+ cache_handler: Optional[CacheHandler] = None,
22
+ should_cache: bool = True
21
23
  ):
22
24
  self.cache = cache_handler
23
25
  self.last_used_tool = last_used_tool
26
+ self.should_cache = should_cache
24
27
 
25
28
 
26
29
  def record_last_tool_used(
27
- self,
28
- last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet],
29
- output: str,
30
- should_cache: bool = True,
30
+ self, last_used_tool: InstanceOf[ToolSet] | InstanceOf[InstructorToolSet], output: str, should_cache: bool = True
31
31
  ) -> Any:
32
+ from versionhq.tool.model import CacheTool
32
33
 
33
34
  self.last_used_tool = last_used_tool
34
35
 
35
- from versionhq.tool.model import CacheTool
36
36
  if self.cache and should_cache and last_used_tool.tool.name != CacheTool().name:
37
37
  self.cache.add(tool=last_used_tool.tool.name, input=last_used_tool.kwargs, output=output)
38
38
 
39
39
 
40
- def has_called_before(self, tool_called: ToolSet = None) -> bool:
41
- if tool_called is None or not self.last_used_tool:
40
+ def has_called_before(self, tool_set: ToolSet = None) -> bool:
41
+ if tool_set is None or not self.last_used_tool:
42
42
  return False
43
43
 
44
- if tool_called := self.last_used_tool:
45
- return bool((tool_called.tool.name == self.last_used_tool.tool.name) and (tool_called.kwargs == self.last_used_tool.kwargs))
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: versionhq
3
- Version: 1.1.9.1
3
+ Version: 1.1.9.3
4
4
  Summary: LLM orchestration frameworks for model-agnostic AI agents that handle complex outbound workflows
5
5
  Author-email: Kuriko Iwai <kuriko@versi0n.io>
6
6
  License: MIT License
@@ -52,12 +52,15 @@ Requires-Dist: setuptools>=75.6.0
52
52
  Requires-Dist: wheel>=0.45.1
53
53
  Requires-Dist: python-dotenv>=1.0.0
54
54
  Requires-Dist: appdirs>=1.4.4
55
+ Requires-Dist: langchain>=0.3.14
56
+ Requires-Dist: langchain-openai>=0.2.14
57
+ Requires-Dist: composio-langchain>=0.6.12
55
58
 
56
59
  # Overview
57
60
 
58
61
  ![MIT license](https://img.shields.io/badge/License-MIT-green)
59
62
  [![Publisher](https://github.com/versionHQ/multi-agent-system/actions/workflows/publish.yml/badge.svg)](https://github.com/versionHQ/multi-agent-system/actions/workflows/publish.yml)
60
- ![PyPI](https://img.shields.io/badge/PyPI-v1.1.7.9-blue)
63
+ ![PyPI](https://img.shields.io/badge/PyPI-v1.1.9.3-blue)
61
64
  ![python ver](https://img.shields.io/badge/Python-3.12/3.13-purple)
62
65
  ![pyenv ver](https://img.shields.io/badge/pyenv-2.5.0-orange)
63
66
 
@@ -1,24 +1,24 @@
1
- versionhq/__init__.py,sha256=HIOFr73z0KeJs9qFVvW1DjRFj7JyiJcRGGxylbQT_bQ,931
1
+ versionhq/__init__.py,sha256=-0hi_ADxpAQM8is-cYDuL5N9F3c4CsqGFYC5txzYx0U,950
2
2
  versionhq/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  versionhq/_utils/cache_handler.py,sha256=3-lw_5ZMWC8hnPAkSQULJ2V1FvZZ-wg9mQaUJGSOjI8,403
4
4
  versionhq/_utils/i18n.py,sha256=TwA_PnYfDLA6VqlUDPuybdV9lgi3Frh_ASsb_X8jJo8,1483
5
- versionhq/_utils/logger.py,sha256=lqRYH45KHMQ4mwE1woa5xNmngYu4O749AYECsnWWpmA,1851
5
+ versionhq/_utils/logger.py,sha256=cPxDz1YrMH4ZsLzLc11XrKBfQbLWNNXRUEVUgjAeeOE,1591
6
6
  versionhq/_utils/process_config.py,sha256=UqoWD5IR4VLxEDGxIyVUylw_ppXwk8Wx1ynVuD-pUSg,822
7
7
  versionhq/_utils/rpm_controller.py,sha256=dUgFd6JtdjiLLTRmrjsBHdTaLn73XFuKpLbJh7thf2A,2289
8
8
  versionhq/_utils/usage_metrics.py,sha256=hhq1OCW8Z4V93vwW2O2j528EyjOlF8wlTsX5IL-7asA,1106
9
9
  versionhq/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- versionhq/agent/model.py,sha256=8QtZfbeys9cCujc4whKfXdoP0aQKMuvL2qN8WQmugew,19152
10
+ versionhq/agent/model.py,sha256=c5beL2iGx7Zpxla59JJPQPUjpBa3hJdeVqspYGsHTm8,18731
11
11
  versionhq/agent/parser.py,sha256=Z_swUPO3piJQuYU8oVYwXWeR2zjmNb4PxbXZeR-GlIg,4694
12
12
  versionhq/agent/TEMPLATES/Backstory.py,sha256=cdngBx1GEv7nroR46FEhnysnBJ9mEVL763_9np6Skkc,395
13
13
  versionhq/agent/TEMPLATES/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  versionhq/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  versionhq/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- versionhq/clients/customer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- versionhq/clients/customer/model.py,sha256=ruxqSvjBHrSJnNq9Jj5Ko1CW6l8RLiPzhbC_F7tbOnM,1670
16
+ versionhq/clients/customer/__init__.py,sha256=rVcesoCFFl46job8Ppf8tRpEWy8A4ArRElfVjWykcRo,81
17
+ versionhq/clients/customer/model.py,sha256=Dl2dzo2FUdzqPEgoymeImM18InOmIhytmxAkjTiK_M8,2119
18
18
  versionhq/clients/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- versionhq/clients/product/model.py,sha256=HxiSv8zq5L0H210jXWfjX_Yg1oyWhi2YASR68JEtmDY,2408
19
+ versionhq/clients/product/model.py,sha256=N8_Oe7W20yYQuJ66owbLD_zNyiSSrYA7i7WI50UuKyQ,2228
20
20
  versionhq/clients/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- versionhq/clients/workflow/model.py,sha256=LPet39sK7vUpBQ1mymdNX1xUPseGWv_5W7je_7sif_M,5883
21
+ versionhq/clients/workflow/model.py,sha256=ss-kXclc6ZRBjY4tGqAStIhEKezkBANR17n0yJNn8Ow,5717
22
22
  versionhq/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  versionhq/llm/llm_vars.py,sha256=YZoXqFBW7XpclUZ14_AAz7WOjoyCXnGcI959GSpX2q0,5343
24
24
  versionhq/llm/model.py,sha256=mXzSuf1s6MebGT7_yqgNppde0NIlAF8bjIXAp2MZ9Uw,8247
@@ -31,13 +31,13 @@ versionhq/task/model.py,sha256=EbgYHLNq8l1zfRDnF-yEcuSZ0aNvzbRmHYgfVyJq84c,19910
31
31
  versionhq/team/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  versionhq/team/model.py,sha256=E52OUVzUtvR--51SFRJos3JdYKri1t2jbvvzoOvShQc,20181
33
33
  versionhq/team/team_planner.py,sha256=uzX2yed7A7gNSs6qH5jIq2zXMVF5BwQQ4HPATsB9DSQ,3675
34
- versionhq/tool/__init__.py,sha256=oU2Y84b7vywWq1xmFaBdXdH8Y9lGv7dmk2LEcj4dL-s,1692
35
- versionhq/tool/composio.py,sha256=e-Vfr-eFm0ipiOerB_zAC1Sl90A39OD_k4QqgszWXWQ,5779
34
+ versionhq/tool/__init__.py,sha256=FvBuEXsOQUYnN7RTFxT20kAkiEYkxWKkiVtgpqOzKZQ,1843
35
+ versionhq/tool/composio_tool.py,sha256=BJqaA1NhV0BT9AdY7OLCGpsAI3VEuCKnOS6D9vuU4zQ,8630
36
36
  versionhq/tool/decorator.py,sha256=W_WjzZy8y43AoiFjHLPUQfNipmpOPe-wQknCWloPwmY,1195
37
- versionhq/tool/model.py,sha256=8A1x8gEdTuP5teUf6o3VqJhrPI5m-XBoBXxgHiWCKcI,7499
38
- versionhq/tool/tool_handler.py,sha256=rmm8snegwug4jl0Sbi_CteFajkbPAZ5koTQWDMwcIrQ,1510
39
- versionhq-1.1.9.1.dist-info/LICENSE,sha256=7CCXuMrAjPVsUvZrsBq9DsxI2rLDUSYXR_qj4yO_ZII,1077
40
- versionhq-1.1.9.1.dist-info/METADATA,sha256=-mFMCnD5q2XGVgaNyE8ddBpX8tgAR4sqlJ-Cmm0hBQA,15955
41
- versionhq-1.1.9.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
42
- versionhq-1.1.9.1.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
43
- versionhq-1.1.9.1.dist-info/RECORD,,
37
+ versionhq/tool/model.py,sha256=cWfLQVjNkag5cYYqhABBK7-jcpl0UJQWuhDciG3MtPQ,8116
38
+ versionhq/tool/tool_handler.py,sha256=A3zUkZkx4JEpFHI2uBkHDpzWfADw-bCYUQhgm6rpITM,1569
39
+ versionhq-1.1.9.3.dist-info/LICENSE,sha256=7CCXuMrAjPVsUvZrsBq9DsxI2rLDUSYXR_qj4yO_ZII,1077
40
+ versionhq-1.1.9.3.dist-info/METADATA,sha256=MiNTSV--oEyOAWcxDsqOMRvI37Ys8dmIuzI6gSg3p1s,16070
41
+ versionhq-1.1.9.3.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
42
+ versionhq-1.1.9.3.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
43
+ versionhq-1.1.9.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,149 +0,0 @@
1
- import os
2
- import uuid
3
- from dotenv import load_dotenv
4
- from typing import Any, Callable, Type, get_args, get_origin, Optional, Tuple
5
-
6
- from pydantic import BaseModel, Field, model_validator, field_validator, UUID4
7
- from pydantic_core import PydanticCustomError
8
-
9
- from composio import ComposioToolSet, Action, App, action
10
-
11
- from versionhq.tool import ComposioAppName, ComposioAuthScheme, composio_app_set, COMPOSIO_STATUS
12
-
13
- load_dotenv(override=True)
14
-
15
- DEFAULT_REDIRECT_URL = os.environ.get("DEFAULT_REDIRECT_URL", None)
16
- DEFAULT_USER_ID = os.environ.get("DEFAULT_USER_ID", None)
17
-
18
-
19
- class Composio(BaseModel):
20
- """
21
- Class to handle composio tools.
22
- """
23
-
24
- id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
25
- app_name: str = Field(default=ComposioAppName.HUBSPOT)
26
- user_id: str = Field(default=DEFAULT_USER_ID)
27
- auth_scheme: str = Field(default=ComposioAuthScheme.OAUTH2)
28
- redirect_url: str = Field(default=DEFAULT_REDIRECT_URL, description="redirect url after successful oauth2 connection")
29
- connect_request_id: str = Field(default=None, description="store the client's composio id to connect with the app")
30
-
31
- @property
32
- def toolset(self) -> ComposioToolSet:
33
- return ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"))
34
-
35
-
36
- def __name__(self):
37
- return self.app_name
38
-
39
-
40
- @field_validator("id", mode="before")
41
- @classmethod
42
- def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
43
- if v:
44
- raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
45
-
46
-
47
- # @model_validator("user_id", mode="before")
48
- # @classmethod
49
- # def _deny_no_user_id(cls, v: Optional[str]) -> None:
50
- # if v is None:
51
- # raise PydanticCustomError("user_id_missing", "Need user_id to connect with the tool", {})
52
-
53
-
54
- @model_validator(mode="after")
55
- def validate_app_name(self):
56
- if self.app_name not in ComposioAppName:
57
- raise PydanticCustomError("no_app_name", f"The app name {self.app_name} is not valid.", {})
58
-
59
- return self
60
-
61
-
62
- @model_validator(mode="after")
63
- def validate_auth_scheme(self):
64
- """
65
- Raise error when the client uses auth scheme unavailable for the app.
66
- """
67
- app_set = next(filter(lambda tup: self.app_name in tup, composio_app_set), None)
68
- if not app_set:
69
- raise PydanticCustomError("no_app_set", f"The app set of {self.app_name} is missing.", {})
70
-
71
- else:
72
- acceptable_auth_scheme = next(filter(lambda item: self.auth_scheme in item, app_set), None)
73
- if acceptable_auth_scheme is None:
74
- raise PydanticCustomError("invalid_auth_scheme", f"The app {self.app_name} must have different auth_scheme.", {})
75
-
76
- return self
77
-
78
-
79
- # connect with composio to use the tool
80
- def connect(self, token: Optional[str] = None, api_key: Optional[str] = None) -> Tuple[str | Any]:
81
- """
82
- Connect with Composio, retrieve `connect_request_id`, and validate the connection.
83
- """
84
-
85
- if not self.user_id:
86
- raise PydanticCustomError("user_id_missing", "Need user_id to connect with the tool", {})
87
-
88
-
89
- connection_request, connected_account = None, None
90
-
91
- if self.auth_scheme == ComposioAuthScheme.API_KEY:
92
- collected_from_user = {}
93
- collected_from_user["api_key"] = api_key
94
- connection_request = self.toolset.initiate_connection(
95
- connected_account_params = collected_from_user,
96
- app=self.app_name,
97
- entity_id=self.user_id,
98
- auth_scheme=self.auth_scheme,
99
- )
100
-
101
- if self.auth_scheme == ComposioAuthScheme.BEARER_TOKEN:
102
- collected_from_user = {}
103
- collected_from_user["token"] = token
104
- connection_request = self.toolset.initiate_connection(
105
- connected_account_params = collected_from_user,
106
- app=self.app_name,
107
- entity_id=self.user_id,
108
- auth_scheme=self.auth_scheme,
109
- )
110
-
111
- if self.auth_scheme == ComposioAuthScheme.OAUTH2:
112
- connection_request = self.toolset.initiate_connection(
113
- app=self.app_name,
114
- redirect_url = self.redirect_url, # clients will be redirected to this url after successful auth.
115
- entity_id=self.user_id,
116
- auth_scheme=self.auth_scheme,
117
- )
118
-
119
- # connection_request.wait_until_active(self.toolset.client, timeout=60)
120
-
121
- if connection_request.connectionStatus is not COMPOSIO_STATUS.FAILED:
122
- self.connect_request_id = connection_request.connectedAccountId
123
- connected_account = self.toolset.get_connected_account(id=self.connect_request_id)
124
-
125
- if connected_account.status is not COMPOSIO_STATUS.FAILED:
126
- setattr(self.toolset, "entity_id", self.user_id)
127
-
128
- return connected_account, connected_account.status if connected_account else connection_request.connectionStatus
129
-
130
-
131
-
132
-
133
- # @action(toolname=ComposioAppName.HUBSPOT)
134
- # def deploy(self, param1: str, param2: str, execute_request: Callable) -> str:
135
- # """
136
- # Define custom actions
137
- # my custom action description which will be passed to llm
138
-
139
- # :param param1: param1 description which will be passed to llm
140
- # :param param2: param2 description which will be passed to llm
141
- # :return info: return description
142
- # """
143
-
144
- # response = execute_request(
145
- # "/my_action_endpoint",
146
- # "GET",
147
- # {} # body can be added here
148
- # ) # execute requests by appending credentials to the request
149
- # return str(response) # complete auth dict is available for local use if needed