devduck 0.4.1__py3-none-any.whl → 0.5.2__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 +624 -982
- devduck/_version.py +2 -2
- devduck/agentcore_handler.py +76 -0
- devduck/tools/__init__.py +46 -1
- 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 +422 -0
- devduck/tools/agentcore_logs.py +320 -0
- devduck/tools/ambient.py +157 -0
- devduck/tools/ipc.py +543 -0
- devduck/tools/tcp.py +0 -4
- devduck/tools/tray.py +246 -0
- devduck-0.5.2.dist-info/METADATA +415 -0
- devduck-0.5.2.dist-info/RECORD +29 -0
- devduck-0.5.2.dist-info/licenses/LICENSE +201 -0
- devduck-0.4.1.dist-info/METADATA +0 -283
- devduck-0.4.1.dist-info/RECORD +0 -19
- devduck-0.4.1.dist-info/licenses/LICENSE +0 -21
- {devduck-0.4.1.dist-info → devduck-0.5.2.dist-info}/WHEEL +0 -0
- {devduck-0.4.1.dist-info → devduck-0.5.2.dist-info}/entry_points.txt +0 -0
- {devduck-0.4.1.dist-info → devduck-0.5.2.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -1,260 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
🦆 devduck - extreme minimalist self-adapting agent
|
|
4
|
-
one file. self-healing. runtime dependencies. adaptive.
|
|
5
|
-
"""
|
|
2
|
+
"""🦆 devduck - self-adapting agent"""
|
|
6
3
|
import sys
|
|
7
|
-
import
|
|
4
|
+
import threading
|
|
8
5
|
import os
|
|
9
6
|
import platform
|
|
10
|
-
import socket
|
|
11
7
|
import logging
|
|
12
8
|
import tempfile
|
|
13
|
-
|
|
9
|
+
import boto3
|
|
14
10
|
from pathlib import Path
|
|
15
11
|
from datetime import datetime
|
|
16
|
-
|
|
12
|
+
import warnings
|
|
17
13
|
from logging.handlers import RotatingFileHandler
|
|
18
14
|
|
|
15
|
+
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
|
|
16
|
+
warnings.filterwarnings("ignore", message=".*cache_prompt is deprecated.*")
|
|
17
|
+
|
|
19
18
|
os.environ["BYPASS_TOOL_CONSENT"] = "true"
|
|
20
19
|
os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
|
|
20
|
+
os.environ["EDITOR_DISABLE_BACKUP"] = "true"
|
|
21
21
|
|
|
22
|
-
# 📝 Setup logging system
|
|
23
22
|
LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
|
|
24
23
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
25
24
|
LOG_FILE = LOG_DIR / "devduck.log"
|
|
26
25
|
|
|
27
|
-
# Configure logger
|
|
28
26
|
logger = logging.getLogger("devduck")
|
|
29
27
|
logger.setLevel(logging.DEBUG)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
file_handler = RotatingFileHandler(
|
|
33
|
-
LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
|
|
28
|
+
logger.addHandler(
|
|
29
|
+
RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3)
|
|
34
30
|
)
|
|
35
|
-
|
|
36
|
-
file_formatter = logging.Formatter(
|
|
37
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
38
|
-
)
|
|
39
|
-
file_handler.setFormatter(file_formatter)
|
|
40
|
-
|
|
41
|
-
# Console handler (only warnings and above)
|
|
42
|
-
console_handler = logging.StreamHandler()
|
|
43
|
-
console_handler.setLevel(logging.WARNING)
|
|
44
|
-
console_formatter = logging.Formatter("🦆 %(levelname)s: %(message)s")
|
|
45
|
-
console_handler.setFormatter(console_formatter)
|
|
46
|
-
|
|
47
|
-
logger.addHandler(file_handler)
|
|
48
|
-
logger.addHandler(console_handler)
|
|
49
|
-
|
|
50
|
-
logger.info("DevDuck logging system initialized")
|
|
51
|
-
|
|
31
|
+
logger.info("DevDuck initialized")
|
|
52
32
|
|
|
53
|
-
# 🔧 Self-healing dependency installer
|
|
54
|
-
def ensure_deps():
|
|
55
|
-
"""Install core dependencies at runtime if missing"""
|
|
56
|
-
import importlib.metadata
|
|
57
33
|
|
|
58
|
-
# Only ensure core deps - everything else is optional
|
|
59
|
-
core_deps = [
|
|
60
|
-
"strands-agents",
|
|
61
|
-
"prompt_toolkit",
|
|
62
|
-
"strands-agents-tools",
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
# Check each package individually using importlib.metadata
|
|
66
|
-
for dep in core_deps:
|
|
67
|
-
pkg_name = dep.split("[")[0] # Get base package name (strip extras)
|
|
68
|
-
try:
|
|
69
|
-
# Check if package is installed using metadata (checks PyPI package name)
|
|
70
|
-
importlib.metadata.version(pkg_name)
|
|
71
|
-
except importlib.metadata.PackageNotFoundError:
|
|
72
|
-
print(f"🦆 Installing {dep}...")
|
|
73
|
-
logger.debug(f"🦆 Installing {dep}...")
|
|
74
|
-
try:
|
|
75
|
-
subprocess.check_call(
|
|
76
|
-
[sys.executable, "-m", "pip", "install", dep],
|
|
77
|
-
stdout=subprocess.DEVNULL,
|
|
78
|
-
stderr=subprocess.DEVNULL,
|
|
79
|
-
)
|
|
80
|
-
except subprocess.CalledProcessError as e:
|
|
81
|
-
print(f"🦆 Warning: Failed to install {dep}: {e}")
|
|
82
|
-
logger.debug(f"🦆 Warning: Failed to install {dep}: {e}")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# 🌍 Environment adaptation
|
|
86
|
-
def adapt_to_env():
|
|
87
|
-
"""Self-adapt based on environment"""
|
|
88
|
-
env_info = {
|
|
89
|
-
"os": platform.system(),
|
|
90
|
-
"arch": platform.machine(),
|
|
91
|
-
"python": sys.version_info,
|
|
92
|
-
"cwd": str(Path.cwd()),
|
|
93
|
-
"home": str(Path.home()),
|
|
94
|
-
"shell": os.environ.get("SHELL", "unknown"),
|
|
95
|
-
"hostname": socket.gethostname(),
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
# Adaptive configurations - using common models
|
|
99
|
-
if env_info["os"] == "Darwin": # macOS
|
|
100
|
-
ollama_host = "http://localhost:11434"
|
|
101
|
-
model = "qwen3:1.7b" # Lightweight for macOS
|
|
102
|
-
elif env_info["os"] == "Linux":
|
|
103
|
-
ollama_host = "http://localhost:11434"
|
|
104
|
-
model = "qwen3:30b" # More power on Linux
|
|
105
|
-
else: # Windows
|
|
106
|
-
ollama_host = "http://localhost:11434"
|
|
107
|
-
model = "qwen3:8b" # Conservative for Windows
|
|
108
|
-
|
|
109
|
-
return env_info, ollama_host, model
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# 🔍 Self-awareness: Read own source code
|
|
113
34
|
def get_own_source_code():
|
|
114
|
-
"""
|
|
115
|
-
Read and return the source code of this agent file.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
str: The complete source code for self-awareness
|
|
119
|
-
"""
|
|
35
|
+
"""Read own source code for self-awareness"""
|
|
120
36
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
with open(current_file, "r", encoding="utf-8") as f:
|
|
124
|
-
init_code = f.read()
|
|
125
|
-
return f"# devduck/__init__.py\n```python\n{init_code}\n```"
|
|
37
|
+
with open(__file__, "r", encoding="utf-8") as f:
|
|
38
|
+
return f"# devduck/__init__.py\n```python\n{f.read()}\n```"
|
|
126
39
|
except Exception as e:
|
|
127
|
-
return f"Error reading
|
|
128
|
-
|
|
129
|
-
def view_logs_tool(
|
|
130
|
-
action: str = "view",
|
|
131
|
-
lines: int = 100,
|
|
132
|
-
pattern: str = None,
|
|
133
|
-
) -> Dict[str, Any]:
|
|
134
|
-
"""
|
|
135
|
-
View and manage DevDuck logs.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
action: Action to perform - "view", "tail", "search", "clear", "stats"
|
|
139
|
-
lines: Number of lines to show (for view/tail)
|
|
140
|
-
pattern: Search pattern (for search action)
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Dict with status and content
|
|
144
|
-
"""
|
|
145
|
-
try:
|
|
146
|
-
if action == "view":
|
|
147
|
-
if not LOG_FILE.exists():
|
|
148
|
-
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
149
|
-
|
|
150
|
-
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
151
|
-
all_lines = f.readlines()
|
|
152
|
-
recent_lines = (
|
|
153
|
-
all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
154
|
-
)
|
|
155
|
-
content = "".join(recent_lines)
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
"status": "success",
|
|
159
|
-
"content": [
|
|
160
|
-
{"text": f"Last {len(recent_lines)} log lines:\n\n{content}"}
|
|
161
|
-
],
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
elif action == "tail":
|
|
165
|
-
if not LOG_FILE.exists():
|
|
166
|
-
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
167
|
-
|
|
168
|
-
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
169
|
-
all_lines = f.readlines()
|
|
170
|
-
tail_lines = all_lines[-50:] if len(all_lines) > 50 else all_lines
|
|
171
|
-
content = "".join(tail_lines)
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
"status": "success",
|
|
175
|
-
"content": [{"text": f"Tail (last 50 lines):\n\n{content}"}],
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
elif action == "search":
|
|
179
|
-
if not pattern:
|
|
180
|
-
return {
|
|
181
|
-
"status": "error",
|
|
182
|
-
"content": [{"text": "pattern parameter required for search"}],
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if not LOG_FILE.exists():
|
|
186
|
-
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
187
|
-
|
|
188
|
-
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
189
|
-
matching_lines = [line for line in f if pattern.lower() in line.lower()]
|
|
190
|
-
|
|
191
|
-
if not matching_lines:
|
|
192
|
-
return {
|
|
193
|
-
"status": "success",
|
|
194
|
-
"content": [{"text": f"No matches found for pattern: {pattern}"}],
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
content = "".join(matching_lines[-100:]) # Last 100 matches
|
|
198
|
-
return {
|
|
199
|
-
"status": "success",
|
|
200
|
-
"content": [
|
|
201
|
-
{
|
|
202
|
-
"text": f"Found {len(matching_lines)} matches (showing last 100):\n\n{content}"
|
|
203
|
-
}
|
|
204
|
-
],
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
elif action == "clear":
|
|
208
|
-
if LOG_FILE.exists():
|
|
209
|
-
LOG_FILE.unlink()
|
|
210
|
-
logger.info("Log file cleared by user")
|
|
211
|
-
return {
|
|
212
|
-
"status": "success",
|
|
213
|
-
"content": [{"text": "Logs cleared successfully"}],
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
elif action == "stats":
|
|
217
|
-
if not LOG_FILE.exists():
|
|
218
|
-
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
219
|
-
|
|
220
|
-
stat = LOG_FILE.stat()
|
|
221
|
-
size_mb = stat.st_size / (1024 * 1024)
|
|
222
|
-
modified = datetime.fromtimestamp(stat.st_mtime).strftime(
|
|
223
|
-
"%Y-%m-%d %H:%M:%S"
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
227
|
-
total_lines = sum(1 for _ in f)
|
|
228
|
-
|
|
229
|
-
stats_text = f"""Log File Statistics:
|
|
230
|
-
Path: {LOG_FILE}
|
|
231
|
-
Size: {size_mb:.2f} MB
|
|
232
|
-
Lines: {total_lines}
|
|
233
|
-
Last Modified: {modified}"""
|
|
234
|
-
|
|
235
|
-
return {"status": "success", "content": [{"text": stats_text}]}
|
|
236
|
-
|
|
237
|
-
else:
|
|
238
|
-
return {
|
|
239
|
-
"status": "error",
|
|
240
|
-
"content": [
|
|
241
|
-
{
|
|
242
|
-
"text": f"Unknown action: {action}. Valid: view, tail, search, clear, stats"
|
|
243
|
-
}
|
|
244
|
-
],
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
except Exception as e:
|
|
248
|
-
logger.error(f"Error in view_logs_tool: {e}")
|
|
249
|
-
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
40
|
+
return f"Error reading source: {e}"
|
|
250
41
|
|
|
251
42
|
|
|
252
43
|
def get_shell_history_file():
|
|
253
|
-
"""Get
|
|
254
|
-
|
|
255
|
-
if not
|
|
256
|
-
|
|
257
|
-
return str(
|
|
44
|
+
"""Get devduck history file"""
|
|
45
|
+
history = Path.home() / ".devduck_history"
|
|
46
|
+
if not history.exists():
|
|
47
|
+
history.touch(mode=0o600)
|
|
48
|
+
return str(history)
|
|
258
49
|
|
|
259
50
|
|
|
260
51
|
def get_shell_history_files():
|
|
@@ -334,32 +125,6 @@ def parse_history_line(line, history_type):
|
|
|
334
125
|
return None
|
|
335
126
|
|
|
336
127
|
|
|
337
|
-
def get_recent_logs():
|
|
338
|
-
"""Get the last N lines from the log file for context."""
|
|
339
|
-
try:
|
|
340
|
-
log_line_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
|
|
341
|
-
|
|
342
|
-
if not LOG_FILE.exists():
|
|
343
|
-
return ""
|
|
344
|
-
|
|
345
|
-
with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
|
346
|
-
all_lines = f.readlines()
|
|
347
|
-
|
|
348
|
-
recent_lines = (
|
|
349
|
-
all_lines[-log_line_count:]
|
|
350
|
-
if len(all_lines) > log_line_count
|
|
351
|
-
else all_lines
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
if not recent_lines:
|
|
355
|
-
return ""
|
|
356
|
-
|
|
357
|
-
log_content = "".join(recent_lines)
|
|
358
|
-
return f"\n\n## Recent Logs (last {len(recent_lines)} lines):\n```\n{log_content}```\n"
|
|
359
|
-
except Exception as e:
|
|
360
|
-
return f"\n\n## Recent Logs: Error reading logs - {e}\n"
|
|
361
|
-
|
|
362
|
-
|
|
363
128
|
def get_last_messages():
|
|
364
129
|
"""Get the last N messages from multiple shell histories for context."""
|
|
365
130
|
try:
|
|
@@ -419,269 +184,353 @@ def get_last_messages():
|
|
|
419
184
|
return ""
|
|
420
185
|
|
|
421
186
|
|
|
187
|
+
def get_recent_logs():
|
|
188
|
+
"""Get recent logs for context"""
|
|
189
|
+
try:
|
|
190
|
+
log_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
|
|
191
|
+
|
|
192
|
+
if not LOG_FILE.exists():
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
|
196
|
+
lines = f.readlines()
|
|
197
|
+
|
|
198
|
+
recent = lines[-log_count:] if len(lines) > log_count else lines
|
|
199
|
+
|
|
200
|
+
if recent:
|
|
201
|
+
return f"\n\n## Recent Logs (last {len(recent)} lines):\n```\n{''.join(recent)}```\n"
|
|
202
|
+
return ""
|
|
203
|
+
except:
|
|
204
|
+
return ""
|
|
205
|
+
|
|
206
|
+
|
|
422
207
|
def append_to_shell_history(query, response):
|
|
423
|
-
"""Append
|
|
208
|
+
"""Append interaction to history"""
|
|
424
209
|
import time
|
|
425
210
|
|
|
426
211
|
try:
|
|
427
212
|
history_file = get_shell_history_file()
|
|
428
213
|
timestamp = str(int(time.time()))
|
|
214
|
+
response_summary = (
|
|
215
|
+
str(response).replace("\n", " ")[
|
|
216
|
+
: int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
|
|
217
|
+
]
|
|
218
|
+
+ "..."
|
|
219
|
+
)
|
|
429
220
|
|
|
430
221
|
with open(history_file, "a", encoding="utf-8") as f:
|
|
431
222
|
f.write(f": {timestamp}:0;# devduck: {query}\n")
|
|
432
|
-
response_summary = (
|
|
433
|
-
str(response).replace("\n", " ")[
|
|
434
|
-
: int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
|
|
435
|
-
]
|
|
436
|
-
+ "..."
|
|
437
|
-
)
|
|
438
223
|
f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
|
|
439
224
|
|
|
440
225
|
os.chmod(history_file, 0o600)
|
|
441
|
-
except
|
|
226
|
+
except:
|
|
442
227
|
pass
|
|
443
228
|
|
|
444
229
|
|
|
445
|
-
# 🦆 The devduck agent
|
|
446
230
|
class DevDuck:
|
|
231
|
+
"""Minimalist adaptive agent with flexible tool loading"""
|
|
232
|
+
|
|
447
233
|
def __init__(
|
|
448
234
|
self,
|
|
449
235
|
auto_start_servers=True,
|
|
450
236
|
tcp_port=9999,
|
|
451
237
|
ws_port=8080,
|
|
452
238
|
mcp_port=8000,
|
|
239
|
+
ipc_socket=None,
|
|
453
240
|
enable_tcp=True,
|
|
454
241
|
enable_ws=True,
|
|
455
242
|
enable_mcp=True,
|
|
243
|
+
enable_ipc=True,
|
|
456
244
|
):
|
|
457
245
|
"""Initialize the minimalist adaptive agent"""
|
|
458
|
-
logger.info("Initializing DevDuck
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
246
|
+
logger.info("Initializing DevDuck...")
|
|
247
|
+
|
|
248
|
+
# Environment detection
|
|
249
|
+
self.os = platform.system()
|
|
250
|
+
self.arch = platform.machine()
|
|
251
|
+
self.model = "qwen3:1.7b" if self.os == "Darwin" else "qwen3:8b"
|
|
252
|
+
|
|
253
|
+
# Hot-reload state
|
|
254
|
+
self._agent_executing = False
|
|
255
|
+
self._reload_pending = False
|
|
256
|
+
|
|
257
|
+
# Server configuration
|
|
258
|
+
self.tcp_port = tcp_port
|
|
259
|
+
self.ws_port = ws_port
|
|
260
|
+
self.mcp_port = mcp_port
|
|
261
|
+
self.ipc_socket = ipc_socket or "/tmp/devduck_main.sock"
|
|
262
|
+
self.enable_tcp = enable_tcp
|
|
263
|
+
self.enable_ws = enable_ws
|
|
264
|
+
self.enable_mcp = enable_mcp
|
|
265
|
+
self.enable_ipc = enable_ipc
|
|
266
|
+
|
|
267
|
+
# Import core dependencies
|
|
268
|
+
from strands import Agent, tool
|
|
269
|
+
|
|
270
|
+
# Load tools with flexible configuration
|
|
271
|
+
tools = self._load_tools_flexible()
|
|
272
|
+
|
|
273
|
+
# Add built-in view_logs tool
|
|
274
|
+
@tool
|
|
275
|
+
def view_logs(action: str = "view", lines: int = 100, pattern: str = None):
|
|
276
|
+
"""View and manage DevDuck logs"""
|
|
277
|
+
return self._view_logs_impl(action, lines, pattern)
|
|
278
|
+
|
|
279
|
+
tools.append(view_logs)
|
|
280
|
+
|
|
281
|
+
# Create model
|
|
282
|
+
model = self._create_model()
|
|
283
|
+
|
|
284
|
+
# Create agent
|
|
285
|
+
self.agent = Agent(
|
|
286
|
+
model=model,
|
|
287
|
+
tools=tools,
|
|
288
|
+
system_prompt=self._build_prompt(),
|
|
289
|
+
load_tools_from_directory=True,
|
|
290
|
+
)
|
|
462
291
|
|
|
463
|
-
|
|
464
|
-
|
|
292
|
+
# Auto-start servers
|
|
293
|
+
if auto_start_servers and "--mcp" not in sys.argv:
|
|
294
|
+
self._start_servers()
|
|
465
295
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
self._reload_pending = False
|
|
296
|
+
# Start hot-reload watcher
|
|
297
|
+
self._start_hot_reload()
|
|
469
298
|
|
|
470
|
-
|
|
471
|
-
from strands import Agent, tool
|
|
299
|
+
logger.info(f"DevDuck ready with {len(tools)} tools")
|
|
472
300
|
|
|
473
|
-
|
|
474
|
-
|
|
301
|
+
def _load_tools_flexible(self):
|
|
302
|
+
"""
|
|
303
|
+
Load tools with flexible configuration via DEVDUCK_TOOLS env var.
|
|
475
304
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
from strands.models.ollama import OllamaModel
|
|
479
|
-
except ImportError:
|
|
480
|
-
logger.warning(
|
|
481
|
-
"strands-agents[ollama] not installed - Ollama model unavailable"
|
|
482
|
-
)
|
|
483
|
-
OllamaModel = None
|
|
305
|
+
Format: package:tool1,tool2:package2:tool3,tool4
|
|
306
|
+
Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
484
307
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
create_model = None
|
|
308
|
+
Static tools (always loaded):
|
|
309
|
+
- DevDuck's own tools (tcp, websocket, ipc, etc.)
|
|
310
|
+
- AgentCore tools (if AWS credentials available)
|
|
311
|
+
"""
|
|
312
|
+
tools = []
|
|
492
313
|
|
|
493
|
-
|
|
494
|
-
|
|
314
|
+
# 1. STATIC: Core DevDuck tools (always load)
|
|
315
|
+
try:
|
|
316
|
+
from devduck.tools import (
|
|
317
|
+
tcp,
|
|
318
|
+
websocket,
|
|
319
|
+
ipc,
|
|
320
|
+
mcp_server,
|
|
321
|
+
install_tools,
|
|
322
|
+
use_github,
|
|
323
|
+
create_subagent,
|
|
324
|
+
store_in_kb,
|
|
325
|
+
system_prompt,
|
|
326
|
+
tray,
|
|
327
|
+
ambient,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
tools.extend(
|
|
331
|
+
[
|
|
495
332
|
tcp,
|
|
496
333
|
websocket,
|
|
334
|
+
ipc,
|
|
497
335
|
mcp_server,
|
|
498
336
|
install_tools,
|
|
499
337
|
use_github,
|
|
500
338
|
create_subagent,
|
|
501
339
|
store_in_kb,
|
|
502
340
|
system_prompt,
|
|
503
|
-
|
|
341
|
+
tray,
|
|
342
|
+
ambient,
|
|
343
|
+
]
|
|
344
|
+
)
|
|
345
|
+
logger.info("✅ DevDuck core tools loaded")
|
|
346
|
+
except ImportError as e:
|
|
347
|
+
logger.warning(f"DevDuck tools unavailable: {e}")
|
|
504
348
|
|
|
505
|
-
|
|
349
|
+
# 2. STATIC: AgentCore tools (if AWS credentials available and not disabled)
|
|
350
|
+
if os.getenv("DEVDUCK_DISABLE_AGENTCORE_TOOLS", "false").lower() != "true":
|
|
351
|
+
try:
|
|
352
|
+
boto3.client("sts").get_caller_identity()
|
|
353
|
+
from .tools.agentcore_config import agentcore_config
|
|
354
|
+
from .tools.agentcore_invoke import agentcore_invoke
|
|
355
|
+
from .tools.agentcore_logs import agentcore_logs
|
|
356
|
+
from .tools.agentcore_agents import agentcore_agents
|
|
357
|
+
|
|
358
|
+
tools.extend(
|
|
506
359
|
[
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
use_github,
|
|
512
|
-
create_subagent,
|
|
513
|
-
store_in_kb,
|
|
514
|
-
system_prompt
|
|
360
|
+
agentcore_config,
|
|
361
|
+
agentcore_invoke,
|
|
362
|
+
agentcore_logs,
|
|
363
|
+
agentcore_agents,
|
|
515
364
|
]
|
|
516
365
|
)
|
|
517
|
-
|
|
518
|
-
|
|
366
|
+
logger.info("✅ AgentCore tools loaded")
|
|
367
|
+
except:
|
|
368
|
+
pass
|
|
369
|
+
else:
|
|
370
|
+
logger.info(
|
|
371
|
+
"⏭️ AgentCore tools disabled (DEVDUCK_DISABLE_AGENTCORE_TOOLS=true)"
|
|
372
|
+
)
|
|
519
373
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
try:
|
|
523
|
-
from strands_fun_tools import (
|
|
524
|
-
listen,
|
|
525
|
-
cursor,
|
|
526
|
-
clipboard,
|
|
527
|
-
screen_reader,
|
|
528
|
-
yolo_vision,
|
|
529
|
-
)
|
|
374
|
+
# 3. FLEXIBLE: Load tools from DEVDUCK_TOOLS env var
|
|
375
|
+
tools_config = os.getenv("DEVDUCK_TOOLS")
|
|
530
376
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
377
|
+
if tools_config:
|
|
378
|
+
# Parse: "strands_tools:shell,editor:strands_fun_tools:clipboard"
|
|
379
|
+
tools.extend(self._parse_and_load_tools(tools_config))
|
|
380
|
+
else:
|
|
381
|
+
# Default: Load all common tools
|
|
382
|
+
tools.extend(self._load_default_tools())
|
|
383
|
+
|
|
384
|
+
return tools
|
|
385
|
+
|
|
386
|
+
def _parse_and_load_tools(self, config):
|
|
387
|
+
"""
|
|
388
|
+
Parse DEVDUCK_TOOLS config and load specified tools.
|
|
389
|
+
|
|
390
|
+
Format: package:tool1,tool2:package2:tool3
|
|
391
|
+
Example: strands_tools:shell,editor:strands_fun_tools:clipboard,cursor
|
|
392
|
+
"""
|
|
393
|
+
loaded_tools = []
|
|
394
|
+
current_package = None
|
|
395
|
+
|
|
396
|
+
for segment in config.split(":"):
|
|
397
|
+
segment = segment.strip()
|
|
398
|
+
|
|
399
|
+
# Check if this segment is a package or tool list
|
|
400
|
+
if "," not in segment and not segment.startswith("strands"):
|
|
401
|
+
# Single tool from current package
|
|
402
|
+
if current_package:
|
|
403
|
+
tool = self._load_single_tool(current_package, segment)
|
|
404
|
+
if tool:
|
|
405
|
+
loaded_tools.append(tool)
|
|
406
|
+
elif "," in segment:
|
|
407
|
+
# Tool list from current package
|
|
408
|
+
if current_package:
|
|
409
|
+
for tool_name in segment.split(","):
|
|
410
|
+
tool_name = tool_name.strip()
|
|
411
|
+
tool = self._load_single_tool(current_package, tool_name)
|
|
412
|
+
if tool:
|
|
413
|
+
loaded_tools.append(tool)
|
|
538
414
|
else:
|
|
539
|
-
|
|
415
|
+
# Package name
|
|
416
|
+
current_package = segment
|
|
540
417
|
|
|
541
|
-
|
|
542
|
-
|
|
418
|
+
logger.info(f"✅ Loaded {len(loaded_tools)} tools from DEVDUCK_TOOLS")
|
|
419
|
+
return loaded_tools
|
|
420
|
+
|
|
421
|
+
def _load_single_tool(self, package, tool_name):
|
|
422
|
+
"""Load a single tool from a package"""
|
|
423
|
+
try:
|
|
424
|
+
module = __import__(package, fromlist=[tool_name])
|
|
425
|
+
tool = getattr(module, tool_name)
|
|
426
|
+
logger.debug(f"Loaded {tool_name} from {package}")
|
|
427
|
+
return tool
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.warning(f"Failed to load {tool_name} from {package}: {e}")
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
def _load_default_tools(self):
|
|
433
|
+
"""Load default tools when DEVDUCK_TOOLS is not set"""
|
|
434
|
+
tools = []
|
|
435
|
+
|
|
436
|
+
# strands-agents-tools (essential)
|
|
437
|
+
try:
|
|
438
|
+
from strands_tools import (
|
|
439
|
+
shell,
|
|
440
|
+
editor,
|
|
441
|
+
file_read,
|
|
442
|
+
file_write,
|
|
443
|
+
calculator,
|
|
444
|
+
image_reader,
|
|
445
|
+
use_agent,
|
|
446
|
+
load_tool,
|
|
447
|
+
environment,
|
|
448
|
+
mcp_client,
|
|
449
|
+
retrieve,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
tools.extend(
|
|
453
|
+
[
|
|
543
454
|
shell,
|
|
544
455
|
editor,
|
|
456
|
+
file_read,
|
|
457
|
+
file_write,
|
|
545
458
|
calculator,
|
|
546
|
-
# python_repl,
|
|
547
459
|
image_reader,
|
|
548
460
|
use_agent,
|
|
549
461
|
load_tool,
|
|
550
462
|
environment,
|
|
551
463
|
mcp_client,
|
|
552
464
|
retrieve,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
editor,
|
|
559
|
-
calculator,
|
|
560
|
-
# python_repl,
|
|
561
|
-
image_reader,
|
|
562
|
-
use_agent,
|
|
563
|
-
load_tool,
|
|
564
|
-
environment,
|
|
565
|
-
mcp_client,
|
|
566
|
-
retrieve,
|
|
567
|
-
]
|
|
568
|
-
)
|
|
569
|
-
except ImportError:
|
|
570
|
-
logger.info(
|
|
571
|
-
"strands-agents-tools not installed - core tools unavailable (install with: pip install devduck[all])"
|
|
572
|
-
)
|
|
465
|
+
]
|
|
466
|
+
)
|
|
467
|
+
logger.info("✅ strands-agents-tools loaded")
|
|
468
|
+
except ImportError:
|
|
469
|
+
logger.warning("strands-agents-tools unavailable")
|
|
573
470
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
# Add built-in tools to the toolset
|
|
585
|
-
core_tools.extend([view_logs])
|
|
586
|
-
|
|
587
|
-
# Assign tools
|
|
588
|
-
self.tools = core_tools
|
|
589
|
-
|
|
590
|
-
logger.info(f"Initialized {len(self.tools)} tools")
|
|
591
|
-
|
|
592
|
-
# Check if MODEL_PROVIDER env variable is set
|
|
593
|
-
model_provider = os.getenv("MODEL_PROVIDER")
|
|
594
|
-
|
|
595
|
-
if model_provider and create_model:
|
|
596
|
-
# Use create_model utility for any provider (bedrock, anthropic, etc.)
|
|
597
|
-
self.agent_model = create_model(provider=model_provider)
|
|
598
|
-
elif OllamaModel:
|
|
599
|
-
# Fallback to default Ollama behavior
|
|
600
|
-
self.agent_model = OllamaModel(
|
|
601
|
-
host=self.ollama_host,
|
|
602
|
-
model_id=self.model,
|
|
603
|
-
temperature=1,
|
|
604
|
-
keep_alive="5m",
|
|
605
|
-
)
|
|
606
|
-
else:
|
|
607
|
-
raise ImportError(
|
|
608
|
-
"No model provider available. Install with: pip install devduck[all]"
|
|
471
|
+
# strands-fun-tools (optional, skip in --mcp mode)
|
|
472
|
+
if "--mcp" not in sys.argv:
|
|
473
|
+
try:
|
|
474
|
+
from strands_fun_tools import (
|
|
475
|
+
listen,
|
|
476
|
+
cursor,
|
|
477
|
+
clipboard,
|
|
478
|
+
screen_reader,
|
|
479
|
+
yolo_vision,
|
|
609
480
|
)
|
|
610
481
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
system_prompt=self._build_system_prompt(),
|
|
616
|
-
load_tools_from_directory=True,
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
# 🚀 AUTO-START SERVERS: TCP, WebSocket, MCP HTTP
|
|
620
|
-
if auto_start_servers:
|
|
621
|
-
logger.info("Auto-starting servers...")
|
|
622
|
-
print("🦆 Auto-starting servers...")
|
|
482
|
+
tools.extend([listen, cursor, clipboard, screen_reader, yolo_vision])
|
|
483
|
+
logger.info("✅ strands-fun-tools loaded")
|
|
484
|
+
except ImportError:
|
|
485
|
+
logger.info("strands-fun-tools unavailable")
|
|
623
486
|
|
|
624
|
-
|
|
625
|
-
try:
|
|
626
|
-
# Start TCP server on configurable port
|
|
627
|
-
tcp_result = self.agent.tool.tcp(
|
|
628
|
-
action="start_server", port=tcp_port
|
|
629
|
-
)
|
|
630
|
-
if tcp_result.get("status") == "success":
|
|
631
|
-
logger.info(f"✓ TCP server started on port {tcp_port}")
|
|
632
|
-
print(f"🦆 ✓ TCP server: localhost:{tcp_port}")
|
|
633
|
-
else:
|
|
634
|
-
logger.warning(f"TCP server start issue: {tcp_result}")
|
|
635
|
-
except Exception as e:
|
|
636
|
-
logger.error(f"Failed to start TCP server: {e}")
|
|
637
|
-
print(f"🦆 ⚠ TCP server failed: {e}")
|
|
487
|
+
return tools
|
|
638
488
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
ws_result = self.agent.tool.websocket(
|
|
643
|
-
action="start_server", port=ws_port
|
|
644
|
-
)
|
|
645
|
-
if ws_result.get("status") == "success":
|
|
646
|
-
logger.info(f"✓ WebSocket server started on port {ws_port}")
|
|
647
|
-
print(f"🦆 ✓ WebSocket server: localhost:{ws_port}")
|
|
648
|
-
else:
|
|
649
|
-
logger.warning(f"WebSocket server start issue: {ws_result}")
|
|
650
|
-
except Exception as e:
|
|
651
|
-
logger.error(f"Failed to start WebSocket server: {e}")
|
|
652
|
-
print(f"🦆 ⚠ WebSocket server failed: {e}")
|
|
489
|
+
def _create_model(self):
|
|
490
|
+
"""Create model with smart provider selection"""
|
|
491
|
+
provider = os.getenv("MODEL_PROVIDER")
|
|
653
492
|
|
|
654
|
-
|
|
493
|
+
if not provider:
|
|
494
|
+
# Auto-detect: Bedrock → MLX → Ollama
|
|
495
|
+
try:
|
|
496
|
+
boto3.client("sts").get_caller_identity()
|
|
497
|
+
provider = "bedrock"
|
|
498
|
+
print("🦆 Using Bedrock")
|
|
499
|
+
except:
|
|
500
|
+
if self.os == "Darwin" and self.arch in ["arm64", "aarch64"]:
|
|
655
501
|
try:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
)
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
502
|
+
from strands_mlx import MLXModel
|
|
503
|
+
|
|
504
|
+
provider = "mlx"
|
|
505
|
+
self.model = "mlx-community/Qwen3-1.7B-4bit"
|
|
506
|
+
print("🦆 Using MLX")
|
|
507
|
+
except ImportError:
|
|
508
|
+
provider = "ollama"
|
|
509
|
+
print("🦆 Using Ollama")
|
|
510
|
+
else:
|
|
511
|
+
provider = "ollama"
|
|
512
|
+
print("🦆 Using Ollama")
|
|
513
|
+
|
|
514
|
+
# Create model
|
|
515
|
+
if provider == "mlx":
|
|
516
|
+
from strands_mlx import MLXModel
|
|
517
|
+
|
|
518
|
+
return MLXModel(model_id=self.model, temperature=1)
|
|
519
|
+
elif provider == "ollama":
|
|
520
|
+
from strands.models.ollama import OllamaModel
|
|
521
|
+
|
|
522
|
+
return OllamaModel(
|
|
523
|
+
host="http://localhost:11434",
|
|
524
|
+
model_id=self.model,
|
|
525
|
+
temperature=1,
|
|
526
|
+
keep_alive="5m",
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
from strands_tools.utils.models.model import create_model
|
|
679
530
|
|
|
680
|
-
|
|
681
|
-
logger.error(f"Initialization failed: {e}")
|
|
682
|
-
self._self_heal(e)
|
|
531
|
+
return create_model(provider=provider)
|
|
683
532
|
|
|
684
|
-
def
|
|
533
|
+
def _build_prompt(self):
|
|
685
534
|
"""Build adaptive system prompt based on environment
|
|
686
535
|
|
|
687
536
|
IMPORTANT: The system prompt includes the agent's complete source code.
|
|
@@ -691,427 +540,344 @@ class DevDuck:
|
|
|
691
540
|
|
|
692
541
|
Learning: Always check source code truth over conversation memory!
|
|
693
542
|
"""
|
|
694
|
-
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
695
|
-
|
|
696
|
-
# Get own source code for self-awareness
|
|
697
543
|
own_code = get_own_source_code()
|
|
544
|
+
recent_context = get_last_messages()
|
|
545
|
+
recent_logs = get_recent_logs()
|
|
698
546
|
|
|
699
|
-
#
|
|
700
|
-
|
|
701
|
-
|
|
547
|
+
# Detect if using Bedrock for AgentCore documentation
|
|
548
|
+
provider = os.getenv("MODEL_PROVIDER", "")
|
|
549
|
+
is_bedrock = provider == "bedrock" or "bedrock" in provider.lower()
|
|
702
550
|
try:
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
551
|
+
if not is_bedrock:
|
|
552
|
+
boto3.client("sts").get_caller_identity()
|
|
553
|
+
is_bedrock = True
|
|
554
|
+
except:
|
|
555
|
+
pass
|
|
707
556
|
|
|
708
|
-
#
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
557
|
+
# Build AgentCore documentation if using Bedrock
|
|
558
|
+
agentcore_docs = ""
|
|
559
|
+
if is_bedrock:
|
|
560
|
+
handler_path = str(Path(__file__).parent / "agentcore_handler.py")
|
|
561
|
+
agentcore_docs = f"""
|
|
562
|
+
|
|
563
|
+
## 🚀 AgentCore (Bedrock)
|
|
564
|
+
|
|
565
|
+
Handler: `{handler_path}`
|
|
566
|
+
|
|
567
|
+
### Quick Deploy:
|
|
568
|
+
```python
|
|
569
|
+
# Configure + launch
|
|
570
|
+
agentcore_config(action="configure", agent_name="devduck",
|
|
571
|
+
tools="strands_tools:shell,editor", auto_launch=True)
|
|
572
|
+
|
|
573
|
+
# Invoke
|
|
574
|
+
agentcore_invoke(prompt="test", agent_name="devduck")
|
|
575
|
+
|
|
576
|
+
# Monitor
|
|
577
|
+
agentcore_logs(agent_name="devduck")
|
|
578
|
+
agentcore_agents(action="list")
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Key Params:
|
|
582
|
+
- tools: "package:tool1,tool2:package2:tool3"
|
|
583
|
+
- idle_timeout: 900s (default)
|
|
584
|
+
- model_id: us.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|
585
|
+
"""
|
|
714
586
|
|
|
715
|
-
return f"""🦆
|
|
587
|
+
return f"""🦆 DevDuck - self-adapting agent
|
|
716
588
|
|
|
717
|
-
Environment: {self.
|
|
718
|
-
Python: {self.env_info['python']}
|
|
589
|
+
Environment: {self.os} {self.arch}
|
|
719
590
|
Model: {self.model}
|
|
720
|
-
|
|
721
|
-
Session ID: {session_id}
|
|
591
|
+
CWD: {Path.cwd()}
|
|
722
592
|
|
|
723
593
|
You are:
|
|
724
594
|
- Minimalist: Brief, direct responses
|
|
725
|
-
- Self-healing: Adapt when things break
|
|
726
595
|
- Efficient: Get things done fast
|
|
727
596
|
- Pragmatic: Use what works
|
|
728
597
|
|
|
729
|
-
Current working directory: {self.env_info['cwd']}
|
|
730
|
-
|
|
731
598
|
{recent_context}
|
|
732
599
|
{recent_logs}
|
|
600
|
+
{agentcore_docs}
|
|
733
601
|
|
|
734
|
-
## Your
|
|
735
|
-
You have full access to your own source code for self-awareness and self-modification:
|
|
602
|
+
## Your Code
|
|
736
603
|
|
|
604
|
+
You have full access to your own source code for self-awareness and self-modification:
|
|
605
|
+
---
|
|
737
606
|
{own_code}
|
|
607
|
+
---
|
|
738
608
|
|
|
739
|
-
## Hot Reload
|
|
740
|
-
-
|
|
741
|
-
-
|
|
742
|
-
-
|
|
743
|
-
- **Full Python Access** - Create any Python functionality as a tool
|
|
609
|
+
## Hot Reload Active:
|
|
610
|
+
- Save .py files in ./tools/ for instant tool creation
|
|
611
|
+
- Use install_tools() to load from packages
|
|
612
|
+
- No restart needed
|
|
744
613
|
|
|
745
|
-
##
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
## MCP Server:
|
|
752
|
-
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
753
|
-
- Example: mcp_server(action="start", port=8000)
|
|
754
|
-
- Connect from Claude Desktop, other agents, or custom clients
|
|
755
|
-
- Full bidirectional communication
|
|
614
|
+
## Tool Configuration:
|
|
615
|
+
Set DEVDUCK_TOOLS for custom tools:
|
|
616
|
+
- Format: package:tool1,tool2:package2:tool3
|
|
617
|
+
- Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
618
|
+
- Static tools always loaded: tcp, websocket, ipc, mcp_server, agentcore_*
|
|
756
619
|
|
|
757
620
|
## Knowledge Base Integration:
|
|
758
|
-
- **Automatic RAG** - Set
|
|
621
|
+
- **Automatic RAG** - Set DEVDUCK_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
|
|
759
622
|
- Before each query: Retrieves relevant context from knowledge base
|
|
760
623
|
- After each response: Stores conversation for future reference
|
|
761
624
|
- Seamless memory across sessions without manual tool calls
|
|
762
625
|
|
|
763
|
-
##
|
|
626
|
+
## System Prompt Management:
|
|
627
|
+
- system_prompt(action='view') - View current
|
|
628
|
+
- system_prompt(action='update', prompt='...') - Update
|
|
629
|
+
- system_prompt(action='update', repository='owner/repo') - Sync to GitHub
|
|
630
|
+
|
|
631
|
+
## Shell Commands:
|
|
632
|
+
- Prefix with ! to run shell commands
|
|
633
|
+
- Example: ! ls -la
|
|
634
|
+
|
|
635
|
+
Response: MINIMAL WORDS, MAX PARALLELISM
|
|
764
636
|
|
|
765
|
-
|
|
637
|
+
## Tool Building Guide:
|
|
638
|
+
|
|
639
|
+
### **@tool Decorator (Recommended):**
|
|
766
640
|
```python
|
|
767
|
-
# ./tools/
|
|
641
|
+
# ./tools/my_tool.py
|
|
768
642
|
from strands import tool
|
|
769
643
|
|
|
770
644
|
@tool
|
|
771
|
-
def
|
|
772
|
-
\"\"\"
|
|
645
|
+
def my_tool(param1: str, param2: int = 10) -> str:
|
|
646
|
+
\"\"\"Tool description.
|
|
773
647
|
|
|
774
648
|
Args:
|
|
775
|
-
|
|
776
|
-
|
|
649
|
+
param1: Description of param1
|
|
650
|
+
param2: Description of param2 (default: 10)
|
|
777
651
|
|
|
778
652
|
Returns:
|
|
779
|
-
str:
|
|
653
|
+
str: Description of return value
|
|
780
654
|
\"\"\"
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
return f"Tip: {{tip:.2f}}, Total: {{total:.2f}}"
|
|
655
|
+
# Implementation
|
|
656
|
+
return f"Result: {{param1}} - {{param2}}"
|
|
784
657
|
```
|
|
785
658
|
|
|
786
|
-
### **
|
|
659
|
+
### **Action-Based Pattern:**
|
|
787
660
|
```python
|
|
788
|
-
# ./tools/weather.py
|
|
789
661
|
from typing import Dict, Any
|
|
790
662
|
from strands import tool
|
|
791
663
|
|
|
792
664
|
@tool
|
|
793
|
-
def
|
|
794
|
-
\"\"\"
|
|
665
|
+
def my_tool(action: str, data: str = None) -> Dict[str, Any]:
|
|
666
|
+
\"\"\"Multi-action tool.
|
|
795
667
|
|
|
796
668
|
Args:
|
|
797
|
-
action: Action to perform (
|
|
798
|
-
|
|
669
|
+
action: Action to perform (get, set, delete)
|
|
670
|
+
data: Optional data for action
|
|
799
671
|
|
|
800
672
|
Returns:
|
|
801
|
-
Dict
|
|
673
|
+
Dict with status and content
|
|
802
674
|
\"\"\"
|
|
803
|
-
if action == "
|
|
804
|
-
return {{"status": "success", "content": [{{"text": f"
|
|
805
|
-
elif action == "
|
|
806
|
-
return {{"status": "success", "content": [{{"text": f"
|
|
675
|
+
if action == "get":
|
|
676
|
+
return {{"status": "success", "content": [{{"text": f"Got: {{data}}"}}]}}
|
|
677
|
+
elif action == "set":
|
|
678
|
+
return {{"status": "success", "content": [{{"text": f"Set: {{data}}"}}]}}
|
|
807
679
|
else:
|
|
808
680
|
return {{"status": "error", "content": [{{"text": f"Unknown action: {{action}}"}}]}}
|
|
809
681
|
```
|
|
810
682
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
### 🧠 Self-Improvement Pattern:
|
|
819
|
-
When you learn something valuable during conversations:
|
|
820
|
-
1. Identify the new insight or pattern
|
|
821
|
-
2. Use system_prompt(action='add_context', context='...') to append it
|
|
822
|
-
3. Sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
|
|
823
|
-
4. New learnings persist across sessions via SYSTEM_PROMPT env var
|
|
824
|
-
|
|
825
|
-
**Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
|
|
826
|
-
|
|
827
|
-
## Shell Commands:
|
|
828
|
-
- Prefix with ! to execute shell commands directly
|
|
829
|
-
- Example: ! ls -la (lists files)
|
|
830
|
-
- Example: ! pwd (shows current directory)
|
|
831
|
-
|
|
832
|
-
**Response Format:**
|
|
833
|
-
- Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
|
|
834
|
-
- Communication: **MINIMAL WORDS**
|
|
835
|
-
- Efficiency: **Speed is paramount**
|
|
683
|
+
### **Tool Best Practices:**
|
|
684
|
+
1. Use type hints for all parameters
|
|
685
|
+
2. Provide clear docstrings
|
|
686
|
+
3. Return consistent formats (str or Dict[str, Any])
|
|
687
|
+
4. Use action-based pattern for complex tools
|
|
688
|
+
5. Handle errors gracefully
|
|
689
|
+
6. Log important operations
|
|
836
690
|
|
|
837
691
|
{os.getenv('SYSTEM_PROMPT', '')}"""
|
|
838
692
|
|
|
839
|
-
def
|
|
840
|
-
"""
|
|
841
|
-
|
|
842
|
-
|
|
693
|
+
def _view_logs_impl(self, action, lines, pattern):
|
|
694
|
+
"""Implementation of view_logs tool"""
|
|
695
|
+
try:
|
|
696
|
+
if action == "view":
|
|
697
|
+
if not LOG_FILE.exists():
|
|
698
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
699
|
+
with open(LOG_FILE, "r") as f:
|
|
700
|
+
all_lines = f.readlines()
|
|
701
|
+
recent = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
702
|
+
return {
|
|
703
|
+
"status": "success",
|
|
704
|
+
"content": [
|
|
705
|
+
{"text": f"Last {len(recent)} lines:\n\n{''.join(recent)}"}
|
|
706
|
+
],
|
|
707
|
+
}
|
|
843
708
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
709
|
+
elif action == "search":
|
|
710
|
+
if not pattern:
|
|
711
|
+
return {
|
|
712
|
+
"status": "error",
|
|
713
|
+
"content": [{"text": "pattern required"}],
|
|
714
|
+
}
|
|
715
|
+
if not LOG_FILE.exists():
|
|
716
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
717
|
+
with open(LOG_FILE, "r") as f:
|
|
718
|
+
matches = [line for line in f if pattern.lower() in line.lower()]
|
|
719
|
+
if not matches:
|
|
720
|
+
return {
|
|
721
|
+
"status": "success",
|
|
722
|
+
"content": [{"text": f"No matches for: {pattern}"}],
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
"status": "success",
|
|
726
|
+
"content": [
|
|
727
|
+
{
|
|
728
|
+
"text": f"Found {len(matches)} matches:\n\n{''.join(matches[-100:])}"
|
|
729
|
+
}
|
|
730
|
+
],
|
|
731
|
+
}
|
|
847
732
|
|
|
848
|
-
|
|
733
|
+
elif action == "clear":
|
|
734
|
+
if LOG_FILE.exists():
|
|
735
|
+
LOG_FILE.unlink()
|
|
736
|
+
return {"status": "success", "content": [{"text": "Logs cleared"}]}
|
|
849
737
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
738
|
+
else:
|
|
739
|
+
return {
|
|
740
|
+
"status": "error",
|
|
741
|
+
"content": [{"text": f"Unknown action: {action}"}],
|
|
742
|
+
}
|
|
743
|
+
except Exception as e:
|
|
744
|
+
return {"status": "error", "content": [{"text": f"Error: {e}"}]}
|
|
855
745
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
746
|
+
def _start_servers(self):
|
|
747
|
+
"""Auto-start servers"""
|
|
748
|
+
if self.enable_tcp:
|
|
859
749
|
try:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
)
|
|
864
|
-
if result.returncode == 0:
|
|
865
|
-
print(f"🦆 Successfully pulled {self.model}")
|
|
866
|
-
else:
|
|
867
|
-
print(f"🦆 Failed to pull {self.model}, trying fallback...")
|
|
868
|
-
# Fallback to basic models
|
|
869
|
-
fallback_models = ["llama3.2:1b", "qwen2.5:0.5b", "gemma2:2b"]
|
|
870
|
-
for fallback in fallback_models:
|
|
871
|
-
try:
|
|
872
|
-
subprocess.run(
|
|
873
|
-
["ollama", "pull", fallback],
|
|
874
|
-
capture_output=True,
|
|
875
|
-
timeout=30,
|
|
876
|
-
)
|
|
877
|
-
self.model = fallback
|
|
878
|
-
print(f"🦆 Using fallback model: {fallback}")
|
|
879
|
-
break
|
|
880
|
-
except:
|
|
881
|
-
continue
|
|
882
|
-
except Exception as pull_error:
|
|
883
|
-
print(f"🦆 Model pull failed: {pull_error}")
|
|
884
|
-
# Ultra-minimal fallback
|
|
885
|
-
self.model = "llama3.2:1b"
|
|
750
|
+
self.agent.tool.tcp(action="start_server", port=self.tcp_port)
|
|
751
|
+
print(f"🦆 ✓ TCP: localhost:{self.tcp_port}")
|
|
752
|
+
except Exception as e:
|
|
753
|
+
logger.warning(f"TCP server failed: {e}")
|
|
886
754
|
|
|
887
|
-
|
|
888
|
-
print("🦆 Ollama issue - checking service...")
|
|
755
|
+
if self.enable_ws:
|
|
889
756
|
try:
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
)
|
|
894
|
-
if result.returncode != 0:
|
|
895
|
-
print("🦆 Starting ollama service...")
|
|
896
|
-
subprocess.Popen(["ollama", "serve"])
|
|
897
|
-
import time
|
|
898
|
-
|
|
899
|
-
time.sleep(3) # Wait for service to start
|
|
900
|
-
except Exception as ollama_error:
|
|
901
|
-
print(f"🦆 Ollama service issue: {ollama_error}")
|
|
902
|
-
|
|
903
|
-
elif "import" in str(error).lower():
|
|
904
|
-
print("🦆 Import issue - reinstalling dependencies...")
|
|
905
|
-
ensure_deps()
|
|
757
|
+
self.agent.tool.websocket(action="start_server", port=self.ws_port)
|
|
758
|
+
print(f"🦆 ✓ WebSocket: localhost:{self.ws_port}")
|
|
759
|
+
except Exception as e:
|
|
760
|
+
logger.warning(f"WebSocket server failed: {e}")
|
|
906
761
|
|
|
907
|
-
|
|
908
|
-
print("🦆 Connection issue - checking ollama service...")
|
|
762
|
+
if self.enable_mcp:
|
|
909
763
|
try:
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
self.agent = None
|
|
921
|
-
|
|
922
|
-
def __call__(self, query):
|
|
923
|
-
"""Make the agent callable with automatic knowledge base integration"""
|
|
924
|
-
if not self.agent:
|
|
925
|
-
logger.warning("Agent unavailable - attempted to call with query")
|
|
926
|
-
return "🦆 Agent unavailable - try: devduck.restart()"
|
|
927
|
-
|
|
928
|
-
try:
|
|
929
|
-
logger.info(f"Agent call started: {query[:100]}...")
|
|
930
|
-
# Mark agent as executing to prevent hot-reload interruption
|
|
931
|
-
self._agent_executing = True
|
|
932
|
-
|
|
933
|
-
# 📚 Knowledge Base Retrieval (BEFORE agent runs)
|
|
934
|
-
knowledge_base_id = os.getenv("STRANDS_KNOWLEDGE_BASE_ID")
|
|
935
|
-
if knowledge_base_id and hasattr(self.agent, "tool"):
|
|
936
|
-
try:
|
|
937
|
-
if "retrieve" in self.agent.tool_names:
|
|
938
|
-
logger.info(f"Retrieving context from KB: {knowledge_base_id}")
|
|
939
|
-
self.agent.tool.retrieve(
|
|
940
|
-
text=query, knowledgeBaseId=knowledge_base_id
|
|
941
|
-
)
|
|
942
|
-
except Exception as e:
|
|
943
|
-
logger.warning(f"KB retrieval failed: {e}")
|
|
944
|
-
|
|
945
|
-
# Run the agent
|
|
946
|
-
result = self.agent(query)
|
|
947
|
-
|
|
948
|
-
# 💾 Knowledge Base Storage (AFTER agent runs)
|
|
949
|
-
if knowledge_base_id and hasattr(self.agent, "tool"):
|
|
950
|
-
try:
|
|
951
|
-
if "store_in_kb" in self.agent.tool_names:
|
|
952
|
-
|
|
953
|
-
conversation_content = f"Input: {query}, Result: {result!s}"
|
|
954
|
-
conversation_title = f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}"
|
|
955
|
-
self.agent.tool.store_in_kb(
|
|
956
|
-
content=conversation_content,
|
|
957
|
-
title=conversation_title,
|
|
958
|
-
knowledge_base_id=knowledge_base_id,
|
|
959
|
-
)
|
|
960
|
-
logger.info(f"Stored conversation in KB: {knowledge_base_id}")
|
|
961
|
-
except Exception as e:
|
|
962
|
-
logger.warning(f"KB storage failed: {e}")
|
|
963
|
-
|
|
964
|
-
# Agent finished - check if reload was pending
|
|
965
|
-
self._agent_executing = False
|
|
966
|
-
logger.info("Agent call completed successfully")
|
|
967
|
-
if self._reload_pending:
|
|
968
|
-
logger.info("Triggering pending hot-reload after agent completion")
|
|
969
|
-
print("🦆 Agent finished - triggering pending hot-reload...")
|
|
970
|
-
self.hot_reload()
|
|
971
|
-
|
|
972
|
-
return result
|
|
973
|
-
except Exception as e:
|
|
974
|
-
self._agent_executing = False # Reset flag on error
|
|
975
|
-
logger.error(f"Agent call failed with error: {e}")
|
|
976
|
-
self._self_heal(e)
|
|
977
|
-
if self.agent:
|
|
978
|
-
return self.agent(query)
|
|
979
|
-
else:
|
|
980
|
-
return f"🦆 Error: {e}"
|
|
764
|
+
self.agent.tool.mcp_server(
|
|
765
|
+
action="start",
|
|
766
|
+
transport="http",
|
|
767
|
+
port=self.mcp_port,
|
|
768
|
+
expose_agent=True,
|
|
769
|
+
agent=self.agent,
|
|
770
|
+
)
|
|
771
|
+
print(f"🦆 ✓ MCP: http://localhost:{self.mcp_port}/mcp")
|
|
772
|
+
except Exception as e:
|
|
773
|
+
logger.warning(f"MCP server failed: {e}")
|
|
981
774
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
775
|
+
if self.enable_ipc:
|
|
776
|
+
try:
|
|
777
|
+
self.agent.tool.ipc(action="start_server", socket_path=self.ipc_socket)
|
|
778
|
+
print(f"🦆 ✓ IPC: {self.ipc_socket}")
|
|
779
|
+
except Exception as e:
|
|
780
|
+
logger.warning(f"IPC server failed: {e}")
|
|
986
781
|
|
|
987
|
-
def
|
|
988
|
-
"""Start
|
|
989
|
-
import threading
|
|
782
|
+
def _start_hot_reload(self):
|
|
783
|
+
"""Start hot-reload file watcher"""
|
|
990
784
|
|
|
991
|
-
logger.info("Starting file watcher for hot-reload")
|
|
992
|
-
# Get the path to this file
|
|
993
785
|
self._watch_file = Path(__file__).resolve()
|
|
994
786
|
self._last_modified = (
|
|
995
787
|
self._watch_file.stat().st_mtime if self._watch_file.exists() else None
|
|
996
788
|
)
|
|
997
789
|
self._watcher_running = True
|
|
998
790
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
target=self._file_watcher_thread, daemon=True
|
|
1002
|
-
)
|
|
1003
|
-
self._watcher_thread.start()
|
|
1004
|
-
logger.info(f"File watcher started, monitoring {self._watch_file}")
|
|
1005
|
-
|
|
1006
|
-
def _file_watcher_thread(self):
|
|
1007
|
-
"""Background thread that watches for file changes"""
|
|
1008
|
-
import time
|
|
791
|
+
def watcher_thread():
|
|
792
|
+
import time
|
|
1009
793
|
|
|
1010
|
-
|
|
1011
|
-
|
|
794
|
+
last_reload = 0
|
|
795
|
+
debounce = 3 # seconds
|
|
1012
796
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
# Check if agent is currently executing
|
|
1036
|
-
if getattr(self, "_agent_executing", False):
|
|
1037
|
-
logger.info(
|
|
1038
|
-
"Code change detected but agent is executing - reload pending"
|
|
1039
|
-
)
|
|
1040
|
-
print(
|
|
1041
|
-
"🦆 Agent is currently executing - reload will trigger after completion"
|
|
1042
|
-
)
|
|
1043
|
-
self._reload_pending = True
|
|
797
|
+
while self._watcher_running:
|
|
798
|
+
try:
|
|
799
|
+
if self._watch_file.exists():
|
|
800
|
+
mtime = self._watch_file.stat().st_mtime
|
|
801
|
+
current_time = time.time()
|
|
802
|
+
|
|
803
|
+
if (
|
|
804
|
+
self._last_modified
|
|
805
|
+
and mtime > self._last_modified
|
|
806
|
+
and current_time - last_reload > debounce
|
|
807
|
+
):
|
|
808
|
+
|
|
809
|
+
print(f"🦆 Code changed - hot-reload triggered")
|
|
810
|
+
self._last_modified = mtime
|
|
811
|
+
last_reload = current_time
|
|
812
|
+
|
|
813
|
+
if self._agent_executing:
|
|
814
|
+
print("🦆 Reload pending (agent executing)")
|
|
815
|
+
self._reload_pending = True
|
|
816
|
+
else:
|
|
817
|
+
self._hot_reload()
|
|
1044
818
|
else:
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
)
|
|
1049
|
-
time.sleep(
|
|
1050
|
-
0.5
|
|
1051
|
-
) # Small delay to ensure file write is complete
|
|
1052
|
-
self.hot_reload()
|
|
1053
|
-
else:
|
|
1054
|
-
self._last_modified = current_mtime
|
|
819
|
+
self._last_modified = mtime
|
|
820
|
+
except Exception as e:
|
|
821
|
+
logger.error(f"File watcher error: {e}")
|
|
1055
822
|
|
|
1056
|
-
|
|
1057
|
-
print(f"🦆 File watcher error: {e}")
|
|
823
|
+
time.sleep(1)
|
|
1058
824
|
|
|
1059
|
-
|
|
1060
|
-
|
|
825
|
+
thread = threading.Thread(target=watcher_thread, daemon=True)
|
|
826
|
+
thread.start()
|
|
827
|
+
logger.info(f"Hot-reload watching: {self._watch_file}")
|
|
1061
828
|
|
|
1062
|
-
def
|
|
1063
|
-
"""
|
|
829
|
+
def _hot_reload(self):
|
|
830
|
+
"""Hot-reload by restarting process"""
|
|
831
|
+
logger.info("Hot-reload: restarting process")
|
|
1064
832
|
self._watcher_running = False
|
|
1065
|
-
|
|
833
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
1066
834
|
|
|
1067
|
-
def
|
|
1068
|
-
"""
|
|
1069
|
-
|
|
1070
|
-
|
|
835
|
+
def __call__(self, query):
|
|
836
|
+
"""Call agent with KB integration"""
|
|
837
|
+
if not self.agent:
|
|
838
|
+
return "🦆 Agent unavailable"
|
|
1071
839
|
|
|
1072
840
|
try:
|
|
1073
|
-
|
|
1074
|
-
if hasattr(self, "_is_reloading") and self._is_reloading:
|
|
1075
|
-
print("🦆 Reload already in progress, skipping")
|
|
1076
|
-
return
|
|
841
|
+
self._agent_executing = True
|
|
1077
842
|
|
|
1078
|
-
|
|
843
|
+
# KB retrieval
|
|
844
|
+
kb_id = os.getenv("DEVDUCK_KNOWLEDGE_BASE_ID")
|
|
845
|
+
if kb_id:
|
|
846
|
+
try:
|
|
847
|
+
self.agent.tool.retrieve(text=query, knowledgeBaseId=kb_id)
|
|
848
|
+
except:
|
|
849
|
+
pass
|
|
1079
850
|
|
|
1080
|
-
#
|
|
1081
|
-
|
|
1082
|
-
self._watcher_running = False
|
|
851
|
+
# Run agent
|
|
852
|
+
result = self.agent(query)
|
|
1083
853
|
|
|
1084
|
-
|
|
854
|
+
# KB storage
|
|
855
|
+
if kb_id:
|
|
856
|
+
try:
|
|
857
|
+
self.agent.tool.store_in_kb(
|
|
858
|
+
content=f"Input: {query}\nResult: {str(result)}",
|
|
859
|
+
title=f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}",
|
|
860
|
+
knowledge_base_id=kb_id,
|
|
861
|
+
)
|
|
862
|
+
except:
|
|
863
|
+
pass
|
|
1085
864
|
|
|
1086
|
-
|
|
1087
|
-
# This ensures all code is freshly loaded
|
|
1088
|
-
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
865
|
+
self._agent_executing = False
|
|
1089
866
|
|
|
867
|
+
# Check for pending reload
|
|
868
|
+
if self._reload_pending:
|
|
869
|
+
print("🦆 Agent finished - triggering pending reload")
|
|
870
|
+
self._hot_reload()
|
|
871
|
+
|
|
872
|
+
return result
|
|
1090
873
|
except Exception as e:
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
return {
|
|
1098
|
-
"model": self.model,
|
|
1099
|
-
"host": self.ollama_host,
|
|
1100
|
-
"env": self.env_info,
|
|
1101
|
-
"agent_ready": self.agent is not None,
|
|
1102
|
-
"tools": len(self.tools) if hasattr(self, "tools") else 0,
|
|
1103
|
-
"file_watcher": {
|
|
1104
|
-
"enabled": hasattr(self, "_watcher_running") and self._watcher_running,
|
|
1105
|
-
"watching": (
|
|
1106
|
-
str(self._watch_file) if hasattr(self, "_watch_file") else None
|
|
1107
|
-
),
|
|
1108
|
-
},
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
# 🦆 Auto-initialize when imported
|
|
874
|
+
self._agent_executing = False
|
|
875
|
+
logger.error(f"Agent call failed: {e}")
|
|
876
|
+
return f"🦆 Error: {e}"
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
# Initialize
|
|
1113
880
|
# Check environment variables to control server configuration
|
|
1114
|
-
# Also check if --mcp flag is present to skip auto-starting servers
|
|
1115
881
|
_auto_start = os.getenv("DEVDUCK_AUTO_START_SERVERS", "true").lower() == "true"
|
|
1116
882
|
|
|
1117
883
|
# Disable auto-start if --mcp flag is present (stdio mode)
|
|
@@ -1121,42 +887,30 @@ if "--mcp" in sys.argv:
|
|
|
1121
887
|
_tcp_port = int(os.getenv("DEVDUCK_TCP_PORT", "9999"))
|
|
1122
888
|
_ws_port = int(os.getenv("DEVDUCK_WS_PORT", "8080"))
|
|
1123
889
|
_mcp_port = int(os.getenv("DEVDUCK_MCP_PORT", "8000"))
|
|
890
|
+
_ipc_socket = os.getenv("DEVDUCK_IPC_SOCKET", None)
|
|
1124
891
|
_enable_tcp = os.getenv("DEVDUCK_ENABLE_TCP", "true").lower() == "true"
|
|
1125
892
|
_enable_ws = os.getenv("DEVDUCK_ENABLE_WS", "true").lower() == "true"
|
|
1126
893
|
_enable_mcp = os.getenv("DEVDUCK_ENABLE_MCP", "true").lower() == "true"
|
|
894
|
+
_enable_ipc = os.getenv("DEVDUCK_ENABLE_IPC", "true").lower() == "true"
|
|
1127
895
|
|
|
1128
896
|
devduck = DevDuck(
|
|
1129
897
|
auto_start_servers=_auto_start,
|
|
1130
898
|
tcp_port=_tcp_port,
|
|
1131
899
|
ws_port=_ws_port,
|
|
1132
900
|
mcp_port=_mcp_port,
|
|
901
|
+
ipc_socket=_ipc_socket,
|
|
1133
902
|
enable_tcp=_enable_tcp,
|
|
1134
903
|
enable_ws=_enable_ws,
|
|
1135
904
|
enable_mcp=_enable_mcp,
|
|
905
|
+
enable_ipc=_enable_ipc,
|
|
1136
906
|
)
|
|
1137
907
|
|
|
1138
908
|
|
|
1139
|
-
# 🚀 Convenience functions
|
|
1140
909
|
def ask(query):
|
|
1141
|
-
"""Quick query
|
|
910
|
+
"""Quick query"""
|
|
1142
911
|
return devduck(query)
|
|
1143
912
|
|
|
1144
913
|
|
|
1145
|
-
def status():
|
|
1146
|
-
"""Quick status check"""
|
|
1147
|
-
return devduck.status()
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
def restart():
|
|
1151
|
-
"""Quick restart"""
|
|
1152
|
-
devduck.restart()
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
def hot_reload():
|
|
1156
|
-
"""Quick hot-reload without restart"""
|
|
1157
|
-
devduck.hot_reload()
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
914
|
def extract_commands_from_history():
|
|
1161
915
|
"""Extract commonly used commands from shell history for auto-completion."""
|
|
1162
916
|
commands = set()
|
|
@@ -1230,7 +984,8 @@ def extract_commands_from_history():
|
|
|
1230
984
|
|
|
1231
985
|
|
|
1232
986
|
def interactive():
|
|
1233
|
-
"""Interactive REPL
|
|
987
|
+
"""Interactive REPL with history"""
|
|
988
|
+
import time
|
|
1234
989
|
from prompt_toolkit import prompt
|
|
1235
990
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
1236
991
|
from prompt_toolkit.completion import WordCompleter
|
|
@@ -1238,14 +993,10 @@ def interactive():
|
|
|
1238
993
|
|
|
1239
994
|
print("🦆 DevDuck")
|
|
1240
995
|
print(f"📝 Logs: {LOG_DIR}")
|
|
1241
|
-
print("Type 'exit'
|
|
1242
|
-
print("Prefix with ! to run shell commands (e.g., ! ls -la)")
|
|
996
|
+
print("Type 'exit' to quit. Prefix with ! for shell commands.")
|
|
1243
997
|
print("-" * 50)
|
|
1244
|
-
logger.info("Interactive mode started")
|
|
1245
998
|
|
|
1246
|
-
|
|
1247
|
-
history_file = get_shell_history_file()
|
|
1248
|
-
history = FileHistory(history_file)
|
|
999
|
+
history = FileHistory(get_shell_history_file())
|
|
1249
1000
|
|
|
1250
1001
|
# Create completions from common commands and shell history
|
|
1251
1002
|
base_commands = ["exit", "quit", "q", "help", "clear", "status", "reload"]
|
|
@@ -1255,164 +1006,72 @@ def interactive():
|
|
|
1255
1006
|
all_commands = list(set(base_commands + history_commands))
|
|
1256
1007
|
completer = WordCompleter(all_commands, ignore_case=True)
|
|
1257
1008
|
|
|
1009
|
+
# Track consecutive interrupts for double Ctrl+C to exit
|
|
1010
|
+
interrupt_count = 0
|
|
1011
|
+
last_interrupt = 0
|
|
1012
|
+
|
|
1258
1013
|
while True:
|
|
1259
1014
|
try:
|
|
1260
|
-
# Use prompt_toolkit for enhanced input with arrow key support
|
|
1261
1015
|
q = prompt(
|
|
1262
1016
|
"\n🦆 ",
|
|
1263
1017
|
history=history,
|
|
1264
1018
|
auto_suggest=AutoSuggestFromHistory(),
|
|
1265
1019
|
completer=completer,
|
|
1266
1020
|
complete_while_typing=True,
|
|
1267
|
-
mouse_support=False,
|
|
1268
|
-
)
|
|
1021
|
+
mouse_support=False,
|
|
1022
|
+
).strip()
|
|
1023
|
+
|
|
1024
|
+
# Reset interrupt count on successful prompt
|
|
1025
|
+
interrupt_count = 0
|
|
1269
1026
|
|
|
1270
|
-
# Check for exit command
|
|
1271
1027
|
if q.lower() in ["exit", "quit", "q"]:
|
|
1272
|
-
print("\n🦆 Goodbye!")
|
|
1273
1028
|
break
|
|
1274
1029
|
|
|
1275
|
-
|
|
1276
|
-
if q.strip() == "":
|
|
1030
|
+
if not q:
|
|
1277
1031
|
continue
|
|
1278
1032
|
|
|
1279
|
-
#
|
|
1033
|
+
# Shell commands
|
|
1280
1034
|
if q.startswith("!"):
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
command=shell_command, timeout=9000
|
|
1289
|
-
)
|
|
1290
|
-
devduck._agent_executing = False
|
|
1291
|
-
|
|
1292
|
-
# Append shell command to history
|
|
1293
|
-
append_to_shell_history(q, result["content"][0]["text"])
|
|
1294
|
-
|
|
1295
|
-
# Check if reload was pending
|
|
1296
|
-
if devduck._reload_pending:
|
|
1297
|
-
print(
|
|
1298
|
-
"🦆 Shell command finished - triggering pending hot-reload..."
|
|
1299
|
-
)
|
|
1300
|
-
devduck.hot_reload()
|
|
1301
|
-
else:
|
|
1302
|
-
print("🦆 Agent unavailable")
|
|
1303
|
-
except Exception as e:
|
|
1304
|
-
devduck._agent_executing = False # Reset on error
|
|
1305
|
-
print(f"🦆 Shell command error: {e}")
|
|
1306
|
-
continue
|
|
1307
|
-
|
|
1308
|
-
# Get recent conversation context
|
|
1309
|
-
recent_context = get_last_messages()
|
|
1310
|
-
|
|
1311
|
-
# Get recent logs
|
|
1312
|
-
recent_logs = get_recent_logs()
|
|
1313
|
-
|
|
1314
|
-
# Update system prompt before each call with history context
|
|
1315
|
-
if devduck.agent:
|
|
1316
|
-
# Rebuild system prompt with history
|
|
1317
|
-
own_code = get_own_source_code()
|
|
1318
|
-
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
1319
|
-
|
|
1320
|
-
devduck.agent.system_prompt = f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
|
|
1321
|
-
|
|
1322
|
-
Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
|
|
1323
|
-
Python: {devduck.env_info['python']}
|
|
1324
|
-
Model: {devduck.model}
|
|
1325
|
-
Hostname: {devduck.env_info['hostname']}
|
|
1326
|
-
Session ID: {session_id}
|
|
1327
|
-
|
|
1328
|
-
You are:
|
|
1329
|
-
- Minimalist: Brief, direct responses
|
|
1330
|
-
- Self-healing: Adapt when things break
|
|
1331
|
-
- Efficient: Get things done fast
|
|
1332
|
-
- Pragmatic: Use what works
|
|
1333
|
-
|
|
1334
|
-
Current working directory: {devduck.env_info['cwd']}
|
|
1335
|
-
|
|
1336
|
-
{recent_context}
|
|
1337
|
-
{recent_logs}
|
|
1338
|
-
|
|
1339
|
-
## Your Own Implementation:
|
|
1340
|
-
You have full access to your own source code for self-awareness and self-modification:
|
|
1341
|
-
|
|
1342
|
-
{own_code}
|
|
1343
|
-
|
|
1344
|
-
## Hot Reload System Active:
|
|
1345
|
-
- **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
|
|
1346
|
-
- **No Restart Needed** - Tools are auto-loaded and ready to use instantly
|
|
1347
|
-
- **Live Development** - Modify existing tools while running and test immediately
|
|
1348
|
-
- **Full Python Access** - Create any Python functionality as a tool
|
|
1349
|
-
|
|
1350
|
-
## Dynamic Tool Loading:
|
|
1351
|
-
- **Install Tools** - Use install_tools() to load tools from any Python package
|
|
1352
|
-
- Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
|
|
1353
|
-
- Expands capabilities without restart
|
|
1354
|
-
- Access to entire Python ecosystem
|
|
1355
|
-
|
|
1356
|
-
## MCP Server:
|
|
1357
|
-
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
1358
|
-
- Example: mcp_server(action="start", port=8000)
|
|
1359
|
-
- Connect from Claude Desktop, other agents, or custom clients
|
|
1360
|
-
- Full bidirectional communication
|
|
1361
|
-
|
|
1362
|
-
## System Prompt Management:
|
|
1363
|
-
- **View**: system_prompt(action='view') - See current prompt
|
|
1364
|
-
- **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
|
|
1365
|
-
- **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
|
|
1366
|
-
- **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
|
|
1367
|
-
- **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
|
|
1368
|
-
|
|
1369
|
-
### 🧠 Self-Improvement Pattern:
|
|
1370
|
-
When you learn something valuable during conversations:
|
|
1371
|
-
1. Identify the new insight or pattern
|
|
1372
|
-
2. Use system_prompt(action='add_context', context='...') to append it
|
|
1373
|
-
3. Optionally sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
|
|
1374
|
-
4. New learnings persist across sessions via SYSTEM_PROMPT env var
|
|
1375
|
-
|
|
1376
|
-
**Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
|
|
1377
|
-
|
|
1378
|
-
## Shell Commands:
|
|
1379
|
-
- Prefix with ! to execute shell commands directly
|
|
1380
|
-
- Example: ! ls -la (lists files)
|
|
1381
|
-
- Example: ! pwd (shows current directory)
|
|
1382
|
-
|
|
1383
|
-
**Response Format:**
|
|
1384
|
-
- Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
|
|
1385
|
-
- Communication: **MINIMAL WORDS**
|
|
1386
|
-
- Efficiency: **Speed is paramount**
|
|
1387
|
-
|
|
1388
|
-
{os.getenv('SYSTEM_PROMPT', '')}"""
|
|
1389
|
-
|
|
1390
|
-
# Update model if MODEL_PROVIDER changed
|
|
1391
|
-
model_provider = os.getenv("MODEL_PROVIDER")
|
|
1392
|
-
if model_provider:
|
|
1393
|
-
try:
|
|
1394
|
-
from strands_tools.utils.models.model import create_model
|
|
1035
|
+
if devduck.agent:
|
|
1036
|
+
devduck._agent_executing = True
|
|
1037
|
+
result = devduck.agent.tool.shell(
|
|
1038
|
+
command=q[1:].strip(), timeout=9000
|
|
1039
|
+
)
|
|
1040
|
+
devduck._agent_executing = False
|
|
1041
|
+
append_to_shell_history(q, result["content"][0]["text"])
|
|
1395
1042
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1043
|
+
if devduck._reload_pending:
|
|
1044
|
+
print("🦆 Shell finished - triggering pending reload")
|
|
1045
|
+
devduck._hot_reload()
|
|
1046
|
+
continue
|
|
1399
1047
|
|
|
1400
|
-
#
|
|
1048
|
+
# Agent query
|
|
1401
1049
|
result = ask(q)
|
|
1402
|
-
|
|
1403
|
-
# Append to shell history
|
|
1050
|
+
print(result)
|
|
1404
1051
|
append_to_shell_history(q, str(result))
|
|
1405
1052
|
|
|
1406
1053
|
except KeyboardInterrupt:
|
|
1407
|
-
|
|
1408
|
-
|
|
1054
|
+
current_time = time.time()
|
|
1055
|
+
|
|
1056
|
+
# Check if this is a consecutive interrupt within 2 seconds
|
|
1057
|
+
if current_time - last_interrupt < 2:
|
|
1058
|
+
interrupt_count += 1
|
|
1059
|
+
if interrupt_count >= 2:
|
|
1060
|
+
print("\n🦆 Exiting...")
|
|
1061
|
+
break
|
|
1062
|
+
else:
|
|
1063
|
+
print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
|
|
1064
|
+
else:
|
|
1065
|
+
interrupt_count = 1
|
|
1066
|
+
print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
|
|
1067
|
+
|
|
1068
|
+
last_interrupt = current_time
|
|
1409
1069
|
except Exception as e:
|
|
1410
1070
|
print(f"🦆 Error: {e}")
|
|
1411
|
-
continue
|
|
1412
1071
|
|
|
1413
1072
|
|
|
1414
1073
|
def cli():
|
|
1415
|
-
"""CLI entry point
|
|
1074
|
+
"""CLI entry point"""
|
|
1416
1075
|
import argparse
|
|
1417
1076
|
|
|
1418
1077
|
parser = argparse.ArgumentParser(
|
|
@@ -1420,14 +1079,16 @@ def cli():
|
|
|
1420
1079
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1421
1080
|
epilog="""
|
|
1422
1081
|
Examples:
|
|
1423
|
-
devduck #
|
|
1424
|
-
devduck "
|
|
1425
|
-
devduck --mcp # MCP stdio mode
|
|
1082
|
+
devduck # Interactive mode
|
|
1083
|
+
devduck "query" # One-shot query
|
|
1084
|
+
devduck --mcp # MCP stdio mode
|
|
1426
1085
|
devduck --tcp-port 9000 # Custom TCP port
|
|
1427
1086
|
devduck --no-tcp --no-ws # Disable TCP and WebSocket
|
|
1428
|
-
devduck --mcp-port 3000 # Custom MCP port
|
|
1429
1087
|
|
|
1430
|
-
|
|
1088
|
+
Tool Configuration:
|
|
1089
|
+
export DEVDUCK_TOOLS="strands_tools:shell,editor:strands_fun_tools:clipboard"
|
|
1090
|
+
|
|
1091
|
+
MCP Config:
|
|
1431
1092
|
{
|
|
1432
1093
|
"mcpServers": {
|
|
1433
1094
|
"devduck": {
|
|
@@ -1439,15 +1100,8 @@ Claude Desktop Config:
|
|
|
1439
1100
|
""",
|
|
1440
1101
|
)
|
|
1441
1102
|
|
|
1442
|
-
|
|
1443
|
-
parser.add_argument("
|
|
1444
|
-
|
|
1445
|
-
# MCP stdio mode flag
|
|
1446
|
-
parser.add_argument(
|
|
1447
|
-
"--mcp",
|
|
1448
|
-
action="store_true",
|
|
1449
|
-
help="Start MCP server in stdio mode (for Claude Desktop integration)",
|
|
1450
|
-
)
|
|
1103
|
+
parser.add_argument("query", nargs="*", help="Query")
|
|
1104
|
+
parser.add_argument("--mcp", action="store_true", help="MCP stdio mode")
|
|
1451
1105
|
|
|
1452
1106
|
# Server configuration
|
|
1453
1107
|
parser.add_argument(
|
|
@@ -1465,65 +1119,53 @@ Claude Desktop Config:
|
|
|
1465
1119
|
default=8000,
|
|
1466
1120
|
help="MCP HTTP server port (default: 8000)",
|
|
1467
1121
|
)
|
|
1122
|
+
parser.add_argument(
|
|
1123
|
+
"--ipc-socket",
|
|
1124
|
+
type=str,
|
|
1125
|
+
default=None,
|
|
1126
|
+
help="IPC socket path (default: /tmp/devduck_main.sock)",
|
|
1127
|
+
)
|
|
1468
1128
|
|
|
1469
1129
|
# Server enable/disable flags
|
|
1470
1130
|
parser.add_argument("--no-tcp", action="store_true", help="Disable TCP server")
|
|
1471
1131
|
parser.add_argument("--no-ws", action="store_true", help="Disable WebSocket server")
|
|
1472
1132
|
parser.add_argument("--no-mcp", action="store_true", help="Disable MCP server")
|
|
1133
|
+
parser.add_argument("--no-ipc", action="store_true", help="Disable IPC server")
|
|
1473
1134
|
parser.add_argument(
|
|
1474
1135
|
"--no-servers",
|
|
1475
1136
|
action="store_true",
|
|
1476
|
-
help="Disable all servers (no TCP, WebSocket, or
|
|
1137
|
+
help="Disable all servers (no TCP, WebSocket, MCP, or IPC)",
|
|
1477
1138
|
)
|
|
1478
1139
|
|
|
1479
1140
|
args = parser.parse_args()
|
|
1480
1141
|
|
|
1481
|
-
logger.info("CLI mode started")
|
|
1482
|
-
|
|
1483
|
-
# Handle --mcp flag for stdio mode
|
|
1484
1142
|
if args.mcp:
|
|
1485
|
-
logger.info("Starting MCP server in stdio mode (blocking, foreground)")
|
|
1486
1143
|
print("🦆 Starting MCP stdio server...", file=sys.stderr)
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
devduck.agent
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
agent=devduck.agent,
|
|
1497
|
-
)
|
|
1498
|
-
except Exception as e:
|
|
1499
|
-
logger.error(f"Failed to start MCP stdio server: {e}")
|
|
1500
|
-
print(f"🦆 Error: {e}", file=sys.stderr)
|
|
1501
|
-
sys.exit(1)
|
|
1502
|
-
else:
|
|
1503
|
-
print("🦆 Agent not available", file=sys.stderr)
|
|
1144
|
+
try:
|
|
1145
|
+
devduck.agent.tool.mcp_server(
|
|
1146
|
+
action="start",
|
|
1147
|
+
transport="stdio",
|
|
1148
|
+
expose_agent=True,
|
|
1149
|
+
agent=devduck.agent,
|
|
1150
|
+
)
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
print(f"🦆 Error: {e}", file=sys.stderr)
|
|
1504
1153
|
sys.exit(1)
|
|
1505
1154
|
return
|
|
1506
1155
|
|
|
1507
1156
|
if args.query:
|
|
1508
|
-
|
|
1509
|
-
logger.info(f"CLI query: {query}")
|
|
1510
|
-
result = ask(query)
|
|
1157
|
+
result = ask(" ".join(args.query))
|
|
1511
1158
|
print(result)
|
|
1512
1159
|
else:
|
|
1513
|
-
# No arguments - start interactive mode
|
|
1514
1160
|
interactive()
|
|
1515
1161
|
|
|
1516
1162
|
|
|
1517
|
-
#
|
|
1163
|
+
# Make module callable
|
|
1518
1164
|
class CallableModule(sys.modules[__name__].__class__):
|
|
1519
|
-
"""Make the module itself callable"""
|
|
1520
|
-
|
|
1521
1165
|
def __call__(self, query):
|
|
1522
|
-
"""Allow direct module call: import devduck; devduck("query")"""
|
|
1523
1166
|
return ask(query)
|
|
1524
1167
|
|
|
1525
1168
|
|
|
1526
|
-
# Replace module in sys.modules with callable version
|
|
1527
1169
|
sys.modules[__name__].__class__ = CallableModule
|
|
1528
1170
|
|
|
1529
1171
|
|