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
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM-based Tool Finder - A tool that uses LLM to find relevant tools based on descriptions.
|
|
3
|
+
|
|
4
|
+
This tool leverages AgenticTool's LLM functionality to create an intelligent tool finder
|
|
5
|
+
that puts only essential tool information (name and description) in the prompt to minimize
|
|
6
|
+
context window cost while letting the LLM decide which tools to return based on the query.
|
|
7
|
+
|
|
8
|
+
Key optimizations:
|
|
9
|
+
- Only sends tool name and description to LLM (no parameters, configs, etc.)
|
|
10
|
+
- Uses compact formatting to reduce token count
|
|
11
|
+
- Caches tool descriptions to avoid repeated processing
|
|
12
|
+
- Excludes irrelevant tools from prompt
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
from .base_tool import BaseTool
|
|
19
|
+
from .tool_registry import register_tool
|
|
20
|
+
from .agentic_tool import AgenticTool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@register_tool("ToolFinderLLM")
|
|
24
|
+
class ToolFinderLLM(BaseTool):
|
|
25
|
+
"""
|
|
26
|
+
LLM-based tool finder that uses natural language processing to select relevant tools.
|
|
27
|
+
|
|
28
|
+
This class leverages AgenticTool's LLM capabilities to analyze tool descriptions
|
|
29
|
+
and match them with user queries. It's optimized for minimal context window cost
|
|
30
|
+
by only sending essential information (tool name and description) to the LLM,
|
|
31
|
+
providing an intelligent alternative to embedding-based similarity search.
|
|
32
|
+
|
|
33
|
+
Cost optimizations:
|
|
34
|
+
- Only includes tool name and description in LLM prompt
|
|
35
|
+
- Uses compact formatting to minimize token usage
|
|
36
|
+
- Excludes unnecessary tool metadata and parameters
|
|
37
|
+
- Implements caching to avoid repeated tool processing
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, tool_config, tooluniverse=None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the LLM-based Tool Finder.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
tool_config (dict): Configuration dictionary containing LLM settings and prompts
|
|
46
|
+
tooluniverse: Reference to the ToolUniverse instance containing all tools
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(tool_config)
|
|
49
|
+
self.tooluniverse = tooluniverse
|
|
50
|
+
|
|
51
|
+
# Extract configuration
|
|
52
|
+
self.name = tool_config.get("name", "ToolFinderLLM")
|
|
53
|
+
self.description = tool_config.get("description", "LLM-based tool finder")
|
|
54
|
+
|
|
55
|
+
# Get LLM configuration from tool_config
|
|
56
|
+
configs = tool_config.get("configs", {})
|
|
57
|
+
self.api_type = configs.get("api_type", "CHATGPT")
|
|
58
|
+
self.model_id = configs.get("model_id", "gpt-4o-1120")
|
|
59
|
+
self.temperature = configs.get("temperature", 0.1)
|
|
60
|
+
self.max_new_tokens = configs.get("max_new_tokens", 4096)
|
|
61
|
+
self.return_json = configs.get("return_json", True)
|
|
62
|
+
|
|
63
|
+
# Tool filtering settings
|
|
64
|
+
self.exclude_tools = tool_config.get(
|
|
65
|
+
"exclude_tools",
|
|
66
|
+
tool_config.get("configs", {}).get(
|
|
67
|
+
"exclude_tools",
|
|
68
|
+
["Tool_RAG", "Tool_Finder", "Finish", "CallAgent", "ToolFinderLLM"],
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
self.include_categories = tool_config.get("include_categories", None)
|
|
72
|
+
self.exclude_categories = tool_config.get("exclude_categories", None)
|
|
73
|
+
|
|
74
|
+
# Return format settings - defaults to False if not specified in config
|
|
75
|
+
self.return_list_only = tool_config.get("configs", {}).get(
|
|
76
|
+
"return_list_only", False
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Initialize the underlying AgenticTool for LLM operations
|
|
80
|
+
self._init_agentic_tool()
|
|
81
|
+
|
|
82
|
+
# Cache for tool descriptions
|
|
83
|
+
self._tool_cache = None
|
|
84
|
+
self._cache_timestamp = None
|
|
85
|
+
|
|
86
|
+
def _init_agentic_tool(self):
|
|
87
|
+
"""Initialize the underlying AgenticTool for LLM operations."""
|
|
88
|
+
|
|
89
|
+
# Create AgenticTool configuration
|
|
90
|
+
agentic_config = {
|
|
91
|
+
"name": f"{self.name}_agentic",
|
|
92
|
+
"description": "Internal agentic tool for LLM-based tool selection",
|
|
93
|
+
"type": "AgenticTool",
|
|
94
|
+
"prompt": self._get_tool_selection_prompt(),
|
|
95
|
+
"input_arguments": ["query", "tools_descriptions", "limit"],
|
|
96
|
+
"parameter": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"query": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "The user query describing what tools are needed",
|
|
102
|
+
"required": True,
|
|
103
|
+
},
|
|
104
|
+
"tools_descriptions": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "JSON string containing all available tool descriptions",
|
|
107
|
+
"required": True,
|
|
108
|
+
},
|
|
109
|
+
"limit": {
|
|
110
|
+
"type": "integer",
|
|
111
|
+
"description": "Maximum number of tools to return",
|
|
112
|
+
"required": True,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"required": ["query", "tools_descriptions", "limit"],
|
|
116
|
+
},
|
|
117
|
+
"configs": {
|
|
118
|
+
"api_type": self.api_type,
|
|
119
|
+
"model_id": self.model_id,
|
|
120
|
+
"temperature": self.temperature,
|
|
121
|
+
"max_new_tokens": self.max_new_tokens,
|
|
122
|
+
"return_json": self.return_json,
|
|
123
|
+
"return_metadata": False,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
try:
|
|
127
|
+
self.agentic_tool = AgenticTool(agentic_config)
|
|
128
|
+
print(
|
|
129
|
+
f"✅ Successfully initialized {self.name} with LLM model: {self.model_id}"
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"❌ Failed to initialize AgenticTool for {self.name}: {str(e)}")
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
def _get_tool_selection_prompt(self):
|
|
136
|
+
"""Get the prompt template for tool selection. Optimized for minimal token usage."""
|
|
137
|
+
return """You are a tool selection assistant. Select the most relevant tools for the user query.
|
|
138
|
+
|
|
139
|
+
Query: {query}
|
|
140
|
+
|
|
141
|
+
Tools:
|
|
142
|
+
{tools_descriptions}
|
|
143
|
+
|
|
144
|
+
Select the {limit} most relevant tools. Return JSON:
|
|
145
|
+
{{
|
|
146
|
+
"selected_tools": [
|
|
147
|
+
{{
|
|
148
|
+
"name": "tool_name",
|
|
149
|
+
"relevance_score": 0.95,
|
|
150
|
+
"reasoning": "Why relevant"
|
|
151
|
+
}}
|
|
152
|
+
],
|
|
153
|
+
"total_selected": 1,
|
|
154
|
+
"selection_reasoning": "Overall strategy"
|
|
155
|
+
}}
|
|
156
|
+
|
|
157
|
+
Requirements:
|
|
158
|
+
- Only select existing tools from the list
|
|
159
|
+
- Rank by relevance (0.0-1.0)
|
|
160
|
+
- Prioritize domain-specific tools for specialized queries
|
|
161
|
+
- Return requested number or fewer if insufficient relevant tools"""
|
|
162
|
+
|
|
163
|
+
def _get_available_tools(self, force_refresh=False):
|
|
164
|
+
"""
|
|
165
|
+
Get available tools with their descriptions, with caching.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
force_refresh (bool): Whether to force refresh the cache
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
list: List of tool dictionaries with names and descriptions
|
|
172
|
+
"""
|
|
173
|
+
current_time = datetime.now()
|
|
174
|
+
|
|
175
|
+
# Use cache if available and not expired (cache for 5 minutes)
|
|
176
|
+
if (
|
|
177
|
+
not force_refresh
|
|
178
|
+
and self._tool_cache is not None
|
|
179
|
+
and self._cache_timestamp is not None
|
|
180
|
+
and (current_time - self._cache_timestamp).seconds < 300
|
|
181
|
+
):
|
|
182
|
+
return self._tool_cache
|
|
183
|
+
|
|
184
|
+
if not self.tooluniverse:
|
|
185
|
+
print("⚠️ ToolUniverse reference not available")
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Get tool names and descriptions
|
|
190
|
+
tool_names, tool_descriptions = self.tooluniverse.refresh_tool_name_desc(
|
|
191
|
+
enable_full_desc=True,
|
|
192
|
+
exclude_names=self.exclude_tools,
|
|
193
|
+
include_categories=self.include_categories,
|
|
194
|
+
exclude_categories=self.exclude_categories,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Format tools for LLM
|
|
198
|
+
available_tools = []
|
|
199
|
+
for name, desc in zip(tool_names, tool_descriptions):
|
|
200
|
+
if name not in self.exclude_tools:
|
|
201
|
+
available_tools.append({"name": name, "description": desc})
|
|
202
|
+
|
|
203
|
+
# Update cache
|
|
204
|
+
self._tool_cache = available_tools
|
|
205
|
+
self._cache_timestamp = current_time
|
|
206
|
+
|
|
207
|
+
print(f"📋 Loaded {len(available_tools)} tools for LLM-based selection")
|
|
208
|
+
return available_tools
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(f"❌ Error getting available tools: {str(e)}")
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
def _prefilter_tools_by_keywords(self, available_tools, query, max_tools=100):
|
|
215
|
+
"""
|
|
216
|
+
Pre-filter tools using keyword matching to reduce context size before LLM processing.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
available_tools (list): All available tools
|
|
220
|
+
query (str): User query
|
|
221
|
+
max_tools (int): Maximum number of tools to send to LLM
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
list: Filtered list of tools
|
|
225
|
+
"""
|
|
226
|
+
if len(available_tools) <= max_tools:
|
|
227
|
+
return available_tools
|
|
228
|
+
|
|
229
|
+
query_lower = query.lower()
|
|
230
|
+
query_words = set(query_lower.split())
|
|
231
|
+
|
|
232
|
+
# Score tools based on keyword matches
|
|
233
|
+
scored_tools = []
|
|
234
|
+
for tool in available_tools:
|
|
235
|
+
name_lower = tool.get("name", "").lower()
|
|
236
|
+
desc_lower = tool.get("description", "").lower()
|
|
237
|
+
|
|
238
|
+
# Calculate basic relevance score
|
|
239
|
+
score = 0
|
|
240
|
+
|
|
241
|
+
# Exact name matches get high priority
|
|
242
|
+
if query_lower in name_lower:
|
|
243
|
+
score += 10
|
|
244
|
+
|
|
245
|
+
# Word matches in name and description
|
|
246
|
+
for word in query_words:
|
|
247
|
+
if len(word) > 2: # Skip very short words
|
|
248
|
+
if word in name_lower:
|
|
249
|
+
score += 3
|
|
250
|
+
if word in desc_lower:
|
|
251
|
+
score += 1
|
|
252
|
+
|
|
253
|
+
scored_tools.append((score, tool))
|
|
254
|
+
|
|
255
|
+
# Sort by score and take top tools
|
|
256
|
+
scored_tools.sort(key=lambda x: x[0], reverse=True)
|
|
257
|
+
filtered_tools = [tool for score, tool in scored_tools[:max_tools]]
|
|
258
|
+
|
|
259
|
+
print(
|
|
260
|
+
f"🔍 Pre-filtered from {len(available_tools)} to {len(filtered_tools)} tools using keywords"
|
|
261
|
+
)
|
|
262
|
+
return filtered_tools
|
|
263
|
+
|
|
264
|
+
def _format_tools_for_prompt(self, tools):
|
|
265
|
+
"""
|
|
266
|
+
Format tools for inclusion in the LLM prompt with minimal information to reduce context cost.
|
|
267
|
+
Only includes name and description to minimize token usage.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
tools (list): List of tool dictionaries
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
str: Compact formatted tool descriptions for the prompt
|
|
274
|
+
"""
|
|
275
|
+
formatted_tools = []
|
|
276
|
+
for i, tool in enumerate(tools, 1):
|
|
277
|
+
name = tool.get("name", "Unknown")
|
|
278
|
+
description = tool.get("description", "No description available")
|
|
279
|
+
|
|
280
|
+
# Truncate very long descriptions to save tokens
|
|
281
|
+
if len(description) > 150:
|
|
282
|
+
description = description[:150] + "..."
|
|
283
|
+
|
|
284
|
+
# Use more compact formatting to save tokens
|
|
285
|
+
formatted_tools.append(f"{i}. {name}: {description}")
|
|
286
|
+
|
|
287
|
+
return "\n".join(formatted_tools)
|
|
288
|
+
|
|
289
|
+
def find_tools_llm(self, query, limit=5, include_reasoning=False, categories=None):
|
|
290
|
+
"""
|
|
291
|
+
Find relevant tools using LLM-based selection.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
query (str): User query describing needed functionality
|
|
295
|
+
limit (int): Maximum number of tools to return
|
|
296
|
+
include_reasoning (bool): Whether to include selection reasoning
|
|
297
|
+
categories (list, optional): List of tool categories to filter by
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
dict: Dictionary containing selected tools and metadata
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
# Get available tools
|
|
304
|
+
available_tools = self._get_available_tools()
|
|
305
|
+
|
|
306
|
+
if not available_tools:
|
|
307
|
+
return {
|
|
308
|
+
"success": False,
|
|
309
|
+
"error": "No tools available for selection",
|
|
310
|
+
"selected_tools": [],
|
|
311
|
+
"total_available": 0,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Filter by categories if specified
|
|
315
|
+
if categories:
|
|
316
|
+
# Get full tool information for category filtering
|
|
317
|
+
all_tools = self.tooluniverse.return_all_loaded_tools()
|
|
318
|
+
category_filtered_tools = []
|
|
319
|
+
|
|
320
|
+
for tool_info in available_tools:
|
|
321
|
+
tool_name = tool_info["name"]
|
|
322
|
+
# Find the full tool data to check category
|
|
323
|
+
for full_tool in all_tools:
|
|
324
|
+
if full_tool.get("name") == tool_name:
|
|
325
|
+
tool_category = full_tool.get("category", "unknown")
|
|
326
|
+
if tool_category in categories:
|
|
327
|
+
category_filtered_tools.append(tool_info)
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
available_tools = category_filtered_tools
|
|
331
|
+
|
|
332
|
+
if not available_tools:
|
|
333
|
+
return {
|
|
334
|
+
"success": False,
|
|
335
|
+
"error": f"No tools available in categories: {categories}",
|
|
336
|
+
"selected_tools": [],
|
|
337
|
+
"total_available": 0,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Pre-filter tools to reduce context size for LLM
|
|
341
|
+
available_tools = self._prefilter_tools_by_keywords(
|
|
342
|
+
available_tools, query, max_tools=50
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Format tools for LLM prompt with minimal information to reduce context cost
|
|
346
|
+
tools_formatted = self._format_tools_for_prompt(available_tools)
|
|
347
|
+
|
|
348
|
+
# Prepare arguments for the agentic tool
|
|
349
|
+
agentic_args = {
|
|
350
|
+
"query": query,
|
|
351
|
+
"tools_descriptions": tools_formatted,
|
|
352
|
+
"limit": limit,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
print(f"🤖 Querying LLM to select tools for: '{query[:100]}...'")
|
|
356
|
+
|
|
357
|
+
# Call the LLM through AgenticTool
|
|
358
|
+
result = self.agentic_tool.run(agentic_args)
|
|
359
|
+
|
|
360
|
+
# Parse the LLM response
|
|
361
|
+
if isinstance(result, dict) and "result" in result:
|
|
362
|
+
llm_response = result["result"]
|
|
363
|
+
else:
|
|
364
|
+
llm_response = result
|
|
365
|
+
|
|
366
|
+
# Parse JSON response from LLM
|
|
367
|
+
if isinstance(llm_response, str):
|
|
368
|
+
try:
|
|
369
|
+
parsed_response = json.loads(llm_response)
|
|
370
|
+
except json.JSONDecodeError as e:
|
|
371
|
+
print(f"❌ Failed to parse LLM response as JSON: {e}")
|
|
372
|
+
print(f"Raw response: {llm_response[:500]}...")
|
|
373
|
+
return {
|
|
374
|
+
"success": False,
|
|
375
|
+
"error": f"Invalid JSON response from LLM: {str(e)}",
|
|
376
|
+
"raw_response": llm_response,
|
|
377
|
+
"selected_tools": [],
|
|
378
|
+
}
|
|
379
|
+
else:
|
|
380
|
+
parsed_response = llm_response
|
|
381
|
+
|
|
382
|
+
# Extract selected tools
|
|
383
|
+
selected_tools = parsed_response.get("selected_tools", [])
|
|
384
|
+
tool_names = [
|
|
385
|
+
tool.get("name") for tool in selected_tools if tool.get("name")
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
# Get actual tool objects
|
|
389
|
+
if tool_names:
|
|
390
|
+
selected_tool_objects = self.tooluniverse.get_tool_by_name(tool_names)
|
|
391
|
+
tool_prompts = self.tooluniverse.prepare_tool_prompts(
|
|
392
|
+
selected_tool_objects
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
selected_tool_objects = []
|
|
396
|
+
tool_prompts = []
|
|
397
|
+
|
|
398
|
+
result_dict = {
|
|
399
|
+
"success": True,
|
|
400
|
+
"selected_tools": tool_names,
|
|
401
|
+
"tool_objects": selected_tool_objects,
|
|
402
|
+
"tool_prompts": tool_prompts,
|
|
403
|
+
"total_selected": len(tool_names),
|
|
404
|
+
"total_available": len(available_tools),
|
|
405
|
+
"query": query,
|
|
406
|
+
"limit_requested": limit,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if include_reasoning:
|
|
410
|
+
result_dict.update(
|
|
411
|
+
{
|
|
412
|
+
"selection_details": selected_tools,
|
|
413
|
+
"selection_reasoning": parsed_response.get(
|
|
414
|
+
"selection_reasoning", ""
|
|
415
|
+
),
|
|
416
|
+
"llm_response": parsed_response,
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
print(f"✅ Selected {len(tool_names)} tools: {', '.join(tool_names)}")
|
|
421
|
+
return result_dict
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
print(f"❌ Error in LLM-based tool selection: {str(e)}")
|
|
425
|
+
return {
|
|
426
|
+
"success": False,
|
|
427
|
+
"error": str(e),
|
|
428
|
+
"selected_tools": [],
|
|
429
|
+
"query": query,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
def find_tools(
|
|
433
|
+
self,
|
|
434
|
+
message=None,
|
|
435
|
+
picked_tool_names=None,
|
|
436
|
+
rag_num=5,
|
|
437
|
+
return_call_result=False,
|
|
438
|
+
categories=None,
|
|
439
|
+
return_list_only=None,
|
|
440
|
+
):
|
|
441
|
+
"""
|
|
442
|
+
Find relevant tools based on a message or pre-selected tool names.
|
|
443
|
+
|
|
444
|
+
This method matches the interface of the original ToolFinderEmbedding to ensure
|
|
445
|
+
seamless replacement. It uses LLM-based selection instead of embedding similarity.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
message (str, optional): Query message to find tools for. Required if picked_tool_names is None.
|
|
449
|
+
picked_tool_names (list, optional): Pre-selected tool names to process. Required if message is None.
|
|
450
|
+
rag_num (int, optional): Number of tools to return after filtering. Defaults to 5.
|
|
451
|
+
return_call_result (bool, optional): If True, returns both prompts and tool names. Defaults to False.
|
|
452
|
+
categories (list, optional): List of tool categories to filter by. Applied before LLM selection.
|
|
453
|
+
return_list_only (bool, optional): If True, returns only a list of tool specifications. Overrides other return options.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
str, tuple, or list:
|
|
457
|
+
- If return_list_only is True: List of tool specifications
|
|
458
|
+
- If return_call_result is False: Tool prompts as a formatted string
|
|
459
|
+
- If return_call_result is True: Tuple of (tool_prompts, tool_names)
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
AssertionError: If both message and picked_tool_names are None
|
|
463
|
+
"""
|
|
464
|
+
# Use class-level configuration if parameter not specified
|
|
465
|
+
if return_list_only is None:
|
|
466
|
+
return_list_only = self.return_list_only
|
|
467
|
+
|
|
468
|
+
if picked_tool_names is None:
|
|
469
|
+
assert picked_tool_names is not None or message is not None
|
|
470
|
+
|
|
471
|
+
# Use LLM-based tool selection with category filtering
|
|
472
|
+
result = self.find_tools_llm(
|
|
473
|
+
query=message,
|
|
474
|
+
limit=rag_num,
|
|
475
|
+
include_reasoning=False,
|
|
476
|
+
categories=categories,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if not result["success"]:
|
|
480
|
+
# Return empty results on failure
|
|
481
|
+
if return_list_only:
|
|
482
|
+
return [] # Return empty list for tool specifications
|
|
483
|
+
elif return_call_result:
|
|
484
|
+
return "", []
|
|
485
|
+
return ""
|
|
486
|
+
|
|
487
|
+
picked_tool_names = result["selected_tools"]
|
|
488
|
+
|
|
489
|
+
# Filter out special tools (matching original behavior)
|
|
490
|
+
picked_tool_names_no_special = []
|
|
491
|
+
for tool in picked_tool_names:
|
|
492
|
+
if tool not in self.exclude_tools:
|
|
493
|
+
picked_tool_names_no_special.append(tool)
|
|
494
|
+
picked_tool_names_no_special = picked_tool_names_no_special[:rag_num]
|
|
495
|
+
picked_tool_names = picked_tool_names_no_special[:rag_num]
|
|
496
|
+
|
|
497
|
+
# Get tool objects and prepare prompts (needed for both list and other formats)
|
|
498
|
+
picked_tools = self.tooluniverse.get_tool_by_name(picked_tool_names)
|
|
499
|
+
picked_tools_prompt = self.tooluniverse.prepare_tool_prompts(picked_tools)
|
|
500
|
+
|
|
501
|
+
# If only list format is requested, return the tool specifications as a list
|
|
502
|
+
if return_list_only:
|
|
503
|
+
return picked_tools_prompt # Return list of tool specifications instead of just names
|
|
504
|
+
|
|
505
|
+
if return_call_result:
|
|
506
|
+
return picked_tools_prompt, picked_tool_names
|
|
507
|
+
return picked_tools_prompt
|
|
508
|
+
|
|
509
|
+
def get_tool_stats(self):
|
|
510
|
+
"""Get statistics about available tools."""
|
|
511
|
+
tools = self._get_available_tools(force_refresh=True)
|
|
512
|
+
|
|
513
|
+
stats = {
|
|
514
|
+
"total_tools": len(tools),
|
|
515
|
+
"excluded_tools": len(self.exclude_tools),
|
|
516
|
+
"cache_status": "cached" if self._tool_cache is not None else "no_cache",
|
|
517
|
+
"last_updated": (
|
|
518
|
+
self._cache_timestamp.isoformat() if self._cache_timestamp else None
|
|
519
|
+
),
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return stats
|
|
523
|
+
|
|
524
|
+
def _format_as_json(self, result, query, limit, categories, return_call_result):
|
|
525
|
+
"""
|
|
526
|
+
Format the find_tools result as a standardized JSON string.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
result: Result from find_tools method (either string, list, or tuple)
|
|
530
|
+
query: Original search query
|
|
531
|
+
limit: Requested number of tools
|
|
532
|
+
categories: Requested categories filter
|
|
533
|
+
return_call_result: Whether return_call_result was True
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
str: JSON formatted search results
|
|
537
|
+
"""
|
|
538
|
+
import json
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
if return_call_result and isinstance(result, tuple) and len(result) == 2:
|
|
542
|
+
# Result is (tool_prompts, tool_names) tuple
|
|
543
|
+
tool_prompts, tool_names = result
|
|
544
|
+
|
|
545
|
+
# Convert tool prompts to clean tool info format
|
|
546
|
+
tools = []
|
|
547
|
+
for i, tool_name in enumerate(tool_names):
|
|
548
|
+
if i < len(tool_prompts):
|
|
549
|
+
tool_prompt = tool_prompts[i]
|
|
550
|
+
tool_info = {
|
|
551
|
+
"name": tool_name,
|
|
552
|
+
"description": tool_prompt.get("description", ""),
|
|
553
|
+
"type": tool_prompt.get("type", ""),
|
|
554
|
+
"parameters": tool_prompt.get("parameter", {}),
|
|
555
|
+
"required": tool_prompt.get("required", []),
|
|
556
|
+
}
|
|
557
|
+
tools.append(tool_info)
|
|
558
|
+
|
|
559
|
+
return json.dumps(
|
|
560
|
+
{
|
|
561
|
+
"query": query,
|
|
562
|
+
"search_method": "AI-powered (ToolFinderLLM)",
|
|
563
|
+
"total_matches": len(tools),
|
|
564
|
+
"categories_filtered": categories,
|
|
565
|
+
"tools": tools,
|
|
566
|
+
},
|
|
567
|
+
indent=2,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
elif isinstance(result, list):
|
|
571
|
+
# Result is already a list of tool prompts
|
|
572
|
+
tools = []
|
|
573
|
+
for tool_prompt in result:
|
|
574
|
+
if isinstance(tool_prompt, dict):
|
|
575
|
+
tool_info = {
|
|
576
|
+
"name": tool_prompt.get("name", ""),
|
|
577
|
+
"description": tool_prompt.get("description", ""),
|
|
578
|
+
"type": tool_prompt.get("type", ""),
|
|
579
|
+
"parameters": tool_prompt.get("parameter", {}),
|
|
580
|
+
"required": tool_prompt.get("required", []),
|
|
581
|
+
}
|
|
582
|
+
tools.append(tool_info)
|
|
583
|
+
|
|
584
|
+
return json.dumps(
|
|
585
|
+
{
|
|
586
|
+
"query": query,
|
|
587
|
+
"search_method": "AI-powered (ToolFinderLLM)",
|
|
588
|
+
"total_matches": len(tools),
|
|
589
|
+
"categories_filtered": categories,
|
|
590
|
+
"tools": tools,
|
|
591
|
+
},
|
|
592
|
+
indent=2,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
else:
|
|
596
|
+
# Fallback for unexpected result format
|
|
597
|
+
return json.dumps(
|
|
598
|
+
{
|
|
599
|
+
"query": query,
|
|
600
|
+
"search_method": "AI-powered (ToolFinderLLM)",
|
|
601
|
+
"total_matches": 0,
|
|
602
|
+
"categories_filtered": categories,
|
|
603
|
+
"tools": [],
|
|
604
|
+
"error": f"Unexpected result format: {type(result)}",
|
|
605
|
+
},
|
|
606
|
+
indent=2,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
except Exception as e:
|
|
610
|
+
# Error handling
|
|
611
|
+
return json.dumps(
|
|
612
|
+
{
|
|
613
|
+
"query": query,
|
|
614
|
+
"search_method": "AI-powered (ToolFinderLLM)",
|
|
615
|
+
"total_matches": 0,
|
|
616
|
+
"categories_filtered": categories,
|
|
617
|
+
"tools": [],
|
|
618
|
+
"error": f"Formatting error: {str(e)}",
|
|
619
|
+
},
|
|
620
|
+
indent=2,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def clear_cache(self):
|
|
624
|
+
"""Clear the tool cache to force refresh on next access."""
|
|
625
|
+
self._tool_cache = None
|
|
626
|
+
self._cache_timestamp = None
|
|
627
|
+
print("🔄 Tool cache cleared")
|
|
628
|
+
|
|
629
|
+
def run(self, arguments):
|
|
630
|
+
"""
|
|
631
|
+
Run the tool finder with given arguments following the standard tool interface.
|
|
632
|
+
|
|
633
|
+
This method now returns JSON format by default to ensure consistency with other
|
|
634
|
+
search tools and simplify integration with SMCP.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
arguments (dict): Dictionary containing:
|
|
638
|
+
- description (str, optional): Query message to find tools for (maps to 'message')
|
|
639
|
+
- limit (int, optional): Number of tools to return (maps to 'rag_num'). Defaults to 5.
|
|
640
|
+
- picked_tool_names (list, optional): Pre-selected tool names to process
|
|
641
|
+
- return_call_result (bool, optional): Whether to return both prompts and names. Defaults to False.
|
|
642
|
+
- return_format (str, optional): 'json' (default) or 'legacy' for old format
|
|
643
|
+
- return_list_only (bool, optional): Whether to return only tool specifications as a list
|
|
644
|
+
- categories (list, optional): List of tool categories to filter by
|
|
645
|
+
"""
|
|
646
|
+
import copy
|
|
647
|
+
|
|
648
|
+
arguments = copy.deepcopy(arguments)
|
|
649
|
+
|
|
650
|
+
# Extract parameters from arguments with defaults
|
|
651
|
+
message = arguments.get("description", None)
|
|
652
|
+
rag_num = arguments.get("limit", 5)
|
|
653
|
+
picked_tool_names = arguments.get("picked_tool_names", None)
|
|
654
|
+
return_call_result = arguments.get("return_call_result", False)
|
|
655
|
+
return_format = arguments.get("return_format", "json") # Default to JSON format
|
|
656
|
+
return_list_only = arguments.get(
|
|
657
|
+
"return_list_only", None
|
|
658
|
+
) # Use class default if not specified
|
|
659
|
+
categories = arguments.get("categories", None)
|
|
660
|
+
|
|
661
|
+
# Call the find_tools method
|
|
662
|
+
result = self.find_tools(
|
|
663
|
+
message=message,
|
|
664
|
+
picked_tool_names=picked_tool_names,
|
|
665
|
+
rag_num=rag_num,
|
|
666
|
+
return_call_result=return_call_result,
|
|
667
|
+
categories=categories,
|
|
668
|
+
return_list_only=return_list_only,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# If return_list_only is True, return the list directly
|
|
672
|
+
if return_list_only or (return_list_only is None and self.return_list_only):
|
|
673
|
+
return result
|
|
674
|
+
|
|
675
|
+
# If return_format is 'json', convert to standardized JSON format
|
|
676
|
+
if return_format == "json":
|
|
677
|
+
return self._format_as_json(
|
|
678
|
+
result, message, rag_num, categories, return_call_result
|
|
679
|
+
)
|
|
680
|
+
else:
|
|
681
|
+
# Return legacy format (original behavior)
|
|
682
|
+
return result
|
|
683
|
+
|
|
684
|
+
# Legacy methods for backward compatibility
|
|
685
|
+
def find_tools_legacy(
|
|
686
|
+
self, query, limit=5, include_reasoning=False, return_format="prompts"
|
|
687
|
+
):
|
|
688
|
+
"""
|
|
689
|
+
Legacy method for finding tools with different parameter names.
|
|
690
|
+
|
|
691
|
+
This provides backward compatibility for any code that might use 'query' instead of 'description'.
|
|
692
|
+
"""
|
|
693
|
+
return self.run(
|
|
694
|
+
{
|
|
695
|
+
"description": query,
|
|
696
|
+
"limit": limit,
|
|
697
|
+
"return_call_result": return_format == "full",
|
|
698
|
+
}
|
|
699
|
+
)
|