agent-mcp 0.1.3__py3-none-any.whl → 0.1.5__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 +66 -12
- agent_mcp/a2a_protocol.py +316 -0
- agent_mcp/agent_lightning_library.py +214 -0
- agent_mcp/camel_mcp_adapter.py +521 -0
- agent_mcp/claude_mcp_adapter.py +195 -0
- agent_mcp/cli.py +47 -0
- agent_mcp/google_ai_mcp_adapter.py +183 -0
- agent_mcp/heterogeneous_group_chat.py +412 -38
- agent_mcp/langchain_mcp_adapter.py +176 -43
- agent_mcp/llamaindex_mcp_adapter.py +410 -0
- agent_mcp/mcp_agent.py +26 -0
- agent_mcp/mcp_transport.py +11 -5
- agent_mcp/microsoft_agent_framework.py +591 -0
- agent_mcp/missing_frameworks.py +435 -0
- agent_mcp/openapi_protocol.py +616 -0
- agent_mcp/payments.py +804 -0
- agent_mcp/pydantic_ai_mcp_adapter.py +628 -0
- agent_mcp/registry.py +768 -0
- agent_mcp/security.py +864 -0
- {agent_mcp-0.1.3.dist-info → agent_mcp-0.1.5.dist-info}/METADATA +173 -49
- agent_mcp-0.1.5.dist-info/RECORD +62 -0
- {agent_mcp-0.1.3.dist-info → agent_mcp-0.1.5.dist-info}/WHEEL +1 -1
- agent_mcp-0.1.5.dist-info/entry_points.txt +4 -0
- agent_mcp-0.1.5.dist-info/top_level.txt +3 -0
- demos/__init__.py +1 -0
- demos/basic/__init__.py +1 -0
- demos/basic/framework_examples.py +108 -0
- demos/basic/langchain_camel_demo.py +272 -0
- demos/basic/simple_chat.py +355 -0
- demos/basic/simple_integration_example.py +51 -0
- demos/collaboration/collaborative_task_example.py +437 -0
- demos/collaboration/group_chat_example.py +130 -0
- demos/collaboration/simplified_crewai_example.py +39 -0
- demos/comprehensive_framework_demo.py +202 -0
- demos/langgraph/autonomous_langgraph_network.py +808 -0
- demos/langgraph/langgraph_agent_network.py +415 -0
- demos/langgraph/langgraph_collaborative_task.py +619 -0
- demos/langgraph/langgraph_example.py +227 -0
- demos/langgraph/run_langgraph_examples.py +213 -0
- demos/network/agent_network_example.py +381 -0
- demos/network/email_agent.py +130 -0
- demos/network/email_agent_demo.py +46 -0
- demos/network/heterogeneous_network_example.py +216 -0
- demos/network/multi_framework_example.py +199 -0
- demos/utils/check_imports.py +49 -0
- demos/workflows/autonomous_agent_workflow.py +248 -0
- demos/workflows/mcp_features_demo.py +353 -0
- demos/workflows/run_agent_collaboration_demo.py +63 -0
- demos/workflows/run_agent_collaboration_with_logs.py +396 -0
- demos/workflows/show_agent_interactions.py +107 -0
- demos/workflows/simplified_autonomous_demo.py +74 -0
- functions/main.py +144 -0
- functions/mcp_network_server.py +513 -0
- functions/utils.py +47 -0
- agent_mcp-0.1.3.dist-info/RECORD +0 -18
- agent_mcp-0.1.3.dist-info/entry_points.txt +0 -2
- agent_mcp-0.1.3.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter for Camel AI agents to work with MCP.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from .mcp_agent import MCPAgent
|
|
8
|
+
from .mcp_transport import MCPTransport
|
|
9
|
+
from camel.agents import ChatAgent # Import the core Camel AI agent
|
|
10
|
+
import traceback
|
|
11
|
+
import json
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
# --- Setup Logger ---
|
|
15
|
+
import logging
|
|
16
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
# --- End Logger Setup ---
|
|
19
|
+
|
|
20
|
+
class CamelMCPAdapter(MCPAgent):
|
|
21
|
+
"""Adapter for Camel AI ChatAgent to work with MCP"""
|
|
22
|
+
|
|
23
|
+
def __init__(self,
|
|
24
|
+
name: str,
|
|
25
|
+
transport: Optional[MCPTransport] = None,
|
|
26
|
+
client_mode: bool = False,
|
|
27
|
+
camel_agent: ChatAgent = None, # Expect a Camel ChatAgent instance
|
|
28
|
+
system_message: str = "",
|
|
29
|
+
**kwargs):
|
|
30
|
+
# Set default system message if none provided and agent doesn't have one
|
|
31
|
+
if not system_message and camel_agent and hasattr(camel_agent, 'system_message') and not camel_agent.system_message:
|
|
32
|
+
system_message = "I am a Camel AI agent ready to assist."
|
|
33
|
+
elif not system_message:
|
|
34
|
+
system_message = "I am a Camel AI agent ready to assist."
|
|
35
|
+
|
|
36
|
+
# Initialize parent with system message
|
|
37
|
+
# Use provided system_message or agent's if available
|
|
38
|
+
effective_system_message = system_message or (camel_agent.system_message.content if camel_agent and camel_agent.system_message else "Camel AI Assistant")
|
|
39
|
+
super().__init__(name=name, system_message=effective_system_message, **kwargs)
|
|
40
|
+
|
|
41
|
+
# Set instance attributes
|
|
42
|
+
self.transport = transport
|
|
43
|
+
self.client_mode = client_mode
|
|
44
|
+
self.camel_agent = camel_agent # Store the Camel ChatAgent
|
|
45
|
+
self.task_queue = asyncio.Queue()
|
|
46
|
+
self._task_processor = None
|
|
47
|
+
self._message_processor = None
|
|
48
|
+
self._processed_tasks = set() # For idempotency check
|
|
49
|
+
|
|
50
|
+
if not self.camel_agent:
|
|
51
|
+
raise ValueError("A camel.agents.ChatAgent instance must be provided.")
|
|
52
|
+
|
|
53
|
+
async def connect_to_server(self, server_url: str):
|
|
54
|
+
"""Connect to another agent's server"""
|
|
55
|
+
if not self.client_mode or not self.transport:
|
|
56
|
+
raise ValueError("Agent not configured for client mode")
|
|
57
|
+
|
|
58
|
+
# Register with the server
|
|
59
|
+
registration = {
|
|
60
|
+
"type": "registration",
|
|
61
|
+
"agent_id": self.mcp_id,
|
|
62
|
+
"name": self.name,
|
|
63
|
+
"capabilities": [] # Define capabilities based on agent if needed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
response = await self.transport.send_message(server_url, registration)
|
|
67
|
+
if response.get("status") == "ok":
|
|
68
|
+
print(f"Successfully connected to server at {server_url}")
|
|
69
|
+
# transport message format
|
|
70
|
+
async def handle_incoming_message(self, message: Dict[str, Any], message_id: Optional[str] = None):
|
|
71
|
+
"""Handle incoming messages from other agents"""
|
|
72
|
+
# First check if type is directly in the message
|
|
73
|
+
msg_type = message.get("type")
|
|
74
|
+
logger.info(f"[{self.name}] Raw message: {message}")
|
|
75
|
+
|
|
76
|
+
# If not, check if it's inside the content field
|
|
77
|
+
if not msg_type and "content" in message and isinstance(message["content"], dict):
|
|
78
|
+
msg_type = message["content"].get("type")
|
|
79
|
+
|
|
80
|
+
# Extract sender with nested JSON support
|
|
81
|
+
sender = self._extract_sender(message)
|
|
82
|
+
task_id = message.get("task_id") or message.get("content", {}).get("task_id") if isinstance(message.get("content"), dict) else message.get("task_id")
|
|
83
|
+
logger.info(f"[{self.name}] Received message (ID: {message_id}) of type '{msg_type}' from {sender} (Task ID: {task_id})")
|
|
84
|
+
|
|
85
|
+
# --- Idempotency Check ---
|
|
86
|
+
if not super()._should_process_message(message):
|
|
87
|
+
# If skipped, acknowledge and stop
|
|
88
|
+
if message_id and self.transport:
|
|
89
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
90
|
+
logger.info(f"[{self.name}] Acknowledged duplicate task {task_id} (msg_id: {message_id})")
|
|
91
|
+
return
|
|
92
|
+
# --- End Idempotency Check ---
|
|
93
|
+
|
|
94
|
+
if msg_type == "task":
|
|
95
|
+
logger.info(f"[{self.name}] Queueing task {task_id} (message_id: {message_id}) from {sender}")
|
|
96
|
+
content = message.get("content", {})
|
|
97
|
+
# Ensure task_id and description are retrieved correctly from nested content if necessary
|
|
98
|
+
current_task_id = content.get("task_id", message.get("task_id"))
|
|
99
|
+
description = content.get("description", message.get("description"))
|
|
100
|
+
reply_to = content.get("reply_to", message.get("reply_to"))
|
|
101
|
+
|
|
102
|
+
if not current_task_id or not description:
|
|
103
|
+
logger.error(f"[{self.name}] Task message missing required fields (task_id or description): {message}")
|
|
104
|
+
# Acknowledge if possible, even if invalid, to prevent reprocessing
|
|
105
|
+
if message_id and self.transport:
|
|
106
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Add message_id to task context for later acknowledgement
|
|
110
|
+
task_context = {
|
|
111
|
+
"type": "task", # Explicitly set type for process_tasks
|
|
112
|
+
"task_id": current_task_id,
|
|
113
|
+
"description": description,
|
|
114
|
+
"reply_to": reply_to,
|
|
115
|
+
"sender": sender,
|
|
116
|
+
"message_id": message_id, # Store message_id for acknowledgement
|
|
117
|
+
"original_message": message # Store original for context if needed
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
logger.debug(f"[{self.name}] Queueing task context: {task_context}")
|
|
121
|
+
await self.task_queue.put(task_context)
|
|
122
|
+
logger.debug(f"[{self.name}] Successfully queued task {current_task_id}")
|
|
123
|
+
|
|
124
|
+
elif msg_type == "task_result":
|
|
125
|
+
# Received a result, treat it as the next step in the conversation
|
|
126
|
+
result_content = message.get("result")
|
|
127
|
+
|
|
128
|
+
# --- Robust extraction for various formats ---
|
|
129
|
+
content = message.get("content")
|
|
130
|
+
if result_content is None and content is not None:
|
|
131
|
+
# 1. Try content["result"]
|
|
132
|
+
if isinstance(content, dict) and "result" in content:
|
|
133
|
+
result_content = content["result"]
|
|
134
|
+
# 2. Try content["text"] as JSON
|
|
135
|
+
elif isinstance(content, dict) and "text" in content:
|
|
136
|
+
text_val = content["text"]
|
|
137
|
+
if isinstance(text_val, str):
|
|
138
|
+
try:
|
|
139
|
+
parsed = json.loads(text_val)
|
|
140
|
+
if isinstance(parsed, dict) and "result" in parsed:
|
|
141
|
+
result_content = parsed["result"]
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
# 3. Try content itself as JSON string
|
|
145
|
+
elif isinstance(content, str):
|
|
146
|
+
try:
|
|
147
|
+
parsed = json.loads(content)
|
|
148
|
+
if isinstance(parsed, dict) and "result" in parsed:
|
|
149
|
+
result_content = parsed["result"]
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
# 4. Fallback: use content["text"] as plain string
|
|
153
|
+
if result_content is None and isinstance(content, dict) and "text" in content:
|
|
154
|
+
result_content = content["text"]
|
|
155
|
+
|
|
156
|
+
# Add direct parsing of content["text"] structure
|
|
157
|
+
if isinstance(result_content, str):
|
|
158
|
+
try:
|
|
159
|
+
text_content = json.loads(result_content)
|
|
160
|
+
if isinstance(text_content, dict):
|
|
161
|
+
result_content = text_content
|
|
162
|
+
except json.JSONDecodeError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# Handle JSON string content
|
|
166
|
+
if isinstance(result_content, str):
|
|
167
|
+
try:
|
|
168
|
+
result_content = json.loads(result_content)
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
original_task_id = (
|
|
173
|
+
(result_content.get("task_id") if isinstance(result_content, dict) else None)
|
|
174
|
+
or message.get("task_id")
|
|
175
|
+
)
|
|
176
|
+
logger.info(f"[{self.name}] Received task_result from {sender} for task {original_task_id}. Content: '{str(result_content)[:100]}...'")
|
|
177
|
+
|
|
178
|
+
if not result_content:
|
|
179
|
+
logger.warning(f"[{self.name}] Received task_result from {sender} with empty content.")
|
|
180
|
+
# Acknowledge the result message even if content is empty
|
|
181
|
+
if message_id and self.transport:
|
|
182
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Create a *new* task for this agent based on the received result
|
|
186
|
+
new_task_id = f"conv_{uuid.uuid4()}" # Generate a new ID for this conversational turn
|
|
187
|
+
new_task_context = {
|
|
188
|
+
"type": "task", # Still a task for this agent to process
|
|
189
|
+
"task_id": new_task_id,
|
|
190
|
+
"description": str(result_content), # The result becomes the new input/description
|
|
191
|
+
"reply_to": message.get("reply_to") or result_content.get("reply_to"),
|
|
192
|
+
"sender": sender, # This agent is the conceptual sender of this internal task
|
|
193
|
+
"message_id": message_id # Carry over original message ID for acknowledgement
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
logger.info(f"[{self.name}] Queueing new conversational task {new_task_id} based on result from {sender}")
|
|
197
|
+
await self.task_queue.put(new_task_context)
|
|
198
|
+
logger.debug(f"[{self.name}] Successfully queued new task {new_task_id}")
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
logger.warning(f"[{self.name}] Received unknown message type: {msg_type}. Message: {message}")
|
|
202
|
+
# Acknowledge non-task messages immediately if they have an ID
|
|
203
|
+
if message_id and self.transport:
|
|
204
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
205
|
+
|
|
206
|
+
# message processor loop making sure the message is a dictionary and then processing it
|
|
207
|
+
async def process_messages(self):
|
|
208
|
+
logger.info(f"[{self.name}] Message processor loop started.")
|
|
209
|
+
while True:
|
|
210
|
+
try:
|
|
211
|
+
logger.debug(f"[{self.name}] Waiting for message from transport...")
|
|
212
|
+
# Get message from transport (without agent_name parameter)
|
|
213
|
+
message, message_id = await self.transport.receive_message()
|
|
214
|
+
logger.debug(f"[{self.name}] Received raw message from transport: {message} (ID: {message_id})")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if message is None:
|
|
218
|
+
logger.debug(f"[{self.name}] Received None message, continuing loop...")
|
|
219
|
+
await asyncio.sleep(0.1) # Avoid busy-waiting
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Ensure message is a dictionary before proceeding
|
|
223
|
+
if not isinstance(message, dict):
|
|
224
|
+
logger.error(f"[{self.name}] Received non-dict message: {message} (type: {type(message)}), skipping.")
|
|
225
|
+
# Attempt to acknowledge if possible
|
|
226
|
+
if message_id and self.transport:
|
|
227
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
logger.info(f"[{self.name}] Processing message {message_id}: {message}")
|
|
231
|
+
await self.handle_incoming_message(message, message_id)
|
|
232
|
+
|
|
233
|
+
except asyncio.CancelledError:
|
|
234
|
+
logger.info(f"[{self.name}] Message processor cancelled.")
|
|
235
|
+
break
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"[{self.name}] Error in message processor: {e}", exc_info=True)
|
|
238
|
+
traceback.print_exc()
|
|
239
|
+
# Avoid immediate exit on error, maybe add a delay
|
|
240
|
+
await asyncio.sleep(1)
|
|
241
|
+
logger.info(f"[{self.name}] Message processor loop finished.")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def process_tasks(self):
|
|
245
|
+
logger.info(f"[{self.name}] Task processor loop started.")
|
|
246
|
+
while True:
|
|
247
|
+
try:
|
|
248
|
+
logger.debug(f"[{self.name}] Waiting for task from queue...")
|
|
249
|
+
task_context = await self.task_queue.get()
|
|
250
|
+
logger.info(f"\n[{self.name}] Dequeued item from task queue: {task_context}")
|
|
251
|
+
|
|
252
|
+
if not isinstance(task_context, dict):
|
|
253
|
+
logger.error(f"[{self.name}] Task item is not a dictionary: {task_context}")
|
|
254
|
+
self.task_queue.task_done()
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Extract task details from the context dictionary
|
|
258
|
+
task_desc = task_context.get("description")
|
|
259
|
+
task_id = task_context.get("task_id")
|
|
260
|
+
reply_to = task_context.get("reply_to")
|
|
261
|
+
message_id = task_context.get("message_id") # Retrieve message_id for ack
|
|
262
|
+
task_type = task_context.get("type") # Should be 'task'
|
|
263
|
+
|
|
264
|
+
logger.info(f"[{self.name}] Processing task details:")
|
|
265
|
+
logger.info(f" - Task ID: {task_id}")
|
|
266
|
+
logger.info(f" - Type: {task_type}")
|
|
267
|
+
logger.info(f" - Reply To: {reply_to}")
|
|
268
|
+
logger.info(f" - Description: {str(task_desc)[:100]}...")
|
|
269
|
+
logger.info(f" - Original Message ID: {message_id}")
|
|
270
|
+
|
|
271
|
+
if not task_desc or not task_id:
|
|
272
|
+
logger.error(f"[{self.name}] Task context missing description or task_id: {task_context}")
|
|
273
|
+
self.task_queue.task_done()
|
|
274
|
+
# Acknowledge message even if task context is bad
|
|
275
|
+
if message_id and self.transport:
|
|
276
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
277
|
+
logger.warning(f"[{self.name}] Acknowledged message {message_id} for invalid task context.")
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# Ensure it's actually a task we should process
|
|
281
|
+
if task_type != "task":
|
|
282
|
+
logger.error(f"[{self.name}] Invalid item type in task queue: {task_type}. Item: {task_context}")
|
|
283
|
+
self.task_queue.task_done()
|
|
284
|
+
if message_id and self.transport:
|
|
285
|
+
asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
logger.info(f"[{self.name}] Starting execution of task {task_id}: '{str(task_desc)[:50]}...'" )
|
|
289
|
+
|
|
290
|
+
# --- Execute task using Camel AI agent ---
|
|
291
|
+
result_str = None
|
|
292
|
+
try:
|
|
293
|
+
logger.debug(f"[{self.name}] Calling camel_agent.astep with task description...")
|
|
294
|
+
# Use astep for asynchronous execution if available, otherwise step
|
|
295
|
+
if hasattr(self.camel_agent, 'astep'):
|
|
296
|
+
response = await self.camel_agent.astep(task_desc)
|
|
297
|
+
else:
|
|
298
|
+
# Fallback to synchronous step in executor if astep not available
|
|
299
|
+
# Note: This blocks the task processor loop. Consider running in executor.
|
|
300
|
+
logger.warning(f"[{self.name}] Camel agent does not have 'astep'. Using synchronous 'step'. This may block.")
|
|
301
|
+
# For safety, run sync 'step' in an executor to avoid blocking asyncio loop
|
|
302
|
+
loop = asyncio.get_running_loop()
|
|
303
|
+
response = await loop.run_in_executor(None, self.camel_agent.step, task_desc)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Extract content from the response object
|
|
307
|
+
# Assuming response has a 'msg' attribute with 'content' based on docs example
|
|
308
|
+
if response and hasattr(response, 'msg') and hasattr(response.msg, 'content'):
|
|
309
|
+
result_str = response.msg.content
|
|
310
|
+
logger.debug(f"[{self.name}] Camel agent execution successful. Result: '{str(result_str)[:100]}...'" )
|
|
311
|
+
elif response and hasattr(response, 'messages') and response.messages:
|
|
312
|
+
# Handle cases where response might be a list of messages (e.g., RolePlaying)
|
|
313
|
+
# Take the last message content as the result for simplicity
|
|
314
|
+
last_msg = response.messages[-1]
|
|
315
|
+
if hasattr(last_msg, 'content'):
|
|
316
|
+
result_str = last_msg.content
|
|
317
|
+
logger.debug(f"[{self.name}] Camel agent execution successful (from messages). Result: '{str(result_str)[:100]}...'" )
|
|
318
|
+
else:
|
|
319
|
+
logger.warning(f"[{self.name}] Camel agent response's last message has no 'content'. Response: {response}")
|
|
320
|
+
result_str = str(response) # Fallback
|
|
321
|
+
elif response:
|
|
322
|
+
logger.warning(f"[{self.name}] Camel agent response format unexpected. Using str(response). Response: {response}")
|
|
323
|
+
result_str = str(response) # Fallback to string representation
|
|
324
|
+
else:
|
|
325
|
+
logger.warning(f"[{self.name}] Camel agent returned None or empty response.")
|
|
326
|
+
result_str = "Agent returned no response."
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(f"[{self.name}] Camel agent execution failed for task {task_id}: {e}", exc_info=True)
|
|
331
|
+
traceback.print_exc()
|
|
332
|
+
result_str = f"Agent execution failed due to an error: {str(e)}"
|
|
333
|
+
|
|
334
|
+
# Ensure result is always a string before sending
|
|
335
|
+
if not isinstance(result_str, str):
|
|
336
|
+
try:
|
|
337
|
+
result_str = json.dumps(result_str) # Try serializing complex types
|
|
338
|
+
except (TypeError, OverflowError):
|
|
339
|
+
result_str = str(result_str) # Fallback
|
|
340
|
+
|
|
341
|
+
logger.info(f"[{self.name}] Sending result for task {task_id} to {reply_to}")
|
|
342
|
+
# --- Send the result back ---
|
|
343
|
+
if reply_to and self.transport:
|
|
344
|
+
try:
|
|
345
|
+
# Extract target agent name from reply_to (assuming format http://.../agent_name)
|
|
346
|
+
target_agent_name = reply_to # Default if parsing fails
|
|
347
|
+
try:
|
|
348
|
+
target_agent_name = reply_to.split('/')[-1]
|
|
349
|
+
if not target_agent_name: # Handle trailing slash case
|
|
350
|
+
target_agent_name = reply_to.split('/')[-2]
|
|
351
|
+
except IndexError:
|
|
352
|
+
logger.warning(f"[{self.name}] Could not reliably extract agent name from reply_to URL: {reply_to}. Using full URL.")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
logger.debug(f"[{self.name}] Sending result to target agent: {target_agent_name} (extracted from {reply_to})")
|
|
356
|
+
|
|
357
|
+
await self.transport.send_message(
|
|
358
|
+
target_agent_name, # Send to the specific agent name/ID
|
|
359
|
+
{
|
|
360
|
+
"type": "task_result",
|
|
361
|
+
"task_id": task_id,
|
|
362
|
+
"result": result_str,
|
|
363
|
+
"sender": self.name,
|
|
364
|
+
"reply_to": self.name, # Add this so the receiving agent knows where to send follow-up messages
|
|
365
|
+
"original_message_id": message_id # Include original message ID for tracing/ack
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
logger.debug(f"[{self.name}] Result sent successfully for task {task_id}")
|
|
369
|
+
|
|
370
|
+
# Acknowledge task completion *after* sending result, using message_id
|
|
371
|
+
if message_id:
|
|
372
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
373
|
+
logger.info(f"[{self.name}] Task {task_id} acknowledged via message_id {message_id}")
|
|
374
|
+
else:
|
|
375
|
+
logger.warning(f"[{self.name}] No message_id found for task {task_id} in context, cannot acknowledge transport message.")
|
|
376
|
+
|
|
377
|
+
except Exception as send_error:
|
|
378
|
+
logger.error(f"[{self.name}] Failed to send result or acknowledge for task {task_id}: {send_error}", exc_info=True)
|
|
379
|
+
traceback.print_exc()
|
|
380
|
+
# Decide if task should be retried or marked done despite send failure
|
|
381
|
+
# For now, mark done to avoid loop, but log error clearly
|
|
382
|
+
else:
|
|
383
|
+
logger.warning(f"[{self.name}] No reply_to address or transport configured for task {task_id}. Cannot send result or acknowledge.")
|
|
384
|
+
# If no reply_to, we might still need to acknowledge if we have a message_id
|
|
385
|
+
if message_id and self.transport:
|
|
386
|
+
logger.warning(f"[{self.name}] Acknowledging message {message_id} for task {task_id} even though result wasn't sent (no reply_to)." )
|
|
387
|
+
await self.transport.acknowledge_message(self.name, message_id)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# Mark task as completed internally *after* processing and attempting send/ack
|
|
391
|
+
super()._mark_task_completed(task_id) # Call base class method
|
|
392
|
+
|
|
393
|
+
self.task_queue.task_done()
|
|
394
|
+
logger.info(f"[{self.name}] Task {task_id} fully processed and marked done.")
|
|
395
|
+
|
|
396
|
+
except asyncio.CancelledError:
|
|
397
|
+
logger.info(f"[{self.name}] Task processor cancelled.")
|
|
398
|
+
break
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"[{self.name}] Unhandled error in task processing loop: {e}", exc_info=True)
|
|
401
|
+
traceback.print_exc()
|
|
402
|
+
# Ensure task_done is called even in unexpected error to prevent queue blockage
|
|
403
|
+
try:
|
|
404
|
+
self.task_queue.task_done()
|
|
405
|
+
except ValueError: # Already done
|
|
406
|
+
pass
|
|
407
|
+
await asyncio.sleep(1) # Prevent fast error loop
|
|
408
|
+
|
|
409
|
+
logger.info(f"[{self.name}] Task processor loop finished.")
|
|
410
|
+
|
|
411
|
+
# --- Helper methods inherited or overridden from MCPAgent ---
|
|
412
|
+
# _should_process_message and _mark_task_completed are inherited from MCPAgent
|
|
413
|
+
# but shown here for clarity from the original langchain adapter template.
|
|
414
|
+
# We rely on the super() calls within handle_incoming_message and process_tasks.
|
|
415
|
+
|
|
416
|
+
# def _should_process_message(self, message: Dict[str, Any]) -> bool:
|
|
417
|
+
# """Check if a message should be processed based on idempotency"""
|
|
418
|
+
# # Implementation relies on MCPAgent's base method via super()
|
|
419
|
+
|
|
420
|
+
# def _mark_task_completed(self, task_id: str) -> None:
|
|
421
|
+
# """Mark a task as completed for idempotency tracking"""
|
|
422
|
+
# # Implementation relies on MCPAgent's base method via super()
|
|
423
|
+
|
|
424
|
+
async def run(self):
|
|
425
|
+
"""Start the agent's message and task processing loops."""
|
|
426
|
+
if not self.transport:
|
|
427
|
+
raise ValueError("Transport must be configured to run the agent.")
|
|
428
|
+
|
|
429
|
+
logger.info(f"[{self.name}] Starting CamelMCPAdapter run loop...")
|
|
430
|
+
|
|
431
|
+
# Start the message processing loop
|
|
432
|
+
self._message_processor = asyncio.create_task(self.process_messages())
|
|
433
|
+
# Start the task processing loop
|
|
434
|
+
self._task_processor = asyncio.create_task(self.process_tasks())
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
# Keep running until tasks are cancelled
|
|
438
|
+
await asyncio.gather(self._message_processor, self._task_processor)
|
|
439
|
+
except asyncio.CancelledError:
|
|
440
|
+
logger.info(f"[{self.name}] Agent run loop cancelled.")
|
|
441
|
+
finally:
|
|
442
|
+
logger.info(f"[{self.name}] Agent run loop finished.")
|
|
443
|
+
|
|
444
|
+
async def stop(self):
|
|
445
|
+
"""Stop the agent's processing loops gracefully."""
|
|
446
|
+
logger.info(f"[{self.name}] Stopping agent...")
|
|
447
|
+
if self._message_processor and not self._message_processor.done():
|
|
448
|
+
self._message_processor.cancel()
|
|
449
|
+
if self._task_processor and not self._task_processor.done():
|
|
450
|
+
self._task_processor.cancel()
|
|
451
|
+
|
|
452
|
+
# Wait for tasks to finish cancelling
|
|
453
|
+
try:
|
|
454
|
+
await asyncio.gather(self._message_processor, self._task_processor, return_exceptions=True)
|
|
455
|
+
except asyncio.CancelledError:
|
|
456
|
+
pass # Expected
|
|
457
|
+
logger.info(f"[{self.name}] Agent stopped.")
|
|
458
|
+
|
|
459
|
+
# Example Usage (for demonstration purposes, would normally be in a separate script)
|
|
460
|
+
# async def main():
|
|
461
|
+
# # Requires setting up a transport (e.g., MCPTransport)
|
|
462
|
+
# # Requires creating a Camel ChatAgent instance
|
|
463
|
+
# from camel.models import ModelFactory
|
|
464
|
+
# from camel.types import ModelType
|
|
465
|
+
# import os
|
|
466
|
+
#
|
|
467
|
+
# # Ensure API key is set
|
|
468
|
+
# # os.environ["OPENAI_API_KEY"] = "your_key_here"
|
|
469
|
+
#
|
|
470
|
+
# # 1. Create a Camel ChatAgent
|
|
471
|
+
# model = ModelFactory.create(model_type=ModelType.GPT_4O_MINI)
|
|
472
|
+
# camel_agent_instance = ChatAgent(system_message="You are a helpful geography expert.", model=model)
|
|
473
|
+
#
|
|
474
|
+
# # 2. Create a transport (replace with actual transport implementation)
|
|
475
|
+
# class MockTransport(MCPTransport): # Replace with your actual transport
|
|
476
|
+
# async def send_message(self, target: str, message: Dict[str, Any]):
|
|
477
|
+
# print(f"[MockTransport] Sending to {target}: {message}")
|
|
478
|
+
# # Simulate response for registration
|
|
479
|
+
# if message.get("type") == "registration":
|
|
480
|
+
# return {"status": "ok"}
|
|
481
|
+
# return {"status": "sent"}
|
|
482
|
+
# async def receive_message(self):
|
|
483
|
+
# print("[MockTransport] receive_message called, simulating task...")
|
|
484
|
+
# await asyncio.sleep(5) # Simulate delay
|
|
485
|
+
# # Simulate receiving a task message
|
|
486
|
+
# return {
|
|
487
|
+
# "type": "task",
|
|
488
|
+
# "task_id": "task-123",
|
|
489
|
+
# "description": "What is the capital of France?",
|
|
490
|
+
# "sender": "user_agent",
|
|
491
|
+
# "reply_to": "http://localhost:8000/user_agent" # Example reply URL
|
|
492
|
+
# }, "msg-abc" # Message and ID
|
|
493
|
+
# async def acknowledge_message(self, agent_id: str, message_id: str):
|
|
494
|
+
# print(f"[MockTransport] Acknowledging message {message_id} for agent {agent_id}")
|
|
495
|
+
# async def connect(self): pass
|
|
496
|
+
# async def disconnect(self): pass
|
|
497
|
+
#
|
|
498
|
+
# transport_instance = MockTransport()
|
|
499
|
+
#
|
|
500
|
+
# # 3. Create the adapter
|
|
501
|
+
# adapter = CamelMCPAdapter(
|
|
502
|
+
# name="CamelGeographyAgent",
|
|
503
|
+
# transport=transport_instance,
|
|
504
|
+
# camel_agent=camel_agent_instance
|
|
505
|
+
# )
|
|
506
|
+
#
|
|
507
|
+
# # 4. Run the adapter
|
|
508
|
+
# print("Starting CamelMCPAdapter...")
|
|
509
|
+
# try:
|
|
510
|
+
# await adapter.run()
|
|
511
|
+
# except KeyboardInterrupt:
|
|
512
|
+
# await adapter.stop()
|
|
513
|
+
# print("Adapter finished.")
|
|
514
|
+
#
|
|
515
|
+
# if __name__ == "__main__":
|
|
516
|
+
# # Note: Requires OPENAI_API_KEY environment variable to be set
|
|
517
|
+
# # if os.getenv("OPENAI_API_KEY"):
|
|
518
|
+
# # asyncio.run(main())
|
|
519
|
+
# # else:
|
|
520
|
+
# # print("Please set the OPENAI_API_KEY environment variable to run the example.")
|
|
521
|
+
# pass # Keep example code commented out in the main file
|