devduck 0.4.1__py3-none-any.whl → 0.5.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 +35 -1
- devduck/_version.py +2 -2
- devduck/tools/__init__.py +3 -1
- devduck/tools/_ambient_input.py +423 -0
- devduck/tools/_tray_app.py +522 -0
- devduck/tools/ambient.py +157 -0
- devduck/tools/ipc.py +543 -0
- devduck/tools/tcp.py +0 -4
- devduck/tools/tray.py +246 -0
- devduck-0.5.0.dist-info/METADATA +554 -0
- devduck-0.5.0.dist-info/RECORD +24 -0
- {devduck-0.4.1.dist-info → devduck-0.5.0.dist-info}/entry_points.txt +1 -0
- devduck-0.5.0.dist-info/licenses/LICENSE +201 -0
- devduck-0.4.1.dist-info/METADATA +0 -283
- devduck-0.4.1.dist-info/RECORD +0 -19
- devduck-0.4.1.dist-info/licenses/LICENSE +0 -21
- {devduck-0.4.1.dist-info → devduck-0.5.0.dist-info}/WHEEL +0 -0
- {devduck-0.4.1.dist-info → devduck-0.5.0.dist-info}/top_level.txt +0 -0
devduck/tools/ipc.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""IPC tool for DevDuck agents with real-time streaming support.
|
|
2
|
+
|
|
3
|
+
This module provides Unix socket IPC server functionality for DevDuck agents,
|
|
4
|
+
allowing local processes (tray, ambient, etc.) to communicate with real-time streaming.
|
|
5
|
+
Similar to websocket.py but uses Unix sockets for inter-process communication.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
1. IPC Server: Listen on Unix socket for local process connections
|
|
9
|
+
2. Real-time Streaming: Responses stream to clients as they're generated
|
|
10
|
+
3. Concurrent Processing: Handle multiple connections simultaneously
|
|
11
|
+
4. Background Processing: Server runs in a background thread
|
|
12
|
+
5. Per-Connection DevDuck: Creates a fresh DevDuck instance for each connection
|
|
13
|
+
6. Callback Handler: Uses Strands callback system for efficient streaming
|
|
14
|
+
7. Bidirectional: Clients can send commands AND receive streaming responses
|
|
15
|
+
|
|
16
|
+
Message Format:
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"type": "turn_start" | "chunk" | "tool_start" | "tool_end" | "turn_end" | "command",
|
|
20
|
+
"turn_id": "uuid",
|
|
21
|
+
"data": "text content",
|
|
22
|
+
"timestamp": 1234567890.123,
|
|
23
|
+
"command": "optional_command_name",
|
|
24
|
+
"params": {"optional": "parameters"}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Usage with DevDuck Agent:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from devduck import devduck
|
|
32
|
+
|
|
33
|
+
# Start IPC server
|
|
34
|
+
result = devduck.agent.tool.ipc(
|
|
35
|
+
action="start_server",
|
|
36
|
+
socket_path="/tmp/devduck_main.sock",
|
|
37
|
+
system_prompt="You are a helpful IPC server assistant.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Stop IPC server
|
|
41
|
+
result = devduck.agent.tool.ipc(
|
|
42
|
+
action="stop_server",
|
|
43
|
+
socket_path="/tmp/devduck_main.sock"
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Client Example (Python):
|
|
48
|
+
```python
|
|
49
|
+
import socket
|
|
50
|
+
import json
|
|
51
|
+
|
|
52
|
+
# Connect to IPC server
|
|
53
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
54
|
+
sock.connect("/tmp/devduck_main.sock")
|
|
55
|
+
|
|
56
|
+
# Send message
|
|
57
|
+
message = json.dumps({"message": "Hello DevDuck!", "turn_id": "123"})
|
|
58
|
+
sock.sendall(message.encode() + b'\n')
|
|
59
|
+
|
|
60
|
+
# Receive streaming response
|
|
61
|
+
buffer = b''
|
|
62
|
+
while True:
|
|
63
|
+
chunk = sock.recv(4096)
|
|
64
|
+
if not chunk:
|
|
65
|
+
break
|
|
66
|
+
buffer += chunk
|
|
67
|
+
# Process complete JSON messages (newline delimited)
|
|
68
|
+
while b'\n' in buffer:
|
|
69
|
+
line, buffer = buffer.split(b'\n', 1)
|
|
70
|
+
msg = json.loads(line.decode())
|
|
71
|
+
print(f"[{msg['type']}] {msg.get('data', '')}")
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
import logging
|
|
76
|
+
import threading
|
|
77
|
+
import time
|
|
78
|
+
import os
|
|
79
|
+
import asyncio
|
|
80
|
+
import json
|
|
81
|
+
import uuid
|
|
82
|
+
import tempfile
|
|
83
|
+
from typing import Any
|
|
84
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
85
|
+
from pathlib import Path
|
|
86
|
+
|
|
87
|
+
from strands import Agent, tool
|
|
88
|
+
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
|
|
91
|
+
# Global registry to store server threads
|
|
92
|
+
IPC_SERVER_THREADS: dict[str, dict[str, Any]] = {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class IPCStreamingCallbackHandler:
|
|
96
|
+
"""Callback handler that streams agent responses directly over Unix socket with turn tracking."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, client_socket, turn_id: str):
|
|
99
|
+
"""Initialize the streaming handler.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
client_socket: The Unix socket connection to stream data to
|
|
103
|
+
turn_id: Unique identifier for this conversation turn
|
|
104
|
+
"""
|
|
105
|
+
self.socket = client_socket
|
|
106
|
+
self.turn_id = turn_id
|
|
107
|
+
self.tool_count = 0
|
|
108
|
+
self.previous_tool_use = None
|
|
109
|
+
|
|
110
|
+
def _send_message(
|
|
111
|
+
self, msg_type: str, data: str = "", metadata: dict = None
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Send a structured message over Unix socket.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
msg_type: Message type (turn_start, chunk, tool_start, tool_end, turn_end)
|
|
117
|
+
data: Text content
|
|
118
|
+
metadata: Additional metadata
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
message = {
|
|
122
|
+
"type": msg_type,
|
|
123
|
+
"turn_id": self.turn_id,
|
|
124
|
+
"data": data,
|
|
125
|
+
"timestamp": time.time(),
|
|
126
|
+
}
|
|
127
|
+
if metadata:
|
|
128
|
+
message.update(metadata)
|
|
129
|
+
|
|
130
|
+
# Send as newline-delimited JSON for easy parsing
|
|
131
|
+
self.socket.sendall(json.dumps(message).encode() + b"\n")
|
|
132
|
+
except (BrokenPipeError, ConnectionResetError, OSError) as e:
|
|
133
|
+
logger.warning(f"Failed to send message over IPC: {e}")
|
|
134
|
+
|
|
135
|
+
def __call__(self, **kwargs: Any) -> None:
|
|
136
|
+
"""Stream events to Unix socket in real-time with turn tracking."""
|
|
137
|
+
reasoningText = kwargs.get("reasoningText", False)
|
|
138
|
+
data = kwargs.get("data", "")
|
|
139
|
+
complete = kwargs.get("complete", False)
|
|
140
|
+
current_tool_use = kwargs.get("current_tool_use", {})
|
|
141
|
+
message = kwargs.get("message", {})
|
|
142
|
+
|
|
143
|
+
# Stream reasoning text
|
|
144
|
+
if reasoningText:
|
|
145
|
+
self._send_message("chunk", reasoningText, {"reasoning": True})
|
|
146
|
+
|
|
147
|
+
# Stream response text chunks
|
|
148
|
+
if data:
|
|
149
|
+
self._send_message("chunk", data)
|
|
150
|
+
|
|
151
|
+
# Stream tool invocation notifications
|
|
152
|
+
if current_tool_use and current_tool_use.get("name"):
|
|
153
|
+
tool_name = current_tool_use.get("name", "Unknown tool")
|
|
154
|
+
if self.previous_tool_use != current_tool_use:
|
|
155
|
+
self.previous_tool_use = current_tool_use
|
|
156
|
+
self.tool_count += 1
|
|
157
|
+
self._send_message(
|
|
158
|
+
"tool_start", tool_name, {"tool_number": self.tool_count}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Stream tool results
|
|
162
|
+
if isinstance(message, dict) and message.get("role") == "user":
|
|
163
|
+
for content in message.get("content", []):
|
|
164
|
+
if isinstance(content, dict):
|
|
165
|
+
tool_result = content.get("toolResult")
|
|
166
|
+
if tool_result:
|
|
167
|
+
status = tool_result.get("status", "unknown")
|
|
168
|
+
self._send_message(
|
|
169
|
+
"tool_end", status, {"success": status == "success"}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def process_ipc_message(connection_agent, message_data, client_socket, turn_id):
|
|
174
|
+
"""Process an IPC message and stream response.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
connection_agent: The agent instance to process the message
|
|
178
|
+
message_data: Parsed message data
|
|
179
|
+
client_socket: Unix socket connection
|
|
180
|
+
turn_id: Unique turn ID
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
message = message_data.get("message", "")
|
|
184
|
+
|
|
185
|
+
# Send turn start notification
|
|
186
|
+
turn_start = {
|
|
187
|
+
"type": "turn_start",
|
|
188
|
+
"turn_id": turn_id,
|
|
189
|
+
"data": message,
|
|
190
|
+
"timestamp": time.time(),
|
|
191
|
+
}
|
|
192
|
+
client_socket.sendall(json.dumps(turn_start).encode() + b"\n")
|
|
193
|
+
|
|
194
|
+
# Create callback handler for this turn
|
|
195
|
+
streaming_handler = IPCStreamingCallbackHandler(client_socket, turn_id)
|
|
196
|
+
connection_agent.callback_handler = streaming_handler
|
|
197
|
+
|
|
198
|
+
# Process message (synchronous, runs in thread pool)
|
|
199
|
+
connection_agent(message)
|
|
200
|
+
|
|
201
|
+
# Send turn end notification
|
|
202
|
+
turn_end = {"type": "turn_end", "turn_id": turn_id, "timestamp": time.time()}
|
|
203
|
+
client_socket.sendall(json.dumps(turn_end).encode() + b"\n")
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"Error processing message in turn {turn_id}: {e}", exc_info=True)
|
|
207
|
+
error_msg = {
|
|
208
|
+
"type": "error",
|
|
209
|
+
"turn_id": turn_id,
|
|
210
|
+
"data": f"Error processing message: {e}",
|
|
211
|
+
"timestamp": time.time(),
|
|
212
|
+
}
|
|
213
|
+
try:
|
|
214
|
+
client_socket.sendall(json.dumps(error_msg).encode() + b"\n")
|
|
215
|
+
except:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def handle_ipc_client(client_socket, client_id, system_prompt: str, socket_path: str):
|
|
220
|
+
"""Handle an IPC client connection with streaming responses.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
client_socket: Unix socket connection object
|
|
224
|
+
client_id: Unique client identifier
|
|
225
|
+
system_prompt: System prompt for the DevDuck agent
|
|
226
|
+
socket_path: Socket path (for logging)
|
|
227
|
+
"""
|
|
228
|
+
logger.info(f"IPC connection established with client {client_id}")
|
|
229
|
+
|
|
230
|
+
# Import DevDuck and create a new instance for this connection
|
|
231
|
+
try:
|
|
232
|
+
from devduck import DevDuck
|
|
233
|
+
|
|
234
|
+
# Create a new DevDuck instance with auto_start_servers=False to avoid recursion
|
|
235
|
+
connection_devduck = DevDuck(auto_start_servers=False)
|
|
236
|
+
|
|
237
|
+
# Override system prompt if provided
|
|
238
|
+
if connection_devduck.agent and system_prompt:
|
|
239
|
+
connection_devduck.agent.system_prompt += (
|
|
240
|
+
"\nCustom system prompt: " + system_prompt
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
connection_agent = connection_devduck.agent
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Failed to create DevDuck instance: {e}", exc_info=True)
|
|
247
|
+
# Fallback to basic Agent if DevDuck fails
|
|
248
|
+
from strands import Agent
|
|
249
|
+
from strands.models.ollama import OllamaModel
|
|
250
|
+
|
|
251
|
+
agent_model = OllamaModel(
|
|
252
|
+
host=os.getenv("OLLAMA_HOST", "http://localhost:11434"),
|
|
253
|
+
model_id=os.getenv("OLLAMA_MODEL", "qwen3:1.7b"),
|
|
254
|
+
temperature=1,
|
|
255
|
+
keep_alive="5m",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
connection_agent = Agent(
|
|
259
|
+
model=agent_model,
|
|
260
|
+
tools=[],
|
|
261
|
+
system_prompt=system_prompt or "You are a helpful IPC server assistant.",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
# Send welcome message
|
|
266
|
+
welcome = {
|
|
267
|
+
"type": "connected",
|
|
268
|
+
"data": "🦆 Welcome to DevDuck IPC!",
|
|
269
|
+
"timestamp": time.time(),
|
|
270
|
+
}
|
|
271
|
+
client_socket.sendall(json.dumps(welcome).encode() + b"\n")
|
|
272
|
+
|
|
273
|
+
# Track active tasks
|
|
274
|
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
275
|
+
buffer = b""
|
|
276
|
+
|
|
277
|
+
while True:
|
|
278
|
+
# Receive data
|
|
279
|
+
chunk = client_socket.recv(4096)
|
|
280
|
+
if not chunk:
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
buffer += chunk
|
|
284
|
+
|
|
285
|
+
# Process complete messages (newline delimited)
|
|
286
|
+
while b"\n" in buffer:
|
|
287
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
message_data = json.loads(line.decode())
|
|
291
|
+
|
|
292
|
+
# Check for exit command
|
|
293
|
+
if message_data.get("message", "").lower() == "exit":
|
|
294
|
+
bye = {
|
|
295
|
+
"type": "disconnected",
|
|
296
|
+
"data": "Connection closed by client request.",
|
|
297
|
+
"timestamp": time.time(),
|
|
298
|
+
}
|
|
299
|
+
client_socket.sendall(json.dumps(bye).encode() + b"\n")
|
|
300
|
+
logger.info(f"Client {client_id} requested to exit")
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
# Generate unique turn ID
|
|
304
|
+
turn_id = message_data.get("turn_id") or str(uuid.uuid4())
|
|
305
|
+
|
|
306
|
+
logger.info(
|
|
307
|
+
f"Received from client {client_id}: {message_data.get('message', '')[:100]}"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Process message in thread pool (concurrent)
|
|
311
|
+
executor.submit(
|
|
312
|
+
process_ipc_message,
|
|
313
|
+
connection_agent,
|
|
314
|
+
message_data,
|
|
315
|
+
client_socket,
|
|
316
|
+
turn_id,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
except json.JSONDecodeError:
|
|
320
|
+
logger.warning(
|
|
321
|
+
f"Invalid JSON from client {client_id}: {line[:100]}"
|
|
322
|
+
)
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"Error handling IPC client {client_id}: {e}", exc_info=True)
|
|
327
|
+
finally:
|
|
328
|
+
try:
|
|
329
|
+
client_socket.close()
|
|
330
|
+
except:
|
|
331
|
+
pass
|
|
332
|
+
logger.info(f"IPC connection with client {client_id} closed")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def run_ipc_server(socket_path: str, system_prompt: str) -> None:
|
|
336
|
+
"""Run an IPC server that processes client requests with DevDuck instances.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
socket_path: Unix socket path to bind
|
|
340
|
+
system_prompt: System prompt for DevDuck agents
|
|
341
|
+
"""
|
|
342
|
+
IPC_SERVER_THREADS[socket_path]["running"] = True
|
|
343
|
+
IPC_SERVER_THREADS[socket_path]["connections"] = 0
|
|
344
|
+
IPC_SERVER_THREADS[socket_path]["start_time"] = time.time()
|
|
345
|
+
|
|
346
|
+
# Remove existing socket if it exists
|
|
347
|
+
if os.path.exists(socket_path):
|
|
348
|
+
os.unlink(socket_path)
|
|
349
|
+
|
|
350
|
+
import socket as unix_socket
|
|
351
|
+
|
|
352
|
+
server_socket = unix_socket.socket(unix_socket.AF_UNIX, unix_socket.SOCK_STREAM)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
server_socket.bind(socket_path)
|
|
356
|
+
server_socket.listen(10)
|
|
357
|
+
logger.info(f"IPC Server listening on {socket_path}")
|
|
358
|
+
|
|
359
|
+
IPC_SERVER_THREADS[socket_path]["socket"] = server_socket
|
|
360
|
+
|
|
361
|
+
client_counter = 0
|
|
362
|
+
|
|
363
|
+
while IPC_SERVER_THREADS[socket_path]["running"]:
|
|
364
|
+
# Set timeout to check periodically if server should stop
|
|
365
|
+
server_socket.settimeout(1.0)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
client_socket, _ = server_socket.accept()
|
|
369
|
+
IPC_SERVER_THREADS[socket_path]["connections"] += 1
|
|
370
|
+
client_counter += 1
|
|
371
|
+
|
|
372
|
+
client_id = f"client_{client_counter}"
|
|
373
|
+
|
|
374
|
+
# Handle client in a new thread
|
|
375
|
+
client_thread = threading.Thread(
|
|
376
|
+
target=handle_ipc_client,
|
|
377
|
+
args=(client_socket, client_id, system_prompt, socket_path),
|
|
378
|
+
daemon=True,
|
|
379
|
+
)
|
|
380
|
+
client_thread.start()
|
|
381
|
+
|
|
382
|
+
except TimeoutError:
|
|
383
|
+
# Expected timeout for checking stop condition
|
|
384
|
+
pass
|
|
385
|
+
except Exception as e:
|
|
386
|
+
if IPC_SERVER_THREADS[socket_path]["running"]:
|
|
387
|
+
logger.error(f"Error accepting connection: {e}")
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.error(f"IPC server error on {socket_path}: {e}", exc_info=True)
|
|
391
|
+
finally:
|
|
392
|
+
try:
|
|
393
|
+
server_socket.close()
|
|
394
|
+
except:
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
# Clean up socket file
|
|
398
|
+
if os.path.exists(socket_path):
|
|
399
|
+
os.unlink(socket_path)
|
|
400
|
+
|
|
401
|
+
logger.info(f"IPC Server on {socket_path} stopped")
|
|
402
|
+
IPC_SERVER_THREADS[socket_path]["running"] = False
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@tool
|
|
406
|
+
def ipc(
|
|
407
|
+
action: str,
|
|
408
|
+
socket_path: str = None,
|
|
409
|
+
system_prompt: str = "You are a helpful IPC server assistant.",
|
|
410
|
+
) -> dict:
|
|
411
|
+
"""Create and manage IPC servers with real-time streaming.
|
|
412
|
+
|
|
413
|
+
This tool creates a Unix socket server for inter-process communication,
|
|
414
|
+
similar to the WebSocket server but for local processes (tray, ambient, etc.).
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
action: Action to perform (start_server, stop_server, get_status)
|
|
418
|
+
socket_path: Unix socket path (default: /tmp/devduck_main.sock)
|
|
419
|
+
system_prompt: System prompt for the server DevDuck instances
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Dictionary containing status and response content
|
|
423
|
+
"""
|
|
424
|
+
# Default socket path
|
|
425
|
+
if not socket_path:
|
|
426
|
+
socket_path = os.path.join(tempfile.gettempdir(), "devduck_main.sock")
|
|
427
|
+
|
|
428
|
+
if action == "start_server":
|
|
429
|
+
if socket_path in IPC_SERVER_THREADS and IPC_SERVER_THREADS[socket_path].get(
|
|
430
|
+
"running", False
|
|
431
|
+
):
|
|
432
|
+
return {
|
|
433
|
+
"status": "error",
|
|
434
|
+
"content": [
|
|
435
|
+
{"text": f"❌ Error: IPC Server already running on {socket_path}"}
|
|
436
|
+
],
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
IPC_SERVER_THREADS[socket_path] = {"running": False}
|
|
440
|
+
server_thread = threading.Thread(
|
|
441
|
+
target=run_ipc_server, args=(socket_path, system_prompt), daemon=True
|
|
442
|
+
)
|
|
443
|
+
server_thread.start()
|
|
444
|
+
|
|
445
|
+
time.sleep(0.5)
|
|
446
|
+
|
|
447
|
+
if not IPC_SERVER_THREADS[socket_path].get("running", False):
|
|
448
|
+
return {
|
|
449
|
+
"status": "error",
|
|
450
|
+
"content": [
|
|
451
|
+
{"text": f"❌ Error: Failed to start IPC Server on {socket_path}"}
|
|
452
|
+
],
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
"status": "success",
|
|
457
|
+
"content": [
|
|
458
|
+
{"text": f"✅ IPC Server started successfully on {socket_path}"},
|
|
459
|
+
{"text": f"System prompt: {system_prompt}"},
|
|
460
|
+
{"text": "🌊 Real-time streaming with concurrent message processing"},
|
|
461
|
+
{"text": "📦 Newline-delimited JSON messages with turn_id"},
|
|
462
|
+
{
|
|
463
|
+
"text": "🦆 Server creates a new DevDuck instance for each connection"
|
|
464
|
+
},
|
|
465
|
+
{"text": "⚡ Send multiple messages without waiting!"},
|
|
466
|
+
{"text": f"📝 Connect from local processes to: {socket_path}"},
|
|
467
|
+
],
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
elif action == "stop_server":
|
|
471
|
+
if socket_path not in IPC_SERVER_THREADS or not IPC_SERVER_THREADS[
|
|
472
|
+
socket_path
|
|
473
|
+
].get("running", False):
|
|
474
|
+
return {
|
|
475
|
+
"status": "error",
|
|
476
|
+
"content": [
|
|
477
|
+
{"text": f"❌ Error: No IPC Server running on {socket_path}"}
|
|
478
|
+
],
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
IPC_SERVER_THREADS[socket_path]["running"] = False
|
|
482
|
+
|
|
483
|
+
# Close socket if exists
|
|
484
|
+
if "socket" in IPC_SERVER_THREADS[socket_path]:
|
|
485
|
+
try:
|
|
486
|
+
IPC_SERVER_THREADS[socket_path]["socket"].close()
|
|
487
|
+
except:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
time.sleep(1.0)
|
|
491
|
+
|
|
492
|
+
connections = IPC_SERVER_THREADS[socket_path].get("connections", 0)
|
|
493
|
+
uptime = time.time() - IPC_SERVER_THREADS[socket_path].get(
|
|
494
|
+
"start_time", time.time()
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
del IPC_SERVER_THREADS[socket_path]
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
"status": "success",
|
|
501
|
+
"content": [
|
|
502
|
+
{"text": f"✅ IPC Server on {socket_path} stopped successfully"},
|
|
503
|
+
{
|
|
504
|
+
"text": f"Statistics: {connections} connections handled, uptime {uptime:.2f} seconds"
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
elif action == "get_status":
|
|
510
|
+
if not IPC_SERVER_THREADS:
|
|
511
|
+
return {
|
|
512
|
+
"status": "success",
|
|
513
|
+
"content": [{"text": "No IPC Servers running"}],
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
status_info = []
|
|
517
|
+
for path, data in IPC_SERVER_THREADS.items():
|
|
518
|
+
if data.get("running", False):
|
|
519
|
+
uptime = time.time() - data.get("start_time", time.time())
|
|
520
|
+
connections = data.get("connections", 0)
|
|
521
|
+
status_info.append(
|
|
522
|
+
f"Socket {path}: Running - {connections} connections, uptime {uptime:.2f}s"
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
status_info.append(f"Socket {path}: Stopped")
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
"status": "success",
|
|
529
|
+
"content": [
|
|
530
|
+
{"text": "IPC Server Status:"},
|
|
531
|
+
{"text": "\n".join(status_info)},
|
|
532
|
+
],
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
else:
|
|
536
|
+
return {
|
|
537
|
+
"status": "error",
|
|
538
|
+
"content": [
|
|
539
|
+
{
|
|
540
|
+
"text": f"Error: Unknown action '{action}'. Supported: start_server, stop_server, get_status"
|
|
541
|
+
}
|
|
542
|
+
],
|
|
543
|
+
}
|
devduck/tools/tcp.py
CHANGED
|
@@ -216,10 +216,6 @@ def handle_client(
|
|
|
216
216
|
)
|
|
217
217
|
|
|
218
218
|
try:
|
|
219
|
-
# Send welcome message
|
|
220
|
-
welcome_msg = "🦆 Welcome to DevDuck TCP Server!\n"
|
|
221
|
-
welcome_msg += "Send a message or 'exit' to close the connection.\n\n"
|
|
222
|
-
streaming_handler._send(welcome_msg)
|
|
223
219
|
|
|
224
220
|
while True:
|
|
225
221
|
# Receive data from the client
|