tooluniverse 0.1.4__py3-none-any.whl → 1.0.0__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.

Files changed (187) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/agentic_tools.json +1156 -0
  23. tooluniverse/data/alphafold_tools.json +87 -0
  24. tooluniverse/data/boltz_tools.json +9 -0
  25. tooluniverse/data/chembl_tools.json +16 -0
  26. tooluniverse/data/clait_tools.json +108 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +544 -168
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +82 -58
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/software_tools.json +4954 -0
  66. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  67. tooluniverse/data/packages/visualization_tools.json +399 -0
  68. tooluniverse/data/pubchem_tools.json +215 -0
  69. tooluniverse/data/pubtator_tools.json +68 -0
  70. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  71. tooluniverse/data/reactome_tools.json +19 -0
  72. tooluniverse/data/semantic_scholar_tools.json +26 -0
  73. tooluniverse/data/special_tools.json +2 -25
  74. tooluniverse/data/tool_composition_tools.json +88 -0
  75. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  76. tooluniverse/data/txagent_client_tools.json +9 -0
  77. tooluniverse/data/uniprot_tools.json +211 -0
  78. tooluniverse/data/url_fetch_tools.json +94 -0
  79. tooluniverse/data/uspto_downloader_tools.json +9 -0
  80. tooluniverse/data/uspto_tools.json +811 -0
  81. tooluniverse/data/xml_tools.json +3275 -0
  82. tooluniverse/dataset_tool.py +296 -0
  83. tooluniverse/default_config.py +165 -0
  84. tooluniverse/efo_tool.py +42 -0
  85. tooluniverse/embedding_database.py +630 -0
  86. tooluniverse/embedding_sync.py +396 -0
  87. tooluniverse/enrichr_tool.py +266 -0
  88. tooluniverse/europe_pmc_tool.py +52 -0
  89. tooluniverse/execute_function.py +1775 -95
  90. tooluniverse/extended_hooks.py +444 -0
  91. tooluniverse/gene_ontology_tool.py +194 -0
  92. tooluniverse/graphql_tool.py +158 -36
  93. tooluniverse/gwas_tool.py +358 -0
  94. tooluniverse/hpa_tool.py +1645 -0
  95. tooluniverse/humanbase_tool.py +389 -0
  96. tooluniverse/logging_config.py +254 -0
  97. tooluniverse/mcp_client_tool.py +764 -0
  98. tooluniverse/mcp_integration.py +413 -0
  99. tooluniverse/mcp_tool_registry.py +925 -0
  100. tooluniverse/medlineplus_tool.py +337 -0
  101. tooluniverse/openalex_tool.py +228 -0
  102. tooluniverse/openfda_adv_tool.py +283 -0
  103. tooluniverse/openfda_tool.py +393 -160
  104. tooluniverse/output_hook.py +1122 -0
  105. tooluniverse/package_tool.py +195 -0
  106. tooluniverse/pubchem_tool.py +158 -0
  107. tooluniverse/pubtator_tool.py +168 -0
  108. tooluniverse/rcsb_pdb_tool.py +38 -0
  109. tooluniverse/reactome_tool.py +108 -0
  110. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  111. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  112. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  113. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  114. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  115. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  116. tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
  117. tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
  118. tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
  119. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  120. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  121. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  122. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  123. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  124. tooluniverse/remote_tool.py +99 -0
  125. tooluniverse/restful_tool.py +53 -30
  126. tooluniverse/scripts/generate_tool_graph.py +408 -0
  127. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  128. tooluniverse/semantic_scholar_tool.py +62 -0
  129. tooluniverse/smcp.py +2452 -0
  130. tooluniverse/smcp_server.py +975 -0
  131. tooluniverse/test/mcp_server_test.py +0 -0
  132. tooluniverse/test/test_admetai_tool.py +370 -0
  133. tooluniverse/test/test_agentic_tool.py +129 -0
  134. tooluniverse/test/test_alphafold_tool.py +71 -0
  135. tooluniverse/test/test_chem_tool.py +37 -0
  136. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  137. tooluniverse/test/test_compose_tool.py +448 -0
  138. tooluniverse/test/test_dailymed.py +69 -0
  139. tooluniverse/test/test_dataset_tool.py +200 -0
  140. tooluniverse/test/test_disease_target_score.py +56 -0
  141. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  142. tooluniverse/test/test_efo.py +31 -0
  143. tooluniverse/test/test_enrichr_tool.py +21 -0
  144. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  145. tooluniverse/test/test_fda_adv.py +95 -0
  146. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  147. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  148. tooluniverse/test/test_gwas_tool.py +139 -0
  149. tooluniverse/test/test_hpa.py +625 -0
  150. tooluniverse/test/test_humanbase_tool.py +20 -0
  151. tooluniverse/test/test_idmap_tools.py +61 -0
  152. tooluniverse/test/test_mcp_server.py +211 -0
  153. tooluniverse/test/test_mcp_tool.py +247 -0
  154. tooluniverse/test/test_medlineplus.py +220 -0
  155. tooluniverse/test/test_openalex_tool.py +32 -0
  156. tooluniverse/test/test_opentargets.py +28 -0
  157. tooluniverse/test/test_pubchem_tool.py +116 -0
  158. tooluniverse/test/test_pubtator_tool.py +37 -0
  159. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  160. tooluniverse/test/test_reactome.py +54 -0
  161. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  162. tooluniverse/test/test_software_tools.py +147 -0
  163. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  164. tooluniverse/test/test_tool_finder.py +26 -0
  165. tooluniverse/test/test_tool_finder_llm.py +252 -0
  166. tooluniverse/test/test_tools_find.py +195 -0
  167. tooluniverse/test/test_uniprot_tools.py +74 -0
  168. tooluniverse/test/test_uspto_tool.py +72 -0
  169. tooluniverse/test/test_xml_tool.py +113 -0
  170. tooluniverse/tool_finder_embedding.py +267 -0
  171. tooluniverse/tool_finder_keyword.py +693 -0
  172. tooluniverse/tool_finder_llm.py +699 -0
  173. tooluniverse/tool_graph_web_ui.py +955 -0
  174. tooluniverse/tool_registry.py +416 -0
  175. tooluniverse/uniprot_tool.py +155 -0
  176. tooluniverse/url_tool.py +253 -0
  177. tooluniverse/uspto_tool.py +240 -0
  178. tooluniverse/utils.py +369 -41
  179. tooluniverse/xml_tool.py +369 -0
  180. tooluniverse-1.0.0.dist-info/METADATA +377 -0
  181. tooluniverse-1.0.0.dist-info/RECORD +186 -0
  182. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +1 -1
  183. tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
  184. tooluniverse-0.1.4.dist-info/METADATA +0 -141
  185. tooluniverse-0.1.4.dist-info/RECORD +0 -18
  186. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  187. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.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
+ )