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.

Files changed (37) hide show
  1. devduck/__init__.py +1439 -483
  2. devduck/__main__.py +7 -0
  3. devduck/_version.py +34 -0
  4. devduck/agentcore_handler.py +76 -0
  5. devduck/test_redduck.py +0 -1
  6. devduck/tools/__init__.py +47 -0
  7. devduck/tools/_ambient_input.py +423 -0
  8. devduck/tools/_tray_app.py +530 -0
  9. devduck/tools/agentcore_agents.py +197 -0
  10. devduck/tools/agentcore_config.py +441 -0
  11. devduck/tools/agentcore_invoke.py +423 -0
  12. devduck/tools/agentcore_logs.py +320 -0
  13. devduck/tools/ambient.py +157 -0
  14. devduck/tools/create_subagent.py +659 -0
  15. devduck/tools/fetch_github_tool.py +201 -0
  16. devduck/tools/install_tools.py +409 -0
  17. devduck/tools/ipc.py +546 -0
  18. devduck/tools/mcp_server.py +600 -0
  19. devduck/tools/scraper.py +935 -0
  20. devduck/tools/speech_to_speech.py +850 -0
  21. devduck/tools/state_manager.py +292 -0
  22. devduck/tools/store_in_kb.py +187 -0
  23. devduck/tools/system_prompt.py +608 -0
  24. devduck/tools/tcp.py +263 -94
  25. devduck/tools/tray.py +247 -0
  26. devduck/tools/use_github.py +438 -0
  27. devduck/tools/websocket.py +498 -0
  28. devduck-0.1.1766644714.dist-info/METADATA +717 -0
  29. devduck-0.1.1766644714.dist-info/RECORD +33 -0
  30. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/entry_points.txt +1 -0
  31. devduck-0.1.1766644714.dist-info/licenses/LICENSE +201 -0
  32. devduck/install.sh +0 -42
  33. devduck-0.1.0.dist-info/METADATA +0 -106
  34. devduck-0.1.0.dist-info/RECORD +0 -11
  35. devduck-0.1.0.dist-info/licenses/LICENSE +0 -21
  36. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/WHEEL +0 -0
  37. {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
+ }