tooluniverse 0.2.0__py3-none-any.whl → 1.0.1__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.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/adverse_event_tools.json +108 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +1 -1
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.1.dist-info/METADATA +387 -0
- tooluniverse-1.0.1.dist-info/RECORD +182 -0
- tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
- tooluniverse/generate_mcp_tools.py +0 -113
- tooluniverse/mcp_server.py +0 -3340
- tooluniverse-0.2.0.dist-info/METADATA +0 -139
- tooluniverse-0.2.0.dist-info/RECORD +0 -21
- tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/top_level.txt +0 -0
tooluniverse/execute_function.py
CHANGED
|
@@ -1,107 +1,1302 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
ToolUniverse Function Execution Module
|
|
3
|
+
|
|
4
|
+
This module provides the core ToolUniverse class for managing and executing various scientific and data tools.
|
|
5
|
+
It supports loading tools from JSON configurations, organizing them by categories, validating function calls,
|
|
6
|
+
and executing tools with proper error handling and caching.
|
|
7
|
+
|
|
8
|
+
The module includes support for:
|
|
9
|
+
- GraphQL tools (OpenTarget, OpenTarget Genetics)
|
|
10
|
+
- RESTful API tools (Monarch, ChEMBL, PubChem, etc.)
|
|
11
|
+
- FDA drug labeling and adverse event tools
|
|
12
|
+
- Clinical trials tools
|
|
13
|
+
- Literature search tools (EuropePMC, Semantic Scholar, PubTator)
|
|
14
|
+
- Biological databases (HPA, Reactome, UniProt)
|
|
15
|
+
- MCP (Model Context Protocol) clients and auto-loaders
|
|
16
|
+
- Enrichment analysis tools
|
|
17
|
+
- Package management tools
|
|
18
|
+
|
|
19
|
+
Classes:
|
|
20
|
+
ToolUniverse: Main class for tool management and execution
|
|
21
|
+
|
|
22
|
+
Constants:
|
|
23
|
+
default_tool_files: Default mapping of tool categories to JSON file paths
|
|
24
|
+
tool_type_mappings: Mapping of tool type strings to their implementation classes
|
|
25
|
+
"""
|
|
26
|
+
|
|
2
27
|
import copy
|
|
3
28
|
import json
|
|
4
29
|
import random
|
|
5
30
|
import string
|
|
6
|
-
from .graphql_tool import OpentargetTool, OpentargetGeneticsTool, OpentargetToolDrugNameMatch
|
|
7
|
-
from .openfda_tool import FDADrugLabelTool, FDADrugLabelSearchTool, FDADrugLabelSearchIDTool, FDADrugLabelGetDrugGenericNameTool
|
|
8
|
-
from .restful_tool import MonarchTool, MonarchDiseasesForMultiplePhenoTool
|
|
9
|
-
|
|
10
31
|
import os
|
|
32
|
+
import time
|
|
33
|
+
from .utils import read_json_list, evaluate_function_call, extract_function_call_json
|
|
34
|
+
from .tool_registry import (
|
|
35
|
+
auto_discover_tools,
|
|
36
|
+
get_tool_registry,
|
|
37
|
+
register_external_tool,
|
|
38
|
+
get_tool_class_lazy,
|
|
39
|
+
)
|
|
40
|
+
from .logging_config import (
|
|
41
|
+
get_logger,
|
|
42
|
+
debug,
|
|
43
|
+
info,
|
|
44
|
+
warning,
|
|
45
|
+
error,
|
|
46
|
+
set_log_level,
|
|
47
|
+
)
|
|
48
|
+
from .output_hook import HookManager
|
|
49
|
+
from .default_config import default_tool_files, get_default_hook_config
|
|
11
50
|
|
|
12
51
|
# Determine the directory where the current file is located
|
|
13
52
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
14
53
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
# Check if lazy loading is enabled (default: True for better performance)
|
|
55
|
+
LAZY_LOADING_ENABLED = os.getenv("TOOLUNIVERSE_LAZY_LOADING", "true").lower() in (
|
|
56
|
+
"true",
|
|
57
|
+
"1",
|
|
58
|
+
"yes",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if LAZY_LOADING_ENABLED:
|
|
62
|
+
# Use lazy auto-discovery by default (much faster)
|
|
63
|
+
debug("Starting lazy tool auto-discovery...")
|
|
64
|
+
tool_type_mappings = auto_discover_tools(lazy=True)
|
|
65
|
+
else:
|
|
66
|
+
# Use full auto-discovery (slower but ensures all tools are immediately available)
|
|
67
|
+
debug("Starting full tool auto-discovery...")
|
|
68
|
+
tool_type_mappings = auto_discover_tools(lazy=False)
|
|
69
|
+
|
|
70
|
+
# Update the registry with any manually added tools
|
|
71
|
+
tool_type_mappings = get_tool_registry()
|
|
72
|
+
|
|
73
|
+
if LAZY_LOADING_ENABLED:
|
|
74
|
+
debug(
|
|
75
|
+
f"Lazy tool registry initialized with {len(tool_type_mappings)} immediately available tools"
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
debug(f"Full tool registry initialized with {len(tool_type_mappings)} tools")
|
|
79
|
+
for _tool_name, _tool_class in sorted(tool_type_mappings.items()):
|
|
80
|
+
debug(f" - {_tool_name}: {_tool_class.__name__}")
|
|
33
81
|
|
|
34
82
|
|
|
35
83
|
class ToolUniverse:
|
|
36
|
-
|
|
37
|
-
|
|
84
|
+
"""
|
|
85
|
+
A comprehensive tool management system for loading, organizing, and executing various scientific and data tools.
|
|
86
|
+
|
|
87
|
+
The ToolUniverse class provides a centralized interface for managing different types of tools including
|
|
88
|
+
GraphQL tools, RESTful APIs, MCP clients, and specialized scientific tools. It handles tool loading,
|
|
89
|
+
filtering, caching, and execution.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
all_tools (list): List of all loaded tool configurations
|
|
93
|
+
all_tool_dict (dict): Dictionary mapping tool names to their configurations
|
|
94
|
+
tool_category_dicts (dict): Dictionary organizing tools by category
|
|
95
|
+
tool_files (dict): Dictionary mapping category names to their JSON file paths
|
|
96
|
+
callable_functions (dict): Cache of instantiated tool objects
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
tool_files=default_tool_files,
|
|
102
|
+
keep_default_tools=True,
|
|
103
|
+
log_level: str = None,
|
|
104
|
+
hooks_enabled: bool = False,
|
|
105
|
+
hook_config: dict = None,
|
|
106
|
+
hook_type: str = None,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Initialize the ToolUniverse with tool file configurations.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
tool_files (dict, optional): Dictionary mapping category names to JSON file paths.
|
|
113
|
+
Defaults to default_tool_files.
|
|
114
|
+
keep_default_tools (bool, optional): Whether to keep default tools when custom
|
|
115
|
+
tool_files are provided. Defaults to True.
|
|
116
|
+
log_level (str, optional): Log level for this instance. Can be 'DEBUG', 'INFO',
|
|
117
|
+
'WARNING', 'ERROR', 'CRITICAL'. If None, uses global setting.
|
|
118
|
+
hooks_enabled (bool, optional): Whether to enable output hooks. Defaults to False.
|
|
119
|
+
hook_config (dict, optional): Configuration for hooks. If None, uses default config.
|
|
120
|
+
hook_type (str or list, optional): Simple hook type selection. Can be 'SummarizationHook',
|
|
121
|
+
'FileSaveHook', or a list of both. Defaults to 'SummarizationHook'.
|
|
122
|
+
If both hook_config and hook_type are provided, hook_config takes precedence.
|
|
123
|
+
"""
|
|
124
|
+
# Set log level if specified
|
|
125
|
+
if log_level is not None:
|
|
126
|
+
set_log_level(log_level)
|
|
127
|
+
|
|
128
|
+
# Get logger for this class
|
|
129
|
+
self.logger = get_logger("ToolUniverse")
|
|
130
|
+
|
|
131
|
+
# Initialize any necessary attributes here FIRST
|
|
38
132
|
self.all_tools = []
|
|
39
133
|
self.all_tool_dict = {}
|
|
40
134
|
self.tool_category_dicts = {}
|
|
135
|
+
self.tool_finder = None
|
|
41
136
|
if tool_files is None:
|
|
42
137
|
tool_files = default_tool_files
|
|
43
138
|
elif keep_default_tools:
|
|
44
139
|
default_tool_files.update(tool_files)
|
|
45
140
|
tool_files = default_tool_files
|
|
46
141
|
self.tool_files = tool_files
|
|
47
|
-
|
|
48
|
-
|
|
142
|
+
|
|
143
|
+
self.logger.debug("Tool files:")
|
|
144
|
+
self.logger.debug(json.dumps(tool_files, indent=2))
|
|
49
145
|
self.callable_functions = {}
|
|
50
146
|
|
|
51
|
-
|
|
52
|
-
|
|
147
|
+
# Refresh the global tool_type_mappings to include any tools registered during imports
|
|
148
|
+
global tool_type_mappings
|
|
149
|
+
tool_type_mappings = get_tool_registry()
|
|
150
|
+
|
|
151
|
+
# Initialize hook system AFTER attributes are initialized
|
|
152
|
+
self.hooks_enabled = hooks_enabled
|
|
153
|
+
if self.hooks_enabled:
|
|
154
|
+
# Determine hook configuration
|
|
155
|
+
if hook_config is not None:
|
|
156
|
+
# Use provided hook_config (takes precedence)
|
|
157
|
+
final_hook_config = hook_config
|
|
158
|
+
self.logger.info("Using provided hook_config")
|
|
159
|
+
elif hook_type is not None:
|
|
160
|
+
# Use hook_type to generate simple configuration
|
|
161
|
+
final_hook_config = self._create_hook_config_from_type(hook_type)
|
|
162
|
+
self.logger.info(f"Using hook_type: {hook_type}")
|
|
163
|
+
else:
|
|
164
|
+
# Use default configuration with SummarizationHook
|
|
165
|
+
final_hook_config = get_default_hook_config()
|
|
166
|
+
self.logger.info("Using default hook configuration (SummarizationHook)")
|
|
167
|
+
|
|
168
|
+
self.hook_manager = HookManager(final_hook_config, self)
|
|
169
|
+
self.logger.info("Output hooks enabled")
|
|
170
|
+
else:
|
|
171
|
+
self.hook_manager = None
|
|
172
|
+
self.logger.debug("Output hooks disabled")
|
|
173
|
+
|
|
174
|
+
def register_custom_tool(self, tool_class, tool_name=None, tool_config=None):
|
|
175
|
+
"""
|
|
176
|
+
Register a custom tool class at runtime.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
tool_class: The tool class to register
|
|
180
|
+
tool_name (str, optional): Name to register under. Uses class name if None.
|
|
181
|
+
tool_config (dict, optional): Tool configuration dictionary to add to all_tools
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
str: The name the tool was registered under
|
|
185
|
+
"""
|
|
186
|
+
name = tool_name or tool_class.__name__
|
|
187
|
+
|
|
188
|
+
# Register the tool class
|
|
189
|
+
register_external_tool(name, tool_class)
|
|
190
|
+
|
|
191
|
+
# Update the global tool_type_mappings
|
|
192
|
+
global tool_type_mappings
|
|
193
|
+
tool_type_mappings = get_tool_registry()
|
|
194
|
+
|
|
195
|
+
# If tool_config is provided, add it to all_tools
|
|
196
|
+
if tool_config:
|
|
197
|
+
# Ensure the config has the correct type
|
|
198
|
+
if "type" not in tool_config:
|
|
199
|
+
tool_config["type"] = name
|
|
200
|
+
|
|
201
|
+
self.all_tools.append(tool_config)
|
|
202
|
+
if "name" in tool_config:
|
|
203
|
+
self.all_tool_dict[tool_config["name"]] = tool_config
|
|
204
|
+
|
|
205
|
+
self.logger.info(f"Custom tool '{name}' registered successfully!")
|
|
206
|
+
return name
|
|
207
|
+
|
|
208
|
+
def force_full_discovery(self):
|
|
209
|
+
"""
|
|
210
|
+
Force full tool discovery, importing all tool modules immediately.
|
|
211
|
+
|
|
212
|
+
This can be useful when you need to ensure all tools are available
|
|
213
|
+
immediately, bypassing lazy loading.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
dict: Updated tool registry with all discovered tools
|
|
217
|
+
"""
|
|
218
|
+
global tool_type_mappings
|
|
219
|
+
self.logger.info("Forcing full tool discovery...")
|
|
220
|
+
|
|
221
|
+
tool_type_mappings = auto_discover_tools(lazy=False)
|
|
222
|
+
self.logger.info(
|
|
223
|
+
f"Full discovery complete. {len(tool_type_mappings)} tools available."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return tool_type_mappings
|
|
227
|
+
|
|
228
|
+
def get_lazy_loading_status(self):
|
|
229
|
+
"""
|
|
230
|
+
Get information about lazy loading status and available tools.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
dict: Dictionary with lazy loading status and tool counts
|
|
234
|
+
"""
|
|
235
|
+
from .tool_registry import _discovery_completed, _lazy_registry
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"lazy_loading_enabled": LAZY_LOADING_ENABLED,
|
|
239
|
+
"full_discovery_completed": _discovery_completed,
|
|
240
|
+
"immediately_available_tools": len(tool_type_mappings),
|
|
241
|
+
"lazy_mappings_available": len(_lazy_registry),
|
|
242
|
+
"loaded_tools_count": (
|
|
243
|
+
len(self.all_tools) if hasattr(self, "all_tools") else 0
|
|
244
|
+
),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def get_tool_types(self):
|
|
248
|
+
"""
|
|
249
|
+
Get the types of tools available in the tool files.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
list: A list of tool type names (category keys).
|
|
253
|
+
"""
|
|
254
|
+
return list(self.tool_files.keys())
|
|
255
|
+
|
|
256
|
+
def _get_api_key(self, key_name: str):
|
|
257
|
+
"""Get API key from environment variables or loaded sources"""
|
|
258
|
+
# First check environment variables (highest priority)
|
|
259
|
+
env_value = os.getenv(key_name)
|
|
260
|
+
if env_value:
|
|
261
|
+
return env_value
|
|
262
|
+
else:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def _check_api_key_requirements(self, tool_config):
|
|
266
|
+
"""
|
|
267
|
+
Check if a tool's required API keys are available.
|
|
268
|
+
Also supports optional_api_keys where at least one key from the list must be available.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
tool_config (dict): Tool configuration containing optional 'required_api_keys' and 'optional_api_keys' fields
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
tuple: (bool, list) - (all_keys_available, missing_keys)
|
|
275
|
+
"""
|
|
276
|
+
required_keys = tool_config.get("required_api_keys", [])
|
|
277
|
+
optional_keys = tool_config.get("optional_api_keys", [])
|
|
278
|
+
|
|
279
|
+
missing_keys = []
|
|
280
|
+
|
|
281
|
+
# Check required keys (all must be available)
|
|
282
|
+
for key in required_keys:
|
|
283
|
+
if not self._get_api_key(key):
|
|
284
|
+
missing_keys.append(key)
|
|
285
|
+
|
|
286
|
+
# Check optional keys (at least one must be available)
|
|
287
|
+
optional_satisfied = True
|
|
288
|
+
if optional_keys:
|
|
289
|
+
optional_available = any(self._get_api_key(key) for key in optional_keys)
|
|
290
|
+
if not optional_available:
|
|
291
|
+
optional_satisfied = False
|
|
292
|
+
# For error reporting, add a descriptive message about optional keys
|
|
293
|
+
missing_keys.append(f"At least one of: {', '.join(optional_keys)}")
|
|
294
|
+
|
|
295
|
+
# Tool is valid if all required keys are available AND optional requirement is satisfied
|
|
296
|
+
all_valid = (
|
|
297
|
+
len([k for k in missing_keys if not k.startswith("At least one of:")]) == 0
|
|
298
|
+
and optional_satisfied
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return all_valid, missing_keys
|
|
302
|
+
|
|
303
|
+
def generate_env_template(
|
|
304
|
+
self, all_missing_keys, output_file: str = ".env.template"
|
|
305
|
+
):
|
|
306
|
+
"""Generate a template .env file with all required API keys"""
|
|
307
|
+
with open(output_file, "w") as f:
|
|
308
|
+
f.write("# API Keys for ToolUniverse\n")
|
|
309
|
+
f.write("# Copy this file to .env and fill in your actual API keys\n\n")
|
|
310
|
+
|
|
311
|
+
for key in sorted(all_missing_keys):
|
|
312
|
+
f.write(f"{key}=your_api_key_here\n\n")
|
|
313
|
+
|
|
314
|
+
self.logger.info(f"Generated API key template: {output_file}")
|
|
315
|
+
self.logger.info("Copy this file to .env and fill in your API keys")
|
|
316
|
+
|
|
317
|
+
def _create_hook_config_from_type(self, hook_type):
|
|
318
|
+
"""
|
|
319
|
+
Create hook configuration from simple hook_type parameter.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
hook_type (str or list): Hook type(s) to enable. Can be 'SummarizationHook',
|
|
323
|
+
'FileSaveHook', or a list of both.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
dict: Generated hook configuration
|
|
327
|
+
"""
|
|
328
|
+
# Handle single hook type
|
|
329
|
+
if isinstance(hook_type, str):
|
|
330
|
+
hook_types = [hook_type]
|
|
331
|
+
else:
|
|
332
|
+
hook_types = hook_type
|
|
333
|
+
|
|
334
|
+
# Validate hook types
|
|
335
|
+
valid_types = ["SummarizationHook", "FileSaveHook"]
|
|
336
|
+
for htype in hook_types:
|
|
337
|
+
if htype not in valid_types:
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"Invalid hook_type: {htype}. Valid types are: {valid_types}"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Create hooks list
|
|
343
|
+
hooks = []
|
|
344
|
+
|
|
345
|
+
for htype in hook_types:
|
|
346
|
+
if htype == "SummarizationHook":
|
|
347
|
+
hooks.append(
|
|
348
|
+
{
|
|
349
|
+
"name": "summarization_hook",
|
|
350
|
+
"type": "SummarizationHook",
|
|
351
|
+
"enabled": True,
|
|
352
|
+
"conditions": {
|
|
353
|
+
"output_length": {"operator": ">", "threshold": 5000}
|
|
354
|
+
},
|
|
355
|
+
"hook_config": {
|
|
356
|
+
"chunk_size": 32000,
|
|
357
|
+
"focus_areas": "key_findings_and_results",
|
|
358
|
+
"max_summary_length": 3000,
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
elif htype == "FileSaveHook":
|
|
363
|
+
hooks.append(
|
|
364
|
+
{
|
|
365
|
+
"name": "file_save_hook",
|
|
366
|
+
"type": "FileSaveHook",
|
|
367
|
+
"enabled": True,
|
|
368
|
+
"conditions": {
|
|
369
|
+
"output_length": {"operator": ">", "threshold": 1000}
|
|
370
|
+
},
|
|
371
|
+
"hook_config": {
|
|
372
|
+
"temp_dir": None,
|
|
373
|
+
"file_prefix": "tool_output",
|
|
374
|
+
"include_metadata": True,
|
|
375
|
+
"auto_cleanup": False,
|
|
376
|
+
"cleanup_age_hours": 24,
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return {"hooks": hooks}
|
|
382
|
+
|
|
383
|
+
def load_tools(
|
|
384
|
+
self,
|
|
385
|
+
tool_type=None,
|
|
386
|
+
exclude_tools=None,
|
|
387
|
+
exclude_categories=None,
|
|
388
|
+
include_tools=None,
|
|
389
|
+
tool_config_files=None,
|
|
390
|
+
tools_file=None,
|
|
391
|
+
include_tool_types=None,
|
|
392
|
+
exclude_tool_types=None,
|
|
393
|
+
):
|
|
394
|
+
"""
|
|
395
|
+
Loads tool definitions from JSON files into the instance's tool registry.
|
|
396
|
+
|
|
397
|
+
If `tool_type` is None, loads all available tool categories from `self.tool_files`.
|
|
398
|
+
Otherwise, loads only the specified tool categories.
|
|
399
|
+
|
|
400
|
+
After loading, deduplicates tools by their 'name' field and updates the internal tool list.
|
|
401
|
+
Also refreshes the tool name and description mapping.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
tool_type (list, optional): List of tool category names to load. If None, loads all categories.
|
|
405
|
+
exclude_tools (list, optional): List of specific tool names to exclude from loading.
|
|
406
|
+
exclude_categories (list, optional): List of tool categories to exclude from loading.
|
|
407
|
+
include_tools (list or str, optional): List of specific tool names to include, or path to a text file
|
|
408
|
+
containing tool names (one per line). If provided, only these tools
|
|
409
|
+
will be loaded regardless of categories.
|
|
410
|
+
tool_config_files (dict, optional): Additional tool configuration files to load.
|
|
411
|
+
Format: {"category_name": "/path/to/config.json"}
|
|
412
|
+
tools_file (str, optional): Path to a text file containing tool names to include (one per line).
|
|
413
|
+
Alternative to include_tools when providing a file path.
|
|
414
|
+
include_tool_types (list, optional): List of tool types to include (e.g., ["OpenTarget", "ChEMBLTool"]).
|
|
415
|
+
If provided, only tools with these types will be loaded.
|
|
416
|
+
exclude_tool_types (list, optional): List of tool types to exclude (e.g., ["ToolFinderEmbedding"]).
|
|
417
|
+
Tools with these types will be excluded.
|
|
418
|
+
|
|
419
|
+
Side Effects:
|
|
420
|
+
- Updates `self.all_tools` with loaded and deduplicated tools.
|
|
421
|
+
- Updates `self.tool_category_dicts` with loaded tools per category.
|
|
422
|
+
- Calls `self.refresh_tool_name_desc()` to update tool name/description mapping.
|
|
423
|
+
- Prints the number of tools before and after loading.
|
|
424
|
+
|
|
425
|
+
Examples:
|
|
426
|
+
# Load specific tools by name
|
|
427
|
+
tu.load_tools(include_tools=["UniProt_get_entry_by_accession", "ChEMBL_get_molecule_by_chembl_id"])
|
|
428
|
+
|
|
429
|
+
# Load tools from a file
|
|
430
|
+
tu.load_tools(tools_file="/path/to/tool_names.txt")
|
|
431
|
+
|
|
432
|
+
# Include only specific tool types
|
|
433
|
+
tu.load_tools(include_tool_types=["OpenTarget", "ChEMBLTool"])
|
|
434
|
+
|
|
435
|
+
# Exclude specific tool types
|
|
436
|
+
tu.load_tools(exclude_tool_types=["ToolFinderEmbedding", "Unknown"])
|
|
437
|
+
|
|
438
|
+
# Load additional config files
|
|
439
|
+
tu.load_tools(tool_config_files={"custom_tools": "/path/to/custom_tools.json"})
|
|
440
|
+
|
|
441
|
+
# Combine multiple options
|
|
442
|
+
tu.load_tools(
|
|
443
|
+
tool_type=["uniprot", "ChEMBL"],
|
|
444
|
+
exclude_tools=["problematic_tool"],
|
|
445
|
+
exclude_tool_types=["Unknown"],
|
|
446
|
+
tool_config_files={"custom": "/path/to/custom.json"}
|
|
447
|
+
)
|
|
448
|
+
"""
|
|
449
|
+
self.logger.debug(f"Number of tools before load tools: {len(self.all_tools)}")
|
|
450
|
+
|
|
451
|
+
# Handle tools_file parameter (alternative to include_tools)
|
|
452
|
+
if tools_file:
|
|
453
|
+
include_tools = self._load_tool_names_from_file(tools_file)
|
|
454
|
+
|
|
455
|
+
# Handle include_tools parameter
|
|
456
|
+
if isinstance(include_tools, str):
|
|
457
|
+
# If include_tools is a string, treat it as a file path
|
|
458
|
+
include_tools = self._load_tool_names_from_file(include_tools)
|
|
459
|
+
|
|
460
|
+
# Convert parameters to sets for efficient lookup
|
|
461
|
+
exclude_tools_set = set(exclude_tools or [])
|
|
462
|
+
exclude_categories_set = set(exclude_categories or [])
|
|
463
|
+
include_tools_set = set(include_tools or []) if include_tools else None
|
|
464
|
+
include_tool_types_set = (
|
|
465
|
+
set(include_tool_types or []) if include_tool_types else None
|
|
466
|
+
)
|
|
467
|
+
exclude_tool_types_set = (
|
|
468
|
+
set(exclude_tool_types or []) if exclude_tool_types else None
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Log operations
|
|
472
|
+
if exclude_tools_set:
|
|
473
|
+
self.logger.info(
|
|
474
|
+
f"Excluding tools by name: {', '.join(list(exclude_tools_set)[:5])}{'...' if len(exclude_tools_set) > 5 else ''}"
|
|
475
|
+
)
|
|
476
|
+
if exclude_categories_set:
|
|
477
|
+
self.logger.info(
|
|
478
|
+
f"Excluding categories: {', '.join(exclude_categories_set)}"
|
|
479
|
+
)
|
|
480
|
+
if include_tools_set:
|
|
481
|
+
self.logger.info(
|
|
482
|
+
f"Including only specific tools: {len(include_tools_set)} tools specified"
|
|
483
|
+
)
|
|
484
|
+
if include_tool_types_set:
|
|
485
|
+
self.logger.info(
|
|
486
|
+
f"Including only tool types: {', '.join(include_tool_types_set)}"
|
|
487
|
+
)
|
|
488
|
+
if exclude_tool_types_set:
|
|
489
|
+
self.logger.info(
|
|
490
|
+
f"Excluding tool types: {', '.join(exclude_tool_types_set)}"
|
|
491
|
+
)
|
|
492
|
+
if tool_config_files:
|
|
493
|
+
self.logger.info(
|
|
494
|
+
f"Loading additional config files: {', '.join(tool_config_files.keys())}"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Merge additional config files with existing tool_files
|
|
498
|
+
all_tool_files = self.tool_files.copy()
|
|
499
|
+
if tool_config_files:
|
|
500
|
+
# Validate that additional config files exist
|
|
501
|
+
for category, file_path in tool_config_files.items():
|
|
502
|
+
if os.path.exists(file_path):
|
|
503
|
+
all_tool_files[category] = file_path
|
|
504
|
+
self.logger.debug(
|
|
505
|
+
f"Added config file for category '{category}': {file_path}"
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
self.logger.warning(
|
|
509
|
+
f"Config file for category '{category}' not found: {file_path}"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Determine which categories to process
|
|
53
513
|
if tool_type is None:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
514
|
+
categories_to_load = [
|
|
515
|
+
cat
|
|
516
|
+
for cat in all_tool_files.keys()
|
|
517
|
+
if cat not in exclude_categories_set
|
|
518
|
+
]
|
|
58
519
|
else:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
520
|
+
assert isinstance(
|
|
521
|
+
tool_type, list
|
|
522
|
+
), "tool_type must be a list of tool category names"
|
|
523
|
+
categories_to_load = [
|
|
524
|
+
cat for cat in tool_type if cat not in exclude_categories_set
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
# Load tools from specified categories
|
|
528
|
+
for each in categories_to_load:
|
|
529
|
+
if each in all_tool_files:
|
|
530
|
+
try:
|
|
531
|
+
loaded_data = read_json_list(all_tool_files[each])
|
|
532
|
+
|
|
533
|
+
# Handle different data formats
|
|
534
|
+
if isinstance(loaded_data, dict):
|
|
535
|
+
# Convert dict of tools to list of tools
|
|
536
|
+
loaded_tool_list = list(loaded_data.values())
|
|
537
|
+
self.logger.debug(
|
|
538
|
+
f"Converted dict to list: {len(loaded_tool_list)} tools"
|
|
539
|
+
)
|
|
540
|
+
elif isinstance(loaded_data, list):
|
|
541
|
+
loaded_tool_list = loaded_data
|
|
542
|
+
else:
|
|
543
|
+
self.logger.warning(
|
|
544
|
+
f"Unexpected data format from {all_tool_files[each]}: {type(loaded_data)}"
|
|
545
|
+
)
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
self.all_tools += loaded_tool_list
|
|
549
|
+
self.tool_category_dicts[each] = loaded_tool_list
|
|
550
|
+
self.logger.debug(
|
|
551
|
+
f"Loaded {len(loaded_tool_list)} tools from category '{each}'"
|
|
552
|
+
)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
self.logger.error(
|
|
555
|
+
f"Error loading tools from category '{each}': {e}"
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
self.logger.warning(
|
|
559
|
+
f"Tool category '{each}' not found in available tool files"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Load auto-discovered configs from decorators
|
|
563
|
+
self._load_auto_discovered_configs()
|
|
564
|
+
|
|
565
|
+
# Filter and deduplicate tools
|
|
566
|
+
self._filter_and_deduplicate_tools(
|
|
567
|
+
exclude_tools_set,
|
|
568
|
+
include_tools_set,
|
|
569
|
+
include_tool_types_set,
|
|
570
|
+
exclude_tool_types_set,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Process MCP Auto Loader tools
|
|
574
|
+
self.logger.debug("Checking for MCP Auto Loader tools...")
|
|
575
|
+
self._process_mcp_auto_loaders()
|
|
576
|
+
|
|
577
|
+
def _load_tool_names_from_file(self, file_path):
|
|
578
|
+
"""
|
|
579
|
+
Load tool names from a text file (one tool name per line).
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
file_path (str): Path to the text file containing tool names
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
list: List of tool names loaded from the file
|
|
586
|
+
"""
|
|
587
|
+
try:
|
|
588
|
+
if not os.path.exists(file_path):
|
|
589
|
+
self.logger.error(f"Tools file not found: {file_path}")
|
|
590
|
+
return []
|
|
591
|
+
|
|
592
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
593
|
+
tool_names = []
|
|
594
|
+
for _line_num, line in enumerate(f, 1):
|
|
595
|
+
line = line.strip()
|
|
596
|
+
if line and not line.startswith(
|
|
597
|
+
"#"
|
|
598
|
+
): # Skip empty lines and comments
|
|
599
|
+
tool_names.append(line)
|
|
600
|
+
|
|
601
|
+
self.logger.info(
|
|
602
|
+
f"Loaded {len(tool_names)} tool names from file: {file_path}"
|
|
603
|
+
)
|
|
604
|
+
return tool_names
|
|
605
|
+
|
|
606
|
+
except Exception as e:
|
|
607
|
+
self.logger.error(f"Error loading tool names from file {file_path}: {e}")
|
|
608
|
+
return []
|
|
609
|
+
|
|
610
|
+
def _filter_and_deduplicate_tools(
|
|
611
|
+
self,
|
|
612
|
+
exclude_tools_set,
|
|
613
|
+
include_tools_set,
|
|
614
|
+
include_tool_types_set=None,
|
|
615
|
+
exclude_tool_types_set=None,
|
|
616
|
+
):
|
|
617
|
+
"""
|
|
618
|
+
Filter tools based on inclusion/exclusion criteria and remove duplicates.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
exclude_tools_set (set): Set of tool names to exclude
|
|
622
|
+
include_tools_set (set or None): Set of tool names to include (if None, include all)
|
|
623
|
+
include_tool_types_set (set or None): Set of tool types to include (if None, include all)
|
|
624
|
+
exclude_tool_types_set (set or None): Set of tool types to exclude (if None, exclude none)
|
|
625
|
+
"""
|
|
64
626
|
tool_name_list = []
|
|
65
627
|
dedup_all_tools = []
|
|
628
|
+
all_missing_keys = set()
|
|
629
|
+
duplicate_names = set()
|
|
630
|
+
excluded_tools_count = 0
|
|
631
|
+
included_tools_count = 0
|
|
632
|
+
missing_included_tools = set()
|
|
633
|
+
|
|
634
|
+
# If include_tools_set is specified, track which tools we found
|
|
635
|
+
if include_tools_set:
|
|
636
|
+
missing_included_tools = include_tools_set.copy()
|
|
637
|
+
|
|
66
638
|
for each in self.all_tools:
|
|
67
|
-
|
|
68
|
-
|
|
639
|
+
# Handle both dict and string entries
|
|
640
|
+
if isinstance(each, dict):
|
|
641
|
+
tool_name = each.get("name", "")
|
|
642
|
+
tool_type = each.get("type", "Unknown")
|
|
643
|
+
elif isinstance(each, str):
|
|
644
|
+
self.logger.warning(f"Found string in all_tools: {each}")
|
|
645
|
+
continue
|
|
646
|
+
else:
|
|
647
|
+
self.logger.warning(f"Unknown type in all_tools: {type(each)} - {each}")
|
|
648
|
+
continue
|
|
649
|
+
|
|
650
|
+
# Check tool type inclusion/exclusion
|
|
651
|
+
if include_tool_types_set and tool_type not in include_tool_types_set:
|
|
652
|
+
self.logger.debug(
|
|
653
|
+
f"Excluding tool '{tool_name}' - type '{tool_type}' not in include list"
|
|
654
|
+
)
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
if exclude_tool_types_set and tool_type in exclude_tool_types_set:
|
|
658
|
+
self.logger.debug(
|
|
659
|
+
f"Excluding tool '{tool_name}' - type '{tool_type}' is excluded"
|
|
660
|
+
)
|
|
661
|
+
continue
|
|
662
|
+
|
|
663
|
+
# If include_tools_set is specified, only include tools in that set
|
|
664
|
+
if include_tools_set:
|
|
665
|
+
if tool_name not in include_tools_set:
|
|
666
|
+
continue
|
|
667
|
+
else:
|
|
668
|
+
missing_included_tools.discard(tool_name)
|
|
669
|
+
included_tools_count += 1
|
|
670
|
+
|
|
671
|
+
# Skip excluded tools
|
|
672
|
+
if tool_name in exclude_tools_set:
|
|
673
|
+
excluded_tools_count += 1
|
|
674
|
+
self.logger.debug(f"Excluding tool by name: {tool_name}")
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
# Check API key requirements
|
|
678
|
+
if "required_api_keys" in each:
|
|
679
|
+
all_keys_available, missing_keys = self._check_api_key_requirements(
|
|
680
|
+
each
|
|
681
|
+
)
|
|
682
|
+
if not all_keys_available:
|
|
683
|
+
all_missing_keys.update(missing_keys)
|
|
684
|
+
self.logger.debug(
|
|
685
|
+
f"Skipping tool '{tool_name}' due to missing API keys: {', '.join(missing_keys)}"
|
|
686
|
+
)
|
|
687
|
+
continue
|
|
688
|
+
|
|
689
|
+
# Handle duplicates
|
|
690
|
+
if tool_name not in tool_name_list:
|
|
691
|
+
tool_name_list.append(tool_name)
|
|
69
692
|
dedup_all_tools.append(each)
|
|
693
|
+
else:
|
|
694
|
+
duplicate_names.add(tool_name)
|
|
695
|
+
|
|
696
|
+
# Report statistics
|
|
697
|
+
if duplicate_names:
|
|
698
|
+
self.logger.debug(
|
|
699
|
+
f"Duplicate tool names found and dropped: {', '.join(list(duplicate_names)[:5])}{'...' if len(duplicate_names) > 5 else ''}"
|
|
700
|
+
)
|
|
701
|
+
if excluded_tools_count > 0:
|
|
702
|
+
self.logger.info(f"Excluded {excluded_tools_count} tools by name")
|
|
703
|
+
if include_tools_set:
|
|
704
|
+
self.logger.info(f"Included {included_tools_count} tools by name filter")
|
|
705
|
+
if missing_included_tools:
|
|
706
|
+
self.logger.warning(
|
|
707
|
+
f"Could not find {len(missing_included_tools)} requested tools: {', '.join(list(missing_included_tools)[:5])}{'...' if len(missing_included_tools) > 5 else ''}"
|
|
708
|
+
)
|
|
709
|
+
|
|
70
710
|
self.all_tools = dedup_all_tools
|
|
71
711
|
self.refresh_tool_name_desc()
|
|
72
712
|
|
|
73
|
-
|
|
713
|
+
info(f"Number of tools after load tools: {len(self.all_tools)}")
|
|
714
|
+
|
|
715
|
+
# Generate template for missing API keys
|
|
716
|
+
if len(all_missing_keys) > 0:
|
|
717
|
+
warning(f"\nMissing API keys: {', '.join(all_missing_keys)}")
|
|
718
|
+
info("Generating .env.template file with missing API keys...")
|
|
719
|
+
self.generate_env_template(all_missing_keys)
|
|
720
|
+
|
|
721
|
+
def _load_auto_discovered_configs(self):
|
|
722
|
+
"""
|
|
723
|
+
Load auto-discovered configs from the decorator registry.
|
|
724
|
+
|
|
725
|
+
This method loads tool configurations that were registered automatically
|
|
726
|
+
via the @register_tool decorator with config parameter.
|
|
727
|
+
"""
|
|
728
|
+
from .tool_registry import get_config_registry
|
|
729
|
+
|
|
730
|
+
discovered_configs = get_config_registry()
|
|
731
|
+
|
|
732
|
+
if discovered_configs:
|
|
733
|
+
self.logger.debug(
|
|
734
|
+
f"Loading {len(discovered_configs)} auto-discovered tool configs"
|
|
735
|
+
)
|
|
736
|
+
for _tool_type, config in discovered_configs.items():
|
|
737
|
+
# Add to all_tools if not already present
|
|
738
|
+
if "name" in config and config["name"] not in [
|
|
739
|
+
tool.get("name") for tool in self.all_tools
|
|
740
|
+
]:
|
|
741
|
+
self.all_tools.append(config)
|
|
742
|
+
self.logger.debug(f"Added auto-discovered config: {config['name']}")
|
|
743
|
+
|
|
744
|
+
def _process_mcp_auto_loaders(self):
|
|
745
|
+
"""
|
|
746
|
+
Process any MCPAutoLoaderTool instances to automatically discover and register MCP tools.
|
|
747
|
+
|
|
748
|
+
This method scans through all loaded tools for MCPAutoLoaderTool instances and runs their
|
|
749
|
+
auto-discovery process to find and register MCP tools from configured servers. It handles
|
|
750
|
+
async operations properly with cleanup and error handling.
|
|
751
|
+
|
|
752
|
+
Side Effects:
|
|
753
|
+
- May add new tools to the tool registry
|
|
754
|
+
- Prints debug information about the discovery process
|
|
755
|
+
- Updates tool counts after MCP registration
|
|
756
|
+
"""
|
|
757
|
+
self.logger.debug("Starting _process_mcp_auto_loaders")
|
|
758
|
+
import asyncio
|
|
759
|
+
import warnings
|
|
760
|
+
|
|
761
|
+
auto_loaders = []
|
|
762
|
+
self.logger.debug(f"Checking {len(self.all_tools)} tools for MCPAutoLoaderTool")
|
|
763
|
+
for tool_config in self.all_tools:
|
|
764
|
+
if tool_config.get("type") == "MCPAutoLoaderTool":
|
|
765
|
+
auto_loaders.append(tool_config)
|
|
766
|
+
self.logger.debug(f"Found MCPAutoLoaderTool: {tool_config['name']}")
|
|
767
|
+
|
|
768
|
+
if not auto_loaders:
|
|
769
|
+
self.logger.debug("No MCP Auto Loader tools found")
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
info(f"Found {len(auto_loaders)} MCP Auto Loader tool(s), processing...")
|
|
773
|
+
|
|
774
|
+
# Check if we're already in an event loop
|
|
775
|
+
try:
|
|
776
|
+
asyncio.get_running_loop()
|
|
777
|
+
in_event_loop = True
|
|
778
|
+
self.logger.debug("Already in an event loop, using async approach")
|
|
779
|
+
except RuntimeError:
|
|
780
|
+
in_event_loop = False
|
|
781
|
+
self.logger.debug("No event loop running, will create new one")
|
|
782
|
+
|
|
783
|
+
# Process each auto loader
|
|
784
|
+
for loader_config in auto_loaders:
|
|
785
|
+
self.logger.debug(f"Processing loader: {loader_config['name']}")
|
|
786
|
+
try:
|
|
787
|
+
# Create auto loader instance
|
|
788
|
+
self.logger.debug("Creating auto loader instance...")
|
|
789
|
+
auto_loader = tool_type_mappings["MCPAutoLoaderTool"](loader_config)
|
|
790
|
+
self.logger.debug("Auto loader instance created")
|
|
791
|
+
|
|
792
|
+
# Run auto-load process with proper session cleanup
|
|
793
|
+
self.logger.debug("Starting auto-load process...")
|
|
794
|
+
|
|
795
|
+
async def _run_auto_load(loader):
|
|
796
|
+
"""Run auto-load with proper cleanup"""
|
|
797
|
+
try:
|
|
798
|
+
result = await loader.auto_load_and_register(self)
|
|
799
|
+
return result
|
|
800
|
+
finally:
|
|
801
|
+
# Ensure session cleanup
|
|
802
|
+
await loader._close_session()
|
|
803
|
+
|
|
804
|
+
if in_event_loop:
|
|
805
|
+
# We're already in an event loop, so we can't use run_until_complete
|
|
806
|
+
# Instead, we'll skip MCP auto-loading for now and warn the user
|
|
807
|
+
warning(
|
|
808
|
+
f"Warning: Cannot process MCP Auto Loader '{loader_config['name']}' because we're already in an event loop."
|
|
809
|
+
)
|
|
810
|
+
self.logger.debug(
|
|
811
|
+
"This is a known limitation when SMCP is used within an async context."
|
|
812
|
+
)
|
|
813
|
+
self.logger.debug(
|
|
814
|
+
"MCP tools will need to be loaded manually or the server should be run outside of an async context."
|
|
815
|
+
)
|
|
816
|
+
continue
|
|
817
|
+
else:
|
|
818
|
+
# No event loop, safe to create one
|
|
819
|
+
# Suppress ResourceWarnings during cleanup
|
|
820
|
+
with warnings.catch_warnings():
|
|
821
|
+
warnings.simplefilter("ignore", ResourceWarning)
|
|
822
|
+
|
|
823
|
+
loop = asyncio.new_event_loop()
|
|
824
|
+
asyncio.set_event_loop(loop)
|
|
825
|
+
try:
|
|
826
|
+
self.logger.debug("Running auto_load_and_register...")
|
|
827
|
+
result = loop.run_until_complete(
|
|
828
|
+
_run_auto_load(auto_loader)
|
|
829
|
+
)
|
|
830
|
+
self.logger.debug(
|
|
831
|
+
f"Auto-load completed with result: {result}"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
info(
|
|
835
|
+
f"MCP Auto Loader '{loader_config['name']}' processed:"
|
|
836
|
+
)
|
|
837
|
+
info(
|
|
838
|
+
f" - Discovered: {result.get('discovered_count', 0)} tools"
|
|
839
|
+
)
|
|
840
|
+
info(
|
|
841
|
+
f" - Registered: {result.get('registered_count', 0)} tools"
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# Print detailed tool information
|
|
845
|
+
if result.get("tools"):
|
|
846
|
+
print(
|
|
847
|
+
f" 📋 Discovered MCP tools: {', '.join(result['tools'])}"
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
if result.get("registered_tools"):
|
|
851
|
+
print(
|
|
852
|
+
f" 🔧 Registered tools in ToolUniverse: {', '.join(result['registered_tools'])}"
|
|
853
|
+
)
|
|
854
|
+
self.logger.debug(
|
|
855
|
+
f" - Tools: {', '.join(result['registered_tools'])}"
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# Show available tools in callable_functions
|
|
859
|
+
expert_tools = [
|
|
860
|
+
name
|
|
861
|
+
for name in self.callable_functions.keys()
|
|
862
|
+
if name.startswith("expert_")
|
|
863
|
+
]
|
|
864
|
+
if expert_tools:
|
|
865
|
+
print(
|
|
866
|
+
f" ✅ Expert tools now available: {', '.join(expert_tools)}"
|
|
867
|
+
)
|
|
868
|
+
else:
|
|
869
|
+
print(
|
|
870
|
+
" ⚠️ No expert tools found in callable_functions after registration"
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
finally:
|
|
874
|
+
self.logger.debug("Closing async loop...")
|
|
875
|
+
# Clean up any remaining tasks
|
|
876
|
+
try:
|
|
877
|
+
pending = asyncio.all_tasks(loop)
|
|
878
|
+
for task in pending:
|
|
879
|
+
task.cancel()
|
|
880
|
+
if pending:
|
|
881
|
+
loop.run_until_complete(
|
|
882
|
+
asyncio.gather(*pending, return_exceptions=True)
|
|
883
|
+
)
|
|
884
|
+
except Exception:
|
|
885
|
+
pass # Ignore cleanup errors
|
|
886
|
+
finally:
|
|
887
|
+
loop.close()
|
|
888
|
+
self.logger.debug("Async loop closed")
|
|
889
|
+
|
|
890
|
+
except Exception as e:
|
|
891
|
+
self.logger.debug(f"Exception in auto loader processing: {e}")
|
|
892
|
+
import traceback
|
|
893
|
+
|
|
894
|
+
traceback.print_exc()
|
|
895
|
+
self.logger.debug(
|
|
896
|
+
f"Failed to process MCP Auto Loader '{loader_config['name']}': {str(e)}"
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
# Update tool count after MCP registration
|
|
900
|
+
self.logger.debug(
|
|
901
|
+
f"Number of tools after MCP auto-loading: {len(self.all_tool_dict)}"
|
|
902
|
+
)
|
|
903
|
+
self.logger.debug("_process_mcp_auto_loaders completed")
|
|
904
|
+
|
|
905
|
+
def select_tools(
|
|
906
|
+
self,
|
|
907
|
+
include_names=None,
|
|
908
|
+
exclude_names=None,
|
|
909
|
+
include_categories=None,
|
|
910
|
+
exclude_categories=None,
|
|
911
|
+
):
|
|
912
|
+
"""
|
|
913
|
+
Select tools based on tool names and/or categories (tool_files keys).
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
include_names (list, optional): List of tool names to include. If None, include all.
|
|
917
|
+
exclude_names (list, optional): List of tool names to exclude.
|
|
918
|
+
include_categories (list, optional): List of categories (tool_files keys) to include.
|
|
919
|
+
If None, include all.
|
|
920
|
+
exclude_categories (list, optional): List of categories (tool_files keys) to exclude.
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
list: List of selected tool configurations.
|
|
924
|
+
"""
|
|
925
|
+
selected_tools = []
|
|
926
|
+
# If categories are specified, use self.tool_category_dicts to filter
|
|
927
|
+
categories = set(self.tool_category_dicts.keys())
|
|
928
|
+
if include_categories is not None:
|
|
929
|
+
categories &= set(include_categories)
|
|
930
|
+
if exclude_categories is not None:
|
|
931
|
+
categories -= set(exclude_categories)
|
|
932
|
+
# Gather tools from selected categories
|
|
933
|
+
for cat in categories:
|
|
934
|
+
selected_tools.extend(self.tool_category_dicts[cat])
|
|
935
|
+
# Further filter by names if needed
|
|
936
|
+
if include_names is not None:
|
|
937
|
+
selected_tools = [
|
|
938
|
+
tool for tool in selected_tools if tool["name"] in include_names
|
|
939
|
+
]
|
|
940
|
+
if exclude_names is not None:
|
|
941
|
+
selected_tools = [
|
|
942
|
+
tool for tool in selected_tools if tool["name"] not in exclude_names
|
|
943
|
+
]
|
|
944
|
+
return selected_tools
|
|
945
|
+
|
|
946
|
+
def filter_tool_lists(
|
|
947
|
+
self,
|
|
948
|
+
tool_name_list,
|
|
949
|
+
tool_desc_list,
|
|
950
|
+
include_names=None,
|
|
951
|
+
exclude_names=None,
|
|
952
|
+
include_categories=None,
|
|
953
|
+
exclude_categories=None,
|
|
954
|
+
):
|
|
955
|
+
"""
|
|
956
|
+
Directly filter tool name and description lists based on names and/or categories.
|
|
957
|
+
|
|
958
|
+
This method takes existing tool name and description lists and filters them according
|
|
959
|
+
to the specified criteria using the select_tools method for category-based filtering.
|
|
960
|
+
|
|
961
|
+
Args:
|
|
962
|
+
tool_name_list (list): List of tool names to filter.
|
|
963
|
+
tool_desc_list (list): List of tool descriptions to filter (must correspond to tool_name_list).
|
|
964
|
+
include_names (list, optional): List of tool names to include.
|
|
965
|
+
exclude_names (list, optional): List of tool names to exclude.
|
|
966
|
+
include_categories (list, optional): List of categories to include.
|
|
967
|
+
exclude_categories (list, optional): List of categories to exclude.
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
tuple: A tuple containing (filtered_tool_name_list, filtered_tool_desc_list).
|
|
971
|
+
"""
|
|
972
|
+
# Build a set of allowed tool names using select_tools for category filtering
|
|
973
|
+
allowed_names = set()
|
|
974
|
+
if any([include_names, exclude_names, include_categories, exclude_categories]):
|
|
975
|
+
filtered_tools = self.select_tools(
|
|
976
|
+
include_names=include_names,
|
|
977
|
+
exclude_names=exclude_names,
|
|
978
|
+
include_categories=include_categories,
|
|
979
|
+
exclude_categories=exclude_categories,
|
|
980
|
+
)
|
|
981
|
+
allowed_names = set(tool["name"] for tool in filtered_tools)
|
|
982
|
+
else:
|
|
983
|
+
allowed_names = set(tool_name_list)
|
|
984
|
+
|
|
985
|
+
# Filter lists by allowed_names
|
|
986
|
+
filtered_tool_name_list = []
|
|
987
|
+
filtered_tool_desc_list = []
|
|
988
|
+
for name, desc in zip(tool_name_list, tool_desc_list):
|
|
989
|
+
if name in allowed_names:
|
|
990
|
+
filtered_tool_name_list.append(name)
|
|
991
|
+
filtered_tool_desc_list.append(desc)
|
|
992
|
+
return filtered_tool_name_list, filtered_tool_desc_list
|
|
74
993
|
|
|
75
994
|
def return_all_loaded_tools(self):
|
|
995
|
+
"""
|
|
996
|
+
Return a deep copy of all loaded tools.
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
list: A deep copy of the all_tools list to prevent external modification.
|
|
1000
|
+
"""
|
|
76
1001
|
return copy.deepcopy(self.all_tools)
|
|
77
1002
|
|
|
78
|
-
def
|
|
1003
|
+
def list_built_in_tools(self, mode="config"):
|
|
1004
|
+
"""
|
|
1005
|
+
List all built-in tool categories and their statistics with two different modes.
|
|
1006
|
+
|
|
1007
|
+
This method provides a comprehensive overview of all available tools in the ToolUniverse,
|
|
1008
|
+
organized by categories. It reads directly from the default tool files to gather statistics,
|
|
1009
|
+
so it works even before calling load_tools().
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
mode (str, optional): Organization mode for tools. Defaults to 'config'.
|
|
1013
|
+
- 'config': Organize by config file categories (original behavior)
|
|
1014
|
+
- 'type': Organize by tool types (implementation classes)
|
|
1015
|
+
|
|
1016
|
+
Returns:
|
|
1017
|
+
dict: A dictionary containing tool statistics with the following structure:
|
|
1018
|
+
|
|
1019
|
+
{
|
|
1020
|
+
'categories': {
|
|
1021
|
+
'category_name': {
|
|
1022
|
+
'count': int, # Number of tools in this category
|
|
1023
|
+
'tools': list # List of tool names (only when mode='type')
|
|
1024
|
+
},
|
|
1025
|
+
...
|
|
1026
|
+
},
|
|
1027
|
+
'total_categories': int, # Total number of tool categories
|
|
1028
|
+
'total_tools': int, # Total number of unique tools
|
|
1029
|
+
'mode': str, # The mode used for organization
|
|
1030
|
+
'summary': str # Human-readable summary of statistics
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
Example:
|
|
1034
|
+
>>> tool_universe = ToolUniverse()
|
|
1035
|
+
>>> # Group by config file categories
|
|
1036
|
+
>>> stats = tool_universe.list_built_in_tools(mode='config')
|
|
1037
|
+
>>> # Group by tool types
|
|
1038
|
+
>>> stats = tool_universe.list_built_in_tools(mode='type')
|
|
1039
|
+
|
|
1040
|
+
Note:
|
|
1041
|
+
- This method reads directly from tool files and works without calling load_tools()
|
|
1042
|
+
- Tools are deduplicated across categories, so the same tool won't be counted multiple times
|
|
1043
|
+
- The summary is automatically printed to console when this method is called
|
|
1044
|
+
"""
|
|
1045
|
+
if mode not in ["config", "type"]:
|
|
1046
|
+
raise ValueError("Mode must be either 'config' or 'type'")
|
|
1047
|
+
|
|
1048
|
+
result = {
|
|
1049
|
+
"categories": {},
|
|
1050
|
+
"total_categories": 0,
|
|
1051
|
+
"total_tools": 0,
|
|
1052
|
+
"mode": mode,
|
|
1053
|
+
"summary": "",
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
all_tool_names = set() # For deduplication across categories
|
|
1057
|
+
all_tools = [] # Store all tools for type-based grouping
|
|
1058
|
+
|
|
1059
|
+
# Read tools from each category file
|
|
1060
|
+
for category, file_path in self.tool_files.items():
|
|
1061
|
+
try:
|
|
1062
|
+
# Read the JSON file for this category
|
|
1063
|
+
tools_in_category = read_json_list(file_path)
|
|
1064
|
+
all_tools.extend(tools_in_category)
|
|
1065
|
+
|
|
1066
|
+
if mode == "config":
|
|
1067
|
+
tool_names = [tool["name"] for tool in tools_in_category]
|
|
1068
|
+
result["categories"][category] = {"count": len(tool_names)}
|
|
1069
|
+
|
|
1070
|
+
# Add to global set for deduplication
|
|
1071
|
+
all_tool_names.update([tool["name"] for tool in tools_in_category])
|
|
1072
|
+
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
warning(
|
|
1075
|
+
f"Warning: Could not read tools from {category} ({file_path}): {e}"
|
|
1076
|
+
)
|
|
1077
|
+
if mode == "config":
|
|
1078
|
+
result["categories"][category] = {"count": 0}
|
|
1079
|
+
|
|
1080
|
+
# Also include remote tools for listing purposes (not auto-loaded elsewhere)
|
|
1081
|
+
try:
|
|
1082
|
+
remote_dir = os.path.join(current_dir, "data", "remote_tools")
|
|
1083
|
+
if os.path.isdir(remote_dir):
|
|
1084
|
+
remote_tools = []
|
|
1085
|
+
for fname in os.listdir(remote_dir):
|
|
1086
|
+
if not fname.lower().endswith(".json"):
|
|
1087
|
+
continue
|
|
1088
|
+
fpath = os.path.join(remote_dir, fname)
|
|
1089
|
+
try:
|
|
1090
|
+
tools_in_file = read_json_list(fpath)
|
|
1091
|
+
if isinstance(tools_in_file, dict):
|
|
1092
|
+
tools_in_file = list(tools_in_file.values())
|
|
1093
|
+
if isinstance(tools_in_file, list):
|
|
1094
|
+
remote_tools.extend(tools_in_file)
|
|
1095
|
+
except Exception as e:
|
|
1096
|
+
warning(
|
|
1097
|
+
f"Warning: Could not read remote tools from {fpath}: {e}"
|
|
1098
|
+
)
|
|
1099
|
+
if remote_tools:
|
|
1100
|
+
all_tools.extend(remote_tools)
|
|
1101
|
+
all_tool_names.update([tool["name"] for tool in remote_tools])
|
|
1102
|
+
if mode == "config":
|
|
1103
|
+
result["categories"]["remote_tools"] = {
|
|
1104
|
+
"count": len(remote_tools)
|
|
1105
|
+
}
|
|
1106
|
+
except Exception as e:
|
|
1107
|
+
warning(f"Warning: Failed to scan remote tools directory: {e}")
|
|
1108
|
+
|
|
1109
|
+
# If mode is 'type', organize by tool types instead
|
|
1110
|
+
if mode == "type":
|
|
1111
|
+
# Deduplicate tools by name first
|
|
1112
|
+
unique_tools = {}
|
|
1113
|
+
for tool in all_tools:
|
|
1114
|
+
if tool["name"] not in unique_tools:
|
|
1115
|
+
unique_tools[tool["name"]] = tool
|
|
1116
|
+
|
|
1117
|
+
# Group by tool type
|
|
1118
|
+
type_groups = {}
|
|
1119
|
+
for tool in unique_tools.values():
|
|
1120
|
+
tool_type = tool.get("type", "Unknown")
|
|
1121
|
+
if tool_type not in type_groups:
|
|
1122
|
+
type_groups[tool_type] = []
|
|
1123
|
+
type_groups[tool_type].append(tool["name"])
|
|
1124
|
+
|
|
1125
|
+
# Build result for type mode
|
|
1126
|
+
for tool_type, tool_names in type_groups.items():
|
|
1127
|
+
result["categories"][tool_type] = {
|
|
1128
|
+
"count": len(tool_names),
|
|
1129
|
+
"tools": sorted(tool_names),
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
# Calculate totals
|
|
1133
|
+
result["total_categories"] = len(result["categories"])
|
|
1134
|
+
result["total_tools"] = len(all_tool_names)
|
|
1135
|
+
|
|
1136
|
+
# Generate summary information
|
|
1137
|
+
mode_title = "Config File Categories" if mode == "config" else "Tool Types"
|
|
1138
|
+
summary_lines = [
|
|
1139
|
+
"=" * 60,
|
|
1140
|
+
f"🔧 ToolUniverse Built-in Tools Overview ({mode_title})",
|
|
1141
|
+
"=" * 60,
|
|
1142
|
+
f"📊 Total Categories: {result['total_categories']}",
|
|
1143
|
+
f"🛠️ Total Unique Tools: {result['total_tools']}",
|
|
1144
|
+
f"📋 Organization Mode: {mode}",
|
|
1145
|
+
"",
|
|
1146
|
+
f"📂 {mode_title} Breakdown:",
|
|
1147
|
+
"-" * 40,
|
|
1148
|
+
]
|
|
1149
|
+
|
|
1150
|
+
# Sort categories by tool count (descending) for better visualization
|
|
1151
|
+
sorted_categories = sorted(
|
|
1152
|
+
result["categories"].items(), key=lambda x: x[1]["count"], reverse=True
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
for category, category_info in sorted_categories:
|
|
1156
|
+
count = category_info["count"]
|
|
1157
|
+
# Add visual indicators for different tool counts
|
|
1158
|
+
if count >= 10:
|
|
1159
|
+
icon = "🟢"
|
|
1160
|
+
elif count >= 5:
|
|
1161
|
+
icon = "🟡"
|
|
1162
|
+
elif count >= 1:
|
|
1163
|
+
icon = "🟠"
|
|
1164
|
+
else:
|
|
1165
|
+
icon = "🔴"
|
|
1166
|
+
|
|
1167
|
+
# Format category name to be more readable
|
|
1168
|
+
if mode == "config":
|
|
1169
|
+
display_name = category.replace("_", " ").title()
|
|
1170
|
+
else:
|
|
1171
|
+
display_name = category
|
|
1172
|
+
|
|
1173
|
+
summary_lines.append(f" {icon} {display_name:<35} {count:>3} tools")
|
|
1174
|
+
|
|
1175
|
+
# For type mode, optionally show some tool examples
|
|
1176
|
+
if (
|
|
1177
|
+
mode == "type"
|
|
1178
|
+
and "tools" in category_info
|
|
1179
|
+
and len(category_info["tools"]) <= 5
|
|
1180
|
+
):
|
|
1181
|
+
for tool_name in category_info["tools"]:
|
|
1182
|
+
summary_lines.append(f" └─ {tool_name}")
|
|
1183
|
+
elif (
|
|
1184
|
+
mode == "type"
|
|
1185
|
+
and "tools" in category_info
|
|
1186
|
+
and len(category_info["tools"]) > 5
|
|
1187
|
+
):
|
|
1188
|
+
for tool_name in category_info["tools"][:3]:
|
|
1189
|
+
summary_lines.append(f" └─ {tool_name}")
|
|
1190
|
+
summary_lines.append(
|
|
1191
|
+
f" └─ ... and {len(category_info['tools']) - 3} more"
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
summary_lines.extend(
|
|
1195
|
+
["-" * 40, "✅ Ready to use! Call load_tools() to initialize.", "=" * 60]
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
result["summary"] = "\n".join(summary_lines)
|
|
1199
|
+
|
|
1200
|
+
# Print summary to console directly
|
|
1201
|
+
print(result["summary"])
|
|
1202
|
+
|
|
1203
|
+
return result
|
|
1204
|
+
|
|
1205
|
+
def refresh_tool_name_desc(
|
|
1206
|
+
self,
|
|
1207
|
+
enable_full_desc=False,
|
|
1208
|
+
include_names=None,
|
|
1209
|
+
exclude_names=None,
|
|
1210
|
+
include_categories=None,
|
|
1211
|
+
exclude_categories=None,
|
|
1212
|
+
):
|
|
1213
|
+
"""
|
|
1214
|
+
Refresh the tool name and description mappings with optional filtering.
|
|
1215
|
+
|
|
1216
|
+
This method rebuilds the internal tool dictionary and generates filtered lists of tool names
|
|
1217
|
+
and descriptions based on the provided filter criteria.
|
|
1218
|
+
|
|
1219
|
+
Args:
|
|
1220
|
+
enable_full_desc (bool, optional): If True, includes full tool JSON as description.
|
|
1221
|
+
If False, uses "name: description" format. Defaults to False.
|
|
1222
|
+
include_names (list, optional): List of tool names to include.
|
|
1223
|
+
exclude_names (list, optional): List of tool names to exclude.
|
|
1224
|
+
include_categories (list, optional): List of categories to include.
|
|
1225
|
+
exclude_categories (list, optional): List of categories to exclude.
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
tuple: A tuple containing (tool_name_list, tool_desc_list) after filtering.
|
|
1229
|
+
"""
|
|
79
1230
|
tool_name_list = []
|
|
80
1231
|
tool_desc_list = []
|
|
81
1232
|
for tool in self.all_tools:
|
|
82
|
-
tool_name_list.append(tool[
|
|
1233
|
+
tool_name_list.append(tool["name"])
|
|
83
1234
|
if enable_full_desc:
|
|
84
1235
|
tool_desc_list.append(json.dumps(tool))
|
|
85
1236
|
else:
|
|
86
|
-
tool_desc_list.append(tool[
|
|
87
|
-
self.all_tool_dict[tool[
|
|
1237
|
+
tool_desc_list.append(tool["name"] + ": " + tool["description"])
|
|
1238
|
+
self.all_tool_dict[tool["name"]] = tool
|
|
1239
|
+
|
|
1240
|
+
# Apply filtering if any filter argument is provided
|
|
1241
|
+
if any([include_names, exclude_names, include_categories, exclude_categories]):
|
|
1242
|
+
tool_name_list, tool_desc_list = self.filter_tool_lists(
|
|
1243
|
+
tool_name_list,
|
|
1244
|
+
tool_desc_list,
|
|
1245
|
+
include_names=include_names,
|
|
1246
|
+
exclude_names=exclude_names,
|
|
1247
|
+
include_categories=include_categories,
|
|
1248
|
+
exclude_categories=exclude_categories,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
self.logger.debug(
|
|
1252
|
+
f"Number of tools after refresh and filter: {len(tool_name_list)}"
|
|
1253
|
+
)
|
|
1254
|
+
|
|
88
1255
|
return tool_name_list, tool_desc_list
|
|
89
1256
|
|
|
90
1257
|
def prepare_one_tool_prompt(self, tool):
|
|
91
|
-
|
|
1258
|
+
"""
|
|
1259
|
+
Prepare a single tool configuration for prompt usage by filtering to essential keys.
|
|
1260
|
+
|
|
1261
|
+
Args:
|
|
1262
|
+
tool (dict): Tool configuration dictionary.
|
|
1263
|
+
|
|
1264
|
+
Returns:
|
|
1265
|
+
dict: Tool configuration with only essential keys for prompting.
|
|
1266
|
+
"""
|
|
1267
|
+
valid_keys = ["name", "description", "parameter", "required"]
|
|
92
1268
|
tool = copy.deepcopy(tool)
|
|
93
1269
|
for key in list(tool.keys()):
|
|
94
1270
|
if key not in valid_keys:
|
|
95
1271
|
del tool[key]
|
|
96
1272
|
return tool
|
|
97
|
-
|
|
1273
|
+
|
|
98
1274
|
def prepare_tool_prompts(self, tool_list):
|
|
1275
|
+
"""
|
|
1276
|
+
Prepare a list of tool configurations for prompt usage.
|
|
1277
|
+
|
|
1278
|
+
Args:
|
|
1279
|
+
tool_list (list): List of tool configuration dictionaries.
|
|
1280
|
+
|
|
1281
|
+
Returns:
|
|
1282
|
+
list: List of tool configurations with only essential keys for prompting.
|
|
1283
|
+
"""
|
|
99
1284
|
copied_list = []
|
|
100
1285
|
for tool in tool_list:
|
|
101
1286
|
copied_list.append(self.prepare_one_tool_prompt(tool))
|
|
102
1287
|
return copied_list
|
|
103
1288
|
|
|
104
1289
|
def remove_keys(self, tool_list, invalid_keys):
|
|
1290
|
+
"""
|
|
1291
|
+
Remove specified keys from a list of tool configurations.
|
|
1292
|
+
|
|
1293
|
+
Args:
|
|
1294
|
+
tool_list (list): List of tool configuration dictionaries.
|
|
1295
|
+
invalid_keys (list): List of keys to remove from each tool configuration.
|
|
1296
|
+
|
|
1297
|
+
Returns:
|
|
1298
|
+
list: Deep copy of tool list with specified keys removed.
|
|
1299
|
+
"""
|
|
105
1300
|
copied_list = copy.deepcopy(tool_list)
|
|
106
1301
|
for tool in copied_list:
|
|
107
1302
|
# Create a list of keys to avoid modifying the dictionary during iteration
|
|
@@ -111,8 +1306,28 @@ class ToolUniverse:
|
|
|
111
1306
|
return copied_list
|
|
112
1307
|
|
|
113
1308
|
def prepare_tool_examples(self, tool_list):
|
|
114
|
-
|
|
115
|
-
|
|
1309
|
+
"""
|
|
1310
|
+
Prepare tool configurations for example usage by keeping extended set of keys.
|
|
1311
|
+
|
|
1312
|
+
This method is similar to prepare_tool_prompts but includes additional keys
|
|
1313
|
+
useful for examples and documentation.
|
|
1314
|
+
|
|
1315
|
+
Args:
|
|
1316
|
+
tool_list (list): List of tool configuration dictionaries.
|
|
1317
|
+
|
|
1318
|
+
Returns:
|
|
1319
|
+
list: Deep copy of tool list with only example-relevant keys.
|
|
1320
|
+
"""
|
|
1321
|
+
valid_keys = [
|
|
1322
|
+
"name",
|
|
1323
|
+
"description",
|
|
1324
|
+
"parameter",
|
|
1325
|
+
"required",
|
|
1326
|
+
"query_schema",
|
|
1327
|
+
"fields",
|
|
1328
|
+
"label",
|
|
1329
|
+
"type",
|
|
1330
|
+
]
|
|
116
1331
|
copied_list = copy.deepcopy(tool_list)
|
|
117
1332
|
for tool in copied_list:
|
|
118
1333
|
# Create a list of keys to avoid modifying the dictionary during iteration
|
|
@@ -121,43 +1336,221 @@ class ToolUniverse:
|
|
|
121
1336
|
del tool[key]
|
|
122
1337
|
return copied_list
|
|
123
1338
|
|
|
124
|
-
def
|
|
1339
|
+
def get_tool_specification_by_names(self, tool_names, format="default"):
|
|
1340
|
+
"""
|
|
1341
|
+
Retrieve tool specifications by their names using tool_specification method.
|
|
1342
|
+
|
|
1343
|
+
Args:
|
|
1344
|
+
tool_names (list): List of tool names to retrieve.
|
|
1345
|
+
format (str, optional): Output format. Options: 'default', 'openai'.
|
|
1346
|
+
If 'openai', returns OpenAI function calling format. Defaults to 'default'.
|
|
1347
|
+
|
|
1348
|
+
Returns:
|
|
1349
|
+
list: List of tool specifications for the specified names.
|
|
1350
|
+
Tools not found will be reported but not included in the result.
|
|
1351
|
+
"""
|
|
125
1352
|
picked_tool_list = []
|
|
126
1353
|
for each_name in tool_names:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
print(f"Tool name {each_name} not found in the loaded tools.")
|
|
1354
|
+
tool_spec = self.tool_specification(each_name, format=format)
|
|
1355
|
+
if tool_spec:
|
|
1356
|
+
picked_tool_list.append(tool_spec)
|
|
131
1357
|
return picked_tool_list
|
|
132
1358
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1359
|
+
def get_tool_by_name(self, tool_names, format="default"):
|
|
1360
|
+
"""
|
|
1361
|
+
Retrieve tool configurations by their names.
|
|
1362
|
+
|
|
1363
|
+
Args:
|
|
1364
|
+
tool_names (list): List of tool names to retrieve.
|
|
1365
|
+
format (str, optional): Output format. Options: 'default', 'openai'.
|
|
1366
|
+
If 'openai', returns OpenAI function calling format. Defaults to 'default'.
|
|
1367
|
+
|
|
1368
|
+
Returns:
|
|
1369
|
+
list: List of tool configurations for the specified names.
|
|
1370
|
+
Tools not found will be reported but not included in the result.
|
|
1371
|
+
"""
|
|
1372
|
+
return self.get_tool_specification_by_names(tool_names, format=format)
|
|
1373
|
+
|
|
1374
|
+
def get_one_tool_by_one_name(self, tool_name, return_prompt=True):
|
|
1375
|
+
"""
|
|
1376
|
+
Retrieve a single tool specification by name, optionally prepared for prompting.
|
|
1377
|
+
|
|
1378
|
+
This is a convenience method that calls get_one_tool_by_one_name.
|
|
1379
|
+
|
|
1380
|
+
Args:
|
|
1381
|
+
tool_name (str): Name of the tool to retrieve.
|
|
1382
|
+
return_prompt (bool, optional): If True, returns tool prepared for prompting.
|
|
1383
|
+
If False, returns full tool configuration. Defaults to True.
|
|
1384
|
+
|
|
1385
|
+
Returns:
|
|
1386
|
+
dict or None: Tool configuration if found, None otherwise.
|
|
1387
|
+
"""
|
|
1388
|
+
warning(
|
|
1389
|
+
"The 'get_one_tool_by_one_name' method is deprecated and will be removed in a future version. "
|
|
1390
|
+
"Please use 'tool_specification' instead."
|
|
1391
|
+
)
|
|
1392
|
+
return self.tool_specification(tool_name, return_prompt=return_prompt)
|
|
1393
|
+
|
|
1394
|
+
def tool_specification(self, tool_name, return_prompt=False, format="default"):
|
|
1395
|
+
"""
|
|
1396
|
+
Retrieve a single tool configuration by name.
|
|
1397
|
+
|
|
1398
|
+
Args:
|
|
1399
|
+
tool_name (str): Name of the tool to retrieve.
|
|
1400
|
+
return_prompt (bool, optional): If True, returns tool prepared for prompting.
|
|
1401
|
+
If False, returns full tool configuration. Defaults to False.
|
|
1402
|
+
format (str, optional): Output format. Options: 'default', 'openai'.
|
|
1403
|
+
If 'openai', returns OpenAI function calling format. Defaults to 'default'.
|
|
1404
|
+
|
|
1405
|
+
Returns:
|
|
1406
|
+
dict or None: Tool configuration if found, None otherwise.
|
|
1407
|
+
"""
|
|
1408
|
+
if tool_name in self.all_tool_dict:
|
|
1409
|
+
tool_config = self.all_tool_dict[tool_name]
|
|
1410
|
+
|
|
1411
|
+
if format == "openai":
|
|
1412
|
+
parameters = tool_config.get("parameter", {})
|
|
1413
|
+
if isinstance(parameters, dict):
|
|
1414
|
+
# 修复 required 字段格式
|
|
1415
|
+
if "properties" in parameters:
|
|
1416
|
+
for _prop_name, prop_config in parameters["properties"].items():
|
|
1417
|
+
if (
|
|
1418
|
+
isinstance(prop_config, dict)
|
|
1419
|
+
and "required" in prop_config
|
|
1420
|
+
):
|
|
1421
|
+
del prop_config["required"]
|
|
1422
|
+
|
|
1423
|
+
if "required" in parameters and not isinstance(
|
|
1424
|
+
parameters["required"], list
|
|
1425
|
+
):
|
|
1426
|
+
if parameters["required"] is True:
|
|
1427
|
+
required_list = []
|
|
1428
|
+
if "properties" in parameters:
|
|
1429
|
+
for prop_name, prop_config in parameters[
|
|
1430
|
+
"properties"
|
|
1431
|
+
].items():
|
|
1432
|
+
if (
|
|
1433
|
+
isinstance(prop_config, dict)
|
|
1434
|
+
and prop_config.get("required") is True
|
|
1435
|
+
):
|
|
1436
|
+
required_list.append(prop_name)
|
|
1437
|
+
parameters["required"] = required_list
|
|
1438
|
+
else:
|
|
1439
|
+
parameters["required"] = []
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
"name": tool_config["name"],
|
|
1443
|
+
"description": tool_config["description"],
|
|
1444
|
+
"parameters": parameters,
|
|
1445
|
+
}
|
|
1446
|
+
elif return_prompt:
|
|
1447
|
+
return self.prepare_one_tool_prompt(tool_config)
|
|
138
1448
|
else:
|
|
139
|
-
|
|
140
|
-
|
|
1449
|
+
return tool_config
|
|
1450
|
+
else:
|
|
1451
|
+
warning(f"Tool name {tool_name} not found in the loaded tools.")
|
|
1452
|
+
return None
|
|
1453
|
+
|
|
1454
|
+
def get_tool_description(self, tool_name):
|
|
1455
|
+
"""
|
|
1456
|
+
Get the description of a tool by its name.
|
|
1457
|
+
|
|
1458
|
+
This is a convenience method that calls get_one_tool_by_one_name.
|
|
1459
|
+
|
|
1460
|
+
Args:
|
|
1461
|
+
tool_name (str): Name of the tool.
|
|
1462
|
+
|
|
1463
|
+
Returns:
|
|
1464
|
+
dict or None: Tool configuration if found, None otherwise.
|
|
1465
|
+
"""
|
|
1466
|
+
return self.get_one_tool_by_one_name(tool_name)
|
|
141
1467
|
|
|
142
1468
|
def get_tool_type_by_name(self, tool_name):
|
|
143
|
-
|
|
1469
|
+
"""
|
|
1470
|
+
Get the type of a tool by its name.
|
|
1471
|
+
|
|
1472
|
+
Args:
|
|
1473
|
+
tool_name (str): Name of the tool.
|
|
1474
|
+
|
|
1475
|
+
Returns:
|
|
1476
|
+
str: The type of the tool.
|
|
1477
|
+
|
|
1478
|
+
Raises:
|
|
1479
|
+
KeyError: If the tool name is not found in loaded tools.
|
|
1480
|
+
"""
|
|
1481
|
+
return self.all_tool_dict[tool_name]["type"]
|
|
144
1482
|
|
|
145
1483
|
def tool_to_str(self, tool_list):
|
|
146
|
-
|
|
1484
|
+
"""
|
|
1485
|
+
Convert a list of tool configurations to a formatted string.
|
|
1486
|
+
|
|
1487
|
+
Args:
|
|
1488
|
+
tool_list (list): List of tool configuration dictionaries.
|
|
1489
|
+
|
|
1490
|
+
Returns:
|
|
1491
|
+
str: JSON-formatted string representation of the tools, with each tool
|
|
1492
|
+
separated by double newlines.
|
|
1493
|
+
"""
|
|
1494
|
+
return "\n\n".join(json.dumps(obj, indent=4) for obj in tool_list)
|
|
147
1495
|
|
|
148
|
-
def extract_function_call_json(
|
|
149
|
-
|
|
1496
|
+
def extract_function_call_json(
|
|
1497
|
+
self, lst, return_message=False, verbose=True, format="llama"
|
|
1498
|
+
):
|
|
1499
|
+
"""
|
|
1500
|
+
Extract function call JSON from input data.
|
|
1501
|
+
|
|
1502
|
+
This method delegates to the utility function extract_function_call_json.
|
|
1503
|
+
|
|
1504
|
+
Args:
|
|
1505
|
+
lst: Input data containing function call information.
|
|
1506
|
+
return_message (bool, optional): Whether to return message along with JSON. Defaults to False.
|
|
1507
|
+
verbose (bool, optional): Whether to enable verbose output. Defaults to True.
|
|
1508
|
+
format (str, optional): Format type for extraction. Defaults to 'llama'.
|
|
1509
|
+
|
|
1510
|
+
Returns:
|
|
1511
|
+
dict or tuple: Function call JSON, optionally with message if return_message is True.
|
|
1512
|
+
"""
|
|
1513
|
+
return extract_function_call_json(
|
|
1514
|
+
lst, return_message=return_message, verbose=verbose, format=format
|
|
1515
|
+
)
|
|
150
1516
|
|
|
151
1517
|
def call_id_gen(self):
|
|
1518
|
+
"""
|
|
1519
|
+
Generate a random call ID for function calls.
|
|
1520
|
+
|
|
1521
|
+
Returns:
|
|
1522
|
+
str: A random 9-character string composed of letters and digits.
|
|
1523
|
+
"""
|
|
152
1524
|
return "".join(random.choices(string.ascii_letters + string.digits, k=9))
|
|
153
1525
|
|
|
154
|
-
def run(self, fcall_str, return_message=False, verbose=True):
|
|
1526
|
+
def run(self, fcall_str, return_message=False, verbose=True, format="llama"):
|
|
1527
|
+
"""
|
|
1528
|
+
Execute function calls from input string or data.
|
|
1529
|
+
|
|
1530
|
+
This method parses function call data, validates it, and executes the corresponding tools.
|
|
1531
|
+
It supports both single function calls and multiple function calls in a list.
|
|
1532
|
+
|
|
1533
|
+
Args:
|
|
1534
|
+
fcall_str: Input string or data containing function call information.
|
|
1535
|
+
return_message (bool, optional): Whether to return formatted messages. Defaults to False.
|
|
1536
|
+
verbose (bool, optional): Whether to enable verbose output. Defaults to True.
|
|
1537
|
+
format (str, optional): Format type for parsing. Defaults to 'llama'.
|
|
1538
|
+
|
|
1539
|
+
Returns:
|
|
1540
|
+
list or str or None:
|
|
1541
|
+
- For multiple function calls: List of formatted messages with tool responses
|
|
1542
|
+
- For single function call: Direct result from the tool
|
|
1543
|
+
- None: If the input is not a valid function call
|
|
1544
|
+
"""
|
|
155
1545
|
if return_message:
|
|
156
1546
|
function_call_json, message = self.extract_function_call_json(
|
|
157
|
-
fcall_str, return_message=return_message, verbose=verbose
|
|
1547
|
+
fcall_str, return_message=return_message, verbose=verbose, format=format
|
|
1548
|
+
)
|
|
158
1549
|
else:
|
|
159
1550
|
function_call_json = self.extract_function_call_json(
|
|
160
|
-
fcall_str, return_message=return_message, verbose=verbose
|
|
1551
|
+
fcall_str, return_message=return_message, verbose=verbose, format=format
|
|
1552
|
+
)
|
|
1553
|
+
message = "" # Initialize message for cases where return_message=False
|
|
161
1554
|
if function_call_json is not None:
|
|
162
1555
|
if isinstance(function_call_json, list):
|
|
163
1556
|
# return the function call+result message with call id.
|
|
@@ -166,58 +1559,345 @@ class ToolUniverse:
|
|
|
166
1559
|
call_result = self.run_one_function(function_call_json[i])
|
|
167
1560
|
call_id = self.call_id_gen()
|
|
168
1561
|
function_call_json[i]["call_id"] = call_id
|
|
169
|
-
call_results.append(
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
1562
|
+
call_results.append(
|
|
1563
|
+
{
|
|
1564
|
+
"role": "tool",
|
|
1565
|
+
"content": json.dumps(
|
|
1566
|
+
{"content": call_result, "call_id": call_id}
|
|
1567
|
+
),
|
|
1568
|
+
}
|
|
1569
|
+
)
|
|
1570
|
+
revised_messages = [
|
|
1571
|
+
{
|
|
1572
|
+
"role": "assistant",
|
|
1573
|
+
"content": message,
|
|
1574
|
+
"tool_calls": json.dumps(function_call_json),
|
|
1575
|
+
}
|
|
1576
|
+
] + call_results
|
|
173
1577
|
return revised_messages
|
|
174
1578
|
else:
|
|
175
1579
|
return self.run_one_function(function_call_json)
|
|
176
1580
|
else:
|
|
177
|
-
|
|
1581
|
+
error("Not a function call")
|
|
178
1582
|
return None
|
|
179
1583
|
|
|
180
1584
|
def run_one_function(self, function_call_json):
|
|
1585
|
+
"""
|
|
1586
|
+
Execute a single function call.
|
|
1587
|
+
|
|
1588
|
+
This method validates the function call, initializes the tool if necessary,
|
|
1589
|
+
and executes it with the provided arguments. If hooks are enabled, it also
|
|
1590
|
+
applies output hooks to process the result.
|
|
1591
|
+
|
|
1592
|
+
Args:
|
|
1593
|
+
function_call_json (dict): Dictionary containing function name and arguments.
|
|
1594
|
+
|
|
1595
|
+
Returns:
|
|
1596
|
+
str or dict: Result from the tool execution, or error message if validation fails.
|
|
1597
|
+
"""
|
|
181
1598
|
check_status, check_message = self.check_function_call(function_call_json)
|
|
182
1599
|
if check_status is False:
|
|
183
|
-
return
|
|
1600
|
+
return (
|
|
1601
|
+
"Invalid function call: " + check_message
|
|
1602
|
+
) # + " You must correct your invalid function call!"
|
|
184
1603
|
function_name = function_call_json["name"]
|
|
185
1604
|
arguments = function_call_json["arguments"]
|
|
1605
|
+
|
|
1606
|
+
# Execute the tool
|
|
186
1607
|
if function_name in self.callable_functions:
|
|
187
|
-
|
|
1608
|
+
result = self.callable_functions[function_name].run(arguments)
|
|
188
1609
|
else:
|
|
189
1610
|
if function_name in self.all_tool_dict:
|
|
190
|
-
|
|
191
|
-
"
|
|
1611
|
+
self.logger.debug(
|
|
1612
|
+
"Initiating callable_function from loaded tool dicts."
|
|
1613
|
+
)
|
|
192
1614
|
tool = self.init_tool(
|
|
193
|
-
self.all_tool_dict[function_name], add_to_cache=True
|
|
194
|
-
|
|
1615
|
+
self.all_tool_dict[function_name], add_to_cache=True
|
|
1616
|
+
)
|
|
1617
|
+
result = tool.run(arguments)
|
|
1618
|
+
else:
|
|
1619
|
+
return f"Tool '{function_name}' not found"
|
|
1620
|
+
|
|
1621
|
+
# Apply output hooks if enabled
|
|
1622
|
+
if self.hook_manager:
|
|
1623
|
+
context = {
|
|
1624
|
+
"tool_name": function_name,
|
|
1625
|
+
"tool_type": (
|
|
1626
|
+
tool.__class__.__name__ if "tool" in locals() else "unknown"
|
|
1627
|
+
),
|
|
1628
|
+
"execution_time": time.time(),
|
|
1629
|
+
"arguments": arguments,
|
|
1630
|
+
}
|
|
1631
|
+
result = self.hook_manager.apply_hooks(
|
|
1632
|
+
result, function_name, arguments, context
|
|
1633
|
+
)
|
|
1634
|
+
|
|
1635
|
+
return result
|
|
1636
|
+
|
|
1637
|
+
def toggle_hooks(self, enabled: bool):
|
|
1638
|
+
"""
|
|
1639
|
+
Enable or disable output hooks globally.
|
|
1640
|
+
|
|
1641
|
+
This method allows runtime control of the hook system. When enabled,
|
|
1642
|
+
it initializes the HookManager if not already present. When disabled,
|
|
1643
|
+
it deactivates the HookManager.
|
|
1644
|
+
|
|
1645
|
+
Args:
|
|
1646
|
+
enabled (bool): True to enable hooks, False to disable
|
|
1647
|
+
"""
|
|
1648
|
+
self.hooks_enabled = enabled
|
|
1649
|
+
if enabled and not self.hook_manager:
|
|
1650
|
+
self.hook_manager = HookManager({}, self)
|
|
1651
|
+
self.logger.info("Output hooks enabled")
|
|
1652
|
+
elif self.hook_manager:
|
|
1653
|
+
self.hook_manager.toggle_hooks(enabled)
|
|
1654
|
+
self.logger.info(f"Output hooks {'enabled' if enabled else 'disabled'}")
|
|
1655
|
+
else:
|
|
1656
|
+
self.logger.debug("Output hooks disabled")
|
|
195
1657
|
|
|
196
1658
|
def init_tool(self, tool=None, tool_name=None, add_to_cache=True):
|
|
1659
|
+
"""
|
|
1660
|
+
Initialize a tool instance from configuration or name.
|
|
1661
|
+
|
|
1662
|
+
This method creates a new tool instance using the tool type mappings and
|
|
1663
|
+
optionally caches it for future use. It handles special cases like the
|
|
1664
|
+
OpentargetToolDrugNameMatch which requires additional dependencies.
|
|
1665
|
+
|
|
1666
|
+
Args:
|
|
1667
|
+
tool (dict, optional): Tool configuration dictionary. Either this or tool_name must be provided.
|
|
1668
|
+
tool_name (str, optional): Name of the tool type to initialize. Either this or tool must be provided.
|
|
1669
|
+
add_to_cache (bool, optional): Whether to cache the initialized tool. Defaults to True.
|
|
1670
|
+
|
|
1671
|
+
Returns:
|
|
1672
|
+
object: Initialized tool instance.
|
|
1673
|
+
|
|
1674
|
+
Raises:
|
|
1675
|
+
KeyError: If the tool type is not found in tool_type_mappings.
|
|
1676
|
+
"""
|
|
1677
|
+
global tool_type_mappings
|
|
1678
|
+
|
|
197
1679
|
if tool_name is not None:
|
|
198
|
-
|
|
1680
|
+
# Use lazy loading to get the tool class
|
|
1681
|
+
tool_class = get_tool_class_lazy(tool_name)
|
|
1682
|
+
if tool_class is None:
|
|
1683
|
+
raise KeyError(f"Tool type '{tool_name}' not found in registry")
|
|
1684
|
+
new_tool = tool_class()
|
|
199
1685
|
else:
|
|
200
|
-
tool_type = tool[
|
|
201
|
-
tool_name = tool[
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1686
|
+
tool_type = tool["type"]
|
|
1687
|
+
tool_name = tool["name"]
|
|
1688
|
+
|
|
1689
|
+
# Use lazy loading to get the tool class
|
|
1690
|
+
tool_class = get_tool_class_lazy(tool_type)
|
|
1691
|
+
if tool_class is None:
|
|
1692
|
+
# Fallback to old method if lazy loading fails
|
|
1693
|
+
if tool_type not in tool_type_mappings:
|
|
1694
|
+
# Refresh registry and try again
|
|
1695
|
+
tool_type_mappings = get_tool_registry()
|
|
1696
|
+
if tool_type not in tool_type_mappings:
|
|
1697
|
+
raise KeyError(f"Tool type '{tool_type}' not found in registry")
|
|
1698
|
+
tool_class = tool_type_mappings[tool_type]
|
|
1699
|
+
|
|
1700
|
+
if "OpentargetToolDrugNameMatch" == tool_type:
|
|
1701
|
+
if "FDADrugLabelGetDrugGenericNameTool" not in self.callable_functions:
|
|
1702
|
+
drug_tool_class = get_tool_class_lazy(
|
|
1703
|
+
"FDADrugLabelGetDrugGenericNameTool"
|
|
1704
|
+
)
|
|
1705
|
+
if drug_tool_class is None:
|
|
1706
|
+
drug_tool_class = tool_type_mappings[
|
|
1707
|
+
"FDADrugLabelGetDrugGenericNameTool"
|
|
1708
|
+
]
|
|
1709
|
+
self.callable_functions["FDADrugLabelGetDrugGenericNameTool"] = (
|
|
1710
|
+
drug_tool_class()
|
|
1711
|
+
)
|
|
1712
|
+
new_tool = tool_class(
|
|
1713
|
+
tool_config=tool,
|
|
1714
|
+
drug_generic_tool=self.callable_functions[
|
|
1715
|
+
"FDADrugLabelGetDrugGenericNameTool"
|
|
1716
|
+
],
|
|
1717
|
+
)
|
|
1718
|
+
elif "ToolFinderEmbedding" == tool_type:
|
|
1719
|
+
new_tool = tool_class(tool_config=tool, tooluniverse=self)
|
|
1720
|
+
elif "ComposeTool" == tool_type:
|
|
1721
|
+
new_tool = tool_class(tool_config=tool, tooluniverse=self)
|
|
1722
|
+
elif "ToolFinderLLM" == tool_type:
|
|
1723
|
+
new_tool = tool_class(tool_config=tool, tooluniverse=self)
|
|
1724
|
+
elif "ToolFinderKeyword" == tool_type:
|
|
1725
|
+
new_tool = tool_class(tool_config=tool, tooluniverse=self)
|
|
206
1726
|
else:
|
|
207
|
-
new_tool =
|
|
1727
|
+
new_tool = tool_class(tool_config=tool)
|
|
208
1728
|
if add_to_cache:
|
|
209
1729
|
self.callable_functions[tool_name] = new_tool
|
|
210
1730
|
return new_tool
|
|
211
1731
|
|
|
212
|
-
def check_function_call(self, fcall_str, function_config=None):
|
|
213
|
-
|
|
214
|
-
|
|
1732
|
+
def check_function_call(self, fcall_str, function_config=None, format="llama"):
|
|
1733
|
+
"""
|
|
1734
|
+
Validate a function call against tool configuration.
|
|
1735
|
+
|
|
1736
|
+
This method checks if a function call is valid by verifying the function name
|
|
1737
|
+
exists and the arguments match the expected parameters.
|
|
1738
|
+
|
|
1739
|
+
Args:
|
|
1740
|
+
fcall_str: Function call string or data to validate.
|
|
1741
|
+
function_config (dict, optional): Specific function configuration to validate against.
|
|
1742
|
+
If None, uses the loaded tool configuration.
|
|
1743
|
+
format (str, optional): Format type for parsing. Defaults to 'llama'.
|
|
1744
|
+
|
|
1745
|
+
Returns:
|
|
1746
|
+
tuple: A tuple of (is_valid, message) where:
|
|
1747
|
+
- is_valid (bool): True if the function call is valid, False otherwise
|
|
1748
|
+
- message (str): Error message if invalid, empty if valid
|
|
1749
|
+
"""
|
|
1750
|
+
function_call_json = self.extract_function_call_json(fcall_str, format=format)
|
|
215
1751
|
if function_call_json is not None:
|
|
216
1752
|
if function_config is not None:
|
|
217
1753
|
return evaluate_function_call(function_config, function_call_json)
|
|
218
|
-
function_name = function_call_json[
|
|
219
|
-
if
|
|
220
|
-
return
|
|
221
|
-
|
|
1754
|
+
function_name = function_call_json["name"]
|
|
1755
|
+
if function_name not in self.all_tool_dict:
|
|
1756
|
+
return (
|
|
1757
|
+
False,
|
|
1758
|
+
f"Function name {function_name} not found in loaded tools.",
|
|
1759
|
+
)
|
|
1760
|
+
return evaluate_function_call(
|
|
1761
|
+
self.all_tool_dict[function_name], function_call_json
|
|
1762
|
+
)
|
|
1763
|
+
else:
|
|
1764
|
+
return False, "Invalid JSON string of function call"
|
|
1765
|
+
|
|
1766
|
+
def export_tool_names(self, output_file, category_filter=None):
|
|
1767
|
+
"""
|
|
1768
|
+
Export tool names to a text file (one per line).
|
|
1769
|
+
|
|
1770
|
+
Args:
|
|
1771
|
+
output_file (str): Path to the output file
|
|
1772
|
+
category_filter (list, optional): List of categories to filter by
|
|
1773
|
+
"""
|
|
1774
|
+
try:
|
|
1775
|
+
tools_to_export = []
|
|
1776
|
+
|
|
1777
|
+
if category_filter:
|
|
1778
|
+
# Filter by categories
|
|
1779
|
+
for category in category_filter:
|
|
1780
|
+
if category in self.tool_category_dicts:
|
|
1781
|
+
tools_to_export.extend(
|
|
1782
|
+
[
|
|
1783
|
+
tool["name"]
|
|
1784
|
+
for tool in self.tool_category_dicts[category]
|
|
1785
|
+
]
|
|
1786
|
+
)
|
|
1787
|
+
else:
|
|
1788
|
+
# Export all tools
|
|
1789
|
+
tools_to_export = [tool["name"] for tool in self.all_tools]
|
|
1790
|
+
|
|
1791
|
+
# Remove duplicates and sort
|
|
1792
|
+
tools_to_export = sorted(set(tools_to_export))
|
|
1793
|
+
|
|
1794
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
1795
|
+
f.write("# ToolUniverse Tool Names\n")
|
|
1796
|
+
f.write(f"# Generated automatically - {len(tools_to_export)} tools\n")
|
|
1797
|
+
if category_filter:
|
|
1798
|
+
f.write(f"# Categories: {', '.join(category_filter)}\n")
|
|
1799
|
+
f.write("\n")
|
|
1800
|
+
|
|
1801
|
+
for tool_name in tools_to_export:
|
|
1802
|
+
f.write(f"{tool_name}\n")
|
|
1803
|
+
|
|
1804
|
+
self.logger.info(
|
|
1805
|
+
f"Exported {len(tools_to_export)} tool names to {output_file}"
|
|
1806
|
+
)
|
|
1807
|
+
return tools_to_export
|
|
1808
|
+
|
|
1809
|
+
except Exception as e:
|
|
1810
|
+
self.logger.error(f"Error exporting tool names to {output_file}: {e}")
|
|
1811
|
+
return []
|
|
1812
|
+
|
|
1813
|
+
def get_available_tools(self, category_filter=None, name_only=True):
|
|
1814
|
+
"""
|
|
1815
|
+
Get available tools, optionally filtered by category.
|
|
1816
|
+
|
|
1817
|
+
Args:
|
|
1818
|
+
category_filter (list, optional): List of categories to filter by
|
|
1819
|
+
name_only (bool): If True, return only tool names; if False, return full configs
|
|
1820
|
+
|
|
1821
|
+
Returns:
|
|
1822
|
+
list: List of tool names or tool configurations
|
|
1823
|
+
"""
|
|
1824
|
+
if not hasattr(self, "all_tools") or not self.all_tools:
|
|
1825
|
+
self.logger.warning("No tools loaded. Call load_tools() first.")
|
|
1826
|
+
return []
|
|
1827
|
+
|
|
1828
|
+
if category_filter:
|
|
1829
|
+
filtered_tools = []
|
|
1830
|
+
for tool in self.all_tools:
|
|
1831
|
+
tool_type = tool.get("type", "")
|
|
1832
|
+
if tool_type in category_filter:
|
|
1833
|
+
filtered_tools.append(tool)
|
|
222
1834
|
else:
|
|
223
|
-
|
|
1835
|
+
filtered_tools = self.all_tools
|
|
1836
|
+
|
|
1837
|
+
if name_only:
|
|
1838
|
+
return [tool["name"] for tool in filtered_tools]
|
|
1839
|
+
else:
|
|
1840
|
+
return filtered_tools
|
|
1841
|
+
|
|
1842
|
+
def find_tools_by_pattern(self, pattern, search_in="name", case_sensitive=False):
|
|
1843
|
+
"""
|
|
1844
|
+
Find tools matching a pattern in their name or description.
|
|
1845
|
+
|
|
1846
|
+
Args:
|
|
1847
|
+
pattern (str): Pattern to search for
|
|
1848
|
+
search_in (str): Where to search - 'name', 'description', or 'both'
|
|
1849
|
+
case_sensitive (bool): Whether search should be case sensitive
|
|
1850
|
+
|
|
1851
|
+
Returns:
|
|
1852
|
+
list: List of matching tool configurations
|
|
1853
|
+
"""
|
|
1854
|
+
if not hasattr(self, "all_tools") or not self.all_tools:
|
|
1855
|
+
self.logger.warning("No tools loaded. Call load_tools() first.")
|
|
1856
|
+
return []
|
|
1857
|
+
|
|
1858
|
+
import re
|
|
1859
|
+
|
|
1860
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
1861
|
+
|
|
1862
|
+
matching_tools = []
|
|
1863
|
+
for tool in self.all_tools:
|
|
1864
|
+
found = False
|
|
1865
|
+
|
|
1866
|
+
if search_in in ["name", "both"]:
|
|
1867
|
+
tool_name = tool.get("name", "")
|
|
1868
|
+
if re.search(pattern, tool_name, flags):
|
|
1869
|
+
found = True
|
|
1870
|
+
|
|
1871
|
+
if search_in in ["description", "both"] and not found:
|
|
1872
|
+
tool_desc = tool.get("description", "")
|
|
1873
|
+
if re.search(pattern, tool_desc, flags):
|
|
1874
|
+
found = True
|
|
1875
|
+
|
|
1876
|
+
if found:
|
|
1877
|
+
matching_tools.append(tool)
|
|
1878
|
+
|
|
1879
|
+
self.logger.info(
|
|
1880
|
+
f"Found {len(matching_tools)} tools matching pattern '{pattern}'"
|
|
1881
|
+
)
|
|
1882
|
+
return matching_tools
|
|
1883
|
+
|
|
1884
|
+
def load_tools_from_names_list(self, tool_names, clear_existing=True):
|
|
1885
|
+
"""
|
|
1886
|
+
Load only specific tools by their names.
|
|
1887
|
+
|
|
1888
|
+
Args:
|
|
1889
|
+
tool_names (list): List of tool names to load
|
|
1890
|
+
clear_existing (bool): Whether to clear existing tools first
|
|
1891
|
+
|
|
1892
|
+
Returns:
|
|
1893
|
+
int: Number of tools successfully loaded
|
|
1894
|
+
"""
|
|
1895
|
+
if clear_existing:
|
|
1896
|
+
self.all_tools = []
|
|
1897
|
+
self.all_tool_dict = {}
|
|
1898
|
+
self.tool_category_dicts = {}
|
|
1899
|
+
|
|
1900
|
+
# Use the enhanced load_tools method
|
|
1901
|
+
original_count = len(self.all_tools)
|
|
1902
|
+
self.load_tools(include_tools=tool_names)
|
|
1903
|
+
return len(self.all_tools) - original_count
|