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,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HeterogeneousGroupChat - A group chat implementation for heterogeneous agents.
|
|
3
|
+
|
|
4
|
+
This module provides a high-level abstraction for creating group chats with agents
|
|
5
|
+
from different frameworks (Autogen, Langchain, etc.) that can collaborate on tasks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from typing import List, Dict, Any, Optional, Union, Sequence
|
|
11
|
+
from .mcp_transport import HTTPTransport
|
|
12
|
+
from .enhanced_mcp_agent import EnhancedMCPAgent
|
|
13
|
+
from .mcp_agent import MCPAgent
|
|
14
|
+
|
|
15
|
+
class HeterogeneousGroupChat:
|
|
16
|
+
"""
|
|
17
|
+
A group chat for heterogeneous agents that abstracts away the complexity
|
|
18
|
+
of setting up connections and coordinating tasks between different frameworks.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
name: str,
|
|
24
|
+
server_url: str = "https://mcp-server-ixlfhxquwq-ew.a.run.app",
|
|
25
|
+
coordinator_config: Optional[Dict[str, Any]] = None
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Initialize a heterogeneous group chat.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: Name of the group chat
|
|
32
|
+
server_url: URL of the deployed MCP server
|
|
33
|
+
coordinator_config: Optional configuration for the coordinator agent
|
|
34
|
+
"""
|
|
35
|
+
self.name = name
|
|
36
|
+
self.server_url = server_url
|
|
37
|
+
self.agents: List[MCPAgent] = []
|
|
38
|
+
self.coordinator: Optional[EnhancedMCPAgent] = None
|
|
39
|
+
self.coordinator_config = coordinator_config or {}
|
|
40
|
+
self.coordinator_url = server_url
|
|
41
|
+
self.agent_tokens: Dict[str, str] = {} # Store agent tokens
|
|
42
|
+
self._register_event = asyncio.Event()
|
|
43
|
+
self._agent_tasks = [] # Initialize list to store agent tasks
|
|
44
|
+
# Initialize directly on the group chat instance first
|
|
45
|
+
self.task_results: Dict[str, Any] = {}
|
|
46
|
+
self.task_dependencies: Dict[str, Dict] = {}
|
|
47
|
+
|
|
48
|
+
def _get_agent_url(self, agent_name: str) -> str:
|
|
49
|
+
"""Get the URL for an agent on the deployed server"""
|
|
50
|
+
return f"{self.server_url}/agents/{agent_name}"
|
|
51
|
+
|
|
52
|
+
def create_coordinator(self, api_key: str) -> EnhancedMCPAgent:
|
|
53
|
+
"""Create the coordinator agent for the group chat"""
|
|
54
|
+
# Avoid creating coordinator if it already exists
|
|
55
|
+
if self.coordinator:
|
|
56
|
+
return self.coordinator
|
|
57
|
+
|
|
58
|
+
# Define coordinator name (use config if provided, else default)
|
|
59
|
+
coordinator_name = self.coordinator_config.get("name", f"{self.name}Coordinator")
|
|
60
|
+
|
|
61
|
+
# Create transport for coordinator, passing its name
|
|
62
|
+
coordinator_transport = HTTPTransport.from_url(
|
|
63
|
+
self.server_url,
|
|
64
|
+
agent_name=coordinator_name
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# --- Default Coordinator Configuration ---
|
|
68
|
+
default_config = {
|
|
69
|
+
"name": coordinator_name,
|
|
70
|
+
"transport": coordinator_transport,
|
|
71
|
+
"system_message": "You are a helpful AI assistant coordinating tasks between other specialized agents. You receive task results and ensure the overall goal is achieved.",
|
|
72
|
+
"llm_config": {
|
|
73
|
+
# Default model, can be overridden by coordinator_config
|
|
74
|
+
"config_list": [{
|
|
75
|
+
"model": "gpt-3.5-turbo",
|
|
76
|
+
"api_key": api_key
|
|
77
|
+
}],
|
|
78
|
+
"cache_seed": 42 # Or None for no caching
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# --- Merge Default and User Config ---
|
|
83
|
+
# User config takes precedence
|
|
84
|
+
final_config = default_config.copy() # Start with defaults
|
|
85
|
+
final_config.update(self.coordinator_config) # Update with user overrides
|
|
86
|
+
|
|
87
|
+
# Ensure llm_config is properly structured if overridden
|
|
88
|
+
if "llm_config" in self.coordinator_config and "config_list" not in final_config["llm_config"]:
|
|
89
|
+
print("Warning: coordinator_config provided llm_config without config_list. Re-structuring.")
|
|
90
|
+
# Assume the user provided a simple dict like {"api_key": ..., "model": ...}
|
|
91
|
+
# We need to wrap it in config_list for AutoGen
|
|
92
|
+
user_llm_config = final_config["llm_config"]
|
|
93
|
+
final_config["llm_config"] = {
|
|
94
|
+
"config_list": [user_llm_config],
|
|
95
|
+
"cache_seed": user_llm_config.get("cache_seed", 42)
|
|
96
|
+
}
|
|
97
|
+
elif "llm_config" in final_config and "api_key" not in final_config["llm_config"].get("config_list", [{}])[0]:
|
|
98
|
+
# If llm_config exists but api_key is missing in the primary config
|
|
99
|
+
print("Warning: api_key missing in llm_config config_list. Injecting from create_coordinator argument.")
|
|
100
|
+
if "config_list" not in final_config["llm_config"]:
|
|
101
|
+
final_config["llm_config"]["config_list"] = [{}]
|
|
102
|
+
final_config["llm_config"]["config_list"][0]["api_key"] = api_key
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --- Create Coordinator Agent ---
|
|
106
|
+
print(f"Creating coordinator with config: {final_config}") # Debug: Log final config
|
|
107
|
+
self.coordinator = EnhancedMCPAgent(**final_config)
|
|
108
|
+
|
|
109
|
+
# --- Set Message Handler ---
|
|
110
|
+
self.coordinator.transport.set_message_handler(self._handle_coordinator_message)
|
|
111
|
+
return self.coordinator
|
|
112
|
+
|
|
113
|
+
def add_agents(self, agents: Union[MCPAgent, Sequence[MCPAgent]]) -> List[MCPAgent]:
|
|
114
|
+
"""
|
|
115
|
+
Add one or more agents to the group chat.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
agents: A single MCPAgent or a sequence of MCPAgents
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of added agents
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
# Add a single agent
|
|
125
|
+
group.add_agents(agent1)
|
|
126
|
+
|
|
127
|
+
# Add multiple agents
|
|
128
|
+
group.add_agents([agent1, agent2, agent3])
|
|
129
|
+
|
|
130
|
+
# Add agents as separate arguments
|
|
131
|
+
group.add_agents(agent1, agent2, agent3)
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(agents, (list, tuple)):
|
|
134
|
+
agents = [agents]
|
|
135
|
+
|
|
136
|
+
added_agents = []
|
|
137
|
+
for agent in agents:
|
|
138
|
+
# Retrieve token if agent was already registered
|
|
139
|
+
token = self.agent_tokens.get(agent.name)
|
|
140
|
+
if not self.server_url:
|
|
141
|
+
raise ValueError("Cannot add agents before connecting. Call connect() first.")
|
|
142
|
+
|
|
143
|
+
# Create transport for the agent, passing its name and token
|
|
144
|
+
agent.transport = HTTPTransport.from_url(self.server_url, agent_name=agent.name, token=token)
|
|
145
|
+
|
|
146
|
+
# Set client mode if needed
|
|
147
|
+
if hasattr(agent, 'client_mode'):
|
|
148
|
+
agent.client_mode = True
|
|
149
|
+
|
|
150
|
+
self.agents.append(agent)
|
|
151
|
+
added_agents.append(agent)
|
|
152
|
+
|
|
153
|
+
return added_agents
|
|
154
|
+
|
|
155
|
+
# Alias for backward compatibility
|
|
156
|
+
add_agent = add_agents
|
|
157
|
+
|
|
158
|
+
async def connect(self):
|
|
159
|
+
"""Register all agents and start their processing loops."""
|
|
160
|
+
print("Registering coordinator...")
|
|
161
|
+
coord_task = await self._register_and_start_agent(self.coordinator)
|
|
162
|
+
if not coord_task:
|
|
163
|
+
print("Coordinator registration failed. Aborting connect.")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
print("Registering agents...")
|
|
167
|
+
tasks = [coord_task] # Start with coordinator task
|
|
168
|
+
for agent in self.agents:
|
|
169
|
+
agent_task = await self._register_and_start_agent(agent)
|
|
170
|
+
if agent_task: # Only add task if registration was successful
|
|
171
|
+
tasks.append(agent_task)
|
|
172
|
+
else:
|
|
173
|
+
print(f"Skipping agent {agent.name} due to registration failure.")
|
|
174
|
+
# Optionally, handle failed agents (e.g., remove from group?)
|
|
175
|
+
|
|
176
|
+
if not tasks:
|
|
177
|
+
print("No agents were successfully registered and started.")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
print(f"All {len(tasks)} agents registered and started.")
|
|
181
|
+
# Store tasks but don't wait for them - they'll run in the background
|
|
182
|
+
self._agent_tasks = tasks
|
|
183
|
+
print("Group chat ready for task submission.")
|
|
184
|
+
|
|
185
|
+
async def _register_and_start_agent(self, agent: MCPAgent):
|
|
186
|
+
"""Register an agent, start its event stream, and its processors."""
|
|
187
|
+
if not agent.transport or not isinstance(agent.transport, HTTPTransport):
|
|
188
|
+
raise ValueError(f"Agent {agent.name} has no valid HTTPTransport defined.")
|
|
189
|
+
|
|
190
|
+
response = await agent.transport.register_agent(agent)
|
|
191
|
+
|
|
192
|
+
# Parse response which may be in {'body': '{...}'} format
|
|
193
|
+
if isinstance(response, dict):
|
|
194
|
+
if 'body' in response:
|
|
195
|
+
# Response is wrapped, parse the body string
|
|
196
|
+
try:
|
|
197
|
+
response = json.loads(response['body'])
|
|
198
|
+
except json.JSONDecodeError:
|
|
199
|
+
print(f"Error parsing agent registration response body: {response}")
|
|
200
|
+
|
|
201
|
+
if response and isinstance(response, dict) and "token" in response:
|
|
202
|
+
token = response["token"]
|
|
203
|
+
self.agent_tokens[agent.name] = token
|
|
204
|
+
agent.transport.token = token
|
|
205
|
+
agent.transport.auth_token = token
|
|
206
|
+
print(f"Agent {agent.name} registered successfully with token.")
|
|
207
|
+
|
|
208
|
+
# Start polling *before* starting the agent's run loop
|
|
209
|
+
await agent.transport.start_polling()
|
|
210
|
+
|
|
211
|
+
# Start agent's main run loop (message processing, etc.)
|
|
212
|
+
# We create the task but don't await it here; the calling function (connect) will gather tasks.
|
|
213
|
+
task = asyncio.create_task(agent.run())
|
|
214
|
+
self._agent_tasks.append(task) # Store the task
|
|
215
|
+
return task # Return the task for potential gathering
|
|
216
|
+
else:
|
|
217
|
+
print(f"Warning: Agent {agent.name} registration failed or did not return a token. Response: {response}")
|
|
218
|
+
# Don't run the agent if registration fails - it won't be able to communicate
|
|
219
|
+
return None # Indicate failure
|
|
220
|
+
|
|
221
|
+
async def submit_task(self, task: Dict[str, Any]) -> None:
|
|
222
|
+
"""Submit a task to the group chat."""
|
|
223
|
+
print(f"***** [{self.name}] ENTERING submit_task *****", flush=True) # Ensure entry is logged
|
|
224
|
+
if not self.coordinator:
|
|
225
|
+
raise ValueError("Group chat not connected. Call connect() first.")
|
|
226
|
+
|
|
227
|
+
self.task_results = {} # Reset results for new task submission
|
|
228
|
+
print("\n=== Submitting task to group ===")
|
|
229
|
+
|
|
230
|
+
# Ensure task is in the correct format
|
|
231
|
+
if not isinstance(task, dict) or 'type' not in task:
|
|
232
|
+
task = {'type': 'task', 'content': task}
|
|
233
|
+
|
|
234
|
+
# Store task dependencies from the input task definition
|
|
235
|
+
# We need a dictionary where keys are the step task_ids
|
|
236
|
+
if isinstance(task['content'], dict) and all(isinstance(v, dict) for v in task['content'].values()):
|
|
237
|
+
# If task is already a dict mapping task_ids to task info
|
|
238
|
+
self.task_dependencies = task['content']
|
|
239
|
+
else:
|
|
240
|
+
# If task has a steps list, convert it to a dict
|
|
241
|
+
self.task_dependencies = {step["task_id"]: step for step in task['content'].get("steps", [])}
|
|
242
|
+
print(f"Parsed Step Dependencies: {self.task_dependencies}")
|
|
243
|
+
|
|
244
|
+
# Also store in coordinator instance if it exists
|
|
245
|
+
if self.coordinator:
|
|
246
|
+
# Ensure the coordinator has the dict initialized
|
|
247
|
+
if not hasattr(self.coordinator, 'task_dependencies') or not isinstance(getattr(self.coordinator, 'task_dependencies', None), dict):
|
|
248
|
+
self.coordinator.task_dependencies = {}
|
|
249
|
+
self.coordinator.task_dependencies.update(self.task_dependencies)
|
|
250
|
+
|
|
251
|
+
if not self.coordinator or not self.coordinator.transport:
|
|
252
|
+
print("CRITICAL ERROR: Coordinator is not initialized or has no transport. Cannot submit task.")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
coordinator_transport = self.coordinator.transport
|
|
256
|
+
|
|
257
|
+
print(f"[DEBUG - {self.name}] Starting submit_task loop over {len(self.task_dependencies)} dependencies.", flush=True)
|
|
258
|
+
print(f"***** [{self.name}] Dependencies Content: {self.task_dependencies} *****", flush=True) # Log content before loop
|
|
259
|
+
|
|
260
|
+
# Assign tasks to agents based on the structure
|
|
261
|
+
# Submit tasks to their respective agents
|
|
262
|
+
for task_id, task_info in self.task_dependencies.items():
|
|
263
|
+
print(f"[DEBUG - {self.name}] Loop Iteration: Processing task_id '{task_id}' for agent '{task_info['agent']}'", flush=True)
|
|
264
|
+
agent_name = task_info["agent"]
|
|
265
|
+
# Create message with all necessary fields including content
|
|
266
|
+
message = {
|
|
267
|
+
"type": "task",
|
|
268
|
+
"task_id": task_id,
|
|
269
|
+
"description": task_info["description"],
|
|
270
|
+
"content": task_info.get("content", {}), # Include task content
|
|
271
|
+
"depends_on": task_info.get("depends_on", []), # Include dependencies
|
|
272
|
+
"reply_to": f"{self.server_url}/message/{self.coordinator.name}" # Full URL for reply
|
|
273
|
+
}
|
|
274
|
+
print(f"Sending task to {agent_name}")
|
|
275
|
+
print(f"Task message: {message}")
|
|
276
|
+
# Use coordinator's transport to send task to agent
|
|
277
|
+
await coordinator_transport.send_message(agent_name, message)
|
|
278
|
+
|
|
279
|
+
print("Task submitted. Waiting for completion...")
|
|
280
|
+
|
|
281
|
+
async def wait_for_completion(self, check_interval: float = 1.0):
|
|
282
|
+
"""
|
|
283
|
+
Wait for all tasks to complete.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
check_interval: How often to check for completion in seconds
|
|
287
|
+
"""
|
|
288
|
+
if not self.coordinator:
|
|
289
|
+
raise ValueError("Group chat not connected. Call connect() first.")
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
while True:
|
|
293
|
+
# Check if all tasks have results
|
|
294
|
+
all_completed = True
|
|
295
|
+
# Use the dependencies stored in the coordinator
|
|
296
|
+
for task_id in self.task_dependencies:
|
|
297
|
+
# Check both group chat and coordinator results
|
|
298
|
+
if task_id not in self.task_results and task_id not in self.coordinator.task_results:
|
|
299
|
+
all_completed = False
|
|
300
|
+
print(f"Waiting for task {task_id}...")
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
if all_completed:
|
|
304
|
+
print("\n=== All tasks completed! ===")
|
|
305
|
+
print("\nResults:")
|
|
306
|
+
# Merge results from both sources
|
|
307
|
+
all_results = {**self.task_results, **self.coordinator.task_results}
|
|
308
|
+
for task_id, result in all_results.items():
|
|
309
|
+
print(f"\n{task_id}:")
|
|
310
|
+
print(result)
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
await asyncio.sleep(check_interval)
|
|
314
|
+
|
|
315
|
+
except KeyboardInterrupt:
|
|
316
|
+
print("\nStopping group chat...")
|
|
317
|
+
|
|
318
|
+
async def _handle_coordinator_message(self, message: Dict, message_id: str):
|
|
319
|
+
"""Handles messages received by the coordinator's transport."""
|
|
320
|
+
if not self.coordinator: # Ensure coordinator exists
|
|
321
|
+
print("[Coordinator Handler] Error: Coordinator not initialized.")
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
print(f"\n[Coordinator {self.coordinator.name}] Received message: {message}")
|
|
325
|
+
|
|
326
|
+
# Handle messages wrapped in 'body' field
|
|
327
|
+
if isinstance(message, dict) and 'body' in message:
|
|
328
|
+
try:
|
|
329
|
+
if isinstance(message['body'], str):
|
|
330
|
+
message = json.loads(message['body'])
|
|
331
|
+
else:
|
|
332
|
+
message = message['body']
|
|
333
|
+
print(f"[Coordinator {self.coordinator.name}] Unwrapped message body: {message}")
|
|
334
|
+
except json.JSONDecodeError:
|
|
335
|
+
print(f"[Coordinator {self.coordinator.name}] Error decoding message body: {message}")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Look for type and task_id at top level
|
|
339
|
+
msg_type = message.get("type")
|
|
340
|
+
task_id = message.get("task_id")
|
|
341
|
+
|
|
342
|
+
print(f"[Coordinator {self.coordinator.name}] Processing message type '{msg_type}' for task {task_id}")
|
|
343
|
+
|
|
344
|
+
if msg_type in ["result", "task_result"]: # Handle both result types
|
|
345
|
+
result_content = message.get("result") or message.get("description") # Try both fields
|
|
346
|
+
if task_id and result_content is not None:
|
|
347
|
+
print(f"[Coordinator {self.coordinator.name}] Storing result for task {task_id}")
|
|
348
|
+
# Store result in both the group chat and coordinator
|
|
349
|
+
self.task_results[task_id] = result_content
|
|
350
|
+
self.coordinator.task_results[task_id] = result_content
|
|
351
|
+
print(f"[Coordinator {self.coordinator.name}] Stored result: {result_content[:100]}...")
|
|
352
|
+
print(f"[Coordinator {self.coordinator.name}] Current task results: {list(self.task_results.keys())}")
|
|
353
|
+
print(f"[Coordinator {self.coordinator.name}] Current dependencies: {self.task_dependencies}")
|
|
354
|
+
|
|
355
|
+
# Acknowledge the message
|
|
356
|
+
try:
|
|
357
|
+
if message_id: # Only acknowledge if we have a message ID
|
|
358
|
+
await self.coordinator.transport.acknowledge_message(self.coordinator.name, message_id)
|
|
359
|
+
print(f"[Coordinator {self.coordinator.name}] Acknowledged message {message_id}")
|
|
360
|
+
except Exception as e:
|
|
361
|
+
print(f"[Coordinator {self.coordinator.name}] Error acknowledging message {message_id}: {e}")
|
|
362
|
+
else:
|
|
363
|
+
print(f"[Coordinator {self.coordinator.name}] Received invalid result message (missing task_id or result): {message}")
|
|
364
|
+
elif msg_type == "get_result": # Handle get result request
|
|
365
|
+
result = None
|
|
366
|
+
if task_id in self.task_results:
|
|
367
|
+
result = self.task_results[task_id]
|
|
368
|
+
elif task_id in self.coordinator.task_results:
|
|
369
|
+
result = self.coordinator.task_results[task_id]
|
|
370
|
+
|
|
371
|
+
if result:
|
|
372
|
+
print(f"[Coordinator {self.coordinator.name}] Found result for task {task_id}")
|
|
373
|
+
# Send result back
|
|
374
|
+
try:
|
|
375
|
+
await self.coordinator.transport.send_message(
|
|
376
|
+
f"{self.server_url}/message/{message.get('sender', 'unknown')}",
|
|
377
|
+
{
|
|
378
|
+
"type": "task_result",
|
|
379
|
+
"task_id": task_id,
|
|
380
|
+
"result": result
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
print(f"[Coordinator {self.coordinator.name}] Sent result for task {task_id}")
|
|
384
|
+
except Exception as e:
|
|
385
|
+
print(f"[Coordinator {self.coordinator.name}] Error sending result: {e}")
|
|
386
|
+
else:
|
|
387
|
+
print(f"[Coordinator {self.coordinator.name}] No result found for task {task_id}")
|
|
388
|
+
else:
|
|
389
|
+
print(f"[Coordinator {self.coordinator.name}] Received unhandled message type '{msg_type}': {message}")
|
|
390
|
+
# Optionally, acknowledge other messages too or handle errors
|
|
391
|
+
try:
|
|
392
|
+
await self.coordinator.transport.acknowledge_message(message_id)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
print(f"[Coordinator {self.coordinator.name}] Error acknowledging message {message_id}: {e}")
|
|
395
|
+
|
|
396
|
+
async def shutdown(self):
|
|
397
|
+
"""Gracefully disconnect all agents and cancel their tasks."""
|
|
398
|
+
print(f"Initiating shutdown for {len(self._agent_tasks)} agent tasks...")
|
|
399
|
+
|
|
400
|
+
# 1. Cancel all running agent tasks
|
|
401
|
+
for task in self._agent_tasks:
|
|
402
|
+
if task and not task.done():
|
|
403
|
+
print(f"Cancelling task {task.get_name()}...")
|
|
404
|
+
task.cancel()
|
|
405
|
+
|
|
406
|
+
# Wait for all tasks to be cancelled
|
|
407
|
+
if self._agent_tasks:
|
|
408
|
+
await asyncio.gather(*[t for t in self._agent_tasks if t], return_exceptions=True)
|
|
409
|
+
print("All agent tasks cancelled or finished.")
|
|
410
|
+
self._agent_tasks.clear() # Clear the list of tasks
|
|
411
|
+
|
|
412
|
+
# 2. Disconnect transports for all agents (coordinator + regular agents)
|
|
413
|
+
all_agents = [self.coordinator] + self.agents
|
|
414
|
+
disconnect_tasks = []
|
|
415
|
+
for agent in all_agents:
|
|
416
|
+
if hasattr(agent, 'transport') and hasattr(agent.transport, 'disconnect'):
|
|
417
|
+
print(f"Disconnecting transport for {agent.name}...")
|
|
418
|
+
disconnect_tasks.append(agent.transport.disconnect())
|
|
419
|
+
|
|
420
|
+
if disconnect_tasks:
|
|
421
|
+
await asyncio.gather(*disconnect_tasks, return_exceptions=True)
|
|
422
|
+
print("All agent transports disconnected.")
|
|
423
|
+
|
|
424
|
+
print("Shutdown complete.")
|