Compare commits

...
2 Commits
Author SHA1 Message Date
Mervin Praison d51327effd Release v2.3.46 2025-12-19 12:22:29 +00:00
Mervin Praison 30ce8857e8 fix: suppress crewai.cli.config INFO log at startup 2025-12-19 12:18:23 +00:00
10 changed files with 342 additions and 15 deletions

No files matched your search

+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.45" \
"praisonai>=2.3.46" \
"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.45" \
"praisonai>=2.3.46" \
"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.45" \
"praisonai>=2.3.46" \
"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"])
+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.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.46.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.46.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+83
View File
@@ -209,6 +209,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 +695,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([])
@@ -3605,6 +3619,75 @@ 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
# 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()
+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.45 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.46 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.45"
__version__ = "2.3.46"