mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 11:51:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed8596261 | ||
|
|
deef65c4bb | ||
|
|
d51327effd | ||
|
|
30ce8857e8 |
No files matched your search
@@ -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.45" \
|
||||
"praisonai>=2.3.48" \
|
||||
"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>=2.3.45" \
|
||||
"praisonai>=2.3.48" \
|
||||
"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>=2.3.45" \
|
||||
"praisonai>=2.3.48" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ SUPPRESSED_PATTERNS = [
|
||||
"PydanticSerializationUnexpectedValue",
|
||||
"Expected 9 fields but got 5 for type `Message`",
|
||||
"Expected `StreamingChoices` but got `Choices`",
|
||||
"serialized value may not be as expected"
|
||||
"serialized value may not be as expected",
|
||||
"Mixing V1 models and V2 models",
|
||||
"Please upgrade `Settings` to V2"
|
||||
]
|
||||
|
||||
@functools.wraps(_original_warn)
|
||||
|
||||
@@ -14,6 +14,7 @@ Usage:
|
||||
context, cleaned_prompt = parser.process("@file:main.py explain this code")
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -43,10 +44,14 @@ class MentionsParser:
|
||||
"url": re.compile(r'@url:(https?://[^\s]+)'),
|
||||
}
|
||||
|
||||
# Default max file chars: 500K (~125K tokens) - fits GPT-4o (128K), Claude 3.5 (200K)
|
||||
DEFAULT_MAX_FILE_CHARS = 500000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_path: Optional[str] = None,
|
||||
verbose: int = 0
|
||||
verbose: int = 0,
|
||||
max_file_chars: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
Initialize MentionsParser.
|
||||
@@ -54,10 +59,24 @@ class MentionsParser:
|
||||
Args:
|
||||
workspace_path: Path to workspace/project root
|
||||
verbose: Verbosity level
|
||||
max_file_chars: Maximum characters to include from files.
|
||||
Default: 500000 (500K chars, ~125K tokens).
|
||||
Set to 0 for no limit.
|
||||
Can be overridden via PRAISON_MAX_FILE_CHARS env var.
|
||||
"""
|
||||
self.workspace_path = Path(workspace_path) if workspace_path else Path.cwd()
|
||||
self.verbose = verbose
|
||||
|
||||
# Configure max file chars: constructor > env var > default
|
||||
if max_file_chars is not None:
|
||||
self.max_file_chars = max_file_chars
|
||||
else:
|
||||
env_val = os.environ.get('PRAISON_MAX_FILE_CHARS')
|
||||
if env_val is not None:
|
||||
self.max_file_chars = int(env_val)
|
||||
else:
|
||||
self.max_file_chars = self.DEFAULT_MAX_FILE_CHARS
|
||||
|
||||
# Lazy-loaded managers
|
||||
self._docs_manager = None
|
||||
self._rules_manager = None
|
||||
@@ -158,9 +177,15 @@ class MentionsParser:
|
||||
|
||||
content = full_path.read_text(encoding="utf-8")
|
||||
|
||||
# Limit content size
|
||||
if len(content) > 50000:
|
||||
content = content[:50000] + "\n... (truncated)"
|
||||
# Limit content size (0 means no limit)
|
||||
if self.max_file_chars > 0 and len(content) > self.max_file_chars:
|
||||
original_size = len(content)
|
||||
content = content[:self.max_file_chars] + "\n... (truncated)"
|
||||
self._log(
|
||||
f"File {file_path} truncated from {original_size:,} to {self.max_file_chars:,} chars. "
|
||||
f"Set PRAISON_MAX_FILE_CHARS=0 for no limit.",
|
||||
logging.WARNING
|
||||
)
|
||||
|
||||
# Detect language for code fence
|
||||
ext = full_path.suffix.lower()
|
||||
@@ -253,9 +278,9 @@ class MentionsParser:
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
content = response.read().decode('utf-8', errors='ignore')
|
||||
|
||||
# Limit content size
|
||||
if len(content) > 50000:
|
||||
content = content[:50000] + "\n... (truncated)"
|
||||
# Limit content size (0 means no limit)
|
||||
if self.max_file_chars > 0 and len(content) > self.max_file_chars:
|
||||
content = content[:self.max_file_chars] + "\n... (truncated)"
|
||||
|
||||
# Try to extract text from HTML
|
||||
if '<html' in content.lower():
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Unit tests for configurable max_file_chars in MentionsParser.
|
||||
|
||||
TDD approach: Write tests first, then implement.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestMentionsParserConfigurableLimit:
|
||||
"""Test suite for configurable file truncation limit."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_workspace(self):
|
||||
"""Create a temporary workspace with test files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield Path(tmpdir)
|
||||
|
||||
@pytest.fixture
|
||||
def small_file(self, temp_workspace):
|
||||
"""Create a small test file (1KB)."""
|
||||
file_path = temp_workspace / "small.py"
|
||||
content = "# Small file\n" + "x = 1\n" * 100 # ~700 chars
|
||||
file_path.write_text(content)
|
||||
return file_path
|
||||
|
||||
@pytest.fixture
|
||||
def large_file(self, temp_workspace):
|
||||
"""Create a large test file (100KB)."""
|
||||
file_path = temp_workspace / "large.py"
|
||||
content = "# Large file\n" + "x = 'a' * 100\n" * 7000 # ~100KB
|
||||
file_path.write_text(content)
|
||||
return file_path
|
||||
|
||||
@pytest.fixture
|
||||
def huge_file(self, temp_workspace):
|
||||
"""Create a huge test file (600KB)."""
|
||||
file_path = temp_workspace / "huge.py"
|
||||
content = "# Huge file\n" + "x = 'a' * 100\n" * 42000 # ~600KB
|
||||
file_path.write_text(content)
|
||||
return file_path
|
||||
|
||||
# ===== Test 1.1: Default value (500K) =====
|
||||
def test_default_max_file_chars_is_500k(self, temp_workspace):
|
||||
"""Default max_file_chars should be 500,000."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(workspace_path=str(temp_workspace))
|
||||
assert parser.max_file_chars == 500000
|
||||
|
||||
# ===== Test 1.2: Custom value via constructor =====
|
||||
def test_custom_max_file_chars_via_constructor(self, temp_workspace):
|
||||
"""max_file_chars should be configurable via constructor."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=100000
|
||||
)
|
||||
assert parser.max_file_chars == 100000
|
||||
|
||||
def test_custom_max_file_chars_zero_means_no_limit(self, temp_workspace):
|
||||
"""max_file_chars=0 should mean no truncation."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=0
|
||||
)
|
||||
assert parser.max_file_chars == 0
|
||||
|
||||
# ===== Test 1.3: Environment variable override =====
|
||||
def test_env_var_override(self, temp_workspace):
|
||||
"""PRAISON_MAX_FILE_CHARS env var should override default."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
with patch.dict(os.environ, {'PRAISON_MAX_FILE_CHARS': '200000'}):
|
||||
# Need to reimport to pick up env var
|
||||
parser = MentionsParser(workspace_path=str(temp_workspace))
|
||||
assert parser.max_file_chars == 200000
|
||||
|
||||
def test_constructor_takes_precedence_over_env_var(self, temp_workspace):
|
||||
"""Constructor value should take precedence over env var."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
with patch.dict(os.environ, {'PRAISON_MAX_FILE_CHARS': '200000'}):
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=100000
|
||||
)
|
||||
assert parser.max_file_chars == 100000
|
||||
|
||||
# ===== Test 1.4: No-limit mode =====
|
||||
def test_no_truncation_when_limit_is_zero(self, temp_workspace, huge_file):
|
||||
"""File should not be truncated when max_file_chars=0."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=0
|
||||
)
|
||||
|
||||
context, _ = parser.process(f"@file:{huge_file.name} explain")
|
||||
|
||||
# Should contain full content, no truncation message
|
||||
assert "(truncated)" not in context
|
||||
# Should have the full file content
|
||||
original_size = huge_file.read_text().__len__()
|
||||
# Context includes header and code fence, so check it's close
|
||||
assert len(context) >= original_size
|
||||
|
||||
# ===== Test 1.5: Truncation behavior =====
|
||||
def test_truncation_at_custom_limit(self, temp_workspace, large_file):
|
||||
"""File should be truncated at custom limit."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=10000 # 10KB limit
|
||||
)
|
||||
|
||||
context, _ = parser.process(f"@file:{large_file.name} explain")
|
||||
|
||||
# Should be truncated
|
||||
assert "(truncated)" in context
|
||||
# Content should be around the limit (plus header/fence overhead)
|
||||
assert len(context) < 15000 # Some overhead for markdown
|
||||
|
||||
def test_no_truncation_when_file_under_limit(self, temp_workspace, small_file):
|
||||
"""Small files should not be truncated."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=500000
|
||||
)
|
||||
|
||||
context, _ = parser.process(f"@file:{small_file.name} explain")
|
||||
|
||||
# Should NOT be truncated
|
||||
assert "(truncated)" not in context
|
||||
|
||||
def test_truncation_warning_logged(self, temp_workspace, large_file, caplog):
|
||||
"""Warning should be logged when file is truncated."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
import logging
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=10000,
|
||||
verbose=1
|
||||
)
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
parser.process(f"@file:{large_file.name} explain")
|
||||
|
||||
# Should log a warning about truncation
|
||||
assert any("truncat" in record.message.lower() for record in caplog.records)
|
||||
|
||||
# ===== Test: URL truncation also uses configurable limit =====
|
||||
def test_url_truncation_uses_same_limit(self, temp_workspace):
|
||||
"""URL content should also respect max_file_chars."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
parser = MentionsParser(
|
||||
workspace_path=str(temp_workspace),
|
||||
max_file_chars=100000
|
||||
)
|
||||
|
||||
# URL truncation should use the same limit
|
||||
assert parser.max_file_chars == 100000
|
||||
|
||||
# ===== Test: Backward compatibility =====
|
||||
def test_backward_compatible_no_args(self, temp_workspace):
|
||||
"""Parser should work without max_file_chars argument."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
|
||||
# Should not raise any errors
|
||||
parser = MentionsParser(workspace_path=str(temp_workspace))
|
||||
assert hasattr(parser, 'max_file_chars')
|
||||
|
||||
# ===== Test: Performance - no impact on small files =====
|
||||
def test_performance_small_file(self, temp_workspace, small_file):
|
||||
"""Processing small files should be fast."""
|
||||
from praisonaiagents.tools.mentions import MentionsParser
|
||||
import time
|
||||
|
||||
parser = MentionsParser(workspace_path=str(temp_workspace))
|
||||
|
||||
start = time.time()
|
||||
for _ in range(100):
|
||||
parser.process(f"@file:{small_file.name} explain")
|
||||
elapsed = time.time() - start
|
||||
|
||||
# 100 iterations should complete in under 1 second
|
||||
assert elapsed < 1.0, f"Performance regression: {elapsed}s for 100 iterations"
|
||||
|
||||
|
||||
class TestProcessMentionsFunction:
|
||||
"""Test the convenience function."""
|
||||
|
||||
def test_process_mentions_uses_default_limit(self):
|
||||
"""process_mentions function should use default limit."""
|
||||
from praisonaiagents.tools.mentions import process_mentions
|
||||
|
||||
# The function should create a parser with default settings
|
||||
# Just verify it works without errors
|
||||
context, cleaned = process_mentions("hello world")
|
||||
assert cleaned == "hello world"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -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.45.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.45.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.48.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.48.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -126,6 +126,7 @@ class PraisonAI:
|
||||
Initialize the PraisonAI object with default parameters.
|
||||
"""
|
||||
self.agent_yaml = agent_yaml
|
||||
self._interactive_mode = False # Flag for interactive TUI mode
|
||||
# Create config_list with AutoGen compatibility
|
||||
# Support multiple environment variable patterns for better compatibility
|
||||
# Priority order: MODEL_NAME > OPENAI_MODEL_NAME for model selection
|
||||
@@ -209,6 +210,11 @@ class PraisonAI:
|
||||
invocation_cmd = "praisonai"
|
||||
version_string = f"PraisonAI version {__version__}"
|
||||
|
||||
# Handle --interactive flag - start interactive TUI mode
|
||||
if getattr(args, 'interactive', False):
|
||||
self._start_interactive_mode(args)
|
||||
return None
|
||||
|
||||
self.framework = args.framework or self.framework
|
||||
|
||||
# Update config_list model if --model flag is provided
|
||||
@@ -690,6 +696,15 @@ class PraisonAI:
|
||||
parser.add_argument("--port", type=int, default=8005, help="Server port (default: 8005)")
|
||||
parser.add_argument("--host", type=str, default="127.0.0.1", help="Server host (default: 127.0.0.1)")
|
||||
|
||||
# Interactive TUI - terminal interface with slash commands
|
||||
parser.add_argument("--interactive", "-i", action="store_true", help="Start interactive terminal mode with slash commands")
|
||||
|
||||
# Autonomy Mode - control AI action approval
|
||||
parser.add_argument("--autonomy", type=str, choices=["suggest", "auto_edit", "full_auto"], help="Set autonomy mode for AI actions")
|
||||
|
||||
# Sandbox Execution - secure command execution
|
||||
parser.add_argument("--sandbox", type=str, choices=["off", "basic", "strict"], help="Enable sandboxed command execution")
|
||||
|
||||
# If we're in a test environment, parse with empty args to avoid pytest interference
|
||||
if in_test_env:
|
||||
args, unknown_args = parser.parse_known_args([])
|
||||
@@ -2421,6 +2436,90 @@ class PraisonAI:
|
||||
except Exception as e:
|
||||
print(f"[red]ERROR: MCP command failed: {e}[/red]")
|
||||
|
||||
# Compiled regex patterns for sensitive data detection (compiled once, zero runtime cost)
|
||||
_SENSITIVE_PATTERNS = None
|
||||
|
||||
@classmethod
|
||||
def _get_sensitive_patterns(cls):
|
||||
"""Lazy-load and compile sensitive patterns only when needed."""
|
||||
if cls._SENSITIVE_PATTERNS is None:
|
||||
import re
|
||||
cls._SENSITIVE_PATTERNS = [
|
||||
# API Keys and Tokens
|
||||
(re.compile(r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "API Key"),
|
||||
(re.compile(r'(?i)(secret[_-]?key|secretkey)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Secret Key"),
|
||||
(re.compile(r'(?i)(access[_-]?token|accesstoken)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Access Token"),
|
||||
(re.compile(r'(?i)(auth[_-]?token|authtoken)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Auth Token"),
|
||||
# AWS
|
||||
(re.compile(r'AKIA[0-9A-Z]{16}'), "AWS Access Key ID"),
|
||||
(re.compile(r'(?i)aws[_-]?secret[_-]?access[_-]?key\s*[=:]\s*["\']?[a-zA-Z0-9/+=]{40}'), "AWS Secret Key"),
|
||||
# Passwords
|
||||
(re.compile(r'(?i)(password|passwd|pwd)\s*[=:]\s*["\']?[^\s"\']{8,}'), "Password"),
|
||||
(re.compile(r'(?i)db[_-]?password\s*[=:]\s*["\']?[^\s"\']+'), "Database Password"),
|
||||
# Private Keys
|
||||
(re.compile(r'-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----'), "Private Key"),
|
||||
(re.compile(r'-----BEGIN PGP PRIVATE KEY BLOCK-----'), "PGP Private Key"),
|
||||
# GitHub/GitLab tokens
|
||||
(re.compile(r'ghp_[a-zA-Z0-9]{36}'), "GitHub Personal Access Token"),
|
||||
(re.compile(r'gho_[a-zA-Z0-9]{36}'), "GitHub OAuth Token"),
|
||||
(re.compile(r'glpat-[a-zA-Z0-9\-]{20,}'), "GitLab Personal Access Token"),
|
||||
# Slack
|
||||
(re.compile(r'xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}'), "Slack Token"),
|
||||
# Generic secrets
|
||||
(re.compile(r'(?i)(client[_-]?secret)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Client Secret"),
|
||||
(re.compile(r'(?i)(private[_-]?key)\s*[=:]\s*["\']?[a-zA-Z0-9_\-/+=]{20,}'), "Private Key Value"),
|
||||
]
|
||||
return cls._SENSITIVE_PATTERNS
|
||||
|
||||
# Sensitive file patterns (simple string matching - very fast)
|
||||
_SENSITIVE_FILES = {'.env', '.env.local', '.env.production', '.env.development',
|
||||
'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519',
|
||||
'.pem', '.key', '.p12', '.pfx', 'credentials', 'secrets.json',
|
||||
'secrets.yaml', 'secrets.yml', '.htpasswd', '.netrc'}
|
||||
_SENSITIVE_EXTENSIONS = {'.pem', '.key', '.p12', '.pfx', '.jks', '.keystore'}
|
||||
|
||||
def _check_sensitive_content(self, diff_content: str, staged_files: list) -> list:
|
||||
"""
|
||||
Check for sensitive content in staged changes.
|
||||
|
||||
Args:
|
||||
diff_content: The git diff content
|
||||
staged_files: List of staged file names
|
||||
|
||||
Returns:
|
||||
List of (file, issue_type, match) tuples for detected issues
|
||||
"""
|
||||
issues = []
|
||||
|
||||
# Quick check for sensitive files by name/extension
|
||||
for file_path in staged_files:
|
||||
file_name = file_path.split('/')[-1].lower()
|
||||
# Check exact file names
|
||||
if file_name in self._SENSITIVE_FILES:
|
||||
issues.append((file_path, "Sensitive File", file_name))
|
||||
continue
|
||||
# Check extensions
|
||||
for ext in self._SENSITIVE_EXTENSIONS:
|
||||
if file_name.endswith(ext):
|
||||
issues.append((file_path, "Sensitive Extension", ext))
|
||||
break
|
||||
|
||||
# Only scan diff content if it's not too large (performance guard)
|
||||
if len(diff_content) < 50000:
|
||||
patterns = self._get_sensitive_patterns()
|
||||
# Scan only added lines (lines starting with +)
|
||||
for line in diff_content.split('\n'):
|
||||
if line.startswith('+') and not line.startswith('+++'):
|
||||
for pattern, issue_type in patterns:
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
# Truncate match for display
|
||||
matched_text = match.group(0)[:50] + '...' if len(match.group(0)) > 50 else match.group(0)
|
||||
issues.append(("diff", issue_type, matched_text))
|
||||
break # One issue per line is enough
|
||||
|
||||
return issues
|
||||
|
||||
def handle_commit_command(self, args: list):
|
||||
"""
|
||||
Handle AI commit message generation.
|
||||
@@ -2428,7 +2527,10 @@ class PraisonAI:
|
||||
Generates a commit message based on staged changes using AI.
|
||||
|
||||
Args:
|
||||
args: Additional arguments (e.g., --push to auto-push)
|
||||
args: Additional arguments:
|
||||
--push: Auto-push after commit
|
||||
-a, --auto: Full auto mode (stage, commit, push) - aborts on security issues
|
||||
--no-verify: Skip sensitive content check
|
||||
"""
|
||||
try:
|
||||
import subprocess
|
||||
@@ -2442,6 +2544,12 @@ class PraisonAI:
|
||||
print("[red]ERROR: Not in a git repository[/red]")
|
||||
return
|
||||
|
||||
# Handle auto mode
|
||||
auto_mode = '-a' in args or '--auto' in args
|
||||
if auto_mode:
|
||||
print("[cyan]Auto-staging all changes...[/cyan]")
|
||||
subprocess.run(["git", "add", "-A"], capture_output=True)
|
||||
|
||||
# Get staged diff
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--cached", "--stat"],
|
||||
@@ -2450,7 +2558,7 @@ class PraisonAI:
|
||||
)
|
||||
|
||||
if not result.stdout.strip():
|
||||
print("[yellow]No staged changes. Use 'git add' to stage files first.[/yellow]")
|
||||
print("[yellow]No staged changes. Use 'git add' to stage files first, or use -a/--auto.[/yellow]")
|
||||
return
|
||||
|
||||
# Get detailed diff for context
|
||||
@@ -2463,6 +2571,46 @@ class PraisonAI:
|
||||
# Limit diff size for context
|
||||
diff_content = diff_result.stdout[:8000] if len(diff_result.stdout) > 8000 else diff_result.stdout
|
||||
|
||||
# Security check for sensitive content (unless --no-verify)
|
||||
if '--no-verify' not in args:
|
||||
# Extract staged file names from stat output
|
||||
staged_files = [line.split('|')[0].strip() for line in result.stdout.strip().split('\n') if '|' in line]
|
||||
issues = self._check_sensitive_content(diff_result.stdout, staged_files)
|
||||
|
||||
if issues:
|
||||
print("\n[bold red]⚠️ SECURITY WARNING: Sensitive content detected![/bold red]")
|
||||
for file_path, issue_type, match in issues:
|
||||
print(f" [red]• {issue_type}[/red] in [yellow]{file_path}[/yellow]: {match}")
|
||||
print("\n[yellow]Options:[/yellow]")
|
||||
print(" [c] Continue anyway (not recommended)")
|
||||
print(" [a] Abort commit")
|
||||
print(" [i] Ignore and add to .gitignore")
|
||||
|
||||
# In auto mode, abort on security issues
|
||||
if auto_mode:
|
||||
print("[red]Auto mode aborted due to security concerns. Use --no-verify to skip.[/red]")
|
||||
return
|
||||
|
||||
sec_choice = input("\nYour choice [c/a/i]: ").strip().lower()
|
||||
if sec_choice == 'a':
|
||||
print("[yellow]Commit aborted due to security concerns.[/yellow]")
|
||||
return
|
||||
elif sec_choice == 'i':
|
||||
# Add sensitive files to .gitignore
|
||||
sensitive_file_paths = [f for f, t, _ in issues if t in ("Sensitive File", "Sensitive Extension")]
|
||||
if sensitive_file_paths:
|
||||
with open('.gitignore', 'a') as gi:
|
||||
gi.write('\n# Auto-added by praisonai commit\n')
|
||||
for fp in sensitive_file_paths:
|
||||
gi.write(f'{fp}\n')
|
||||
print(f"[green]Added {len(sensitive_file_paths)} file(s) to .gitignore[/green]")
|
||||
# Unstage the sensitive files
|
||||
subprocess.run(["git", "reset", "HEAD", "--"] + sensitive_file_paths, capture_output=True)
|
||||
print("[cyan]Unstaged sensitive files. Please re-run commit.[/cyan]")
|
||||
return
|
||||
# else continue
|
||||
print("[yellow]Proceeding despite security warnings...[/yellow]")
|
||||
|
||||
print("[bold]Staged changes:[/bold]")
|
||||
print(result.stdout)
|
||||
print("\n[bold]Generating commit message...[/bold]")
|
||||
@@ -2508,6 +2656,14 @@ Provide ONLY the commit message, no explanations."""
|
||||
print("\n[bold green]Suggested commit message:[/bold green]")
|
||||
print(f"[cyan]{commit_message}[/cyan]")
|
||||
|
||||
# In auto mode, skip confirmation and commit + push
|
||||
if auto_mode:
|
||||
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
||||
print("[green]✅ Committed successfully![/green]")
|
||||
subprocess.run(["git", "push"], check=True)
|
||||
print("[green]✅ Pushed to remote![/green]")
|
||||
return
|
||||
|
||||
# Ask for confirmation
|
||||
print("\n[bold]Options:[/bold]")
|
||||
print(" [y] Use this message and commit")
|
||||
@@ -2752,6 +2908,10 @@ Provide ONLY the commit message, no explanations."""
|
||||
"backstory": "You are a helpful AI assistant"
|
||||
}
|
||||
|
||||
# Suppress verbose output in interactive mode (no Agent Info, Task, Response panels)
|
||||
if getattr(self, '_interactive_mode', False):
|
||||
agent_config["verbose"] = False
|
||||
|
||||
# Add llm if specified
|
||||
if hasattr(self, 'args') and self.args.llm:
|
||||
# Check if max_tokens is specified - pass as dict config
|
||||
@@ -3605,6 +3765,78 @@ Now, {final_instruction.lower()}:"""
|
||||
print(f"[red]ERROR: Research failed: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
def _start_interactive_mode(self, args):
|
||||
"""Start interactive TUI mode with slash commands."""
|
||||
try:
|
||||
from praisonai.cli.features import InteractiveTUIHandler, SlashCommandHandler, CostTrackerHandler
|
||||
from praisonai.cli.features.interactive_tui import InteractiveConfig
|
||||
|
||||
# Set interactive mode flag to suppress verbose agent output
|
||||
self._interactive_mode = True
|
||||
|
||||
# Initialize handlers
|
||||
slash_handler = SlashCommandHandler()
|
||||
cost_handler = CostTrackerHandler()
|
||||
cost_handler.initialize()
|
||||
|
||||
# Configure interactive session
|
||||
config = InteractiveConfig(
|
||||
prompt="praisonai> ",
|
||||
multiline=False,
|
||||
enable_completions=True,
|
||||
show_status_bar=True
|
||||
)
|
||||
|
||||
# Define input handler
|
||||
def on_input(text):
|
||||
"""Handle regular input - send to agent."""
|
||||
if not text.strip():
|
||||
return ""
|
||||
|
||||
# Track cost if enabled
|
||||
result = self.handle_direct_prompt(text)
|
||||
return result
|
||||
|
||||
# Define command handler
|
||||
def on_command(cmd):
|
||||
"""Handle slash commands."""
|
||||
result = slash_handler.execute(cmd)
|
||||
if result.get("type") == "exit":
|
||||
return result
|
||||
|
||||
# Handle special commands
|
||||
if result.get("type") == "cost":
|
||||
return {"type": "info", "message": cost_handler.get_summary()}
|
||||
|
||||
return result
|
||||
|
||||
# Initialize TUI handler
|
||||
tui_handler = InteractiveTUIHandler()
|
||||
session = tui_handler.initialize(
|
||||
config=config,
|
||||
on_input=on_input,
|
||||
on_command=on_command
|
||||
)
|
||||
|
||||
# Add slash commands for completion
|
||||
session.add_commands(["help", "exit", "quit", "cost", "tokens", "model", "diff", "commit", "undo", "plan", "map", "clear", "settings"])
|
||||
|
||||
print("[bold green]PraisonAI Interactive Mode[/bold green]")
|
||||
print("Type your prompt or use /help for commands. /exit to quit.\n")
|
||||
|
||||
# Run the interactive session
|
||||
tui_handler.run()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"[red]ERROR: Interactive mode requires additional dependencies: {e}[/red]")
|
||||
print("Install with: pip install prompt_toolkit")
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n[yellow]Exiting interactive mode.[/yellow]")
|
||||
except Exception as e:
|
||||
print(f"[red]ERROR: Interactive mode failed: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
praison_ai = PraisonAI()
|
||||
praison_ai.main()
|
||||
@@ -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.45 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.48 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.45"
|
||||
__version__ = "2.3.48"
|
||||
Reference in new issue
Block a user