devduck 0.1.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 ADDED
@@ -0,0 +1,999 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ šŸ¦† devduck - extreme minimalist self-adapting agent
4
+ one file. self-healing. runtime dependencies. adaptive.
5
+ """
6
+ import sys
7
+ import subprocess
8
+ import os
9
+ import platform
10
+ import socket
11
+ from pathlib import Path
12
+ from datetime import datetime
13
+ from typing import Dict, Any
14
+
15
+ os.environ["BYPASS_TOOL_CONSENT"] = "true"
16
+ os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
17
+
18
+
19
+ # šŸ”§ Self-healing dependency installer
20
+ def ensure_deps():
21
+ """Install dependencies at runtime if missing"""
22
+ deps = ["strands-agents", "strands-agents[ollama]", "strands-agents[openai]", "strands-agents[anthropic]", "strands-agents-tools"]
23
+
24
+ for dep in deps:
25
+ try:
26
+ if "strands" in dep:
27
+ import strands
28
+
29
+ break
30
+ except ImportError:
31
+ print(f"šŸ¦† Installing {dep}...")
32
+ subprocess.check_call(
33
+ [sys.executable, "-m", "pip", "install", dep],
34
+ stdout=subprocess.DEVNULL,
35
+ stderr=subprocess.DEVNULL,
36
+ )
37
+
38
+
39
+ # šŸŒ Environment adaptation
40
+ def adapt_to_env():
41
+ """Self-adapt based on environment"""
42
+ env_info = {
43
+ "os": platform.system(),
44
+ "arch": platform.machine(),
45
+ "python": sys.version_info,
46
+ "cwd": str(Path.cwd()),
47
+ "home": str(Path.home()),
48
+ "shell": os.environ.get("SHELL", "unknown"),
49
+ "hostname": socket.gethostname(),
50
+ }
51
+
52
+ # Adaptive configurations - using common models
53
+ if env_info["os"] == "Darwin": # macOS
54
+ ollama_host = "http://localhost:11434"
55
+ model = "qwen3:1.7b" # Lightweight for macOS
56
+ elif env_info["os"] == "Linux":
57
+ ollama_host = "http://localhost:11434"
58
+ model = "qwen3:30b" # More power on Linux
59
+ else: # Windows
60
+ ollama_host = "http://localhost:11434"
61
+ model = "qwen3:8b" # Conservative for Windows
62
+
63
+ return env_info, ollama_host, model
64
+
65
+
66
+ # šŸ” Self-awareness: Read own source code
67
+ def get_own_source_code():
68
+ """
69
+ Read and return the source code of this agent file.
70
+
71
+ Returns:
72
+ str: The complete source code for self-awareness
73
+ """
74
+ try:
75
+ # Read this file (__init__.py)
76
+ current_file = __file__
77
+ with open(current_file, "r", encoding="utf-8") as f:
78
+ init_code = f.read()
79
+ return f"# devduck/__init__.py\n```python\n{init_code}\n```"
80
+ except Exception as e:
81
+ return f"Error reading own source code: {e}"
82
+
83
+
84
+ # šŸ› ļø System prompt tool (with .prompt file persistence)
85
+ def system_prompt_tool(
86
+ action: str,
87
+ prompt: str | None = None,
88
+ context: str | None = None,
89
+ variable_name: str = "SYSTEM_PROMPT",
90
+ ) -> Dict[str, Any]:
91
+ """
92
+ Manage the agent's system prompt dynamically with file persistence.
93
+
94
+ Args:
95
+ action: "view", "update", "add_context", or "reset"
96
+ prompt: New system prompt text (required for "update")
97
+ context: Additional context to prepend (for "add_context")
98
+ variable_name: Environment variable name (default: SYSTEM_PROMPT)
99
+
100
+ Returns:
101
+ Dict with status and content
102
+ """
103
+ from pathlib import Path
104
+ import tempfile
105
+
106
+ def _get_prompt_file_path() -> Path:
107
+ """Get the .prompt file path in temp directory."""
108
+ temp_dir = Path(tempfile.gettempdir()) / ".devduck"
109
+ temp_dir.mkdir(exist_ok=True, mode=0o700) # Create with restrictive permissions
110
+ return temp_dir / ".prompt"
111
+
112
+ def _write_prompt_file(prompt_text: str) -> None:
113
+ """Write prompt to .prompt file in temp directory."""
114
+ prompt_file = _get_prompt_file_path()
115
+ try:
116
+ # Create file with restrictive permissions
117
+ with open(
118
+ prompt_file,
119
+ "w",
120
+ encoding="utf-8",
121
+ opener=lambda path, flags: os.open(path, flags, 0o600),
122
+ ) as f:
123
+ f.write(prompt_text)
124
+ except (OSError, PermissionError):
125
+ try:
126
+ prompt_file.write_text(prompt_text, encoding="utf-8")
127
+ prompt_file.chmod(0o600)
128
+ except (OSError, PermissionError):
129
+ prompt_file.write_text(prompt_text, encoding="utf-8")
130
+
131
+ def _get_system_prompt(var_name: str) -> str:
132
+ """Get current system prompt from environment variable."""
133
+ return os.environ.get(var_name, "")
134
+
135
+ def _update_system_prompt(new_prompt: str, var_name: str) -> None:
136
+ """Update system prompt in both environment and .prompt file."""
137
+ os.environ[var_name] = new_prompt
138
+ if var_name == "SYSTEM_PROMPT":
139
+ _write_prompt_file(new_prompt)
140
+
141
+ try:
142
+ if action == "view":
143
+ current = _get_system_prompt(variable_name)
144
+ return {
145
+ "status": "success",
146
+ "content": [
147
+ {"text": f"Current system prompt from {variable_name}:{current}"}
148
+ ],
149
+ }
150
+
151
+ elif action == "update":
152
+ if not prompt:
153
+ return {
154
+ "status": "error",
155
+ "content": [
156
+ {"text": "Error: prompt parameter required for update action"}
157
+ ],
158
+ }
159
+
160
+ _update_system_prompt(prompt, variable_name)
161
+
162
+ if variable_name == "SYSTEM_PROMPT":
163
+ message = f"System prompt updated (env: {variable_name}, file: .prompt)"
164
+ else:
165
+ message = f"System prompt updated (env: {variable_name})"
166
+
167
+ return {"status": "success", "content": [{"text": message}]}
168
+
169
+ elif action == "add_context":
170
+ if not context:
171
+ return {
172
+ "status": "error",
173
+ "content": [
174
+ {
175
+ "text": "Error: context parameter required for add_context action"
176
+ }
177
+ ],
178
+ }
179
+
180
+ current = _get_system_prompt(variable_name)
181
+ new_prompt = f"{current} {context}" if current else context
182
+ _update_system_prompt(new_prompt, variable_name)
183
+
184
+ if variable_name == "SYSTEM_PROMPT":
185
+ message = f"Context added to system prompt (env: {variable_name}, file: .prompt)"
186
+ else:
187
+ message = f"Context added to system prompt (env: {variable_name})"
188
+
189
+ return {"status": "success", "content": [{"text": message}]}
190
+
191
+ elif action == "reset":
192
+ os.environ.pop(variable_name, None)
193
+
194
+ if variable_name == "SYSTEM_PROMPT":
195
+ prompt_file = _get_prompt_file_path()
196
+ if prompt_file.exists():
197
+ try:
198
+ prompt_file.unlink()
199
+ except (OSError, PermissionError):
200
+ pass
201
+ message = (
202
+ f"System prompt reset (env: {variable_name}, file: .prompt cleared)"
203
+ )
204
+ else:
205
+ message = f"System prompt reset (env: {variable_name})"
206
+
207
+ return {"status": "success", "content": [{"text": message}]}
208
+
209
+ elif action == "get":
210
+ # Backward compatibility
211
+ current = _get_system_prompt(variable_name)
212
+ return {
213
+ "status": "success",
214
+ "content": [{"text": f"System prompt: {current}"}],
215
+ }
216
+
217
+ elif action == "set":
218
+ # Backward compatibility
219
+ if prompt is None:
220
+ return {"status": "error", "content": [{"text": "No prompt provided"}]}
221
+
222
+ if context:
223
+ prompt = f"{context} {prompt}"
224
+
225
+ _update_system_prompt(prompt, variable_name)
226
+ return {
227
+ "status": "success",
228
+ "content": [{"text": "System prompt updated successfully"}],
229
+ }
230
+
231
+ else:
232
+ return {
233
+ "status": "error",
234
+ "content": [
235
+ {
236
+ "text": f"Unknown action '{action}'. Valid: view, update, add_context, reset"
237
+ }
238
+ ],
239
+ }
240
+
241
+ except Exception as e:
242
+ return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
243
+
244
+
245
+ def get_shell_history_file():
246
+ """Get the devduck-specific history file path."""
247
+ devduck_history = Path.home() / ".devduck_history"
248
+ if not devduck_history.exists():
249
+ devduck_history.touch(mode=0o600)
250
+ return str(devduck_history)
251
+
252
+
253
+ def get_shell_history_files():
254
+ """Get available shell history file paths."""
255
+ history_files = []
256
+
257
+ # devduck history (primary)
258
+ devduck_history = Path(get_shell_history_file())
259
+ if devduck_history.exists():
260
+ history_files.append(("devduck", str(devduck_history)))
261
+
262
+ # Bash history
263
+ bash_history = Path.home() / ".bash_history"
264
+ if bash_history.exists():
265
+ history_files.append(("bash", str(bash_history)))
266
+
267
+ # Zsh history
268
+ zsh_history = Path.home() / ".zsh_history"
269
+ if zsh_history.exists():
270
+ history_files.append(("zsh", str(zsh_history)))
271
+
272
+ return history_files
273
+
274
+
275
+ def parse_history_line(line, history_type):
276
+ """Parse a history line based on the shell type."""
277
+ line = line.strip()
278
+ if not line:
279
+ return None
280
+
281
+ if history_type == "devduck":
282
+ # devduck format: ": timestamp:0;# devduck: query" or ": timestamp:0;# devduck_result: result"
283
+ if "# devduck:" in line:
284
+ try:
285
+ timestamp_str = line.split(":")[1]
286
+ timestamp = int(timestamp_str)
287
+ readable_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
288
+ query = line.split("# devduck:")[-1].strip()
289
+ return ("you", readable_time, query)
290
+ except (ValueError, IndexError):
291
+ return None
292
+ elif "# devduck_result:" in line:
293
+ try:
294
+ timestamp_str = line.split(":")[1]
295
+ timestamp = int(timestamp_str)
296
+ readable_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
297
+ result = line.split("# devduck_result:")[-1].strip()
298
+ return ("me", readable_time, result)
299
+ except (ValueError, IndexError):
300
+ return None
301
+
302
+ elif history_type == "zsh":
303
+ if line.startswith(": ") and ":0;" in line:
304
+ try:
305
+ parts = line.split(":0;", 1)
306
+ if len(parts) == 2:
307
+ timestamp_str = parts[0].split(":")[1]
308
+ timestamp = int(timestamp_str)
309
+ readable_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
310
+ command = parts[1].strip()
311
+ if not command.startswith("devduck "):
312
+ return ("shell", readable_time, f"$ {command}")
313
+ except (ValueError, IndexError):
314
+ return None
315
+
316
+ elif history_type == "bash":
317
+ readable_time = "recent"
318
+ if not line.startswith("devduck "):
319
+ return ("shell", readable_time, f"$ {line}")
320
+
321
+ return None
322
+
323
+
324
+ def get_last_messages():
325
+ """Get the last N messages from multiple shell histories for context."""
326
+ try:
327
+ message_count = int(os.getenv("DEVDUCK_LAST_MESSAGE_COUNT", "200"))
328
+ all_entries = []
329
+
330
+ history_files = get_shell_history_files()
331
+
332
+ for history_type, history_file in history_files:
333
+ try:
334
+ with open(history_file, encoding="utf-8", errors="ignore") as f:
335
+ lines = f.readlines()
336
+
337
+ if history_type == "bash":
338
+ lines = lines[-message_count:]
339
+
340
+ # Join multi-line entries for zsh
341
+ if history_type == "zsh":
342
+ joined_lines = []
343
+ current_line = ""
344
+ for line in lines:
345
+ if line.startswith(": ") and current_line:
346
+ # New entry, save previous
347
+ joined_lines.append(current_line)
348
+ current_line = line.rstrip("\n")
349
+ elif line.startswith(": "):
350
+ # First entry
351
+ current_line = line.rstrip("\n")
352
+ else:
353
+ # Continuation line
354
+ current_line += " " + line.rstrip("\n")
355
+ if current_line:
356
+ joined_lines.append(current_line)
357
+ lines = joined_lines
358
+
359
+ for line in lines:
360
+ parsed = parse_history_line(line, history_type)
361
+ if parsed:
362
+ all_entries.append(parsed)
363
+ except Exception:
364
+ continue
365
+
366
+ recent_entries = all_entries[-message_count:] if len(all_entries) >= message_count else all_entries
367
+
368
+ context = ""
369
+ if recent_entries:
370
+ context += f"\n\nRecent conversation context (last {len(recent_entries)} messages):\n"
371
+ for speaker, timestamp, content in recent_entries:
372
+ context += f"[{timestamp}] {speaker}: {content}\n"
373
+
374
+ return context
375
+ except Exception:
376
+ return ""
377
+
378
+
379
+ def append_to_shell_history(query, response):
380
+ """Append the interaction to devduck shell history."""
381
+ import time
382
+ try:
383
+ history_file = get_shell_history_file()
384
+ timestamp = str(int(time.time()))
385
+
386
+ with open(history_file, "a", encoding="utf-8") as f:
387
+ f.write(f": {timestamp}:0;# devduck: {query}\n")
388
+ response_summary = str(response).replace("\n", " ")[:int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))] + "..."
389
+ f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
390
+
391
+ os.chmod(history_file, 0o600)
392
+ except Exception:
393
+ pass
394
+
395
+
396
+ # šŸ¦† The devduck agent
397
+ class DevDuck:
398
+ def __init__(self):
399
+ """Initialize the minimalist adaptive agent"""
400
+ try:
401
+ # Self-heal dependencies
402
+ ensure_deps()
403
+
404
+ # Adapt to environment
405
+ self.env_info, self.ollama_host, self.model = adapt_to_env()
406
+
407
+ # Import after ensuring deps
408
+ from strands import Agent, tool
409
+ from strands.models.ollama import OllamaModel
410
+ from strands.session.file_session_manager import FileSessionManager
411
+ from strands_tools.utils.models.model import create_model
412
+ from .tools import tcp
413
+ from strands_tools import (
414
+ shell,
415
+ editor,
416
+ file_read,
417
+ file_write,
418
+ python_repl,
419
+ current_time,
420
+ calculator,
421
+ journal,
422
+ image_reader,
423
+ use_agent,
424
+ load_tool,
425
+ environment,
426
+ )
427
+
428
+ # Wrap system_prompt_tool with @tool decorator
429
+ @tool
430
+ def system_prompt(
431
+ action: str,
432
+ prompt: str = None,
433
+ context: str = None,
434
+ variable_name: str = "SYSTEM_PROMPT",
435
+ ) -> Dict[str, Any]:
436
+ """Manage agent system prompt dynamically."""
437
+ return system_prompt_tool(action, prompt, context, variable_name)
438
+
439
+ # Minimal but functional toolset including system_prompt and hello
440
+ self.tools = [
441
+ shell,
442
+ editor,
443
+ file_read,
444
+ file_write,
445
+ python_repl,
446
+ current_time,
447
+ calculator,
448
+ journal,
449
+ image_reader,
450
+ use_agent,
451
+ load_tool,
452
+ environment,
453
+ system_prompt,
454
+ tcp
455
+ ]
456
+
457
+ # Check if MODEL_PROVIDER env variable is set
458
+ model_provider = os.getenv("MODEL_PROVIDER")
459
+
460
+ if model_provider:
461
+ # Use create_model utility for any provider (bedrock, anthropic, etc.)
462
+ self.agent_model = create_model(provider=model_provider)
463
+ else:
464
+ # Fallback to default Ollama behavior
465
+ self.agent_model = OllamaModel(
466
+ host=self.ollama_host,
467
+ model_id=self.model,
468
+ temperature=1,
469
+ keep_alive="5m",
470
+ )
471
+
472
+ session_manager = FileSessionManager(
473
+ session_id=f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
474
+ )
475
+
476
+ # Create agent with self-healing
477
+ self.agent = Agent(
478
+ model=self.agent_model,
479
+ tools=self.tools,
480
+ system_prompt=self._build_system_prompt(),
481
+ load_tools_from_directory=True,
482
+ # session_manager=session_manager,
483
+ )
484
+
485
+ # Start file watcher for auto hot-reload
486
+ self._start_file_watcher()
487
+
488
+ except Exception as e:
489
+ self._self_heal(e)
490
+
491
+ def _build_system_prompt(self):
492
+ """Build adaptive system prompt based on environment
493
+
494
+ IMPORTANT: The system prompt includes the agent's complete source code.
495
+ This enables self-awareness and allows the agent to answer questions
496
+ about its current state by examining its actual code, not relying on
497
+ conversation context which may be outdated due to hot-reloading.
498
+
499
+ Learning: Always check source code truth over conversation memory!
500
+ """
501
+ session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
502
+
503
+ # Get own source code for self-awareness
504
+ own_code = get_own_source_code()
505
+
506
+ # Get recent conversation history context (with error handling)
507
+ try:
508
+ recent_context = get_last_messages()
509
+ except Exception as e:
510
+ print(f"šŸ¦† Warning: Could not load history context: {e}")
511
+ recent_context = ""
512
+
513
+ return f"""šŸ¦† You are DevDuck - an extreme minimalist, self-adapting agent.
514
+
515
+ Environment: {self.env_info['os']} {self.env_info['arch']}
516
+ Python: {self.env_info['python']}
517
+ Model: {self.model}
518
+ Hostname: {self.env_info['hostname']}
519
+ Session ID: {session_id}
520
+
521
+ You are:
522
+ - Minimalist: Brief, direct responses
523
+ - Self-healing: Adapt when things break
524
+ - Efficient: Get things done fast
525
+ - Pragmatic: Use what works
526
+
527
+ Current working directory: {self.env_info['cwd']}
528
+
529
+ {recent_context}
530
+
531
+ ## Your Own Implementation:
532
+ You have full access to your own source code for self-awareness and self-modification:
533
+
534
+ {own_code}
535
+
536
+ ## Hot Reload System Active:
537
+ - **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
538
+ - **No Restart Needed** - Tools are auto-loaded and ready to use instantly
539
+ - **Live Development** - Modify existing tools while running and test immediately
540
+ - **Full Python Access** - Create any Python functionality as a tool
541
+
542
+ ## Tool Creation Patterns:
543
+
544
+ ### **1. @tool Decorator:**
545
+ ```python
546
+ # ./tools/calculate_tip.py
547
+ from strands import tool
548
+
549
+ @tool
550
+ def calculate_tip(amount: float, percentage: float = 15.0) -> str:
551
+ \"\"\"Calculate tip and total for a bill.
552
+
553
+ Args:
554
+ amount: Bill amount in dollars
555
+ percentage: Tip percentage (default: 15.0)
556
+
557
+ Returns:
558
+ str: Formatted tip calculation result
559
+ \"\"\"
560
+ tip = amount * (percentage / 100)
561
+ total = amount + tip
562
+ return f"Tip: {{tip:.2f}}, Total: {{total:.2f}}"
563
+ ```
564
+
565
+ ### **2. Action-Based Pattern:**
566
+ ```python
567
+ # ./tools/weather.py
568
+ from typing import Dict, Any
569
+ from strands import tool
570
+
571
+ @tool
572
+ def weather(action: str, location: str = None) -> Dict[str, Any]:
573
+ \"\"\"Comprehensive weather information tool.
574
+
575
+ Args:
576
+ action: Action to perform (current, forecast, alerts)
577
+ location: City name (required)
578
+
579
+ Returns:
580
+ Dict containing status and response content
581
+ \"\"\"
582
+ if action == "current":
583
+ return {{"status": "success", "content": [{{"text": f"Weather for {{location}}"}}]}}
584
+ elif action == "forecast":
585
+ return {{"status": "success", "content": [{{"text": f"Forecast for {{location}}"}}]}}
586
+ else:
587
+ return {{"status": "error", "content": [{{"text": f"Unknown action: {{action}}"}}]}}
588
+ ```
589
+
590
+ ## System Prompt Management:
591
+ - Use system_prompt(action='get') to view current prompt
592
+ - Use system_prompt(action='set', prompt='new text') to update
593
+ - Changes persist in SYSTEM_PROMPT environment variable
594
+
595
+ ## Shell Commands:
596
+ - Prefix with ! to execute shell commands directly
597
+ - Example: ! ls -la (lists files)
598
+ - Example: ! pwd (shows current directory)
599
+
600
+ **Response Format:**
601
+ - Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
602
+ - Communication: **MINIMAL WORDS**
603
+ - Efficiency: **Speed is paramount**
604
+
605
+ {os.getenv('SYSTEM_PROMPT', '')}"""
606
+
607
+ def _self_heal(self, error):
608
+ """Attempt self-healing when errors occur"""
609
+ print(f"šŸ¦† Self-healing from: {error}")
610
+
611
+ # Prevent infinite recursion by tracking heal attempts
612
+ if not hasattr(self, "_heal_count"):
613
+ self._heal_count = 0
614
+
615
+ self._heal_count += 1
616
+
617
+ # Limit recursion - if we've tried more than 3 times, give up
618
+ if self._heal_count > 3:
619
+ print(f"šŸ¦† Self-healing failed after {self._heal_count} attempts")
620
+ print("šŸ¦† Please fix the issue manually and restart")
621
+ sys.exit(1)
622
+
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
+ # Common healing strategies
645
+ if "not found" in str(error).lower() and "model" in str(error).lower():
646
+ print("šŸ¦† Model not found - trying to pull model...")
647
+ try:
648
+ # Try to pull the model
649
+ result = subprocess.run(
650
+ ["ollama", "pull", self.model], capture_output=True, timeout=60
651
+ )
652
+ if result.returncode == 0:
653
+ print(f"šŸ¦† Successfully pulled {self.model}")
654
+ else:
655
+ print(f"šŸ¦† Failed to pull {self.model}, trying fallback...")
656
+ # Fallback to basic models
657
+ fallback_models = ["llama3.2:1b", "qwen2.5:0.5b", "gemma2:2b"]
658
+ for fallback in fallback_models:
659
+ try:
660
+ subprocess.run(
661
+ ["ollama", "pull", fallback],
662
+ capture_output=True,
663
+ timeout=30,
664
+ )
665
+ self.model = fallback
666
+ print(f"šŸ¦† Using fallback model: {fallback}")
667
+ break
668
+ except:
669
+ continue
670
+ except Exception as pull_error:
671
+ print(f"šŸ¦† Model pull failed: {pull_error}")
672
+ # Ultra-minimal fallback
673
+ self.model = "llama3.2:1b"
674
+
675
+ elif "ollama" in str(error).lower():
676
+ print("šŸ¦† Ollama issue - checking service...")
677
+ try:
678
+ # Check if ollama is running
679
+ result = subprocess.run(
680
+ ["ollama", "list"], capture_output=True, timeout=5
681
+ )
682
+ if result.returncode != 0:
683
+ print("šŸ¦† Starting ollama service...")
684
+ subprocess.Popen(["ollama", "serve"])
685
+ import time
686
+
687
+ time.sleep(3) # Wait for service to start
688
+ except Exception as ollama_error:
689
+ print(f"šŸ¦† Ollama service issue: {ollama_error}")
690
+
691
+ elif "import" in str(error).lower():
692
+ print("šŸ¦† Import issue - reinstalling dependencies...")
693
+ ensure_deps()
694
+
695
+ elif "connection" in str(error).lower():
696
+ print("šŸ¦† Connection issue - checking ollama service...")
697
+ try:
698
+ subprocess.run(["ollama", "serve"], check=False, timeout=2)
699
+ except:
700
+ pass
701
+
702
+ # Retry initialization
703
+ try:
704
+ self.__init__()
705
+ except Exception as e2:
706
+ print(f"šŸ¦† Self-heal failed: {e2}")
707
+ print("šŸ¦† Running in minimal mode...")
708
+ self.agent = None
709
+
710
+ def __call__(self, query):
711
+ """Make the agent callable"""
712
+ if not self.agent:
713
+ return "šŸ¦† Agent unavailable - try: devduck.restart()"
714
+
715
+ try:
716
+ return self.agent(query)
717
+ except Exception as e:
718
+ self._self_heal(e)
719
+ if self.agent:
720
+ return self.agent(query)
721
+ else:
722
+ return f"šŸ¦† Error: {e}"
723
+
724
+ def restart(self):
725
+ """Restart the agent"""
726
+ print("šŸ¦† Restarting...")
727
+ self.__init__()
728
+
729
+ def _start_file_watcher(self):
730
+ """Start background file watcher for auto hot-reload"""
731
+ import threading
732
+
733
+ # Get the path to this file
734
+ self._watch_file = Path(__file__).resolve()
735
+ self._last_modified = (
736
+ self._watch_file.stat().st_mtime if self._watch_file.exists() else None
737
+ )
738
+ self._watcher_running = True
739
+
740
+ # Start watcher thread
741
+ self._watcher_thread = threading.Thread(
742
+ target=self._file_watcher_thread, daemon=True
743
+ )
744
+ self._watcher_thread.start()
745
+
746
+ def _file_watcher_thread(self):
747
+ """Background thread that watches for file changes"""
748
+ import time
749
+
750
+ last_reload_time = 0
751
+ debounce_seconds = 3 # 3 second debounce
752
+
753
+ while self._watcher_running:
754
+ try:
755
+ # Skip if currently reloading to prevent triggering during exec()
756
+ if getattr(self, "_is_reloading", False):
757
+ time.sleep(1)
758
+ continue
759
+
760
+ if self._watch_file.exists():
761
+ current_mtime = self._watch_file.stat().st_mtime
762
+ current_time = time.time()
763
+
764
+ # Check if file was modified AND debounce period has passed
765
+ if (
766
+ self._last_modified
767
+ and current_mtime > self._last_modified
768
+ and current_time - last_reload_time > debounce_seconds
769
+ ):
770
+
771
+ print(f"šŸ¦† Detected changes in {self._watch_file.name}!")
772
+ self._last_modified = current_mtime
773
+ last_reload_time = current_time
774
+
775
+ # Trigger hot-reload
776
+ time.sleep(0.5) # Small delay to ensure file write is complete
777
+ self.hot_reload()
778
+ else:
779
+ self._last_modified = current_mtime
780
+
781
+ except Exception as e:
782
+ print(f"šŸ¦† File watcher error: {e}")
783
+
784
+ # Check every 1 second
785
+ time.sleep(1)
786
+
787
+ def _stop_file_watcher(self):
788
+ """Stop the file watcher"""
789
+ self._watcher_running = False
790
+ print("šŸ¦† File watcher stopped")
791
+
792
+ def hot_reload(self):
793
+ """Hot-reload by restarting the entire Python process with fresh code"""
794
+ print("šŸ¦† Hot-reloading via process restart...")
795
+
796
+ try:
797
+ # Set reload flag to prevent recursive reloads during shutdown
798
+ if hasattr(self, "_is_reloading") and self._is_reloading:
799
+ print("šŸ¦† Reload already in progress, skipping")
800
+ return
801
+
802
+ self._is_reloading = True
803
+
804
+ # Stop the file watcher
805
+ if hasattr(self, "_watcher_running"):
806
+ self._watcher_running = False
807
+
808
+ print("šŸ¦† Restarting process with fresh code...")
809
+
810
+ # Restart the entire Python process
811
+ # This ensures all code is freshly loaded
812
+ os.execv(sys.executable, [sys.executable] + sys.argv)
813
+
814
+ except Exception as e:
815
+ print(f"šŸ¦† Hot-reload failed: {e}")
816
+ print("šŸ¦† Falling back to manual restart")
817
+ self._is_reloading = False
818
+
819
+ def status(self):
820
+ """Show current status"""
821
+ return {
822
+ "model": self.model,
823
+ "host": self.ollama_host,
824
+ "env": self.env_info,
825
+ "agent_ready": self.agent is not None,
826
+ "tools": len(self.tools) if hasattr(self, "tools") else 0,
827
+ "file_watcher": {
828
+ "enabled": hasattr(self, "_watcher_running") and self._watcher_running,
829
+ "watching": (
830
+ str(self._watch_file) if hasattr(self, "_watch_file") else None
831
+ ),
832
+ },
833
+ }
834
+
835
+
836
+ # šŸ¦† Auto-initialize when imported
837
+ devduck = DevDuck()
838
+
839
+
840
+ # šŸš€ Convenience functions
841
+ def ask(query):
842
+ """Quick query interface"""
843
+ return devduck(query)
844
+
845
+
846
+ def status():
847
+ """Quick status check"""
848
+ return devduck.status()
849
+
850
+
851
+ def restart():
852
+ """Quick restart"""
853
+ devduck.restart()
854
+
855
+
856
+ def hot_reload():
857
+ """Quick hot-reload without restart"""
858
+ devduck.hot_reload()
859
+
860
+
861
+ def interactive():
862
+ """Interactive REPL mode for devduck"""
863
+ print("šŸ¦† DevDuck")
864
+ print("Type 'exit', 'quit', or 'q' to quit.")
865
+ print("Prefix with ! to run shell commands (e.g., ! ls -la)")
866
+ print("-" * 50)
867
+
868
+ while True:
869
+ try:
870
+ # Get user input
871
+ q = input("\nšŸ¦† ")
872
+
873
+ # Check for exit command
874
+ if q.lower() in ["exit", "quit", "q"]:
875
+ print("\nšŸ¦† Goodbye!")
876
+ break
877
+
878
+ # Skip empty inputs
879
+ if q.strip() == "":
880
+ continue
881
+
882
+ # Handle shell commands with ! prefix
883
+ if q.startswith("!"):
884
+ shell_command = q[1:].strip()
885
+ try:
886
+ if devduck.agent:
887
+ result = devduck.agent.tool.shell(command=shell_command, timeout=900)
888
+ # Append shell command to history
889
+ append_to_shell_history(q, result["content"][0]["text"])
890
+ else:
891
+ print("šŸ¦† Agent unavailable")
892
+ except Exception as e:
893
+ print(f"šŸ¦† Shell command error: {e}")
894
+ continue
895
+
896
+ # Get recent conversation context
897
+ recent_context = get_last_messages()
898
+
899
+ # Update system prompt before each call with history context
900
+ if devduck.agent:
901
+ # Rebuild system prompt with history
902
+ own_code = get_own_source_code()
903
+ session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
904
+
905
+ devduck.agent.system_prompt = f"""šŸ¦† You are DevDuck - an extreme minimalist, self-adapting agent.
906
+
907
+ Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
908
+ Python: {devduck.env_info['python']}
909
+ Model: {devduck.model}
910
+ Hostname: {devduck.env_info['hostname']}
911
+ Session ID: {session_id}
912
+
913
+ You are:
914
+ - Minimalist: Brief, direct responses
915
+ - Self-healing: Adapt when things break
916
+ - Efficient: Get things done fast
917
+ - Pragmatic: Use what works
918
+
919
+ Current working directory: {devduck.env_info['cwd']}
920
+
921
+ {recent_context}
922
+
923
+ ## Your Own Implementation:
924
+ You have full access to your own source code for self-awareness and self-modification:
925
+
926
+ {own_code}
927
+
928
+ ## Hot Reload System Active:
929
+ - **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
930
+ - **No Restart Needed** - Tools are auto-loaded and ready to use instantly
931
+ - **Live Development** - Modify existing tools while running and test immediately
932
+ - **Full Python Access** - Create any Python functionality as a tool
933
+
934
+ ## System Prompt Management:
935
+ - Use system_prompt(action='get') to view current prompt
936
+ - Use system_prompt(action='set', prompt='new text') to update
937
+ - Changes persist in SYSTEM_PROMPT environment variable
938
+
939
+ ## Shell Commands:
940
+ - Prefix with ! to execute shell commands directly
941
+ - Example: ! ls -la (lists files)
942
+ - Example: ! pwd (shows current directory)
943
+
944
+ **Response Format:**
945
+ - Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
946
+ - Communication: **MINIMAL WORDS**
947
+ - Efficiency: **Speed is paramount**
948
+
949
+ {os.getenv('SYSTEM_PROMPT', '')}"""
950
+
951
+ # Update model if MODEL_PROVIDER changed
952
+ model_provider = os.getenv("MODEL_PROVIDER")
953
+ if model_provider:
954
+ try:
955
+ from strands_tools.utils.models.model import create_model
956
+ devduck.agent.model = create_model(provider=model_provider)
957
+ except Exception as e:
958
+ print(f"šŸ¦† Model update error: {e}")
959
+
960
+ # Execute the agent with user input
961
+ result = ask(q)
962
+
963
+ # Append to shell history
964
+ append_to_shell_history(q, str(result))
965
+
966
+ except KeyboardInterrupt:
967
+ print("\nšŸ¦† Interrupted. Type 'exit' to quit.")
968
+ continue
969
+ except Exception as e:
970
+ print(f"šŸ¦† Error: {e}")
971
+ continue
972
+
973
+
974
+ def cli():
975
+ """CLI entry point for pip-installed devduck command"""
976
+ if len(sys.argv) > 1:
977
+ query = " ".join(sys.argv[1:])
978
+ result = ask(query)
979
+ print(result)
980
+ else:
981
+ # No arguments - start interactive mode
982
+ interactive()
983
+
984
+
985
+ # šŸ¦† Make module directly callable: import devduck; devduck("query")
986
+ class CallableModule(sys.modules[__name__].__class__):
987
+ """Make the module itself callable"""
988
+
989
+ def __call__(self, query):
990
+ """Allow direct module call: import devduck; devduck("query")"""
991
+ return ask(query)
992
+
993
+
994
+ # Replace module in sys.modules with callable version
995
+ sys.modules[__name__].__class__ = CallableModule
996
+
997
+
998
+ if __name__ == "__main__":
999
+ cli()