agent-mcp 0.1.2__py3-none-any.whl → 0.1.3__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.
- agent_mcp/__init__.py +16 -0
- agent_mcp/crewai_mcp_adapter.py +281 -0
- agent_mcp/enhanced_mcp_agent.py +601 -0
- agent_mcp/heterogeneous_group_chat.py +424 -0
- agent_mcp/langchain_mcp_adapter.py +325 -0
- agent_mcp/langgraph_mcp_adapter.py +325 -0
- agent_mcp/mcp_agent.py +632 -0
- agent_mcp/mcp_decorator.py +257 -0
- agent_mcp/mcp_langgraph.py +733 -0
- agent_mcp/mcp_transaction.py +97 -0
- agent_mcp/mcp_transport.py +700 -0
- agent_mcp/mcp_transport_enhanced.py +46 -0
- agent_mcp/proxy_agent.py +24 -0
- agent_mcp-0.1.3.dist-info/METADATA +331 -0
- agent_mcp-0.1.3.dist-info/RECORD +18 -0
- agent_mcp-0.1.3.dist-info/top_level.txt +1 -0
- agent_mcp-0.1.2.dist-info/METADATA +0 -475
- agent_mcp-0.1.2.dist-info/RECORD +0 -5
- agent_mcp-0.1.2.dist-info/top_level.txt +0 -1
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced MCP Agent with client/server capabilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import collections
|
|
7
|
+
from typing import Optional, Dict, Any, List
|
|
8
|
+
from .mcp_agent import MCPAgent
|
|
9
|
+
from .mcp_transport import MCPTransport
|
|
10
|
+
|
|
11
|
+
class EnhancedMCPAgent(MCPAgent):
|
|
12
|
+
"""MCPAgent with client/server capabilities"""
|
|
13
|
+
|
|
14
|
+
def __init__(self,
|
|
15
|
+
name: str,
|
|
16
|
+
transport: Optional[MCPTransport] = None,
|
|
17
|
+
server_mode: bool = False,
|
|
18
|
+
client_mode: bool = False,
|
|
19
|
+
**kwargs):
|
|
20
|
+
super().__init__(name=name, **kwargs)
|
|
21
|
+
|
|
22
|
+
self.transport = transport
|
|
23
|
+
self.server_mode = server_mode
|
|
24
|
+
self.client_mode = client_mode
|
|
25
|
+
self.connected_agents = {}
|
|
26
|
+
self.task_queue = asyncio.Queue()
|
|
27
|
+
self.task_results = {}
|
|
28
|
+
self.task_dependencies = {}
|
|
29
|
+
self._pending_tasks = {}
|
|
30
|
+
self._task_processor = None
|
|
31
|
+
self._message_processor = None
|
|
32
|
+
|
|
33
|
+
def start_server(self):
|
|
34
|
+
"""Start agent in server mode"""
|
|
35
|
+
if not self.server_mode or not self.transport:
|
|
36
|
+
raise ValueError("Agent not configured for server mode")
|
|
37
|
+
|
|
38
|
+
# Start the transport server
|
|
39
|
+
self.transport.start()
|
|
40
|
+
|
|
41
|
+
async def connect_to_server(self, server_url: str):
|
|
42
|
+
"""Connect to another agent's server"""
|
|
43
|
+
if not self.client_mode or not self.transport:
|
|
44
|
+
raise ValueError("Agent not configured for client mode")
|
|
45
|
+
|
|
46
|
+
# Register with the server
|
|
47
|
+
registration = {
|
|
48
|
+
"type": "registration",
|
|
49
|
+
"agent_id": self.mcp_id,
|
|
50
|
+
"name": self.name,
|
|
51
|
+
"capabilities": self.list_available_tools()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
response = await self.transport.send_message(server_url, registration)
|
|
55
|
+
if response.get("status") == "ok":
|
|
56
|
+
self.connected_agents[server_url] = response.get("server_id")
|
|
57
|
+
print(f"Successfully connected to server at {server_url}")
|
|
58
|
+
|
|
59
|
+
async def handle_incoming_message(self, message: Dict[str, Any]):
|
|
60
|
+
"""Handle incoming messages from other agents"""
|
|
61
|
+
# First check if type is directly in the message
|
|
62
|
+
msg_type = message.get("type")
|
|
63
|
+
|
|
64
|
+
# If not, check if it's inside the content field
|
|
65
|
+
if not msg_type and "content" in message and isinstance(message["content"], dict):
|
|
66
|
+
msg_type = message["content"].get("type")
|
|
67
|
+
|
|
68
|
+
print(f"[DEBUG] {self.name}: Received message of type: {msg_type}")
|
|
69
|
+
|
|
70
|
+
if msg_type == "registration":
|
|
71
|
+
# Handle new agent registration
|
|
72
|
+
await self._handle_registration(message)
|
|
73
|
+
elif msg_type == "tool_call":
|
|
74
|
+
# Handle tool execution request
|
|
75
|
+
await self._handle_tool_call(message)
|
|
76
|
+
elif msg_type == "task":
|
|
77
|
+
# Handle new task assignment
|
|
78
|
+
await self._handle_task(message)
|
|
79
|
+
elif msg_type == "task_result":
|
|
80
|
+
# Handle task result
|
|
81
|
+
print(f"[DEBUG] {self.name}: Processing task_result message: {message}")
|
|
82
|
+
await self._handle_task_result(message)
|
|
83
|
+
elif msg_type == "get_result":
|
|
84
|
+
# Handle get result request
|
|
85
|
+
await self._handle_get_result(message)
|
|
86
|
+
else:
|
|
87
|
+
print(f"[WARN] {self.name}: Received unknown message type: {msg_type}")
|
|
88
|
+
|
|
89
|
+
async def _handle_registration(self, message: Dict[str, Any]):
|
|
90
|
+
"""Handle agent registration"""
|
|
91
|
+
agent_id = message.get("agent_id")
|
|
92
|
+
agent_name = message.get("name")
|
|
93
|
+
capabilities = message.get("capabilities", [])
|
|
94
|
+
|
|
95
|
+
self.connected_agents[agent_id] = {
|
|
96
|
+
"name": agent_name,
|
|
97
|
+
"capabilities": capabilities
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
print(f"New agent registered: {agent_name} ({agent_id})")
|
|
101
|
+
return {"status": "ok", "server_id": self.mcp_id}
|
|
102
|
+
|
|
103
|
+
async def _handle_tool_call(self, message: Dict[str, Any]):
|
|
104
|
+
"""Handle tool execution request"""
|
|
105
|
+
tool_name = message.get("tool")
|
|
106
|
+
arguments = message.get("arguments", {})
|
|
107
|
+
|
|
108
|
+
if tool_name in self.mcp_tools:
|
|
109
|
+
result = await self.execute_tool(tool_name, **arguments)
|
|
110
|
+
return {"status": "ok", "result": result}
|
|
111
|
+
else:
|
|
112
|
+
return {"status": "error", "message": f"Tool {tool_name} not found"}
|
|
113
|
+
|
|
114
|
+
async def _handle_task(self, message: Dict[str, Any]):
|
|
115
|
+
"""Handle incoming task"""
|
|
116
|
+
print(f"[DEBUG] {self.name}: Received task message: {message}")
|
|
117
|
+
|
|
118
|
+
# --- Idempotency Check ---
|
|
119
|
+
# Check if we should process this message based on task_id
|
|
120
|
+
if not self._should_process_message(message):
|
|
121
|
+
# If not, acknowledge it immediately if possible and stop
|
|
122
|
+
message_id = message.get("message_id")
|
|
123
|
+
if message_id and self.transport:
|
|
124
|
+
# Run acknowledge in background, don't wait
|
|
125
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
126
|
+
print(f"[DEBUG] {self.name}: Acknowledged duplicate task {message.get('task_id')} (msg_id: {message_id})")
|
|
127
|
+
return {"status": "skipped", "message": "Task already completed"}
|
|
128
|
+
# --- End Idempotency Check ---
|
|
129
|
+
|
|
130
|
+
task_id = message.get("task_id") or message.get("content", {}).get("task_id")
|
|
131
|
+
depends_on = message.get("depends_on", [])
|
|
132
|
+
message_id = message.get("message_id") # Get the message ID for acknowledgment
|
|
133
|
+
|
|
134
|
+
if not task_id:
|
|
135
|
+
print(f"[ERROR] {self.name}: Received task without task_id: {message}")
|
|
136
|
+
return {"status": "error", "message": "No task_id provided"}
|
|
137
|
+
|
|
138
|
+
# If this task has dependencies, check if they're met
|
|
139
|
+
if depends_on:
|
|
140
|
+
print(f"[DEBUG] {self.name}: Task {task_id} depends on: {depends_on}")
|
|
141
|
+
# Check if all dependencies are in task_results
|
|
142
|
+
for dep_id in depends_on:
|
|
143
|
+
if dep_id not in self.task_results:
|
|
144
|
+
print(f"[DEBUG] {self.name}: Dependency {dep_id} not met for task {task_id}, waiting...")
|
|
145
|
+
return {"status": "waiting", "message": f"Waiting for dependency {dep_id}"}
|
|
146
|
+
print(f"[DEBUG] {self.name}: All dependencies met for task {task_id}")
|
|
147
|
+
|
|
148
|
+
# Store task info if we're the coordinator
|
|
149
|
+
if self.server_mode:
|
|
150
|
+
print(f"[DEBUG] {self.name}: Storing task {task_id} in coordinator")
|
|
151
|
+
self.task_results[task_id] = None
|
|
152
|
+
|
|
153
|
+
print(f"[DEBUG] {self.name}: Queueing task {task_id} for processing")
|
|
154
|
+
await self.task_queue.put(message)
|
|
155
|
+
print(f"[DEBUG] {self.name}: Successfully queued task {task_id}")
|
|
156
|
+
|
|
157
|
+
return {"status": "ok"}
|
|
158
|
+
|
|
159
|
+
async def _handle_task_result(self, message: Dict[str, Any]):
|
|
160
|
+
"""Handle task result from an agent"""
|
|
161
|
+
content = message.get("content", {})
|
|
162
|
+
if isinstance(content, dict) and "text" in content:
|
|
163
|
+
# Handle case where content has a text field containing JSON
|
|
164
|
+
try:
|
|
165
|
+
import json
|
|
166
|
+
text_content = json.loads(content["text"])
|
|
167
|
+
task_id = text_content.get("task_id")
|
|
168
|
+
result = text_content.get("result")
|
|
169
|
+
except (json.JSONDecodeError, AttributeError) as e:
|
|
170
|
+
print(f"[ERROR] {self.name}: Failed to parse text content: {e}")
|
|
171
|
+
task_id = content.get("task_id")
|
|
172
|
+
result = content.get("result")
|
|
173
|
+
else:
|
|
174
|
+
# Normal case - extract directly from content or root
|
|
175
|
+
task_id = content.get("task_id") if isinstance(content, dict) else message.get("task_id")
|
|
176
|
+
result = content.get("result") if isinstance(content, dict) else message.get("result")
|
|
177
|
+
original_message_id = message.get('id')
|
|
178
|
+
sender_name = message.get('from', 'Unknown Sender')
|
|
179
|
+
|
|
180
|
+
print(f"[DEBUG] {self.name}: Handling task result for task_id: {task_id}")
|
|
181
|
+
|
|
182
|
+
if not task_id:
|
|
183
|
+
print(f"[ERROR] {self.name}: Received task result without task_id: {message}")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
print(f"[DEBUG] {self.name}: Received result for task {task_id} from {sender_name} (original_message_id: {original_message_id})")
|
|
187
|
+
|
|
188
|
+
# Store result
|
|
189
|
+
self.task_results[task_id] = result
|
|
190
|
+
|
|
191
|
+
# Check for dependent tasks
|
|
192
|
+
if task_id in self.task_dependencies:
|
|
193
|
+
print(f"[DEBUG] {self.name}: Found dependent tasks for {task_id}")
|
|
194
|
+
|
|
195
|
+
# Get tasks that depend on this one
|
|
196
|
+
task_ids = self.task_dependencies[task_id]
|
|
197
|
+
dependent_tasks = [self._pending_tasks[tid] for tid in task_ids if tid in self._pending_tasks]
|
|
198
|
+
|
|
199
|
+
# Remove this task from dependencies
|
|
200
|
+
del self.task_dependencies[task_id]
|
|
201
|
+
|
|
202
|
+
# Process each dependent task
|
|
203
|
+
for dependent_task in dependent_tasks:
|
|
204
|
+
print(f"[DEBUG] {self.name}: Processing dependent task {dependent_task}")
|
|
205
|
+
# Ensure task_info is a dictionary
|
|
206
|
+
if not isinstance(dependent_task, dict):
|
|
207
|
+
print(f"[WARNING] {self.name}: Skipping invalid task_info (not a dictionary): {dependent_task}")
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# Check if all dependencies are met
|
|
211
|
+
dependencies = dependent_task.get("depends_on", [])
|
|
212
|
+
all_deps_met = True
|
|
213
|
+
|
|
214
|
+
for dep in dependencies:
|
|
215
|
+
if dep not in self.task_results:
|
|
216
|
+
all_deps_met = False
|
|
217
|
+
# Re-add to dependencies since not all deps are met
|
|
218
|
+
task_id = dependent_task.get('task_id')
|
|
219
|
+
if not task_id:
|
|
220
|
+
print(f"[WARNING] {self.name}: Skipping task without task_id: {dependent_task}")
|
|
221
|
+
continue
|
|
222
|
+
# Add task_id to dependencies and store full task info
|
|
223
|
+
if dep not in self.task_dependencies:
|
|
224
|
+
self.task_dependencies[dep] = set()
|
|
225
|
+
self.task_dependencies[dep].add(task_id)
|
|
226
|
+
self._pending_tasks[task_id] = dependent_task
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if all_deps_met:
|
|
230
|
+
task_id = dependent_task.get('task_id')
|
|
231
|
+
if task_id in self._pending_tasks:
|
|
232
|
+
full_task = self._pending_tasks[task_id]
|
|
233
|
+
print(f"[DEBUG] {self.name}: All dependencies met for task {task_id}")
|
|
234
|
+
# Forward task to agent
|
|
235
|
+
# Ensure task has proper structure
|
|
236
|
+
task_to_assign = {
|
|
237
|
+
"task_id": task_id,
|
|
238
|
+
"description": full_task.get("description"),
|
|
239
|
+
"type": "task",
|
|
240
|
+
"depends_on": dependencies,
|
|
241
|
+
"content": full_task.get("content", {})
|
|
242
|
+
}
|
|
243
|
+
await self.assign_task(full_task["agent"], task_to_assign)
|
|
244
|
+
# Clean up
|
|
245
|
+
del self._pending_tasks[task_id]
|
|
246
|
+
for dep in dependencies:
|
|
247
|
+
if dep not in self.task_results:
|
|
248
|
+
# Extract fields from content if present
|
|
249
|
+
content = dependent_task.get("content", {})
|
|
250
|
+
task_id = content.get("task_id") or dependent_task.get("task_id")
|
|
251
|
+
description = content.get("description") or dependent_task.get("description")
|
|
252
|
+
depends_on = content.get("depends_on") or dependent_task.get("depends_on", [])
|
|
253
|
+
agent = dependent_task.get("agent")
|
|
254
|
+
|
|
255
|
+
if not task_id or not agent or not description:
|
|
256
|
+
print(f"[ERROR] {self.name}: Missing required fields in dependent task: {dependent_task}")
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Maintain consistent message structure
|
|
260
|
+
validated_task_info = {
|
|
261
|
+
"type": "task",
|
|
262
|
+
"content": {
|
|
263
|
+
"task_id": task_id,
|
|
264
|
+
"description": description,
|
|
265
|
+
"depends_on": depends_on,
|
|
266
|
+
"type": "task"
|
|
267
|
+
},
|
|
268
|
+
"agent": agent
|
|
269
|
+
}
|
|
270
|
+
task_id = validated_task_info.get('task_id')
|
|
271
|
+
if task_id:
|
|
272
|
+
if dep not in self.task_dependencies:
|
|
273
|
+
self.task_dependencies[dep] = set()
|
|
274
|
+
self.task_dependencies[dep].add(task_id)
|
|
275
|
+
# Store full task info
|
|
276
|
+
self._pending_tasks[task_id] = validated_task_info
|
|
277
|
+
|
|
278
|
+
# Acknowledge the task result if we have the original message ID
|
|
279
|
+
if original_message_id and self.transport:
|
|
280
|
+
try:
|
|
281
|
+
await self.transport.acknowledge_message(self.name, original_message_id)
|
|
282
|
+
print(f"[DEBUG] {self.name}: Acknowledged task result for {task_id} with message_id {original_message_id}")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
print(f"[ERROR] {self.name}: Error acknowledging task result: {e}")
|
|
285
|
+
traceback.print_exc()
|
|
286
|
+
|
|
287
|
+
return {"status": "ok"}
|
|
288
|
+
|
|
289
|
+
async def _handle_get_result(self, message: Dict[str, Any]):
|
|
290
|
+
"""Handle get result request"""
|
|
291
|
+
task_id = message.get("task_id")
|
|
292
|
+
if task_id in self.task_results:
|
|
293
|
+
return {"status": "ok", "result": self.task_results[task_id]}
|
|
294
|
+
else:
|
|
295
|
+
return {"status": "error", "message": f"Result for task {task_id} not found"}
|
|
296
|
+
|
|
297
|
+
async def assign_task(self, target_url: str, task: Dict[str, Any]):
|
|
298
|
+
"""Assign a task to another agent"""
|
|
299
|
+
print(f"{self.name}: Assigning task {task} to {target_url}")
|
|
300
|
+
|
|
301
|
+
# Extract task details from either content or root level
|
|
302
|
+
task_id = task.get("task_id")
|
|
303
|
+
description = task.get("description")
|
|
304
|
+
depends_on = task.get("depends_on", [])
|
|
305
|
+
|
|
306
|
+
# If task details are in content, use those instead
|
|
307
|
+
if "content" in task and isinstance(task["content"], dict):
|
|
308
|
+
content = task["content"]
|
|
309
|
+
task_id = content.get("task_id", task_id)
|
|
310
|
+
description = content.get("description", description)
|
|
311
|
+
depends_on = content.get("depends_on", depends_on)
|
|
312
|
+
|
|
313
|
+
message = {
|
|
314
|
+
"type": "task",
|
|
315
|
+
"content": {
|
|
316
|
+
"task_id": task_id,
|
|
317
|
+
"description": description,
|
|
318
|
+
"depends_on": depends_on,
|
|
319
|
+
"type": "task"
|
|
320
|
+
},
|
|
321
|
+
"from": self.mcp_id
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Only include reply_to if it exists in the task
|
|
325
|
+
if "reply_to" in task:
|
|
326
|
+
message["content"]["reply_to"] = task["reply_to"]
|
|
327
|
+
|
|
328
|
+
return await self.transport.send_message(target_url, message)
|
|
329
|
+
|
|
330
|
+
async def process_messages(self):
|
|
331
|
+
"""Process incoming messages from transport"""
|
|
332
|
+
print(f"{self.name}: Starting message processor...")
|
|
333
|
+
while True:
|
|
334
|
+
try:
|
|
335
|
+
# Get message with timeout (transport now handles this)
|
|
336
|
+
message, message_id = await self.transport.receive_message()
|
|
337
|
+
|
|
338
|
+
# Handle timeout case
|
|
339
|
+
if message is None:
|
|
340
|
+
await asyncio.sleep(0.1) # Prevent tight loop
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Skip invalid messages
|
|
344
|
+
if not isinstance(message, dict):
|
|
345
|
+
print(f"{self.name}: Skipping invalid message format: {message}")
|
|
346
|
+
if message_id: # Still acknowledge to avoid retries
|
|
347
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
print(f"{self.name}: Processing message ID: {message_id}, Type: {message.get('type', 'unknown')}")
|
|
351
|
+
|
|
352
|
+
# Add message_id for tracking
|
|
353
|
+
message['message_id'] = message_id
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Process the message
|
|
357
|
+
await self.handle_incoming_message(message)
|
|
358
|
+
|
|
359
|
+
# Only acknowledge after successful processing
|
|
360
|
+
if message_id:
|
|
361
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
362
|
+
print(f"{self.name}: Acknowledged message {message_id}")
|
|
363
|
+
except Exception as e:
|
|
364
|
+
print(f"{self.name}: Error handling message {message_id}: {e}")
|
|
365
|
+
import traceback
|
|
366
|
+
traceback.print_exc()
|
|
367
|
+
# Don't acknowledge on error so it can be retried
|
|
368
|
+
|
|
369
|
+
except asyncio.CancelledError:
|
|
370
|
+
print(f"{self.name}: Message processor cancelled")
|
|
371
|
+
break
|
|
372
|
+
except Exception as e:
|
|
373
|
+
print(f"{self.name}: Error in message processor: {e}")
|
|
374
|
+
import traceback
|
|
375
|
+
traceback.print_exc()
|
|
376
|
+
await asyncio.sleep(1) # Brief pause on unexpected error
|
|
377
|
+
|
|
378
|
+
async def process_tasks(self):
|
|
379
|
+
"""Process tasks from the queue"""
|
|
380
|
+
print(f"{self.name}: Starting task processor...")
|
|
381
|
+
while True:
|
|
382
|
+
try:
|
|
383
|
+
task = await self.task_queue.get()
|
|
384
|
+
print(f"{self.name}: Processing task: {task}")
|
|
385
|
+
|
|
386
|
+
# Get task description and task_id
|
|
387
|
+
task_desc = task.get("description") or task.get("content", {}).get("description") if isinstance(task.get("content"), dict) else task.get("description", "")
|
|
388
|
+
task_id = task.get("task_id") or task.get("content", {}).get("task_id") if isinstance(task.get("content"), dict) else task.get("task_id")
|
|
389
|
+
message_id = task.get('message_id')
|
|
390
|
+
|
|
391
|
+
if not task_desc or not task_id:
|
|
392
|
+
print(f"{self.name}: Error: Task is missing description or task_id")
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
# Check if this task has dependencies
|
|
396
|
+
depends_on = task.get("depends_on", [])
|
|
397
|
+
if depends_on:
|
|
398
|
+
print(f"{self.name}: Task {task_id} depends on: {depends_on}")
|
|
399
|
+
# Check if we have all dependencies
|
|
400
|
+
missing_deps = []
|
|
401
|
+
for dep_id in depends_on:
|
|
402
|
+
if dep_id not in self.task_results:
|
|
403
|
+
# Try to get dependency result from coordinator
|
|
404
|
+
if self.transport and self.transport.remote_url:
|
|
405
|
+
try:
|
|
406
|
+
result = await self.transport.send_message(
|
|
407
|
+
f"{self.transport.remote_url}/message/{self.name}",
|
|
408
|
+
{
|
|
409
|
+
"type": "get_result",
|
|
410
|
+
"task_id": dep_id,
|
|
411
|
+
"result": "",
|
|
412
|
+
"sender": self.name,
|
|
413
|
+
"original_message_id": message_id,
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
if result and result.get("result"):
|
|
417
|
+
self.task_results[dep_id] = result["result"]
|
|
418
|
+
continue
|
|
419
|
+
except Exception as e:
|
|
420
|
+
print(f"{self.name}: Error getting dependency result: {e}")
|
|
421
|
+
|
|
422
|
+
missing_deps.append(dep_id)
|
|
423
|
+
|
|
424
|
+
if missing_deps:
|
|
425
|
+
print(f"{self.name}: Task {task_id} is missing dependencies: {missing_deps}. Putting back in queue.")
|
|
426
|
+
# Put task back in queue and wait
|
|
427
|
+
await self.task_queue.put(task)
|
|
428
|
+
await asyncio.sleep(1)
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
# Add dependency results as context
|
|
432
|
+
task_context = "\nBased on the following findings:\n"
|
|
433
|
+
for dep_id in depends_on:
|
|
434
|
+
task_context += f"\nFrom {dep_id}:\n{self.task_results[dep_id]}"
|
|
435
|
+
else:
|
|
436
|
+
task_context = ""
|
|
437
|
+
|
|
438
|
+
# Generate response using LLM if configured
|
|
439
|
+
if hasattr(self, 'llm_config') and self.llm_config:
|
|
440
|
+
print(f"{self.name}: Generating response for task {task_id}...")
|
|
441
|
+
try:
|
|
442
|
+
response = self.generate_reply(
|
|
443
|
+
messages=[{
|
|
444
|
+
"role": "user",
|
|
445
|
+
"content": f"Please help with this task: {task_desc}{task_context}"
|
|
446
|
+
}]
|
|
447
|
+
)
|
|
448
|
+
print(f"{self.name}: Generated response for task {task_id}: {response}")
|
|
449
|
+
|
|
450
|
+
# Store result locally
|
|
451
|
+
self.task_results[task_id] = response
|
|
452
|
+
|
|
453
|
+
# --- Mark task completed for idempotency ---
|
|
454
|
+
self._mark_task_completed(task_id)
|
|
455
|
+
# --- End mark task completed ---
|
|
456
|
+
|
|
457
|
+
# Send result back if there's a reply_to
|
|
458
|
+
if "reply_to" in task and self.transport:
|
|
459
|
+
try:
|
|
460
|
+
await self.transport.send_message(
|
|
461
|
+
task["reply_to"],
|
|
462
|
+
{
|
|
463
|
+
"type": "task_result",
|
|
464
|
+
"task_id": task_id,
|
|
465
|
+
"result": response,
|
|
466
|
+
"sender": self.name,
|
|
467
|
+
"original_message_id": message_id
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
print(f"{self.name}: Result sent successfully")
|
|
471
|
+
|
|
472
|
+
# Try to acknowledge task completion
|
|
473
|
+
if "message_id" in task:
|
|
474
|
+
await self.transport.acknowledge_message(self.name, task["message_id"])
|
|
475
|
+
print(f"{self.name}: Task {task_id} acknowledged with message_id {task['message_id']}")
|
|
476
|
+
except Exception as send_error:
|
|
477
|
+
print(f"{self.name}: Error sending result: {send_error}")
|
|
478
|
+
traceback.print_exc()
|
|
479
|
+
except Exception as e:
|
|
480
|
+
print(f"{self.name}: Error generating response: {e}")
|
|
481
|
+
traceback.print_exc()
|
|
482
|
+
response = f"Error generating response: {e}"
|
|
483
|
+
|
|
484
|
+
# Send result back if there's a reply_to
|
|
485
|
+
if "reply_to" in task and self.transport:
|
|
486
|
+
reply_url = task["reply_to"]
|
|
487
|
+
print(f"{self.name}: Sending result back to {reply_url}")
|
|
488
|
+
try:
|
|
489
|
+
result = await self.transport.send_message(
|
|
490
|
+
reply_url,
|
|
491
|
+
{
|
|
492
|
+
"type": "task_result",
|
|
493
|
+
"task_id": task_id,
|
|
494
|
+
"result": response,
|
|
495
|
+
"sender": self.name,
|
|
496
|
+
"original_message_id": message_id
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
print(f"{self.name}: Result sent successfully: {result}")
|
|
500
|
+
|
|
501
|
+
# Store result locally too
|
|
502
|
+
self.task_results[task_id] = response
|
|
503
|
+
|
|
504
|
+
# Try to acknowledge task completion
|
|
505
|
+
try:
|
|
506
|
+
if "message_id" in task:
|
|
507
|
+
await self.transport.acknowledge_message(self.name, task["message_id"])
|
|
508
|
+
print(f"{self.name}: Acknowledged completion of task {task_id} with message_id {task['message_id']}")
|
|
509
|
+
else:
|
|
510
|
+
print(f"{self.name}: No message_id found for task {task_id}, cannot acknowledge")
|
|
511
|
+
except Exception as e:
|
|
512
|
+
print(f"{self.name}: Error acknowledging task completion: {e}")
|
|
513
|
+
traceback.print_exc()
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"{self.name}: Error sending result: {e}")
|
|
516
|
+
traceback.print_exc()
|
|
517
|
+
# Store result locally if no reply_to or transport info is available
|
|
518
|
+
print(f"{self.name}: No reply_to or transport info, storing result locally for task {task_id}")
|
|
519
|
+
self.task_results[task_id] = response
|
|
520
|
+
|
|
521
|
+
self.task_queue.task_done()
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
print(f"{self.name}: Error processing task: {str(e)}")
|
|
525
|
+
|
|
526
|
+
async def run(self):
|
|
527
|
+
"""Run the agent's main loop"""
|
|
528
|
+
if not self.transport:
|
|
529
|
+
raise ValueError("Transport not configured for agent")
|
|
530
|
+
|
|
531
|
+
# For remote connections, we need to connect() instead of start()
|
|
532
|
+
if hasattr(self.transport, 'is_remote') and self.transport.is_remote:
|
|
533
|
+
# Connect to remote server
|
|
534
|
+
await self.transport.connect(agent_name=self.name)
|
|
535
|
+
# Brief pause to ensure connection is ready
|
|
536
|
+
await asyncio.sleep(1)
|
|
537
|
+
else:
|
|
538
|
+
# For local server, just start it
|
|
539
|
+
self.transport.start()
|
|
540
|
+
|
|
541
|
+
# Start message and task processing in new tasks
|
|
542
|
+
self._message_processor = asyncio.create_task(
|
|
543
|
+
self.process_messages(),
|
|
544
|
+
name=f"messages_{self.name}"
|
|
545
|
+
)
|
|
546
|
+
self._task_processor = asyncio.create_task(
|
|
547
|
+
self.process_tasks(),
|
|
548
|
+
name=f"tasks_{self.name}"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Brief pause to let tasks start
|
|
552
|
+
await asyncio.sleep(0.1)
|
|
553
|
+
|
|
554
|
+
# Create a task to monitor the processors
|
|
555
|
+
monitor_task = asyncio.create_task(
|
|
556
|
+
self._monitor_processors(),
|
|
557
|
+
name=f"monitor_{self.name}"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Return the monitor task so it can be awaited
|
|
561
|
+
return monitor_task
|
|
562
|
+
|
|
563
|
+
async def _monitor_processors(self):
|
|
564
|
+
"""Monitor the message and task processors"""
|
|
565
|
+
try:
|
|
566
|
+
# Wait for both processors to complete or error
|
|
567
|
+
await asyncio.gather(
|
|
568
|
+
self._message_processor,
|
|
569
|
+
self._task_processor
|
|
570
|
+
)
|
|
571
|
+
except asyncio.CancelledError:
|
|
572
|
+
# Handle cancellation gracefully
|
|
573
|
+
await self.shutdown()
|
|
574
|
+
raise
|
|
575
|
+
except Exception as e:
|
|
576
|
+
print(f"{self.name}: Error in processors: {e}")
|
|
577
|
+
await self.shutdown()
|
|
578
|
+
raise
|
|
579
|
+
|
|
580
|
+
async def shutdown(self):
|
|
581
|
+
"""Shutdown the agent's tasks and disconnect transport"""
|
|
582
|
+
if hasattr(self, '_message_processor'):
|
|
583
|
+
self._message_processor.cancel()
|
|
584
|
+
try:
|
|
585
|
+
await self._message_processor
|
|
586
|
+
except asyncio.CancelledError:
|
|
587
|
+
pass
|
|
588
|
+
|
|
589
|
+
if hasattr(self, '_task_processor'):
|
|
590
|
+
self._task_processor.cancel()
|
|
591
|
+
try:
|
|
592
|
+
await self._task_processor
|
|
593
|
+
except asyncio.CancelledError:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
# Disconnect transport if remote
|
|
597
|
+
if hasattr(self.transport, 'is_remote') and self.transport.is_remote:
|
|
598
|
+
try:
|
|
599
|
+
await self.transport.disconnect()
|
|
600
|
+
except Exception as e:
|
|
601
|
+
print(f"{self.name}: Error during disconnect: {e}")
|