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/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point for python -m devduck or python devduck."""
3
+
4
+ from devduck import cli
5
+
6
+ if __name__ == "__main__":
7
+ cli()
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,7 @@
1
+ """DevDuck tools package."""
2
+
3
+ from .tcp import tcp
4
+ from .mcp_server import mcp_server
5
+ from .install_tools import install_tools
6
+
7
+ __all__ = ["tcp", "mcp_server", "install_tools"]
@@ -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
+ }