agent-mcp 0.1.2__py3-none-any.whl → 0.1.4__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/camel_mcp_adapter.py +521 -0
- agent_mcp/cli.py +47 -0
- agent_mcp/crewai_mcp_adapter.py +281 -0
- agent_mcp/enhanced_mcp_agent.py +601 -0
- agent_mcp/heterogeneous_group_chat.py +798 -0
- agent_mcp/langchain_mcp_adapter.py +458 -0
- agent_mcp/langgraph_mcp_adapter.py +325 -0
- agent_mcp/mcp_agent.py +658 -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 +706 -0
- agent_mcp/mcp_transport_enhanced.py +46 -0
- agent_mcp/proxy_agent.py +24 -0
- agent_mcp-0.1.4.dist-info/METADATA +333 -0
- agent_mcp-0.1.4.dist-info/RECORD +49 -0
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.4.dist-info}/WHEEL +1 -1
- agent_mcp-0.1.4.dist-info/entry_points.txt +2 -0
- agent_mcp-0.1.4.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/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.2.dist-info/METADATA +0 -475
- agent_mcp-0.1.2.dist-info/RECORD +0 -5
- agent_mcp-0.1.2.dist-info/entry_points.txt +0 -2
- agent_mcp-0.1.2.dist-info/top_level.txt +0 -1
agent_mcp/mcp_agent.py
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCPAgent - An AutoGen agent with Model Context Protocol capabilities.
|
|
3
|
+
|
|
4
|
+
This module provides a transparent implementation of the Model Context Protocol
|
|
5
|
+
for AutoGen agents, allowing them to standardize context provision to LLMs and
|
|
6
|
+
interact with other MCP-capable systems with minimal configuration.
|
|
7
|
+
|
|
8
|
+
The Model Context Protocol (MCP) is a standardized way for AI agents to share and
|
|
9
|
+
manage context information. This implementation extends AutoGen's ConversableAgent
|
|
10
|
+
to provide MCP capabilities including:
|
|
11
|
+
|
|
12
|
+
- Context Management: Store and retrieve contextual information
|
|
13
|
+
- Tool Registration: Register and manage MCP-compatible tools
|
|
14
|
+
- Standardized Communication: Interact with other MCP agents seamlessly
|
|
15
|
+
- Task Tracking: Track completed tasks for idempotency
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> agent = MCPAgent(name="my_agent")
|
|
19
|
+
>>> agent.register_mcp_tool(name="my_tool", description="Does something", func=my_func)
|
|
20
|
+
>>> agent.context_set("key", "value")
|
|
21
|
+
>>> context = agent.context_get("key")
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
context_store (Dict): Central store for agent's contextual information
|
|
25
|
+
mcp_tools (Dict): Registry of MCP-compatible tools available to the agent
|
|
26
|
+
mcp_id (str): Unique identifier for this MCP agent instance
|
|
27
|
+
mcp_version (str): Version of MCP protocol implemented
|
|
28
|
+
completed_task_ids (set): Set of completed task IDs for idempotency
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import uuid
|
|
33
|
+
import inspect
|
|
34
|
+
from typing import Any, Dict, List, Optional, Union, Callable, Tuple
|
|
35
|
+
import logging
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
# Setup basic logging
|
|
39
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# Import AutoGen
|
|
43
|
+
from autogen import ConversableAgent, Agent
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MCPAgent(ConversableAgent):
|
|
47
|
+
"""
|
|
48
|
+
An AutoGen agent with Model Context Protocol capabilities.
|
|
49
|
+
|
|
50
|
+
This agent extends the ConversableAgent to implement the Model Context Protocol,
|
|
51
|
+
enabling standardized context provision to LLMs and seamless interaction with
|
|
52
|
+
other MCP-capable systems.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
context_store (Dict): Store for the agent's current context
|
|
56
|
+
mcp_tools (Dict): Dictionary of MCP tools available to this agent
|
|
57
|
+
mcp_id (str): Unique identifier for this MCP agent
|
|
58
|
+
mcp_version (str): The MCP version implemented by this agent
|
|
59
|
+
completed_task_ids (set): Set of completed task IDs for idempotency
|
|
60
|
+
transport (Any): Optional transport layer for MCP communication
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
name: str,
|
|
66
|
+
system_message: Optional[str] = None,
|
|
67
|
+
is_termination_msg: Optional[Callable[[Dict], bool]] = None,
|
|
68
|
+
max_consecutive_auto_reply: Optional[int] = None,
|
|
69
|
+
human_input_mode: str = "NEVER",
|
|
70
|
+
transport: Optional[Any] = None,
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize an MCPAgent.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: The name of the agent
|
|
78
|
+
system_message: System message for the agent
|
|
79
|
+
is_termination_msg: Function to determine if a message should terminate a conversation
|
|
80
|
+
max_consecutive_auto_reply: Maximum number of consecutive automated replies
|
|
81
|
+
human_input_mode: Human input mode setting
|
|
82
|
+
transport: Optional transport layer for MCP communication
|
|
83
|
+
**kwargs: Additional keyword arguments passed to ConversableAgent
|
|
84
|
+
"""
|
|
85
|
+
if system_message is None:
|
|
86
|
+
system_message = (
|
|
87
|
+
"You are an AI assistant that follows the Model Context Protocol (MCP). "
|
|
88
|
+
"You can access and manipulate context through the provided MCP tools. "
|
|
89
|
+
"Use these tools to enhance your responses with relevant information."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Initialize ConversableAgent without transport
|
|
93
|
+
super().__init__(
|
|
94
|
+
name=name,
|
|
95
|
+
system_message=system_message,
|
|
96
|
+
is_termination_msg=is_termination_msg,
|
|
97
|
+
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
|
98
|
+
human_input_mode=human_input_mode,
|
|
99
|
+
**kwargs,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# MCP specific attributes
|
|
103
|
+
self.context_store = {}
|
|
104
|
+
self.mcp_tools = {}
|
|
105
|
+
self.mcp_id = str(uuid.uuid4())
|
|
106
|
+
self.mcp_version = "0.1.0" # MCP version implemented
|
|
107
|
+
self.completed_task_ids = set() # Set of completed task IDs for idempotency
|
|
108
|
+
self.transport = transport # Store transport at MCPAgent level
|
|
109
|
+
|
|
110
|
+
# Register default MCP tools
|
|
111
|
+
self._register_default_mcp_tools()
|
|
112
|
+
|
|
113
|
+
def _register_default_mcp_tools(self):
|
|
114
|
+
"""Register default MCP tools that are available to all MCP agents."""
|
|
115
|
+
|
|
116
|
+
# Context management tools
|
|
117
|
+
def context_get(key: str) -> Dict:
|
|
118
|
+
"""Get a context item by key."""
|
|
119
|
+
return self._mcp_context_get(key)
|
|
120
|
+
|
|
121
|
+
def context_set(key: str, value: Any) -> Dict:
|
|
122
|
+
"""Set a context item with the given key and value."""
|
|
123
|
+
return self._mcp_context_set(key, value)
|
|
124
|
+
|
|
125
|
+
def context_list() -> Dict:
|
|
126
|
+
"""List all available context keys."""
|
|
127
|
+
return self._mcp_context_list()
|
|
128
|
+
|
|
129
|
+
def context_remove(key: str) -> Dict:
|
|
130
|
+
"""Remove a context item by key."""
|
|
131
|
+
return self._mcp_context_remove(key)
|
|
132
|
+
|
|
133
|
+
def mcp_info() -> Dict:
|
|
134
|
+
"""Get information about this MCP agent's capabilities."""
|
|
135
|
+
return self._mcp_info()
|
|
136
|
+
|
|
137
|
+
# Register the tools with valid names for AutoGen (only letters, numbers, underscore, dash)
|
|
138
|
+
self.register_mcp_tool(
|
|
139
|
+
name="context_get",
|
|
140
|
+
description="Get a specific context item by key",
|
|
141
|
+
func=context_get,
|
|
142
|
+
key_description="The key of the context item to retrieve"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
self.register_mcp_tool(
|
|
146
|
+
name="context_set",
|
|
147
|
+
description="Set a context item with the given key and value",
|
|
148
|
+
func=context_set,
|
|
149
|
+
key_description="The key to store the value under",
|
|
150
|
+
value_description="The value to store"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self.register_mcp_tool(
|
|
154
|
+
name="context_list",
|
|
155
|
+
description="List all available context keys",
|
|
156
|
+
func=context_list
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.register_mcp_tool(
|
|
160
|
+
name="context_remove",
|
|
161
|
+
description="Remove a context item by key",
|
|
162
|
+
func=context_remove,
|
|
163
|
+
key_description="The key of the context item to remove"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Metadata tools
|
|
167
|
+
self.register_mcp_tool(
|
|
168
|
+
name="mcp_info",
|
|
169
|
+
description="Get information about this MCP agent's capabilities",
|
|
170
|
+
func=mcp_info
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def register_mcp_tool(
|
|
174
|
+
self, name: str, description: str, func: Callable, **kwargs
|
|
175
|
+
) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Register an MCP tool with this agent.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: The name of the tool, used for invocation
|
|
181
|
+
description: Description of what the tool does
|
|
182
|
+
func: The function to be called when the tool is invoked
|
|
183
|
+
**kwargs: Additional tool configuration
|
|
184
|
+
"""
|
|
185
|
+
if name in self.mcp_tools:
|
|
186
|
+
print(f"Warning: Overriding existing MCP tool '{name}'")
|
|
187
|
+
|
|
188
|
+
# Inspect function signature to build parameter info
|
|
189
|
+
sig = inspect.signature(func)
|
|
190
|
+
params = []
|
|
191
|
+
|
|
192
|
+
for param_name, param in sig.parameters.items():
|
|
193
|
+
if param_name == 'self':
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
param_info = {
|
|
197
|
+
"name": param_name,
|
|
198
|
+
"description": kwargs.get(f"{param_name}_description", f"Parameter {param_name}"),
|
|
199
|
+
"required": param.default == inspect.Parameter.empty
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Add type information if available
|
|
203
|
+
if param.annotation != inspect.Parameter.empty:
|
|
204
|
+
param_info["type"] = str(param.annotation.__name__)
|
|
205
|
+
|
|
206
|
+
params.append(param_info)
|
|
207
|
+
|
|
208
|
+
# Register the tool
|
|
209
|
+
self.mcp_tools[name] = {
|
|
210
|
+
"name": name,
|
|
211
|
+
"description": description,
|
|
212
|
+
"parameters": params,
|
|
213
|
+
"function": func,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Create a wrapper that calls the function correctly
|
|
217
|
+
# For functions defined within context_management, they already handle self
|
|
218
|
+
def tool_wrapper(**kwargs):
|
|
219
|
+
return func(**kwargs)
|
|
220
|
+
|
|
221
|
+
# Register the tool with AutoGen's function mechanism
|
|
222
|
+
function_schema = {
|
|
223
|
+
"name": name,
|
|
224
|
+
"description": description,
|
|
225
|
+
"parameters": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"properties": {},
|
|
228
|
+
"required": []
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Add parameter descriptions to the schema
|
|
233
|
+
for param in params:
|
|
234
|
+
param_name = param["name"]
|
|
235
|
+
function_schema["parameters"]["properties"][param_name] = {
|
|
236
|
+
"type": param.get("type", "string"),
|
|
237
|
+
"description": param["description"]
|
|
238
|
+
}
|
|
239
|
+
if param["required"]:
|
|
240
|
+
function_schema["parameters"]["required"].append(param_name)
|
|
241
|
+
|
|
242
|
+
# Register with AutoGen - use the simplest form
|
|
243
|
+
self.register_function({name: tool_wrapper})
|
|
244
|
+
|
|
245
|
+
def register_agent_as_tool(self, agent: Agent, name: Optional[str] = None) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Register another agent as a tool that can be called by this agent.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
agent: The agent to register as a tool
|
|
251
|
+
name: Optional custom name for the tool, defaults to agent's name
|
|
252
|
+
"""
|
|
253
|
+
if name is None:
|
|
254
|
+
# Use valid characters for AutoGen
|
|
255
|
+
name = f"agent_{agent.name}"
|
|
256
|
+
|
|
257
|
+
def agent_tool_wrapper(message: str, **kwargs):
|
|
258
|
+
"""Wrapper to call another agent and return its response."""
|
|
259
|
+
response = agent.generate_reply(sender=self, messages=[{"role": "user", "content": message}])
|
|
260
|
+
return {"response": response if response else "No response from agent."}
|
|
261
|
+
|
|
262
|
+
self.register_mcp_tool(
|
|
263
|
+
name=name,
|
|
264
|
+
description=f"Send a message to agent '{agent.name}' and get their response",
|
|
265
|
+
func=agent_tool_wrapper,
|
|
266
|
+
message_description="The message to send to the agent"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# MCP Context Tool Implementations
|
|
270
|
+
def has_context(self, key: str) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Check if a key exists in the agent's context.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
key: The key to check for existence
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
True if the key exists in the context, False otherwise
|
|
279
|
+
"""
|
|
280
|
+
return key in self.context_store
|
|
281
|
+
|
|
282
|
+
def _mcp_context_get(self, key: str) -> Dict:
|
|
283
|
+
"""
|
|
284
|
+
Get a context item by key.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
key: The key of the context item to retrieve
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Dict containing the value or an error message
|
|
291
|
+
"""
|
|
292
|
+
if key in self.context_store:
|
|
293
|
+
return {"status": "success", "value": self.context_store[key]}
|
|
294
|
+
return {"status": "error", "message": f"Key '{key}' not found in context"}
|
|
295
|
+
|
|
296
|
+
def _mcp_context_set(self, key: str, value: Any) -> Dict:
|
|
297
|
+
"""
|
|
298
|
+
Set a context item with the given key and value.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
key: The key to store the value under
|
|
302
|
+
value: The value to store
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Dict indicating success or failure
|
|
306
|
+
"""
|
|
307
|
+
self.context_store[key] = value
|
|
308
|
+
return {"status": "success", "message": f"Context key '{key}' set successfully"}
|
|
309
|
+
|
|
310
|
+
def _mcp_context_list(self) -> Dict:
|
|
311
|
+
"""
|
|
312
|
+
List all available context keys.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Dict containing the list of context keys
|
|
316
|
+
"""
|
|
317
|
+
return {"status": "success", "keys": list(self.context_store.keys())}
|
|
318
|
+
|
|
319
|
+
def _mcp_context_remove(self, key: str) -> Dict:
|
|
320
|
+
"""
|
|
321
|
+
Remove a context item by key.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
key: The key of the context item to remove
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Dict indicating success or failure
|
|
328
|
+
"""
|
|
329
|
+
if key in self.context_store:
|
|
330
|
+
del self.context_store[key]
|
|
331
|
+
return {"status": "success", "message": f"Context key '{key}' removed successfully"}
|
|
332
|
+
return {"status": "error", "message": f"Key '{key}' not found in context"}
|
|
333
|
+
|
|
334
|
+
def _mcp_info(self) -> Dict:
|
|
335
|
+
"""
|
|
336
|
+
Get information about this MCP agent's capabilities.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dict containing MCP agent information
|
|
340
|
+
"""
|
|
341
|
+
return {
|
|
342
|
+
"id": self.mcp_id,
|
|
343
|
+
"name": self.name,
|
|
344
|
+
"version": self.mcp_version,
|
|
345
|
+
"tools": [
|
|
346
|
+
{
|
|
347
|
+
"name": name,
|
|
348
|
+
"description": tool["description"],
|
|
349
|
+
"parameters": tool["parameters"]
|
|
350
|
+
}
|
|
351
|
+
for name, tool in self.mcp_tools.items()
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Override ConversableAgent methods to integrate MCP
|
|
356
|
+
def generate_reply(
|
|
357
|
+
self,
|
|
358
|
+
messages: Optional[List[Dict]] = None,
|
|
359
|
+
sender: Optional[Agent] = None,
|
|
360
|
+
exclude_list: Optional[List[str]] = None,
|
|
361
|
+
**kwargs,
|
|
362
|
+
) -> Union[str, Dict, None]:
|
|
363
|
+
"""
|
|
364
|
+
Generate a reply based on the conversation history and with MCP context.
|
|
365
|
+
|
|
366
|
+
This overrides the base ConversableAgent method to integrate MCP context
|
|
367
|
+
into the generation process.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
messages: Optional list of messages to process
|
|
371
|
+
sender: The sender agent of the message
|
|
372
|
+
exclude_list: List of function names to exclude from auto-function calling
|
|
373
|
+
**kwargs: Additional keyword arguments
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
The generated reply
|
|
377
|
+
"""
|
|
378
|
+
# Inject MCP context into the prompt if available
|
|
379
|
+
if messages:
|
|
380
|
+
last_message = messages[-1]
|
|
381
|
+
if "content" in last_message and isinstance(last_message["content"], str):
|
|
382
|
+
# Check if message contains MCP tool calls
|
|
383
|
+
self._process_mcp_tool_calls(last_message)
|
|
384
|
+
|
|
385
|
+
# For LLM-based generation, handle context in a different way
|
|
386
|
+
# For AutoGen, we can't directly modify system_message since it's a property
|
|
387
|
+
if hasattr(self, "llm_config") and self.llm_config:
|
|
388
|
+
context_summary = self._generate_context_summary()
|
|
389
|
+
|
|
390
|
+
if context_summary and messages:
|
|
391
|
+
# Instead of modifying system_message, add context in the message list
|
|
392
|
+
context_msg = {
|
|
393
|
+
"role": "system",
|
|
394
|
+
"content": f"Current context information:\n{context_summary}"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Insert the context message at an appropriate position in the conversation
|
|
398
|
+
if len(messages) > 1:
|
|
399
|
+
# Insert before the last message
|
|
400
|
+
messages = messages[:-1] + [context_msg] + [messages[-1]]
|
|
401
|
+
else:
|
|
402
|
+
# Insert before the only message
|
|
403
|
+
messages = [context_msg] + messages
|
|
404
|
+
|
|
405
|
+
# Call the parent class method to generate the reply
|
|
406
|
+
reply = super().generate_reply(
|
|
407
|
+
messages=messages, sender=sender, exclude_list=exclude_list, **kwargs
|
|
408
|
+
)
|
|
409
|
+
return reply
|
|
410
|
+
|
|
411
|
+
def _mark_task_completed(self, task_id: Optional[str]) -> None:
|
|
412
|
+
"""Mark a task as completed to prevent duplicate processing.
|
|
413
|
+
|
|
414
|
+
This method is used for idempotency to ensure tasks are not processed multiple times.
|
|
415
|
+
The task ID is stored in a set for efficient lookup.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
task_id: The unique identifier of the task to mark as completed
|
|
419
|
+
"""
|
|
420
|
+
if task_id:
|
|
421
|
+
self.completed_task_ids.add(task_id)
|
|
422
|
+
logger.info(f"[{self.name}] Marked task_id {task_id} as completed")
|
|
423
|
+
|
|
424
|
+
def _generate_context_summary(self) -> str:
|
|
425
|
+
"""Generate a summary of available context for inclusion in the system message.
|
|
426
|
+
|
|
427
|
+
This method creates a human-readable summary of the current context store,
|
|
428
|
+
handling different types of values appropriately (dictionaries, lists, long strings).
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
A formatted string containing a summary of all context items
|
|
432
|
+
"""
|
|
433
|
+
if not self.context_store:
|
|
434
|
+
return ""
|
|
435
|
+
|
|
436
|
+
summary_parts = []
|
|
437
|
+
for key, value in self.context_store.items():
|
|
438
|
+
# For complex objects, just indicate their type
|
|
439
|
+
if isinstance(value, dict):
|
|
440
|
+
summary_parts.append(f"- {key}: Dictionary with {len(value)} items")
|
|
441
|
+
elif isinstance(value, list):
|
|
442
|
+
summary_parts.append(f"- {key}: List with {len(value)} items")
|
|
443
|
+
elif isinstance(value, str) and len(value) > 100:
|
|
444
|
+
summary_parts.append(f"- {key}: Text ({len(value)} chars)")
|
|
445
|
+
else:
|
|
446
|
+
summary_parts.append(f"- {key}: {value}")
|
|
447
|
+
|
|
448
|
+
return "\n".join(summary_parts)
|
|
449
|
+
|
|
450
|
+
def _process_mcp_tool_calls(self, message: Dict) -> None:
|
|
451
|
+
"""Process any MCP tool calls in a message.
|
|
452
|
+
|
|
453
|
+
This method handles multiple tool call formats:
|
|
454
|
+
1. OpenAI function call format
|
|
455
|
+
2. Explicit MCP call format: mcp.call({...})
|
|
456
|
+
3. Natural language tool call detection
|
|
457
|
+
|
|
458
|
+
The method executes tool calls and stores results in the context store
|
|
459
|
+
for future reference.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
message: The message containing potential tool calls
|
|
463
|
+
"""
|
|
464
|
+
content = message.get("content", "")
|
|
465
|
+
if not isinstance(content, str):
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
# Check for tool_calls in the OpenAI message format
|
|
469
|
+
if "tool_calls" in message:
|
|
470
|
+
tool_calls = message.get("tool_calls", [])
|
|
471
|
+
for tool_call in tool_calls:
|
|
472
|
+
try:
|
|
473
|
+
# Extract tool name and arguments
|
|
474
|
+
function = tool_call.get("function", {})
|
|
475
|
+
tool_name = function.get("name")
|
|
476
|
+
arguments_str = function.get("arguments", "{}")
|
|
477
|
+
arguments = json.loads(arguments_str)
|
|
478
|
+
|
|
479
|
+
if tool_name in self.mcp_tools:
|
|
480
|
+
# Execute the tool
|
|
481
|
+
func = self.mcp_tools[tool_name]["function"]
|
|
482
|
+
result = func(**arguments)
|
|
483
|
+
|
|
484
|
+
# Store the result in the context
|
|
485
|
+
result_key = f"result_{uuid.uuid4().hex[:8]}"
|
|
486
|
+
self.context_store[result_key] = result
|
|
487
|
+
print(f"Executed tool '{tool_name}' with result: {result}")
|
|
488
|
+
except Exception as e:
|
|
489
|
+
print(f"Error processing OpenAI tool call: {e}")
|
|
490
|
+
|
|
491
|
+
# Check for explicit MCP calls in the format mcp.call({...})
|
|
492
|
+
import re
|
|
493
|
+
tool_call_pattern = r"mcp\.call\(([^)]+)\)"
|
|
494
|
+
explicit_calls = re.findall(tool_call_pattern, content)
|
|
495
|
+
for call in explicit_calls:
|
|
496
|
+
try:
|
|
497
|
+
# Parse the tool call arguments
|
|
498
|
+
call_args = json.loads(f"{{{call}}}")
|
|
499
|
+
tool_name = call_args.get("tool")
|
|
500
|
+
arguments = call_args.get("arguments", {})
|
|
501
|
+
|
|
502
|
+
if tool_name in self.mcp_tools:
|
|
503
|
+
# Execute the tool
|
|
504
|
+
func = self.mcp_tools[tool_name]["function"]
|
|
505
|
+
result = func(**arguments)
|
|
506
|
+
|
|
507
|
+
# Store the result in the context
|
|
508
|
+
result_key = f"result_{uuid.uuid4().hex[:8]}"
|
|
509
|
+
self.context_store[result_key] = result
|
|
510
|
+
print(f"Executed explicit MCP call to '{tool_name}' with result: {result}")
|
|
511
|
+
except Exception as e:
|
|
512
|
+
print(f"Error processing explicit MCP tool call: {e}")
|
|
513
|
+
|
|
514
|
+
# Add basic natural language detection for common context operations
|
|
515
|
+
# This is a simplified approach - in production, you would use more robust NLP
|
|
516
|
+
content_lower = content.lower()
|
|
517
|
+
|
|
518
|
+
# Very basic pattern matching for user requests to update context
|
|
519
|
+
if ("add" in content_lower and "to my interests" in content_lower) or \
|
|
520
|
+
("update my interests" in content_lower):
|
|
521
|
+
try:
|
|
522
|
+
# Extract the interest to add - very simplified regex extraction
|
|
523
|
+
interest_match = re.search(r"add ['\"]?([^'\"]+)['\"]? to my interests", content_lower)
|
|
524
|
+
if interest_match:
|
|
525
|
+
interest = interest_match.group(1).strip()
|
|
526
|
+
if "user_preferences" in self.context_store:
|
|
527
|
+
user_prefs = self.context_store["user_preferences"]
|
|
528
|
+
if isinstance(user_prefs, dict) and "interests" in user_prefs:
|
|
529
|
+
if interest not in user_prefs["interests"]:
|
|
530
|
+
user_prefs["interests"].append(interest)
|
|
531
|
+
self.update_context("user_preferences", user_prefs)
|
|
532
|
+
print(f"Added '{interest}' to user interests via natural language detection")
|
|
533
|
+
except Exception as e:
|
|
534
|
+
print(f"Error processing natural language context update: {e}")
|
|
535
|
+
|
|
536
|
+
def update_context(self, key: str, value: Any) -> None:
|
|
537
|
+
"""
|
|
538
|
+
Update the MCP context with a new key-value pair.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
key: The context key
|
|
542
|
+
value: The context value
|
|
543
|
+
"""
|
|
544
|
+
self.context_store[key] = value
|
|
545
|
+
|
|
546
|
+
def get_context(self, key: str) -> Any:
|
|
547
|
+
"""
|
|
548
|
+
Get a value from the MCP context.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
key: The context key to retrieve
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
The context value or None if not found
|
|
555
|
+
"""
|
|
556
|
+
return self.context_store.get(key)
|
|
557
|
+
|
|
558
|
+
def list_available_tools(self) -> List[Dict]:
|
|
559
|
+
"""
|
|
560
|
+
Get a list of all available MCP tools.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
List of tool definitions
|
|
564
|
+
"""
|
|
565
|
+
return [
|
|
566
|
+
{
|
|
567
|
+
"name": name,
|
|
568
|
+
"description": tool["description"],
|
|
569
|
+
"parameters": tool["parameters"]
|
|
570
|
+
}
|
|
571
|
+
for name, tool in self.mcp_tools.items()
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
def execute_tool(self, tool_name: str, **kwargs) -> Any:
|
|
575
|
+
"""
|
|
576
|
+
Execute an MCP tool by name with the provided arguments.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
tool_name: The name of the tool to execute
|
|
580
|
+
**kwargs: Arguments to pass to the tool
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
The result of the tool execution
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
ValueError: If the tool is not found
|
|
587
|
+
"""
|
|
588
|
+
if tool_name not in self.mcp_tools:
|
|
589
|
+
raise ValueError(f"Tool '{tool_name}' not found")
|
|
590
|
+
|
|
591
|
+
# Get the tool and its function
|
|
592
|
+
tool = self.mcp_tools[tool_name]
|
|
593
|
+
func = tool["function"]
|
|
594
|
+
|
|
595
|
+
# Call the function directly without passing self again (it's already bound)
|
|
596
|
+
return func(**kwargs)
|
|
597
|
+
|
|
598
|
+
def _should_process_message(self, message: Dict[str, Any]) -> bool:
|
|
599
|
+
"""
|
|
600
|
+
Checks if a message with a task_id has already been completed.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
message: The message to check
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
True if the message should be processed, False otherwise
|
|
607
|
+
"""
|
|
608
|
+
if message is None:
|
|
609
|
+
return True # Can't determine, assume process
|
|
610
|
+
|
|
611
|
+
message_type = message.get('type')
|
|
612
|
+
task_id = message.get('task_id')
|
|
613
|
+
|
|
614
|
+
if message_type == 'task' and task_id:
|
|
615
|
+
if task_id in self.completed_task_ids:
|
|
616
|
+
logger.info(f"[{self.name}] Identified already completed task_id: {task_id}. Skipping processing.")
|
|
617
|
+
return False # Already completed, do not process
|
|
618
|
+
|
|
619
|
+
return True # Not a task with a known completed ID, or not a task at all
|
|
620
|
+
|
|
621
|
+
def _mark_task_completed(self, task_id: Optional[str]):
|
|
622
|
+
"""
|
|
623
|
+
Marks a task ID as completed.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
task_id: The task ID to mark as completed
|
|
627
|
+
"""
|
|
628
|
+
if task_id:
|
|
629
|
+
logger.debug(f"[{self.name}] Marking task_id {task_id} as completed.")
|
|
630
|
+
self.completed_task_ids.add(task_id)
|
|
631
|
+
else:
|
|
632
|
+
logger.warning(f"[{self.name}] Attempted to mark task completed, but task_id was None.")
|
|
633
|
+
|
|
634
|
+
def _extract_sender(self, message: Dict) -> str:
|
|
635
|
+
"""Centralized sender extraction with nested JSON support
|
|
636
|
+
TODO: Migrate to MessageSchema validation (GitHub Issue #1?)
|
|
637
|
+
"""
|
|
638
|
+
# First check root level
|
|
639
|
+
if sender := message.get('sender') or message.get('from'):
|
|
640
|
+
return sender
|
|
641
|
+
|
|
642
|
+
content = message.get('content', {})
|
|
643
|
+
|
|
644
|
+
# Check nested JSON in content.text
|
|
645
|
+
if isinstance(content, dict):
|
|
646
|
+
try:
|
|
647
|
+
if text_content := content.get('text'):
|
|
648
|
+
parsed = json.loads(text_content)
|
|
649
|
+
if sender := parsed.get('sender') or parsed.get('from'):
|
|
650
|
+
return sender
|
|
651
|
+
except json.JSONDecodeError:
|
|
652
|
+
pass
|
|
653
|
+
|
|
654
|
+
# Fallback to content.sender
|
|
655
|
+
if sender := content.get('sender') or content.get('from'):
|
|
656
|
+
return sender
|
|
657
|
+
|
|
658
|
+
return "Unknown"
|