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
@@ -0,0 +1,498 @@
1
+ """WebSocket tool for DevDuck agents with real-time streaming support.
2
+
3
+ This module provides WebSocket server functionality for DevDuck agents,
4
+ allowing them to communicate over WebSocket protocol with real-time response streaming.
5
+ The tool runs server operations in background threads, enabling concurrent
6
+ communication without blocking the main agent.
7
+
8
+ Key Features:
9
+ 1. WebSocket Server: Listen for incoming connections and process them with a DevDuck agent
10
+ 2. Real-time Streaming: Responses stream to clients as they're generated (non-blocking)
11
+ 3. Concurrent Processing: Handle multiple messages simultaneously
12
+ 4. Background Processing: Server runs in a background thread
13
+ 5. Per-Connection DevDuck: Creates a fresh DevDuck instance for each client connection
14
+ 6. Callback Handler: Uses Strands callback system for efficient streaming
15
+ 7. Browser Compatible: Works with browser WebSocket clients
16
+
17
+ Message Format:
18
+ ```json
19
+ {
20
+ "type": "turn_start" | "chunk" | "tool_start" | "tool_end" | "turn_end",
21
+ "turn_id": "uuid",
22
+ "data": "text content",
23
+ "timestamp": 1234567890.123
24
+ }
25
+ ```
26
+
27
+ Usage with DevDuck Agent:
28
+
29
+ ```python
30
+ from devduck import devduck
31
+
32
+ # Start a streaming WebSocket server
33
+ result = devduck.agent.tool.websocket(
34
+ action="start_server",
35
+ host="127.0.0.1",
36
+ port=8080,
37
+ system_prompt="You are a helpful WebSocket server assistant.",
38
+ )
39
+
40
+ # Stop the WebSocket server
41
+ result = devduck.agent.tool.websocket(action="stop_server", port=8080)
42
+ ```
43
+
44
+ For testing with browser:
45
+ ```javascript
46
+ const ws = new WebSocket('ws://localhost:8080');
47
+ ws.onmessage = (event) => {
48
+ const msg = JSON.parse(event.data);
49
+ console.log(`[${msg.turn_id}] ${msg.type}: ${msg.data}`);
50
+ };
51
+ ws.send('Hello DevDuck!');
52
+ ```
53
+ """
54
+
55
+ import logging
56
+ import threading
57
+ import time
58
+ import os
59
+ import asyncio
60
+ import json
61
+ import uuid
62
+ from typing import Any
63
+ from concurrent.futures import ThreadPoolExecutor
64
+
65
+ from strands import Agent, tool
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+ # Global registry to store server threads
70
+ WS_SERVER_THREADS: dict[int, dict[str, Any]] = {}
71
+
72
+
73
+ class WebSocketStreamingCallbackHandler:
74
+ """Callback handler that streams agent responses directly over WebSocket with turn tracking."""
75
+
76
+ def __init__(self, websocket, loop, turn_id: str):
77
+ """Initialize the streaming handler.
78
+
79
+ Args:
80
+ websocket: The WebSocket connection to stream data to
81
+ loop: The event loop to use for async operations
82
+ turn_id: Unique identifier for this conversation turn
83
+ """
84
+ self.websocket = websocket
85
+ self.loop = loop
86
+ self.turn_id = turn_id
87
+ self.tool_count = 0
88
+ self.previous_tool_use = None
89
+
90
+ async def _send_message(
91
+ self, msg_type: str, data: str = "", metadata: dict = None
92
+ ) -> None:
93
+ """Send a structured message over WebSocket.
94
+
95
+ Args:
96
+ msg_type: Message type (turn_start, chunk, tool_start, tool_end, turn_end)
97
+ data: Text content
98
+ metadata: Additional metadata
99
+ """
100
+ try:
101
+ message = {
102
+ "type": msg_type,
103
+ "turn_id": self.turn_id,
104
+ "data": data,
105
+ "timestamp": time.time(),
106
+ }
107
+ if metadata:
108
+ message.update(metadata)
109
+
110
+ await self.websocket.send(json.dumps(message))
111
+ except Exception as e:
112
+ logger.warning(f"Failed to send message over WebSocket: {e}")
113
+
114
+ def _schedule_message(
115
+ self, msg_type: str, data: str = "", metadata: dict = None
116
+ ) -> None:
117
+ """Schedule an async message send from sync context.
118
+
119
+ Args:
120
+ msg_type: Message type
121
+ data: Text content
122
+ metadata: Additional metadata
123
+ """
124
+ asyncio.run_coroutine_threadsafe(
125
+ self._send_message(msg_type, data, metadata), self.loop
126
+ )
127
+
128
+ def __call__(self, **kwargs: Any) -> None:
129
+ """Stream events to WebSocket in real-time with turn tracking."""
130
+ reasoningText = kwargs.get("reasoningText", False)
131
+ data = kwargs.get("data", "")
132
+ complete = kwargs.get("complete", False)
133
+ current_tool_use = kwargs.get("current_tool_use", {})
134
+ message = kwargs.get("message", {})
135
+
136
+ # Stream reasoning text
137
+ if reasoningText:
138
+ self._schedule_message("chunk", reasoningText, {"reasoning": True})
139
+
140
+ # Stream response text chunks
141
+ if data:
142
+ self._schedule_message("chunk", data)
143
+
144
+ # Stream tool invocation notifications
145
+ if current_tool_use and current_tool_use.get("name"):
146
+ tool_name = current_tool_use.get("name", "Unknown tool")
147
+ if self.previous_tool_use != current_tool_use:
148
+ self.previous_tool_use = current_tool_use
149
+ self.tool_count += 1
150
+ self._schedule_message(
151
+ "tool_start", tool_name, {"tool_number": self.tool_count}
152
+ )
153
+
154
+ # Stream tool results
155
+ if isinstance(message, dict) and message.get("role") == "user":
156
+ for content in message.get("content", []):
157
+ if isinstance(content, dict):
158
+ tool_result = content.get("toolResult")
159
+ if tool_result:
160
+ status = tool_result.get("status", "unknown")
161
+ self._schedule_message(
162
+ "tool_end", status, {"success": status == "success"}
163
+ )
164
+
165
+
166
+ async def process_message_async(connection_agent, message, websocket, loop, turn_id):
167
+ """Process a message in a concurrent task.
168
+
169
+ Args:
170
+ connection_agent: The agent instance to process the message
171
+ message: The message to process
172
+ websocket: WebSocket connection
173
+ loop: Event loop
174
+ turn_id: Unique turn ID
175
+ """
176
+ try:
177
+ # Send turn start notification
178
+ turn_start = {
179
+ "type": "turn_start",
180
+ "turn_id": turn_id,
181
+ "data": message,
182
+ "timestamp": time.time(),
183
+ }
184
+ await websocket.send(json.dumps(turn_start))
185
+
186
+ # Create callback handler for this turn
187
+ streaming_handler = WebSocketStreamingCallbackHandler(websocket, loop, turn_id)
188
+ connection_agent.callback_handler = streaming_handler
189
+
190
+ # Process message in a thread to avoid blocking the event loop
191
+ with ThreadPoolExecutor() as executor:
192
+ await loop.run_in_executor(executor, connection_agent, message)
193
+
194
+ # Send turn end notification
195
+ turn_end = {"type": "turn_end", "turn_id": turn_id, "timestamp": time.time()}
196
+ await websocket.send(json.dumps(turn_end))
197
+
198
+ except Exception as e:
199
+ logger.error(f"Error processing message in turn {turn_id}: {e}", exc_info=True)
200
+ error_msg = {
201
+ "type": "error",
202
+ "turn_id": turn_id,
203
+ "data": f"Error processing message: {e}",
204
+ "timestamp": time.time(),
205
+ }
206
+ await websocket.send(json.dumps(error_msg))
207
+
208
+
209
+ async def handle_websocket_client(websocket, system_prompt: str):
210
+ """Handle a WebSocket client connection with streaming responses.
211
+
212
+ Args:
213
+ websocket: WebSocket connection object
214
+ system_prompt: System prompt for the DevDuck agent
215
+ """
216
+ client_address = websocket.remote_address
217
+ logger.info(f"WebSocket connection established with {client_address}")
218
+
219
+ # Get the current event loop
220
+ loop = asyncio.get_running_loop()
221
+
222
+ # Import DevDuck and create a new instance for this connection
223
+ try:
224
+ from devduck import DevDuck
225
+
226
+ # Create a new DevDuck instance with auto_start_servers=False to avoid recursion
227
+ connection_devduck = DevDuck(auto_start_servers=False)
228
+
229
+ # Override system prompt if provided
230
+ if connection_devduck.agent and system_prompt:
231
+ connection_devduck.agent.system_prompt += (
232
+ "\nCustom system prompt:" + system_prompt
233
+ )
234
+
235
+ connection_agent = connection_devduck.agent
236
+
237
+ except Exception as e:
238
+ logger.error(f"Failed to create DevDuck instance: {e}", exc_info=True)
239
+ # Fallback to basic Agent if DevDuck fails
240
+ from strands import Agent
241
+ from strands.models.ollama import OllamaModel
242
+
243
+ agent_model = OllamaModel(
244
+ host=os.getenv("OLLAMA_HOST", "http://localhost:11434"),
245
+ model_id=os.getenv("OLLAMA_MODEL", "qwen3:1.7b"),
246
+ temperature=1,
247
+ keep_alive="5m",
248
+ )
249
+
250
+ connection_agent = Agent(
251
+ model=agent_model,
252
+ tools=[],
253
+ system_prompt=system_prompt
254
+ or "You are a helpful WebSocket server assistant.",
255
+ )
256
+
257
+ # Track active tasks for concurrent processing
258
+ active_tasks = set()
259
+
260
+ try:
261
+ # Send welcome message
262
+ welcome = {
263
+ "type": "connected",
264
+ "data": "🦆 Welcome to DevDuck!",
265
+ "timestamp": time.time(),
266
+ }
267
+ await websocket.send(json.dumps(welcome))
268
+
269
+ async for message in websocket:
270
+ message = message.strip()
271
+ logger.info(f"Received from {client_address}: {message}")
272
+
273
+ if message.lower() == "exit":
274
+ bye = {
275
+ "type": "disconnected",
276
+ "data": "Connection closed by client request.",
277
+ "timestamp": time.time(),
278
+ }
279
+ await websocket.send(json.dumps(bye))
280
+ logger.info(f"Client {client_address} requested to exit")
281
+ break
282
+
283
+ # Generate unique turn ID for this conversation turn
284
+ turn_id = str(uuid.uuid4())
285
+
286
+ # Launch message processing as concurrent task (don't await)
287
+ task = asyncio.create_task(
288
+ process_message_async(
289
+ connection_agent, message, websocket, loop, turn_id
290
+ )
291
+ )
292
+ active_tasks.add(task)
293
+
294
+ # Clean up completed tasks
295
+ task.add_done_callback(active_tasks.discard)
296
+
297
+ # Wait for all active tasks to complete before closing
298
+ if active_tasks:
299
+ logger.info(f"Waiting for {len(active_tasks)} active tasks to complete...")
300
+ await asyncio.gather(*active_tasks, return_exceptions=True)
301
+
302
+ except Exception as e:
303
+ logger.error(
304
+ f"Error handling WebSocket client {client_address}: {e}", exc_info=True
305
+ )
306
+ finally:
307
+ logger.info(f"WebSocket connection with {client_address} closed")
308
+
309
+
310
+ def run_websocket_server(
311
+ host: str,
312
+ port: int,
313
+ system_prompt: str,
314
+ ) -> None:
315
+ """Run a WebSocket server that processes client requests with DevDuck instances."""
316
+ import websockets
317
+
318
+ WS_SERVER_THREADS[port]["running"] = True
319
+ WS_SERVER_THREADS[port]["connections"] = 0
320
+ WS_SERVER_THREADS[port]["start_time"] = time.time()
321
+
322
+ async def server_handler(websocket):
323
+ """Handle incoming WebSocket connections.
324
+
325
+ Args:
326
+ websocket: WebSocket connection object
327
+ """
328
+ WS_SERVER_THREADS[port]["connections"] += 1
329
+ await handle_websocket_client(websocket, system_prompt)
330
+
331
+ async def start_server():
332
+ stop_future = asyncio.Future()
333
+ WS_SERVER_THREADS[port]["stop_future"] = stop_future
334
+
335
+ server = await websockets.serve(server_handler, host, port)
336
+ logger.info(f"WebSocket Server listening on {host}:{port}")
337
+
338
+ # Wait for stop signal
339
+ await stop_future
340
+
341
+ # Close the server
342
+ server.close()
343
+ await server.wait_closed()
344
+
345
+ try:
346
+ loop = asyncio.new_event_loop()
347
+ asyncio.set_event_loop(loop)
348
+ WS_SERVER_THREADS[port]["loop"] = loop
349
+ loop.run_until_complete(start_server())
350
+ except OSError as e:
351
+ # Port conflict - handled upstream, no need for scary errors
352
+ if "Address already in use" in str(e) or "address already in use" in str(e):
353
+ logger.debug(f"Port {port} unavailable (handled upstream)")
354
+ else:
355
+ logger.error(f"WebSocket server error on {host}:{port}: {e}")
356
+ except Exception as e:
357
+ logger.error(f"WebSocket server error on {host}:{port}: {e}")
358
+ finally:
359
+ logger.info(f"WebSocket Server on {host}:{port} stopped")
360
+ WS_SERVER_THREADS[port]["running"] = False
361
+
362
+
363
+ @tool
364
+ def websocket(
365
+ action: str,
366
+ host: str = "127.0.0.1",
367
+ port: int = 8080,
368
+ system_prompt: str = "You are a helpful WebSocket server assistant.",
369
+ ) -> dict:
370
+ """Create and manage WebSocket servers with real-time streaming.
371
+
372
+ Args:
373
+ action: Action to perform (start_server, stop_server, get_status)
374
+ host: Host address for server
375
+ port: Port number for server
376
+ system_prompt: System prompt for the server DevDuck instances
377
+
378
+ Returns:
379
+ Dictionary containing status and response content
380
+ """
381
+ if action == "start_server":
382
+ if port in WS_SERVER_THREADS and WS_SERVER_THREADS[port].get("running", False):
383
+ return {
384
+ "status": "error",
385
+ "content": [
386
+ {
387
+ "text": f"❌ Error: WebSocket Server already running on port {port}"
388
+ }
389
+ ],
390
+ }
391
+
392
+ WS_SERVER_THREADS[port] = {"running": False}
393
+ server_thread = threading.Thread(
394
+ target=run_websocket_server,
395
+ args=(host, port, system_prompt),
396
+ )
397
+ server_thread.daemon = True
398
+ server_thread.start()
399
+
400
+ time.sleep(0.5)
401
+
402
+ if not WS_SERVER_THREADS[port].get("running", False):
403
+ return {
404
+ "status": "error",
405
+ "content": [
406
+ {
407
+ "text": f"❌ Error: Failed to start WebSocket Server on {host}:{port}"
408
+ }
409
+ ],
410
+ }
411
+
412
+ return {
413
+ "status": "success",
414
+ "content": [
415
+ {"text": f"✅ WebSocket Server started successfully on {host}:{port}"},
416
+ {"text": f"System prompt: {system_prompt}"},
417
+ {"text": "🌊 Real-time streaming with concurrent message processing"},
418
+ {"text": "📦 Structured JSON messages with turn_id"},
419
+ {
420
+ "text": "🦆 Server creates a new DevDuck instance for each connection"
421
+ },
422
+ {"text": "⚡ Send multiple messages without waiting!"},
423
+ {"text": f"📝 Test with: ws://localhost:{port}"},
424
+ ],
425
+ }
426
+
427
+ elif action == "stop_server":
428
+ if port not in WS_SERVER_THREADS or not WS_SERVER_THREADS[port].get(
429
+ "running", False
430
+ ):
431
+ return {
432
+ "status": "error",
433
+ "content": [
434
+ {"text": f"❌ Error: No WebSocket Server running on port {port}"}
435
+ ],
436
+ }
437
+
438
+ WS_SERVER_THREADS[port]["running"] = False
439
+
440
+ # Signal the server to stop
441
+ if "stop_future" in WS_SERVER_THREADS[port]:
442
+ loop = WS_SERVER_THREADS[port]["loop"]
443
+ loop.call_soon_threadsafe(
444
+ lambda: WS_SERVER_THREADS[port]["stop_future"].set_result(None)
445
+ )
446
+
447
+ time.sleep(1.0)
448
+
449
+ connections = WS_SERVER_THREADS[port].get("connections", 0)
450
+ uptime = time.time() - WS_SERVER_THREADS[port].get("start_time", time.time())
451
+
452
+ del WS_SERVER_THREADS[port]
453
+
454
+ return {
455
+ "status": "success",
456
+ "content": [
457
+ {"text": f"✅ WebSocket Server on port {port} stopped successfully"},
458
+ {
459
+ "text": f"Statistics: {connections} connections handled, uptime {uptime:.2f} seconds"
460
+ },
461
+ ],
462
+ }
463
+
464
+ elif action == "get_status":
465
+ if not WS_SERVER_THREADS:
466
+ return {
467
+ "status": "success",
468
+ "content": [{"text": "No WebSocket Servers running"}],
469
+ }
470
+
471
+ status_info = []
472
+ for port, data in WS_SERVER_THREADS.items():
473
+ if data.get("running", False):
474
+ uptime = time.time() - data.get("start_time", time.time())
475
+ connections = data.get("connections", 0)
476
+ status_info.append(
477
+ f"Port {port}: Running - {connections} connections, uptime {uptime:.2f}s"
478
+ )
479
+ else:
480
+ status_info.append(f"Port {port}: Stopped")
481
+
482
+ return {
483
+ "status": "success",
484
+ "content": [
485
+ {"text": "WebSocket Server Status:"},
486
+ {"text": "\n".join(status_info)},
487
+ ],
488
+ }
489
+
490
+ else:
491
+ return {
492
+ "status": "error",
493
+ "content": [
494
+ {
495
+ "text": f"Error: Unknown action '{action}'. Supported: start_server, stop_server, get_status"
496
+ }
497
+ ],
498
+ }