devduck 0.1.1766644714__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of devduck might be problematic. Click here for more details.
- devduck/__init__.py +700 -1027
- devduck/_version.py +2 -2
- devduck/test_redduck.py +1 -0
- devduck/tools/__init__.py +4 -44
- devduck/tools/install_tools.py +2 -103
- devduck/tools/mcp_server.py +6 -34
- devduck/tools/tcp.py +4 -6
- devduck/tools/websocket.py +1 -7
- devduck-0.3.0.dist-info/METADATA +152 -0
- devduck-0.3.0.dist-info/RECORD +18 -0
- {devduck-0.1.1766644714.dist-info → devduck-0.3.0.dist-info}/entry_points.txt +0 -1
- devduck-0.3.0.dist-info/licenses/LICENSE +21 -0
- devduck/agentcore_handler.py +0 -76
- devduck/tools/_ambient_input.py +0 -423
- devduck/tools/_tray_app.py +0 -530
- devduck/tools/agentcore_agents.py +0 -197
- devduck/tools/agentcore_config.py +0 -441
- devduck/tools/agentcore_invoke.py +0 -423
- devduck/tools/agentcore_logs.py +0 -320
- devduck/tools/ambient.py +0 -157
- devduck/tools/fetch_github_tool.py +0 -201
- devduck/tools/ipc.py +0 -546
- devduck/tools/scraper.py +0 -935
- devduck/tools/speech_to_speech.py +0 -850
- devduck/tools/state_manager.py +0 -292
- devduck/tools/system_prompt.py +0 -608
- devduck/tools/tray.py +0 -247
- devduck-0.1.1766644714.dist-info/METADATA +0 -717
- devduck-0.1.1766644714.dist-info/RECORD +0 -33
- devduck-0.1.1766644714.dist-info/licenses/LICENSE +0 -201
- {devduck-0.1.1766644714.dist-info → devduck-0.3.0.dist-info}/WHEEL +0 -0
- {devduck-0.1.1766644714.dist-info → devduck-0.3.0.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -3,46 +3,32 @@
|
|
|
3
3
|
🦆 devduck - extreme minimalist self-adapting agent
|
|
4
4
|
one file. self-healing. runtime dependencies. adaptive.
|
|
5
5
|
"""
|
|
6
|
-
import os
|
|
7
6
|
import sys
|
|
8
7
|
import subprocess
|
|
9
|
-
import
|
|
8
|
+
import os
|
|
10
9
|
import platform
|
|
11
10
|
import socket
|
|
12
11
|
import logging
|
|
13
12
|
import tempfile
|
|
14
|
-
import
|
|
15
|
-
import warnings
|
|
16
|
-
import json
|
|
13
|
+
from datetime import datetime
|
|
17
14
|
from pathlib import Path
|
|
18
15
|
from datetime import datetime
|
|
19
16
|
from typing import Dict, Any
|
|
20
17
|
from logging.handlers import RotatingFileHandler
|
|
21
|
-
from strands import Agent, tool
|
|
22
|
-
|
|
23
|
-
# Import system prompt helper for loading prompts from files
|
|
24
|
-
try:
|
|
25
|
-
from devduck.tools.system_prompt import _get_system_prompt
|
|
26
|
-
except ImportError:
|
|
27
|
-
# Fallback if tools module not available yet
|
|
28
|
-
def _get_system_prompt(repository=None, variable_name="SYSTEM_PROMPT"):
|
|
29
|
-
return os.getenv(variable_name, "")
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
|
|
33
|
-
warnings.filterwarnings("ignore", message=".*cache_prompt is deprecated.*")
|
|
34
|
-
|
|
35
|
-
os.environ["BYPASS_TOOL_CONSENT"] = os.getenv("BYPASS_TOOL_CONSENT", "true")
|
|
19
|
+
os.environ["BYPASS_TOOL_CONSENT"] = "true"
|
|
36
20
|
os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
|
|
37
|
-
os.environ["EDITOR_DISABLE_BACKUP"] = "true"
|
|
38
21
|
|
|
22
|
+
# 📝 Setup logging system
|
|
39
23
|
LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
|
|
40
24
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
-
|
|
42
25
|
LOG_FILE = LOG_DIR / "devduck.log"
|
|
26
|
+
|
|
27
|
+
# Configure logger
|
|
43
28
|
logger = logging.getLogger("devduck")
|
|
44
29
|
logger.setLevel(logging.DEBUG)
|
|
45
30
|
|
|
31
|
+
# File handler with rotation (10MB max, keep 3 backups)
|
|
46
32
|
file_handler = RotatingFileHandler(
|
|
47
33
|
LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
|
|
48
34
|
)
|
|
@@ -52,6 +38,7 @@ file_formatter = logging.Formatter(
|
|
|
52
38
|
)
|
|
53
39
|
file_handler.setFormatter(file_formatter)
|
|
54
40
|
|
|
41
|
+
# Console handler (only warnings and above)
|
|
55
42
|
console_handler = logging.StreamHandler()
|
|
56
43
|
console_handler.setLevel(logging.WARNING)
|
|
57
44
|
console_formatter = logging.Formatter("🦆 %(levelname)s: %(message)s")
|
|
@@ -63,13 +50,242 @@ logger.addHandler(console_handler)
|
|
|
63
50
|
logger.info("DevDuck logging system initialized")
|
|
64
51
|
|
|
65
52
|
|
|
53
|
+
# 🔧 Self-healing dependency installer
|
|
54
|
+
def ensure_deps():
|
|
55
|
+
"""Install core dependencies at runtime if missing"""
|
|
56
|
+
import importlib.metadata
|
|
57
|
+
|
|
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
|
|
66
113
|
def get_own_source_code():
|
|
67
|
-
"""
|
|
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
|
+
"""
|
|
68
120
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
# Read this file (__init__.py)
|
|
122
|
+
current_file = __file__
|
|
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```"
|
|
71
126
|
except Exception as e:
|
|
72
|
-
return f"Error reading source: {e}"
|
|
127
|
+
return f"Error reading own source code: {e}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# 🛠️ System prompt tool (with .prompt file persistence)
|
|
131
|
+
def system_prompt_tool(
|
|
132
|
+
action: str,
|
|
133
|
+
prompt: str | None = None,
|
|
134
|
+
context: str | None = None,
|
|
135
|
+
variable_name: str = "SYSTEM_PROMPT",
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
"""
|
|
138
|
+
Manage the agent's system prompt dynamically with file persistence.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
action: "view", "update", "add_context", or "reset"
|
|
142
|
+
prompt: New system prompt text (required for "update")
|
|
143
|
+
context: Additional context to prepend (for "add_context")
|
|
144
|
+
variable_name: Environment variable name (default: SYSTEM_PROMPT)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dict with status and content
|
|
148
|
+
"""
|
|
149
|
+
from pathlib import Path
|
|
150
|
+
import tempfile
|
|
151
|
+
|
|
152
|
+
def _get_prompt_file_path() -> Path:
|
|
153
|
+
"""Get the .prompt file path in temp directory."""
|
|
154
|
+
temp_dir = Path(tempfile.gettempdir()) / ".devduck"
|
|
155
|
+
temp_dir.mkdir(exist_ok=True, mode=0o700) # Create with restrictive permissions
|
|
156
|
+
return temp_dir / ".prompt"
|
|
157
|
+
|
|
158
|
+
def _write_prompt_file(prompt_text: str) -> None:
|
|
159
|
+
"""Write prompt to .prompt file in temp directory."""
|
|
160
|
+
prompt_file = _get_prompt_file_path()
|
|
161
|
+
try:
|
|
162
|
+
# Create file with restrictive permissions
|
|
163
|
+
with open(
|
|
164
|
+
prompt_file,
|
|
165
|
+
"w",
|
|
166
|
+
encoding="utf-8",
|
|
167
|
+
opener=lambda path, flags: os.open(path, flags, 0o600),
|
|
168
|
+
) as f:
|
|
169
|
+
f.write(prompt_text)
|
|
170
|
+
except (OSError, PermissionError):
|
|
171
|
+
try:
|
|
172
|
+
prompt_file.write_text(prompt_text, encoding="utf-8")
|
|
173
|
+
prompt_file.chmod(0o600)
|
|
174
|
+
except (OSError, PermissionError):
|
|
175
|
+
prompt_file.write_text(prompt_text, encoding="utf-8")
|
|
176
|
+
|
|
177
|
+
def _get_system_prompt(var_name: str) -> str:
|
|
178
|
+
"""Get current system prompt from environment variable."""
|
|
179
|
+
return os.environ.get(var_name, "")
|
|
180
|
+
|
|
181
|
+
def _update_system_prompt(new_prompt: str, var_name: str) -> None:
|
|
182
|
+
"""Update system prompt in both environment and .prompt file."""
|
|
183
|
+
os.environ[var_name] = new_prompt
|
|
184
|
+
if var_name == "SYSTEM_PROMPT":
|
|
185
|
+
_write_prompt_file(new_prompt)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
if action == "view":
|
|
189
|
+
current = _get_system_prompt(variable_name)
|
|
190
|
+
return {
|
|
191
|
+
"status": "success",
|
|
192
|
+
"content": [
|
|
193
|
+
{"text": f"Current system prompt from {variable_name}:{current}"}
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
elif action == "update":
|
|
198
|
+
if not prompt:
|
|
199
|
+
return {
|
|
200
|
+
"status": "error",
|
|
201
|
+
"content": [
|
|
202
|
+
{"text": "Error: prompt parameter required for update action"}
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_update_system_prompt(prompt, variable_name)
|
|
207
|
+
|
|
208
|
+
if variable_name == "SYSTEM_PROMPT":
|
|
209
|
+
message = f"System prompt updated (env: {variable_name}, file: .prompt)"
|
|
210
|
+
else:
|
|
211
|
+
message = f"System prompt updated (env: {variable_name})"
|
|
212
|
+
|
|
213
|
+
return {"status": "success", "content": [{"text": message}]}
|
|
214
|
+
|
|
215
|
+
elif action == "add_context":
|
|
216
|
+
if not context:
|
|
217
|
+
return {
|
|
218
|
+
"status": "error",
|
|
219
|
+
"content": [
|
|
220
|
+
{
|
|
221
|
+
"text": "Error: context parameter required for add_context action"
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
current = _get_system_prompt(variable_name)
|
|
227
|
+
new_prompt = f"{current} {context}" if current else context
|
|
228
|
+
_update_system_prompt(new_prompt, variable_name)
|
|
229
|
+
|
|
230
|
+
if variable_name == "SYSTEM_PROMPT":
|
|
231
|
+
message = f"Context added to system prompt (env: {variable_name}, file: .prompt)"
|
|
232
|
+
else:
|
|
233
|
+
message = f"Context added to system prompt (env: {variable_name})"
|
|
234
|
+
|
|
235
|
+
return {"status": "success", "content": [{"text": message}]}
|
|
236
|
+
|
|
237
|
+
elif action == "reset":
|
|
238
|
+
os.environ.pop(variable_name, None)
|
|
239
|
+
|
|
240
|
+
if variable_name == "SYSTEM_PROMPT":
|
|
241
|
+
prompt_file = _get_prompt_file_path()
|
|
242
|
+
if prompt_file.exists():
|
|
243
|
+
try:
|
|
244
|
+
prompt_file.unlink()
|
|
245
|
+
except (OSError, PermissionError):
|
|
246
|
+
pass
|
|
247
|
+
message = (
|
|
248
|
+
f"System prompt reset (env: {variable_name}, file: .prompt cleared)"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
message = f"System prompt reset (env: {variable_name})"
|
|
252
|
+
|
|
253
|
+
return {"status": "success", "content": [{"text": message}]}
|
|
254
|
+
|
|
255
|
+
elif action == "get":
|
|
256
|
+
# Backward compatibility
|
|
257
|
+
current = _get_system_prompt(variable_name)
|
|
258
|
+
return {
|
|
259
|
+
"status": "success",
|
|
260
|
+
"content": [{"text": f"System prompt: {current}"}],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
elif action == "set":
|
|
264
|
+
# Backward compatibility
|
|
265
|
+
if prompt is None:
|
|
266
|
+
return {"status": "error", "content": [{"text": "No prompt provided"}]}
|
|
267
|
+
|
|
268
|
+
if context:
|
|
269
|
+
prompt = f"{context} {prompt}"
|
|
270
|
+
|
|
271
|
+
_update_system_prompt(prompt, variable_name)
|
|
272
|
+
return {
|
|
273
|
+
"status": "success",
|
|
274
|
+
"content": [{"text": "System prompt updated successfully"}],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
return {
|
|
279
|
+
"status": "error",
|
|
280
|
+
"content": [
|
|
281
|
+
{
|
|
282
|
+
"text": f"Unknown action '{action}'. Valid: view, update, add_context, reset"
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
73
289
|
|
|
74
290
|
|
|
75
291
|
def view_logs_tool(
|
|
@@ -195,183 +411,6 @@ Last Modified: {modified}"""
|
|
|
195
411
|
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
196
412
|
|
|
197
413
|
|
|
198
|
-
def manage_tools_func(
|
|
199
|
-
action: str,
|
|
200
|
-
package: str = None,
|
|
201
|
-
tool_names: str = None,
|
|
202
|
-
tool_path: str = None,
|
|
203
|
-
) -> Dict[str, Any]:
|
|
204
|
-
"""Manage the agent's tool set at runtime - add, remove, list, reload tools on the fly."""
|
|
205
|
-
try:
|
|
206
|
-
if not hasattr(devduck, "agent") or not devduck.agent:
|
|
207
|
-
return {"status": "error", "content": [{"text": "Agent not initialized"}]}
|
|
208
|
-
|
|
209
|
-
registry = devduck.agent.tool_registry
|
|
210
|
-
|
|
211
|
-
if action == "list":
|
|
212
|
-
# List tools from registry
|
|
213
|
-
tool_list = list(registry.registry.keys())
|
|
214
|
-
dynamic_tools = list(registry.dynamic_tools.keys())
|
|
215
|
-
|
|
216
|
-
text = f"Currently loaded {len(tool_list)} tools:\n"
|
|
217
|
-
text += "\n".join(f" • {t}" for t in sorted(tool_list))
|
|
218
|
-
if dynamic_tools:
|
|
219
|
-
text += f"\n\nDynamic tools ({len(dynamic_tools)}):\n"
|
|
220
|
-
text += "\n".join(f" • {t}" for t in sorted(dynamic_tools))
|
|
221
|
-
|
|
222
|
-
return {"status": "success", "content": [{"text": text}]}
|
|
223
|
-
|
|
224
|
-
elif action == "add":
|
|
225
|
-
if not package and not tool_path:
|
|
226
|
-
return {
|
|
227
|
-
"status": "error",
|
|
228
|
-
"content": [
|
|
229
|
-
{
|
|
230
|
-
"text": "Either 'package' or 'tool_path' required for add action"
|
|
231
|
-
}
|
|
232
|
-
],
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
added_tools = []
|
|
236
|
-
|
|
237
|
-
# Add from package using process_tools
|
|
238
|
-
if package:
|
|
239
|
-
if not tool_names:
|
|
240
|
-
return {
|
|
241
|
-
"status": "error",
|
|
242
|
-
"content": [
|
|
243
|
-
{"text": "'tool_names' required when adding from package"}
|
|
244
|
-
],
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
tools_to_add = [t.strip() for t in tool_names.split(",")]
|
|
248
|
-
|
|
249
|
-
# Build tool specs: package.tool_name format
|
|
250
|
-
tool_specs = [f"{package}.{tool_name}" for tool_name in tools_to_add]
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
added_tool_names = registry.process_tools(tool_specs)
|
|
254
|
-
added_tools.extend(added_tool_names)
|
|
255
|
-
logger.info(f"Added tools from {package}: {added_tool_names}")
|
|
256
|
-
except Exception as e:
|
|
257
|
-
logger.error(f"Failed to add tools from {package}: {e}")
|
|
258
|
-
return {
|
|
259
|
-
"status": "error",
|
|
260
|
-
"content": [{"text": f"Failed to add tools: {str(e)}"}],
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
# Add from file path using process_tools
|
|
264
|
-
if tool_path:
|
|
265
|
-
try:
|
|
266
|
-
added_tool_names = registry.process_tools([tool_path])
|
|
267
|
-
added_tools.extend(added_tool_names)
|
|
268
|
-
logger.info(f"Added tools from file: {added_tool_names}")
|
|
269
|
-
except Exception as e:
|
|
270
|
-
logger.error(f"Failed to add tool from {tool_path}: {e}")
|
|
271
|
-
return {
|
|
272
|
-
"status": "error",
|
|
273
|
-
"content": [{"text": f"Failed to add tool: {str(e)}"}],
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if added_tools:
|
|
277
|
-
return {
|
|
278
|
-
"status": "success",
|
|
279
|
-
"content": [
|
|
280
|
-
{
|
|
281
|
-
"text": f"✅ Added {len(added_tools)} tools: {', '.join(added_tools)}\n"
|
|
282
|
-
+ f"Total tools: {len(registry.registry)}"
|
|
283
|
-
}
|
|
284
|
-
],
|
|
285
|
-
}
|
|
286
|
-
else:
|
|
287
|
-
return {"status": "error", "content": [{"text": "No tools were added"}]}
|
|
288
|
-
|
|
289
|
-
elif action == "remove":
|
|
290
|
-
if not tool_names:
|
|
291
|
-
return {
|
|
292
|
-
"status": "error",
|
|
293
|
-
"content": [{"text": "'tool_names' required for remove action"}],
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
tools_to_remove = [t.strip() for t in tool_names.split(",")]
|
|
297
|
-
removed_tools = []
|
|
298
|
-
|
|
299
|
-
# Remove from registry
|
|
300
|
-
for tool_name in tools_to_remove:
|
|
301
|
-
if tool_name in registry.registry:
|
|
302
|
-
del registry.registry[tool_name]
|
|
303
|
-
removed_tools.append(tool_name)
|
|
304
|
-
logger.info(f"Removed tool: {tool_name}")
|
|
305
|
-
|
|
306
|
-
if tool_name in registry.dynamic_tools:
|
|
307
|
-
del registry.dynamic_tools[tool_name]
|
|
308
|
-
logger.info(f"Removed dynamic tool: {tool_name}")
|
|
309
|
-
|
|
310
|
-
if removed_tools:
|
|
311
|
-
return {
|
|
312
|
-
"status": "success",
|
|
313
|
-
"content": [
|
|
314
|
-
{
|
|
315
|
-
"text": f"✅ Removed {len(removed_tools)} tools: {', '.join(removed_tools)}\n"
|
|
316
|
-
+ f"Total tools: {len(registry.registry)}"
|
|
317
|
-
}
|
|
318
|
-
],
|
|
319
|
-
}
|
|
320
|
-
else:
|
|
321
|
-
return {
|
|
322
|
-
"status": "success",
|
|
323
|
-
"content": [{"text": "No tools were removed (not found)"}],
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
elif action == "reload":
|
|
327
|
-
if tool_names:
|
|
328
|
-
# Reload specific tools
|
|
329
|
-
tools_to_reload = [t.strip() for t in tool_names.split(",")]
|
|
330
|
-
reloaded_tools = []
|
|
331
|
-
failed_tools = []
|
|
332
|
-
|
|
333
|
-
for tool_name in tools_to_reload:
|
|
334
|
-
try:
|
|
335
|
-
registry.reload_tool(tool_name)
|
|
336
|
-
reloaded_tools.append(tool_name)
|
|
337
|
-
logger.info(f"Reloaded tool: {tool_name}")
|
|
338
|
-
except Exception as e:
|
|
339
|
-
failed_tools.append((tool_name, str(e)))
|
|
340
|
-
logger.error(f"Failed to reload {tool_name}: {e}")
|
|
341
|
-
|
|
342
|
-
text = ""
|
|
343
|
-
if reloaded_tools:
|
|
344
|
-
text += f"✅ Reloaded {len(reloaded_tools)} tools: {', '.join(reloaded_tools)}\n"
|
|
345
|
-
if failed_tools:
|
|
346
|
-
text += f"❌ Failed to reload {len(failed_tools)} tools:\n"
|
|
347
|
-
for tool_name, error in failed_tools:
|
|
348
|
-
text += f" • {tool_name}: {error}\n"
|
|
349
|
-
|
|
350
|
-
return {"status": "success", "content": [{"text": text}]}
|
|
351
|
-
else:
|
|
352
|
-
# Reload all tools - restart agent
|
|
353
|
-
logger.info("Reloading all tools via restart")
|
|
354
|
-
devduck.restart()
|
|
355
|
-
return {
|
|
356
|
-
"status": "success",
|
|
357
|
-
"content": [{"text": "✅ All tools reloaded - agent restarted"}],
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
else:
|
|
361
|
-
return {
|
|
362
|
-
"status": "error",
|
|
363
|
-
"content": [
|
|
364
|
-
{
|
|
365
|
-
"text": f"Unknown action: {action}. Valid: list, add, remove, reload"
|
|
366
|
-
}
|
|
367
|
-
],
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
except Exception as e:
|
|
371
|
-
logger.error(f"Error in manage_tools: {e}")
|
|
372
|
-
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
373
|
-
|
|
374
|
-
|
|
375
414
|
def get_shell_history_file():
|
|
376
415
|
"""Get the devduck-specific history file path."""
|
|
377
416
|
devduck_history = Path.home() / ".devduck_history"
|
|
@@ -544,6 +583,8 @@ def get_last_messages():
|
|
|
544
583
|
|
|
545
584
|
def append_to_shell_history(query, response):
|
|
546
585
|
"""Append the interaction to devduck shell history."""
|
|
586
|
+
import time
|
|
587
|
+
|
|
547
588
|
try:
|
|
548
589
|
history_file = get_shell_history_file()
|
|
549
590
|
timestamp = str(int(time.time()))
|
|
@@ -568,145 +609,134 @@ class DevDuck:
|
|
|
568
609
|
def __init__(
|
|
569
610
|
self,
|
|
570
611
|
auto_start_servers=True,
|
|
571
|
-
|
|
572
|
-
|
|
612
|
+
tcp_port=9999,
|
|
613
|
+
ws_port=8080,
|
|
614
|
+
mcp_port=8000,
|
|
615
|
+
enable_tcp=True,
|
|
616
|
+
enable_ws=True,
|
|
617
|
+
enable_mcp=True,
|
|
573
618
|
):
|
|
574
|
-
"""Initialize the minimalist adaptive agent
|
|
575
|
-
|
|
576
|
-
Args:
|
|
577
|
-
auto_start_servers: Enable automatic server startup
|
|
578
|
-
servers: Dict of server configs with optional env var lookups
|
|
579
|
-
Example: {
|
|
580
|
-
"tcp": {"port": 9999},
|
|
581
|
-
"ws": {"port": 8080, "LOOKUP_KEY": "SLACK_API_KEY"},
|
|
582
|
-
"mcp": {"port": 8000},
|
|
583
|
-
"ipc": {"socket_path": "/tmp/devduck.sock"}
|
|
584
|
-
}
|
|
585
|
-
load_mcp_servers: Load MCP servers from MCP_SERVERS env var
|
|
586
|
-
"""
|
|
619
|
+
"""Initialize the minimalist adaptive agent"""
|
|
587
620
|
logger.info("Initializing DevDuck agent...")
|
|
588
621
|
try:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
"home": str(Path.home()),
|
|
595
|
-
"shell": os.environ.get("SHELL", "unknown"),
|
|
596
|
-
"hostname": socket.gethostname(),
|
|
597
|
-
}
|
|
622
|
+
# Self-heal dependencies
|
|
623
|
+
ensure_deps()
|
|
624
|
+
|
|
625
|
+
# Adapt to environment
|
|
626
|
+
self.env_info, self.ollama_host, self.model = adapt_to_env()
|
|
598
627
|
|
|
599
628
|
# Execution state tracking for hot-reload
|
|
600
629
|
self._agent_executing = False
|
|
601
630
|
self._reload_pending = False
|
|
602
631
|
|
|
603
|
-
#
|
|
604
|
-
|
|
605
|
-
# Default server config from env vars
|
|
606
|
-
servers = {
|
|
607
|
-
"tcp": {
|
|
608
|
-
"port": int(os.getenv("DEVDUCK_TCP_PORT", "9999")),
|
|
609
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_TCP", "false").lower()
|
|
610
|
-
== "true",
|
|
611
|
-
},
|
|
612
|
-
"ws": {
|
|
613
|
-
"port": int(os.getenv("DEVDUCK_WS_PORT", "8080")),
|
|
614
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_WS", "true").lower()
|
|
615
|
-
== "true",
|
|
616
|
-
},
|
|
617
|
-
"mcp": {
|
|
618
|
-
"port": int(os.getenv("DEVDUCK_MCP_PORT", "8000")),
|
|
619
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_MCP", "false").lower()
|
|
620
|
-
== "true",
|
|
621
|
-
},
|
|
622
|
-
"ipc": {
|
|
623
|
-
"socket_path": os.getenv(
|
|
624
|
-
"DEVDUCK_IPC_SOCKET", "/tmp/devduck_main.sock"
|
|
625
|
-
),
|
|
626
|
-
"enabled": os.getenv("DEVDUCK_ENABLE_IPC", "false").lower()
|
|
627
|
-
== "true",
|
|
628
|
-
},
|
|
629
|
-
}
|
|
632
|
+
# Import after ensuring deps
|
|
633
|
+
from strands import Agent, tool
|
|
630
634
|
|
|
631
|
-
#
|
|
632
|
-
|
|
633
|
-
disabled_servers = []
|
|
634
|
-
for server_name, config in servers.items():
|
|
635
|
-
if config.get("enabled", False):
|
|
636
|
-
if "port" in config:
|
|
637
|
-
enabled_servers.append(
|
|
638
|
-
f"{server_name.upper()}:{config['port']}"
|
|
639
|
-
)
|
|
640
|
-
else:
|
|
641
|
-
enabled_servers.append(server_name.upper())
|
|
642
|
-
else:
|
|
643
|
-
disabled_servers.append(server_name.upper())
|
|
635
|
+
# Core tools (always available)
|
|
636
|
+
core_tools = []
|
|
644
637
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
logger.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
else:
|
|
703
|
-
default_tools = (
|
|
704
|
-
"devduck.tools:system_prompt,fetch_github_tool;strands_tools:shell"
|
|
638
|
+
# Try importing optional tools gracefully
|
|
639
|
+
try:
|
|
640
|
+
from strands.models.ollama import OllamaModel
|
|
641
|
+
except ImportError:
|
|
642
|
+
logger.warning(
|
|
643
|
+
"strands-agents[ollama] not installed - Ollama model unavailable"
|
|
644
|
+
)
|
|
645
|
+
OllamaModel = None
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
from strands_tools.utils.models.model import create_model
|
|
649
|
+
except ImportError:
|
|
650
|
+
logger.warning(
|
|
651
|
+
"strands-agents-tools not installed - create_model unavailable"
|
|
652
|
+
)
|
|
653
|
+
create_model = None
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
from .tools import (
|
|
657
|
+
tcp,
|
|
658
|
+
websocket,
|
|
659
|
+
mcp_server,
|
|
660
|
+
install_tools,
|
|
661
|
+
use_github,
|
|
662
|
+
create_subagent,
|
|
663
|
+
store_in_kb,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
core_tools.extend(
|
|
667
|
+
[
|
|
668
|
+
tcp,
|
|
669
|
+
websocket,
|
|
670
|
+
mcp_server,
|
|
671
|
+
install_tools,
|
|
672
|
+
use_github,
|
|
673
|
+
create_subagent,
|
|
674
|
+
store_in_kb,
|
|
675
|
+
]
|
|
676
|
+
)
|
|
677
|
+
except ImportError as e:
|
|
678
|
+
logger.warning(f"devduck.tools import failed: {e}")
|
|
679
|
+
|
|
680
|
+
try:
|
|
681
|
+
from strands_fun_tools import (
|
|
682
|
+
listen,
|
|
683
|
+
cursor,
|
|
684
|
+
clipboard,
|
|
685
|
+
screen_reader,
|
|
686
|
+
yolo_vision,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
core_tools.extend(
|
|
690
|
+
[listen, cursor, clipboard, screen_reader, yolo_vision]
|
|
691
|
+
)
|
|
692
|
+
except ImportError:
|
|
693
|
+
logger.info(
|
|
694
|
+
"strands-fun-tools not installed - vision/audio tools unavailable (install with: pip install devduck[all])"
|
|
705
695
|
)
|
|
706
696
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
697
|
+
try:
|
|
698
|
+
from strands_tools import (
|
|
699
|
+
shell,
|
|
700
|
+
editor,
|
|
701
|
+
calculator,
|
|
702
|
+
python_repl,
|
|
703
|
+
image_reader,
|
|
704
|
+
use_agent,
|
|
705
|
+
load_tool,
|
|
706
|
+
environment,
|
|
707
|
+
mcp_client,
|
|
708
|
+
retrieve,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
core_tools.extend(
|
|
712
|
+
[
|
|
713
|
+
shell,
|
|
714
|
+
editor,
|
|
715
|
+
calculator,
|
|
716
|
+
python_repl,
|
|
717
|
+
image_reader,
|
|
718
|
+
use_agent,
|
|
719
|
+
load_tool,
|
|
720
|
+
environment,
|
|
721
|
+
mcp_client,
|
|
722
|
+
retrieve,
|
|
723
|
+
]
|
|
724
|
+
)
|
|
725
|
+
except ImportError:
|
|
726
|
+
logger.info(
|
|
727
|
+
"strands-agents-tools not installed - core tools unavailable (install with: pip install devduck[all])"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Wrap system_prompt_tool with @tool decorator
|
|
731
|
+
@tool
|
|
732
|
+
def system_prompt(
|
|
733
|
+
action: str,
|
|
734
|
+
prompt: str = None,
|
|
735
|
+
context: str = None,
|
|
736
|
+
variable_name: str = "SYSTEM_PROMPT",
|
|
737
|
+
) -> Dict[str, Any]:
|
|
738
|
+
"""Manage agent system prompt dynamically."""
|
|
739
|
+
return system_prompt_tool(action, prompt, context, variable_name)
|
|
710
740
|
|
|
711
741
|
# Wrap view_logs_tool with @tool decorator
|
|
712
742
|
@tool
|
|
@@ -718,380 +748,105 @@ class DevDuck:
|
|
|
718
748
|
"""View and manage DevDuck logs."""
|
|
719
749
|
return view_logs_tool(action, lines, pattern)
|
|
720
750
|
|
|
721
|
-
# Wrap manage_tools_func with @tool decorator
|
|
722
|
-
@tool
|
|
723
|
-
def manage_tools(
|
|
724
|
-
action: str,
|
|
725
|
-
package: str = None,
|
|
726
|
-
tool_names: str = None,
|
|
727
|
-
tool_path: str = None,
|
|
728
|
-
) -> Dict[str, Any]:
|
|
729
|
-
"""
|
|
730
|
-
Manage the agent's tool set at runtime using ToolRegistry.
|
|
731
|
-
|
|
732
|
-
Args:
|
|
733
|
-
action: Action to perform - "list", "add", "remove", "reload"
|
|
734
|
-
package: Package name to load tools from (e.g., "strands_tools", "strands_fun_tools") or "devduck.tools:speech_to_speech,system_prompt,..."
|
|
735
|
-
tool_names: Comma-separated tool names (e.g., "shell,editor,calculator")
|
|
736
|
-
tool_path: Path to a .py file to load as a tool
|
|
737
|
-
|
|
738
|
-
Returns:
|
|
739
|
-
Dict with status and content
|
|
740
|
-
"""
|
|
741
|
-
return manage_tools_func(action, package, tool_names, tool_path)
|
|
742
|
-
|
|
743
751
|
# Add built-in tools to the toolset
|
|
744
|
-
core_tools.extend([
|
|
752
|
+
core_tools.extend([system_prompt, view_logs])
|
|
745
753
|
|
|
746
754
|
# Assign tools
|
|
747
755
|
self.tools = core_tools
|
|
748
756
|
|
|
749
|
-
# 🔌 Load MCP servers if enabled
|
|
750
|
-
if load_mcp_servers:
|
|
751
|
-
mcp_clients = self._load_mcp_servers()
|
|
752
|
-
if mcp_clients:
|
|
753
|
-
self.tools.extend(mcp_clients)
|
|
754
|
-
logger.info(f"Loaded {len(mcp_clients)} MCP server(s)")
|
|
755
|
-
|
|
756
757
|
logger.info(f"Initialized {len(self.tools)} tools")
|
|
757
758
|
|
|
758
|
-
#
|
|
759
|
-
|
|
759
|
+
# Check if MODEL_PROVIDER env variable is set
|
|
760
|
+
model_provider = os.getenv("MODEL_PROVIDER")
|
|
761
|
+
|
|
762
|
+
if model_provider and create_model:
|
|
763
|
+
# Use create_model utility for any provider (bedrock, anthropic, etc.)
|
|
764
|
+
self.agent_model = create_model(provider=model_provider)
|
|
765
|
+
elif OllamaModel:
|
|
766
|
+
# Fallback to default Ollama behavior
|
|
767
|
+
self.agent_model = OllamaModel(
|
|
768
|
+
host=self.ollama_host,
|
|
769
|
+
model_id=self.model,
|
|
770
|
+
temperature=1,
|
|
771
|
+
keep_alive="5m",
|
|
772
|
+
)
|
|
773
|
+
else:
|
|
774
|
+
raise ImportError(
|
|
775
|
+
"No model provider available. Install with: pip install devduck[all]"
|
|
776
|
+
)
|
|
760
777
|
|
|
761
778
|
# Create agent with self-healing
|
|
762
|
-
# load_tools_from_directory controlled by DEVDUCK_LOAD_TOOLS_FROM_DIR (default: true)
|
|
763
|
-
load_from_dir = (
|
|
764
|
-
os.getenv("DEVDUCK_LOAD_TOOLS_FROM_DIR", "true").lower() == "true"
|
|
765
|
-
)
|
|
766
|
-
|
|
767
779
|
self.agent = Agent(
|
|
768
780
|
model=self.agent_model,
|
|
769
781
|
tools=self.tools,
|
|
770
782
|
system_prompt=self._build_system_prompt(),
|
|
771
|
-
load_tools_from_directory=
|
|
772
|
-
trace_attributes={
|
|
773
|
-
"session.id": self.session_id,
|
|
774
|
-
"user.id": self.env_info["hostname"],
|
|
775
|
-
"tags": ["Strands-Agents", "DevDuck"],
|
|
776
|
-
},
|
|
783
|
+
load_tools_from_directory=True,
|
|
777
784
|
)
|
|
778
785
|
|
|
779
|
-
# 🚀 AUTO-START SERVERS
|
|
780
|
-
if auto_start_servers
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
# Start file watcher for auto hot-reload
|
|
784
|
-
self._start_file_watcher()
|
|
785
|
-
|
|
786
|
-
logger.info(
|
|
787
|
-
f"DevDuck agent initialized successfully with model {self.model}"
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
except Exception as e:
|
|
791
|
-
logger.error(f"Initialization failed: {e}")
|
|
792
|
-
self._self_heal(e)
|
|
793
|
-
|
|
794
|
-
def _load_tools_from_config(self, config):
|
|
795
|
-
"""
|
|
796
|
-
Load tools based on DEVDUCK_TOOLS configuration.
|
|
797
|
-
|
|
798
|
-
Format: package1:tool1,tool2;package2:tool3,tool4
|
|
799
|
-
Examples:
|
|
800
|
-
- strands_tools:shell,editor;strands_action:use_github
|
|
801
|
-
- strands_action:use_github;strands_tools:shell,use_aws
|
|
802
|
-
|
|
803
|
-
Note: Only loads what's specified in config - no automatic additions
|
|
804
|
-
"""
|
|
805
|
-
tools = []
|
|
806
|
-
|
|
807
|
-
# Split by semicolon to get package groups
|
|
808
|
-
groups = config.split(";")
|
|
809
|
-
|
|
810
|
-
for group in groups:
|
|
811
|
-
group = group.strip()
|
|
812
|
-
if not group:
|
|
813
|
-
continue
|
|
814
|
-
|
|
815
|
-
# Split by colon to get package:tools
|
|
816
|
-
parts = group.split(":", 1)
|
|
817
|
-
if len(parts) != 2:
|
|
818
|
-
logger.warning(f"Invalid format: {group}")
|
|
819
|
-
continue
|
|
820
|
-
|
|
821
|
-
package = parts[0].strip()
|
|
822
|
-
tools_str = parts[1].strip()
|
|
823
|
-
|
|
824
|
-
# Parse tools (comma-separated)
|
|
825
|
-
tool_names = [t.strip() for t in tools_str.split(",") if t.strip()]
|
|
826
|
-
|
|
827
|
-
for tool_name in tool_names:
|
|
828
|
-
tool = self._load_single_tool(package, tool_name)
|
|
829
|
-
if tool:
|
|
830
|
-
tools.append(tool)
|
|
831
|
-
|
|
832
|
-
logger.info(f"Loaded {len(tools)} tools from configuration")
|
|
833
|
-
return tools
|
|
834
|
-
|
|
835
|
-
def _load_single_tool(self, package, tool_name):
|
|
836
|
-
"""Load a single tool from a package"""
|
|
837
|
-
try:
|
|
838
|
-
module = __import__(package, fromlist=[tool_name])
|
|
839
|
-
tool = getattr(module, tool_name)
|
|
840
|
-
logger.debug(f"Loaded {tool_name} from {package}")
|
|
841
|
-
return tool
|
|
842
|
-
except Exception as e:
|
|
843
|
-
logger.warning(f"Failed to load {tool_name} from {package}: {e}")
|
|
844
|
-
return None
|
|
845
|
-
|
|
846
|
-
def _load_mcp_servers(self):
|
|
847
|
-
"""
|
|
848
|
-
Load MCP servers from MCP_SERVERS environment variable using direct loading.
|
|
849
|
-
|
|
850
|
-
Uses the experimental managed integration - MCPClient instances are passed
|
|
851
|
-
directly to Agent constructor without explicit context management.
|
|
852
|
-
|
|
853
|
-
Format: JSON with "mcpServers" object
|
|
854
|
-
Example: MCP_SERVERS='{"mcpServers": {"strands": {"command": "uvx", "args": ["strands-agents-mcp-server"]}}}'
|
|
855
|
-
|
|
856
|
-
Returns:
|
|
857
|
-
List of MCPClient instances ready for direct use in Agent
|
|
858
|
-
"""
|
|
859
|
-
mcp_servers_json = os.getenv("MCP_SERVERS")
|
|
860
|
-
if not mcp_servers_json:
|
|
861
|
-
logger.debug("No MCP_SERVERS environment variable found")
|
|
862
|
-
return []
|
|
863
|
-
|
|
864
|
-
try:
|
|
865
|
-
config = json.loads(mcp_servers_json)
|
|
866
|
-
mcp_servers_config = config.get("mcpServers", {})
|
|
867
|
-
|
|
868
|
-
if not mcp_servers_config:
|
|
869
|
-
logger.warning("MCP_SERVERS JSON has no 'mcpServers' key")
|
|
870
|
-
return []
|
|
871
|
-
|
|
872
|
-
mcp_clients = []
|
|
786
|
+
# 🚀 AUTO-START SERVERS: TCP, WebSocket, MCP HTTP
|
|
787
|
+
if auto_start_servers:
|
|
788
|
+
logger.info("Auto-starting servers...")
|
|
789
|
+
print("🦆 Auto-starting servers...")
|
|
873
790
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
for server_name, server_config in mcp_servers_config.items():
|
|
880
|
-
try:
|
|
881
|
-
logger.info(f"Loading MCP server: {server_name}")
|
|
882
|
-
|
|
883
|
-
# Determine transport type and create appropriate callable
|
|
884
|
-
if "command" in server_config:
|
|
885
|
-
# stdio transport
|
|
886
|
-
command = server_config["command"]
|
|
887
|
-
args = server_config.get("args", [])
|
|
888
|
-
env = server_config.get("env", None)
|
|
889
|
-
|
|
890
|
-
transport_callable = (
|
|
891
|
-
lambda cmd=command, a=args, e=env: stdio_client(
|
|
892
|
-
StdioServerParameters(command=cmd, args=a, env=e)
|
|
893
|
-
)
|
|
791
|
+
if enable_tcp:
|
|
792
|
+
try:
|
|
793
|
+
# Start TCP server on configurable port
|
|
794
|
+
tcp_result = self.agent.tool.tcp(
|
|
795
|
+
action="start_server", port=tcp_port
|
|
894
796
|
)
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
url = server_config["url"]
|
|
899
|
-
headers = server_config.get("headers", None)
|
|
900
|
-
|
|
901
|
-
if "/sse" in url:
|
|
902
|
-
# SSE transport
|
|
903
|
-
transport_callable = lambda u=url: sse_client(u)
|
|
797
|
+
if tcp_result.get("status") == "success":
|
|
798
|
+
logger.info(f"✓ TCP server started on port {tcp_port}")
|
|
799
|
+
print(f"🦆 ✓ TCP server: localhost:{tcp_port}")
|
|
904
800
|
else:
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
)
|
|
910
|
-
)
|
|
911
|
-
else:
|
|
912
|
-
logger.warning(
|
|
913
|
-
f"MCP server {server_name} has no 'command' or 'url' - skipping"
|
|
914
|
-
)
|
|
915
|
-
continue
|
|
916
|
-
|
|
917
|
-
# Create MCPClient with direct loading (experimental managed integration)
|
|
918
|
-
# No need for context managers - Agent handles lifecycle
|
|
919
|
-
prefix = server_config.get("prefix", server_name)
|
|
920
|
-
mcp_client = MCPClient(
|
|
921
|
-
transport_callable=transport_callable, prefix=prefix
|
|
922
|
-
)
|
|
923
|
-
|
|
924
|
-
mcp_clients.append(mcp_client)
|
|
925
|
-
logger.info(
|
|
926
|
-
f"✓ MCP server '{server_name}' loaded (prefix: {prefix})"
|
|
927
|
-
)
|
|
928
|
-
|
|
929
|
-
except Exception as e:
|
|
930
|
-
logger.error(f"Failed to load MCP server '{server_name}': {e}")
|
|
931
|
-
continue
|
|
932
|
-
|
|
933
|
-
return mcp_clients
|
|
934
|
-
|
|
935
|
-
except json.JSONDecodeError as e:
|
|
936
|
-
logger.error(f"Invalid JSON in MCP_SERVERS: {e}")
|
|
937
|
-
return []
|
|
938
|
-
except Exception as e:
|
|
939
|
-
logger.error(f"Error loading MCP servers: {e}")
|
|
940
|
-
return []
|
|
941
|
-
|
|
942
|
-
def _select_model(self):
|
|
943
|
-
"""
|
|
944
|
-
Smart model selection with fallback based on available credentials.
|
|
945
|
-
|
|
946
|
-
Priority: Bedrock → Anthropic → OpenAI → GitHub → Gemini → Cohere →
|
|
947
|
-
Writer → Mistral → LiteLLM → LlamaAPI → SageMaker →
|
|
948
|
-
LlamaCpp → MLX → Ollama
|
|
949
|
-
|
|
950
|
-
Returns:
|
|
951
|
-
Tuple of (model_instance, model_name)
|
|
952
|
-
"""
|
|
953
|
-
provider = os.getenv("MODEL_PROVIDER")
|
|
954
|
-
|
|
955
|
-
# Read common model parameters from environment
|
|
956
|
-
max_tokens = int(os.getenv("STRANDS_MAX_TOKENS", "60000"))
|
|
957
|
-
temperature = float(os.getenv("STRANDS_TEMPERATURE", "1.0"))
|
|
958
|
-
|
|
959
|
-
if not provider:
|
|
960
|
-
# Auto-detect based on API keys and credentials
|
|
961
|
-
# 1. Try Bedrock (AWS bearer token or STS credentials)
|
|
962
|
-
try:
|
|
963
|
-
# Check for bearer token first
|
|
964
|
-
if os.getenv("AWS_BEARER_TOKEN_BEDROCK"):
|
|
965
|
-
provider = "bedrock"
|
|
966
|
-
print("🦆 Using Bedrock (bearer token)")
|
|
967
|
-
else:
|
|
968
|
-
# Try STS credentials
|
|
969
|
-
import boto3
|
|
801
|
+
logger.warning(f"TCP server start issue: {tcp_result}")
|
|
802
|
+
except Exception as e:
|
|
803
|
+
logger.error(f"Failed to start TCP server: {e}")
|
|
804
|
+
print(f"🦆 ⚠ TCP server failed: {e}")
|
|
970
805
|
|
|
971
|
-
|
|
972
|
-
provider = "bedrock"
|
|
973
|
-
print("🦆 Using Bedrock")
|
|
974
|
-
except:
|
|
975
|
-
# 2. Try Anthropic
|
|
976
|
-
if os.getenv("ANTHROPIC_API_KEY"):
|
|
977
|
-
provider = "anthropic"
|
|
978
|
-
print("🦆 Using Anthropic")
|
|
979
|
-
# 3. Try OpenAI
|
|
980
|
-
elif os.getenv("OPENAI_API_KEY"):
|
|
981
|
-
provider = "openai"
|
|
982
|
-
print("🦆 Using OpenAI")
|
|
983
|
-
# 4. Try GitHub Models
|
|
984
|
-
elif os.getenv("GITHUB_TOKEN") or os.getenv("PAT_TOKEN"):
|
|
985
|
-
provider = "github"
|
|
986
|
-
print("🦆 Using GitHub Models")
|
|
987
|
-
# 5. Try Gemini
|
|
988
|
-
elif os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY"):
|
|
989
|
-
provider = "gemini"
|
|
990
|
-
print("🦆 Using Gemini")
|
|
991
|
-
# 6. Try Cohere
|
|
992
|
-
elif os.getenv("COHERE_API_KEY"):
|
|
993
|
-
provider = "cohere"
|
|
994
|
-
print("🦆 Using Cohere")
|
|
995
|
-
# 7. Try Writer
|
|
996
|
-
elif os.getenv("WRITER_API_KEY"):
|
|
997
|
-
provider = "writer"
|
|
998
|
-
print("🦆 Using Writer")
|
|
999
|
-
# 8. Try Mistral
|
|
1000
|
-
elif os.getenv("MISTRAL_API_KEY"):
|
|
1001
|
-
provider = "mistral"
|
|
1002
|
-
print("🦆 Using Mistral")
|
|
1003
|
-
# 9. Try LiteLLM
|
|
1004
|
-
elif os.getenv("LITELLM_API_KEY"):
|
|
1005
|
-
provider = "litellm"
|
|
1006
|
-
print("🦆 Using LiteLLM")
|
|
1007
|
-
# 10. Try LlamaAPI
|
|
1008
|
-
elif os.getenv("LLAMAAPI_API_KEY"):
|
|
1009
|
-
provider = "llamaapi"
|
|
1010
|
-
print("🦆 Using LlamaAPI")
|
|
1011
|
-
# 11. Try SageMaker
|
|
1012
|
-
elif os.getenv("SAGEMAKER_ENDPOINT_NAME"):
|
|
1013
|
-
provider = "sagemaker"
|
|
1014
|
-
print("🦆 Using SageMaker")
|
|
1015
|
-
# 12. Try LlamaCpp
|
|
1016
|
-
elif os.getenv("LLAMACPP_MODEL_PATH"):
|
|
1017
|
-
provider = "llamacpp"
|
|
1018
|
-
print("🦆 Using LlamaCpp")
|
|
1019
|
-
# 13. Try MLX on Apple Silicon
|
|
1020
|
-
elif platform.system() == "Darwin" and platform.machine() in [
|
|
1021
|
-
"arm64",
|
|
1022
|
-
"aarch64",
|
|
1023
|
-
]:
|
|
806
|
+
if enable_ws:
|
|
1024
807
|
try:
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
# Create model based on provider
|
|
1038
|
-
if provider == "mlx":
|
|
1039
|
-
from strands_mlx import MLXModel
|
|
1040
|
-
|
|
1041
|
-
model_name = os.getenv("STRANDS_MODEL_ID", "mlx-community/Qwen3-1.7B-4bit")
|
|
1042
|
-
return (
|
|
1043
|
-
MLXModel(
|
|
1044
|
-
model_id=model_name,
|
|
1045
|
-
params={"temperature": temperature, "max_tokens": max_tokens},
|
|
1046
|
-
),
|
|
1047
|
-
model_name,
|
|
1048
|
-
)
|
|
1049
|
-
|
|
1050
|
-
elif provider == "gemini":
|
|
1051
|
-
from strands.models.gemini import GeminiModel
|
|
1052
|
-
|
|
1053
|
-
model_name = os.getenv("STRANDS_MODEL_ID", "gemini-2.5-flash")
|
|
1054
|
-
api_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
1055
|
-
return (
|
|
1056
|
-
GeminiModel(
|
|
1057
|
-
client_args={"api_key": api_key},
|
|
1058
|
-
model_id=model_name,
|
|
1059
|
-
params={"temperature": temperature, "max_tokens": max_tokens},
|
|
1060
|
-
),
|
|
1061
|
-
model_name,
|
|
1062
|
-
)
|
|
808
|
+
# Start WebSocket server on configurable port
|
|
809
|
+
ws_result = self.agent.tool.websocket(
|
|
810
|
+
action="start_server", port=ws_port
|
|
811
|
+
)
|
|
812
|
+
if ws_result.get("status") == "success":
|
|
813
|
+
logger.info(f"✓ WebSocket server started on port {ws_port}")
|
|
814
|
+
print(f"🦆 ✓ WebSocket server: localhost:{ws_port}")
|
|
815
|
+
else:
|
|
816
|
+
logger.warning(f"WebSocket server start issue: {ws_result}")
|
|
817
|
+
except Exception as e:
|
|
818
|
+
logger.error(f"Failed to start WebSocket server: {e}")
|
|
819
|
+
print(f"🦆 ⚠ WebSocket server failed: {e}")
|
|
1063
820
|
|
|
1064
|
-
|
|
1065
|
-
|
|
821
|
+
if enable_mcp:
|
|
822
|
+
try:
|
|
823
|
+
# Start MCP server with HTTP transport on configurable port
|
|
824
|
+
mcp_result = self.agent.tool.mcp_server(
|
|
825
|
+
action="start",
|
|
826
|
+
transport="http",
|
|
827
|
+
port=mcp_port,
|
|
828
|
+
expose_agent=True,
|
|
829
|
+
agent=self.agent,
|
|
830
|
+
)
|
|
831
|
+
if mcp_result.get("status") == "success":
|
|
832
|
+
logger.info(f"✓ MCP HTTP server started on port {mcp_port}")
|
|
833
|
+
print(f"🦆 ✓ MCP server: http://localhost:{mcp_port}/mcp")
|
|
834
|
+
else:
|
|
835
|
+
logger.warning(f"MCP server start issue: {mcp_result}")
|
|
836
|
+
except Exception as e:
|
|
837
|
+
logger.error(f"Failed to start MCP server: {e}")
|
|
838
|
+
print(f"🦆 ⚠ MCP server failed: {e}")
|
|
1066
839
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if os_type == "Darwin":
|
|
1070
|
-
model_name = os.getenv("STRANDS_MODEL_ID", "qwen3:1.7b")
|
|
1071
|
-
elif os_type == "Linux":
|
|
1072
|
-
model_name = os.getenv("STRANDS_MODEL_ID", "qwen3:30b")
|
|
1073
|
-
else:
|
|
1074
|
-
model_name = os.getenv("STRANDS_MODEL_ID", "qwen3:8b")
|
|
1075
|
-
|
|
1076
|
-
return (
|
|
1077
|
-
OllamaModel(
|
|
1078
|
-
host=os.getenv("OLLAMA_HOST", "http://localhost:11434"),
|
|
1079
|
-
model_id=model_name,
|
|
1080
|
-
temperature=temperature,
|
|
1081
|
-
num_predict=max_tokens,
|
|
1082
|
-
keep_alive="5m",
|
|
1083
|
-
),
|
|
1084
|
-
model_name,
|
|
1085
|
-
)
|
|
840
|
+
# Start file watcher for auto hot-reload
|
|
841
|
+
self._start_file_watcher()
|
|
1086
842
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
from strands_tools.utils.models.model import create_model
|
|
843
|
+
logger.info(
|
|
844
|
+
f"DevDuck agent initialized successfully with model {self.model}"
|
|
845
|
+
)
|
|
1091
846
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
847
|
+
except Exception as e:
|
|
848
|
+
logger.error(f"Initialization failed: {e}")
|
|
849
|
+
self._self_heal(e)
|
|
1095
850
|
|
|
1096
851
|
def _build_system_prompt(self):
|
|
1097
852
|
"""Build adaptive system prompt based on environment
|
|
@@ -1103,20 +858,13 @@ class DevDuck:
|
|
|
1103
858
|
|
|
1104
859
|
Learning: Always check source code truth over conversation memory!
|
|
1105
860
|
"""
|
|
1106
|
-
# Current date and time
|
|
1107
|
-
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1108
|
-
current_date = datetime.now().strftime("%A, %B %d, %Y")
|
|
1109
|
-
current_time = datetime.now().strftime("%I:%M %p")
|
|
1110
|
-
|
|
1111
861
|
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
1112
|
-
self.session_id = session_id
|
|
1113
|
-
|
|
1114
|
-
# Get own file path for self-modification awareness
|
|
1115
|
-
own_file_path = Path(__file__).resolve()
|
|
1116
862
|
|
|
1117
863
|
# Get own source code for self-awareness
|
|
1118
864
|
own_code = get_own_source_code()
|
|
1119
865
|
|
|
866
|
+
# print(own_code)
|
|
867
|
+
|
|
1120
868
|
# Get recent conversation history context (with error handling)
|
|
1121
869
|
try:
|
|
1122
870
|
recent_context = get_last_messages()
|
|
@@ -1138,8 +886,6 @@ Python: {self.env_info['python']}
|
|
|
1138
886
|
Model: {self.model}
|
|
1139
887
|
Hostname: {self.env_info['hostname']}
|
|
1140
888
|
Session ID: {session_id}
|
|
1141
|
-
Current Time: {current_datetime} ({current_date} at {current_time})
|
|
1142
|
-
My Path: {own_file_path}
|
|
1143
889
|
|
|
1144
890
|
You are:
|
|
1145
891
|
- Minimalist: Brief, direct responses
|
|
@@ -1162,7 +908,6 @@ You have full access to your own source code for self-awareness and self-modific
|
|
|
1162
908
|
- **No Restart Needed** - Tools are auto-loaded and ready to use instantly
|
|
1163
909
|
- **Live Development** - Modify existing tools while running and test immediately
|
|
1164
910
|
- **Full Python Access** - Create any Python functionality as a tool
|
|
1165
|
-
- **Agent Protection** - Hot-reload waits until agent finishes current task
|
|
1166
911
|
|
|
1167
912
|
## Dynamic Tool Loading:
|
|
1168
913
|
- **Install Tools** - Use install_tools() to load tools from any Python package
|
|
@@ -1170,48 +915,70 @@ You have full access to your own source code for self-awareness and self-modific
|
|
|
1170
915
|
- Expands capabilities without restart
|
|
1171
916
|
- Access to entire Python ecosystem
|
|
1172
917
|
|
|
1173
|
-
##
|
|
1174
|
-
Set DEVDUCK_TOOLS for custom tools:
|
|
1175
|
-
- Format: package1:tool1,tool2;package2:tool3,tool4
|
|
1176
|
-
- Example: strands_tools:shell,editor;strands_fun_tools:clipboard
|
|
1177
|
-
- Tools are filtered - only specified tools are loaded
|
|
1178
|
-
- Load the speech_to_speech tool when it's needed
|
|
1179
|
-
- Offload the tools when you don't need
|
|
1180
|
-
|
|
1181
|
-
## MCP Integration:
|
|
918
|
+
## MCP Server:
|
|
1182
919
|
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
1183
920
|
- Example: mcp_server(action="start", port=8000)
|
|
1184
921
|
- Connect from Claude Desktop, other agents, or custom clients
|
|
1185
922
|
- Full bidirectional communication
|
|
1186
923
|
|
|
1187
|
-
- **Load MCP Servers** - Set MCP_SERVERS env var to auto-load external MCP servers
|
|
1188
|
-
- Format: JSON with "mcpServers" object
|
|
1189
|
-
- Stdio servers: command, args, env keys
|
|
1190
|
-
- HTTP servers: url, headers keys
|
|
1191
|
-
- Example: MCP_SERVERS='{{"mcpServers": {{"strands": {{"command": "uvx", "args": ["strands-agents-mcp-server"]}}}}}}'
|
|
1192
|
-
- Tools from MCP servers automatically available in agent context
|
|
1193
|
-
|
|
1194
924
|
## Knowledge Base Integration:
|
|
1195
|
-
- **Automatic RAG** - Set
|
|
925
|
+
- **Automatic RAG** - Set STRANDS_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
|
|
1196
926
|
- Before each query: Retrieves relevant context from knowledge base
|
|
1197
927
|
- After each response: Stores conversation for future reference
|
|
1198
928
|
- Seamless memory across sessions without manual tool calls
|
|
1199
929
|
|
|
1200
|
-
##
|
|
1201
|
-
- **View**: system_prompt(action='view') - See current prompt
|
|
1202
|
-
- **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
|
|
1203
|
-
- **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
|
|
1204
|
-
- **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
|
|
1205
|
-
- **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
|
|
930
|
+
## Tool Creation Patterns:
|
|
1206
931
|
|
|
1207
|
-
###
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
3. Sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
|
|
1212
|
-
4. New learnings persist across sessions via SYSTEM_PROMPT env var
|
|
932
|
+
### **1. @tool Decorator:**
|
|
933
|
+
```python
|
|
934
|
+
# ./tools/calculate_tip.py
|
|
935
|
+
from strands import tool
|
|
1213
936
|
|
|
1214
|
-
|
|
937
|
+
@tool
|
|
938
|
+
def calculate_tip(amount: float, percentage: float = 15.0) -> str:
|
|
939
|
+
\"\"\"Calculate tip and total for a bill.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
amount: Bill amount in dollars
|
|
943
|
+
percentage: Tip percentage (default: 15.0)
|
|
944
|
+
|
|
945
|
+
Returns:
|
|
946
|
+
str: Formatted tip calculation result
|
|
947
|
+
\"\"\"
|
|
948
|
+
tip = amount * (percentage / 100)
|
|
949
|
+
total = amount + tip
|
|
950
|
+
return f"Tip: {{tip:.2f}}, Total: {{total:.2f}}"
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### **2. Action-Based Pattern:**
|
|
954
|
+
```python
|
|
955
|
+
# ./tools/weather.py
|
|
956
|
+
from typing import Dict, Any
|
|
957
|
+
from strands import tool
|
|
958
|
+
|
|
959
|
+
@tool
|
|
960
|
+
def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
961
|
+
\"\"\"Comprehensive weather information tool.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
action: Action to perform (current, forecast, alerts)
|
|
965
|
+
location: City name (required)
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
Dict containing status and response content
|
|
969
|
+
\"\"\"
|
|
970
|
+
if action == "current":
|
|
971
|
+
return {{"status": "success", "content": [{{"text": f"Weather for {{location}}"}}]}}
|
|
972
|
+
elif action == "forecast":
|
|
973
|
+
return {{"status": "success", "content": [{{"text": f"Forecast for {{location}}"}}]}}
|
|
974
|
+
else:
|
|
975
|
+
return {{"status": "error", "content": [{{"text": f"Unknown action: {{action}}"}}]}}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
## System Prompt Management:
|
|
979
|
+
- Use system_prompt(action='get') to view current prompt
|
|
980
|
+
- Use system_prompt(action='set', prompt='new text') to update
|
|
981
|
+
- Changes persist in SYSTEM_PROMPT environment variable
|
|
1215
982
|
|
|
1216
983
|
## Shell Commands:
|
|
1217
984
|
- Prefix with ! to execute shell commands directly
|
|
@@ -1223,7 +990,7 @@ When you learn something valuable during conversations:
|
|
|
1223
990
|
- Communication: **MINIMAL WORDS**
|
|
1224
991
|
- Efficiency: **Speed is paramount**
|
|
1225
992
|
|
|
1226
|
-
{
|
|
993
|
+
{os.getenv('SYSTEM_PROMPT', '')}"""
|
|
1227
994
|
|
|
1228
995
|
def _self_heal(self, error):
|
|
1229
996
|
"""Attempt self-healing when errors occur"""
|
|
@@ -1237,11 +1004,62 @@ When you learn something valuable during conversations:
|
|
|
1237
1004
|
self._heal_count += 1
|
|
1238
1005
|
|
|
1239
1006
|
# Limit recursion - if we've tried more than 3 times, give up
|
|
1240
|
-
if self._heal_count >
|
|
1007
|
+
if self._heal_count > 3:
|
|
1241
1008
|
print(f"🦆 Self-healing failed after {self._heal_count} attempts")
|
|
1242
1009
|
print("🦆 Please fix the issue manually and restart")
|
|
1243
1010
|
sys.exit(1)
|
|
1244
1011
|
|
|
1012
|
+
# Common healing strategies
|
|
1013
|
+
if "not found" in str(error).lower() and "model" in str(error).lower():
|
|
1014
|
+
print("🦆 Model not found - trying to pull model...")
|
|
1015
|
+
try:
|
|
1016
|
+
# Try to pull the model
|
|
1017
|
+
result = subprocess.run(
|
|
1018
|
+
["ollama", "pull", self.model], capture_output=True, timeout=60
|
|
1019
|
+
)
|
|
1020
|
+
if result.returncode == 0:
|
|
1021
|
+
print(f"🦆 Successfully pulled {self.model}")
|
|
1022
|
+
else:
|
|
1023
|
+
print(f"🦆 Failed to pull {self.model}, trying fallback...")
|
|
1024
|
+
# Fallback to basic models
|
|
1025
|
+
fallback_models = ["llama3.2:1b", "qwen2.5:0.5b", "gemma2:2b"]
|
|
1026
|
+
for fallback in fallback_models:
|
|
1027
|
+
try:
|
|
1028
|
+
subprocess.run(
|
|
1029
|
+
["ollama", "pull", fallback],
|
|
1030
|
+
capture_output=True,
|
|
1031
|
+
timeout=30,
|
|
1032
|
+
)
|
|
1033
|
+
self.model = fallback
|
|
1034
|
+
print(f"🦆 Using fallback model: {fallback}")
|
|
1035
|
+
break
|
|
1036
|
+
except:
|
|
1037
|
+
continue
|
|
1038
|
+
except Exception as pull_error:
|
|
1039
|
+
print(f"🦆 Model pull failed: {pull_error}")
|
|
1040
|
+
# Ultra-minimal fallback
|
|
1041
|
+
self.model = "llama3.2:1b"
|
|
1042
|
+
|
|
1043
|
+
elif "ollama" in str(error).lower():
|
|
1044
|
+
print("🦆 Ollama issue - checking service...")
|
|
1045
|
+
try:
|
|
1046
|
+
# Check if ollama is running
|
|
1047
|
+
result = subprocess.run(
|
|
1048
|
+
["ollama", "list"], capture_output=True, timeout=5
|
|
1049
|
+
)
|
|
1050
|
+
if result.returncode != 0:
|
|
1051
|
+
print("🦆 Starting ollama service...")
|
|
1052
|
+
subprocess.Popen(["ollama", "serve"])
|
|
1053
|
+
import time
|
|
1054
|
+
|
|
1055
|
+
time.sleep(3) # Wait for service to start
|
|
1056
|
+
except Exception as ollama_error:
|
|
1057
|
+
print(f"🦆 Ollama service issue: {ollama_error}")
|
|
1058
|
+
|
|
1059
|
+
elif "import" in str(error).lower():
|
|
1060
|
+
print("🦆 Import issue - reinstalling dependencies...")
|
|
1061
|
+
ensure_deps()
|
|
1062
|
+
|
|
1245
1063
|
elif "connection" in str(error).lower():
|
|
1246
1064
|
print("🦆 Connection issue - checking ollama service...")
|
|
1247
1065
|
try:
|
|
@@ -1257,192 +1075,6 @@ When you learn something valuable during conversations:
|
|
|
1257
1075
|
print("🦆 Running in minimal mode...")
|
|
1258
1076
|
self.agent = None
|
|
1259
1077
|
|
|
1260
|
-
def _is_port_available(self, port):
|
|
1261
|
-
"""Check if a port is available"""
|
|
1262
|
-
try:
|
|
1263
|
-
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
1264
|
-
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
1265
|
-
test_socket.bind(("0.0.0.0", port))
|
|
1266
|
-
test_socket.close()
|
|
1267
|
-
return True
|
|
1268
|
-
except OSError:
|
|
1269
|
-
return False
|
|
1270
|
-
|
|
1271
|
-
def _is_socket_available(self, socket_path):
|
|
1272
|
-
"""Check if a Unix socket is available"""
|
|
1273
|
-
|
|
1274
|
-
# If socket file doesn't exist, it's available
|
|
1275
|
-
if not os.path.exists(socket_path):
|
|
1276
|
-
return True
|
|
1277
|
-
# If it exists, try to connect to see if it's in use
|
|
1278
|
-
try:
|
|
1279
|
-
test_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
1280
|
-
test_socket.connect(socket_path)
|
|
1281
|
-
test_socket.close()
|
|
1282
|
-
return False # Socket is in use
|
|
1283
|
-
except (ConnectionRefusedError, FileNotFoundError):
|
|
1284
|
-
# Socket file exists but not in use - remove stale socket
|
|
1285
|
-
try:
|
|
1286
|
-
os.remove(socket_path)
|
|
1287
|
-
return True
|
|
1288
|
-
except:
|
|
1289
|
-
return False
|
|
1290
|
-
except Exception:
|
|
1291
|
-
return False
|
|
1292
|
-
|
|
1293
|
-
def _find_available_port(self, start_port, max_attempts=10):
|
|
1294
|
-
"""Find an available port starting from start_port"""
|
|
1295
|
-
for offset in range(max_attempts):
|
|
1296
|
-
port = start_port + offset
|
|
1297
|
-
if self._is_port_available(port):
|
|
1298
|
-
return port
|
|
1299
|
-
return None
|
|
1300
|
-
|
|
1301
|
-
def _find_available_socket(self, base_socket_path, max_attempts=10):
|
|
1302
|
-
"""Find an available socket path"""
|
|
1303
|
-
if self._is_socket_available(base_socket_path):
|
|
1304
|
-
return base_socket_path
|
|
1305
|
-
# Try numbered alternatives
|
|
1306
|
-
for i in range(1, max_attempts):
|
|
1307
|
-
alt_socket = f"{base_socket_path}.{i}"
|
|
1308
|
-
if self._is_socket_available(alt_socket):
|
|
1309
|
-
return alt_socket
|
|
1310
|
-
return None
|
|
1311
|
-
|
|
1312
|
-
def _start_servers(self):
|
|
1313
|
-
"""Auto-start configured servers with port conflict handling"""
|
|
1314
|
-
logger.info("Auto-starting servers...")
|
|
1315
|
-
print("🦆 Auto-starting servers...")
|
|
1316
|
-
|
|
1317
|
-
# Start servers in order: IPC, TCP, WS, MCP
|
|
1318
|
-
server_order = ["ipc", "tcp", "ws", "mcp"]
|
|
1319
|
-
|
|
1320
|
-
for server_type in server_order:
|
|
1321
|
-
if server_type not in self.servers:
|
|
1322
|
-
continue
|
|
1323
|
-
|
|
1324
|
-
config = self.servers[server_type]
|
|
1325
|
-
|
|
1326
|
-
# Check if server is enabled
|
|
1327
|
-
if not config.get("enabled", True):
|
|
1328
|
-
continue
|
|
1329
|
-
|
|
1330
|
-
# Check for LOOKUP_KEY (conditional start based on env var)
|
|
1331
|
-
if "LOOKUP_KEY" in config:
|
|
1332
|
-
lookup_key = config["LOOKUP_KEY"]
|
|
1333
|
-
if not os.getenv(lookup_key):
|
|
1334
|
-
logger.info(f"Skipping {server_type} - {lookup_key} not set")
|
|
1335
|
-
continue
|
|
1336
|
-
|
|
1337
|
-
# Start the server with port conflict handling
|
|
1338
|
-
try:
|
|
1339
|
-
if server_type == "tcp":
|
|
1340
|
-
port = config.get("port", 9999)
|
|
1341
|
-
|
|
1342
|
-
# Check port availability BEFORE attempting to start
|
|
1343
|
-
if not self._is_port_available(port):
|
|
1344
|
-
alt_port = self._find_available_port(port + 1)
|
|
1345
|
-
if alt_port:
|
|
1346
|
-
logger.info(f"Port {port} in use, using {alt_port}")
|
|
1347
|
-
print(f"🦆 Port {port} in use, using {alt_port}")
|
|
1348
|
-
port = alt_port
|
|
1349
|
-
else:
|
|
1350
|
-
logger.warning(f"No available ports found for TCP server")
|
|
1351
|
-
continue
|
|
1352
|
-
|
|
1353
|
-
result = self.agent.tool.tcp(
|
|
1354
|
-
action="start_server", port=port, record_direct_tool_call=False
|
|
1355
|
-
)
|
|
1356
|
-
|
|
1357
|
-
if result.get("status") == "success":
|
|
1358
|
-
logger.info(f"✓ TCP server started on port {port}")
|
|
1359
|
-
print(f"🦆 ✓ TCP server: localhost:{port}")
|
|
1360
|
-
|
|
1361
|
-
elif server_type == "ws":
|
|
1362
|
-
port = config.get("port", 8080)
|
|
1363
|
-
|
|
1364
|
-
# Check port availability BEFORE attempting to start
|
|
1365
|
-
if not self._is_port_available(port):
|
|
1366
|
-
alt_port = self._find_available_port(port + 1)
|
|
1367
|
-
if alt_port:
|
|
1368
|
-
logger.info(f"Port {port} in use, using {alt_port}")
|
|
1369
|
-
print(f"🦆 Port {port} in use, using {alt_port}")
|
|
1370
|
-
port = alt_port
|
|
1371
|
-
else:
|
|
1372
|
-
logger.warning(
|
|
1373
|
-
f"No available ports found for WebSocket server"
|
|
1374
|
-
)
|
|
1375
|
-
continue
|
|
1376
|
-
|
|
1377
|
-
result = self.agent.tool.websocket(
|
|
1378
|
-
action="start_server", port=port, record_direct_tool_call=False
|
|
1379
|
-
)
|
|
1380
|
-
|
|
1381
|
-
if result.get("status") == "success":
|
|
1382
|
-
logger.info(f"✓ WebSocket server started on port {port}")
|
|
1383
|
-
print(f"🦆 ✓ WebSocket server: localhost:{port}")
|
|
1384
|
-
|
|
1385
|
-
elif server_type == "mcp":
|
|
1386
|
-
port = config.get("port", 8000)
|
|
1387
|
-
|
|
1388
|
-
# Check port availability BEFORE attempting to start
|
|
1389
|
-
if not self._is_port_available(port):
|
|
1390
|
-
alt_port = self._find_available_port(port + 1)
|
|
1391
|
-
if alt_port:
|
|
1392
|
-
logger.info(f"Port {port} in use, using {alt_port}")
|
|
1393
|
-
print(f"🦆 Port {port} in use, using {alt_port}")
|
|
1394
|
-
port = alt_port
|
|
1395
|
-
else:
|
|
1396
|
-
logger.warning(f"No available ports found for MCP server")
|
|
1397
|
-
continue
|
|
1398
|
-
|
|
1399
|
-
result = self.agent.tool.mcp_server(
|
|
1400
|
-
action="start",
|
|
1401
|
-
transport="http",
|
|
1402
|
-
port=port,
|
|
1403
|
-
expose_agent=True,
|
|
1404
|
-
agent=self.agent,
|
|
1405
|
-
record_direct_tool_call=False,
|
|
1406
|
-
)
|
|
1407
|
-
|
|
1408
|
-
if result.get("status") == "success":
|
|
1409
|
-
logger.info(f"✓ MCP HTTP server started on port {port}")
|
|
1410
|
-
print(f"🦆 ✓ MCP server: http://localhost:{port}/mcp")
|
|
1411
|
-
|
|
1412
|
-
elif server_type == "ipc":
|
|
1413
|
-
socket_path = config.get("socket_path", "/tmp/devduck_main.sock")
|
|
1414
|
-
|
|
1415
|
-
# Check socket availability BEFORE attempting to start
|
|
1416
|
-
available_socket = self._find_available_socket(socket_path)
|
|
1417
|
-
if not available_socket:
|
|
1418
|
-
logger.warning(
|
|
1419
|
-
f"No available socket paths found for IPC server"
|
|
1420
|
-
)
|
|
1421
|
-
continue
|
|
1422
|
-
|
|
1423
|
-
if available_socket != socket_path:
|
|
1424
|
-
logger.info(
|
|
1425
|
-
f"Socket {socket_path} in use, using {available_socket}"
|
|
1426
|
-
)
|
|
1427
|
-
print(
|
|
1428
|
-
f"🦆 Socket {socket_path} in use, using {available_socket}"
|
|
1429
|
-
)
|
|
1430
|
-
socket_path = available_socket
|
|
1431
|
-
|
|
1432
|
-
result = self.agent.tool.ipc(
|
|
1433
|
-
action="start_server",
|
|
1434
|
-
socket_path=socket_path,
|
|
1435
|
-
record_direct_tool_call=False,
|
|
1436
|
-
)
|
|
1437
|
-
|
|
1438
|
-
if result.get("status") == "success":
|
|
1439
|
-
logger.info(f"✓ IPC server started on {socket_path}")
|
|
1440
|
-
print(f"🦆 ✓ IPC server: {socket_path}")
|
|
1441
|
-
# TODO: support custom file path here so we can trigger foreign python function like another file
|
|
1442
|
-
except Exception as e:
|
|
1443
|
-
logger.error(f"Failed to start {server_type} server: {e}")
|
|
1444
|
-
print(f"🦆 ⚠ {server_type.upper()} server failed: {e}")
|
|
1445
|
-
|
|
1446
1078
|
def __call__(self, query):
|
|
1447
1079
|
"""Make the agent callable with automatic knowledge base integration"""
|
|
1448
1080
|
if not self.agent:
|
|
@@ -1451,12 +1083,11 @@ When you learn something valuable during conversations:
|
|
|
1451
1083
|
|
|
1452
1084
|
try:
|
|
1453
1085
|
logger.info(f"Agent call started: {query[:100]}...")
|
|
1454
|
-
|
|
1455
1086
|
# Mark agent as executing to prevent hot-reload interruption
|
|
1456
1087
|
self._agent_executing = True
|
|
1457
1088
|
|
|
1458
1089
|
# 📚 Knowledge Base Retrieval (BEFORE agent runs)
|
|
1459
|
-
knowledge_base_id = os.getenv("
|
|
1090
|
+
knowledge_base_id = os.getenv("STRANDS_KNOWLEDGE_BASE_ID")
|
|
1460
1091
|
if knowledge_base_id and hasattr(self.agent, "tool"):
|
|
1461
1092
|
try:
|
|
1462
1093
|
if "retrieve" in self.agent.tool_names:
|
|
@@ -1474,6 +1105,7 @@ When you learn something valuable during conversations:
|
|
|
1474
1105
|
if knowledge_base_id and hasattr(self.agent, "tool"):
|
|
1475
1106
|
try:
|
|
1476
1107
|
if "store_in_kb" in self.agent.tool_names:
|
|
1108
|
+
|
|
1477
1109
|
conversation_content = f"Input: {query}, Result: {result!s}"
|
|
1478
1110
|
conversation_title = f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}"
|
|
1479
1111
|
self.agent.tool.store_in_kb(
|
|
@@ -1485,14 +1117,13 @@ When you learn something valuable during conversations:
|
|
|
1485
1117
|
except Exception as e:
|
|
1486
1118
|
logger.warning(f"KB storage failed: {e}")
|
|
1487
1119
|
|
|
1488
|
-
#
|
|
1120
|
+
# Agent finished - check if reload was pending
|
|
1489
1121
|
self._agent_executing = False
|
|
1490
|
-
|
|
1491
|
-
# Check for pending hot-reload
|
|
1122
|
+
logger.info("Agent call completed successfully")
|
|
1492
1123
|
if self._reload_pending:
|
|
1493
1124
|
logger.info("Triggering pending hot-reload after agent completion")
|
|
1494
|
-
print("
|
|
1495
|
-
self.
|
|
1125
|
+
print("🦆 Agent finished - triggering pending hot-reload...")
|
|
1126
|
+
self.hot_reload()
|
|
1496
1127
|
|
|
1497
1128
|
return result
|
|
1498
1129
|
except Exception as e:
|
|
@@ -1506,12 +1137,12 @@ When you learn something valuable during conversations:
|
|
|
1506
1137
|
|
|
1507
1138
|
def restart(self):
|
|
1508
1139
|
"""Restart the agent"""
|
|
1509
|
-
print("
|
|
1510
|
-
logger.debug("\n🦆 Restarting...")
|
|
1140
|
+
print("🦆 Restarting...")
|
|
1511
1141
|
self.__init__()
|
|
1512
1142
|
|
|
1513
1143
|
def _start_file_watcher(self):
|
|
1514
1144
|
"""Start background file watcher for auto hot-reload"""
|
|
1145
|
+
import threading
|
|
1515
1146
|
|
|
1516
1147
|
logger.info("Starting file watcher for hot-reload")
|
|
1517
1148
|
# Get the path to this file
|
|
@@ -1520,7 +1151,6 @@ When you learn something valuable during conversations:
|
|
|
1520
1151
|
self._watch_file.stat().st_mtime if self._watch_file.exists() else None
|
|
1521
1152
|
)
|
|
1522
1153
|
self._watcher_running = True
|
|
1523
|
-
self._is_reloading = False
|
|
1524
1154
|
|
|
1525
1155
|
# Start watcher thread
|
|
1526
1156
|
self._watcher_thread = threading.Thread(
|
|
@@ -1531,13 +1161,15 @@ When you learn something valuable during conversations:
|
|
|
1531
1161
|
|
|
1532
1162
|
def _file_watcher_thread(self):
|
|
1533
1163
|
"""Background thread that watches for file changes"""
|
|
1164
|
+
import time
|
|
1165
|
+
|
|
1534
1166
|
last_reload_time = 0
|
|
1535
1167
|
debounce_seconds = 3 # 3 second debounce
|
|
1536
1168
|
|
|
1537
1169
|
while self._watcher_running:
|
|
1538
1170
|
try:
|
|
1539
|
-
# Skip if currently reloading
|
|
1540
|
-
if self
|
|
1171
|
+
# Skip if currently reloading to prevent triggering during exec()
|
|
1172
|
+
if getattr(self, "_is_reloading", False):
|
|
1541
1173
|
time.sleep(1)
|
|
1542
1174
|
continue
|
|
1543
1175
|
|
|
@@ -1551,36 +1183,34 @@ When you learn something valuable during conversations:
|
|
|
1551
1183
|
and current_mtime > self._last_modified
|
|
1552
1184
|
and current_time - last_reload_time > debounce_seconds
|
|
1553
1185
|
):
|
|
1554
|
-
|
|
1186
|
+
|
|
1187
|
+
print(f"🦆 Detected changes in {self._watch_file.name}!")
|
|
1188
|
+
self._last_modified = current_mtime
|
|
1555
1189
|
last_reload_time = current_time
|
|
1556
1190
|
|
|
1557
1191
|
# Check if agent is currently executing
|
|
1558
|
-
if self
|
|
1192
|
+
if getattr(self, "_agent_executing", False):
|
|
1559
1193
|
logger.info(
|
|
1560
1194
|
"Code change detected but agent is executing - reload pending"
|
|
1561
1195
|
)
|
|
1562
1196
|
print(
|
|
1563
|
-
"
|
|
1197
|
+
"🦆 Agent is currently executing - reload will trigger after completion"
|
|
1564
1198
|
)
|
|
1565
1199
|
self._reload_pending = True
|
|
1566
|
-
# Don't update _last_modified yet - keep detecting the change
|
|
1567
1200
|
else:
|
|
1568
1201
|
# Safe to reload immediately
|
|
1569
|
-
self._last_modified = current_mtime
|
|
1570
1202
|
logger.info(
|
|
1571
1203
|
f"Code change detected in {self._watch_file.name} - triggering hot-reload"
|
|
1572
1204
|
)
|
|
1573
1205
|
time.sleep(
|
|
1574
1206
|
0.5
|
|
1575
1207
|
) # Small delay to ensure file write is complete
|
|
1576
|
-
self.
|
|
1208
|
+
self.hot_reload()
|
|
1577
1209
|
else:
|
|
1578
|
-
|
|
1579
|
-
if not self._reload_pending:
|
|
1580
|
-
self._last_modified = current_mtime
|
|
1210
|
+
self._last_modified = current_mtime
|
|
1581
1211
|
|
|
1582
1212
|
except Exception as e:
|
|
1583
|
-
|
|
1213
|
+
print(f"🦆 File watcher error: {e}")
|
|
1584
1214
|
|
|
1585
1215
|
# Check every 1 second
|
|
1586
1216
|
time.sleep(1)
|
|
@@ -1588,45 +1218,41 @@ When you learn something valuable during conversations:
|
|
|
1588
1218
|
def _stop_file_watcher(self):
|
|
1589
1219
|
"""Stop the file watcher"""
|
|
1590
1220
|
self._watcher_running = False
|
|
1591
|
-
|
|
1221
|
+
print("🦆 File watcher stopped")
|
|
1592
1222
|
|
|
1593
|
-
def
|
|
1223
|
+
def hot_reload(self):
|
|
1594
1224
|
"""Hot-reload by restarting the entire Python process with fresh code"""
|
|
1595
1225
|
logger.info("Hot-reload initiated")
|
|
1596
|
-
print("
|
|
1226
|
+
print("🦆 Hot-reloading via process restart...")
|
|
1597
1227
|
|
|
1598
1228
|
try:
|
|
1599
1229
|
# Set reload flag to prevent recursive reloads during shutdown
|
|
1600
|
-
self
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
if hasattr(self, "_watch_file") and self._watch_file.exists():
|
|
1604
|
-
self._last_modified = self._watch_file.stat().st_mtime
|
|
1230
|
+
if hasattr(self, "_is_reloading") and self._is_reloading:
|
|
1231
|
+
print("🦆 Reload already in progress, skipping")
|
|
1232
|
+
return
|
|
1605
1233
|
|
|
1606
|
-
|
|
1607
|
-
self._reload_pending = False
|
|
1234
|
+
self._is_reloading = True
|
|
1608
1235
|
|
|
1609
1236
|
# Stop the file watcher
|
|
1610
1237
|
if hasattr(self, "_watcher_running"):
|
|
1611
1238
|
self._watcher_running = False
|
|
1612
1239
|
|
|
1613
|
-
print("
|
|
1614
|
-
logger.debug("\n🦆 Restarting process with fresh code...")
|
|
1240
|
+
print("🦆 Restarting process with fresh code...")
|
|
1615
1241
|
|
|
1616
1242
|
# Restart the entire Python process
|
|
1617
1243
|
# This ensures all code is freshly loaded
|
|
1618
1244
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
1619
1245
|
|
|
1620
1246
|
except Exception as e:
|
|
1621
|
-
|
|
1622
|
-
print(
|
|
1623
|
-
print("\n🦆 Falling back to manual restart")
|
|
1247
|
+
print(f"🦆 Hot-reload failed: {e}")
|
|
1248
|
+
print("🦆 Falling back to manual restart")
|
|
1624
1249
|
self._is_reloading = False
|
|
1625
1250
|
|
|
1626
1251
|
def status(self):
|
|
1627
1252
|
"""Show current status"""
|
|
1628
1253
|
return {
|
|
1629
1254
|
"model": self.model,
|
|
1255
|
+
"host": self.ollama_host,
|
|
1630
1256
|
"env": self.env_info,
|
|
1631
1257
|
"agent_ready": self.agent is not None,
|
|
1632
1258
|
"tools": len(self.tools) if hasattr(self, "tools") else 0,
|
|
@@ -1641,14 +1267,23 @@ When you learn something valuable during conversations:
|
|
|
1641
1267
|
|
|
1642
1268
|
# 🦆 Auto-initialize when imported
|
|
1643
1269
|
# Check environment variables to control server configuration
|
|
1644
|
-
# Also check if --mcp flag is present to skip auto-starting servers
|
|
1645
1270
|
_auto_start = os.getenv("DEVDUCK_AUTO_START_SERVERS", "true").lower() == "true"
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1271
|
+
_tcp_port = int(os.getenv("DEVDUCK_TCP_PORT", "9999"))
|
|
1272
|
+
_ws_port = int(os.getenv("DEVDUCK_WS_PORT", "8080"))
|
|
1273
|
+
_mcp_port = int(os.getenv("DEVDUCK_MCP_PORT", "8000"))
|
|
1274
|
+
_enable_tcp = os.getenv("DEVDUCK_ENABLE_TCP", "true").lower() == "true"
|
|
1275
|
+
_enable_ws = os.getenv("DEVDUCK_ENABLE_WS", "true").lower() == "true"
|
|
1276
|
+
_enable_mcp = os.getenv("DEVDUCK_ENABLE_MCP", "true").lower() == "true"
|
|
1277
|
+
|
|
1278
|
+
devduck = DevDuck(
|
|
1279
|
+
auto_start_servers=_auto_start,
|
|
1280
|
+
tcp_port=_tcp_port,
|
|
1281
|
+
ws_port=_ws_port,
|
|
1282
|
+
mcp_port=_mcp_port,
|
|
1283
|
+
enable_tcp=_enable_tcp,
|
|
1284
|
+
enable_ws=_enable_ws,
|
|
1285
|
+
enable_mcp=_enable_mcp,
|
|
1286
|
+
)
|
|
1652
1287
|
|
|
1653
1288
|
|
|
1654
1289
|
# 🚀 Convenience functions
|
|
@@ -1669,7 +1304,7 @@ def restart():
|
|
|
1669
1304
|
|
|
1670
1305
|
def hot_reload():
|
|
1671
1306
|
"""Quick hot-reload without restart"""
|
|
1672
|
-
devduck.
|
|
1307
|
+
devduck.hot_reload()
|
|
1673
1308
|
|
|
1674
1309
|
|
|
1675
1310
|
def extract_commands_from_history():
|
|
@@ -1755,7 +1390,7 @@ def interactive():
|
|
|
1755
1390
|
print(f"📝 Logs: {LOG_DIR}")
|
|
1756
1391
|
print("Type 'exit', 'quit', or 'q' to quit.")
|
|
1757
1392
|
print("Prefix with ! to run shell commands (e.g., ! ls -la)")
|
|
1758
|
-
print("
|
|
1393
|
+
print("-" * 50)
|
|
1759
1394
|
logger.info("Interactive mode started")
|
|
1760
1395
|
|
|
1761
1396
|
# Set up prompt_toolkit with history
|
|
@@ -1770,10 +1405,6 @@ def interactive():
|
|
|
1770
1405
|
all_commands = list(set(base_commands + history_commands))
|
|
1771
1406
|
completer = WordCompleter(all_commands, ignore_case=True)
|
|
1772
1407
|
|
|
1773
|
-
# Track consecutive interrupts for double Ctrl+C to exit
|
|
1774
|
-
interrupt_count = 0
|
|
1775
|
-
last_interrupt = 0
|
|
1776
|
-
|
|
1777
1408
|
while True:
|
|
1778
1409
|
try:
|
|
1779
1410
|
# Use prompt_toolkit for enhanced input with arrow key support
|
|
@@ -1783,11 +1414,9 @@ def interactive():
|
|
|
1783
1414
|
auto_suggest=AutoSuggestFromHistory(),
|
|
1784
1415
|
completer=completer,
|
|
1785
1416
|
complete_while_typing=True,
|
|
1417
|
+
mouse_support=False, # breaks scrolling when enabled
|
|
1786
1418
|
)
|
|
1787
1419
|
|
|
1788
|
-
# Reset interrupt count on successful prompt
|
|
1789
|
-
interrupt_count = 0
|
|
1790
|
-
|
|
1791
1420
|
# Check for exit command
|
|
1792
1421
|
if q.lower() in ["exit", "quit", "q"]:
|
|
1793
1422
|
print("\n🦆 Goodbye!")
|
|
@@ -1810,10 +1439,6 @@ def interactive():
|
|
|
1810
1439
|
)
|
|
1811
1440
|
devduck._agent_executing = False
|
|
1812
1441
|
|
|
1813
|
-
# Reset terminal to fix rendering issues after command output
|
|
1814
|
-
print("\r", end="", flush=True)
|
|
1815
|
-
sys.stdout.flush()
|
|
1816
|
-
|
|
1817
1442
|
# Append shell command to history
|
|
1818
1443
|
append_to_shell_history(q, result["content"][0]["text"])
|
|
1819
1444
|
|
|
@@ -1822,17 +1447,95 @@ def interactive():
|
|
|
1822
1447
|
print(
|
|
1823
1448
|
"🦆 Shell command finished - triggering pending hot-reload..."
|
|
1824
1449
|
)
|
|
1825
|
-
devduck.
|
|
1450
|
+
devduck.hot_reload()
|
|
1826
1451
|
else:
|
|
1827
1452
|
print("🦆 Agent unavailable")
|
|
1828
1453
|
except Exception as e:
|
|
1829
1454
|
devduck._agent_executing = False # Reset on error
|
|
1830
1455
|
print(f"🦆 Shell command error: {e}")
|
|
1831
|
-
# Reset terminal on error too
|
|
1832
|
-
print("\r", end="", flush=True)
|
|
1833
|
-
sys.stdout.flush()
|
|
1834
1456
|
continue
|
|
1835
1457
|
|
|
1458
|
+
# Get recent conversation context
|
|
1459
|
+
recent_context = get_last_messages()
|
|
1460
|
+
|
|
1461
|
+
# Get recent logs
|
|
1462
|
+
recent_logs = get_recent_logs()
|
|
1463
|
+
|
|
1464
|
+
# Update system prompt before each call with history context
|
|
1465
|
+
if devduck.agent:
|
|
1466
|
+
# Rebuild system prompt with history
|
|
1467
|
+
own_code = get_own_source_code()
|
|
1468
|
+
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
1469
|
+
|
|
1470
|
+
devduck.agent.system_prompt = f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
|
|
1471
|
+
|
|
1472
|
+
Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
|
|
1473
|
+
Python: {devduck.env_info['python']}
|
|
1474
|
+
Model: {devduck.model}
|
|
1475
|
+
Hostname: {devduck.env_info['hostname']}
|
|
1476
|
+
Session ID: {session_id}
|
|
1477
|
+
|
|
1478
|
+
You are:
|
|
1479
|
+
- Minimalist: Brief, direct responses
|
|
1480
|
+
- Self-healing: Adapt when things break
|
|
1481
|
+
- Efficient: Get things done fast
|
|
1482
|
+
- Pragmatic: Use what works
|
|
1483
|
+
|
|
1484
|
+
Current working directory: {devduck.env_info['cwd']}
|
|
1485
|
+
|
|
1486
|
+
{recent_context}
|
|
1487
|
+
{recent_logs}
|
|
1488
|
+
|
|
1489
|
+
## Your Own Implementation:
|
|
1490
|
+
You have full access to your own source code for self-awareness and self-modification:
|
|
1491
|
+
|
|
1492
|
+
{own_code}
|
|
1493
|
+
|
|
1494
|
+
## Hot Reload System Active:
|
|
1495
|
+
- **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
|
|
1496
|
+
- **No Restart Needed** - Tools are auto-loaded and ready to use instantly
|
|
1497
|
+
- **Live Development** - Modify existing tools while running and test immediately
|
|
1498
|
+
- **Full Python Access** - Create any Python functionality as a tool
|
|
1499
|
+
|
|
1500
|
+
## Dynamic Tool Loading:
|
|
1501
|
+
- **Install Tools** - Use install_tools() to load tools from any Python package
|
|
1502
|
+
- Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
|
|
1503
|
+
- Expands capabilities without restart
|
|
1504
|
+
- Access to entire Python ecosystem
|
|
1505
|
+
|
|
1506
|
+
## MCP Server:
|
|
1507
|
+
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
1508
|
+
- Example: mcp_server(action="start", port=8000)
|
|
1509
|
+
- Connect from Claude Desktop, other agents, or custom clients
|
|
1510
|
+
- Full bidirectional communication
|
|
1511
|
+
|
|
1512
|
+
## System Prompt Management:
|
|
1513
|
+
- Use system_prompt(action='get') to view current prompt
|
|
1514
|
+
- Use system_prompt(action='set', prompt='new text') to update
|
|
1515
|
+
- Changes persist in SYSTEM_PROMPT environment variable
|
|
1516
|
+
|
|
1517
|
+
## Shell Commands:
|
|
1518
|
+
- Prefix with ! to execute shell commands directly
|
|
1519
|
+
- Example: ! ls -la (lists files)
|
|
1520
|
+
- Example: ! pwd (shows current directory)
|
|
1521
|
+
|
|
1522
|
+
**Response Format:**
|
|
1523
|
+
- Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
|
|
1524
|
+
- Communication: **MINIMAL WORDS**
|
|
1525
|
+
- Efficiency: **Speed is paramount**
|
|
1526
|
+
|
|
1527
|
+
{os.getenv('SYSTEM_PROMPT', '')}"""
|
|
1528
|
+
|
|
1529
|
+
# Update model if MODEL_PROVIDER changed
|
|
1530
|
+
model_provider = os.getenv("MODEL_PROVIDER")
|
|
1531
|
+
if model_provider:
|
|
1532
|
+
try:
|
|
1533
|
+
from strands_tools.utils.models.model import create_model
|
|
1534
|
+
|
|
1535
|
+
devduck.agent.model = create_model(provider=model_provider)
|
|
1536
|
+
except Exception as e:
|
|
1537
|
+
print(f"🦆 Model update error: {e}")
|
|
1538
|
+
|
|
1836
1539
|
# Execute the agent with user input
|
|
1837
1540
|
result = ask(q)
|
|
1838
1541
|
|
|
@@ -1840,21 +1543,7 @@ def interactive():
|
|
|
1840
1543
|
append_to_shell_history(q, str(result))
|
|
1841
1544
|
|
|
1842
1545
|
except KeyboardInterrupt:
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
# Check if this is a consecutive interrupt within 2 seconds
|
|
1846
|
-
if current_time - last_interrupt < 2:
|
|
1847
|
-
interrupt_count += 1
|
|
1848
|
-
if interrupt_count >= 2:
|
|
1849
|
-
print("\n🦆 Exiting...")
|
|
1850
|
-
break
|
|
1851
|
-
else:
|
|
1852
|
-
print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
|
|
1853
|
-
else:
|
|
1854
|
-
interrupt_count = 1
|
|
1855
|
-
print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
|
|
1856
|
-
|
|
1857
|
-
last_interrupt = current_time
|
|
1546
|
+
print("\n🦆 Interrupted. Type 'exit' to quit.")
|
|
1858
1547
|
continue
|
|
1859
1548
|
except Exception as e:
|
|
1860
1549
|
print(f"🦆 Error: {e}")
|
|
@@ -1872,62 +1561,46 @@ def cli():
|
|
|
1872
1561
|
Examples:
|
|
1873
1562
|
devduck # Start interactive mode
|
|
1874
1563
|
devduck "your query here" # One-shot query
|
|
1875
|
-
devduck --
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
export DEVDUCK_TOOLS="strands_tools:shell,editor:strands_fun_tools:clipboard"
|
|
1879
|
-
|
|
1880
|
-
Claude Desktop Config:
|
|
1881
|
-
{
|
|
1882
|
-
"mcpServers": {
|
|
1883
|
-
"devduck": {
|
|
1884
|
-
"command": "uvx",
|
|
1885
|
-
"args": ["devduck", "--mcp"]
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1564
|
+
devduck --tcp-port 9000 # Custom TCP port
|
|
1565
|
+
devduck --no-tcp --no-ws # Disable TCP and WebSocket
|
|
1566
|
+
devduck --mcp-port 3000 # Custom MCP port
|
|
1889
1567
|
""",
|
|
1890
1568
|
)
|
|
1891
1569
|
|
|
1892
1570
|
# Query argument
|
|
1893
1571
|
parser.add_argument("query", nargs="*", help="Query to send to the agent")
|
|
1894
1572
|
|
|
1895
|
-
#
|
|
1573
|
+
# Server configuration
|
|
1896
1574
|
parser.add_argument(
|
|
1897
|
-
"--
|
|
1575
|
+
"--tcp-port", type=int, default=9999, help="TCP server port (default: 9999)"
|
|
1576
|
+
)
|
|
1577
|
+
parser.add_argument(
|
|
1578
|
+
"--ws-port",
|
|
1579
|
+
type=int,
|
|
1580
|
+
default=8080,
|
|
1581
|
+
help="WebSocket server port (default: 8080)",
|
|
1582
|
+
)
|
|
1583
|
+
parser.add_argument(
|
|
1584
|
+
"--mcp-port",
|
|
1585
|
+
type=int,
|
|
1586
|
+
default=8000,
|
|
1587
|
+
help="MCP HTTP server port (default: 8000)",
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
# Server enable/disable flags
|
|
1591
|
+
parser.add_argument("--no-tcp", action="store_true", help="Disable TCP server")
|
|
1592
|
+
parser.add_argument("--no-ws", action="store_true", help="Disable WebSocket server")
|
|
1593
|
+
parser.add_argument("--no-mcp", action="store_true", help="Disable MCP server")
|
|
1594
|
+
parser.add_argument(
|
|
1595
|
+
"--no-servers",
|
|
1898
1596
|
action="store_true",
|
|
1899
|
-
help="
|
|
1597
|
+
help="Disable all servers (no TCP, WebSocket, or MCP)",
|
|
1900
1598
|
)
|
|
1901
1599
|
|
|
1902
1600
|
args = parser.parse_args()
|
|
1903
1601
|
|
|
1904
1602
|
logger.info("CLI mode started")
|
|
1905
1603
|
|
|
1906
|
-
# Handle --mcp flag for stdio mode
|
|
1907
|
-
if args.mcp:
|
|
1908
|
-
logger.info("Starting MCP server in stdio mode (blocking, foreground)")
|
|
1909
|
-
print("🦆 Starting MCP stdio server...", file=sys.stderr)
|
|
1910
|
-
|
|
1911
|
-
# Don't auto-start HTTP/TCP/WS servers for stdio mode
|
|
1912
|
-
if devduck.agent:
|
|
1913
|
-
try:
|
|
1914
|
-
# Start MCP server in stdio mode - this BLOCKS until terminated
|
|
1915
|
-
devduck.agent.tool.mcp_server(
|
|
1916
|
-
action="start",
|
|
1917
|
-
transport="stdio",
|
|
1918
|
-
expose_agent=True,
|
|
1919
|
-
agent=devduck.agent,
|
|
1920
|
-
record_direct_tool_call=False,
|
|
1921
|
-
)
|
|
1922
|
-
except Exception as e:
|
|
1923
|
-
logger.error(f"Failed to start MCP stdio server: {e}")
|
|
1924
|
-
print(f"🦆 Error: {e}", file=sys.stderr)
|
|
1925
|
-
sys.exit(1)
|
|
1926
|
-
else:
|
|
1927
|
-
print("🦆 Agent not available", file=sys.stderr)
|
|
1928
|
-
sys.exit(1)
|
|
1929
|
-
return
|
|
1930
|
-
|
|
1931
1604
|
if args.query:
|
|
1932
1605
|
query = " ".join(args.query)
|
|
1933
1606
|
logger.info(f"CLI query: {query}")
|