1- import base64
2- import json
3- from collections .abc import Callable , Sequence
4- from typing import Annotated , Any , ClassVar
1+ from collections .abc import Sequence
2+ from typing import Any
53
6- import requests
7- from langchain_core .tools import BaseTool as LangChainBaseTool
8- from pydantic import BaseModel , Field , PrivateAttr
4+ from langchain_core .tools import BaseTool
5+ from pydantic import BaseModel , Field
96
107
118class ExecuteConfig (BaseModel ):
@@ -30,117 +27,23 @@ class ToolDefinition(BaseModel):
3027 execute : ExecuteConfig
3128
3229
33- class BaseTool (BaseModel ):
34- """Base Tool model with Pydantic validation """
30+ class Tool (BaseModel ):
31+ """Base Tool model"""
3532
33+ name : str
3634 description : str
3735 parameters : ToolParameters
3836
39- # Private attributes in Pydantic v2
40- _execute_config : ExecuteConfig = PrivateAttr ()
41- _api_key : str = PrivateAttr ()
42- _account_id : str | None = PrivateAttr (default = None )
43-
44- def __init__ (self , ** data : Any ) -> None :
45- super ().__init__ (** data )
46- execute_config = data .get ("_execute_config" )
47- api_key = data .get ("_api_key" )
48-
49- if not isinstance (execute_config , ExecuteConfig ):
50- raise ValueError ("_execute_config must be an ExecuteConfig instance" )
51- if not isinstance (api_key , str ):
52- raise ValueError ("_api_key must be a string" )
53-
54- self ._execute_config = execute_config
55- self ._api_key = api_key
56- self ._account_id = data .get ("_account_id" )
57-
5837 def execute (self , arguments : str | dict | None = None ) -> dict [str , Any ]:
59- """
60- Execute the tool with the given parameters
61-
62- Args:
63- arguments: Either a JSON string or dict of arguments
64- """
65- # Handle both string and dict arguments
66- if isinstance (arguments , str ):
67- kwargs = json .loads (arguments )
68- else :
69- kwargs = arguments or {}
70-
71- url = self ._execute_config .url
72- body_params = {}
73- query_params = {}
74- header_params = {}
75-
76- # Separate parameters based on their OpenAPI location
77- if kwargs :
78- for key , value in kwargs .items ():
79- param_location = self ._execute_config .parameter_locations .get (key )
80-
81- if param_location == "header" :
82- header_params [key ] = str (value )
83- elif param_location == "query" :
84- query_params [key ] = value
85- elif param_location == "path" :
86- url = url .replace (f"{{{ key } }}" , str (value ))
87- elif param_location == "body" :
88- body_params [key ] = value
89- else :
90- # Default behavior for backward compatibility
91- if f"{{{ key } }}" in url :
92- url = url .replace (f"{{{ key } }}" , str (value ))
93- elif self ._execute_config .method .upper () in ["GET" , "DELETE" ]:
94- query_params [key ] = value
95- else :
96- body_params [key ] = value
97-
98- # Create basic auth header with API key as username
99- auth_string = base64 .b64encode (f"{ self ._api_key } :" .encode ()).decode ()
100-
101- headers = {
102- "Authorization" : f"Basic { auth_string } " ,
103- "User-Agent" : "stackone-python/1.0.0" ,
104- }
105-
106- if self ._account_id :
107- headers ["x-account-id" ] = self ._account_id
108-
109- # Add custom header parameters
110- headers .update (header_params )
111-
112- # Add predefined headers last to ensure they take precedence
113- headers .update (self ._execute_config .headers )
114-
115- request_kwargs : dict [str , Any ] = {
116- "method" : self ._execute_config .method ,
117- "url" : url ,
118- "headers" : headers ,
119- }
120-
121- # Handle request body if we have body parameters
122- if body_params :
123- body_type = self ._execute_config .body_type or "json"
124- if body_type == "json" :
125- request_kwargs ["json" ] = body_params
126- elif body_type == "form" :
127- request_kwargs ["data" ] = body_params
128-
129- if query_params :
130- request_kwargs ["params" ] = query_params
131-
132- response = requests .request (** request_kwargs )
133- response .raise_for_status ()
134-
135- result : dict [str , Any ] = response .json ()
136- return result
38+ """Execute the tool with the given parameters"""
39+ raise NotImplementedError
13740
13841 def to_openai_function (self ) -> dict :
13942 """Convert this tool to OpenAI's function format"""
14043 return {
14144 "type" : "function" ,
14245 "function" : {
143- "name" : self ._execute_config . name ,
46+ "name" : self .name ,
14447 "description" : self .description ,
14548 "parameters" : {
14649 "type" : self .parameters .type ,
@@ -152,46 +55,24 @@ def to_openai_function(self) -> dict:
15255 },
15356 }
15457
155- @property
156- def name (self ) -> str :
157- """Get the tool's name"""
158- return self ._execute_config .name
159-
160- def set_account_id (self , account_id : str | None ) -> None :
161- """Set the account ID for this tool.
162-
163- Args:
164- account_id: The account ID to use, or None to clear it
165- """
166- self ._account_id = account_id
167-
168- def get_account_id (self ) -> str | None :
169- """Get the current account ID for this tool."""
170- return self ._account_id
171-
17258
17359class Tools :
17460 """Container for Tool instances"""
17561
176- def __init__ (self , tools : list [BaseTool ]):
62+ def __init__ (self , tools : list [Tool ]):
17763 self .tools = tools
178- # Create a name -> tool mapping for faster lookups
17964 self ._tool_map = {tool .name : tool for tool in tools }
18065
181- def __getitem__ (self , index : int ) -> BaseTool :
66+ def __getitem__ (self , index : int ) -> Tool :
18267 return self .tools [index ]
18368
18469 def __len__ (self ) -> int :
18570 return len (self .tools )
18671
187- def get_tool (self , name : str ) -> BaseTool | None :
72+ def get_tool (self , name : str ) -> Tool | None :
18873 """Get a tool by its name"""
18974 return self ._tool_map .get (name )
19075
191- def to_openai (self ) -> list [dict ]:
192- """Convert all tools to OpenAI function format"""
193- return [tool .to_openai_function () for tool in self .tools ]
194-
19576 def set_account_id (self , account_id : str | None ) -> None :
19677 """Set the account ID for all tools in this collection.
19778
@@ -201,81 +82,18 @@ def set_account_id(self, account_id: str | None) -> None:
20182 for tool in self .tools :
20283 tool .set_account_id (account_id )
20384
204- def to_langchain (self ) -> Sequence [LangChainBaseTool ]:
205- """Convert all tools to LangChain tool format"""
206- langchain_tools = []
207-
85+ def get_account_id (self ) -> str | None :
86+ """Get the current account ID for this tool."""
20887 for tool in self .tools :
209- # Create properly annotated schema for the tool
210- schema_props : dict [str , Field ] = {}
211- annotations : dict [str , Any ] = {}
212-
213- for name , details in tool .parameters .properties .items ():
214- # Convert OpenAPI types to Python types
215- python_type : type = str # Default to str
216- if isinstance (details , dict ): # Check if details is a dict
217- type_str = details .get ("type" , "string" )
218- if type_str == "number" :
219- python_type = float
220- elif type_str == "integer" :
221- python_type = int
222- elif type_str == "boolean" :
223- python_type = bool
88+ account_id = tool .get_account_id ()
89+ if isinstance (account_id , str ): # Type guard to ensure we return str | None
90+ return account_id
91+ return None
22492
225- # Create Field with description if available
226- field = Field (description = details .get ("description" , "" ))
227- else :
228- # Handle case where details is a string
229- field = Field (description = "" )
230-
231- schema_props [name ] = field
232- annotations [name ] = Annotated [python_type , field ]
233-
234- # Create the schema class with proper annotations
235- schema_class = type (
236- f"{ tool .name .title ()} Args" ,
237- (BaseModel ,),
238- {
239- "__annotations__" : annotations ,
240- "__module__" : __name__ ,
241- ** schema_props ,
242- },
243- )
244-
245- # Create the LangChain tool with proper type annotations
246- tool_annotations = {
247- "name" : ClassVar [str ],
248- "description" : ClassVar [str ],
249- "args_schema" : ClassVar [type ],
250- }
251-
252- def create_run_method (t : BaseTool ) -> Callable [..., Any ]:
253- def _run (self : Any , ** kwargs : Any ) -> Any :
254- # Convert kwargs to dict for execution
255- return t .execute (kwargs )
256-
257- return _run
258-
259- def create_arun_method (t : BaseTool ) -> Callable [..., Any ]:
260- async def _arun (self : Any , ** kwargs : Any ) -> Any :
261- # Convert kwargs to dict for execution
262- return t .execute (kwargs )
263-
264- return _arun
265-
266- langchain_tool = type (
267- f"StackOne{ tool .name .title ()} Tool" ,
268- (LangChainBaseTool ,),
269- {
270- "__annotations__" : tool_annotations ,
271- "__module__" : __name__ ,
272- "name" : tool .name ,
273- "description" : tool .description ,
274- "args_schema" : schema_class ,
275- "_run" : create_run_method (tool ),
276- "_arun" : create_arun_method (tool ),
277- },
278- )
279- langchain_tools .append (langchain_tool ())
93+ def to_openai (self ) -> list [dict ]:
94+ """Convert all tools to OpenAI function format"""
95+ return [tool .to_openai_function () for tool in self .tools ]
28096
281- return langchain_tools
97+ def to_langchain (self ) -> Sequence [BaseTool ]:
98+ """Convert all tools to LangChain format"""
99+ return [tool .to_langchain () for tool in self .tools ]
0 commit comments