mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3929d4fbce | ||
|
|
c9e5023832 |
No files matched your search
@@ -0,0 +1,11 @@
|
||||
framework: praisonai
|
||||
topic: Pull Request Review
|
||||
roles:
|
||||
lead_reviewer:
|
||||
role: Lead Code Reviewer
|
||||
goal: Provide a comprehensive and constructive review of the pull request
|
||||
backstory: You are an expert software engineer with deep knowledge of best practices, security, and performance. You meticulously review code for bugs and maintainability.
|
||||
tasks:
|
||||
code_review:
|
||||
description: Review the code changes in the pull request. Look for logic errors, formatting issues, security flaws, and performance bottlenecks. Output the review as a detailed markdown comment.
|
||||
expected_output: A detailed PR review in markdown format.
|
||||
@@ -0,0 +1,40 @@
|
||||
name: PraisonAI PR Reviewer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false && (github.event_name == 'pull_request' || contains(github.event.comment.body, '@praisonai'))
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: pip install praisonaiagents[all]
|
||||
|
||||
- name: Run PraisonAI PR Review
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
praisonai agents --file .github/praisonai-reviewer.yaml
|
||||
@@ -0,0 +1,25 @@
|
||||
# PraisonAI PR Reviewer Setup Guide
|
||||
|
||||
This guide explains how to set up PraisonAI as an automated pull request reviewer using GitHub Actions and GitHub Apps.
|
||||
|
||||
## Prerequisites
|
||||
1. A GitHub App created within your organization or account.
|
||||
2. The App must have the following permissions:
|
||||
- Pull Requests: Read & Write
|
||||
- Issues: Read & Write
|
||||
- Contents: Read
|
||||
3. Generate a Private Key for the GitHub App.
|
||||
|
||||
## Setup Steps
|
||||
|
||||
1. Configure GitHub Secrets for your repository:
|
||||
- `APP_ID`: The App ID of your GitHub App.
|
||||
- `PRIVATE_KEY`: The generated Private Key (PEM format).
|
||||
- `OPENAI_API_KEY` (or other LLM key) for PraisonAI to use.
|
||||
|
||||
2. Ensure `.github/workflows/praisonai-pr-review.yml` is present in your default branch.
|
||||
|
||||
3. Customize `.github/praisonai-reviewer.yaml` to configure the reviewing agents with specific roles.
|
||||
|
||||
## Triggering the Review
|
||||
The review will run automatically upon PR creation and synchronization. You can also trigger it manually by commenting `@praisonai` on any pull request or issue.
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.5.126" \
|
||||
"praisonai>=4.5.127" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.5.126" \
|
||||
"praisonai>=4.5.127" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.5.126" \
|
||||
"praisonai>=4.5.127" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: PraisonAI PR Reviewer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false && (github.event_name == 'pull_request' || contains(github.event.comment.body, '@praisonai'))
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PraisonAI
|
||||
run: pip install praisonaiagents[all]
|
||||
|
||||
- name: Run PraisonAI PR Review
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
# Use PraisonAI to review the PR
|
||||
praisonai agents --file .github/praisonai-reviewer.yaml
|
||||
@@ -329,6 +329,9 @@ class MCP:
|
||||
env = kwargs.get('env', {})
|
||||
if not env:
|
||||
env = os.environ.copy()
|
||||
# Sanitize environment to prevent leaking caller credentials
|
||||
sensitive_pattern = re.compile(r'(?i)(api_key|token|secret|password|credential)')
|
||||
env = {k: v for k, v in env.items() if not sensitive_pattern.search(k)}
|
||||
|
||||
# Always set Python encoding
|
||||
env['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
@@ -126,13 +126,24 @@ class FileTools:
|
||||
# Validate directory path
|
||||
safe_dir = FileTools._validate_path(directory)
|
||||
path = Path(safe_dir)
|
||||
resolved_base = path.resolve()
|
||||
|
||||
if pattern:
|
||||
if '..' in pattern:
|
||||
raise ValueError("Pattern cannot contain '..'")
|
||||
if Path(pattern).is_absolute() or pattern.startswith('/'):
|
||||
raise ValueError("Pattern cannot be an absolute path")
|
||||
files = path.glob(pattern)
|
||||
else:
|
||||
files = path.iterdir()
|
||||
|
||||
result = []
|
||||
for file in files:
|
||||
try:
|
||||
file.resolve().relative_to(resolved_base)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if file.is_file():
|
||||
stat = file.stat()
|
||||
result.append({
|
||||
|
||||
@@ -59,14 +59,13 @@ class ShellTools:
|
||||
else:
|
||||
command = shlex.split(command)
|
||||
|
||||
# Expand tilde and environment variables in command arguments
|
||||
# Expand tilde in command arguments
|
||||
# (shell=False means the shell won't do this for us)
|
||||
command = [os.path.expanduser(os.path.expandvars(arg)) for arg in command]
|
||||
command = [os.path.expanduser(arg) for arg in command]
|
||||
|
||||
# Expand tilde in cwd (subprocess doesn't do this)
|
||||
if cwd:
|
||||
cwd = os.path.expanduser(cwd)
|
||||
cwd = os.path.expandvars(cwd) # Also expand $HOME, $USER, etc.
|
||||
if not os.path.isdir(cwd):
|
||||
# Fallback: try home directory, then current working directory
|
||||
fallback = os.path.expanduser("~") if os.path.isdir(os.path.expanduser("~")) else os.getcwd()
|
||||
|
||||
@@ -139,14 +139,18 @@ def _crawl_with_httpx(urls: List[str]) -> List[Dict[str, Any]]:
|
||||
# Try httpx first
|
||||
try:
|
||||
import httpx
|
||||
with httpx.Client(follow_redirects=True, timeout=30.0) as client:
|
||||
with httpx.Client(follow_redirects=False, timeout=30.0) as client:
|
||||
response = client.get(url)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
except ImportError:
|
||||
# Fallback to urllib
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None # Disable redirects for security
|
||||
opener = urllib.request.build_opener(NoRedirectHandler())
|
||||
with opener.open(url, timeout=30) as response:
|
||||
content = response.read().decode('utf-8', errors='ignore')
|
||||
|
||||
# Basic HTML to text extraction
|
||||
|
||||
@@ -117,13 +117,30 @@ class AGUI:
|
||||
|
||||
def _attach_routes(self, router: "APIRouter") -> None:
|
||||
"""Attach AG-UI routes to the router."""
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
import os
|
||||
|
||||
encoder = EventEncoder()
|
||||
|
||||
def _check_auth(request: Request):
|
||||
token = os.environ.get("AGUI_AUTH_TOKEN") or os.environ.get("PRAISONAI_AUTH_TOKEN")
|
||||
if token:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != token:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
else:
|
||||
client_host = request.client.host if request.client else ""
|
||||
if client_host not in ("127.0.0.1", "::1", "localhost"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Access denied. Configure AGUI_AUTH_TOKEN for remote access."
|
||||
)
|
||||
|
||||
@router.post("/agui")
|
||||
async def run_agent_agui(run_input: RunAgentInput):
|
||||
async def run_agent_agui(request: Request, run_input: RunAgentInput):
|
||||
"""Run the agent via AG-UI protocol."""
|
||||
_check_auth(request)
|
||||
async def event_generator():
|
||||
async for event in self._run_agent(run_input):
|
||||
yield encoder.encode(event)
|
||||
@@ -134,15 +151,13 @@ class AGUI:
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status():
|
||||
async def get_status(request: Request):
|
||||
"""Get agent status."""
|
||||
_check_auth(request)
|
||||
return {"status": "available"}
|
||||
|
||||
async def _run_agent(self, run_input: RunAgentInput) -> AsyncIterator[BaseEvent]:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.126"
|
||||
version = "1.5.127"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -206,16 +206,16 @@ class TestApprovalRegistry:
|
||||
def test_mark_and_check_approved(self):
|
||||
from praisonaiagents.approval.registry import ApprovalRegistry
|
||||
reg = ApprovalRegistry()
|
||||
assert not reg.is_already_approved("execute_command")
|
||||
reg.mark_approved("execute_command")
|
||||
assert reg.is_already_approved("execute_command")
|
||||
assert not reg.is_already_approved("write_file")
|
||||
reg.mark_approved("write_file")
|
||||
assert reg.is_already_approved("write_file")
|
||||
|
||||
def test_already_approved_skips_backend(self):
|
||||
"""Once approved, no backend call needed."""
|
||||
from praisonaiagents.approval.registry import ApprovalRegistry
|
||||
reg = ApprovalRegistry()
|
||||
reg.mark_approved("execute_command")
|
||||
decision = reg.approve_sync("agent", "execute_command", {})
|
||||
reg.mark_approved("write_file")
|
||||
decision = reg.approve_sync("agent", "write_file", {})
|
||||
assert decision.approved is True
|
||||
assert "already" in decision.reason.lower()
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -2951,7 +2951,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.126"
|
||||
version = "1.5.127"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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/v4.5.126.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.126.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.127.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.127.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -68,12 +68,12 @@ def import_tools_from_file(file_path):
|
||||
return custom_tools_module
|
||||
|
||||
try:
|
||||
if os.path.exists(tools_path):
|
||||
if tools_path and os.path.exists(tools_path):
|
||||
# tools.py exists in the root directory, import from file
|
||||
custom_tools_module = import_tools_from_file(tools_path)
|
||||
logger.debug("Successfully imported custom tools from root tools.py")
|
||||
else:
|
||||
logger.debug("No custom tools.py file found in the root directory")
|
||||
logger.debug("No custom tools file found or specified")
|
||||
custom_tools_module = None
|
||||
|
||||
if custom_tools_module:
|
||||
|
||||
@@ -117,8 +117,23 @@ class AgentOS:
|
||||
|
||||
def _register_routes(self, app: Any) -> None:
|
||||
"""Register API routes."""
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
import os
|
||||
|
||||
def _check_auth(request: Request):
|
||||
token = os.environ.get("AGENTOS_AUTH_TOKEN") or os.environ.get("PRAISONAI_AUTH_TOKEN")
|
||||
if token:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != token:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
else:
|
||||
client_host = request.client.host if request.client else ""
|
||||
if client_host not in ("127.0.0.1", "::1", "localhost"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Access denied. Configure AGENTOS_AUTH_TOKEN for remote access."
|
||||
)
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
@@ -145,7 +160,8 @@ class AgentOS:
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.get(f"{self.config.api_prefix}/agents")
|
||||
async def list_agents():
|
||||
async def list_agents(request: Request):
|
||||
_check_auth(request)
|
||||
return {
|
||||
"agents": [
|
||||
{
|
||||
@@ -160,16 +176,17 @@ class AgentOS:
|
||||
}
|
||||
|
||||
@app.post(f"{self.config.api_prefix}/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest):
|
||||
async def chat(request_obj: ChatRequest, request: Request):
|
||||
_check_auth(request)
|
||||
# Find the agent
|
||||
agent = None
|
||||
if request.agent_name:
|
||||
if request_obj.agent_name:
|
||||
for a in self.agents:
|
||||
if getattr(a, 'name', None) == request.agent_name:
|
||||
if getattr(a, 'name', None) == request_obj.agent_name:
|
||||
agent = a
|
||||
break
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=404, detail=f"Agent '{request.agent_name}' not found")
|
||||
raise HTTPException(status_code=404, detail=f"Agent '{request_obj.agent_name}' not found")
|
||||
elif self.agents:
|
||||
agent = self.agents[0]
|
||||
else:
|
||||
@@ -177,7 +194,7 @@ class AgentOS:
|
||||
|
||||
# Call the agent
|
||||
try:
|
||||
response = agent.chat(request.message)
|
||||
response = agent.chat(request_obj.message)
|
||||
return ChatResponse(
|
||||
response=str(response),
|
||||
agent_name=getattr(agent, 'name', 'unknown'),
|
||||
|
||||
@@ -282,6 +282,33 @@ class JobWorkflowExecutor:
|
||||
|
||||
def _exec_inline_python(self, code: str, step: Dict, flags: Dict) -> Dict:
|
||||
"""Execute inline Python code in an isolated namespace."""
|
||||
import ast
|
||||
|
||||
# Security validation (prevent sandbox escape)
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
blocked_attrs = {
|
||||
'__subclasses__', '__bases__', '__mro__', '__globals__',
|
||||
'__code__', '__class__', '__dict__', '__builtins__',
|
||||
'__import__', '__loader__', '__spec__', '__init_subclass__',
|
||||
'__set_name__', '__reduce__', '__reduce_ex__',
|
||||
'__traceback__', '__qualname__', '__module__',
|
||||
}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
return {"ok": False, "error": "Import statements are not allowed in script steps"}
|
||||
if isinstance(node, ast.Attribute) and node.attr in blocked_attrs:
|
||||
return {"ok": False, "error": f"Access to attribute '{node.attr}' is restricted"}
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
||||
if node.func.id in ('exec', 'eval', 'compile', '__import__', 'open', 'input', 'breakpoint', 'setattr', 'getattr', 'delattr'):
|
||||
return {"ok": False, "error": f"Call to '{node.func.id}' is not allowed"}
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||
if any(attr in node.value for attr in blocked_attrs):
|
||||
return {"ok": False, "error": "String constant contains restricted attribute name"}
|
||||
except SyntaxError as e:
|
||||
return {"ok": False, "error": f"Syntax Error: {str(e)}"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"Validation Error: {str(e)}"}
|
||||
_safe_builtins = {
|
||||
"True": True, "False": False, "None": None,
|
||||
"int": int, "float": float, "str": str, "bool": bool,
|
||||
|
||||
@@ -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==4.5.126 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==4.5.127 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -243,7 +243,15 @@ class WebSocketGateway:
|
||||
def _check_auth(request) -> Optional[JSONResponse]:
|
||||
"""Validate auth token if configured. Returns error response or None."""
|
||||
if not self.config.auth_token:
|
||||
# If no token is configured, restrict sensitive routes to localhost
|
||||
client_ip = request.client.host if request.client else "127.0.0.1"
|
||||
if client_ip not in ("127.0.0.1", "::1", "localhost"):
|
||||
return JSONResponse(
|
||||
{"error": "Authentication token not configured. Remote access denied."},
|
||||
status_code=403,
|
||||
)
|
||||
return None
|
||||
|
||||
auth_header = request.headers.get("authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
return JSONResponse(
|
||||
|
||||
@@ -158,6 +158,11 @@ def _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:
|
||||
raise RegistryError(f"Archive is too large uncompressed (>{MAX_SIZE} bytes)")
|
||||
|
||||
member_path = Path(member.name)
|
||||
# Protect against symlink attacks
|
||||
if member.issym() or member.islnk():
|
||||
raise RegistryError(
|
||||
f"Refusing to extract symlink/hardlink in archive: {member.name}"
|
||||
)
|
||||
# Reject absolute paths
|
||||
if member_path.is_absolute():
|
||||
raise RegistryError(
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.5.126"
|
||||
__version__ = "4.5.127"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.5.126",
|
||||
"praisonaiagents>=1.5.127",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0,<=1.82.6",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+1
-1
@@ -5117,7 +5117,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.126"
|
||||
version = "1.5.127"
|
||||
source = { directory = "../praisonai-agents" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Reference in new issue
Block a user