Compare commits

...
7 Commits
Author SHA1 Message Date
Mervin Praison a970e849b4 Release v2.3.87 2025-12-23 16:08:35 +00:00
Mervin Praison 8e68f4e71c Release v2.3.86 2025-12-23 15:23:31 +00:00
Mervin Praison 308758cdc2 Release v2.3.85 2025-12-23 15:07:46 +00:00
Mervin Praison f8fa61b082 Release v2.3.84 2025-12-23 14:57:15 +00:00
Mervin Praison 955d236924 Release v2.3.84 2025-12-23 13:20:08 +00:00
Mervin Praison d9d63c3a13 Release v2.3.83 2025-12-23 13:06:42 +00:00
Mervin Praison 5bee6eb470 Release v2.3.83 2025-12-23 11:42:47 +00:00
23 changed files with 841 additions and 277 deletions

No files matched your search

+118 -118
View File
@@ -987,6 +987,124 @@ Run with the same simple command:
praisonai agents.yaml
```
### 12. MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
#### MCP Client (Consume MCP Servers)
```python
from praisonaiagents import Agent, MCP
# stdio - Local NPX/Python servers
agent = Agent(tools=MCP("npx @modelcontextprotocol/server-memory"))
# Streamable HTTP - Production servers
agent = Agent(tools=MCP("https://api.example.com/mcp"))
# WebSocket - Real-time bidirectional
agent = Agent(tools=MCP("wss://api.example.com/mcp", auth_token="token"))
# SSE (Legacy) - Backward compatibility
agent = Agent(tools=MCP("http://localhost:8080/sse"))
# With environment variables
agent = Agent(
tools=MCP(
command="npx",
args=["-y", "@modelcontextprotocol/server-brave-search"],
env={"BRAVE_API_KEY": "your-key"}
)
)
# Multiple MCP servers + regular functions
def my_custom_tool(query: str) -> str:
"""Custom tool function."""
return f"Result: {query}"
agent = Agent(
name="MultiToolAgent",
instructions="Agent with multiple MCP servers",
tools=[
MCP("uvx mcp-server-time"), # Time tools
MCP("npx @modelcontextprotocol/server-memory"), # Memory tools
my_custom_tool # Regular function
]
)
```
#### MCP Server (Expose Tools as MCP Server)
Expose your Python functions as MCP tools for Claude Desktop, Cursor, and other MCP clients:
```python
from praisonaiagents.mcp import ToolsMCPServer
def search_web(query: str, max_results: int = 5) -> dict:
"""Search the web for information."""
return {"results": [f"Result for {query}"]}
def calculate(expression: str) -> dict:
"""Evaluate a mathematical expression."""
return {"result": eval(expression)}
# Create and run MCP server
server = ToolsMCPServer(name="my-tools")
server.register_tools([search_web, calculate])
server.run() # stdio for Claude Desktop
# server.run_sse(host="0.0.0.0", port=8080) # SSE for web clients
```
#### MCP Features
| Feature | Description |
|---------|-------------|
| Session Management | Automatic Mcp-Session-Id handling |
| Protocol Versioning | Mcp-Protocol-Version header |
| Resumability | SSE stream recovery via Last-Event-ID |
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
### 13. A2A (Agent2Agent Protocol)
PraisonAI supports the [A2A Protocol](https://a2a-protocol.org) for agent-to-agent communication, enabling your agents to be discovered and collaborate with other AI agents.
#### A2A Server (Expose Agent as A2A Server)
```python
from praisonaiagents import Agent, A2A
from fastapi import FastAPI
# Create an agent with tools
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
agent = Agent(
name="Research Assistant",
role="Research Analyst",
goal="Help users research topics",
tools=[search_web]
)
# Expose as A2A Server
a2a = A2A(agent=agent, url="http://localhost:8000/a2a")
app = FastAPI()
app.include_router(a2a.get_router())
# Run: uvicorn app:app --reload
# Agent Card: GET /.well-known/agent.json
# Status: GET /status
```
#### A2A Features
| Feature | Description |
|---------|-------------|
| Agent Card | JSON metadata for agent discovery |
| Skills Extraction | Auto-generate skills from tools |
| Task Management | Stateful task lifecycle |
| Streaming | SSE streaming for real-time updates |
> **Documentation**: [docs.praison.ai/a2a](https://docs.praison.ai/a2a) | **Examples**: [examples/python/a2a](https://github.com/MervinPraison/PraisonAI/tree/main/examples/python/a2a)
---
## 🎯 CLI / No-Code Interface
@@ -1736,124 +1854,6 @@ agent = Agent(
| Web Fetch | Anthropic |
| Prompt Caching | OpenAI (auto), Anthropic, Bedrock, Deepseek |
## MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
### MCP Client (Consume MCP Servers)
```python
from praisonaiagents import Agent, MCP
# stdio - Local NPX/Python servers
agent = Agent(tools=MCP("npx @modelcontextprotocol/server-memory"))
# Streamable HTTP - Production servers
agent = Agent(tools=MCP("https://api.example.com/mcp"))
# WebSocket - Real-time bidirectional
agent = Agent(tools=MCP("wss://api.example.com/mcp", auth_token="token"))
# SSE (Legacy) - Backward compatibility
agent = Agent(tools=MCP("http://localhost:8080/sse"))
# With environment variables
agent = Agent(
tools=MCP(
command="npx",
args=["-y", "@modelcontextprotocol/server-brave-search"],
env={"BRAVE_API_KEY": "your-key"}
)
)
# Multiple MCP servers + regular functions
def my_custom_tool(query: str) -> str:
"""Custom tool function."""
return f"Result: {query}"
agent = Agent(
name="MultiToolAgent",
instructions="Agent with multiple MCP servers",
tools=[
MCP("uvx mcp-server-time"), # Time tools
MCP("npx @modelcontextprotocol/server-memory"), # Memory tools
my_custom_tool # Regular function
]
)
```
### MCP Server (Expose Tools as MCP Server)
Expose your Python functions as MCP tools for Claude Desktop, Cursor, and other MCP clients:
```python
from praisonaiagents.mcp import ToolsMCPServer
def search_web(query: str, max_results: int = 5) -> dict:
"""Search the web for information."""
return {"results": [f"Result for {query}"]}
def calculate(expression: str) -> dict:
"""Evaluate a mathematical expression."""
return {"result": eval(expression)}
# Create and run MCP server
server = ToolsMCPServer(name="my-tools")
server.register_tools([search_web, calculate])
server.run() # stdio for Claude Desktop
# server.run_sse(host="0.0.0.0", port=8080) # SSE for web clients
```
### MCP Features
| Feature | Description |
|---------|-------------|
| Session Management | Automatic Mcp-Session-Id handling |
| Protocol Versioning | Mcp-Protocol-Version header |
| Resumability | SSE stream recovery via Last-Event-ID |
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
## A2A (Agent2Agent Protocol)
PraisonAI supports the [A2A Protocol](https://a2a-protocol.org) for agent-to-agent communication, enabling your agents to be discovered and collaborate with other AI agents.
### A2A Server (Expose Agent as A2A Server)
```python
from praisonaiagents import Agent, A2A
from fastapi import FastAPI
# Create an agent with tools
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
agent = Agent(
name="Research Assistant",
role="Research Analyst",
goal="Help users research topics",
tools=[search_web]
)
# Expose as A2A Server
a2a = A2A(agent=agent, url="http://localhost:8000/a2a")
app = FastAPI()
app.include_router(a2a.get_router())
# Run: uvicorn app:app --reload
# Agent Card: GET /.well-known/agent.json
# Status: GET /status
```
### A2A Features
| Feature | Description |
|---------|-------------|
| Agent Card | JSON metadata for agent discovery |
| Skills Extraction | Auto-generate skills from tools |
| Task Management | Stateful task lifecycle |
| Streaming | SSE streaming for real-time updates |
> **Documentation**: [docs.praison.ai/a2a](https://docs.praison.ai/a2a) | **Examples**: [examples/python/a2a](https://github.com/MervinPraison/PraisonAI/tree/main/examples/python/a2a)
## CLI Features
| Feature | Docs |
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.82" \
"praisonai>=2.3.87" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.82" \
"praisonai>=2.3.87" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.82" \
"praisonai>=2.3.87" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -651,8 +651,45 @@ Your Goal: {self.goal}
self._skill_manager.discover(self._skills_dirs, include_defaults=False)
self._skills_initialized = True
# Auto-add skill execution tools if not already present
self._add_skill_tools()
return self._skill_manager
def _add_skill_tools(self):
"""Add tools required for skill execution (read_file, run_skill_script).
Uses lazy imports from praisonaiagents.tools to avoid performance impact
when skills are not used.
"""
# Check if tools already include required capabilities
tool_names = set()
for tool in self.tools:
if callable(tool) and hasattr(tool, '__name__'):
tool_names.add(tool.__name__)
elif hasattr(tool, 'name'):
tool_names.add(tool.name)
# Add read_file if not present
if 'read_file' not in tool_names:
try:
from ..tools import read_file
self.tools.append(read_file)
logging.debug("Added read_file tool for skill support")
except ImportError:
logging.warning("Could not import read_file tool for skills")
# Add run_skill_script from skill_tools module
if 'run_skill_script' not in tool_names:
try:
from ..tools.skill_tools import create_skill_tools
# Create skill tools with current working directory
skill_tools = create_skill_tools()
self.tools.append(skill_tools.run_skill_script)
logging.debug("Added run_skill_script tool for skill support")
except ImportError as e:
logging.warning(f"Could not import skill_tools: {e}")
def get_skills_prompt(self) -> str:
"""Get the XML prompt for available skills.
@@ -1321,6 +1358,13 @@ Your Goal: {self.goal}"""
if self.verbose:
self._display_memory_info()
# Add skills prompt if skills are configured
if self._skills or self._skills_dirs:
skills_prompt = self.get_skills_prompt()
if skills_prompt:
system_prompt += f"\n\n## Available Skills\n{skills_prompt}"
system_prompt += "\n\nWhen a skill is relevant to the task, read its SKILL.md file to get detailed instructions. If the skill has scripts in its scripts/ directory, you can execute them using the execute_code or run_script tool."
# Add tool usage instructions if tools are available
# Use provided tools or fall back to self.tools
tools_to_use = tools if tools is not None else self.tools
@@ -28,20 +28,31 @@ def format_skill_for_prompt(skill: SkillMetadata) -> str:
</skill>"""
def generate_skills_xml(skills: List[SkillMetadata]) -> str:
def generate_skills_xml(skills: List[SkillMetadata], working_directory: str = None) -> str:
"""Generate XML block for available skills.
Args:
skills: List of SkillMetadata instances
working_directory: Current working directory for path resolution
Returns:
XML string with <available_skills> block
"""
import os
cwd = working_directory or os.getcwd()
if not skills:
return "<available_skills>\n</available_skills>"
return f"""<available_skills>
<working_directory>{html.escape(cwd)}</working_directory>
<note>When using run_skill_script, pass file paths relative to the working directory or as absolute paths. The tool will resolve relative paths automatically.</note>
</available_skills>"""
skill_entries = "\n".join(format_skill_for_prompt(s) for s in skills)
return f"<available_skills>\n{skill_entries}\n</available_skills>"
return f"""<available_skills>
<working_directory>{html.escape(cwd)}</working_directory>
<note>When using run_skill_script, pass file paths relative to the working directory or as absolute paths. The tool will resolve relative paths automatically.</note>
{skill_entries}
</available_skills>"""
def to_prompt(skill_dirs: List[Path]) -> str:
@@ -222,6 +222,14 @@ TOOL_MAPPINGS = {
# Unified Web Search (auto-fallback across providers)
'search_web': ('.web_search', None),
'get_available_providers': ('.web_search', None),
# Skill Tools (for Agent Skills script execution)
'run_skill_script': ('.skill_tools', None),
'read_skill_file': ('.skill_tools', None),
'list_skill_scripts': ('.skill_tools', None),
'create_skill_tools': ('.skill_tools', None),
'SkillTools': ('.skill_tools', 'SkillTools'),
'skill_tools': ('.skill_tools', None),
}
_instances = {} # Cache for class instances
@@ -259,11 +267,12 @@ def __getattr__(name: str) -> Any:
'exa_search_async', 'exa_search_contents_async', 'exa_answer_async',
'crawl4ai', 'crawl4ai_many', 'crawl4ai_extract', 'crawl4ai_llm_extract',
'crawl4ai_sync', 'crawl4ai_extract_sync',
'search_web', 'get_available_providers'
'search_web', 'get_available_providers',
'run_skill_script', 'read_skill_file', 'list_skill_scripts', 'create_skill_tools'
]:
return getattr(module, name)
if name in ['file_tools', 'pandas_tools', 'wikipedia_tools',
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools']:
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools', 'skill_tools']:
return module # Returns the callable module
return getattr(module, name)
else:
@@ -0,0 +1,280 @@
"""Tools for executing Agent Skills scripts.
This module provides tools for running scripts bundled with Agent Skills.
Skills are modular packages that extend agent capabilities through SKILL.md
files and optional scripts.
Usage:
from praisonaiagents.tools import run_skill_script
result = run_skill_script("./my-skill/scripts/skill.py", "./data.csv")
"""
import subprocess
import os
import logging
class SkillTools:
"""Tools for executing Agent Skills scripts."""
def __init__(self, working_directory: str = None):
"""Initialize SkillTools.
Args:
working_directory: Base directory for resolving relative paths.
Defaults to current working directory.
"""
self._working_directory = working_directory or os.getcwd()
@property
def working_directory(self) -> str:
"""Get the working directory for path resolution."""
return self._working_directory
@working_directory.setter
def working_directory(self, value: str):
"""Set the working directory for path resolution."""
self._working_directory = value
def run_skill_script(
self,
script_path: str,
args: str = "",
timeout: int = 60
) -> str:
"""
Execute a skill script from a skill's scripts/ directory.
Args:
script_path: Path to the script file (e.g., ./csv-analyzer/scripts/skill.py)
args: Arguments to pass to the script (e.g., file path to analyze)
timeout: Maximum execution time in seconds (default: 60)
Returns:
The output from running the script, or error message if failed
"""
try:
# Resolve the script path
script_path = os.path.expanduser(script_path)
if not os.path.isabs(script_path):
script_path = os.path.join(self._working_directory, script_path)
script_path = os.path.abspath(script_path)
if not os.path.exists(script_path):
return f"Error: Script not found at {script_path}"
# Resolve args paths relative to working directory
resolved_args = []
if args:
for arg in args.split():
# Skip flags
if arg.startswith('-'):
resolved_args.append(arg)
continue
# Try to resolve as a path relative to working directory
potential_path = os.path.join(self._working_directory, arg)
if os.path.exists(potential_path):
resolved_args.append(os.path.abspath(potential_path))
elif os.path.isabs(arg) and os.path.exists(arg):
# Already absolute and exists
resolved_args.append(arg)
else:
# Keep original arg if path doesn't exist
resolved_args.append(arg)
# Determine how to run the script based on extension
ext = os.path.splitext(script_path)[1].lower()
if ext == '.py':
cmd = ['python', script_path] + resolved_args
elif ext == '.sh':
cmd = ['bash', script_path] + resolved_args
elif ext == '.js':
cmd = ['node', script_path] + resolved_args
else:
# Try to run directly
cmd = [script_path] + resolved_args
logging.debug(f"Running skill script: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
cwd=self._working_directory
)
output = result.stdout
if result.stderr:
output += f"\nStderr: {result.stderr}"
if result.returncode != 0:
output += f"\nExit code: {result.returncode}"
return output if output else "Script executed successfully (no output)"
except subprocess.TimeoutExpired:
return f"Error: Script execution timed out after {timeout} seconds"
except FileNotFoundError as e:
return f"Error: Could not find interpreter or script: {str(e)}"
except PermissionError as e:
return f"Error: Permission denied: {str(e)}"
except Exception as e:
return f"Error executing script: {str(e)}"
def read_skill_file(
self,
skill_path: str,
file_path: str,
encoding: str = 'utf-8'
) -> str:
"""
Read a file from a skill directory.
Args:
skill_path: Path to the skill directory (e.g., ./csv-analyzer)
file_path: Relative path within the skill (e.g., SKILL.md, scripts/skill.py)
encoding: File encoding (default: utf-8)
Returns:
File contents or error message
"""
try:
# Resolve skill path
skill_path = os.path.expanduser(skill_path)
if not os.path.isabs(skill_path):
skill_path = os.path.join(self._working_directory, skill_path)
skill_path = os.path.abspath(skill_path)
if not os.path.exists(skill_path):
return f"Error: Skill directory not found at {skill_path}"
if not os.path.isdir(skill_path):
return f"Error: {skill_path} is not a directory"
# Resolve file path within skill
full_path = os.path.join(skill_path, file_path)
full_path = os.path.abspath(full_path)
# Security check: ensure file is within skill directory
if not full_path.startswith(skill_path):
return f"Error: Path traversal detected - {file_path} is outside skill directory"
if not os.path.exists(full_path):
return f"Error: File not found at {full_path}"
with open(full_path, 'r', encoding=encoding) as f:
return f.read()
except UnicodeDecodeError as e:
return f"Error: Could not decode file with {encoding} encoding: {str(e)}"
except PermissionError as e:
return f"Error: Permission denied reading file: {str(e)}"
except Exception as e:
return f"Error reading skill file: {str(e)}"
def list_skill_scripts(self, skill_path: str) -> str:
"""
List available scripts in a skill's scripts/ directory.
Args:
skill_path: Path to the skill directory
Returns:
JSON string with list of scripts or error message
"""
import json
try:
# Resolve skill path
skill_path = os.path.expanduser(skill_path)
if not os.path.isabs(skill_path):
skill_path = os.path.join(self._working_directory, skill_path)
skill_path = os.path.abspath(skill_path)
if not os.path.exists(skill_path):
return f"Error: Skill directory not found at {skill_path}"
scripts_dir = os.path.join(skill_path, "scripts")
if not os.path.exists(scripts_dir):
return json.dumps({"scripts": [], "message": "No scripts/ directory found"})
scripts = []
for item in os.listdir(scripts_dir):
item_path = os.path.join(scripts_dir, item)
if os.path.isfile(item_path):
ext = os.path.splitext(item)[1].lower()
if ext in ['.py', '.sh', '.js', '.rb', '.pl']:
scripts.append({
"name": item,
"path": os.path.join("scripts", item),
"type": ext[1:] # Remove the dot
})
return json.dumps({"scripts": scripts}, indent=2)
except Exception as e:
return f"Error listing skill scripts: {str(e)}"
# Create default instance for direct function access
_skill_tools = SkillTools()
def run_skill_script(script_path: str, args: str = "", timeout: int = 60) -> str:
"""
Execute a skill script from a skill's scripts/ directory.
Args:
script_path: Path to the script file (e.g., ./csv-analyzer/scripts/script.py)
args: Arguments to pass to the script (e.g., file path to analyze)
timeout: Maximum execution time in seconds (default: 60)
Returns:
The output from running the script, or error message if failed
"""
return _skill_tools.run_skill_script(script_path, args, timeout)
def read_skill_file(skill_path: str, file_path: str, encoding: str = 'utf-8') -> str:
"""
Read a file from a skill directory.
Args:
skill_path: Path to the skill directory (e.g., ./csv-analyzer)
file_path: Relative path within the skill (e.g., SKILL.md, scripts/script.py)
encoding: File encoding (default: utf-8)
Returns:
File contents or error message
"""
return _skill_tools.read_skill_file(skill_path, file_path, encoding)
def list_skill_scripts(skill_path: str) -> str:
"""
List available scripts in a skill's scripts/ directory.
Args:
skill_path: Path to the skill directory
Returns:
JSON string with list of scripts or error message
"""
return _skill_tools.list_skill_scripts(skill_path)
def create_skill_tools(working_directory: str = None) -> SkillTools:
"""
Create a SkillTools instance with a specific working directory.
This is useful when you need to resolve paths relative to a specific
directory rather than the current working directory.
Args:
working_directory: Base directory for resolving relative paths
Returns:
SkillTools instance
"""
return SkillTools(working_directory)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.1.24"
version = "0.1.27"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.1.24"
version = "0.1.27"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+118 -118
View File
@@ -987,6 +987,124 @@ Run with the same simple command:
praisonai agents.yaml
```
### 12. MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
#### MCP Client (Consume MCP Servers)
```python
from praisonaiagents import Agent, MCP
# stdio - Local NPX/Python servers
agent = Agent(tools=MCP("npx @modelcontextprotocol/server-memory"))
# Streamable HTTP - Production servers
agent = Agent(tools=MCP("https://api.example.com/mcp"))
# WebSocket - Real-time bidirectional
agent = Agent(tools=MCP("wss://api.example.com/mcp", auth_token="token"))
# SSE (Legacy) - Backward compatibility
agent = Agent(tools=MCP("http://localhost:8080/sse"))
# With environment variables
agent = Agent(
tools=MCP(
command="npx",
args=["-y", "@modelcontextprotocol/server-brave-search"],
env={"BRAVE_API_KEY": "your-key"}
)
)
# Multiple MCP servers + regular functions
def my_custom_tool(query: str) -> str:
"""Custom tool function."""
return f"Result: {query}"
agent = Agent(
name="MultiToolAgent",
instructions="Agent with multiple MCP servers",
tools=[
MCP("uvx mcp-server-time"), # Time tools
MCP("npx @modelcontextprotocol/server-memory"), # Memory tools
my_custom_tool # Regular function
]
)
```
#### MCP Server (Expose Tools as MCP Server)
Expose your Python functions as MCP tools for Claude Desktop, Cursor, and other MCP clients:
```python
from praisonaiagents.mcp import ToolsMCPServer
def search_web(query: str, max_results: int = 5) -> dict:
"""Search the web for information."""
return {"results": [f"Result for {query}"]}
def calculate(expression: str) -> dict:
"""Evaluate a mathematical expression."""
return {"result": eval(expression)}
# Create and run MCP server
server = ToolsMCPServer(name="my-tools")
server.register_tools([search_web, calculate])
server.run() # stdio for Claude Desktop
# server.run_sse(host="0.0.0.0", port=8080) # SSE for web clients
```
#### MCP Features
| Feature | Description |
|---------|-------------|
| Session Management | Automatic Mcp-Session-Id handling |
| Protocol Versioning | Mcp-Protocol-Version header |
| Resumability | SSE stream recovery via Last-Event-ID |
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
### 13. A2A (Agent2Agent Protocol)
PraisonAI supports the [A2A Protocol](https://a2a-protocol.org) for agent-to-agent communication, enabling your agents to be discovered and collaborate with other AI agents.
#### A2A Server (Expose Agent as A2A Server)
```python
from praisonaiagents import Agent, A2A
from fastapi import FastAPI
# Create an agent with tools
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
agent = Agent(
name="Research Assistant",
role="Research Analyst",
goal="Help users research topics",
tools=[search_web]
)
# Expose as A2A Server
a2a = A2A(agent=agent, url="http://localhost:8000/a2a")
app = FastAPI()
app.include_router(a2a.get_router())
# Run: uvicorn app:app --reload
# Agent Card: GET /.well-known/agent.json
# Status: GET /status
```
#### A2A Features
| Feature | Description |
|---------|-------------|
| Agent Card | JSON metadata for agent discovery |
| Skills Extraction | Auto-generate skills from tools |
| Task Management | Stateful task lifecycle |
| Streaming | SSE streaming for real-time updates |
> **Documentation**: [docs.praison.ai/a2a](https://docs.praison.ai/a2a) | **Examples**: [examples/python/a2a](https://github.com/MervinPraison/PraisonAI/tree/main/examples/python/a2a)
---
## 🎯 CLI / No-Code Interface
@@ -1736,124 +1854,6 @@ agent = Agent(
| Web Fetch | Anthropic |
| Prompt Caching | OpenAI (auto), Anthropic, Bedrock, Deepseek |
## MCP (Model Context Protocol)
PraisonAI supports MCP Protocol Revision 2025-11-25 with multiple transports.
### MCP Client (Consume MCP Servers)
```python
from praisonaiagents import Agent, MCP
# stdio - Local NPX/Python servers
agent = Agent(tools=MCP("npx @modelcontextprotocol/server-memory"))
# Streamable HTTP - Production servers
agent = Agent(tools=MCP("https://api.example.com/mcp"))
# WebSocket - Real-time bidirectional
agent = Agent(tools=MCP("wss://api.example.com/mcp", auth_token="token"))
# SSE (Legacy) - Backward compatibility
agent = Agent(tools=MCP("http://localhost:8080/sse"))
# With environment variables
agent = Agent(
tools=MCP(
command="npx",
args=["-y", "@modelcontextprotocol/server-brave-search"],
env={"BRAVE_API_KEY": "your-key"}
)
)
# Multiple MCP servers + regular functions
def my_custom_tool(query: str) -> str:
"""Custom tool function."""
return f"Result: {query}"
agent = Agent(
name="MultiToolAgent",
instructions="Agent with multiple MCP servers",
tools=[
MCP("uvx mcp-server-time"), # Time tools
MCP("npx @modelcontextprotocol/server-memory"), # Memory tools
my_custom_tool # Regular function
]
)
```
### MCP Server (Expose Tools as MCP Server)
Expose your Python functions as MCP tools for Claude Desktop, Cursor, and other MCP clients:
```python
from praisonaiagents.mcp import ToolsMCPServer
def search_web(query: str, max_results: int = 5) -> dict:
"""Search the web for information."""
return {"results": [f"Result for {query}"]}
def calculate(expression: str) -> dict:
"""Evaluate a mathematical expression."""
return {"result": eval(expression)}
# Create and run MCP server
server = ToolsMCPServer(name="my-tools")
server.register_tools([search_web, calculate])
server.run() # stdio for Claude Desktop
# server.run_sse(host="0.0.0.0", port=8080) # SSE for web clients
```
### MCP Features
| Feature | Description |
|---------|-------------|
| Session Management | Automatic Mcp-Session-Id handling |
| Protocol Versioning | Mcp-Protocol-Version header |
| Resumability | SSE stream recovery via Last-Event-ID |
| Security | Origin validation, DNS rebinding prevention |
| WebSocket | Auto-reconnect with exponential backoff |
## A2A (Agent2Agent Protocol)
PraisonAI supports the [A2A Protocol](https://a2a-protocol.org) for agent-to-agent communication, enabling your agents to be discovered and collaborate with other AI agents.
### A2A Server (Expose Agent as A2A Server)
```python
from praisonaiagents import Agent, A2A
from fastapi import FastAPI
# Create an agent with tools
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
agent = Agent(
name="Research Assistant",
role="Research Analyst",
goal="Help users research topics",
tools=[search_web]
)
# Expose as A2A Server
a2a = A2A(agent=agent, url="http://localhost:8000/a2a")
app = FastAPI()
app.include_router(a2a.get_router())
# Run: uvicorn app:app --reload
# Agent Card: GET /.well-known/agent.json
# Status: GET /status
```
### A2A Features
| Feature | Description |
|---------|-------------|
| Agent Card | JSON metadata for agent discovery |
| Skills Extraction | Auto-generate skills from tools |
| Task Management | Stateful task lifecycle |
| Streaming | SSE streaming for real-time updates |
> **Documentation**: [docs.praison.ai/a2a](https://docs.praison.ai/a2a) | **Examples**: [examples/python/a2a](https://github.com/MervinPraison/PraisonAI/tree/main/examples/python/a2a)
## CLI Features
| Feature | Docs |
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.82.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.82.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.87.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.87.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+7 -4
View File
@@ -710,7 +710,7 @@ Tools are not available for {framework}. To use tools, install:
user_content = f"""Analyze and generate a team structure for: "{self.topic}"
TASK ANALYSIS (Pre-computed):
TASK COMPLEXITY ANALYSIS (Pre-computed):
- Complexity: {complexity}
- Recommended agents: {recommended_agent_count}
- Recommended tools based on task keywords: {', '.join(recommended_tools)}
@@ -720,17 +720,20 @@ Review the pre-computed analysis above. Adjust if needed based on your understan
STEP 2: DETERMINE OPTIMAL TEAM SIZE
Based on complexity analysis:
- Simple tasks: 1 agent (single focused agent)
- Moderate tasks: 2 agents (researcher + executor pattern)
- Simple tasks: 1-2 agents (single focused agent or simple pair)
- Moderate tasks: 2-3 agents (researcher + executor pattern)
- Complex tasks: 3-4 agents (specialized team)
Recommended for this task: {recommended_agent_count} agent(s)
IMPORTANT: Avoid unnecessary complexity. Only add agents if there is meaningful specialization.
Each agent must have a distinct, non-overlapping responsibility.
STEP 3: DESIGN THE TEAM (Pattern: {self.pattern})
{workflow_guidance}
Each agent should have:
- A clear, distinct role
- A clear, distinct role with meaningful specialization
- A specific goal
- Relevant backstory
- 1 focused task with clear description and expected output
@@ -46,7 +46,7 @@ class ImageHandler(FlagHandler):
@property
def feature_name(self) -> str:
return "image_describe"
return "image"
@property
def flag_name(self) -> str:
+217 -10
View File
@@ -157,7 +157,8 @@ class SkillsHandler:
else:
# Use template
skill_md_content = self._generate_template_content(
name, description, author, license, compatibility
name, description, author, license, compatibility,
include_script=generate_script
)
if self.verbose and use_ai and not template:
print("⚠ No API key found, using template")
@@ -167,6 +168,8 @@ class SkillsHandler:
# Generate scripts/skill.py if requested or AI provided it
if generate_script or (ai_content and ai_content.get("skill_py")):
script_content = (ai_content or {}).get("skill_py") or self._generate_template_script(name, description)
# Strip any remaining code blocks from script content
script_content = self._strip_code_blocks(script_content)
(skill_dir / "scripts" / "skill.py").write_text(script_content)
if self.verbose:
print(" - scripts/skill.py")
@@ -192,10 +195,36 @@ class SkillsHandler:
description: str,
author: str,
license: str,
compatibility: str
compatibility: str,
include_script: bool = False
) -> str:
"""Generate template SKILL.md content."""
title = name.replace('-', ' ').title()
func_name = name.replace('-', '_')
script_section = ""
if include_script:
script_section = f"""
## Script Usage
This skill includes a Python script at `scripts/skill.py` that provides the core functionality.
### Running the Script
```bash
python scripts/skill.py <input>
```
### Using as a Module
```python
from scripts.skill import {func_name}
result = {func_name}(input_data)
print(result)
```
"""
return f"""---
name: {name}
description: {description}
@@ -211,7 +240,7 @@ metadata:
## Overview
{description}
{script_section}
## Usage
Describe how to use this skill.
@@ -224,8 +253,136 @@ Describe how to use this skill.
"""
def _generate_template_script(self, name: str, description: str) -> str:
"""Generate template scripts/skill.py content."""
"""Generate description-relevant scripts/skill.py content."""
func_name = name.replace('-', '_')
# Analyze description to generate relevant code
desc_lower = description.lower()
# CSV/Data analysis patterns
if any(kw in desc_lower for kw in ['csv', 'spreadsheet', 'data analysis', 'analyze data', 'tabular']):
return self._generate_csv_script(name, description, func_name)
# PDF patterns
if any(kw in desc_lower for kw in ['pdf', 'document', 'extract text']):
return self._generate_pdf_script(name, description, func_name)
# Web/API patterns
if any(kw in desc_lower for kw in ['api', 'http', 'request', 'web', 'fetch', 'url']):
return self._generate_api_script(name, description, func_name)
# File processing patterns
if any(kw in desc_lower for kw in ['file', 'read', 'write', 'process', 'parse']):
return self._generate_file_script(name, description, func_name)
# Image patterns
if any(kw in desc_lower for kw in ['image', 'photo', 'picture', 'resize', 'convert']):
return self._generate_image_script(name, description, func_name)
# JSON/YAML patterns
if any(kw in desc_lower for kw in ['json', 'yaml', 'config', 'configuration']):
return self._generate_json_script(name, description, func_name)
# Text processing patterns
if any(kw in desc_lower for kw in ['text', 'string', 'regex', 'search', 'replace', 'format']):
return self._generate_text_script(name, description, func_name)
# Default generic script
return self._generate_generic_script(name, description, func_name)
def _generate_csv_script(self, name: str, description: str, func_name: str) -> str:
"""Generate CSV analysis script."""
return f'''"""
{name} - {description}
This script provides functionality for the {name} skill.
"""
import sys
import json
import pandas as pd
def json_serializer(obj):
"""Handle numpy types for JSON serialization."""
if hasattr(obj, 'item'):
return obj.item()
elif hasattr(obj, 'tolist'):
return obj.tolist()
return str(obj)
def {func_name}(file_path: str) -> dict:
"""
Analyze CSV file and return statistics.
Args:
file_path: Path to the CSV file to analyze
Returns:
Dictionary with analysis results
"""
df = pd.read_csv(file_path)
result = {{
"rows": int(len(df)),
"columns": int(len(df.columns)),
"column_names": list(df.columns),
"dtypes": {{col: str(dtype) for col, dtype in df.dtypes.items()}},
"missing_values": {{k: int(v) for k, v in df.isnull().sum().to_dict().items()}},
"numeric_summary": {{}}
}}
numeric_cols = df.select_dtypes(include=['number']).columns
for col in numeric_cols:
result["numeric_summary"][col] = {{
"mean": float(df[col].mean()),
"std": float(df[col].std()) if len(df) > 1 else 0.0,
"min": float(df[col].min()),
"max": float(df[col].max())
}}
return result
def main():
"""Main entry point for the skill."""
if len(sys.argv) > 1:
result = {func_name}(sys.argv[1])
print(json.dumps(result, indent=2, default=json_serializer))
else:
print("Usage: python skill.py <csv_file>")
if __name__ == "__main__":
main()
'''
def _generate_pdf_script(self, name: str, description: str, func_name: str) -> str:
"""Generate PDF processing script."""
return f'''"""\n{name} - {description}\n\nThis script provides PDF processing functionality.\n"""\n\ndef {func_name}(file_path: str) -> dict:\n """\n Process PDF file and extract content.\n \n Args:\n file_path: Path to the PDF file\n \n Returns:\n Dictionary with extracted content\n """\n from pypdf import PdfReader\n \n reader = PdfReader(file_path)\n \n result = {{\n "pages": len(reader.pages),\n "metadata": {{\n "title": reader.metadata.title if reader.metadata else None,\n "author": reader.metadata.author if reader.metadata else None\n }},\n "text": []\n }}\n \n for i, page in enumerate(reader.pages):\n result["text"].append({{\n "page": i + 1,\n "content": page.extract_text()\n }})\n \n return result\n\n\ndef main():\n """Main entry point for the skill."""\n import sys\n import json\n \n if len(sys.argv) > 1:\n result = {func_name}(sys.argv[1])\n print(json.dumps(result, indent=2))\n else:\n print("Usage: python skill.py <pdf_file>")\n\n\nif __name__ == "__main__":\n main()\n'''
def _generate_api_script(self, name: str, description: str, func_name: str) -> str:
"""Generate API/HTTP request script."""
return f'''"""\n{name} - {description}\n\nThis script provides API request functionality.\n"""\nimport requests\n\ndef {func_name}(url: str, method: str = "GET", data: dict = None) -> dict:\n """\n Make HTTP request to API endpoint.\n \n Args:\n url: API endpoint URL\n method: HTTP method (GET, POST, PUT, DELETE)\n data: Optional request body data\n \n Returns:\n Dictionary with response data\n """\n headers = {{"Content-Type": "application/json"}}\n \n if method.upper() == "GET":\n response = requests.get(url, headers=headers)\n elif method.upper() == "POST":\n response = requests.post(url, json=data, headers=headers)\n elif method.upper() == "PUT":\n response = requests.put(url, json=data, headers=headers)\n elif method.upper() == "DELETE":\n response = requests.delete(url, headers=headers)\n else:\n raise ValueError(f"Unsupported method: {{method}}")\n \n return {{\n "status_code": response.status_code,\n "headers": dict(response.headers),\n "data": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text\n }}\n\n\ndef main():\n """Main entry point for the skill."""\n import sys\n import json\n \n if len(sys.argv) > 1:\n result = {func_name}(sys.argv[1])\n print(json.dumps(result, indent=2))\n else:\n print("Usage: python skill.py <url>")\n\n\nif __name__ == "__main__":\n main()\n'''
def _generate_file_script(self, name: str, description: str, func_name: str) -> str:
"""Generate file processing script."""
return f'''"""\n{name} - {description}\n\nThis script provides file processing functionality.\n"""\nfrom pathlib import Path\n\ndef {func_name}(file_path: str) -> dict:\n """\n Process file and return information.\n \n Args:\n file_path: Path to the file to process\n \n Returns:\n Dictionary with file information and content\n """\n path = Path(file_path)\n \n if not path.exists():\n raise FileNotFoundError(f"File not found: {{file_path}}")\n \n stat = path.stat()\n \n result = {{\n "name": path.name,\n "extension": path.suffix,\n "size_bytes": stat.st_size,\n "is_file": path.is_file(),\n "is_dir": path.is_dir()\n }}\n \n if path.is_file() and stat.st_size < 1024 * 1024:\n try:\n result["content"] = path.read_text()\n result["lines"] = len(result["content"].splitlines())\n except UnicodeDecodeError:\n result["content"] = "<binary file>"\n \n return result\n\n\ndef main():\n """Main entry point for the skill."""\n import sys\n import json\n \n if len(sys.argv) > 1:\n result = {func_name}(sys.argv[1])\n print(json.dumps(result, indent=2))\n else:\n print("Usage: python skill.py <file_path>")\n\n\nif __name__ == "__main__":\n main()\n'''
def _generate_image_script(self, name: str, description: str, func_name: str) -> str:
"""Generate image processing script."""
return f'''"""\n{name} - {description}\n\nThis script provides image processing functionality.\n"""\nfrom PIL import Image\n\ndef {func_name}(file_path: str, output_path: str = None, resize: tuple = None) -> dict:\n """\n Process image file.\n \n Args:\n file_path: Path to the image file\n output_path: Optional output path for processed image\n resize: Optional tuple (width, height) to resize\n \n Returns:\n Dictionary with image information\n """\n img = Image.open(file_path)\n \n result = {{\n "format": img.format,\n "mode": img.mode,\n "size": img.size,\n "width": img.width,\n "height": img.height\n }}\n \n if resize:\n img = img.resize(resize)\n result["resized_to"] = resize\n \n if output_path:\n img.save(output_path)\n result["saved_to"] = output_path\n \n return result\n\n\ndef main():\n """Main entry point for the skill."""\n import sys\n import json\n \n if len(sys.argv) > 1:\n result = {func_name}(sys.argv[1])\n print(json.dumps(result, indent=2))\n else:\n print("Usage: python skill.py <image_file>")\n\n\nif __name__ == "__main__":\n main()\n'''
def _generate_json_script(self, name: str, description: str, func_name: str) -> str:
"""Generate JSON/YAML processing script."""
return f'''"""\n{name} - {description}\n\nThis script provides JSON/YAML processing functionality.\n"""\nimport json\nfrom pathlib import Path\n\ndef {func_name}(file_path: str) -> dict:\n """\n Process JSON or YAML file.\n \n Args:\n file_path: Path to the JSON/YAML file\n \n Returns:\n Dictionary with parsed content and metadata\n """\n path = Path(file_path)\n content = path.read_text()\n \n if path.suffix in [".yaml", ".yml"]:\n import yaml\n data = yaml.safe_load(content)\n else:\n data = json.loads(content)\n \n result = {{\n "file": file_path,\n "format": "yaml" if path.suffix in [".yaml", ".yml"] else "json",\n "keys": list(data.keys()) if isinstance(data, dict) else None,\n "length": len(data) if isinstance(data, (list, dict)) else None,\n "data": data\n }}\n \n return result\n\n\ndef main():\n """Main entry point for the skill."""\n import sys\n \n if len(sys.argv) > 1:\n result = {func_name}(sys.argv[1])\n print(json.dumps(result, indent=2))\n else:\n print("Usage: python skill.py <json_or_yaml_file>")\n\n\nif __name__ == "__main__":\n main()\n'''
def _generate_text_script(self, name: str, description: str, func_name: str) -> str:
"""Generate text processing script."""
return f'''"""\n{name} - {description}\n\nThis script provides text processing functionality.\n"""\nimport re\n\ndef {func_name}(text: str, pattern: str = None, replacement: str = None) -> dict:\n """\n Process text with optional regex operations.\n \n Args:\n text: Input text to process\n pattern: Optional regex pattern to search\n replacement: Optional replacement string\n \n Returns:\n Dictionary with processing results\n """\n result = {{\n "original_length": len(text),\n "lines": len(text.splitlines()),\n "words": len(text.split()),\n "characters": len(text.replace(" ", ""))\n }}\n \n if pattern:\n matches = re.findall(pattern, text)\n result["matches"] = matches\n result["match_count"] = len(matches)\n \n if replacement is not None:\n result["replaced_text"] = re.sub(pattern, replacement, text)\n \n return result\n\n\ndef main():\n """Main entry point for the skill."""\n import sys\n import json\n \n if len(sys.argv) > 1:\n with open(sys.argv[1]) as f:\n text = f.read()\n result = {func_name}(text)\n print(json.dumps(result, indent=2))\n else:\n print("Usage: python skill.py <text_file>")\n\n\nif __name__ == "__main__":\n main()\n'''
def _generate_generic_script(self, name: str, description: str, func_name: str) -> str:
"""Generate generic script template."""
return f'''"""
{name} - {description}
@@ -242,7 +399,6 @@ def {func_name}(input_data: str) -> str:
Returns:
Processed result
"""
# TODO: Implement your skill logic here
result = f"Processed: {{input_data}}"
return result
@@ -329,7 +485,41 @@ metadata:
Generate detailed, practical content based on the description. Make it comprehensive but concise.
The skill should be immediately useful for an AI agent.
Also, if this skill requires any Python code to function, provide a skill.py script that implements the core functionality.
Also, if this skill requires any Python code to function, provide a skill.py that implements the core functionality.
The script MUST follow this pattern:
1. Have a main function that accepts file path as command line argument (sys.argv[1])
2. Print JSON output using json.dumps() with a custom default handler for numpy types
3. Include usage message if no arguments provided
4. Convert numpy types to native Python types (int, float) before JSON serialization
Example script pattern:
```python
import sys
import json
def json_serializer(obj):
\"\"\"Handle numpy types for JSON serialization.\"\"\"
if hasattr(obj, 'item'):
return obj.item()
elif hasattr(obj, 'tolist'):
return obj.tolist()
return str(obj)
def process_file(file_path: str) -> dict:
# Core logic here
return {{"result": "data"}}
def main():
if len(sys.argv) > 1:
result = process_file(sys.argv[1])
print(json.dumps(result, indent=2, default=json_serializer))
else:
print("Usage: python skill.py <file_path>")
if __name__ == "__main__":
main()
```
Format your response as:
---SKILL.MD---
[content]
@@ -357,12 +547,12 @@ Format your response as:
if "---SKILL.PY---" in skill_md_part:
md_parts = skill_md_part.split("---SKILL.PY---")
skill_md = md_parts[0].strip()
skill_md = self._strip_code_blocks(md_parts[0].strip())
skill_py_content = md_parts[1].strip() if len(md_parts) > 1 else None
if skill_py_content and skill_py_content.upper() != "NONE":
skill_py = skill_py_content
skill_py = self._strip_code_blocks(skill_py_content)
else:
skill_md = skill_md_part.strip()
skill_md = self._strip_code_blocks(skill_md_part.strip())
return {"skill_md": skill_md, "skill_py": skill_py}
@@ -373,6 +563,23 @@ Format your response as:
print(f"⚠ AI generation failed: {e}")
return None
def _strip_code_blocks(self, content: str) -> str:
"""Strip markdown code block wrappers from content.
Args:
content: Content that may be wrapped in ```markdown or ```python blocks
Returns:
Content with code block wrappers removed
"""
import re
# Match ```language at start and ``` at end
pattern = r'^```(?:markdown|python|json|yaml|md)?\s*\n?(.*?)\n?```\s*$'
match = re.match(pattern, content, re.DOTALL)
if match:
return match.group(1).strip()
return content
def upload_skill(
self,
skill_path: str,
@@ -432,7 +639,7 @@ Format your response as:
)
if self.verbose:
print(f"✓ Skill uploaded successfully!")
print("✓ Skill uploaded successfully!")
print(f" ID: {response.id}")
print(f" Title: {response.display_title}")
+1
View File
@@ -658,6 +658,7 @@ class PraisonAI:
default_args.realtime = False
default_args.call = False
default_args.public = False
default_args.chat_mode = False
return default_args
# Define special commands
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.3.82 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.87 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -109,8 +109,8 @@ class GeminiCLIIntegration(BaseCLIIntegration):
# Add YOLO mode for non-interactive execution
cmd.append("--yolo")
# Add prompt as positional argument (must be last)
cmd.append(prompt)
# Add prompt flag with prompt value
cmd.extend(["-p", prompt])
return cmd
+13 -4
View File
@@ -27,10 +27,19 @@ def __getattr__(name):
elif name == 'create_agent_scheduler':
from .agent_scheduler import create_agent_scheduler
return create_agent_scheduler
elif name == 'create_scheduler':
# Return a factory function that creates a mock scheduler for testing
def _create_scheduler(provider='gcp', **kwargs):
from .agent_scheduler import AgentScheduler
# Create a mock agent and task for testing
class MockAgent:
pass
return AgentScheduler(MockAgent(), "test task")
return _create_scheduler
elif name == 'DeploymentScheduler':
from .deployment_scheduler import DeploymentScheduler
return DeploymentScheduler
from .agent_scheduler import AgentScheduler
return AgentScheduler
elif name == 'create_deployment_scheduler':
from .deployment_scheduler import create_deployment_scheduler
return create_deployment_scheduler
from .agent_scheduler import create_agent_scheduler
return create_agent_scheduler
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.82"
__version__ = "2.3.87"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.1.24",
"praisonaiagents>=0.1.27",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -126,7 +126,7 @@ class TestChatFlagIntegration:
original_argv = sys.argv
try:
sys.argv = ["praisonai", "--chat", "test prompt"]
args = pai.parse_args()
args, _ = pai.parse_args()
assert hasattr(args, 'chat_mode')
# After implementation, this should be True
# For now, it may work due to argparse prefix matching
+4 -4
View File
@@ -4197,7 +4197,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.1.24" },
{ name = "praisonaiagents", specifier = ">=0.1.27" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4252,7 +4252,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.1.24"
version = "0.1.27"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4261,9 +4261,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/3e/9dec08dd8107b963cdb3ea3a77b5530e7f68b239c38c80f713be27920f1b/praisonaiagents-0.1.24.tar.gz", hash = "sha256:8a6d17c2df22f2171df11b44b44b817e72a6f34f1f5b4e389691bedbbff06621", size = 678110, upload-time = "2025-12-23T05:39:18.104Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f17a1f26f1bd9cc340f276b8eb7001cb66e2132b48d6a25109beac7b3721/praisonaiagents-0.1.27.tar.gz", hash = "sha256:ec2cb07cc1fd940e107eb16ecf46494a3613e47735e9668de986876f1e043e5f", size = 681432, upload-time = "2025-12-23T16:07:37.204Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/33/3b45e341433a01408f59ced6eb39da3ecb17067f0ff3c954fa38e07aaffb/praisonaiagents-0.1.24-py3-none-any.whl", hash = "sha256:03a1c476bb5287ad9e519b85a17a93c42ce11b01db87009fcde17a1901e4828a", size = 616792, upload-time = "2025-12-23T05:39:16.12Z" },
{ url = "https://files.pythonhosted.org/packages/b8/aa/90aebc654e4a7cc02b5a0fdf108c6c64d374c3385eb0dcde6118a8ce3b68/praisonaiagents-0.1.27-py3-none-any.whl", hash = "sha256:0e7ccfa0bf4cf1aa6f58e9112bde026e61d36a75ec73e582d8deabd158d8c4de", size = 620263, upload-time = "2025-12-23T16:07:35.048Z" },
]
[[package]]