devduck 0.1.0__py3-none-any.whl → 0.1.1766644714__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 devduck might be problematic. Click here for more details.
- devduck/__init__.py +1439 -483
- devduck/__main__.py +7 -0
- devduck/_version.py +34 -0
- devduck/agentcore_handler.py +76 -0
- devduck/test_redduck.py +0 -1
- devduck/tools/__init__.py +47 -0
- devduck/tools/_ambient_input.py +423 -0
- devduck/tools/_tray_app.py +530 -0
- devduck/tools/agentcore_agents.py +197 -0
- devduck/tools/agentcore_config.py +441 -0
- devduck/tools/agentcore_invoke.py +423 -0
- devduck/tools/agentcore_logs.py +320 -0
- devduck/tools/ambient.py +157 -0
- devduck/tools/create_subagent.py +659 -0
- devduck/tools/fetch_github_tool.py +201 -0
- devduck/tools/install_tools.py +409 -0
- devduck/tools/ipc.py +546 -0
- devduck/tools/mcp_server.py +600 -0
- devduck/tools/scraper.py +935 -0
- devduck/tools/speech_to_speech.py +850 -0
- devduck/tools/state_manager.py +292 -0
- devduck/tools/store_in_kb.py +187 -0
- devduck/tools/system_prompt.py +608 -0
- devduck/tools/tcp.py +263 -94
- devduck/tools/tray.py +247 -0
- devduck/tools/use_github.py +438 -0
- devduck/tools/websocket.py +498 -0
- devduck-0.1.1766644714.dist-info/METADATA +717 -0
- devduck-0.1.1766644714.dist-info/RECORD +33 -0
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/entry_points.txt +1 -0
- devduck-0.1.1766644714.dist-info/licenses/LICENSE +201 -0
- devduck/install.sh +0 -42
- devduck-0.1.0.dist-info/METADATA +0 -106
- devduck-0.1.0.dist-info/RECORD +0 -11
- devduck-0.1.0.dist-info/licenses/LICENSE +0 -21
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/WHEEL +0 -0
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""GitHub Tool Fetcher for Strands Agent.
|
|
2
|
+
|
|
3
|
+
This tool fetches Python tool files from GitHub repositories and loads them
|
|
4
|
+
as available tools in the current Strands agent. It combines HTTP fetching
|
|
5
|
+
with the load_tool functionality to enable dynamic tool loading from remote
|
|
6
|
+
GitHub repositories.
|
|
7
|
+
|
|
8
|
+
Usage with Strands Agents:
|
|
9
|
+
python
|
|
10
|
+
from strands import Agent
|
|
11
|
+
from tools.fetch_github_tool import fetch_github_tool
|
|
12
|
+
|
|
13
|
+
agent = Agent(tools=[fetch_github_tool])
|
|
14
|
+
|
|
15
|
+
# Fetch and load a tool from GitHub
|
|
16
|
+
agent.tool.fetch_github_tool(
|
|
17
|
+
github_url="https://github.com/owner/repo/blob/main/tools/my_tool.py",
|
|
18
|
+
tool_name="my_tool"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Now you can use the fetched tool
|
|
22
|
+
agent.tool.my_tool(param1="value")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Supported GitHub URL formats:
|
|
26
|
+
- https://github.com/owner/repo/blob/branch/path/to/file.py
|
|
27
|
+
- https://github.com/owner/repo/tree/branch/path/to/file.py
|
|
28
|
+
- https://raw.githubusercontent.com/owner/repo/branch/path/to/file.py
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import requests
|
|
37
|
+
from strands import tool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_github_url(github_url: str) -> dict[str, str]:
|
|
41
|
+
"""Parse GitHub URL to extract repository information.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
github_url: GitHub URL to the file
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary with owner, repo, branch, and file_path
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If URL format is not supported
|
|
51
|
+
"""
|
|
52
|
+
# Handle raw.githubusercontent.com URLs
|
|
53
|
+
raw_pattern = r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)"
|
|
54
|
+
raw_match = re.match(raw_pattern, github_url)
|
|
55
|
+
|
|
56
|
+
if raw_match:
|
|
57
|
+
owner, repo, branch, file_path = raw_match.groups()
|
|
58
|
+
return {"owner": owner, "repo": repo, "branch": branch, "file_path": file_path}
|
|
59
|
+
|
|
60
|
+
# Handle github.com/owner/repo/blob/branch/path URLs
|
|
61
|
+
blob_pattern = r"https://github\.com/([^/]+)/([^/]+)/(?:blob|tree)/([^/]+)/(.+)"
|
|
62
|
+
blob_match = re.match(blob_pattern, github_url)
|
|
63
|
+
|
|
64
|
+
if blob_match:
|
|
65
|
+
owner, repo, branch, file_path = blob_match.groups()
|
|
66
|
+
return {"owner": owner, "repo": repo, "branch": branch, "file_path": file_path}
|
|
67
|
+
|
|
68
|
+
raise ValueError(f"Unsupported GitHub URL format: {github_url}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_raw_url(owner: str, repo: str, branch: str, file_path: str) -> str:
|
|
72
|
+
"""Build GitHub raw content URL.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
owner: Repository owner
|
|
76
|
+
repo: Repository name
|
|
77
|
+
branch: Branch name
|
|
78
|
+
file_path: Path to file in repository
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Raw GitHub URL for the file
|
|
82
|
+
"""
|
|
83
|
+
return f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{file_path}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@tool
|
|
87
|
+
def fetch_github_tool(
|
|
88
|
+
github_url: str,
|
|
89
|
+
tool_name: str | None = None,
|
|
90
|
+
local_dir: str = "./github_tools",
|
|
91
|
+
agent: Any = None,
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
"""Fetch a Python tool file from GitHub and load it as a Strands tool.
|
|
94
|
+
|
|
95
|
+
This tool downloads Python files from GitHub repositories, saves them locally,
|
|
96
|
+
and registers them as available tools in the current Strands agent. It supports
|
|
97
|
+
various GitHub URL formats and automatically handles the conversion to raw content URLs.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
github_url: GitHub URL to the Python tool file. Supports formats like:
|
|
101
|
+
- https://github.com/owner/repo/blob/main/tools/my_tool.py
|
|
102
|
+
- https://github.com/owner/repo/tree/main/tools/my_tool.py
|
|
103
|
+
- https://raw.githubusercontent.com/owner/repo/main/tools/my_tool.py
|
|
104
|
+
tool_name: Name to register the tool under. If not provided, will extract
|
|
105
|
+
from the filename (e.g., "my_tool.py" becomes "my_tool")
|
|
106
|
+
local_dir: Local directory to save the fetched tool file. Defaults to "./github_tools"
|
|
107
|
+
agent: Agent instance (automatically provided by Strands)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dict containing status and response content:
|
|
111
|
+
{
|
|
112
|
+
"status": "success|error",
|
|
113
|
+
"content": [{"text": "Response message"}]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
# Fetch a tool from GitHub and load it
|
|
118
|
+
agent.tool.fetch_github_tool(
|
|
119
|
+
github_url="https://github.com/cagataycali/my-tools/blob/main/weather_tool.py",
|
|
120
|
+
tool_name="weather"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Tool name can be auto-detected from filename
|
|
124
|
+
agent.tool.fetch_github_tool(
|
|
125
|
+
github_url="https://github.com/cagataycali/my-tools/blob/main/calculator.py"
|
|
126
|
+
)
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
# Parse the GitHub URL
|
|
130
|
+
try:
|
|
131
|
+
url_info = parse_github_url(github_url)
|
|
132
|
+
except ValueError as e:
|
|
133
|
+
return {"status": "error", "content": [{"text": f"❌ {e!s}"}]}
|
|
134
|
+
|
|
135
|
+
# Extract tool name from filename if not provided
|
|
136
|
+
if not tool_name:
|
|
137
|
+
filename = os.path.basename(url_info["file_path"])
|
|
138
|
+
tool_name = os.path.splitext(filename)[0]
|
|
139
|
+
|
|
140
|
+
# Check if it's a Python file first (before making HTTP request)
|
|
141
|
+
if not url_info["file_path"].endswith(".py"):
|
|
142
|
+
return {
|
|
143
|
+
"status": "error",
|
|
144
|
+
"content": [
|
|
145
|
+
{
|
|
146
|
+
"text": f"❌ File must be a Python file (.py), got: {url_info['file_path']}"
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Build raw GitHub URL
|
|
152
|
+
raw_url = build_raw_url(
|
|
153
|
+
url_info["owner"],
|
|
154
|
+
url_info["repo"],
|
|
155
|
+
url_info["branch"],
|
|
156
|
+
url_info["file_path"],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Create local directory if it doesn't exist
|
|
160
|
+
local_path = Path(local_dir)
|
|
161
|
+
local_path.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
# Download the file
|
|
164
|
+
response = requests.get(raw_url, timeout=30)
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
# Save the file locally
|
|
167
|
+
local_file_path = local_path / f"{tool_name}.py"
|
|
168
|
+
with open(local_file_path, "w", encoding="utf-8") as f:
|
|
169
|
+
f.write(response.text)
|
|
170
|
+
|
|
171
|
+
# Load the tool using load_tool functionality
|
|
172
|
+
if agent and hasattr(agent, "tool_registry"):
|
|
173
|
+
agent.tool_registry.load_tool_from_filepath(
|
|
174
|
+
tool_name=tool_name, tool_path=str(local_file_path)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
success_message = f"""✅ Successfully fetched and loaded GitHub tool!
|
|
178
|
+
|
|
179
|
+
📂 **Source:** {github_url}
|
|
180
|
+
🏷️ **Tool Name:** {tool_name}
|
|
181
|
+
💾 **Local Path:** {local_file_path}
|
|
182
|
+
🔧 **Status:** Ready to use
|
|
183
|
+
|
|
184
|
+
You can now use the tool with: agent.tool.{tool_name}(...)"""
|
|
185
|
+
|
|
186
|
+
return {"status": "success", "content": [{"text": success_message}]}
|
|
187
|
+
else:
|
|
188
|
+
return {
|
|
189
|
+
"status": "error",
|
|
190
|
+
"content": [
|
|
191
|
+
{"text": "❌ Agent instance not available for tool registration"}
|
|
192
|
+
],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
except requests.RequestException as e:
|
|
196
|
+
return {
|
|
197
|
+
"status": "error",
|
|
198
|
+
"content": [{"text": f"❌ Failed to download file from GitHub: {e!s}"}],
|
|
199
|
+
}
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return {"status": "error", "content": [{"text": f"❌ Unexpected error: {e!s}"}]}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Dynamic Tool Installation for DevDuck.
|
|
2
|
+
|
|
3
|
+
Install and load tools from any Python package at runtime, expanding DevDuck's
|
|
4
|
+
capabilities on-the-fly without requiring restarts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import logging
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from strands import tool
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tool
|
|
19
|
+
def install_tools(
|
|
20
|
+
action: str,
|
|
21
|
+
package: Optional[str] = None,
|
|
22
|
+
module: Optional[str] = None,
|
|
23
|
+
tool_names: Optional[List[str]] = None,
|
|
24
|
+
agent: Any = None,
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
"""Install and load tools from Python packages dynamically.
|
|
27
|
+
|
|
28
|
+
This tool allows DevDuck to expand its capabilities by installing Python packages
|
|
29
|
+
and loading their tools into the agent's registry at runtime.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
action: Action to perform - "install", "load", "install_and_load", "list_loaded", "list_available"
|
|
33
|
+
package: Python package to install (e.g., "strands-agents-tools", "strands-fun-tools")
|
|
34
|
+
module: Module to import tools from (e.g., "strands_tools", "strands_fun_tools")
|
|
35
|
+
tool_names: Optional list of specific tools to load. If None, loads all available tools
|
|
36
|
+
agent: Parent agent instance (auto-injected by Strands framework)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Result dictionary with status and content
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
# List available tools in a package (without loading)
|
|
43
|
+
install_tools(
|
|
44
|
+
action="list_available",
|
|
45
|
+
package="strands-fun-tools",
|
|
46
|
+
module="strands_fun_tools"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Install and load all tools from strands-agents-tools
|
|
50
|
+
install_tools(
|
|
51
|
+
action="install_and_load",
|
|
52
|
+
package="strands-agents-tools",
|
|
53
|
+
module="strands_tools"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Install and load specific tools
|
|
57
|
+
install_tools(
|
|
58
|
+
action="install_and_load",
|
|
59
|
+
package="strands-fun-tools",
|
|
60
|
+
module="strands_fun_tools",
|
|
61
|
+
tool_names=["clipboard", "cursor", "bluetooth"]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Load tools from already installed package
|
|
65
|
+
install_tools(
|
|
66
|
+
action="load",
|
|
67
|
+
module="strands_tools",
|
|
68
|
+
tool_names=["shell", "calculator"]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# List currently loaded tools
|
|
72
|
+
install_tools(action="list_loaded")
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
if action == "install":
|
|
76
|
+
return _install_package(package)
|
|
77
|
+
elif action == "load":
|
|
78
|
+
return _load_tools_from_module(module, tool_names, agent)
|
|
79
|
+
elif action == "install_and_load":
|
|
80
|
+
# Install first
|
|
81
|
+
install_result = _install_package(package)
|
|
82
|
+
if install_result["status"] == "error":
|
|
83
|
+
return install_result
|
|
84
|
+
|
|
85
|
+
# Then load
|
|
86
|
+
return _load_tools_from_module(module, tool_names, agent)
|
|
87
|
+
elif action == "list_loaded":
|
|
88
|
+
return _list_loaded_tools(agent)
|
|
89
|
+
elif action == "list_available":
|
|
90
|
+
return _list_available_tools(package, module)
|
|
91
|
+
else:
|
|
92
|
+
return {
|
|
93
|
+
"status": "error",
|
|
94
|
+
"content": [
|
|
95
|
+
{
|
|
96
|
+
"text": f"❌ Unknown action: {action}\n\n"
|
|
97
|
+
f"Valid actions: install, load, install_and_load, list_loaded, list_available"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.exception("Error in install_tools")
|
|
104
|
+
return {"status": "error", "content": [{"text": f"❌ Error: {str(e)}"}]}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _install_package(package: str) -> Dict[str, Any]:
|
|
108
|
+
"""Install a Python package using pip."""
|
|
109
|
+
if not package:
|
|
110
|
+
return {
|
|
111
|
+
"status": "error",
|
|
112
|
+
"content": [
|
|
113
|
+
{"text": "❌ package parameter is required for install action"}
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
logger.info(f"Installing package: {package}")
|
|
119
|
+
|
|
120
|
+
# Use subprocess to install the package
|
|
121
|
+
result = subprocess.run(
|
|
122
|
+
[sys.executable, "-m", "pip", "install", package],
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
timeout=300, # 5 minute timeout
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if result.returncode != 0:
|
|
129
|
+
return {
|
|
130
|
+
"status": "error",
|
|
131
|
+
"content": [
|
|
132
|
+
{"text": f"❌ Failed to install {package}:\n{result.stderr}"}
|
|
133
|
+
],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
logger.info(f"Successfully installed: {package}")
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"status": "success",
|
|
140
|
+
"content": [{"text": f"✅ Successfully installed package: {package}"}],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
except subprocess.TimeoutExpired:
|
|
144
|
+
return {
|
|
145
|
+
"status": "error",
|
|
146
|
+
"content": [
|
|
147
|
+
{"text": f"❌ Installation of {package} timed out (>5 minutes)"}
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.exception(f"Error installing package {package}")
|
|
152
|
+
return {
|
|
153
|
+
"status": "error",
|
|
154
|
+
"content": [{"text": f"❌ Failed to install {package}: {str(e)}"}],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load_tools_from_module(
|
|
159
|
+
module: str, tool_names: Optional[List[str]], agent: Any
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""Load tools from a Python module into the agent's registry."""
|
|
162
|
+
if not module:
|
|
163
|
+
return {
|
|
164
|
+
"status": "error",
|
|
165
|
+
"content": [{"text": "❌ module parameter is required for load action"}],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if not agent:
|
|
169
|
+
return {
|
|
170
|
+
"status": "error",
|
|
171
|
+
"content": [{"text": "❌ agent instance is required for load action"}],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if not hasattr(agent, "tool_registry") or not hasattr(
|
|
175
|
+
agent.tool_registry, "register_tool"
|
|
176
|
+
):
|
|
177
|
+
return {
|
|
178
|
+
"status": "error",
|
|
179
|
+
"content": [{"text": "❌ Agent does not have a tool registry"}],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Import the module
|
|
184
|
+
logger.info(f"Importing module: {module}")
|
|
185
|
+
imported_module = importlib.import_module(module)
|
|
186
|
+
|
|
187
|
+
# Get all tool objects from the module
|
|
188
|
+
available_tools = {}
|
|
189
|
+
for attr_name in dir(imported_module):
|
|
190
|
+
attr = getattr(imported_module, attr_name)
|
|
191
|
+
# Check if it's a tool (has tool_name and tool_spec attributes)
|
|
192
|
+
if hasattr(attr, "tool_name") and hasattr(attr, "tool_spec"):
|
|
193
|
+
available_tools[attr.tool_name] = attr
|
|
194
|
+
|
|
195
|
+
if not available_tools:
|
|
196
|
+
return {
|
|
197
|
+
"status": "error",
|
|
198
|
+
"content": [{"text": f"❌ No tools found in module: {module}"}],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Filter tools if specific ones requested
|
|
202
|
+
if tool_names:
|
|
203
|
+
tools_to_load = {
|
|
204
|
+
name: tool
|
|
205
|
+
for name, tool in available_tools.items()
|
|
206
|
+
if name in tool_names
|
|
207
|
+
}
|
|
208
|
+
missing_tools = set(tool_names) - set(tools_to_load.keys())
|
|
209
|
+
if missing_tools:
|
|
210
|
+
return {
|
|
211
|
+
"status": "error",
|
|
212
|
+
"content": [
|
|
213
|
+
{
|
|
214
|
+
"text": f"❌ Requested tools not found: {', '.join(missing_tools)}\n\n"
|
|
215
|
+
f"Available tools: {', '.join(available_tools.keys())}"
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
else:
|
|
220
|
+
tools_to_load = available_tools
|
|
221
|
+
|
|
222
|
+
# Load tools into agent registry
|
|
223
|
+
loaded_tools = []
|
|
224
|
+
skipped_tools = []
|
|
225
|
+
|
|
226
|
+
for tool_name, tool_obj in tools_to_load.items():
|
|
227
|
+
try:
|
|
228
|
+
# Check if tool already exists
|
|
229
|
+
existing_tools = agent.tool_registry.get_all_tools_config()
|
|
230
|
+
if tool_name in existing_tools:
|
|
231
|
+
skipped_tools.append(f"{tool_name} (already loaded)")
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# Register the tool
|
|
235
|
+
agent.tool_registry.register_tool(tool_obj)
|
|
236
|
+
loaded_tools.append(tool_name)
|
|
237
|
+
logger.info(f"Loaded tool: {tool_name}")
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
skipped_tools.append(f"{tool_name} (error: {str(e)})")
|
|
241
|
+
logger.error(f"Failed to load tool {tool_name}: {e}")
|
|
242
|
+
|
|
243
|
+
# Build result message
|
|
244
|
+
result_lines = [f"✅ Loaded {len(loaded_tools)} tools from {module}"]
|
|
245
|
+
|
|
246
|
+
if loaded_tools:
|
|
247
|
+
result_lines.append(f"\n📦 Loaded tools:")
|
|
248
|
+
for tool_name in loaded_tools:
|
|
249
|
+
result_lines.append(f" • {tool_name}")
|
|
250
|
+
|
|
251
|
+
if skipped_tools:
|
|
252
|
+
result_lines.append(f"\n⚠️ Skipped tools:")
|
|
253
|
+
for skip_msg in skipped_tools:
|
|
254
|
+
result_lines.append(f" • {skip_msg}")
|
|
255
|
+
|
|
256
|
+
result_lines.append(
|
|
257
|
+
f"\n🔧 Total available tools: {len(existing_tools) + len(loaded_tools)}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
261
|
+
|
|
262
|
+
except ImportError as e:
|
|
263
|
+
logger.exception(f"Failed to import module {module}")
|
|
264
|
+
return {
|
|
265
|
+
"status": "error",
|
|
266
|
+
"content": [
|
|
267
|
+
{
|
|
268
|
+
"text": f"❌ Failed to import module {module}: {str(e)}\n\n"
|
|
269
|
+
f"Make sure the package is installed first using action='install'"
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.exception(f"Error loading tools from {module}")
|
|
275
|
+
return {
|
|
276
|
+
"status": "error",
|
|
277
|
+
"content": [{"text": f"❌ Failed to load tools: {str(e)}"}],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _list_loaded_tools(agent: Any) -> Dict[str, Any]:
|
|
282
|
+
"""List all currently loaded tools in the agent."""
|
|
283
|
+
if not agent:
|
|
284
|
+
return {
|
|
285
|
+
"status": "error",
|
|
286
|
+
"content": [{"text": "❌ agent instance is required"}],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if not hasattr(agent, "tool_registry"):
|
|
290
|
+
return {
|
|
291
|
+
"status": "error",
|
|
292
|
+
"content": [{"text": "❌ Agent does not have a tool registry"}],
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
all_tools = agent.tool_registry.get_all_tools_config()
|
|
297
|
+
|
|
298
|
+
result_lines = [f"🔧 **Loaded Tools ({len(all_tools)})**\n"]
|
|
299
|
+
|
|
300
|
+
# Group tools by category (if available)
|
|
301
|
+
for tool_name, tool_spec in sorted(all_tools.items()):
|
|
302
|
+
description = tool_spec.get("description", "No description available")
|
|
303
|
+
# Truncate long descriptions
|
|
304
|
+
if len(description) > 100:
|
|
305
|
+
description = description[:97] + "..."
|
|
306
|
+
|
|
307
|
+
result_lines.append(f"**{tool_name}**")
|
|
308
|
+
result_lines.append(f" {description}\n")
|
|
309
|
+
|
|
310
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.exception("Error listing loaded tools")
|
|
314
|
+
return {
|
|
315
|
+
"status": "error",
|
|
316
|
+
"content": [{"text": f"❌ Failed to list tools: {str(e)}"}],
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _list_available_tools(package: Optional[str], module: str) -> Dict[str, Any]:
|
|
321
|
+
"""List available tools in a package without loading them."""
|
|
322
|
+
if not module:
|
|
323
|
+
return {
|
|
324
|
+
"status": "error",
|
|
325
|
+
"content": [
|
|
326
|
+
{"text": "❌ module parameter is required for list_available action"}
|
|
327
|
+
],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# Try to import the module
|
|
332
|
+
try:
|
|
333
|
+
imported_module = importlib.import_module(module)
|
|
334
|
+
logger.info(f"Module {module} already installed")
|
|
335
|
+
except ImportError:
|
|
336
|
+
# Module not installed - try to install package first
|
|
337
|
+
if not package:
|
|
338
|
+
return {
|
|
339
|
+
"status": "error",
|
|
340
|
+
"content": [
|
|
341
|
+
{
|
|
342
|
+
"text": f"❌ Module {module} not found and no package specified to install.\n\n"
|
|
343
|
+
f"Please provide the 'package' parameter to install first."
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logger.info(f"Module {module} not found, installing package {package}")
|
|
349
|
+
install_result = _install_package(package)
|
|
350
|
+
if install_result["status"] == "error":
|
|
351
|
+
return install_result
|
|
352
|
+
|
|
353
|
+
# Try importing again after installation
|
|
354
|
+
try:
|
|
355
|
+
imported_module = importlib.import_module(module)
|
|
356
|
+
except ImportError as e:
|
|
357
|
+
return {
|
|
358
|
+
"status": "error",
|
|
359
|
+
"content": [
|
|
360
|
+
{
|
|
361
|
+
"text": f"❌ Failed to import {module} even after installing {package}: {str(e)}"
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Discover tools in the module
|
|
367
|
+
available_tools = {}
|
|
368
|
+
for attr_name in dir(imported_module):
|
|
369
|
+
attr = getattr(imported_module, attr_name)
|
|
370
|
+
# Check if it's a tool (has tool_name and tool_spec attributes)
|
|
371
|
+
if hasattr(attr, "tool_name") and hasattr(attr, "tool_spec"):
|
|
372
|
+
tool_spec = attr.tool_spec
|
|
373
|
+
description = tool_spec.get("description", "No description available")
|
|
374
|
+
available_tools[attr.tool_name] = description
|
|
375
|
+
|
|
376
|
+
if not available_tools:
|
|
377
|
+
return {
|
|
378
|
+
"status": "success",
|
|
379
|
+
"content": [{"text": f"⚠️ No tools found in module: {module}"}],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Build result message
|
|
383
|
+
result_lines = [
|
|
384
|
+
f"📦 **Available Tools in {module} ({len(available_tools)})**\n"
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
for tool_name, description in sorted(available_tools.items()):
|
|
388
|
+
# Truncate long descriptions
|
|
389
|
+
if len(description) > 100:
|
|
390
|
+
description = description[:97] + "..."
|
|
391
|
+
|
|
392
|
+
result_lines.append(f"**{tool_name}**")
|
|
393
|
+
result_lines.append(f" {description}\n")
|
|
394
|
+
|
|
395
|
+
result_lines.append(f"\n💡 To load these tools, use:")
|
|
396
|
+
result_lines.append(f" install_tools(action='load', module='{module}')")
|
|
397
|
+
result_lines.append(f" # Or load specific tools:")
|
|
398
|
+
result_lines.append(
|
|
399
|
+
f" install_tools(action='load', module='{module}', tool_names=['tool1', 'tool2'])"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.exception(f"Error listing available tools from {module}")
|
|
406
|
+
return {
|
|
407
|
+
"status": "error",
|
|
408
|
+
"content": [{"text": f"❌ Failed to list available tools: {str(e)}"}],
|
|
409
|
+
}
|