Source code for lwe.core.tool_manager
import os
import json
import importlib
import traceback
from pathlib import Path
import langchain_community.tools
from langchain_core.utils.function_calling import convert_to_openai_function
from lwe.core.config import Config
from lwe.core.logger import Logger
import lwe.core.util as util
LANGCHAIN_TOOL_PREFIX = "Langchain-"
[docs]
class ToolManager:
"""
Manage tools.
"""
def __init__(self, config=None, additional_tools=None):
self.config = config or Config()
self.additional_tools = additional_tools or {}
self.log = Logger(self.__class__.__name__, self.config)
self.user_tool_dirs = (
self.config.args.tools_dir
or util.get_environment_variable_list("tool_dir")
or self.config.get("directories.tools")
)
self.make_user_tool_dirs()
self.system_tool_dirs = [
os.path.join(util.get_package_root(self), "tools"),
]
self.all_tool_dirs = self.system_tool_dirs + self.user_tool_dirs
[docs]
def make_user_tool_dirs(self):
for tool_dir in self.user_tool_dirs:
if not os.path.exists(tool_dir):
os.makedirs(tool_dir)
[docs]
def load_tool(self, tool_name):
self.log.debug("Loading tool from dirs: %s" % ", ".join(self.all_tool_dirs))
tool_filepath = None
try:
for tool_dir in self.all_tool_dirs:
if os.path.exists(tool_dir) and os.path.isdir(tool_dir):
self.log.debug(f"Processing directory: {tool_dir}")
filename = f"{tool_name}.py"
if filename in os.listdir(tool_dir):
self.log.debug(f"Loading tool file {filename} from directory: {tool_dir}")
try:
filepath = os.path.join(tool_dir, filename)
with open(filepath, "r") as _:
tool_filepath = filepath
except Exception as e:
self.log.warning(
f"Can't open tool file {tool_name} from directory: {tool_dir}: {e}"
)
else:
message = f"Failed to load tool {tool_name}: Directory {tool_dir!r} not found or not a directory"
self.log.error(message)
return False, None, message
except Exception as e:
message = f"An error occurred while loading tool {tool_name}: {e}"
self.log.error(message)
return False, None, message
if tool_filepath is not None:
message = f"Successfully loaded tool file {tool_name} from directory: {tool_dir}"
self.log.debug(message)
return True, tool_filepath, message
return False, None, f"Tool {tool_name} not found"
[docs]
def is_langchain_tool(self, tool_name):
self.log.debug(f"Checking for Langchain tool: {tool_name}")
return tool_name.lower().startswith(LANGCHAIN_TOOL_PREFIX.lower())
[docs]
def get_langchain_tool(self, tool_name):
self.log.debug(f"Loading Langchain tool: {tool_name}")
tool_name = util.remove_prefix(tool_name, LANGCHAIN_TOOL_PREFIX)
try:
tool = getattr(langchain_community.tools, tool_name)
tool_instance = tool()
return tool_instance
except Exception as e:
self.log.warning(f"Could not load Langchain tool: {tool_name}: {str(e)}")
return None
[docs]
def get_langchain_tool_spec(self, tool_name):
self.log.debug(f"Loading tool spec for Langchain tool: {tool_name}")
tool_instance = self.get_langchain_tool(tool_name)
if not tool_instance:
raise RuntimeError(f"Langchain tool {tool_name} not found")
spec = convert_to_openai_function(tool_instance)
spec["name"] = tool_name
return spec
[docs]
def run_langchain_tool(self, tool_name, input_data):
self.log.debug(f"Running langchaing tool: {tool_name} with data: {input_data}")
tool_instance = self.get_langchain_tool(tool_name)
if not tool_instance:
raise RuntimeError(f"Langchain tool {tool_name} not found")
try:
result = tool_instance.run(input_data)
except Exception as e:
message = (
f"Error: Exception occurred while running langchain tool {tool_name}: {str(e)}"
)
self.log.error(message)
return False, None, message
message = f"Langchain tool {tool_name} executed successfully, output data: {result}"
self.log.info(message)
return True, result, message
[docs]
def load_tools(self):
self.log.debug("Loading tools from dirs: %s" % ", ".join(self.all_tool_dirs))
self.tools = self.additional_tools
try:
for tool_dir in self.all_tool_dirs:
if os.path.exists(tool_dir) and os.path.isdir(tool_dir):
self.log.info(f"Processing directory: {tool_dir}")
for filename in os.listdir(tool_dir):
filepath = os.path.join(tool_dir, filename)
if filepath.endswith(".py"):
tool_name = Path(filename).stem
self.log.debug(
f"Loading tool file {filename} from directory: {tool_dir}"
)
self.tools[tool_name] = filepath
else:
message = f"Failed to load directory {tool_dir!r}: not found or not a directory"
self.log.error(message)
return False, None, message
return True, self.tools, "Successfully loaded tools"
except Exception as e:
message = f"An error occurred while loading tools: {e}"
self.log.error(message)
return False, None, message
[docs]
def setup_tool_instance(self, tool_name, tool_path):
self.log.debug(f"Loading tool {tool_name} from {tool_path}")
try:
spec = importlib.util.spec_from_file_location(tool_name, tool_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
tool_class_name = util.snake_to_class(tool_name)
tool_class = getattr(module, tool_class_name)
tool_instance = tool_class(config=self.config)
tool_instance.set_name(tool_name)
tool_instance.set_filepath(tool_path)
return tool_instance
except Exception as e:
self.log.error(f"Error creating tool instance for {tool_name}: {e}")
raise RuntimeError(f"Error creating tool instance for {tool_name}") from e
[docs]
def dereference_tool_schema(self, schema, defs):
if isinstance(schema, dict):
if "$ref" in schema:
ref_path = schema["$ref"].split("/")[-1] # Get the last part after '/'
return self.dereference_tool_schema(defs[ref_path], defs)
else:
for key, value in schema.items():
schema[key] = self.dereference_tool_schema(value, defs)
elif isinstance(schema, list):
return [self.dereference_tool_schema(item, defs) for item in schema]
return schema
[docs]
def cleanup_tool_definition(self, tool):
"""Remove items that are not needed in the tool definition."""
if "parameters" in tool:
defs = tool["parameters"].pop("$defs", None)
if defs:
self.log.debug("Dereferencing $defs in tool parameters")
tool["parameters"] = self.dereference_tool_schema(tool["parameters"], defs)
return tool
[docs]
def get_tool_config(self, tool_name):
self.log.debug(f"Getting config for tool: {tool_name}")
if self.is_langchain_tool(tool_name):
return self.get_langchain_tool_spec(tool_name)
try:
_success, tool_path, user_message = self.load_tool(tool_name)
tool_instance = self.setup_tool_instance(tool_name, tool_path)
config = self.cleanup_tool_definition(tool_instance.get_config())
return config
except Exception as e:
self.log.error(f"Error loading tool configuration for {tool_name}: {str(e)}")
raise RuntimeError(f"Failed to load configuration for {tool_name}") from e
[docs]
def get_tool(self, tool_name):
self.log.debug(f"Getting tool: {tool_name}")
success, tool_path, user_message = self.load_tool(tool_name)
if not success:
return False, tool_name, user_message
tool_instance = self.setup_tool_instance(tool_name, tool_path)
return True, tool_instance, f"Tool {tool_name!r} retrieved successfully"
[docs]
def run_tool(self, tool_name, input_data):
if isinstance(input_data, str):
input_data = json.loads(input_data, strict=False)
if self.is_langchain_tool(tool_name):
return self.run_langchain_tool(tool_name, input_data)
self.log.debug(f"Running tool: {tool_name} with data: {input_data}")
success, tool_instance, user_message = self.get_tool(tool_name)
if not success:
return False, tool_instance, user_message
try:
output_data = tool_instance(**input_data)
self.log.info(f"Tool {tool_name} executed successfully, output data: {output_data}")
return True, output_data, f"Tool {tool_name!r} executed successfully"
except Exception as e:
message = f"Error: Exception occurred while executing {tool_name}: {str(e)}"
self.log.error(message)
if self.config.debug:
traceback.print_exc()
return False, None, message
[docs]
def is_system_tool(self, filepath):
for dir in self.system_tool_dirs:
if filepath.startswith(dir):
return True
return False