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