devduck 1.1.0__py3-none-any.whl → 1.1.4__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 +99 -15
- devduck/_version.py +2 -2
- devduck/tools/__init__.py +41 -49
- devduck/tools/fetch_github_tool.py +201 -0
- devduck/tools/scraper.py +935 -0
- devduck/tools/speech_to_speech.py +109 -9
- devduck/tools/system_prompt.py +276 -153
- {devduck-1.1.0.dist-info → devduck-1.1.4.dist-info}/METADATA +2 -1
- {devduck-1.1.0.dist-info → devduck-1.1.4.dist-info}/RECORD +13 -11
- {devduck-1.1.0.dist-info → devduck-1.1.4.dist-info}/WHEEL +0 -0
- {devduck-1.1.0.dist-info → devduck-1.1.4.dist-info}/entry_points.txt +0 -0
- {devduck-1.1.0.dist-info → devduck-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {devduck-1.1.0.dist-info → devduck-1.1.4.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -3,19 +3,31 @@
|
|
|
3
3
|
🦆 devduck - extreme minimalist self-adapting agent
|
|
4
4
|
one file. self-healing. runtime dependencies. adaptive.
|
|
5
5
|
"""
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
import subprocess
|
|
8
|
-
import
|
|
9
|
+
import threading
|
|
9
10
|
import platform
|
|
10
11
|
import socket
|
|
11
12
|
import logging
|
|
12
13
|
import tempfile
|
|
13
14
|
import time
|
|
14
15
|
import warnings
|
|
16
|
+
import json
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
from datetime import datetime
|
|
17
19
|
from typing import Dict, Any
|
|
18
20
|
from logging.handlers import RotatingFileHandler
|
|
21
|
+
from strands import Agent, tool
|
|
22
|
+
|
|
23
|
+
# Import system prompt helper for loading prompts from files
|
|
24
|
+
try:
|
|
25
|
+
from devduck.tools.system_prompt import _get_system_prompt
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Fallback if tools module not available yet
|
|
28
|
+
def _get_system_prompt(repository=None, variable_name="SYSTEM_PROMPT"):
|
|
29
|
+
return os.getenv(variable_name, "")
|
|
30
|
+
|
|
19
31
|
|
|
20
32
|
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
|
|
21
33
|
warnings.filterwarnings("ignore", message=".*cache_prompt is deprecated.*")
|
|
@@ -594,7 +606,7 @@ class DevDuck:
|
|
|
594
606
|
servers = {
|
|
595
607
|
"tcp": {
|
|
596
608
|
"port": int(os.getenv("DEVDUCK_TCP_PORT", "9999")),
|
|
597
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_TCP", "
|
|
609
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_TCP", "false").lower()
|
|
598
610
|
== "true",
|
|
599
611
|
},
|
|
600
612
|
"ws": {
|
|
@@ -604,25 +616,93 @@ class DevDuck:
|
|
|
604
616
|
},
|
|
605
617
|
"mcp": {
|
|
606
618
|
"port": int(os.getenv("DEVDUCK_MCP_PORT", "8000")),
|
|
607
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_MCP", "
|
|
619
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_MCP", "false").lower()
|
|
608
620
|
== "true",
|
|
609
621
|
},
|
|
610
622
|
"ipc": {
|
|
611
623
|
"socket_path": os.getenv(
|
|
612
624
|
"DEVDUCK_IPC_SOCKET", "/tmp/devduck_main.sock"
|
|
613
625
|
),
|
|
614
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_IPC", "
|
|
626
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_IPC", "false").lower()
|
|
615
627
|
== "true",
|
|
616
628
|
},
|
|
617
629
|
}
|
|
618
630
|
|
|
619
|
-
|
|
631
|
+
# Show server configuration status
|
|
632
|
+
enabled_servers = []
|
|
633
|
+
disabled_servers = []
|
|
634
|
+
for server_name, config in servers.items():
|
|
635
|
+
if config.get("enabled", False):
|
|
636
|
+
if "port" in config:
|
|
637
|
+
enabled_servers.append(
|
|
638
|
+
f"{server_name.upper()}:{config['port']}"
|
|
639
|
+
)
|
|
640
|
+
else:
|
|
641
|
+
enabled_servers.append(server_name.upper())
|
|
642
|
+
else:
|
|
643
|
+
disabled_servers.append(server_name.upper())
|
|
620
644
|
|
|
621
|
-
|
|
645
|
+
logger.debug(
|
|
646
|
+
f"🦆 Server config: {', '.join(enabled_servers) if enabled_servers else 'none enabled'}"
|
|
647
|
+
)
|
|
648
|
+
if disabled_servers:
|
|
649
|
+
logger.debug(f"🦆 Disabled: {', '.join(disabled_servers)}")
|
|
650
|
+
|
|
651
|
+
self.servers = servers
|
|
622
652
|
|
|
623
653
|
# Load tools with flexible configuration
|
|
624
|
-
# Default tool config
|
|
625
|
-
|
|
654
|
+
# Default tool config
|
|
655
|
+
# Agent can load additional tools on-demand via fetch_github_tool
|
|
656
|
+
|
|
657
|
+
# 🔧 Available DevDuck Tools (load on-demand):
|
|
658
|
+
# - system_prompt: https://github.com/cagataycali/devduck/blob/main/devduck/tools/system_prompt.py
|
|
659
|
+
# - store_in_kb: https://github.com/cagataycali/devduck/blob/main/devduck/tools/store_in_kb.py
|
|
660
|
+
# - ipc: https://github.com/cagataycali/devduck/blob/main/devduck/tools/ipc.py
|
|
661
|
+
# - tcp: https://github.com/cagataycali/devduck/blob/main/devduck/tools/tcp.py
|
|
662
|
+
# - websocket: https://github.com/cagataycali/devduck/blob/main/devduck/tools/websocket.py
|
|
663
|
+
# - mcp_server: https://github.com/cagataycali/devduck/blob/main/devduck/tools/mcp_server.py
|
|
664
|
+
# - scraper: https://github.com/cagataycali/devduck/blob/main/devduck/tools/scraper.py
|
|
665
|
+
# - tray: https://github.com/cagataycali/devduck/blob/main/devduck/tools/tray.py
|
|
666
|
+
# - ambient: https://github.com/cagataycali/devduck/blob/main/devduck/tools/ambient.py
|
|
667
|
+
# - agentcore_config: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_config.py
|
|
668
|
+
# - agentcore_invoke: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_invoke.py
|
|
669
|
+
# - agentcore_logs: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_logs.py
|
|
670
|
+
# - agentcore_agents: https://github.com/cagataycali/devduck/blob/main/devduck/tools/agentcore_agents.py
|
|
671
|
+
# - create_subagent: https://github.com/cagataycali/devduck/blob/main/devduck/tools/create_subagent.py
|
|
672
|
+
# - use_github: https://github.com/cagataycali/devduck/blob/main/devduck/tools/use_github.py
|
|
673
|
+
# - speech_to_speech: https://github.com/cagataycali/devduck/blob/main/devduck/tools/speech_to_speech.py
|
|
674
|
+
# - state_manager: https://github.com/cagataycali/devduck/blob/main/devduck/tools/state_manager.py
|
|
675
|
+
|
|
676
|
+
# 📦 Strands Tools
|
|
677
|
+
# - editor, file_read, file_write, image_reader, load_tool, retrieve
|
|
678
|
+
# - calculator, use_agent, environment, mcp_client, speak, slack
|
|
679
|
+
|
|
680
|
+
# 🎮 Strands Fun Tools
|
|
681
|
+
# - listen, cursor, clipboard, screen_reader, bluetooth, yolo_vision
|
|
682
|
+
|
|
683
|
+
# 🔍 Strands Google
|
|
684
|
+
# - use_google, google_auth
|
|
685
|
+
|
|
686
|
+
# 🔧 Auto-append server tools based on enabled servers
|
|
687
|
+
server_tools_needed = []
|
|
688
|
+
if servers.get("tcp", {}).get("enabled", False):
|
|
689
|
+
server_tools_needed.append("tcp")
|
|
690
|
+
if servers.get("ws", {}).get("enabled", True):
|
|
691
|
+
server_tools_needed.append("websocket")
|
|
692
|
+
if servers.get("mcp", {}).get("enabled", False):
|
|
693
|
+
server_tools_needed.append("mcp_server")
|
|
694
|
+
if servers.get("ipc", {}).get("enabled", False):
|
|
695
|
+
server_tools_needed.append("ipc")
|
|
696
|
+
|
|
697
|
+
# Append to default tools if any server tools are needed
|
|
698
|
+
if server_tools_needed:
|
|
699
|
+
server_tools_str = ",".join(server_tools_needed)
|
|
700
|
+
default_tools = f"devduck.tools:system_prompt,fetch_github_tool,{server_tools_str};strands_tools:shell"
|
|
701
|
+
logger.info(f"Auto-added server tools: {server_tools_str}")
|
|
702
|
+
else:
|
|
703
|
+
default_tools = (
|
|
704
|
+
"devduck.tools:system_prompt,fetch_github_tool;strands_tools:shell"
|
|
705
|
+
)
|
|
626
706
|
|
|
627
707
|
tools_config = os.getenv("DEVDUCK_TOOLS", default_tools)
|
|
628
708
|
logger.info(f"Loading tools from config: {tools_config}")
|
|
@@ -776,8 +856,6 @@ class DevDuck:
|
|
|
776
856
|
Returns:
|
|
777
857
|
List of MCPClient instances ready for direct use in Agent
|
|
778
858
|
"""
|
|
779
|
-
import json
|
|
780
|
-
|
|
781
859
|
mcp_servers_json = os.getenv("MCP_SERVERS")
|
|
782
860
|
if not mcp_servers_json:
|
|
783
861
|
logger.debug("No MCP_SERVERS environment variable found")
|
|
@@ -1145,7 +1223,7 @@ When you learn something valuable during conversations:
|
|
|
1145
1223
|
- Communication: **MINIMAL WORDS**
|
|
1146
1224
|
- Efficiency: **Speed is paramount**
|
|
1147
1225
|
|
|
1148
|
-
{
|
|
1226
|
+
{_get_system_prompt()}"""
|
|
1149
1227
|
|
|
1150
1228
|
def _self_heal(self, error):
|
|
1151
1229
|
"""Attempt self-healing when errors occur"""
|
|
@@ -1192,7 +1270,6 @@ When you learn something valuable during conversations:
|
|
|
1192
1270
|
|
|
1193
1271
|
def _is_socket_available(self, socket_path):
|
|
1194
1272
|
"""Check if a Unix socket is available"""
|
|
1195
|
-
import os
|
|
1196
1273
|
|
|
1197
1274
|
# If socket file doesn't exist, it's available
|
|
1198
1275
|
if not os.path.exists(socket_path):
|
|
@@ -1430,11 +1507,11 @@ When you learn something valuable during conversations:
|
|
|
1430
1507
|
def restart(self):
|
|
1431
1508
|
"""Restart the agent"""
|
|
1432
1509
|
print("\n🦆 Restarting...")
|
|
1510
|
+
logger.debug("\n🦆 Restarting...")
|
|
1433
1511
|
self.__init__()
|
|
1434
1512
|
|
|
1435
1513
|
def _start_file_watcher(self):
|
|
1436
1514
|
"""Start background file watcher for auto hot-reload"""
|
|
1437
|
-
import threading
|
|
1438
1515
|
|
|
1439
1516
|
logger.info("Starting file watcher for hot-reload")
|
|
1440
1517
|
# Get the path to this file
|
|
@@ -1534,6 +1611,7 @@ When you learn something valuable during conversations:
|
|
|
1534
1611
|
self._watcher_running = False
|
|
1535
1612
|
|
|
1536
1613
|
print("\n🦆 Restarting process with fresh code...")
|
|
1614
|
+
logger.debug("\n🦆 Restarting process with fresh code...")
|
|
1537
1615
|
|
|
1538
1616
|
# Restart the entire Python process
|
|
1539
1617
|
# This ensures all code is freshly loaded
|
|
@@ -1677,7 +1755,7 @@ def interactive():
|
|
|
1677
1755
|
print(f"📝 Logs: {LOG_DIR}")
|
|
1678
1756
|
print("Type 'exit', 'quit', or 'q' to quit.")
|
|
1679
1757
|
print("Prefix with ! to run shell commands (e.g., ! ls -la)")
|
|
1680
|
-
print("
|
|
1758
|
+
print("\n\n")
|
|
1681
1759
|
logger.info("Interactive mode started")
|
|
1682
1760
|
|
|
1683
1761
|
# Set up prompt_toolkit with history
|
|
@@ -1705,7 +1783,6 @@ def interactive():
|
|
|
1705
1783
|
auto_suggest=AutoSuggestFromHistory(),
|
|
1706
1784
|
completer=completer,
|
|
1707
1785
|
complete_while_typing=True,
|
|
1708
|
-
mouse_support=False, # breaks scrolling when enabled
|
|
1709
1786
|
)
|
|
1710
1787
|
|
|
1711
1788
|
# Reset interrupt count on successful prompt
|
|
@@ -1733,6 +1810,10 @@ def interactive():
|
|
|
1733
1810
|
)
|
|
1734
1811
|
devduck._agent_executing = False
|
|
1735
1812
|
|
|
1813
|
+
# Reset terminal to fix rendering issues after command output
|
|
1814
|
+
print("\r", end="", flush=True)
|
|
1815
|
+
sys.stdout.flush()
|
|
1816
|
+
|
|
1736
1817
|
# Append shell command to history
|
|
1737
1818
|
append_to_shell_history(q, result["content"][0]["text"])
|
|
1738
1819
|
|
|
@@ -1747,6 +1828,9 @@ def interactive():
|
|
|
1747
1828
|
except Exception as e:
|
|
1748
1829
|
devduck._agent_executing = False # Reset on error
|
|
1749
1830
|
print(f"🦆 Shell command error: {e}")
|
|
1831
|
+
# Reset terminal on error too
|
|
1832
|
+
print("\r", end="", flush=True)
|
|
1833
|
+
sys.stdout.flush()
|
|
1750
1834
|
continue
|
|
1751
1835
|
|
|
1752
1836
|
# Execute the agent with user input
|
devduck/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 1,
|
|
31
|
+
__version__ = version = '1.1.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 4)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
devduck/tools/__init__.py
CHANGED
|
@@ -1,55 +1,47 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
DevDuck Tools Package
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from .
|
|
4
|
+
This module exports all available tools for devduck.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .agentcore_agents import agentcore_agents
|
|
8
|
+
from .agentcore_config import agentcore_config
|
|
9
|
+
from .agentcore_invoke import agentcore_invoke
|
|
10
|
+
from .agentcore_logs import agentcore_logs
|
|
7
11
|
from .ambient import ambient
|
|
8
|
-
from .websocket import websocket
|
|
9
|
-
from .ipc import ipc
|
|
10
|
-
from .use_github import use_github
|
|
11
12
|
from .create_subagent import create_subagent
|
|
13
|
+
from .fetch_github_tool import fetch_github_tool
|
|
14
|
+
from .install_tools import install_tools
|
|
15
|
+
from .ipc import ipc
|
|
16
|
+
from .mcp_server import mcp_server
|
|
17
|
+
from .scraper import scraper
|
|
18
|
+
from .speech_to_speech import speech_to_speech
|
|
19
|
+
from .state_manager import state_manager
|
|
12
20
|
from .store_in_kb import store_in_kb
|
|
13
21
|
from .system_prompt import system_prompt
|
|
14
|
-
from .
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from .agentcore_config import agentcore_config
|
|
19
|
-
from .agentcore_invoke import agentcore_invoke
|
|
20
|
-
from .agentcore_logs import agentcore_logs
|
|
21
|
-
from .agentcore_agents import agentcore_agents
|
|
22
|
+
from .tcp import tcp
|
|
23
|
+
from .tray import tray
|
|
24
|
+
from .use_github import use_github
|
|
25
|
+
from .websocket import websocket
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"websocket",
|
|
45
|
-
"ipc",
|
|
46
|
-
"mcp_server",
|
|
47
|
-
"install_tools",
|
|
48
|
-
"use_github",
|
|
49
|
-
"create_subagent",
|
|
50
|
-
"store_in_kb",
|
|
51
|
-
"system_prompt",
|
|
52
|
-
"state_manager",
|
|
53
|
-
"tray",
|
|
54
|
-
"ambient",
|
|
55
|
-
]
|
|
27
|
+
__all__ = [
|
|
28
|
+
"agentcore_agents",
|
|
29
|
+
"agentcore_config",
|
|
30
|
+
"agentcore_invoke",
|
|
31
|
+
"agentcore_logs",
|
|
32
|
+
"ambient",
|
|
33
|
+
"create_subagent",
|
|
34
|
+
"fetch_github_tool",
|
|
35
|
+
"install_tools",
|
|
36
|
+
"ipc",
|
|
37
|
+
"mcp_server",
|
|
38
|
+
"scraper",
|
|
39
|
+
"speech_to_speech",
|
|
40
|
+
"state_manager",
|
|
41
|
+
"store_in_kb",
|
|
42
|
+
"system_prompt",
|
|
43
|
+
"tcp",
|
|
44
|
+
"tray",
|
|
45
|
+
"use_github",
|
|
46
|
+
"websocket",
|
|
47
|
+
]
|
|
@@ -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}"}]}
|