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 +999 -0
- devduck/install.sh +42 -0
- devduck/test_redduck.py +79 -0
- devduck/tools/__init__.py +0 -0
- devduck/tools/tcp.py +457 -0
- devduck-0.1.0.dist-info/METADATA +106 -0
- devduck-0.1.0.dist-info/RECORD +11 -0
- devduck-0.1.0.dist-info/WHEEL +5 -0
- devduck-0.1.0.dist-info/entry_points.txt +2 -0
- devduck-0.1.0.dist-info/licenses/LICENSE +21 -0
- devduck-0.1.0.dist-info/top_level.txt +1 -0
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()
|