praisonaiagents 0.0.76__py3-none-any.whl → 0.0.78__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/agent/agent.py +181 -7
- praisonaiagents/llm/llm.py +6 -0
- praisonaiagents/mcp/__init__.py +3 -0
- praisonaiagents/mcp/mcp.py +83 -35
- praisonaiagents/mcp/mcp_sse.py +184 -0
- {praisonaiagents-0.0.76.dist-info → praisonaiagents-0.0.78.dist-info}/METADATA +5 -1
- {praisonaiagents-0.0.76.dist-info → praisonaiagents-0.0.78.dist-info}/RECORD +9 -8
- {praisonaiagents-0.0.76.dist-info → praisonaiagents-0.0.78.dist-info}/WHEEL +1 -1
- {praisonaiagents-0.0.76.dist-info → praisonaiagents-0.0.78.dist-info}/top_level.txt +0 -0
praisonaiagents/agent/agent.py
CHANGED
@@ -22,9 +22,16 @@ import inspect
|
|
22
22
|
import uuid
|
23
23
|
from dataclasses import dataclass
|
24
24
|
|
25
|
+
# Don't import FastAPI dependencies here - use lazy loading instead
|
26
|
+
|
25
27
|
if TYPE_CHECKING:
|
26
28
|
from ..task.task import Task
|
27
29
|
|
30
|
+
# Shared variables for API server
|
31
|
+
_shared_app = None
|
32
|
+
_server_started = False
|
33
|
+
_registered_agents = {}
|
34
|
+
|
28
35
|
@dataclass
|
29
36
|
class ChatCompletionMessage:
|
30
37
|
content: str
|
@@ -530,11 +537,21 @@ Your Goal: {self.goal}
|
|
530
537
|
from ..mcp.mcp import MCP
|
531
538
|
if isinstance(self.tools, MCP):
|
532
539
|
logging.debug(f"Looking for MCP tool {function_name}")
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
540
|
+
|
541
|
+
# Handle SSE MCP client
|
542
|
+
if hasattr(self.tools, 'is_sse') and self.tools.is_sse:
|
543
|
+
if hasattr(self.tools, 'sse_client'):
|
544
|
+
for tool in self.tools.sse_client.tools:
|
545
|
+
if tool.name == function_name:
|
546
|
+
logging.debug(f"Found matching SSE MCP tool: {function_name}")
|
547
|
+
return tool(**arguments)
|
548
|
+
# Handle stdio MCP client
|
549
|
+
elif hasattr(self.tools, 'runner'):
|
550
|
+
# Check if any of the MCP tools match the function name
|
551
|
+
for mcp_tool in self.tools.runner.tools:
|
552
|
+
if hasattr(mcp_tool, 'name') and mcp_tool.name == function_name:
|
553
|
+
logging.debug(f"Found matching MCP tool: {function_name}")
|
554
|
+
return self.tools.runner.call_tool(function_name, arguments)
|
538
555
|
|
539
556
|
# Try to find the function in the agent's tools list first
|
540
557
|
func = None
|
@@ -815,7 +832,11 @@ Your Goal: {self.goal}
|
|
815
832
|
logging.debug("Converting MCP tool to OpenAI format")
|
816
833
|
openai_tool = tool_param.to_openai_tool()
|
817
834
|
if openai_tool:
|
818
|
-
|
835
|
+
# Handle both single tool and list of tools
|
836
|
+
if isinstance(openai_tool, list):
|
837
|
+
tool_param = openai_tool
|
838
|
+
else:
|
839
|
+
tool_param = [openai_tool]
|
819
840
|
logging.debug(f"Converted MCP tool: {tool_param}")
|
820
841
|
|
821
842
|
# Pass everything to LLM class
|
@@ -1385,4 +1406,157 @@ Your Goal: {self.goal}
|
|
1385
1406
|
|
1386
1407
|
except Exception as e:
|
1387
1408
|
logging.error(f"Error in execute_tool_async: {str(e)}", exc_info=True)
|
1388
|
-
return {"error": f"Error in execute_tool_async: {str(e)}"}
|
1409
|
+
return {"error": f"Error in execute_tool_async: {str(e)}"}
|
1410
|
+
|
1411
|
+
def launch(self, path: str = '/', port: int = 8000, host: str = '0.0.0.0', autostart: bool = True, debug: bool = False, blocking: bool = True):
|
1412
|
+
"""
|
1413
|
+
Launch the agent as an HTTP API endpoint.
|
1414
|
+
|
1415
|
+
Args:
|
1416
|
+
path: API endpoint path (default: '/')
|
1417
|
+
port: Server port (default: 8000)
|
1418
|
+
host: Server host (default: '0.0.0.0')
|
1419
|
+
autostart: Whether to start the server automatically (default: True)
|
1420
|
+
debug: Enable debug mode for uvicorn (default: False)
|
1421
|
+
blocking: If True, blocks the main thread to keep the server running (default: True)
|
1422
|
+
|
1423
|
+
Returns:
|
1424
|
+
None
|
1425
|
+
"""
|
1426
|
+
global _server_started, _registered_agents, _shared_app
|
1427
|
+
|
1428
|
+
# Try to import FastAPI dependencies - lazy loading
|
1429
|
+
try:
|
1430
|
+
import uvicorn
|
1431
|
+
from fastapi import FastAPI, HTTPException, Request
|
1432
|
+
from fastapi.responses import JSONResponse
|
1433
|
+
from pydantic import BaseModel
|
1434
|
+
|
1435
|
+
# Define the request model here since we need pydantic
|
1436
|
+
class AgentQuery(BaseModel):
|
1437
|
+
query: str
|
1438
|
+
|
1439
|
+
except ImportError as e:
|
1440
|
+
# Check which specific module is missing
|
1441
|
+
missing_module = str(e).split("No module named '")[-1].rstrip("'")
|
1442
|
+
display_error(f"Missing dependency: {missing_module}. Required for launch() method.")
|
1443
|
+
logging.error(f"Missing dependency: {missing_module}. Required for launch() method.")
|
1444
|
+
print(f"\nTo add API capabilities, install the required dependencies:")
|
1445
|
+
print(f"pip install {missing_module}")
|
1446
|
+
print("\nOr install all API dependencies with:")
|
1447
|
+
print("pip install 'praisonaiagents[api]'")
|
1448
|
+
return None
|
1449
|
+
|
1450
|
+
# Initialize shared FastAPI app if not already created
|
1451
|
+
if _shared_app is None:
|
1452
|
+
_shared_app = FastAPI(
|
1453
|
+
title="PraisonAI Agents API",
|
1454
|
+
description="API for interacting with PraisonAI Agents"
|
1455
|
+
)
|
1456
|
+
|
1457
|
+
# Add a root endpoint with a welcome message
|
1458
|
+
@_shared_app.get("/")
|
1459
|
+
async def root():
|
1460
|
+
return {"message": "Welcome to PraisonAI Agents API. See /docs for usage."}
|
1461
|
+
|
1462
|
+
# Normalize path to ensure it starts with /
|
1463
|
+
if not path.startswith('/'):
|
1464
|
+
path = f'/{path}'
|
1465
|
+
|
1466
|
+
# Check if path is already registered by another agent
|
1467
|
+
if path in _registered_agents and _registered_agents[path] != self.agent_id:
|
1468
|
+
existing_agent = _registered_agents[path]
|
1469
|
+
logging.warning(f"Path '{path}' is already registered by another agent. Please use a different path.")
|
1470
|
+
print(f"⚠️ Warning: Path '{path}' is already registered by another agent.")
|
1471
|
+
# Use a modified path to avoid conflicts
|
1472
|
+
original_path = path
|
1473
|
+
path = f"{path}_{self.agent_id[:6]}"
|
1474
|
+
logging.warning(f"Using '{path}' instead of '{original_path}'")
|
1475
|
+
print(f"🔄 Using '{path}' instead")
|
1476
|
+
|
1477
|
+
# Register the agent to this path
|
1478
|
+
_registered_agents[path] = self.agent_id
|
1479
|
+
|
1480
|
+
# Define the endpoint handler
|
1481
|
+
@_shared_app.post(path)
|
1482
|
+
async def handle_agent_query(request: Request, query_data: Optional[AgentQuery] = None):
|
1483
|
+
# Handle both direct JSON with query field and form data
|
1484
|
+
if query_data is None:
|
1485
|
+
try:
|
1486
|
+
request_data = await request.json()
|
1487
|
+
if "query" not in request_data:
|
1488
|
+
raise HTTPException(status_code=400, detail="Missing 'query' field in request")
|
1489
|
+
query = request_data["query"]
|
1490
|
+
except:
|
1491
|
+
# Fallback to form data or query params
|
1492
|
+
form_data = await request.form()
|
1493
|
+
if "query" in form_data:
|
1494
|
+
query = form_data["query"]
|
1495
|
+
else:
|
1496
|
+
raise HTTPException(status_code=400, detail="Missing 'query' field in request")
|
1497
|
+
else:
|
1498
|
+
query = query_data.query
|
1499
|
+
|
1500
|
+
try:
|
1501
|
+
# Use async version if available, otherwise use sync version
|
1502
|
+
if asyncio.iscoroutinefunction(self.chat):
|
1503
|
+
response = await self.achat(query)
|
1504
|
+
else:
|
1505
|
+
# Run sync function in a thread to avoid blocking
|
1506
|
+
loop = asyncio.get_event_loop()
|
1507
|
+
response = await loop.run_in_executor(None, lambda: self.chat(query))
|
1508
|
+
|
1509
|
+
return {"response": response}
|
1510
|
+
except Exception as e:
|
1511
|
+
logging.error(f"Error processing query: {str(e)}", exc_info=True)
|
1512
|
+
return JSONResponse(
|
1513
|
+
status_code=500,
|
1514
|
+
content={"error": f"Error processing query: {str(e)}"}
|
1515
|
+
)
|
1516
|
+
|
1517
|
+
print(f"🚀 Agent '{self.name}' available at http://{host}:{port}{path}")
|
1518
|
+
|
1519
|
+
# Start the server if this is the first launch call and autostart is True
|
1520
|
+
if autostart and not _server_started:
|
1521
|
+
_server_started = True
|
1522
|
+
|
1523
|
+
# Add healthcheck endpoint
|
1524
|
+
@_shared_app.get("/health")
|
1525
|
+
async def healthcheck():
|
1526
|
+
return {"status": "ok", "agents": list(_registered_agents.keys())}
|
1527
|
+
|
1528
|
+
# Start the server in a separate thread to not block execution
|
1529
|
+
import threading
|
1530
|
+
def run_server():
|
1531
|
+
try:
|
1532
|
+
uvicorn.run(_shared_app, host=host, port=port, log_level="debug" if debug else "info")
|
1533
|
+
except Exception as e:
|
1534
|
+
logging.error(f"Error starting server: {str(e)}", exc_info=True)
|
1535
|
+
print(f"❌ Error starting server: {str(e)}")
|
1536
|
+
|
1537
|
+
server_thread = threading.Thread(target=run_server, daemon=True)
|
1538
|
+
server_thread.start()
|
1539
|
+
|
1540
|
+
# Give the server a moment to start up
|
1541
|
+
import time
|
1542
|
+
time.sleep(0.5)
|
1543
|
+
|
1544
|
+
print(f"✅ FastAPI server started at http://{host}:{port}")
|
1545
|
+
print(f"📚 API documentation available at http://{host}:{port}/docs")
|
1546
|
+
|
1547
|
+
# If blocking is True, keep the main thread alive
|
1548
|
+
if blocking:
|
1549
|
+
print("\nServer is running in blocking mode. Press Ctrl+C to stop...")
|
1550
|
+
try:
|
1551
|
+
while True:
|
1552
|
+
time.sleep(1)
|
1553
|
+
except KeyboardInterrupt:
|
1554
|
+
print("\nServer stopped")
|
1555
|
+
else:
|
1556
|
+
# Note for non-blocking mode
|
1557
|
+
print("\nNote: Server is running in a background thread. To keep it alive, either:")
|
1558
|
+
print("1. Set blocking=True when calling launch()")
|
1559
|
+
print("2. Keep your main application running")
|
1560
|
+
print("3. Use a loop in your code to prevent the program from exiting")
|
1561
|
+
|
1562
|
+
return None
|
praisonaiagents/llm/llm.py
CHANGED
@@ -293,6 +293,12 @@ class LLM:
|
|
293
293
|
if isinstance(tool, dict) and 'type' in tool and tool['type'] == 'function':
|
294
294
|
logging.debug(f"Using pre-formatted OpenAI tool: {tool['function']['name']}")
|
295
295
|
formatted_tools.append(tool)
|
296
|
+
# Handle lists of tools (e.g. from MCP.to_openai_tool())
|
297
|
+
elif isinstance(tool, list):
|
298
|
+
for subtool in tool:
|
299
|
+
if isinstance(subtool, dict) and 'type' in subtool and subtool['type'] == 'function':
|
300
|
+
logging.debug(f"Using pre-formatted OpenAI tool from list: {subtool['function']['name']}")
|
301
|
+
formatted_tools.append(subtool)
|
296
302
|
elif callable(tool):
|
297
303
|
tool_def = self._generate_tool_definition(tool.__name__)
|
298
304
|
if tool_def:
|
praisonaiagents/mcp/__init__.py
CHANGED
praisonaiagents/mcp/mcp.py
CHANGED
@@ -6,6 +6,7 @@ import inspect
|
|
6
6
|
import shlex
|
7
7
|
import logging
|
8
8
|
import os
|
9
|
+
import re
|
9
10
|
from typing import Any, List, Optional, Callable, Iterable, Union
|
10
11
|
from functools import wraps, partial
|
11
12
|
|
@@ -126,6 +127,13 @@ class MCP:
|
|
126
127
|
tools=MCP("/path/to/python /path/to/app.py")
|
127
128
|
)
|
128
129
|
|
130
|
+
# Method 3: Using an SSE endpoint
|
131
|
+
agent = Agent(
|
132
|
+
instructions="You are a helpful assistant...",
|
133
|
+
llm="gpt-4o-mini",
|
134
|
+
tools=MCP("http://localhost:8080/sse")
|
135
|
+
)
|
136
|
+
|
129
137
|
agent.start("What is the stock price of Tesla?")
|
130
138
|
```
|
131
139
|
"""
|
@@ -139,6 +147,7 @@ class MCP:
|
|
139
147
|
- The command to run the MCP server (e.g., Python path)
|
140
148
|
- A complete command string (e.g., "/path/to/python /path/to/app.py")
|
141
149
|
- For NPX: 'npx' command with args for smithery tools
|
150
|
+
- An SSE URL (e.g., "http://localhost:8080/sse")
|
142
151
|
args: Arguments to pass to the command (when command_or_string is the command)
|
143
152
|
command: Alternative parameter name for backward compatibility
|
144
153
|
timeout: Timeout in seconds for MCP server initialization and tool calls (default: 60)
|
@@ -149,7 +158,44 @@ class MCP:
|
|
149
158
|
if command_or_string is None and command is not None:
|
150
159
|
command_or_string = command
|
151
160
|
|
152
|
-
#
|
161
|
+
# Set up logging - default to WARNING level to hide INFO messages
|
162
|
+
if debug:
|
163
|
+
logging.getLogger("mcp-wrapper").setLevel(logging.DEBUG)
|
164
|
+
logging.getLogger("mcp-sse").setLevel(logging.DEBUG)
|
165
|
+
logging.getLogger("mcp.client").setLevel(logging.DEBUG)
|
166
|
+
logging.getLogger("sse").setLevel(logging.DEBUG)
|
167
|
+
logging.getLogger("mcp-server").setLevel(logging.DEBUG)
|
168
|
+
logging.getLogger("mcp-client").setLevel(logging.DEBUG)
|
169
|
+
logging.getLogger("_client").setLevel(logging.DEBUG)
|
170
|
+
logging.getLogger("httpx").setLevel(logging.DEBUG)
|
171
|
+
logging.getLogger("llm").setLevel(logging.DEBUG)
|
172
|
+
else:
|
173
|
+
# Set all MCP-related loggers to WARNING level by default
|
174
|
+
logging.getLogger("mcp-wrapper").setLevel(logging.WARNING)
|
175
|
+
logging.getLogger("mcp-sse").setLevel(logging.WARNING)
|
176
|
+
logging.getLogger("mcp.client").setLevel(logging.WARNING)
|
177
|
+
logging.getLogger("sse").setLevel(logging.WARNING)
|
178
|
+
logging.getLogger("mcp-server").setLevel(logging.WARNING)
|
179
|
+
logging.getLogger("mcp-client").setLevel(logging.WARNING)
|
180
|
+
logging.getLogger("_client").setLevel(logging.WARNING)
|
181
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
182
|
+
logging.getLogger("llm").setLevel(logging.WARNING)
|
183
|
+
|
184
|
+
# Store additional parameters
|
185
|
+
self.timeout = timeout
|
186
|
+
self.debug = debug
|
187
|
+
|
188
|
+
# Check if this is an SSE URL
|
189
|
+
if isinstance(command_or_string, str) and re.match(r'^https?://', command_or_string):
|
190
|
+
# Import the SSE client implementation
|
191
|
+
from .mcp_sse import SSEMCPClient
|
192
|
+
self.sse_client = SSEMCPClient(command_or_string, debug=debug)
|
193
|
+
self._tools = list(self.sse_client.tools)
|
194
|
+
self.is_sse = True
|
195
|
+
self.is_npx = False
|
196
|
+
return
|
197
|
+
|
198
|
+
# Handle the single string format for stdio client
|
153
199
|
if isinstance(command_or_string, str) and args is None:
|
154
200
|
# Split the string into command and args using shell-like parsing
|
155
201
|
parts = shlex.split(command_or_string)
|
@@ -162,7 +208,9 @@ class MCP:
|
|
162
208
|
# Use the original format with separate command and args
|
163
209
|
cmd = command_or_string
|
164
210
|
arguments = args or []
|
165
|
-
|
211
|
+
|
212
|
+
# Set up stdio client
|
213
|
+
self.is_sse = False
|
166
214
|
self.server_params = StdioServerParameters(
|
167
215
|
command=cmd,
|
168
216
|
args=arguments,
|
@@ -173,13 +221,6 @@ class MCP:
|
|
173
221
|
# Wait for initialization
|
174
222
|
if not self.runner.initialized.wait(timeout=30):
|
175
223
|
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
224
|
|
184
225
|
# Automatically detect if this is an NPX command
|
185
226
|
self.is_npx = cmd == 'npx' or (isinstance(cmd, str) and os.path.basename(cmd) == 'npx')
|
@@ -199,6 +240,9 @@ class MCP:
|
|
199
240
|
Returns:
|
200
241
|
List[Callable]: Functions that can be used as tools
|
201
242
|
"""
|
243
|
+
if self.is_sse:
|
244
|
+
return list(self.sse_client.tools)
|
245
|
+
|
202
246
|
tool_functions = []
|
203
247
|
|
204
248
|
for tool in self.runner.tools:
|
@@ -303,8 +347,6 @@ class MCP:
|
|
303
347
|
logging.error(f"Failed to initialize NPX MCP tools: {e}")
|
304
348
|
raise RuntimeError(f"Failed to initialize NPX MCP tools: {e}")
|
305
349
|
|
306
|
-
|
307
|
-
|
308
350
|
def __iter__(self) -> Iterable[Callable]:
|
309
351
|
"""
|
310
352
|
Allow the MCP instance to be used directly as an iterable of tools.
|
@@ -320,37 +362,43 @@ class MCP:
|
|
320
362
|
provider/model format (e.g., "openai/gpt-4o-mini").
|
321
363
|
|
322
364
|
Returns:
|
323
|
-
dict: OpenAI-compatible tool definition
|
365
|
+
dict or list: OpenAI-compatible tool definition(s)
|
324
366
|
"""
|
367
|
+
if self.is_sse and hasattr(self, 'sse_client') and self.sse_client.tools:
|
368
|
+
# Return all tools from SSE client
|
369
|
+
return self.sse_client.to_openai_tools()
|
370
|
+
|
325
371
|
# For simplicity, we'll convert the first tool only if multiple exist
|
326
372
|
# More complex implementations could handle multiple tools
|
327
|
-
if not self.runner.tools:
|
373
|
+
if not hasattr(self, 'runner') or not self.runner.tools:
|
328
374
|
logging.warning("No MCP tools available to convert to OpenAI format")
|
329
375
|
return None
|
330
376
|
|
331
|
-
#
|
332
|
-
|
377
|
+
# Convert all tools to OpenAI format
|
378
|
+
openai_tools = []
|
379
|
+
for tool in self.runner.tools:
|
380
|
+
# Create OpenAI tool definition
|
381
|
+
parameters = {}
|
382
|
+
if hasattr(tool, 'inputSchema') and tool.inputSchema:
|
383
|
+
parameters = tool.inputSchema
|
384
|
+
else:
|
385
|
+
# Create a minimal schema if none exists
|
386
|
+
parameters = {
|
387
|
+
"type": "object",
|
388
|
+
"properties": {},
|
389
|
+
"required": []
|
390
|
+
}
|
391
|
+
|
392
|
+
openai_tools.append({
|
393
|
+
"type": "function",
|
394
|
+
"function": {
|
395
|
+
"name": tool.name,
|
396
|
+
"description": tool.description if hasattr(tool, 'description') else f"Call the {tool.name} tool",
|
397
|
+
"parameters": parameters
|
398
|
+
}
|
399
|
+
})
|
333
400
|
|
334
|
-
|
335
|
-
parameters = {}
|
336
|
-
if hasattr(tool, 'inputSchema') and tool.inputSchema:
|
337
|
-
parameters = tool.inputSchema
|
338
|
-
else:
|
339
|
-
# Create a minimal schema if none exists
|
340
|
-
parameters = {
|
341
|
-
"type": "object",
|
342
|
-
"properties": {},
|
343
|
-
"required": []
|
344
|
-
}
|
345
|
-
|
346
|
-
return {
|
347
|
-
"type": "function",
|
348
|
-
"function": {
|
349
|
-
"name": tool.name,
|
350
|
-
"description": tool.description if hasattr(tool, 'description') else f"Call the {tool.name} tool",
|
351
|
-
"parameters": parameters
|
352
|
-
}
|
353
|
-
}
|
401
|
+
return openai_tools
|
354
402
|
|
355
403
|
def __del__(self):
|
356
404
|
"""Clean up resources when the object is garbage collected."""
|
@@ -0,0 +1,184 @@
|
|
1
|
+
"""
|
2
|
+
SSE (Server-Sent Events) client implementation for MCP (Model Context Protocol).
|
3
|
+
This module provides the necessary classes and functions to connect to an MCP server
|
4
|
+
over SSE transport.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import logging
|
9
|
+
import threading
|
10
|
+
import inspect
|
11
|
+
import json
|
12
|
+
from typing import List, Dict, Any, Optional, Callable, Iterable
|
13
|
+
|
14
|
+
from mcp import ClientSession
|
15
|
+
from mcp.client.sse import sse_client
|
16
|
+
|
17
|
+
logger = logging.getLogger("mcp-sse")
|
18
|
+
|
19
|
+
# Global event loop for async operations
|
20
|
+
_event_loop = None
|
21
|
+
|
22
|
+
def get_event_loop():
|
23
|
+
"""Get or create a global event loop."""
|
24
|
+
global _event_loop
|
25
|
+
if _event_loop is None or _event_loop.is_closed():
|
26
|
+
_event_loop = asyncio.new_event_loop()
|
27
|
+
asyncio.set_event_loop(_event_loop)
|
28
|
+
return _event_loop
|
29
|
+
|
30
|
+
|
31
|
+
class SSEMCPTool:
|
32
|
+
"""A wrapper for an MCP tool that can be used with praisonaiagents."""
|
33
|
+
|
34
|
+
def __init__(self, name: str, description: str, session: ClientSession, input_schema: Optional[Dict[str, Any]] = None):
|
35
|
+
self.name = name
|
36
|
+
self.__name__ = name # Required for Agent to recognize it as a tool
|
37
|
+
self.__qualname__ = name # Required for Agent to recognize it as a tool
|
38
|
+
self.__doc__ = description # Required for Agent to recognize it as a tool
|
39
|
+
self.description = description
|
40
|
+
self.session = session
|
41
|
+
self.input_schema = input_schema or {}
|
42
|
+
|
43
|
+
# Create a signature based on input schema
|
44
|
+
params = []
|
45
|
+
if input_schema and 'properties' in input_schema:
|
46
|
+
for param_name in input_schema['properties']:
|
47
|
+
params.append(
|
48
|
+
inspect.Parameter(
|
49
|
+
name=param_name,
|
50
|
+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
51
|
+
default=inspect.Parameter.empty if param_name in input_schema.get('required', []) else None,
|
52
|
+
annotation=str # Default to string
|
53
|
+
)
|
54
|
+
)
|
55
|
+
|
56
|
+
self.__signature__ = inspect.Signature(params)
|
57
|
+
|
58
|
+
def __call__(self, **kwargs):
|
59
|
+
"""Synchronous wrapper for the async call."""
|
60
|
+
logger.debug(f"Tool {self.name} called with args: {kwargs}")
|
61
|
+
|
62
|
+
# Use the global event loop
|
63
|
+
loop = get_event_loop()
|
64
|
+
|
65
|
+
# Run the async call in the event loop
|
66
|
+
future = asyncio.run_coroutine_threadsafe(self._async_call(**kwargs), loop)
|
67
|
+
try:
|
68
|
+
# Wait for the result with a timeout
|
69
|
+
return future.result(timeout=30)
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"Error calling tool {self.name}: {e}")
|
72
|
+
return f"Error: {str(e)}"
|
73
|
+
|
74
|
+
async def _async_call(self, **kwargs):
|
75
|
+
"""Call the tool with the provided arguments."""
|
76
|
+
logger.debug(f"Async calling tool {self.name} with args: {kwargs}")
|
77
|
+
try:
|
78
|
+
result = await self.session.call_tool(self.name, kwargs)
|
79
|
+
|
80
|
+
# Extract text from result
|
81
|
+
if hasattr(result, 'content') and result.content:
|
82
|
+
if hasattr(result.content[0], 'text'):
|
83
|
+
return result.content[0].text
|
84
|
+
return str(result.content[0])
|
85
|
+
return str(result)
|
86
|
+
except Exception as e:
|
87
|
+
logger.error(f"Error in _async_call for {self.name}: {e}")
|
88
|
+
raise
|
89
|
+
|
90
|
+
def to_openai_tool(self):
|
91
|
+
"""Convert the tool to OpenAI format."""
|
92
|
+
return {
|
93
|
+
"type": "function",
|
94
|
+
"function": {
|
95
|
+
"name": self.name,
|
96
|
+
"description": self.description,
|
97
|
+
"parameters": self.input_schema
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
|
102
|
+
class SSEMCPClient:
|
103
|
+
"""A client for connecting to an MCP server over SSE."""
|
104
|
+
|
105
|
+
def __init__(self, server_url: str, debug: bool = False):
|
106
|
+
"""
|
107
|
+
Initialize an SSE MCP client.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
server_url: The URL of the SSE MCP server
|
111
|
+
debug: Whether to enable debug logging
|
112
|
+
"""
|
113
|
+
self.server_url = server_url
|
114
|
+
self.debug = debug
|
115
|
+
self.session = None
|
116
|
+
self.tools = []
|
117
|
+
|
118
|
+
# Set up logging
|
119
|
+
if debug:
|
120
|
+
logger.setLevel(logging.DEBUG)
|
121
|
+
else:
|
122
|
+
# Set to WARNING by default to hide INFO messages
|
123
|
+
logger.setLevel(logging.WARNING)
|
124
|
+
|
125
|
+
self._initialize()
|
126
|
+
|
127
|
+
def _initialize(self):
|
128
|
+
"""Initialize the connection and tools."""
|
129
|
+
# Use the global event loop
|
130
|
+
loop = get_event_loop()
|
131
|
+
|
132
|
+
# Start a background thread to run the event loop
|
133
|
+
def run_event_loop():
|
134
|
+
asyncio.set_event_loop(loop)
|
135
|
+
loop.run_forever()
|
136
|
+
|
137
|
+
self.loop_thread = threading.Thread(target=run_event_loop, daemon=True)
|
138
|
+
self.loop_thread.start()
|
139
|
+
|
140
|
+
# Run the initialization in the event loop
|
141
|
+
future = asyncio.run_coroutine_threadsafe(self._async_initialize(), loop)
|
142
|
+
self.tools = future.result(timeout=30)
|
143
|
+
|
144
|
+
async def _async_initialize(self):
|
145
|
+
"""Asynchronously initialize the connection and tools."""
|
146
|
+
logger.debug(f"Connecting to MCP server at {self.server_url}")
|
147
|
+
|
148
|
+
# Create SSE client
|
149
|
+
self._streams_context = sse_client(url=self.server_url)
|
150
|
+
streams = await self._streams_context.__aenter__()
|
151
|
+
|
152
|
+
self._session_context = ClientSession(*streams)
|
153
|
+
self.session = await self._session_context.__aenter__()
|
154
|
+
|
155
|
+
# Initialize
|
156
|
+
await self.session.initialize()
|
157
|
+
|
158
|
+
# List available tools
|
159
|
+
logger.debug("Listing tools...")
|
160
|
+
response = await self.session.list_tools()
|
161
|
+
tools_data = response.tools
|
162
|
+
logger.debug(f"Found {len(tools_data)} tools: {[tool.name for tool in tools_data]}")
|
163
|
+
|
164
|
+
# Create tool wrappers
|
165
|
+
tools = []
|
166
|
+
for tool in tools_data:
|
167
|
+
input_schema = tool.inputSchema if hasattr(tool, 'inputSchema') else None
|
168
|
+
wrapper = SSEMCPTool(
|
169
|
+
name=tool.name,
|
170
|
+
description=tool.description if hasattr(tool, 'description') else f"Call the {tool.name} tool",
|
171
|
+
session=self.session,
|
172
|
+
input_schema=input_schema
|
173
|
+
)
|
174
|
+
tools.append(wrapper)
|
175
|
+
|
176
|
+
return tools
|
177
|
+
|
178
|
+
def __iter__(self):
|
179
|
+
"""Return an iterator over the tools."""
|
180
|
+
return iter(self.tools)
|
181
|
+
|
182
|
+
def to_openai_tools(self):
|
183
|
+
"""Convert all tools to OpenAI format."""
|
184
|
+
return [tool.to_openai_tool() for tool in self.tools]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: praisonaiagents
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.78
|
4
4
|
Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
|
5
5
|
Author: Mervin Praison
|
6
6
|
Requires-Dist: pydantic
|
@@ -19,8 +19,12 @@ Requires-Dist: chonkie>=1.0.2; extra == "knowledge"
|
|
19
19
|
Provides-Extra: llm
|
20
20
|
Requires-Dist: litellm>=1.50.0; extra == "llm"
|
21
21
|
Requires-Dist: pydantic>=2.4.2; extra == "llm"
|
22
|
+
Provides-Extra: api
|
23
|
+
Requires-Dist: fastapi>=0.115.0; extra == "api"
|
24
|
+
Requires-Dist: uvicorn>=0.34.0; extra == "api"
|
22
25
|
Provides-Extra: all
|
23
26
|
Requires-Dist: praisonaiagents[memory]; extra == "all"
|
24
27
|
Requires-Dist: praisonaiagents[knowledge]; extra == "all"
|
25
28
|
Requires-Dist: praisonaiagents[llm]; extra == "all"
|
26
29
|
Requires-Dist: praisonaiagents[mcp]; extra == "all"
|
30
|
+
Requires-Dist: praisonaiagents[api]; extra == "all"
|
@@ -1,7 +1,7 @@
|
|
1
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
|
-
praisonaiagents/agent/agent.py,sha256=
|
4
|
+
praisonaiagents/agent/agent.py,sha256=mkZOHL_qgZaiX_DXTgiDlMIfjTmrqeMpi088VCC_YOo,73946
|
5
5
|
praisonaiagents/agent/image_agent.py,sha256=-5MXG594HVwSpFMcidt16YBp7udtik-Cp7eXlzLE1fY,8696
|
6
6
|
praisonaiagents/agents/__init__.py,sha256=_1d6Pqyk9EoBSo7E68sKyd1jDRlN1vxvVIRpoMc0Jcw,168
|
7
7
|
praisonaiagents/agents/agents.py,sha256=uAOHyn77noFvg3sYVFRhQUuc1LDpCMpfLND8CKOXAd4,37971
|
@@ -10,9 +10,10 @@ praisonaiagents/knowledge/__init__.py,sha256=xL1Eh-a3xsHyIcU4foOWF-JdWYIYBALJH9b
|
|
10
10
|
praisonaiagents/knowledge/chunking.py,sha256=G6wyHa7_8V0_7VpnrrUXbEmUmptlT16ISJYaxmkSgmU,7678
|
11
11
|
praisonaiagents/knowledge/knowledge.py,sha256=Po0JZsgjYJrXdNSggmUGOWidZEF0f8xo4nhsZZfh8tY,13217
|
12
12
|
praisonaiagents/llm/__init__.py,sha256=ttPQQJQq6Tah-0updoEXDZFKWtJAM93rBWRoIgxRWO8,689
|
13
|
-
praisonaiagents/llm/llm.py,sha256=
|
14
|
-
praisonaiagents/mcp/__init__.py,sha256=
|
15
|
-
praisonaiagents/mcp/mcp.py,sha256=
|
13
|
+
praisonaiagents/llm/llm.py,sha256=5SII0qUgaVbDTHdNfq4foV_vAjSwilz9Mw6p_S5LZfk,88393
|
14
|
+
praisonaiagents/mcp/__init__.py,sha256=ibbqe3_7XB7VrIcUcetkZiUZS1fTVvyMy_AqCSFG8qc,240
|
15
|
+
praisonaiagents/mcp/mcp.py,sha256=Fub1x-LroOs8pQhmOLSislBLnoHHGzR0ARGyUTUMzsM,16270
|
16
|
+
praisonaiagents/mcp/mcp_sse.py,sha256=xi5auCf83GMnsXzNEu5ooxNKjSdzKM5plB_DBn4RLzQ,6565
|
16
17
|
praisonaiagents/memory/memory.py,sha256=I8dOTkrl1i-GgQbDcrFOsSruzJ7MiI6Ys37DK27wrUs,35537
|
17
18
|
praisonaiagents/process/__init__.py,sha256=lkYbL7Hn5a0ldvJtkdH23vfIIZLIcanK-65C0MwaorY,52
|
18
19
|
praisonaiagents/process/process.py,sha256=HPw84OhnKQW3EyrDkpoQu0DcpxThbrzR2hWUgwQh9Pw,59955
|
@@ -39,7 +40,7 @@ praisonaiagents/tools/xml_tools.py,sha256=iYTMBEk5l3L3ryQ1fkUnNVYK-Nnua2Kx2S0dxN
|
|
39
40
|
praisonaiagents/tools/yaml_tools.py,sha256=uogAZrhXV9O7xvspAtcTfpKSQYL2nlOTvCQXN94-G9A,14215
|
40
41
|
praisonaiagents/tools/yfinance_tools.py,sha256=s2PBj_1v7oQnOobo2fDbQBACEHl61ftG4beG6Z979ZE,8529
|
41
42
|
praisonaiagents/tools/train/data/generatecot.py,sha256=H6bNh-E2hqL5MW6kX3hqZ05g9ETKN2-kudSjiuU_SD8,19403
|
42
|
-
praisonaiagents-0.0.
|
43
|
-
praisonaiagents-0.0.
|
44
|
-
praisonaiagents-0.0.
|
45
|
-
praisonaiagents-0.0.
|
43
|
+
praisonaiagents-0.0.78.dist-info/METADATA,sha256=VwX1r4Ib2OxhTYVqSHpEluxljiGzNy7eifYi6wBRbTk,1149
|
44
|
+
praisonaiagents-0.0.78.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
45
|
+
praisonaiagents-0.0.78.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
|
46
|
+
praisonaiagents-0.0.78.dist-info/RECORD,,
|
File without changes
|