1+ """
2+ Base class for built-in tools inspired by Qwen-Agent.
3+
4+ This module provides the base infrastructure for implementing built-in tools
5+ that are compatible with the HelpingAI tools framework.
6+ """
7+
8+ import os
9+ import tempfile
10+ from abc import ABC , abstractmethod
11+ from pathlib import Path
12+ from typing import Dict , Any , Union , Optional
13+ from urllib .parse import urlparse
14+ from urllib .request import urlopen
15+
16+ from ..core import Fn
17+ from ..errors import ToolExecutionError
18+
19+
20+ class BuiltinToolBase (ABC ):
21+ """Base class for built-in tools.
22+
23+ This class provides common functionality for built-in tools including
24+ file handling, parameter validation, and integration with HelpingAI's tool framework.
25+ """
26+
27+ # To be overridden by subclasses
28+ name : str = ""
29+ description : str = ""
30+ parameters : Dict [str , Any ] = {}
31+
32+ def __init__ (self , config : Optional [Dict [str , Any ]] = None ):
33+ """Initialize the built-in tool.
34+
35+ Args:
36+ config: Optional configuration dictionary
37+ """
38+ self .config = config or {}
39+
40+ # Set up working directory
41+ default_work_dir = os .path .join (tempfile .gettempdir (), 'helpingai_tools' , self .name )
42+ self .work_dir = self .config .get ('work_dir' , default_work_dir )
43+ os .makedirs (self .work_dir , exist_ok = True )
44+
45+ if not self .name :
46+ raise ValueError (f"Tool class { self .__class__ .__name__ } must define a 'name' attribute" )
47+
48+ if not self .description :
49+ raise ValueError (f"Tool class { self .__class__ .__name__ } must define a 'description' attribute" )
50+
51+ @abstractmethod
52+ def execute (self , ** kwargs ) -> str :
53+ """Execute the tool with given parameters.
54+
55+ Args:
56+ **kwargs: Tool parameters
57+
58+ Returns:
59+ Tool execution result as string
60+
61+ Raises:
62+ ToolExecutionError: If execution fails
63+ """
64+ raise NotImplementedError
65+
66+ def to_fn (self ) -> Fn :
67+ """Convert this built-in tool to an Fn object.
68+
69+ Returns:
70+ Fn object that can be used with HelpingAI's tool framework
71+ """
72+ def tool_function (** kwargs ) -> str :
73+ """Wrapper function for tool execution."""
74+ try :
75+ return self .execute (** kwargs )
76+ except Exception as e :
77+ raise ToolExecutionError (
78+ f"Failed to execute built-in tool '{ self .name } ': { e } " ,
79+ tool_name = self .name ,
80+ original_error = e
81+ )
82+
83+ return Fn (
84+ name = self .name ,
85+ description = self .description ,
86+ parameters = self .parameters ,
87+ function = tool_function
88+ )
89+
90+ def _validate_parameters (self , params : Dict [str , Any ]) -> None :
91+ """Validate tool parameters against schema.
92+
93+ Args:
94+ params: Parameters to validate
95+
96+ Raises:
97+ ValueError: If validation fails
98+ """
99+ # Check required parameters
100+ required_params = self .parameters .get ('required' , [])
101+ for param in required_params :
102+ if param not in params :
103+ raise ValueError (f"Missing required parameter '{ param } ' for tool '{ self .name } '" )
104+
105+ # Check for unknown parameters
106+ allowed_params = set (self .parameters .get ('properties' , {}).keys ())
107+ provided_params = set (params .keys ())
108+ unknown_params = provided_params - allowed_params
109+
110+ if unknown_params :
111+ raise ValueError (f"Unknown parameters for tool '{ self .name } ': { ', ' .join (unknown_params )} " )
112+
113+ def _download_file (self , url : str , filename : str = None ) -> str :
114+ """Download a file from URL to working directory.
115+
116+ Args:
117+ url: URL to download from
118+ filename: Optional filename, will be inferred from URL if not provided
119+
120+ Returns:
121+ Path to downloaded file
122+
123+ Raises:
124+ ToolExecutionError: If download fails
125+ """
126+ try :
127+ if not filename :
128+ parsed_url = urlparse (url )
129+ filename = os .path .basename (parsed_url .path ) or 'downloaded_file'
130+
131+ file_path = os .path .join (self .work_dir , filename )
132+
133+ with urlopen (url ) as response :
134+ with open (file_path , 'wb' ) as f :
135+ f .write (response .read ())
136+
137+ return file_path
138+
139+ except Exception as e :
140+ raise ToolExecutionError (
141+ f"Failed to download file from { url } : { e } " ,
142+ tool_name = self .name ,
143+ original_error = e
144+ )
145+
146+ def _read_file (self , file_path : str ) -> str :
147+ """Read file content as text.
148+
149+ Args:
150+ file_path: Path to file
151+
152+ Returns:
153+ File content as string
154+
155+ Raises:
156+ ToolExecutionError: If reading fails
157+ """
158+ try :
159+ if file_path .startswith (('http://' , 'https://' )):
160+ # Download the file first
161+ local_path = self ._download_file (file_path )
162+ file_path = local_path
163+
164+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
165+ return f .read ()
166+
167+ except Exception as e :
168+ raise ToolExecutionError (
169+ f"Failed to read file { file_path } : { e } " ,
170+ tool_name = self .name ,
171+ original_error = e
172+ )
173+
174+ def _write_file (self , content : str , filename : str ) -> str :
175+ """Write content to file in working directory.
176+
177+ Args:
178+ content: Content to write
179+ filename: Filename
180+
181+ Returns:
182+ Path to written file
183+
184+ Raises:
185+ ToolExecutionError: If writing fails
186+ """
187+ try :
188+ file_path = os .path .join (self .work_dir , filename )
189+
190+ with open (file_path , 'w' , encoding = 'utf-8' ) as f :
191+ f .write (content )
192+
193+ return file_path
194+
195+ except Exception as e :
196+ raise ToolExecutionError (
197+ f"Failed to write file { filename } : { e } " ,
198+ tool_name = self .name ,
199+ original_error = e
200+ )
201+
202+ def _cleanup_work_dir (self ) -> None :
203+ """Clean up the working directory."""
204+ try :
205+ import shutil
206+ if os .path .exists (self .work_dir ):
207+ shutil .rmtree (self .work_dir )
208+ except Exception :
209+ # Ignore cleanup errors
210+ pass
0 commit comments