devduck 0.1.0__py3-none-any.whl → 0.1.1766644714__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.

Files changed (37) hide show
  1. devduck/__init__.py +1439 -483
  2. devduck/__main__.py +7 -0
  3. devduck/_version.py +34 -0
  4. devduck/agentcore_handler.py +76 -0
  5. devduck/test_redduck.py +0 -1
  6. devduck/tools/__init__.py +47 -0
  7. devduck/tools/_ambient_input.py +423 -0
  8. devduck/tools/_tray_app.py +530 -0
  9. devduck/tools/agentcore_agents.py +197 -0
  10. devduck/tools/agentcore_config.py +441 -0
  11. devduck/tools/agentcore_invoke.py +423 -0
  12. devduck/tools/agentcore_logs.py +320 -0
  13. devduck/tools/ambient.py +157 -0
  14. devduck/tools/create_subagent.py +659 -0
  15. devduck/tools/fetch_github_tool.py +201 -0
  16. devduck/tools/install_tools.py +409 -0
  17. devduck/tools/ipc.py +546 -0
  18. devduck/tools/mcp_server.py +600 -0
  19. devduck/tools/scraper.py +935 -0
  20. devduck/tools/speech_to_speech.py +850 -0
  21. devduck/tools/state_manager.py +292 -0
  22. devduck/tools/store_in_kb.py +187 -0
  23. devduck/tools/system_prompt.py +608 -0
  24. devduck/tools/tcp.py +263 -94
  25. devduck/tools/tray.py +247 -0
  26. devduck/tools/use_github.py +438 -0
  27. devduck/tools/websocket.py +498 -0
  28. devduck-0.1.1766644714.dist-info/METADATA +717 -0
  29. devduck-0.1.1766644714.dist-info/RECORD +33 -0
  30. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/entry_points.txt +1 -0
  31. devduck-0.1.1766644714.dist-info/licenses/LICENSE +201 -0
  32. devduck/install.sh +0 -42
  33. devduck-0.1.0.dist-info/METADATA +0 -106
  34. devduck-0.1.0.dist-info/RECORD +0 -11
  35. devduck-0.1.0.dist-info/licenses/LICENSE +0 -21
  36. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/WHEEL +0 -0
  37. {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/top_level.txt +0 -0
devduck/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """Entry point for python -m devduck or python devduck."""
3
+
4
+ from devduck import cli
5
+
6
+ if __name__ == "__main__":
7
+ cli()
devduck/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.1766644714'
32
+ __version_tuple__ = version_tuple = (0, 1, 1766644714)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """DevDuck AgentCore Handler"""
3
+ import json
4
+ import os
5
+ import threading
6
+ from bedrock_agentcore.runtime import BedrockAgentCoreApp
7
+
8
+ # Configure for AgentCore deployment
9
+ os.environ["DEVDUCK_AUTO_START_SERVERS"] = "false"
10
+ os.environ["MODEL_PROVIDER"] = "bedrock"
11
+
12
+ from devduck import devduck
13
+
14
+ app = BedrockAgentCoreApp()
15
+
16
+
17
+ @app.entrypoint
18
+ async def invoke(payload, context):
19
+ """AgentCore entrypoint - streaming by default with async generator"""
20
+ mode = payload.get("mode", "streaming") # streaming (default), sync, async
21
+
22
+ query = payload.get("prompt", payload.get("text", ""))
23
+ if not query:
24
+ yield {"error": "No query provided"}
25
+ return
26
+
27
+ print(f"Mode: {mode}, Query: {query}")
28
+
29
+ agent = devduck.agent
30
+
31
+ if mode == "sync":
32
+ # Sync mode - return result directly (blocking)
33
+ try:
34
+ result = agent(query)
35
+ yield {"statusCode": 200, "response": str(result)}
36
+ except Exception as e:
37
+ print(f"Error in sync: {str(e)}")
38
+ yield {"statusCode": 500, "error": str(e)}
39
+
40
+ elif mode == "async":
41
+ # Async mode - fire and forget in background thread
42
+ task_id = app.add_async_task("devduck_processing", payload)
43
+ thread = threading.Thread(
44
+ target=lambda: _run_in_thread(agent, query, task_id), daemon=True
45
+ )
46
+ thread.start()
47
+ yield {"statusCode": 200, "task_id": task_id}
48
+
49
+ else:
50
+ # Streaming mode (default) - stream events as they happen
51
+ try:
52
+ stream = agent.stream_async(query)
53
+ async for event in stream:
54
+ print(event)
55
+ yield event
56
+ except Exception as e:
57
+ print(f"Error in streaming: {str(e)}")
58
+ yield {"error": str(e)}
59
+
60
+
61
+ def _run_in_thread(agent, query, task_id):
62
+ """Run agent in background thread for async mode"""
63
+ try:
64
+ result = agent(query)
65
+ print(f"DevDuck result: {result}")
66
+ app.complete_async_task(task_id)
67
+ except Exception as e:
68
+ print(f"Error in async thread: {str(e)}")
69
+ try:
70
+ app.complete_async_task(task_id)
71
+ except:
72
+ pass
73
+
74
+
75
+ if __name__ == "__main__":
76
+ app.run()
devduck/test_redduck.py CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env python3
2
1
  """🦆 DevDuck test suite"""
3
2
 
4
3
 
devduck/tools/__init__.py CHANGED
@@ -0,0 +1,47 @@
1
+ """
2
+ DevDuck Tools Package
3
+
4
+ This module exports all available tools for devduck.
5
+ """
6
+
7
+ from .agentcore_agents import agentcore_agents
8
+ from .agentcore_config import agentcore_config
9
+ from .agentcore_invoke import agentcore_invoke
10
+ from .agentcore_logs import agentcore_logs
11
+ from .ambient import ambient
12
+ from .create_subagent import create_subagent
13
+ from .fetch_github_tool import fetch_github_tool
14
+ from .install_tools import install_tools
15
+ from .ipc import ipc
16
+ from .mcp_server import mcp_server
17
+ from .scraper import scraper
18
+ from .speech_to_speech import speech_to_speech
19
+ from .state_manager import state_manager
20
+ from .store_in_kb import store_in_kb
21
+ from .system_prompt import system_prompt
22
+ from .tcp import tcp
23
+ from .tray import tray
24
+ from .use_github import use_github
25
+ from .websocket import websocket
26
+
27
+ __all__ = [
28
+ "agentcore_agents",
29
+ "agentcore_config",
30
+ "agentcore_invoke",
31
+ "agentcore_logs",
32
+ "ambient",
33
+ "create_subagent",
34
+ "fetch_github_tool",
35
+ "install_tools",
36
+ "ipc",
37
+ "mcp_server",
38
+ "scraper",
39
+ "speech_to_speech",
40
+ "state_manager",
41
+ "store_in_kb",
42
+ "system_prompt",
43
+ "tcp",
44
+ "tray",
45
+ "use_github",
46
+ "websocket",
47
+ ]
@@ -0,0 +1,423 @@
1
+ """
2
+ 🦆 DevDuck Ambient Input - Preserves scroll position
3
+ """
4
+
5
+ import tkinter as tk
6
+ from tkinter import font as tkfont, scrolledtext
7
+ import threading
8
+ import json
9
+ import tempfile
10
+ import queue
11
+ import socket as unix_socket
12
+ import os
13
+ import uuid
14
+ import subprocess
15
+
16
+
17
+ class IPCClient:
18
+ """IPC client for connecting to devduck main server"""
19
+
20
+ def __init__(self, socket_path="/tmp/devduck_main.sock"):
21
+ self.socket_path = socket_path
22
+ self.sock = None
23
+ self.connected = False
24
+ self.message_queue = queue.Queue()
25
+
26
+ def connect(self):
27
+ try:
28
+ self.sock = unix_socket.socket(unix_socket.AF_UNIX, unix_socket.SOCK_STREAM)
29
+ self.sock.connect(self.socket_path)
30
+ self.connected = True
31
+ threading.Thread(target=self._listen, daemon=True).start()
32
+ return True
33
+ except Exception as e:
34
+ print(f"Failed to connect: {e}")
35
+ return False
36
+
37
+ def send_message(self, message, turn_id=None):
38
+ if not self.connected:
39
+ print("Not connected!")
40
+ return False
41
+ try:
42
+ data = {"message": message, "turn_id": turn_id or str(uuid.uuid4())}
43
+ msg = json.dumps(data).encode() + b"\n"
44
+ self.sock.sendall(msg)
45
+ print(f"✓ Sent: {message[:50]} [turn: {data['turn_id'][:8]}]")
46
+ return True
47
+ except Exception as e:
48
+ print(f"Send failed: {e}")
49
+ self.connected = False
50
+ return False
51
+
52
+ def _listen(self):
53
+ buffer = b""
54
+ while self.connected:
55
+ try:
56
+ chunk = self.sock.recv(4096)
57
+ if not chunk:
58
+ self.connected = False
59
+ break
60
+ buffer += chunk
61
+ while b"\n" in buffer:
62
+ line, buffer = buffer.split(b"\n", 1)
63
+ try:
64
+ msg = json.loads(line.decode())
65
+ self.message_queue.put(msg)
66
+ except json.JSONDecodeError:
67
+ continue
68
+ except Exception as e:
69
+ print(f"Listen error: {e}")
70
+ self.connected = False
71
+ break
72
+
73
+ def disconnect(self):
74
+ self.connected = False
75
+ if self.sock:
76
+ try:
77
+ self.sock.close()
78
+ except:
79
+ pass
80
+
81
+
82
+ class MinimalAmbientInput:
83
+ def __init__(self):
84
+ self.root = tk.Tk()
85
+ self.root.title("🦆 devduck")
86
+
87
+ self.root.attributes("-topmost", True)
88
+ self.root.attributes("-alpha", 0.85)
89
+
90
+ screen_width = self.root.winfo_screenwidth()
91
+ screen_height = self.root.winfo_screenheight()
92
+
93
+ # Better size - bigger and more usable
94
+ window_width = 900
95
+ window_height = 650
96
+
97
+ # CENTER on screen
98
+ x = (screen_width - window_width) // 2
99
+ y = (screen_height - window_height) // 2
100
+
101
+ self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
102
+ self.root.configure(bg="#00ff88")
103
+
104
+ # Inner frame
105
+ inner_frame = tk.Frame(self.root, bg="#000000")
106
+ inner_frame.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
107
+
108
+ # Output area - NORMAL state for copying, but read-only via bindings
109
+ self.text = scrolledtext.ScrolledText(
110
+ inner_frame,
111
+ font=tkfont.Font(family="-apple-system", size=14),
112
+ bg="#000000",
113
+ fg="#ffffff",
114
+ insertbackground="#00ff88",
115
+ bd=0,
116
+ highlightthickness=0,
117
+ wrap=tk.WORD,
118
+ padx=10,
119
+ pady=10,
120
+ state="normal", # Keep normal for selection/copying
121
+ height=20,
122
+ )
123
+ self.text.pack(fill=tk.BOTH, expand=True)
124
+
125
+ # Make text read-only by blocking all modification events
126
+ self.text.bind("<Key>", lambda e: "break") # Block keyboard input
127
+ self.text.bind(
128
+ "<Button-2>", lambda e: "break"
129
+ ) # Block middle-click paste (Linux)
130
+ self.text.bind("<Button-3>", lambda e: None) # Allow right-click
131
+
132
+ # Input frame at bottom - bigger and more prominent
133
+ input_frame = tk.Frame(inner_frame, bg="#000000", height=90)
134
+ input_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(10, 0))
135
+ input_frame.pack_propagate(False)
136
+
137
+ # Container for prompt + input
138
+ input_container = tk.Frame(input_frame, bg="#000000")
139
+ input_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
140
+
141
+ # Green prompt label - bigger
142
+ prompt_label = tk.Label(
143
+ input_container,
144
+ text=">",
145
+ font=tkfont.Font(family="-apple-system", size=20, weight="bold"),
146
+ fg="#00ff88",
147
+ bg="#000000",
148
+ )
149
+ prompt_label.pack(side=tk.LEFT, padx=(0, 10))
150
+
151
+ # Input entry - bigger, more visible
152
+ self.input_entry = tk.Entry(
153
+ input_container,
154
+ font=tkfont.Font(family="-apple-system", size=18),
155
+ bg="#1a1a1a",
156
+ fg="#ffffff",
157
+ insertbackground="#00ff88",
158
+ bd=0,
159
+ highlightthickness=2,
160
+ highlightbackground="#00ff88",
161
+ highlightcolor="#00ff88",
162
+ )
163
+ self.input_entry.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, ipady=10)
164
+
165
+ # Tags for output - Green user text, no ugly background
166
+ self.text.tag_config(
167
+ "user",
168
+ foreground="#00ff88",
169
+ background="#000000",
170
+ font=tkfont.Font(family="-apple-system", size=14, weight="bold"),
171
+ spacing1=5,
172
+ spacing3=5,
173
+ lmargin1=0,
174
+ lmargin2=0,
175
+ rmargin=0,
176
+ )
177
+ self.text.tag_config(
178
+ "assistant",
179
+ foreground="#ffffff",
180
+ background="#000000",
181
+ spacing1=0,
182
+ spacing3=0,
183
+ lmargin1=0,
184
+ lmargin2=0,
185
+ rmargin=0,
186
+ )
187
+ self.text.tag_config(
188
+ "tool",
189
+ foreground="#00ff88",
190
+ font=tkfont.Font(family="-apple-system", size=12),
191
+ )
192
+ self.text.tag_config("error", foreground="#ef4444")
193
+
194
+ # Bindings
195
+ self.input_entry.bind("<Return>", self.on_enter)
196
+ self.input_entry.bind("<Escape>", lambda e: self.root.withdraw())
197
+
198
+ # State - Turn tracking
199
+ self.ipc_client = IPCClient()
200
+ self.turns = {}
201
+ self.turn_order = []
202
+
203
+ # Command listener
204
+ self.command_socket_path = os.path.join(
205
+ tempfile.gettempdir(), "devduck_ambient.sock"
206
+ )
207
+ self._start_command_listener()
208
+
209
+ # Start
210
+ self.root.after(50, self.process_messages)
211
+ self.root.after(1000, self._try_connect)
212
+
213
+ # Force layout update before showing
214
+ self.root.update_idletasks()
215
+
216
+ self.root.deiconify()
217
+ self._force_focus()
218
+
219
+ def _try_connect(self):
220
+ if self.ipc_client.connect():
221
+ print("✓ Connected to IPC server")
222
+ self._append_text("✓ Connected to DevDuck\n\n", "tool")
223
+ else:
224
+ print("✗ Connection failed, retrying...")
225
+ self.root.after(5000, self._try_connect)
226
+
227
+ def _append_text(self, text, tag=None):
228
+ """Append text - NO auto-scroll, but stays in normal state"""
229
+ if tag:
230
+ self.text.insert(tk.END, text, tag)
231
+ else:
232
+ self.text.insert(tk.END, text)
233
+ # NO self.text.see(tk.END) - user controls scroll
234
+
235
+ def _render_all_turns(self):
236
+ """Render all turns - PRESERVES scroll position"""
237
+ # Save current scroll position BEFORE deleting
238
+ try:
239
+ yview = self.text.yview()
240
+ scroll_position = yview[0] # Top of visible area
241
+ except:
242
+ scroll_position = 0.0
243
+
244
+ # Delete and re-render
245
+ self.text.delete("1.0", tk.END)
246
+
247
+ for turn_id in self.turn_order:
248
+ if turn_id in self.turns:
249
+ turn = self.turns[turn_id]
250
+
251
+ # User message with "> " prefix
252
+ query = turn.get("query", "")
253
+ if query:
254
+ self.text.insert(tk.END, f"> {query}\n\n", "user")
255
+
256
+ # Assistant response
257
+ buffer = turn.get("buffer", "")
258
+ if buffer:
259
+ self.text.insert(tk.END, buffer, "assistant")
260
+
261
+ # Add spacing between turns
262
+ self.text.insert(tk.END, "\n")
263
+
264
+ # Restore scroll position AFTER re-rendering
265
+ try:
266
+ self.text.yview_moveto(scroll_position)
267
+ except:
268
+ pass
269
+
270
+ def _start_command_listener(self):
271
+ def server_thread():
272
+ if os.path.exists(self.command_socket_path):
273
+ os.unlink(self.command_socket_path)
274
+
275
+ server = unix_socket.socket(unix_socket.AF_UNIX, unix_socket.SOCK_STREAM)
276
+ server.bind(self.command_socket_path)
277
+ server.listen(1)
278
+
279
+ while True:
280
+ try:
281
+ conn, _ = server.accept()
282
+ data = conn.recv(4096)
283
+ if data:
284
+ cmd = json.loads(data.decode("utf-8"))
285
+ action = cmd.get("action")
286
+ if action == "show":
287
+ self.root.after(0, self._force_focus)
288
+ elif action == "hide":
289
+ self.root.after(0, self.root.withdraw)
290
+ elif action == "set_text":
291
+ text = cmd.get("text", "")
292
+ self.root.after(0, lambda: self._set_input_text(text))
293
+ conn.send(json.dumps({"status": "success"}).encode("utf-8"))
294
+ conn.close()
295
+ except Exception as e:
296
+ print(f"Command error: {e}")
297
+
298
+ threading.Thread(target=server_thread, daemon=True).start()
299
+
300
+ def _set_input_text(self, text):
301
+ """Set input text"""
302
+ self.input_entry.delete(0, tk.END)
303
+ self.input_entry.insert(0, text)
304
+
305
+ def _force_focus(self):
306
+ self.root.deiconify()
307
+ self.root.lift()
308
+ self.root.attributes("-topmost", True)
309
+
310
+ try:
311
+ subprocess.run(
312
+ [
313
+ "osascript",
314
+ "-e",
315
+ f'tell application "System Events" to set frontmost of the first process whose unix id is {os.getpid()} to true',
316
+ ],
317
+ check=False,
318
+ capture_output=True,
319
+ timeout=1,
320
+ )
321
+ except:
322
+ pass
323
+
324
+ self.root.focus_force()
325
+ self.input_entry.focus_set()
326
+
327
+ def process_messages(self):
328
+ """Process messages with turn-based buffering"""
329
+ needs_render = False
330
+
331
+ while not self.ipc_client.message_queue.empty():
332
+ msg = self.ipc_client.message_queue.get()
333
+ msg_type = msg.get("type")
334
+ turn_id = msg.get("turn_id")
335
+
336
+ if msg_type == "turn_start":
337
+ if turn_id not in self.turns:
338
+ self.turns[turn_id] = {
339
+ "query": msg.get("data", ""),
340
+ "buffer": "",
341
+ "tools": [],
342
+ }
343
+ self.turn_order.append(turn_id)
344
+ needs_render = True
345
+
346
+ elif msg_type == "chunk":
347
+ if turn_id in self.turns:
348
+ chunk = msg.get("data", "")
349
+ self.turns[turn_id]["buffer"] += chunk
350
+ needs_render = True
351
+
352
+ elif msg_type == "tool_start":
353
+ if turn_id in self.turns:
354
+ tool_name = msg.get("data", "")
355
+ tool_num = msg.get("tool_number", 0)
356
+ tool_text = f"\n🛠️ #{tool_num}: {tool_name} "
357
+ self.turns[turn_id]["buffer"] += tool_text
358
+ needs_render = True
359
+
360
+ elif msg_type == "tool_end":
361
+ if turn_id in self.turns:
362
+ success = msg.get("success", False)
363
+ icon = "✅" if success else "❌"
364
+ self.turns[turn_id]["buffer"] += f"{icon}\n"
365
+ needs_render = True
366
+
367
+ elif msg_type == "turn_end":
368
+ if turn_id in self.turns:
369
+ self.turns[turn_id]["buffer"] += "\n"
370
+ needs_render = True
371
+
372
+ elif msg_type == "error":
373
+ error_msg = msg.get("data", "Unknown error")
374
+ if turn_id and turn_id in self.turns:
375
+ self.turns[turn_id]["buffer"] += f"\n❌ Error: {error_msg}\n"
376
+ needs_render = True
377
+
378
+ if needs_render:
379
+ self._render_all_turns()
380
+
381
+ self.root.after(50, self.process_messages)
382
+
383
+ def on_enter(self, event):
384
+ """Handle Enter key in input field"""
385
+ query = self.input_entry.get().strip()
386
+
387
+ if not query:
388
+ return "break"
389
+
390
+ if not self.ipc_client.connected:
391
+ self._append_text("\n❌ Not connected\n", "error")
392
+ return "break"
393
+
394
+ try:
395
+ turn_id = str(uuid.uuid4())
396
+ self.input_entry.delete(0, tk.END)
397
+
398
+ if not self.ipc_client.send_message(query, turn_id=turn_id):
399
+ self._append_text("❌ Send failed\n", "error")
400
+
401
+ except Exception as e:
402
+ print(f"❌ Error: {e}")
403
+ import traceback
404
+
405
+ traceback.print_exc()
406
+
407
+ return "break"
408
+
409
+ def run(self):
410
+ self.root.mainloop()
411
+
412
+ def cleanup(self):
413
+ self.ipc_client.disconnect()
414
+ if os.path.exists(self.command_socket_path):
415
+ os.unlink(self.command_socket_path)
416
+
417
+
418
+ if __name__ == "__main__":
419
+ app = MinimalAmbientInput()
420
+ try:
421
+ app.run()
422
+ finally:
423
+ app.cleanup()