devduck 0.1.0__py3-none-any.whl → 0.2.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 +546 -91
- devduck/__main__.py +7 -0
- devduck/_version.py +34 -0
- devduck/tools/__init__.py +7 -0
- devduck/tools/install_tools.py +308 -0
- devduck/tools/mcp_server.py +572 -0
- devduck/tools/tcp.py +263 -93
- devduck/tools/websocket.py +492 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/METADATA +48 -11
- devduck-0.2.0.dist-info/RECORD +16 -0
- devduck-0.1.0.dist-info/RECORD +0 -11
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/WHEEL +0 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/entry_points.txt +0 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.1.0.dist-info → devduck-0.2.0.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -8,32 +8,78 @@ import subprocess
|
|
|
8
8
|
import os
|
|
9
9
|
import platform
|
|
10
10
|
import socket
|
|
11
|
+
import logging
|
|
12
|
+
import tempfile
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from datetime import datetime
|
|
13
15
|
from typing import Dict, Any
|
|
16
|
+
from logging.handlers import RotatingFileHandler
|
|
14
17
|
|
|
15
18
|
os.environ["BYPASS_TOOL_CONSENT"] = "true"
|
|
16
19
|
os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
|
|
17
20
|
|
|
21
|
+
# 📝 Setup logging system
|
|
22
|
+
LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
|
|
23
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
LOG_FILE = LOG_DIR / "devduck.log"
|
|
25
|
+
|
|
26
|
+
# Configure logger
|
|
27
|
+
logger = logging.getLogger("devduck")
|
|
28
|
+
logger.setLevel(logging.DEBUG)
|
|
29
|
+
|
|
30
|
+
# File handler with rotation (10MB max, keep 3 backups)
|
|
31
|
+
file_handler = RotatingFileHandler(
|
|
32
|
+
LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
|
|
33
|
+
)
|
|
34
|
+
file_handler.setLevel(logging.DEBUG)
|
|
35
|
+
file_formatter = logging.Formatter(
|
|
36
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
37
|
+
)
|
|
38
|
+
file_handler.setFormatter(file_formatter)
|
|
39
|
+
|
|
40
|
+
# Console handler (only warnings and above)
|
|
41
|
+
console_handler = logging.StreamHandler()
|
|
42
|
+
console_handler.setLevel(logging.WARNING)
|
|
43
|
+
console_formatter = logging.Formatter("🦆 %(levelname)s: %(message)s")
|
|
44
|
+
console_handler.setFormatter(console_formatter)
|
|
45
|
+
|
|
46
|
+
logger.addHandler(file_handler)
|
|
47
|
+
logger.addHandler(console_handler)
|
|
48
|
+
|
|
49
|
+
logger.info("DevDuck logging system initialized")
|
|
50
|
+
|
|
18
51
|
|
|
19
52
|
# 🔧 Self-healing dependency installer
|
|
20
53
|
def ensure_deps():
|
|
21
54
|
"""Install dependencies at runtime if missing"""
|
|
22
|
-
|
|
55
|
+
import importlib.metadata
|
|
56
|
+
|
|
57
|
+
deps = [
|
|
58
|
+
"strands-agents",
|
|
59
|
+
"strands-agents[ollama]",
|
|
60
|
+
"strands-agents[openai]",
|
|
61
|
+
"strands-agents[anthropic]",
|
|
62
|
+
"strands-agents-tools",
|
|
63
|
+
]
|
|
23
64
|
|
|
65
|
+
# Check each package individually using importlib.metadata
|
|
24
66
|
for dep in deps:
|
|
67
|
+
pkg_name = dep.split("[")[0] # Get base package name (strip extras)
|
|
25
68
|
try:
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
break
|
|
30
|
-
except ImportError:
|
|
69
|
+
# Check if package is installed using metadata (checks PyPI package name)
|
|
70
|
+
importlib.metadata.version(pkg_name)
|
|
71
|
+
except importlib.metadata.PackageNotFoundError:
|
|
31
72
|
print(f"🦆 Installing {dep}...")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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}")
|
|
37
83
|
|
|
38
84
|
|
|
39
85
|
# 🌍 Environment adaptation
|
|
@@ -242,6 +288,129 @@ def system_prompt_tool(
|
|
|
242
288
|
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
243
289
|
|
|
244
290
|
|
|
291
|
+
def view_logs_tool(
|
|
292
|
+
action: str = "view",
|
|
293
|
+
lines: int = 100,
|
|
294
|
+
pattern: str = None,
|
|
295
|
+
) -> Dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
View and manage DevDuck logs.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
action: Action to perform - "view", "tail", "search", "clear", "stats"
|
|
301
|
+
lines: Number of lines to show (for view/tail)
|
|
302
|
+
pattern: Search pattern (for search action)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Dict with status and content
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
if action == "view":
|
|
309
|
+
if not LOG_FILE.exists():
|
|
310
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
311
|
+
|
|
312
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
313
|
+
all_lines = f.readlines()
|
|
314
|
+
recent_lines = (
|
|
315
|
+
all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
316
|
+
)
|
|
317
|
+
content = "".join(recent_lines)
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"status": "success",
|
|
321
|
+
"content": [
|
|
322
|
+
{"text": f"Last {len(recent_lines)} log lines:\n\n{content}"}
|
|
323
|
+
],
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
elif action == "tail":
|
|
327
|
+
if not LOG_FILE.exists():
|
|
328
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
329
|
+
|
|
330
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
331
|
+
all_lines = f.readlines()
|
|
332
|
+
tail_lines = all_lines[-50:] if len(all_lines) > 50 else all_lines
|
|
333
|
+
content = "".join(tail_lines)
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
"status": "success",
|
|
337
|
+
"content": [{"text": f"Tail (last 50 lines):\n\n{content}"}],
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
elif action == "search":
|
|
341
|
+
if not pattern:
|
|
342
|
+
return {
|
|
343
|
+
"status": "error",
|
|
344
|
+
"content": [{"text": "pattern parameter required for search"}],
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if not LOG_FILE.exists():
|
|
348
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
349
|
+
|
|
350
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
351
|
+
matching_lines = [line for line in f if pattern.lower() in line.lower()]
|
|
352
|
+
|
|
353
|
+
if not matching_lines:
|
|
354
|
+
return {
|
|
355
|
+
"status": "success",
|
|
356
|
+
"content": [{"text": f"No matches found for pattern: {pattern}"}],
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
content = "".join(matching_lines[-100:]) # Last 100 matches
|
|
360
|
+
return {
|
|
361
|
+
"status": "success",
|
|
362
|
+
"content": [
|
|
363
|
+
{
|
|
364
|
+
"text": f"Found {len(matching_lines)} matches (showing last 100):\n\n{content}"
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
elif action == "clear":
|
|
370
|
+
if LOG_FILE.exists():
|
|
371
|
+
LOG_FILE.unlink()
|
|
372
|
+
logger.info("Log file cleared by user")
|
|
373
|
+
return {
|
|
374
|
+
"status": "success",
|
|
375
|
+
"content": [{"text": "Logs cleared successfully"}],
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
elif action == "stats":
|
|
379
|
+
if not LOG_FILE.exists():
|
|
380
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
381
|
+
|
|
382
|
+
stat = LOG_FILE.stat()
|
|
383
|
+
size_mb = stat.st_size / (1024 * 1024)
|
|
384
|
+
modified = datetime.fromtimestamp(stat.st_mtime).strftime(
|
|
385
|
+
"%Y-%m-%d %H:%M:%S"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
389
|
+
total_lines = sum(1 for _ in f)
|
|
390
|
+
|
|
391
|
+
stats_text = f"""Log File Statistics:
|
|
392
|
+
Path: {LOG_FILE}
|
|
393
|
+
Size: {size_mb:.2f} MB
|
|
394
|
+
Lines: {total_lines}
|
|
395
|
+
Last Modified: {modified}"""
|
|
396
|
+
|
|
397
|
+
return {"status": "success", "content": [{"text": stats_text}]}
|
|
398
|
+
|
|
399
|
+
else:
|
|
400
|
+
return {
|
|
401
|
+
"status": "error",
|
|
402
|
+
"content": [
|
|
403
|
+
{
|
|
404
|
+
"text": f"Unknown action: {action}. Valid: view, tail, search, clear, stats"
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(f"Error in view_logs_tool: {e}")
|
|
411
|
+
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
412
|
+
|
|
413
|
+
|
|
245
414
|
def get_shell_history_file():
|
|
246
415
|
"""Get the devduck-specific history file path."""
|
|
247
416
|
devduck_history = Path.home() / ".devduck_history"
|
|
@@ -253,22 +422,22 @@ def get_shell_history_file():
|
|
|
253
422
|
def get_shell_history_files():
|
|
254
423
|
"""Get available shell history file paths."""
|
|
255
424
|
history_files = []
|
|
256
|
-
|
|
425
|
+
|
|
257
426
|
# devduck history (primary)
|
|
258
427
|
devduck_history = Path(get_shell_history_file())
|
|
259
428
|
if devduck_history.exists():
|
|
260
429
|
history_files.append(("devduck", str(devduck_history)))
|
|
261
|
-
|
|
430
|
+
|
|
262
431
|
# Bash history
|
|
263
432
|
bash_history = Path.home() / ".bash_history"
|
|
264
433
|
if bash_history.exists():
|
|
265
434
|
history_files.append(("bash", str(bash_history)))
|
|
266
|
-
|
|
435
|
+
|
|
267
436
|
# Zsh history
|
|
268
437
|
zsh_history = Path.home() / ".zsh_history"
|
|
269
438
|
if zsh_history.exists():
|
|
270
439
|
history_files.append(("zsh", str(zsh_history)))
|
|
271
|
-
|
|
440
|
+
|
|
272
441
|
return history_files
|
|
273
442
|
|
|
274
443
|
|
|
@@ -277,14 +446,16 @@ def parse_history_line(line, history_type):
|
|
|
277
446
|
line = line.strip()
|
|
278
447
|
if not line:
|
|
279
448
|
return None
|
|
280
|
-
|
|
449
|
+
|
|
281
450
|
if history_type == "devduck":
|
|
282
451
|
# devduck format: ": timestamp:0;# devduck: query" or ": timestamp:0;# devduck_result: result"
|
|
283
452
|
if "# devduck:" in line:
|
|
284
453
|
try:
|
|
285
454
|
timestamp_str = line.split(":")[1]
|
|
286
455
|
timestamp = int(timestamp_str)
|
|
287
|
-
readable_time = datetime.fromtimestamp(timestamp).strftime(
|
|
456
|
+
readable_time = datetime.fromtimestamp(timestamp).strftime(
|
|
457
|
+
"%Y-%m-%d %H:%M:%S"
|
|
458
|
+
)
|
|
288
459
|
query = line.split("# devduck:")[-1].strip()
|
|
289
460
|
return ("you", readable_time, query)
|
|
290
461
|
except (ValueError, IndexError):
|
|
@@ -293,12 +464,14 @@ def parse_history_line(line, history_type):
|
|
|
293
464
|
try:
|
|
294
465
|
timestamp_str = line.split(":")[1]
|
|
295
466
|
timestamp = int(timestamp_str)
|
|
296
|
-
readable_time = datetime.fromtimestamp(timestamp).strftime(
|
|
467
|
+
readable_time = datetime.fromtimestamp(timestamp).strftime(
|
|
468
|
+
"%Y-%m-%d %H:%M:%S"
|
|
469
|
+
)
|
|
297
470
|
result = line.split("# devduck_result:")[-1].strip()
|
|
298
471
|
return ("me", readable_time, result)
|
|
299
472
|
except (ValueError, IndexError):
|
|
300
473
|
return None
|
|
301
|
-
|
|
474
|
+
|
|
302
475
|
elif history_type == "zsh":
|
|
303
476
|
if line.startswith(": ") and ":0;" in line:
|
|
304
477
|
try:
|
|
@@ -306,37 +479,65 @@ def parse_history_line(line, history_type):
|
|
|
306
479
|
if len(parts) == 2:
|
|
307
480
|
timestamp_str = parts[0].split(":")[1]
|
|
308
481
|
timestamp = int(timestamp_str)
|
|
309
|
-
readable_time = datetime.fromtimestamp(timestamp).strftime(
|
|
482
|
+
readable_time = datetime.fromtimestamp(timestamp).strftime(
|
|
483
|
+
"%Y-%m-%d %H:%M:%S"
|
|
484
|
+
)
|
|
310
485
|
command = parts[1].strip()
|
|
311
486
|
if not command.startswith("devduck "):
|
|
312
487
|
return ("shell", readable_time, f"$ {command}")
|
|
313
488
|
except (ValueError, IndexError):
|
|
314
489
|
return None
|
|
315
|
-
|
|
490
|
+
|
|
316
491
|
elif history_type == "bash":
|
|
317
492
|
readable_time = "recent"
|
|
318
493
|
if not line.startswith("devduck "):
|
|
319
494
|
return ("shell", readable_time, f"$ {line}")
|
|
320
|
-
|
|
495
|
+
|
|
321
496
|
return None
|
|
322
497
|
|
|
323
498
|
|
|
499
|
+
def get_recent_logs():
|
|
500
|
+
"""Get the last N lines from the log file for context."""
|
|
501
|
+
try:
|
|
502
|
+
log_line_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
|
|
503
|
+
|
|
504
|
+
if not LOG_FILE.exists():
|
|
505
|
+
return ""
|
|
506
|
+
|
|
507
|
+
with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
|
508
|
+
all_lines = f.readlines()
|
|
509
|
+
|
|
510
|
+
recent_lines = (
|
|
511
|
+
all_lines[-log_line_count:]
|
|
512
|
+
if len(all_lines) > log_line_count
|
|
513
|
+
else all_lines
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if not recent_lines:
|
|
517
|
+
return ""
|
|
518
|
+
|
|
519
|
+
log_content = "".join(recent_lines)
|
|
520
|
+
return f"\n\n## Recent Logs (last {len(recent_lines)} lines):\n```\n{log_content}```\n"
|
|
521
|
+
except Exception as e:
|
|
522
|
+
return f"\n\n## Recent Logs: Error reading logs - {e}\n"
|
|
523
|
+
|
|
524
|
+
|
|
324
525
|
def get_last_messages():
|
|
325
526
|
"""Get the last N messages from multiple shell histories for context."""
|
|
326
527
|
try:
|
|
327
528
|
message_count = int(os.getenv("DEVDUCK_LAST_MESSAGE_COUNT", "200"))
|
|
328
529
|
all_entries = []
|
|
329
|
-
|
|
530
|
+
|
|
330
531
|
history_files = get_shell_history_files()
|
|
331
|
-
|
|
532
|
+
|
|
332
533
|
for history_type, history_file in history_files:
|
|
333
534
|
try:
|
|
334
535
|
with open(history_file, encoding="utf-8", errors="ignore") as f:
|
|
335
536
|
lines = f.readlines()
|
|
336
|
-
|
|
537
|
+
|
|
337
538
|
if history_type == "bash":
|
|
338
539
|
lines = lines[-message_count:]
|
|
339
|
-
|
|
540
|
+
|
|
340
541
|
# Join multi-line entries for zsh
|
|
341
542
|
if history_type == "zsh":
|
|
342
543
|
joined_lines = []
|
|
@@ -355,22 +556,26 @@ def get_last_messages():
|
|
|
355
556
|
if current_line:
|
|
356
557
|
joined_lines.append(current_line)
|
|
357
558
|
lines = joined_lines
|
|
358
|
-
|
|
559
|
+
|
|
359
560
|
for line in lines:
|
|
360
561
|
parsed = parse_history_line(line, history_type)
|
|
361
562
|
if parsed:
|
|
362
563
|
all_entries.append(parsed)
|
|
363
564
|
except Exception:
|
|
364
565
|
continue
|
|
365
|
-
|
|
366
|
-
recent_entries =
|
|
367
|
-
|
|
566
|
+
|
|
567
|
+
recent_entries = (
|
|
568
|
+
all_entries[-message_count:]
|
|
569
|
+
if len(all_entries) >= message_count
|
|
570
|
+
else all_entries
|
|
571
|
+
)
|
|
572
|
+
|
|
368
573
|
context = ""
|
|
369
574
|
if recent_entries:
|
|
370
575
|
context += f"\n\nRecent conversation context (last {len(recent_entries)} messages):\n"
|
|
371
576
|
for speaker, timestamp, content in recent_entries:
|
|
372
577
|
context += f"[{timestamp}] {speaker}: {content}\n"
|
|
373
|
-
|
|
578
|
+
|
|
374
579
|
return context
|
|
375
580
|
except Exception:
|
|
376
581
|
return ""
|
|
@@ -379,15 +584,21 @@ def get_last_messages():
|
|
|
379
584
|
def append_to_shell_history(query, response):
|
|
380
585
|
"""Append the interaction to devduck shell history."""
|
|
381
586
|
import time
|
|
587
|
+
|
|
382
588
|
try:
|
|
383
589
|
history_file = get_shell_history_file()
|
|
384
590
|
timestamp = str(int(time.time()))
|
|
385
|
-
|
|
591
|
+
|
|
386
592
|
with open(history_file, "a", encoding="utf-8") as f:
|
|
387
593
|
f.write(f": {timestamp}:0;# devduck: {query}\n")
|
|
388
|
-
response_summary =
|
|
594
|
+
response_summary = (
|
|
595
|
+
str(response).replace("\n", " ")[
|
|
596
|
+
: int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
|
|
597
|
+
]
|
|
598
|
+
+ "..."
|
|
599
|
+
)
|
|
389
600
|
f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
|
|
390
|
-
|
|
601
|
+
|
|
391
602
|
os.chmod(history_file, 0o600)
|
|
392
603
|
except Exception:
|
|
393
604
|
pass
|
|
@@ -395,8 +606,9 @@ def append_to_shell_history(query, response):
|
|
|
395
606
|
|
|
396
607
|
# 🦆 The devduck agent
|
|
397
608
|
class DevDuck:
|
|
398
|
-
def __init__(self):
|
|
609
|
+
def __init__(self, auto_start_servers=True):
|
|
399
610
|
"""Initialize the minimalist adaptive agent"""
|
|
611
|
+
logger.info("Initializing DevDuck agent...")
|
|
400
612
|
try:
|
|
401
613
|
# Self-heal dependencies
|
|
402
614
|
ensure_deps()
|
|
@@ -404,25 +616,32 @@ class DevDuck:
|
|
|
404
616
|
# Adapt to environment
|
|
405
617
|
self.env_info, self.ollama_host, self.model = adapt_to_env()
|
|
406
618
|
|
|
619
|
+
# Execution state tracking for hot-reload
|
|
620
|
+
self._agent_executing = False
|
|
621
|
+
self._reload_pending = False
|
|
622
|
+
|
|
407
623
|
# Import after ensuring deps
|
|
408
624
|
from strands import Agent, tool
|
|
409
625
|
from strands.models.ollama import OllamaModel
|
|
410
|
-
from strands.session.file_session_manager import FileSessionManager
|
|
411
626
|
from strands_tools.utils.models.model import create_model
|
|
412
|
-
from .tools import tcp
|
|
627
|
+
from .tools import tcp, websocket, mcp_server, install_tools
|
|
628
|
+
from strands_fun_tools import (
|
|
629
|
+
listen,
|
|
630
|
+
cursor,
|
|
631
|
+
clipboard,
|
|
632
|
+
screen_reader,
|
|
633
|
+
yolo_vision,
|
|
634
|
+
)
|
|
413
635
|
from strands_tools import (
|
|
414
636
|
shell,
|
|
415
637
|
editor,
|
|
416
|
-
file_read,
|
|
417
|
-
file_write,
|
|
418
|
-
python_repl,
|
|
419
|
-
current_time,
|
|
420
638
|
calculator,
|
|
421
|
-
|
|
639
|
+
python_repl,
|
|
422
640
|
image_reader,
|
|
423
641
|
use_agent,
|
|
424
642
|
load_tool,
|
|
425
643
|
environment,
|
|
644
|
+
mcp_client,
|
|
426
645
|
)
|
|
427
646
|
|
|
428
647
|
# Wrap system_prompt_tool with @tool decorator
|
|
@@ -436,24 +655,42 @@ class DevDuck:
|
|
|
436
655
|
"""Manage agent system prompt dynamically."""
|
|
437
656
|
return system_prompt_tool(action, prompt, context, variable_name)
|
|
438
657
|
|
|
439
|
-
#
|
|
658
|
+
# Wrap view_logs_tool with @tool decorator
|
|
659
|
+
@tool
|
|
660
|
+
def view_logs(
|
|
661
|
+
action: str = "view",
|
|
662
|
+
lines: int = 100,
|
|
663
|
+
pattern: str = None,
|
|
664
|
+
) -> Dict[str, Any]:
|
|
665
|
+
"""View and manage DevDuck logs."""
|
|
666
|
+
return view_logs_tool(action, lines, pattern)
|
|
667
|
+
|
|
668
|
+
# Minimal but functional toolset including system_prompt and view_logs
|
|
440
669
|
self.tools = [
|
|
441
670
|
shell,
|
|
442
671
|
editor,
|
|
443
|
-
file_read,
|
|
444
|
-
file_write,
|
|
445
|
-
python_repl,
|
|
446
|
-
current_time,
|
|
447
672
|
calculator,
|
|
448
|
-
|
|
673
|
+
python_repl,
|
|
449
674
|
image_reader,
|
|
450
675
|
use_agent,
|
|
451
676
|
load_tool,
|
|
452
677
|
environment,
|
|
453
678
|
system_prompt,
|
|
454
|
-
|
|
679
|
+
view_logs,
|
|
680
|
+
tcp,
|
|
681
|
+
websocket,
|
|
682
|
+
mcp_server,
|
|
683
|
+
install_tools,
|
|
684
|
+
mcp_client,
|
|
685
|
+
listen,
|
|
686
|
+
cursor,
|
|
687
|
+
clipboard,
|
|
688
|
+
screen_reader,
|
|
689
|
+
yolo_vision,
|
|
455
690
|
]
|
|
456
691
|
|
|
692
|
+
logger.info(f"Initialized {len(self.tools)} tools")
|
|
693
|
+
|
|
457
694
|
# Check if MODEL_PROVIDER env variable is set
|
|
458
695
|
model_provider = os.getenv("MODEL_PROVIDER")
|
|
459
696
|
|
|
@@ -469,23 +706,72 @@ class DevDuck:
|
|
|
469
706
|
keep_alive="5m",
|
|
470
707
|
)
|
|
471
708
|
|
|
472
|
-
session_manager = FileSessionManager(
|
|
473
|
-
session_id=f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
474
|
-
)
|
|
475
|
-
|
|
476
709
|
# Create agent with self-healing
|
|
477
710
|
self.agent = Agent(
|
|
478
711
|
model=self.agent_model,
|
|
479
712
|
tools=self.tools,
|
|
480
713
|
system_prompt=self._build_system_prompt(),
|
|
481
714
|
load_tools_from_directory=True,
|
|
482
|
-
# session_manager=session_manager,
|
|
483
715
|
)
|
|
484
716
|
|
|
485
|
-
#
|
|
486
|
-
|
|
717
|
+
# 🚀 AUTO-START SERVERS: TCP (9999), WebSocket (8080), MCP HTTP (8000)
|
|
718
|
+
if auto_start_servers:
|
|
719
|
+
logger.info("Auto-starting servers...")
|
|
720
|
+
print("🦆 Auto-starting servers...")
|
|
721
|
+
|
|
722
|
+
try:
|
|
723
|
+
# Start TCP server on port 9999
|
|
724
|
+
tcp_result = self.agent.tool.tcp(action="start_server", port=9999)
|
|
725
|
+
if tcp_result.get("status") == "success":
|
|
726
|
+
logger.info("✓ TCP server started on port 9999")
|
|
727
|
+
print("🦆 ✓ TCP server: localhost:9999")
|
|
728
|
+
else:
|
|
729
|
+
logger.warning(f"TCP server start issue: {tcp_result}")
|
|
730
|
+
except Exception as e:
|
|
731
|
+
logger.error(f"Failed to start TCP server: {e}")
|
|
732
|
+
print(f"🦆 ⚠ TCP server failed: {e}")
|
|
733
|
+
|
|
734
|
+
try:
|
|
735
|
+
# Start WebSocket server on port 8080
|
|
736
|
+
ws_result = self.agent.tool.websocket(
|
|
737
|
+
action="start_server", port=8080
|
|
738
|
+
)
|
|
739
|
+
if ws_result.get("status") == "success":
|
|
740
|
+
logger.info("✓ WebSocket server started on port 8080")
|
|
741
|
+
print("🦆 ✓ WebSocket server: localhost:8080")
|
|
742
|
+
else:
|
|
743
|
+
logger.warning(f"WebSocket server start issue: {ws_result}")
|
|
744
|
+
except Exception as e:
|
|
745
|
+
logger.error(f"Failed to start WebSocket server: {e}")
|
|
746
|
+
print(f"🦆 ⚠ WebSocket server failed: {e}")
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
# Start MCP server with HTTP transport on port 8000
|
|
750
|
+
mcp_result = self.agent.tool.mcp_server(
|
|
751
|
+
action="start",
|
|
752
|
+
transport="http",
|
|
753
|
+
port=8000,
|
|
754
|
+
expose_agent=True,
|
|
755
|
+
agent=self.agent,
|
|
756
|
+
)
|
|
757
|
+
if mcp_result.get("status") == "success":
|
|
758
|
+
logger.info("✓ MCP HTTP server started on port 8000")
|
|
759
|
+
print("🦆 ✓ MCP server: http://localhost:8000/mcp")
|
|
760
|
+
else:
|
|
761
|
+
logger.warning(f"MCP server start issue: {mcp_result}")
|
|
762
|
+
except Exception as e:
|
|
763
|
+
logger.error(f"Failed to start MCP server: {e}")
|
|
764
|
+
print(f"🦆 ⚠ MCP server failed: {e}")
|
|
765
|
+
|
|
766
|
+
# Start file watcher for auto hot-reload
|
|
767
|
+
self._start_file_watcher()
|
|
768
|
+
|
|
769
|
+
logger.info(
|
|
770
|
+
f"DevDuck agent initialized successfully with model {self.model}"
|
|
771
|
+
)
|
|
487
772
|
|
|
488
773
|
except Exception as e:
|
|
774
|
+
logger.error(f"Initialization failed: {e}")
|
|
489
775
|
self._self_heal(e)
|
|
490
776
|
|
|
491
777
|
def _build_system_prompt(self):
|
|
@@ -502,7 +788,9 @@ class DevDuck:
|
|
|
502
788
|
|
|
503
789
|
# Get own source code for self-awareness
|
|
504
790
|
own_code = get_own_source_code()
|
|
505
|
-
|
|
791
|
+
|
|
792
|
+
# print(own_code)
|
|
793
|
+
|
|
506
794
|
# Get recent conversation history context (with error handling)
|
|
507
795
|
try:
|
|
508
796
|
recent_context = get_last_messages()
|
|
@@ -510,6 +798,13 @@ class DevDuck:
|
|
|
510
798
|
print(f"🦆 Warning: Could not load history context: {e}")
|
|
511
799
|
recent_context = ""
|
|
512
800
|
|
|
801
|
+
# Get recent logs for immediate visibility
|
|
802
|
+
try:
|
|
803
|
+
recent_logs = get_recent_logs()
|
|
804
|
+
except Exception as e:
|
|
805
|
+
print(f"🦆 Warning: Could not load recent logs: {e}")
|
|
806
|
+
recent_logs = ""
|
|
807
|
+
|
|
513
808
|
return f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
|
|
514
809
|
|
|
515
810
|
Environment: {self.env_info['os']} {self.env_info['arch']}
|
|
@@ -527,6 +822,7 @@ You are:
|
|
|
527
822
|
Current working directory: {self.env_info['cwd']}
|
|
528
823
|
|
|
529
824
|
{recent_context}
|
|
825
|
+
{recent_logs}
|
|
530
826
|
|
|
531
827
|
## Your Own Implementation:
|
|
532
828
|
You have full access to your own source code for self-awareness and self-modification:
|
|
@@ -539,6 +835,18 @@ You have full access to your own source code for self-awareness and self-modific
|
|
|
539
835
|
- **Live Development** - Modify existing tools while running and test immediately
|
|
540
836
|
- **Full Python Access** - Create any Python functionality as a tool
|
|
541
837
|
|
|
838
|
+
## Dynamic Tool Loading:
|
|
839
|
+
- **Install Tools** - Use install_tools() to load tools from any Python package
|
|
840
|
+
- Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
|
|
841
|
+
- Expands capabilities without restart
|
|
842
|
+
- Access to entire Python ecosystem
|
|
843
|
+
|
|
844
|
+
## MCP Server:
|
|
845
|
+
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
846
|
+
- Example: mcp_server(action="start", port=8000)
|
|
847
|
+
- Connect from Claude Desktop, other agents, or custom clients
|
|
848
|
+
- Full bidirectional communication
|
|
849
|
+
|
|
542
850
|
## Tool Creation Patterns:
|
|
543
851
|
|
|
544
852
|
### **1. @tool Decorator:**
|
|
@@ -606,41 +914,21 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
|
606
914
|
|
|
607
915
|
def _self_heal(self, error):
|
|
608
916
|
"""Attempt self-healing when errors occur"""
|
|
917
|
+
logger.error(f"Self-healing triggered by error: {error}")
|
|
609
918
|
print(f"🦆 Self-healing from: {error}")
|
|
610
919
|
|
|
611
920
|
# Prevent infinite recursion by tracking heal attempts
|
|
612
921
|
if not hasattr(self, "_heal_count"):
|
|
613
922
|
self._heal_count = 0
|
|
614
|
-
|
|
923
|
+
|
|
615
924
|
self._heal_count += 1
|
|
616
|
-
|
|
925
|
+
|
|
617
926
|
# Limit recursion - if we've tried more than 3 times, give up
|
|
618
927
|
if self._heal_count > 3:
|
|
619
928
|
print(f"🦆 Self-healing failed after {self._heal_count} attempts")
|
|
620
929
|
print("🦆 Please fix the issue manually and restart")
|
|
621
930
|
sys.exit(1)
|
|
622
931
|
|
|
623
|
-
# Handle tool validation errors by resetting session
|
|
624
|
-
if "Expected toolResult blocks" in str(error):
|
|
625
|
-
print("🦆 Tool validation error detected - resetting session...")
|
|
626
|
-
# Add timestamp postfix to create fresh session
|
|
627
|
-
postfix = datetime.now().strftime("%H%M%S")
|
|
628
|
-
new_session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}-{postfix}"
|
|
629
|
-
print(f"🦆 New session: {new_session_id}")
|
|
630
|
-
|
|
631
|
-
# Update session manager with new session
|
|
632
|
-
try:
|
|
633
|
-
from strands.session.file_session_manager import FileSessionManager
|
|
634
|
-
|
|
635
|
-
self.agent.session_manager = FileSessionManager(
|
|
636
|
-
session_id=new_session_id
|
|
637
|
-
)
|
|
638
|
-
print("🦆 Session reset successful - continuing with fresh history")
|
|
639
|
-
self._heal_count = 0 # Reset counter on success
|
|
640
|
-
return # Early return - no need for full restart
|
|
641
|
-
except Exception as session_error:
|
|
642
|
-
print(f"🦆 Session reset failed: {session_error}")
|
|
643
|
-
|
|
644
932
|
# Common healing strategies
|
|
645
933
|
if "not found" in str(error).lower() and "model" in str(error).lower():
|
|
646
934
|
print("🦆 Model not found - trying to pull model...")
|
|
@@ -710,11 +998,28 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
|
710
998
|
def __call__(self, query):
|
|
711
999
|
"""Make the agent callable"""
|
|
712
1000
|
if not self.agent:
|
|
1001
|
+
logger.warning("Agent unavailable - attempted to call with query")
|
|
713
1002
|
return "🦆 Agent unavailable - try: devduck.restart()"
|
|
714
1003
|
|
|
715
1004
|
try:
|
|
716
|
-
|
|
1005
|
+
logger.info(f"Agent call started: {query[:100]}...")
|
|
1006
|
+
# Mark agent as executing to prevent hot-reload interruption
|
|
1007
|
+
self._agent_executing = True
|
|
1008
|
+
|
|
1009
|
+
result = self.agent(query)
|
|
1010
|
+
|
|
1011
|
+
# Agent finished - check if reload was pending
|
|
1012
|
+
self._agent_executing = False
|
|
1013
|
+
logger.info("Agent call completed successfully")
|
|
1014
|
+
if self._reload_pending:
|
|
1015
|
+
logger.info("Triggering pending hot-reload after agent completion")
|
|
1016
|
+
print("🦆 Agent finished - triggering pending hot-reload...")
|
|
1017
|
+
self.hot_reload()
|
|
1018
|
+
|
|
1019
|
+
return result
|
|
717
1020
|
except Exception as e:
|
|
1021
|
+
self._agent_executing = False # Reset flag on error
|
|
1022
|
+
logger.error(f"Agent call failed with error: {e}")
|
|
718
1023
|
self._self_heal(e)
|
|
719
1024
|
if self.agent:
|
|
720
1025
|
return self.agent(query)
|
|
@@ -730,6 +1035,7 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
|
730
1035
|
"""Start background file watcher for auto hot-reload"""
|
|
731
1036
|
import threading
|
|
732
1037
|
|
|
1038
|
+
logger.info("Starting file watcher for hot-reload")
|
|
733
1039
|
# Get the path to this file
|
|
734
1040
|
self._watch_file = Path(__file__).resolve()
|
|
735
1041
|
self._last_modified = (
|
|
@@ -742,6 +1048,7 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
|
742
1048
|
target=self._file_watcher_thread, daemon=True
|
|
743
1049
|
)
|
|
744
1050
|
self._watcher_thread.start()
|
|
1051
|
+
logger.info(f"File watcher started, monitoring {self._watch_file}")
|
|
745
1052
|
|
|
746
1053
|
def _file_watcher_thread(self):
|
|
747
1054
|
"""Background thread that watches for file changes"""
|
|
@@ -772,9 +1079,24 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
|
772
1079
|
self._last_modified = current_mtime
|
|
773
1080
|
last_reload_time = current_time
|
|
774
1081
|
|
|
775
|
-
#
|
|
776
|
-
|
|
777
|
-
|
|
1082
|
+
# Check if agent is currently executing
|
|
1083
|
+
if getattr(self, "_agent_executing", False):
|
|
1084
|
+
logger.info(
|
|
1085
|
+
"Code change detected but agent is executing - reload pending"
|
|
1086
|
+
)
|
|
1087
|
+
print(
|
|
1088
|
+
"🦆 Agent is currently executing - reload will trigger after completion"
|
|
1089
|
+
)
|
|
1090
|
+
self._reload_pending = True
|
|
1091
|
+
else:
|
|
1092
|
+
# Safe to reload immediately
|
|
1093
|
+
logger.info(
|
|
1094
|
+
f"Code change detected in {self._watch_file.name} - triggering hot-reload"
|
|
1095
|
+
)
|
|
1096
|
+
time.sleep(
|
|
1097
|
+
0.5
|
|
1098
|
+
) # Small delay to ensure file write is complete
|
|
1099
|
+
self.hot_reload()
|
|
778
1100
|
else:
|
|
779
1101
|
self._last_modified = current_mtime
|
|
780
1102
|
|
|
@@ -791,6 +1113,7 @@ def weather(action: str, location: str = None) -> Dict[str, Any]:
|
|
|
791
1113
|
|
|
792
1114
|
def hot_reload(self):
|
|
793
1115
|
"""Hot-reload by restarting the entire Python process with fresh code"""
|
|
1116
|
+
logger.info("Hot-reload initiated")
|
|
794
1117
|
print("🦆 Hot-reloading via process restart...")
|
|
795
1118
|
|
|
796
1119
|
try:
|
|
@@ -858,17 +1181,115 @@ def hot_reload():
|
|
|
858
1181
|
devduck.hot_reload()
|
|
859
1182
|
|
|
860
1183
|
|
|
1184
|
+
def extract_commands_from_history():
|
|
1185
|
+
"""Extract commonly used commands from shell history for auto-completion."""
|
|
1186
|
+
commands = set()
|
|
1187
|
+
history_files = get_shell_history_files()
|
|
1188
|
+
|
|
1189
|
+
# Limit the number of recent commands to process for performance
|
|
1190
|
+
max_recent_commands = 100
|
|
1191
|
+
|
|
1192
|
+
for history_type, history_file in history_files:
|
|
1193
|
+
try:
|
|
1194
|
+
with open(history_file, encoding="utf-8", errors="ignore") as f:
|
|
1195
|
+
lines = f.readlines()
|
|
1196
|
+
|
|
1197
|
+
# Take recent commands for better relevance
|
|
1198
|
+
recent_lines = (
|
|
1199
|
+
lines[-max_recent_commands:]
|
|
1200
|
+
if len(lines) > max_recent_commands
|
|
1201
|
+
else lines
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
for line in recent_lines:
|
|
1205
|
+
line = line.strip()
|
|
1206
|
+
if not line:
|
|
1207
|
+
continue
|
|
1208
|
+
|
|
1209
|
+
if history_type == "devduck":
|
|
1210
|
+
# Extract devduck commands
|
|
1211
|
+
if "# devduck:" in line:
|
|
1212
|
+
try:
|
|
1213
|
+
query = line.split("# devduck:")[-1].strip()
|
|
1214
|
+
# Extract first word as command
|
|
1215
|
+
first_word = query.split()[0] if query.split() else None
|
|
1216
|
+
if (
|
|
1217
|
+
first_word and len(first_word) > 2
|
|
1218
|
+
): # Only meaningful commands
|
|
1219
|
+
commands.add(first_word.lower())
|
|
1220
|
+
except (ValueError, IndexError):
|
|
1221
|
+
continue
|
|
1222
|
+
|
|
1223
|
+
elif history_type == "zsh":
|
|
1224
|
+
# Zsh format: ": timestamp:0;command"
|
|
1225
|
+
if line.startswith(": ") and ":0;" in line:
|
|
1226
|
+
try:
|
|
1227
|
+
parts = line.split(":0;", 1)
|
|
1228
|
+
if len(parts) == 2:
|
|
1229
|
+
full_command = parts[1].strip()
|
|
1230
|
+
# Extract first word as command
|
|
1231
|
+
first_word = (
|
|
1232
|
+
full_command.split()[0]
|
|
1233
|
+
if full_command.split()
|
|
1234
|
+
else None
|
|
1235
|
+
)
|
|
1236
|
+
if (
|
|
1237
|
+
first_word and len(first_word) > 1
|
|
1238
|
+
): # Only meaningful commands
|
|
1239
|
+
commands.add(first_word.lower())
|
|
1240
|
+
except (ValueError, IndexError):
|
|
1241
|
+
continue
|
|
1242
|
+
|
|
1243
|
+
elif history_type == "bash":
|
|
1244
|
+
# Bash format: simple command per line
|
|
1245
|
+
first_word = line.split()[0] if line.split() else None
|
|
1246
|
+
if first_word and len(first_word) > 1: # Only meaningful commands
|
|
1247
|
+
commands.add(first_word.lower())
|
|
1248
|
+
|
|
1249
|
+
except Exception:
|
|
1250
|
+
# Skip files that can't be read
|
|
1251
|
+
continue
|
|
1252
|
+
|
|
1253
|
+
return list(commands)
|
|
1254
|
+
|
|
1255
|
+
|
|
861
1256
|
def interactive():
|
|
862
1257
|
"""Interactive REPL mode for devduck"""
|
|
1258
|
+
from prompt_toolkit import prompt
|
|
1259
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
1260
|
+
from prompt_toolkit.completion import WordCompleter
|
|
1261
|
+
from prompt_toolkit.history import FileHistory
|
|
1262
|
+
|
|
863
1263
|
print("🦆 DevDuck")
|
|
1264
|
+
print(f"📝 Logs: {LOG_DIR}")
|
|
864
1265
|
print("Type 'exit', 'quit', or 'q' to quit.")
|
|
865
1266
|
print("Prefix with ! to run shell commands (e.g., ! ls -la)")
|
|
866
1267
|
print("-" * 50)
|
|
1268
|
+
logger.info("Interactive mode started")
|
|
1269
|
+
|
|
1270
|
+
# Set up prompt_toolkit with history
|
|
1271
|
+
history_file = get_shell_history_file()
|
|
1272
|
+
history = FileHistory(history_file)
|
|
1273
|
+
|
|
1274
|
+
# Create completions from common commands and shell history
|
|
1275
|
+
base_commands = ["exit", "quit", "q", "help", "clear", "status", "reload"]
|
|
1276
|
+
history_commands = extract_commands_from_history()
|
|
1277
|
+
|
|
1278
|
+
# Combine base commands with commands from history
|
|
1279
|
+
all_commands = list(set(base_commands + history_commands))
|
|
1280
|
+
completer = WordCompleter(all_commands, ignore_case=True)
|
|
867
1281
|
|
|
868
1282
|
while True:
|
|
869
1283
|
try:
|
|
870
|
-
#
|
|
871
|
-
q =
|
|
1284
|
+
# Use prompt_toolkit for enhanced input with arrow key support
|
|
1285
|
+
q = prompt(
|
|
1286
|
+
"\n🦆 ",
|
|
1287
|
+
history=history,
|
|
1288
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
1289
|
+
completer=completer,
|
|
1290
|
+
complete_while_typing=True,
|
|
1291
|
+
mouse_support=False, # breaks scrolling when enabled
|
|
1292
|
+
)
|
|
872
1293
|
|
|
873
1294
|
# Check for exit command
|
|
874
1295
|
if q.lower() in ["exit", "quit", "q"]:
|
|
@@ -884,24 +1305,42 @@ def interactive():
|
|
|
884
1305
|
shell_command = q[1:].strip()
|
|
885
1306
|
try:
|
|
886
1307
|
if devduck.agent:
|
|
887
|
-
|
|
1308
|
+
devduck._agent_executing = (
|
|
1309
|
+
True # Prevent hot-reload during shell execution
|
|
1310
|
+
)
|
|
1311
|
+
result = devduck.agent.tool.shell(
|
|
1312
|
+
command=shell_command, timeout=9000
|
|
1313
|
+
)
|
|
1314
|
+
devduck._agent_executing = False
|
|
1315
|
+
|
|
888
1316
|
# Append shell command to history
|
|
889
1317
|
append_to_shell_history(q, result["content"][0]["text"])
|
|
1318
|
+
|
|
1319
|
+
# Check if reload was pending
|
|
1320
|
+
if devduck._reload_pending:
|
|
1321
|
+
print(
|
|
1322
|
+
"🦆 Shell command finished - triggering pending hot-reload..."
|
|
1323
|
+
)
|
|
1324
|
+
devduck.hot_reload()
|
|
890
1325
|
else:
|
|
891
1326
|
print("🦆 Agent unavailable")
|
|
892
1327
|
except Exception as e:
|
|
1328
|
+
devduck._agent_executing = False # Reset on error
|
|
893
1329
|
print(f"🦆 Shell command error: {e}")
|
|
894
1330
|
continue
|
|
895
1331
|
|
|
896
1332
|
# Get recent conversation context
|
|
897
1333
|
recent_context = get_last_messages()
|
|
898
|
-
|
|
1334
|
+
|
|
1335
|
+
# Get recent logs
|
|
1336
|
+
recent_logs = get_recent_logs()
|
|
1337
|
+
|
|
899
1338
|
# Update system prompt before each call with history context
|
|
900
1339
|
if devduck.agent:
|
|
901
1340
|
# Rebuild system prompt with history
|
|
902
1341
|
own_code = get_own_source_code()
|
|
903
1342
|
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
904
|
-
|
|
1343
|
+
|
|
905
1344
|
devduck.agent.system_prompt = f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
|
|
906
1345
|
|
|
907
1346
|
Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
|
|
@@ -919,6 +1358,7 @@ You are:
|
|
|
919
1358
|
Current working directory: {devduck.env_info['cwd']}
|
|
920
1359
|
|
|
921
1360
|
{recent_context}
|
|
1361
|
+
{recent_logs}
|
|
922
1362
|
|
|
923
1363
|
## Your Own Implementation:
|
|
924
1364
|
You have full access to your own source code for self-awareness and self-modification:
|
|
@@ -931,6 +1371,18 @@ You have full access to your own source code for self-awareness and self-modific
|
|
|
931
1371
|
- **Live Development** - Modify existing tools while running and test immediately
|
|
932
1372
|
- **Full Python Access** - Create any Python functionality as a tool
|
|
933
1373
|
|
|
1374
|
+
## Dynamic Tool Loading:
|
|
1375
|
+
- **Install Tools** - Use install_tools() to load tools from any Python package
|
|
1376
|
+
- Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
|
|
1377
|
+
- Expands capabilities without restart
|
|
1378
|
+
- Access to entire Python ecosystem
|
|
1379
|
+
|
|
1380
|
+
## MCP Server:
|
|
1381
|
+
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
1382
|
+
- Example: mcp_server(action="start", port=8000)
|
|
1383
|
+
- Connect from Claude Desktop, other agents, or custom clients
|
|
1384
|
+
- Full bidirectional communication
|
|
1385
|
+
|
|
934
1386
|
## System Prompt Management:
|
|
935
1387
|
- Use system_prompt(action='get') to view current prompt
|
|
936
1388
|
- Use system_prompt(action='set', prompt='new text') to update
|
|
@@ -947,19 +1399,20 @@ You have full access to your own source code for self-awareness and self-modific
|
|
|
947
1399
|
- Efficiency: **Speed is paramount**
|
|
948
1400
|
|
|
949
1401
|
{os.getenv('SYSTEM_PROMPT', '')}"""
|
|
950
|
-
|
|
1402
|
+
|
|
951
1403
|
# Update model if MODEL_PROVIDER changed
|
|
952
1404
|
model_provider = os.getenv("MODEL_PROVIDER")
|
|
953
1405
|
if model_provider:
|
|
954
1406
|
try:
|
|
955
1407
|
from strands_tools.utils.models.model import create_model
|
|
1408
|
+
|
|
956
1409
|
devduck.agent.model = create_model(provider=model_provider)
|
|
957
1410
|
except Exception as e:
|
|
958
1411
|
print(f"🦆 Model update error: {e}")
|
|
959
1412
|
|
|
960
1413
|
# Execute the agent with user input
|
|
961
1414
|
result = ask(q)
|
|
962
|
-
|
|
1415
|
+
|
|
963
1416
|
# Append to shell history
|
|
964
1417
|
append_to_shell_history(q, str(result))
|
|
965
1418
|
|
|
@@ -973,8 +1426,10 @@ You have full access to your own source code for self-awareness and self-modific
|
|
|
973
1426
|
|
|
974
1427
|
def cli():
|
|
975
1428
|
"""CLI entry point for pip-installed devduck command"""
|
|
1429
|
+
logger.info("CLI mode started")
|
|
976
1430
|
if len(sys.argv) > 1:
|
|
977
1431
|
query = " ".join(sys.argv[1:])
|
|
1432
|
+
logger.info(f"CLI query: {query}")
|
|
978
1433
|
result = ask(query)
|
|
979
1434
|
print(result)
|
|
980
1435
|
else:
|