tooluniverse 1.0.11.2__py3-none-any.whl → 1.0.12__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/build_optimizer.py +115 -22
- tooluniverse/data/encode_tools.json +139 -0
- tooluniverse/data/gbif_tools.json +152 -0
- tooluniverse/data/gdc_tools.json +116 -0
- tooluniverse/data/gtex_tools.json +116 -0
- tooluniverse/data/icgc_tools.json +0 -0
- tooluniverse/data/mgnify_tools.json +121 -0
- tooluniverse/data/obis_tools.json +122 -0
- tooluniverse/data/optimizer_tools.json +275 -0
- tooluniverse/data/rnacentral_tools.json +99 -0
- tooluniverse/data/smolagent_tools.json +206 -0
- tooluniverse/data/wikipathways_tools.json +106 -0
- tooluniverse/default_config.py +12 -0
- tooluniverse/encode_tool.py +245 -0
- tooluniverse/execute_function.py +46 -8
- tooluniverse/gbif_tool.py +166 -0
- tooluniverse/gdc_tool.py +175 -0
- tooluniverse/generate_tools.py +121 -9
- tooluniverse/gtex_tool.py +168 -0
- tooluniverse/mgnify_tool.py +181 -0
- tooluniverse/obis_tool.py +185 -0
- tooluniverse/pypi_package_inspector_tool.py +3 -2
- tooluniverse/rnacentral_tool.py +124 -0
- tooluniverse/smcp_server.py +1 -1
- tooluniverse/smolagent_tool.py +555 -0
- tooluniverse/tools/ArgumentDescriptionOptimizer.py +55 -0
- tooluniverse/tools/ENCODE_list_files.py +59 -0
- tooluniverse/tools/ENCODE_search_experiments.py +67 -0
- tooluniverse/tools/GBIF_search_occurrences.py +67 -0
- tooluniverse/tools/GBIF_search_species.py +55 -0
- tooluniverse/tools/GDC_list_files.py +55 -0
- tooluniverse/tools/GDC_search_cases.py +55 -0
- tooluniverse/tools/GTEx_get_expression_summary.py +49 -0
- tooluniverse/tools/GTEx_query_eqtl.py +59 -0
- tooluniverse/tools/MGnify_list_analyses.py +52 -0
- tooluniverse/tools/MGnify_search_studies.py +55 -0
- tooluniverse/tools/OBIS_search_occurrences.py +59 -0
- tooluniverse/tools/OBIS_search_taxa.py +52 -0
- tooluniverse/tools/RNAcentral_get_by_accession.py +46 -0
- tooluniverse/tools/RNAcentral_search.py +52 -0
- tooluniverse/tools/TestCaseGenerator.py +46 -0
- tooluniverse/tools/ToolDescriptionOptimizer.py +67 -0
- tooluniverse/tools/ToolDiscover.py +4 -0
- tooluniverse/tools/UniProt_search.py +17 -44
- tooluniverse/tools/WikiPathways_get_pathway.py +52 -0
- tooluniverse/tools/WikiPathways_search.py +52 -0
- tooluniverse/tools/__init__.py +43 -1
- tooluniverse/tools/advanced_literature_search_agent.py +46 -0
- tooluniverse/tools/alphafold_get_annotations.py +4 -10
- tooluniverse/tools/download_binary_file.py +3 -6
- tooluniverse/tools/open_deep_research_agent.py +46 -0
- tooluniverse/wikipathways_tool.py +122 -0
- {tooluniverse-1.0.11.2.dist-info → tooluniverse-1.0.12.dist-info}/METADATA +3 -1
- {tooluniverse-1.0.11.2.dist-info → tooluniverse-1.0.12.dist-info}/RECORD +58 -17
- {tooluniverse-1.0.11.2.dist-info → tooluniverse-1.0.12.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.11.2.dist-info → tooluniverse-1.0.12.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.11.2.dist-info → tooluniverse-1.0.12.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-1.0.11.2.dist-info → tooluniverse-1.0.12.dist-info}/top_level.txt +0 -0
tooluniverse/smcp_server.py
CHANGED
|
@@ -455,7 +455,7 @@ Examples:
|
|
|
455
455
|
|
|
456
456
|
try:
|
|
457
457
|
print(f"🚀 Starting {args.name}...", file=sys.stderr)
|
|
458
|
-
print("📡 Transport: stdio
|
|
458
|
+
print("📡 Transport: stdio", file=sys.stderr)
|
|
459
459
|
print(f"🔍 Search enabled: {not args.no_search}", file=sys.stderr)
|
|
460
460
|
|
|
461
461
|
if args.categories is not None:
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .base_tool import BaseTool
|
|
7
|
+
from .tool_registry import register_tool
|
|
8
|
+
|
|
9
|
+
# Global lock for stdout/stderr redirection (for thread safety)
|
|
10
|
+
_STREAM_LOCK = threading.Lock()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _safe_import(module_path: str, symbol: str):
|
|
14
|
+
"""Safely import a symbol from a module, raising a helpful error if missing."""
|
|
15
|
+
try:
|
|
16
|
+
module = __import__(module_path, fromlist=[symbol])
|
|
17
|
+
return getattr(module, symbol)
|
|
18
|
+
except Exception as e: # noqa: BLE001
|
|
19
|
+
raise ImportError(
|
|
20
|
+
f"Failed to import '{symbol}' from '{module_path}'. Please install and configure 'smolagents'. Original error: {e}"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolUniverseTool: # Lazy base; will subclass smolagents.Tool at runtime
|
|
25
|
+
"""
|
|
26
|
+
Adapter that wraps a ToolUniverse tool and exposes it as a smolagents Tool.
|
|
27
|
+
|
|
28
|
+
We create the real subclass dynamically to avoid hard dependency when the
|
|
29
|
+
module is imported without smolagents installed.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __new__(cls, *args, **kwargs): # pragma: no cover - construct dynamic subclass
|
|
33
|
+
# Import here to avoid import-time dependency when not used
|
|
34
|
+
Tool = _safe_import("smolagents", "Tool")
|
|
35
|
+
|
|
36
|
+
# Arguments: tool_name, tooluniverse_instance, tool_config
|
|
37
|
+
tool_name: str = args[0]
|
|
38
|
+
tooluniverse_instance = args[1]
|
|
39
|
+
tool_config = args[2] if len(args) > 2 else None
|
|
40
|
+
|
|
41
|
+
tu_config = getattr(tooluniverse_instance, "all_tool_dict", {}).get(
|
|
42
|
+
tool_name, {}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Helpers to build class attributes
|
|
46
|
+
def _convert_parameter_schema(parameter_schema: Dict) -> Dict:
|
|
47
|
+
properties = parameter_schema.get("properties", {})
|
|
48
|
+
required = set(parameter_schema.get("required", []) or [])
|
|
49
|
+
inputs: Dict[str, Dict[str, Any]] = {}
|
|
50
|
+
for param_name, info in properties.items():
|
|
51
|
+
entry: Dict[str, Any] = {
|
|
52
|
+
"type": info.get("type", "string"),
|
|
53
|
+
"description": info.get("description", ""),
|
|
54
|
+
}
|
|
55
|
+
if param_name not in required and "default" in info:
|
|
56
|
+
entry["nullable"] = True
|
|
57
|
+
inputs[param_name] = entry
|
|
58
|
+
return inputs
|
|
59
|
+
|
|
60
|
+
def _infer_output_type(return_schema: Dict) -> str:
|
|
61
|
+
schema_type = return_schema.get("type", "string")
|
|
62
|
+
mapping = {
|
|
63
|
+
"object": "string",
|
|
64
|
+
"array": "string",
|
|
65
|
+
"string": "string",
|
|
66
|
+
"integer": "integer",
|
|
67
|
+
"number": "number",
|
|
68
|
+
"boolean": "boolean",
|
|
69
|
+
}
|
|
70
|
+
return mapping.get(schema_type, "string")
|
|
71
|
+
|
|
72
|
+
inputs_schema = _convert_parameter_schema(tu_config.get("parameter", {}))
|
|
73
|
+
output_type = _infer_output_type(tu_config.get("return_schema", {}))
|
|
74
|
+
|
|
75
|
+
# Build a forward function with explicit parameters to satisfy
|
|
76
|
+
# smolagents' validation (parameters must match keys in `inputs`).
|
|
77
|
+
def __call_tool(
|
|
78
|
+
self, __kwargs, _tool_name=tool_name, _tu=tooluniverse_instance
|
|
79
|
+
):
|
|
80
|
+
try:
|
|
81
|
+
result = _tu.run_one_function(
|
|
82
|
+
{"name": _tool_name, "arguments": __kwargs}
|
|
83
|
+
)
|
|
84
|
+
if isinstance(result, dict):
|
|
85
|
+
import json
|
|
86
|
+
|
|
87
|
+
return json.dumps(result, ensure_ascii=False)
|
|
88
|
+
return result
|
|
89
|
+
except Exception as e: # noqa: BLE001
|
|
90
|
+
return f"Error executing tool {_tool_name}: {e}"
|
|
91
|
+
|
|
92
|
+
param_names = list(inputs_schema.keys())
|
|
93
|
+
if param_names:
|
|
94
|
+
# Dynamically create a function with signature: (self, p1, p2, ...)
|
|
95
|
+
params_sig = ", ".join(param_names)
|
|
96
|
+
body_lines = [" _kwargs = {"]
|
|
97
|
+
for p in param_names:
|
|
98
|
+
body_lines.append(f" '{p}': {p},")
|
|
99
|
+
body_lines.append(" }")
|
|
100
|
+
body_lines.append(" return __call_tool(self, _kwargs)")
|
|
101
|
+
func_src = [f"def _forward(self, {params_sig}):"] + body_lines
|
|
102
|
+
func_src = "\n".join(func_src)
|
|
103
|
+
ns: Dict[str, Any] = {"__call_tool": __call_tool}
|
|
104
|
+
exec(func_src, ns)
|
|
105
|
+
_forward = ns["_forward"] # type: ignore[assignment]
|
|
106
|
+
else:
|
|
107
|
+
# No inputs -> 0-arg forward
|
|
108
|
+
def _forward(self): # type: ignore[override]
|
|
109
|
+
return __call_tool(self, {})
|
|
110
|
+
|
|
111
|
+
attrs = {
|
|
112
|
+
"name": tool_name,
|
|
113
|
+
"description": tu_config.get("description", ""),
|
|
114
|
+
"inputs": inputs_schema,
|
|
115
|
+
"output_type": output_type,
|
|
116
|
+
"forward": _forward,
|
|
117
|
+
"tool_config": tool_config or {},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
DynamicToolCls = type(f"ToolUniverseTool_{tool_name}", (Tool,), attrs) # type: ignore[misc]
|
|
121
|
+
return DynamicToolCls()
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_tooluniverse(
|
|
125
|
+
cls,
|
|
126
|
+
tool_name: str,
|
|
127
|
+
tooluniverse_instance,
|
|
128
|
+
tool_config: Optional[Dict[str, Any]] = None,
|
|
129
|
+
):
|
|
130
|
+
"""Factory to create a smolagents-compatible Tool from a ToolUniverse tool.
|
|
131
|
+
|
|
132
|
+
This mirrors common factory patterns (e.g., from_langchain) and returns
|
|
133
|
+
an instance of the dynamically constructed Tool subclass.
|
|
134
|
+
"""
|
|
135
|
+
return cls(tool_name, tooluniverse_instance, tool_config or {})
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@register_tool("SmolAgentTool")
|
|
139
|
+
class SmolAgentTool(BaseTool):
|
|
140
|
+
"""Wrap smolagents agents so they can be used as ToolUniverse tools.
|
|
141
|
+
|
|
142
|
+
Supports:
|
|
143
|
+
- CodeAgent, ToolCallingAgent, Agent, ManagedAgent
|
|
144
|
+
- Mixed tools: ToolUniverse tools and smolagents-native tools
|
|
145
|
+
- Streaming integration with ToolUniverse stream callbacks
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, tool_config: Dict[str, Any]):
|
|
149
|
+
super().__init__(tool_config)
|
|
150
|
+
settings = tool_config.get("settings", {})
|
|
151
|
+
|
|
152
|
+
self.agent_type: str = settings.get("agent_type", "CodeAgent")
|
|
153
|
+
self.available_tools: List[Any] = settings.get("available_tools", [])
|
|
154
|
+
self.model_config: Dict[str, Any] = settings.get("model", {})
|
|
155
|
+
self.agent_init_params: Dict[str, Any] = settings.get("agent_init_params", {})
|
|
156
|
+
self.sub_agents_config: List[Dict[str, Any]] = settings.get("sub_agents", [])
|
|
157
|
+
|
|
158
|
+
# Will be set by ToolUniverse runtime
|
|
159
|
+
self.tooluniverse = None
|
|
160
|
+
self.agent = None
|
|
161
|
+
|
|
162
|
+
# -------------------------
|
|
163
|
+
# Initialization helpers
|
|
164
|
+
# -------------------------
|
|
165
|
+
def _get_api_key(self) -> Optional[str]:
|
|
166
|
+
api_key = self.model_config.get("api_key")
|
|
167
|
+
if isinstance(api_key, str) and api_key.startswith("env:"):
|
|
168
|
+
import os
|
|
169
|
+
|
|
170
|
+
return os.environ.get(api_key[4:])
|
|
171
|
+
return api_key
|
|
172
|
+
|
|
173
|
+
def _init_model(self):
|
|
174
|
+
provider = self.model_config.get("provider", "HfApiModel")
|
|
175
|
+
model_id = self.model_config.get("model_id")
|
|
176
|
+
api_key = self._get_api_key()
|
|
177
|
+
|
|
178
|
+
if provider == "HfApiModel":
|
|
179
|
+
HfApiModel = _safe_import("smolagents", "HfApiModel")
|
|
180
|
+
return HfApiModel(model_id, token=api_key)
|
|
181
|
+
if provider == "OpenAIModel":
|
|
182
|
+
OpenAIModel = _safe_import("smolagents", "OpenAIModel")
|
|
183
|
+
return OpenAIModel(
|
|
184
|
+
model_id=model_id,
|
|
185
|
+
api_key=api_key,
|
|
186
|
+
api_base=self.model_config.get("api_base"),
|
|
187
|
+
)
|
|
188
|
+
if provider == "LiteLLMModel":
|
|
189
|
+
LiteLLMModel = _safe_import("smolagents", "LiteLLMModel")
|
|
190
|
+
return LiteLLMModel(model_id=model_id, api_key=api_key)
|
|
191
|
+
if provider == "InferenceClientModel":
|
|
192
|
+
InferenceClientModel = _safe_import("smolagents", "InferenceClientModel")
|
|
193
|
+
return InferenceClientModel(
|
|
194
|
+
model_id=model_id,
|
|
195
|
+
provider=self.model_config.get("provider_name"),
|
|
196
|
+
token=api_key,
|
|
197
|
+
)
|
|
198
|
+
if provider == "TransformersModel":
|
|
199
|
+
TransformersModel = _safe_import("smolagents", "TransformersModel")
|
|
200
|
+
return TransformersModel(
|
|
201
|
+
model_id=model_id,
|
|
202
|
+
)
|
|
203
|
+
if provider == "AzureOpenAIModel":
|
|
204
|
+
AzureOpenAIModel = _safe_import("smolagents", "AzureOpenAIModel")
|
|
205
|
+
return AzureOpenAIModel(
|
|
206
|
+
model_id=model_id,
|
|
207
|
+
azure_endpoint=self.model_config.get("azure_endpoint"),
|
|
208
|
+
api_key=api_key,
|
|
209
|
+
api_version=self.model_config.get("api_version"),
|
|
210
|
+
)
|
|
211
|
+
if provider == "AmazonBedrockModel":
|
|
212
|
+
AmazonBedrockModel = _safe_import("smolagents", "AmazonBedrockModel")
|
|
213
|
+
return AmazonBedrockModel(model_id=model_id)
|
|
214
|
+
|
|
215
|
+
raise ValueError(f"Unsupported model provider: {provider}")
|
|
216
|
+
|
|
217
|
+
def _import_smolagents_tool(self, class_name: str, import_path: str):
|
|
218
|
+
"""Dynamically import smolagents tool class with helpful error messages."""
|
|
219
|
+
import importlib
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
module = importlib.import_module(import_path)
|
|
223
|
+
except ImportError as e:
|
|
224
|
+
raise ImportError(
|
|
225
|
+
f"Failed to import module '{import_path}' for smolagents tool '{class_name}'. "
|
|
226
|
+
f"Please ensure the module path is correct. "
|
|
227
|
+
f"Common paths include 'smolagents.tools' or 'smolagents.default_tools'. "
|
|
228
|
+
f"Original error: {e}"
|
|
229
|
+
) from e
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
tool_class = getattr(module, class_name)
|
|
233
|
+
except AttributeError as e:
|
|
234
|
+
available_attrs = [attr for attr in dir(module) if not attr.startswith("_")]
|
|
235
|
+
raise AttributeError(
|
|
236
|
+
f"Class '{class_name}' not found in module '{import_path}'. "
|
|
237
|
+
f"Available classes in the module: {', '.join(available_attrs[:10])}"
|
|
238
|
+
f"{'...' if len(available_attrs) > 10 else ''}. "
|
|
239
|
+
f"Please check the class name spelling and ensure it exists in the module. "
|
|
240
|
+
f"Original error: {e}"
|
|
241
|
+
) from e
|
|
242
|
+
|
|
243
|
+
return tool_class
|
|
244
|
+
|
|
245
|
+
def _convert_tools(self) -> List[Any]:
|
|
246
|
+
"""Convert mixed tool definitions to smolagents Tool instances."""
|
|
247
|
+
converted: List[Any] = []
|
|
248
|
+
for spec in self.available_tools:
|
|
249
|
+
if isinstance(spec, str):
|
|
250
|
+
converted.append(
|
|
251
|
+
ToolUniverseTool.from_tooluniverse(spec, self.tooluniverse)
|
|
252
|
+
)
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
if not isinstance(spec, dict):
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
spec_type = spec.get("type", "tooluniverse")
|
|
259
|
+
if spec_type == "smolagents":
|
|
260
|
+
cls_name = spec.get("class")
|
|
261
|
+
if not cls_name:
|
|
262
|
+
continue
|
|
263
|
+
import_path = spec.get("import_path", "smolagents.tools")
|
|
264
|
+
kwargs = spec.get("kwargs", {})
|
|
265
|
+
tool_cls = self._import_smolagents_tool(cls_name, import_path)
|
|
266
|
+
converted.append(tool_cls(**kwargs))
|
|
267
|
+
else:
|
|
268
|
+
name = spec.get("name")
|
|
269
|
+
if name:
|
|
270
|
+
converted.append(
|
|
271
|
+
ToolUniverseTool.from_tooluniverse(name, self.tooluniverse)
|
|
272
|
+
)
|
|
273
|
+
return converted
|
|
274
|
+
|
|
275
|
+
def _create_sub_agents(self, sub_configs: List[Dict[str, Any]]) -> List[Any]:
|
|
276
|
+
"""Recursively create sub-agent instances (for ManagedAgent)."""
|
|
277
|
+
sub_agents: List[Any] = []
|
|
278
|
+
for cfg in sub_configs:
|
|
279
|
+
sub_tool_config: Dict[str, Any] = {
|
|
280
|
+
"name": cfg.get("name", "sub_agent"),
|
|
281
|
+
"type": "SmolAgentTool",
|
|
282
|
+
"description": cfg.get("description", ""),
|
|
283
|
+
"settings": cfg,
|
|
284
|
+
}
|
|
285
|
+
sub_tool = SmolAgentTool(sub_tool_config)
|
|
286
|
+
sub_tool.tooluniverse = self.tooluniverse
|
|
287
|
+
sub_tool._init_agent()
|
|
288
|
+
if sub_tool.agent is not None:
|
|
289
|
+
sub_agents.append(sub_tool.agent)
|
|
290
|
+
return sub_agents
|
|
291
|
+
|
|
292
|
+
def _init_agent(self) -> None:
|
|
293
|
+
if self.agent is not None:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
model = self._init_model()
|
|
297
|
+
tools = self._convert_tools()
|
|
298
|
+
|
|
299
|
+
init_kwargs: Dict[str, Any] = {"tools": tools, "model": model}
|
|
300
|
+
# Give the agent an explicit name if supported
|
|
301
|
+
if isinstance(self.tool_config.get("name", None), str):
|
|
302
|
+
init_kwargs["name"] = self.tool_config["name"]
|
|
303
|
+
init_kwargs.update(self.agent_init_params or {})
|
|
304
|
+
|
|
305
|
+
# Sanitize unsupported kwargs based on agent type and common params
|
|
306
|
+
def _sanitize(agent_type: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
307
|
+
common_allowed = {
|
|
308
|
+
"tools",
|
|
309
|
+
"model",
|
|
310
|
+
"name",
|
|
311
|
+
"prompt_templates",
|
|
312
|
+
"planning_interval",
|
|
313
|
+
"stream_outputs",
|
|
314
|
+
"max_steps",
|
|
315
|
+
}
|
|
316
|
+
codeagent_allowed = common_allowed.union(
|
|
317
|
+
{
|
|
318
|
+
"add_base_tools",
|
|
319
|
+
"additional_authorized_imports",
|
|
320
|
+
"verbosity_level",
|
|
321
|
+
"executor_type",
|
|
322
|
+
"executor_kwargs",
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
toolcalling_allowed = common_allowed
|
|
326
|
+
agent_allowed = common_allowed
|
|
327
|
+
|
|
328
|
+
if agent_type == "CodeAgent":
|
|
329
|
+
allowed = codeagent_allowed
|
|
330
|
+
elif agent_type == "ToolCallingAgent":
|
|
331
|
+
allowed = toolcalling_allowed
|
|
332
|
+
elif agent_type == "Agent" or agent_type == "ManagedAgent":
|
|
333
|
+
allowed = agent_allowed
|
|
334
|
+
else:
|
|
335
|
+
allowed = common_allowed
|
|
336
|
+
|
|
337
|
+
# Drop unsupported keys (e.g., max_tool_threads)
|
|
338
|
+
return {k: v for k, v in params.items() if k in allowed}
|
|
339
|
+
|
|
340
|
+
init_kwargs = _sanitize(self.agent_type, init_kwargs)
|
|
341
|
+
|
|
342
|
+
# Construct agent by type
|
|
343
|
+
if self.agent_type == "ManagedAgent":
|
|
344
|
+
# Emulate a managed multi-agent system by wrapping sub-agents
|
|
345
|
+
# as smolagents Tools and composing a top-level CodeAgent.
|
|
346
|
+
CodeAgent = _safe_import("smolagents", "CodeAgent")
|
|
347
|
+
|
|
348
|
+
# Convert top-level available tools
|
|
349
|
+
top_tools = tools[:]
|
|
350
|
+
|
|
351
|
+
# Build sub-agents and wrap as tools
|
|
352
|
+
sub_agents = self._create_sub_agents(self.sub_agents_config)
|
|
353
|
+
|
|
354
|
+
# Dynamically create a Tool wrapper around a smolagents agent
|
|
355
|
+
Tool = _safe_import("smolagents", "Tool")
|
|
356
|
+
|
|
357
|
+
def _wrap_agent_as_tool(agent_obj, tool_name: str):
|
|
358
|
+
# smolagents expects class attributes on Tool subclasses
|
|
359
|
+
def _forward(self, task: str): # type: ignore[override]
|
|
360
|
+
return agent_obj.run(task)
|
|
361
|
+
|
|
362
|
+
attrs = {
|
|
363
|
+
"name": tool_name,
|
|
364
|
+
"description": f"Agent tool wrapper for {tool_name}",
|
|
365
|
+
"inputs": {
|
|
366
|
+
"task": {
|
|
367
|
+
"type": "string",
|
|
368
|
+
"description": "Task for sub-agent",
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
"output_type": "string",
|
|
372
|
+
"forward": _forward,
|
|
373
|
+
}
|
|
374
|
+
AgentToolCls = type(f"AgentTool_{tool_name}", (Tool,), attrs) # type: ignore[misc]
|
|
375
|
+
return AgentToolCls()
|
|
376
|
+
|
|
377
|
+
for idx, sub in enumerate(sub_agents):
|
|
378
|
+
name = getattr(sub, "name", f"sub_agent_{idx+1}")
|
|
379
|
+
top_tools.append(_wrap_agent_as_tool(sub, name))
|
|
380
|
+
|
|
381
|
+
# Construct the orchestrator agent (CodeAgent) with both native tools and agent-tools
|
|
382
|
+
orchestrator_kwargs = {"tools": top_tools, "model": model}
|
|
383
|
+
if isinstance(self.tool_config.get("name", None), str):
|
|
384
|
+
orchestrator_kwargs["name"] = self.tool_config["name"]
|
|
385
|
+
orchestrator_kwargs.update(self.agent_init_params or {})
|
|
386
|
+
orchestrator_kwargs = _sanitize("CodeAgent", orchestrator_kwargs)
|
|
387
|
+
self.agent = CodeAgent(**orchestrator_kwargs)
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
if self.agent_type == "CodeAgent":
|
|
391
|
+
CodeAgent = _safe_import("smolagents", "CodeAgent")
|
|
392
|
+
self.agent = CodeAgent(**init_kwargs)
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
if self.agent_type == "ToolCallingAgent":
|
|
396
|
+
ToolCallingAgent = _safe_import("smolagents", "ToolCallingAgent")
|
|
397
|
+
self.agent = ToolCallingAgent(**init_kwargs)
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
if self.agent_type == "Agent":
|
|
401
|
+
Agent = _safe_import("smolagents", "Agent")
|
|
402
|
+
self.agent = Agent(**init_kwargs)
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
raise ValueError(f"Unsupported agent type: {self.agent_type}")
|
|
406
|
+
|
|
407
|
+
# -------------------------
|
|
408
|
+
# Execution
|
|
409
|
+
# -------------------------
|
|
410
|
+
def run(
|
|
411
|
+
self,
|
|
412
|
+
arguments: Dict[str, Any],
|
|
413
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
414
|
+
**_: Any,
|
|
415
|
+
) -> Dict[str, Any]:
|
|
416
|
+
"""Execute the agent with optional streaming back into ToolUniverse.
|
|
417
|
+
|
|
418
|
+
Supports:
|
|
419
|
+
- Streaming output (when stream_callback is provided and agent.stream_outputs=True)
|
|
420
|
+
- Execution timeout (via agent_init_params.max_execution_time)
|
|
421
|
+
- Thread-safe stdout/stderr redirection
|
|
422
|
+
"""
|
|
423
|
+
import sys
|
|
424
|
+
import time
|
|
425
|
+
|
|
426
|
+
self._init_agent()
|
|
427
|
+
task = arguments.get("task", "")
|
|
428
|
+
if not task:
|
|
429
|
+
# Fallback to 'query' for agents whose parameter is named 'query'
|
|
430
|
+
task = arguments.get("query", "")
|
|
431
|
+
|
|
432
|
+
# Get max_execution_time from config (default: None = unlimited)
|
|
433
|
+
max_execution_time = self.agent_init_params.get("max_execution_time")
|
|
434
|
+
timeout_error: Optional[Exception] = None
|
|
435
|
+
execution_completed = threading.Event()
|
|
436
|
+
|
|
437
|
+
def _execute_with_timeout():
|
|
438
|
+
"""Inner function to execute agent.run with timeout protection."""
|
|
439
|
+
try:
|
|
440
|
+
# If streaming desired and agent supports streaming via stdout, capture and forward
|
|
441
|
+
wants_stream = bool(stream_callback) and bool(
|
|
442
|
+
getattr(self.agent, "stream_outputs", False)
|
|
443
|
+
)
|
|
444
|
+
if wants_stream:
|
|
445
|
+
|
|
446
|
+
class _StreamProxy:
|
|
447
|
+
def __init__(self, cb):
|
|
448
|
+
self._cb = cb
|
|
449
|
+
self._buf = ""
|
|
450
|
+
self._last_line = None
|
|
451
|
+
|
|
452
|
+
def write(self, s: str):
|
|
453
|
+
if not s:
|
|
454
|
+
return
|
|
455
|
+
self._buf += s
|
|
456
|
+
while "\n" in self._buf:
|
|
457
|
+
line, self._buf = self._buf.split("\n", 1)
|
|
458
|
+
if not line.strip():
|
|
459
|
+
continue
|
|
460
|
+
# Deduplicate consecutive identical lines
|
|
461
|
+
if line == self._last_line:
|
|
462
|
+
continue
|
|
463
|
+
self._last_line = line
|
|
464
|
+
self._cb(line + "\n")
|
|
465
|
+
|
|
466
|
+
def flush(self):
|
|
467
|
+
if self._buf.strip():
|
|
468
|
+
if self._buf != self._last_line:
|
|
469
|
+
self._cb(self._buf)
|
|
470
|
+
self._last_line = self._buf
|
|
471
|
+
self._buf = ""
|
|
472
|
+
|
|
473
|
+
# Use lock to protect stdout redirection (thread-safe)
|
|
474
|
+
with _STREAM_LOCK:
|
|
475
|
+
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
476
|
+
proxy = _StreamProxy(stream_callback)
|
|
477
|
+
sys.stdout = proxy # forward stdout only to avoid dupes
|
|
478
|
+
try:
|
|
479
|
+
result = self.agent.run(task)
|
|
480
|
+
finally:
|
|
481
|
+
sys.stdout = old_stdout
|
|
482
|
+
sys.stderr = old_stderr
|
|
483
|
+
|
|
484
|
+
execution_completed.set()
|
|
485
|
+
return result
|
|
486
|
+
|
|
487
|
+
# Non-streaming path (also protect with lock for consistency)
|
|
488
|
+
with _STREAM_LOCK:
|
|
489
|
+
result = self.agent.run(task)
|
|
490
|
+
execution_completed.set()
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
except Exception as e: # noqa: BLE001
|
|
494
|
+
execution_completed.set()
|
|
495
|
+
raise e
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
start_time = time.time()
|
|
499
|
+
|
|
500
|
+
# Execute with timeout if specified
|
|
501
|
+
if max_execution_time is not None and max_execution_time > 0:
|
|
502
|
+
import threading as th
|
|
503
|
+
|
|
504
|
+
result_container: List[Any] = []
|
|
505
|
+
exception_container: List[Exception] = []
|
|
506
|
+
|
|
507
|
+
def _worker():
|
|
508
|
+
try:
|
|
509
|
+
result_container.append(_execute_with_timeout())
|
|
510
|
+
except Exception as e: # noqa: BLE001
|
|
511
|
+
exception_container.append(e)
|
|
512
|
+
|
|
513
|
+
worker_thread = th.Thread(target=_worker, daemon=True)
|
|
514
|
+
worker_thread.start()
|
|
515
|
+
worker_thread.join(timeout=max_execution_time)
|
|
516
|
+
|
|
517
|
+
if worker_thread.is_alive():
|
|
518
|
+
# Timeout occurred
|
|
519
|
+
timeout_error = TimeoutError(
|
|
520
|
+
f"Agent execution exceeded maximum time limit of {max_execution_time} seconds. "
|
|
521
|
+
f"Task: {task[:100]}..."
|
|
522
|
+
)
|
|
523
|
+
if stream_callback:
|
|
524
|
+
stream_callback(f"\n[TIMEOUT] {timeout_error}\n")
|
|
525
|
+
return {
|
|
526
|
+
"output": None,
|
|
527
|
+
"success": False,
|
|
528
|
+
"error": str(timeout_error),
|
|
529
|
+
"error_type": "timeout",
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if exception_container:
|
|
533
|
+
raise exception_container[0]
|
|
534
|
+
|
|
535
|
+
result = result_container[0] if result_container else None
|
|
536
|
+
else:
|
|
537
|
+
# No timeout - direct execution
|
|
538
|
+
result = _execute_with_timeout()
|
|
539
|
+
|
|
540
|
+
elapsed_time = time.time() - start_time
|
|
541
|
+
return {
|
|
542
|
+
"output": result,
|
|
543
|
+
"success": True,
|
|
544
|
+
"execution_time": elapsed_time,
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
except Exception as e: # noqa: BLE001
|
|
548
|
+
if stream_callback:
|
|
549
|
+
stream_callback(f"\n[ERROR] {e}\n")
|
|
550
|
+
return {
|
|
551
|
+
"output": None,
|
|
552
|
+
"success": False,
|
|
553
|
+
"error": str(e),
|
|
554
|
+
"error_type": type(e).__name__,
|
|
555
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ArgumentDescriptionOptimizer
|
|
3
|
+
|
|
4
|
+
Optimizes the descriptions of tool arguments/parameters based on test case results and actual usa...
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional, Callable
|
|
8
|
+
from ._shared_client import get_shared_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ArgumentDescriptionOptimizer(
|
|
12
|
+
parameter_schema: str,
|
|
13
|
+
test_results: str,
|
|
14
|
+
*,
|
|
15
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
16
|
+
use_cache: bool = False,
|
|
17
|
+
validate: bool = True,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Optimizes the descriptions of tool arguments/parameters based on test case results and actual usa...
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
parameter_schema : str
|
|
25
|
+
JSON string of the original parameter schema with properties and descriptions.
|
|
26
|
+
test_results : str
|
|
27
|
+
A JSON string containing test case input/output pairs showing parameter usage.
|
|
28
|
+
stream_callback : Callable, optional
|
|
29
|
+
Callback for streaming output
|
|
30
|
+
use_cache : bool, default False
|
|
31
|
+
Enable caching
|
|
32
|
+
validate : bool, default True
|
|
33
|
+
Validate parameters
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
dict[str, Any]
|
|
38
|
+
"""
|
|
39
|
+
# Handle mutable defaults to avoid B006 linting error
|
|
40
|
+
|
|
41
|
+
return get_shared_client().run_one_function(
|
|
42
|
+
{
|
|
43
|
+
"name": "ArgumentDescriptionOptimizer",
|
|
44
|
+
"arguments": {
|
|
45
|
+
"parameter_schema": parameter_schema,
|
|
46
|
+
"test_results": test_results,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
stream_callback=stream_callback,
|
|
50
|
+
use_cache=use_cache,
|
|
51
|
+
validate=validate,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["ArgumentDescriptionOptimizer"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ENCODE_list_files
|
|
3
|
+
|
|
4
|
+
List ENCODE files with filters (file_format, output_type, assay). Use to programmatically retriev...
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional, Callable
|
|
8
|
+
from ._shared_client import get_shared_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ENCODE_list_files(
|
|
12
|
+
file_type: Optional[str] = None,
|
|
13
|
+
assay_title: Optional[str] = None,
|
|
14
|
+
limit: Optional[int] = 10,
|
|
15
|
+
*,
|
|
16
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
17
|
+
use_cache: bool = False,
|
|
18
|
+
validate: bool = True,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
List ENCODE files with filters (file_format, output_type, assay). Use to programmatically retriev...
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
file_type : str
|
|
26
|
+
File type filter (e.g., 'fastq', 'bam', 'bigWig').
|
|
27
|
+
assay_title : str
|
|
28
|
+
Assay filter (e.g., 'ChIP-seq').
|
|
29
|
+
limit : int
|
|
30
|
+
Max number of results (1–100).
|
|
31
|
+
stream_callback : Callable, optional
|
|
32
|
+
Callback for streaming output
|
|
33
|
+
use_cache : bool, default False
|
|
34
|
+
Enable caching
|
|
35
|
+
validate : bool, default True
|
|
36
|
+
Validate parameters
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
dict[str, Any]
|
|
41
|
+
"""
|
|
42
|
+
# Handle mutable defaults to avoid B006 linting error
|
|
43
|
+
|
|
44
|
+
return get_shared_client().run_one_function(
|
|
45
|
+
{
|
|
46
|
+
"name": "ENCODE_list_files",
|
|
47
|
+
"arguments": {
|
|
48
|
+
"file_type": file_type,
|
|
49
|
+
"assay_title": assay_title,
|
|
50
|
+
"limit": limit,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
stream_callback=stream_callback,
|
|
54
|
+
use_cache=use_cache,
|
|
55
|
+
validate=validate,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["ENCODE_list_files"]
|