devduck 0.1.0__py3-none-any.whl → 0.2.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 devduck might be problematic. Click here for more details.
- devduck/__init__.py +546 -91
- devduck/__main__.py +7 -0
- devduck/_version.py +34 -0
- devduck/tools/__init__.py +7 -0
- devduck/tools/install_tools.py +308 -0
- devduck/tools/mcp_server.py +572 -0
- devduck/tools/tcp.py +263 -93
- devduck/tools/websocket.py +492 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/METADATA +48 -11
- devduck-0.2.0.dist-info/RECORD +16 -0
- devduck-0.1.0.dist-info/RECORD +0 -11
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/WHEEL +0 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/entry_points.txt +0 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/top_level.txt +0 -0
devduck/__main__.py
ADDED
devduck/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
devduck/tools/__init__.py
CHANGED
|
@@ -0,0 +1,308 @@
|
|
|
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"
|
|
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
|
+
# Install and load all tools from strands-agents-tools
|
|
43
|
+
install_tools(
|
|
44
|
+
action="install_and_load",
|
|
45
|
+
package="strands-agents-tools",
|
|
46
|
+
module="strands_tools"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Install and load specific tools
|
|
50
|
+
install_tools(
|
|
51
|
+
action="install_and_load",
|
|
52
|
+
package="strands-fun-tools",
|
|
53
|
+
module="strands_fun_tools",
|
|
54
|
+
tool_names=["clipboard", "cursor", "bluetooth"]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Load tools from already installed package
|
|
58
|
+
install_tools(
|
|
59
|
+
action="load",
|
|
60
|
+
module="strands_tools",
|
|
61
|
+
tool_names=["shell", "calculator"]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# List currently loaded tools
|
|
65
|
+
install_tools(action="list_loaded")
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
if action == "install":
|
|
69
|
+
return _install_package(package)
|
|
70
|
+
elif action == "load":
|
|
71
|
+
return _load_tools_from_module(module, tool_names, agent)
|
|
72
|
+
elif action == "install_and_load":
|
|
73
|
+
# Install first
|
|
74
|
+
install_result = _install_package(package)
|
|
75
|
+
if install_result["status"] == "error":
|
|
76
|
+
return install_result
|
|
77
|
+
|
|
78
|
+
# Then load
|
|
79
|
+
return _load_tools_from_module(module, tool_names, agent)
|
|
80
|
+
elif action == "list_loaded":
|
|
81
|
+
return _list_loaded_tools(agent)
|
|
82
|
+
else:
|
|
83
|
+
return {
|
|
84
|
+
"status": "error",
|
|
85
|
+
"content": [
|
|
86
|
+
{
|
|
87
|
+
"text": f"❌ Unknown action: {action}\n\n"
|
|
88
|
+
f"Valid actions: install, load, install_and_load, list_loaded"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.exception("Error in install_tools")
|
|
95
|
+
return {"status": "error", "content": [{"text": f"❌ Error: {str(e)}"}]}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _install_package(package: str) -> Dict[str, Any]:
|
|
99
|
+
"""Install a Python package using pip."""
|
|
100
|
+
if not package:
|
|
101
|
+
return {
|
|
102
|
+
"status": "error",
|
|
103
|
+
"content": [
|
|
104
|
+
{"text": "❌ package parameter is required for install action"}
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
logger.info(f"Installing package: {package}")
|
|
110
|
+
|
|
111
|
+
# Use subprocess to install the package
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
[sys.executable, "-m", "pip", "install", package],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
timeout=300, # 5 minute timeout
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if result.returncode != 0:
|
|
120
|
+
return {
|
|
121
|
+
"status": "error",
|
|
122
|
+
"content": [
|
|
123
|
+
{"text": f"❌ Failed to install {package}:\n{result.stderr}"}
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
logger.info(f"Successfully installed: {package}")
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"status": "success",
|
|
131
|
+
"content": [{"text": f"✅ Successfully installed package: {package}"}],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
except subprocess.TimeoutExpired:
|
|
135
|
+
return {
|
|
136
|
+
"status": "error",
|
|
137
|
+
"content": [
|
|
138
|
+
{"text": f"❌ Installation of {package} timed out (>5 minutes)"}
|
|
139
|
+
],
|
|
140
|
+
}
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.exception(f"Error installing package {package}")
|
|
143
|
+
return {
|
|
144
|
+
"status": "error",
|
|
145
|
+
"content": [{"text": f"❌ Failed to install {package}: {str(e)}"}],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _load_tools_from_module(
|
|
150
|
+
module: str, tool_names: Optional[List[str]], agent: Any
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Load tools from a Python module into the agent's registry."""
|
|
153
|
+
if not module:
|
|
154
|
+
return {
|
|
155
|
+
"status": "error",
|
|
156
|
+
"content": [{"text": "❌ module parameter is required for load action"}],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if not agent:
|
|
160
|
+
return {
|
|
161
|
+
"status": "error",
|
|
162
|
+
"content": [{"text": "❌ agent instance is required for load action"}],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if not hasattr(agent, "tool_registry") or not hasattr(
|
|
166
|
+
agent.tool_registry, "register_tool"
|
|
167
|
+
):
|
|
168
|
+
return {
|
|
169
|
+
"status": "error",
|
|
170
|
+
"content": [{"text": "❌ Agent does not have a tool registry"}],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Import the module
|
|
175
|
+
logger.info(f"Importing module: {module}")
|
|
176
|
+
imported_module = importlib.import_module(module)
|
|
177
|
+
|
|
178
|
+
# Get all tool objects from the module
|
|
179
|
+
available_tools = {}
|
|
180
|
+
for attr_name in dir(imported_module):
|
|
181
|
+
attr = getattr(imported_module, attr_name)
|
|
182
|
+
# Check if it's a tool (has tool_name and tool_spec attributes)
|
|
183
|
+
if hasattr(attr, "tool_name") and hasattr(attr, "tool_spec"):
|
|
184
|
+
available_tools[attr.tool_name] = attr
|
|
185
|
+
|
|
186
|
+
if not available_tools:
|
|
187
|
+
return {
|
|
188
|
+
"status": "error",
|
|
189
|
+
"content": [{"text": f"❌ No tools found in module: {module}"}],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Filter tools if specific ones requested
|
|
193
|
+
if tool_names:
|
|
194
|
+
tools_to_load = {
|
|
195
|
+
name: tool
|
|
196
|
+
for name, tool in available_tools.items()
|
|
197
|
+
if name in tool_names
|
|
198
|
+
}
|
|
199
|
+
missing_tools = set(tool_names) - set(tools_to_load.keys())
|
|
200
|
+
if missing_tools:
|
|
201
|
+
return {
|
|
202
|
+
"status": "error",
|
|
203
|
+
"content": [
|
|
204
|
+
{
|
|
205
|
+
"text": f"❌ Requested tools not found: {', '.join(missing_tools)}\n\n"
|
|
206
|
+
f"Available tools: {', '.join(available_tools.keys())}"
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
}
|
|
210
|
+
else:
|
|
211
|
+
tools_to_load = available_tools
|
|
212
|
+
|
|
213
|
+
# Load tools into agent registry
|
|
214
|
+
loaded_tools = []
|
|
215
|
+
skipped_tools = []
|
|
216
|
+
|
|
217
|
+
for tool_name, tool_obj in tools_to_load.items():
|
|
218
|
+
try:
|
|
219
|
+
# Check if tool already exists
|
|
220
|
+
existing_tools = agent.tool_registry.get_all_tools_config()
|
|
221
|
+
if tool_name in existing_tools:
|
|
222
|
+
skipped_tools.append(f"{tool_name} (already loaded)")
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# Register the tool
|
|
226
|
+
agent.tool_registry.register_tool(tool_obj)
|
|
227
|
+
loaded_tools.append(tool_name)
|
|
228
|
+
logger.info(f"Loaded tool: {tool_name}")
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
skipped_tools.append(f"{tool_name} (error: {str(e)})")
|
|
232
|
+
logger.error(f"Failed to load tool {tool_name}: {e}")
|
|
233
|
+
|
|
234
|
+
# Build result message
|
|
235
|
+
result_lines = [f"✅ Loaded {len(loaded_tools)} tools from {module}"]
|
|
236
|
+
|
|
237
|
+
if loaded_tools:
|
|
238
|
+
result_lines.append(f"\n📦 Loaded tools:")
|
|
239
|
+
for tool_name in loaded_tools:
|
|
240
|
+
result_lines.append(f" • {tool_name}")
|
|
241
|
+
|
|
242
|
+
if skipped_tools:
|
|
243
|
+
result_lines.append(f"\n⚠️ Skipped tools:")
|
|
244
|
+
for skip_msg in skipped_tools:
|
|
245
|
+
result_lines.append(f" • {skip_msg}")
|
|
246
|
+
|
|
247
|
+
result_lines.append(
|
|
248
|
+
f"\n🔧 Total available tools: {len(existing_tools) + len(loaded_tools)}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
252
|
+
|
|
253
|
+
except ImportError as e:
|
|
254
|
+
logger.exception(f"Failed to import module {module}")
|
|
255
|
+
return {
|
|
256
|
+
"status": "error",
|
|
257
|
+
"content": [
|
|
258
|
+
{
|
|
259
|
+
"text": f"❌ Failed to import module {module}: {str(e)}\n\n"
|
|
260
|
+
f"Make sure the package is installed first using action='install'"
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
}
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.exception(f"Error loading tools from {module}")
|
|
266
|
+
return {
|
|
267
|
+
"status": "error",
|
|
268
|
+
"content": [{"text": f"❌ Failed to load tools: {str(e)}"}],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _list_loaded_tools(agent: Any) -> Dict[str, Any]:
|
|
273
|
+
"""List all currently loaded tools in the agent."""
|
|
274
|
+
if not agent:
|
|
275
|
+
return {
|
|
276
|
+
"status": "error",
|
|
277
|
+
"content": [{"text": "❌ agent instance is required"}],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if not hasattr(agent, "tool_registry"):
|
|
281
|
+
return {
|
|
282
|
+
"status": "error",
|
|
283
|
+
"content": [{"text": "❌ Agent does not have a tool registry"}],
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
all_tools = agent.tool_registry.get_all_tools_config()
|
|
288
|
+
|
|
289
|
+
result_lines = [f"🔧 **Loaded Tools ({len(all_tools)})**\n"]
|
|
290
|
+
|
|
291
|
+
# Group tools by category (if available)
|
|
292
|
+
for tool_name, tool_spec in sorted(all_tools.items()):
|
|
293
|
+
description = tool_spec.get("description", "No description available")
|
|
294
|
+
# Truncate long descriptions
|
|
295
|
+
if len(description) > 100:
|
|
296
|
+
description = description[:97] + "..."
|
|
297
|
+
|
|
298
|
+
result_lines.append(f"**{tool_name}**")
|
|
299
|
+
result_lines.append(f" {description}\n")
|
|
300
|
+
|
|
301
|
+
return {"status": "success", "content": [{"text": "\n".join(result_lines)}]}
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.exception("Error listing loaded tools")
|
|
305
|
+
return {
|
|
306
|
+
"status": "error",
|
|
307
|
+
"content": [{"text": f"❌ Failed to list tools: {str(e)}"}],
|
|
308
|
+
}
|