praisonaiagents 0.0.65__py3-none-any.whl → 0.0.67__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.
- praisonaiagents/__init__.py +3 -1
- praisonaiagents/mcp/__init__.py +6 -0
- praisonaiagents/mcp/mcp.py +319 -0
- {praisonaiagents-0.0.65.dist-info → praisonaiagents-0.0.67.dist-info}/METADATA +2 -2
- {praisonaiagents-0.0.65.dist-info → praisonaiagents-0.0.67.dist-info}/RECORD +7 -5
- {praisonaiagents-0.0.65.dist-info → praisonaiagents-0.0.67.dist-info}/WHEEL +1 -1
- {praisonaiagents-0.0.65.dist-info → praisonaiagents-0.0.67.dist-info}/top_level.txt +0 -0
praisonaiagents/__init__.py
CHANGED
@@ -10,6 +10,7 @@ from .tools.tools import Tools
|
|
10
10
|
from .agents.autoagents import AutoAgents
|
11
11
|
from .knowledge.knowledge import Knowledge
|
12
12
|
from .knowledge.chunking import Chunking
|
13
|
+
from .mcp.mcp import MCP
|
13
14
|
from .main import (
|
14
15
|
TaskOutput,
|
15
16
|
ReflectionOutput,
|
@@ -51,5 +52,6 @@ __all__ = [
|
|
51
52
|
'sync_display_callbacks',
|
52
53
|
'async_display_callbacks',
|
53
54
|
'Knowledge',
|
54
|
-
'Chunking'
|
55
|
+
'Chunking',
|
56
|
+
'MCP'
|
55
57
|
]
|
@@ -0,0 +1,319 @@
|
|
1
|
+
import asyncio
|
2
|
+
import threading
|
3
|
+
import queue
|
4
|
+
import time
|
5
|
+
import inspect
|
6
|
+
import shlex
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
from typing import Any, List, Optional, Callable, Iterable, Union
|
10
|
+
from functools import wraps, partial
|
11
|
+
|
12
|
+
from mcp import ClientSession, StdioServerParameters
|
13
|
+
from mcp.client.stdio import stdio_client
|
14
|
+
|
15
|
+
class MCPToolRunner(threading.Thread):
|
16
|
+
"""A dedicated thread for running MCP operations."""
|
17
|
+
|
18
|
+
def __init__(self, server_params):
|
19
|
+
super().__init__(daemon=True)
|
20
|
+
self.server_params = server_params
|
21
|
+
self.queue = queue.Queue()
|
22
|
+
self.result_queue = queue.Queue()
|
23
|
+
self.initialized = threading.Event()
|
24
|
+
self.tools = []
|
25
|
+
self.start()
|
26
|
+
|
27
|
+
def run(self):
|
28
|
+
"""Main thread function that processes MCP requests."""
|
29
|
+
asyncio.run(self._run_async())
|
30
|
+
|
31
|
+
async def _run_async(self):
|
32
|
+
"""Async entry point for MCP operations."""
|
33
|
+
try:
|
34
|
+
# Set up MCP session
|
35
|
+
async with stdio_client(self.server_params) as (read, write):
|
36
|
+
async with ClientSession(read, write) as session:
|
37
|
+
# Initialize connection
|
38
|
+
await session.initialize()
|
39
|
+
|
40
|
+
# Get tools
|
41
|
+
tools_result = await session.list_tools()
|
42
|
+
self.tools = tools_result.tools
|
43
|
+
|
44
|
+
# Signal that initialization is complete
|
45
|
+
self.initialized.set()
|
46
|
+
|
47
|
+
# Process requests
|
48
|
+
while True:
|
49
|
+
try:
|
50
|
+
# Check for new requests
|
51
|
+
try:
|
52
|
+
item = self.queue.get(block=False)
|
53
|
+
if item is None: # Shutdown signal
|
54
|
+
break
|
55
|
+
|
56
|
+
tool_name, arguments = item
|
57
|
+
try:
|
58
|
+
result = await session.call_tool(tool_name, arguments)
|
59
|
+
self.result_queue.put((True, result))
|
60
|
+
except Exception as e:
|
61
|
+
self.result_queue.put((False, str(e)))
|
62
|
+
except queue.Empty:
|
63
|
+
pass
|
64
|
+
|
65
|
+
# Give other tasks a chance to run
|
66
|
+
await asyncio.sleep(0.01)
|
67
|
+
except asyncio.CancelledError:
|
68
|
+
break
|
69
|
+
except Exception as e:
|
70
|
+
self.initialized.set() # Ensure we don't hang
|
71
|
+
self.result_queue.put((False, f"MCP initialization error: {str(e)}"))
|
72
|
+
|
73
|
+
def call_tool(self, tool_name, arguments):
|
74
|
+
"""Call an MCP tool and wait for the result."""
|
75
|
+
if not self.initialized.is_set():
|
76
|
+
self.initialized.wait(timeout=30)
|
77
|
+
if not self.initialized.is_set():
|
78
|
+
return "Error: MCP initialization timed out"
|
79
|
+
|
80
|
+
# Put request in queue
|
81
|
+
self.queue.put((tool_name, arguments))
|
82
|
+
|
83
|
+
# Wait for result
|
84
|
+
success, result = self.result_queue.get()
|
85
|
+
if not success:
|
86
|
+
return f"Error: {result}"
|
87
|
+
|
88
|
+
# Process result
|
89
|
+
if hasattr(result, 'content') and result.content:
|
90
|
+
if hasattr(result.content[0], 'text'):
|
91
|
+
return result.content[0].text
|
92
|
+
return str(result.content[0])
|
93
|
+
return str(result)
|
94
|
+
|
95
|
+
def shutdown(self):
|
96
|
+
"""Signal the thread to shut down."""
|
97
|
+
self.queue.put(None)
|
98
|
+
|
99
|
+
|
100
|
+
class MCP:
|
101
|
+
"""
|
102
|
+
Model Context Protocol (MCP) integration for PraisonAI Agents.
|
103
|
+
|
104
|
+
This class provides a simple way to connect to MCP servers and use their tools
|
105
|
+
within PraisonAI agents.
|
106
|
+
|
107
|
+
Example:
|
108
|
+
```python
|
109
|
+
from praisonaiagents import Agent
|
110
|
+
from praisonaiagents.mcp import MCP
|
111
|
+
|
112
|
+
# Method 1: Using command and args separately
|
113
|
+
agent = Agent(
|
114
|
+
instructions="You are a helpful assistant...",
|
115
|
+
llm="gpt-4o-mini",
|
116
|
+
tools=MCP(
|
117
|
+
command="/path/to/python",
|
118
|
+
args=["/path/to/app.py"]
|
119
|
+
)
|
120
|
+
)
|
121
|
+
|
122
|
+
# Method 2: Using a single command string
|
123
|
+
agent = Agent(
|
124
|
+
instructions="You are a helpful assistant...",
|
125
|
+
llm="gpt-4o-mini",
|
126
|
+
tools=MCP("/path/to/python /path/to/app.py")
|
127
|
+
)
|
128
|
+
|
129
|
+
agent.start("What is the stock price of Tesla?")
|
130
|
+
```
|
131
|
+
"""
|
132
|
+
|
133
|
+
def __init__(self, command_or_string=None, args=None, *, command=None, timeout=60, debug=False, **kwargs):
|
134
|
+
"""
|
135
|
+
Initialize the MCP connection and get tools.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
command_or_string: Either:
|
139
|
+
- The command to run the MCP server (e.g., Python path)
|
140
|
+
- A complete command string (e.g., "/path/to/python /path/to/app.py")
|
141
|
+
- For NPX: 'npx' command with args for smithery tools
|
142
|
+
args: Arguments to pass to the command (when command_or_string is the command)
|
143
|
+
command: Alternative parameter name for backward compatibility
|
144
|
+
timeout: Timeout in seconds for MCP server initialization and tool calls (default: 60)
|
145
|
+
debug: Enable debug logging for MCP operations (default: False)
|
146
|
+
**kwargs: Additional parameters for StdioServerParameters
|
147
|
+
"""
|
148
|
+
# Handle backward compatibility with named parameter 'command'
|
149
|
+
if command_or_string is None and command is not None:
|
150
|
+
command_or_string = command
|
151
|
+
|
152
|
+
# Handle the single string format
|
153
|
+
if isinstance(command_or_string, str) and args is None:
|
154
|
+
# Split the string into command and args using shell-like parsing
|
155
|
+
parts = shlex.split(command_or_string)
|
156
|
+
if not parts:
|
157
|
+
raise ValueError("Empty command string")
|
158
|
+
|
159
|
+
cmd = parts[0]
|
160
|
+
arguments = parts[1:] if len(parts) > 1 else []
|
161
|
+
else:
|
162
|
+
# Use the original format with separate command and args
|
163
|
+
cmd = command_or_string
|
164
|
+
arguments = args or []
|
165
|
+
|
166
|
+
self.server_params = StdioServerParameters(
|
167
|
+
command=cmd,
|
168
|
+
args=arguments,
|
169
|
+
**kwargs
|
170
|
+
)
|
171
|
+
self.runner = MCPToolRunner(self.server_params)
|
172
|
+
|
173
|
+
# Wait for initialization
|
174
|
+
if not self.runner.initialized.wait(timeout=30):
|
175
|
+
print("Warning: MCP initialization timed out")
|
176
|
+
|
177
|
+
# Store additional parameters
|
178
|
+
self.timeout = timeout
|
179
|
+
self.debug = debug
|
180
|
+
|
181
|
+
if debug:
|
182
|
+
logging.getLogger("mcp-wrapper").setLevel(logging.DEBUG)
|
183
|
+
|
184
|
+
# Automatically detect if this is an NPX command
|
185
|
+
self.is_npx = cmd == 'npx' or (isinstance(cmd, str) and os.path.basename(cmd) == 'npx')
|
186
|
+
|
187
|
+
# For NPX-based MCP servers, use a different approach
|
188
|
+
if self.is_npx:
|
189
|
+
self._function_declarations = []
|
190
|
+
self._initialize_npx_mcp_tools(cmd, arguments)
|
191
|
+
else:
|
192
|
+
# Generate tool functions immediately and store them
|
193
|
+
self._tools = self._generate_tool_functions()
|
194
|
+
|
195
|
+
def _generate_tool_functions(self) -> List[Callable]:
|
196
|
+
"""
|
197
|
+
Generate functions for each MCP tool.
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
List[Callable]: Functions that can be used as tools
|
201
|
+
"""
|
202
|
+
tool_functions = []
|
203
|
+
|
204
|
+
for tool in self.runner.tools:
|
205
|
+
wrapper = self._create_tool_wrapper(tool)
|
206
|
+
tool_functions.append(wrapper)
|
207
|
+
|
208
|
+
return tool_functions
|
209
|
+
|
210
|
+
def _create_tool_wrapper(self, tool):
|
211
|
+
"""Create a wrapper function for an MCP tool."""
|
212
|
+
# Determine parameter names from the schema
|
213
|
+
param_names = []
|
214
|
+
param_annotations = {}
|
215
|
+
required_params = []
|
216
|
+
|
217
|
+
if hasattr(tool, 'inputSchema') and tool.inputSchema:
|
218
|
+
properties = tool.inputSchema.get("properties", {})
|
219
|
+
required = tool.inputSchema.get("required", [])
|
220
|
+
|
221
|
+
for name, prop in properties.items():
|
222
|
+
param_names.append(name)
|
223
|
+
|
224
|
+
# Set annotation based on property type
|
225
|
+
prop_type = prop.get("type", "string")
|
226
|
+
if prop_type == "string":
|
227
|
+
param_annotations[name] = str
|
228
|
+
elif prop_type == "integer":
|
229
|
+
param_annotations[name] = int
|
230
|
+
elif prop_type == "number":
|
231
|
+
param_annotations[name] = float
|
232
|
+
elif prop_type == "boolean":
|
233
|
+
param_annotations[name] = bool
|
234
|
+
elif prop_type == "array":
|
235
|
+
param_annotations[name] = list
|
236
|
+
elif prop_type == "object":
|
237
|
+
param_annotations[name] = dict
|
238
|
+
else:
|
239
|
+
param_annotations[name] = Any
|
240
|
+
|
241
|
+
if name in required:
|
242
|
+
required_params.append(name)
|
243
|
+
|
244
|
+
# Create the function signature
|
245
|
+
params = []
|
246
|
+
for name in param_names:
|
247
|
+
is_required = name in required_params
|
248
|
+
param = inspect.Parameter(
|
249
|
+
name=name,
|
250
|
+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
251
|
+
default=inspect.Parameter.empty if is_required else None,
|
252
|
+
annotation=param_annotations.get(name, Any)
|
253
|
+
)
|
254
|
+
params.append(param)
|
255
|
+
|
256
|
+
# Create function template to be properly decorated
|
257
|
+
def template_function(*args, **kwargs):
|
258
|
+
return None
|
259
|
+
|
260
|
+
# Create a proper function with the correct signature
|
261
|
+
template_function.__signature__ = inspect.Signature(params)
|
262
|
+
template_function.__annotations__ = param_annotations
|
263
|
+
template_function.__name__ = tool.name
|
264
|
+
template_function.__qualname__ = tool.name
|
265
|
+
template_function.__doc__ = tool.description
|
266
|
+
|
267
|
+
# Create the actual function using a decorator
|
268
|
+
@wraps(template_function)
|
269
|
+
def wrapper(*args, **kwargs):
|
270
|
+
# Map positional args to parameter names
|
271
|
+
all_args = {}
|
272
|
+
for i, arg in enumerate(args):
|
273
|
+
if i < len(param_names):
|
274
|
+
all_args[param_names[i]] = arg
|
275
|
+
|
276
|
+
# Add keyword args
|
277
|
+
all_args.update(kwargs)
|
278
|
+
|
279
|
+
# Call the tool
|
280
|
+
return self.runner.call_tool(tool.name, all_args)
|
281
|
+
|
282
|
+
# Make sure the wrapper has the correct signature for inspection
|
283
|
+
wrapper.__signature__ = inspect.Signature(params)
|
284
|
+
|
285
|
+
return wrapper
|
286
|
+
|
287
|
+
def _initialize_npx_mcp_tools(self, cmd, arguments):
|
288
|
+
"""Initialize the NPX MCP tools by extracting tool definitions."""
|
289
|
+
try:
|
290
|
+
# For NPX tools, we'll use the same approach as regular MCP tools
|
291
|
+
# but we need to handle the initialization differently
|
292
|
+
if self.debug:
|
293
|
+
logging.debug(f"Initializing NPX MCP tools with command: {cmd} {' '.join(arguments)}")
|
294
|
+
|
295
|
+
# Generate tool functions using the regular MCP approach
|
296
|
+
self._tools = self._generate_tool_functions()
|
297
|
+
|
298
|
+
if self.debug:
|
299
|
+
logging.debug(f"Generated {len(self._tools)} NPX MCP tools")
|
300
|
+
|
301
|
+
except Exception as e:
|
302
|
+
if self.debug:
|
303
|
+
logging.error(f"Failed to initialize NPX MCP tools: {e}")
|
304
|
+
raise RuntimeError(f"Failed to initialize NPX MCP tools: {e}")
|
305
|
+
|
306
|
+
|
307
|
+
|
308
|
+
def __iter__(self) -> Iterable[Callable]:
|
309
|
+
"""
|
310
|
+
Allow the MCP instance to be used directly as an iterable of tools.
|
311
|
+
|
312
|
+
This makes it possible to pass the MCP instance directly to the Agent's tools parameter.
|
313
|
+
"""
|
314
|
+
return iter(self._tools)
|
315
|
+
|
316
|
+
def __del__(self):
|
317
|
+
"""Clean up resources when the object is garbage collected."""
|
318
|
+
if hasattr(self, 'runner'):
|
319
|
+
self.runner.shutdown()
|
@@ -1,4 +1,4 @@
|
|
1
|
-
praisonaiagents/__init__.py,sha256=
|
1
|
+
praisonaiagents/__init__.py,sha256=Z2_rSA6mYozz0r3ioUgKzl3QV8uWRDS_QaqPg2oGjqg,1324
|
2
2
|
praisonaiagents/main.py,sha256=l29nGEbV2ReBi4szURbnH0Fk0w2F_QZTmECysyZjYcA,15066
|
3
3
|
praisonaiagents/agent/__init__.py,sha256=j0T19TVNbfZcClvpbZDDinQxZ0oORgsMrMqx16jZ-bA,128
|
4
4
|
praisonaiagents/agent/agent.py,sha256=h3s0-1M88zujllDHnKijHmYeVihD75d-K9s2Y3IHLY4,61850
|
@@ -11,6 +11,8 @@ praisonaiagents/knowledge/chunking.py,sha256=FzoNY0q8MkvG4gADqk4JcRhmH3lcEHbRdon
|
|
11
11
|
praisonaiagents/knowledge/knowledge.py,sha256=fQNREDiwdoisfIxJBLVkteXgq_8Gbypfc3UaZbxf5QY,13210
|
12
12
|
praisonaiagents/llm/__init__.py,sha256=ttPQQJQq6Tah-0updoEXDZFKWtJAM93rBWRoIgxRWO8,689
|
13
13
|
praisonaiagents/llm/llm.py,sha256=6QMRW47fgFozibzaqxa3dwxlD756rMLCRjL3eNsw8QQ,74088
|
14
|
+
praisonaiagents/mcp/__init__.py,sha256=IkYdrAK1bDQDm_0t3Wjt63Zwv3_IJgqz84Wqz9GH2iQ,111
|
15
|
+
praisonaiagents/mcp/mcp.py,sha256=7EJo2Eyw89ePQzeKUzRZgW3E2ztLkMwQj2lCjFlzecQ,12281
|
14
16
|
praisonaiagents/memory/memory.py,sha256=I8dOTkrl1i-GgQbDcrFOsSruzJ7MiI6Ys37DK27wrUs,35537
|
15
17
|
praisonaiagents/process/__init__.py,sha256=lkYbL7Hn5a0ldvJtkdH23vfIIZLIcanK-65C0MwaorY,52
|
16
18
|
praisonaiagents/process/process.py,sha256=HPw84OhnKQW3EyrDkpoQu0DcpxThbrzR2hWUgwQh9Pw,59955
|
@@ -37,7 +39,7 @@ praisonaiagents/tools/xml_tools.py,sha256=iYTMBEk5l3L3ryQ1fkUnNVYK-Nnua2Kx2S0dxN
|
|
37
39
|
praisonaiagents/tools/yaml_tools.py,sha256=uogAZrhXV9O7xvspAtcTfpKSQYL2nlOTvCQXN94-G9A,14215
|
38
40
|
praisonaiagents/tools/yfinance_tools.py,sha256=s2PBj_1v7oQnOobo2fDbQBACEHl61ftG4beG6Z979ZE,8529
|
39
41
|
praisonaiagents/tools/train/data/generatecot.py,sha256=H6bNh-E2hqL5MW6kX3hqZ05g9ETKN2-kudSjiuU_SD8,19403
|
40
|
-
praisonaiagents-0.0.
|
41
|
-
praisonaiagents-0.0.
|
42
|
-
praisonaiagents-0.0.
|
43
|
-
praisonaiagents-0.0.
|
42
|
+
praisonaiagents-0.0.67.dist-info/METADATA,sha256=sGyO2OxLcuZGGmq-2WZHg9RIvSB1tLPFOUpEst8VeU0,830
|
43
|
+
praisonaiagents-0.0.67.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
44
|
+
praisonaiagents-0.0.67.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
|
45
|
+
praisonaiagents-0.0.67.dist-info/RECORD,,
|
File without changes
|