Compare commits

...
2 Commits
Author SHA1 Message Date
Mervin Praison c1f334905e Release v2.3.53 2025-12-19 15:38:30 +00:00
Mervin Praison 6d3ba7cdf8 Release v2.3.52 2025-12-19 15:34:19 +00:00
11 changed files with 568 additions and 24 deletions

No files matched your search

+4 -3
View File
@@ -984,9 +984,10 @@ praisonai -i
# - Slash commands: /help, /exit, /tools, /clear
# Chat mode - single prompt with interactive style (for testing/scripting)
praisonai "list files in current folder" --chat-mode
praisonai "search the web for AI news" --chat-mode
praisonai "read README.md" --chat-mode
# Use --chat (or --chat-mode for backward compatibility)
praisonai "list files in current folder" --chat
praisonai "search the web for AI news" --chat
praisonai "read README.md" --chat
```
### Chat UI (Web Interface):
+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.51" \
"praisonai>=2.3.53" \
"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.51" \
"praisonai>=2.3.53" \
"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.51" \
"praisonai>=2.3.53" \
"praisonai[ui]" \
"praisonai[crewai]"
+4 -3
View File
@@ -984,9 +984,10 @@ praisonai -i
# - Slash commands: /help, /exit, /tools, /clear
# Chat mode - single prompt with interactive style (for testing/scripting)
praisonai "list files in current folder" --chat-mode
praisonai "search the web for AI news" --chat-mode
praisonai "read README.md" --chat-mode
# Use --chat (or --chat-mode for backward compatibility)
praisonai "list files in current folder" --chat
praisonai "search the web for AI news" --chat
praisonai "read README.md" --chat
```
### Chat UI (Web Interface):
+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.51.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.51.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.53.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.53.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
@@ -380,6 +380,195 @@ class MessageQueueHandler:
pass
# ============================================================================
# AsyncProcessor - Background processing
# ============================================================================
class AsyncProcessor:
"""
Runs work functions in background threads.
Allows agent processing to happen asynchronously while
the main thread remains responsive for user input.
"""
def __init__(
self,
on_complete: Optional[Callable[[str], None]] = None,
on_status: Optional[Callable[[str], None]] = None,
on_error: Optional[Callable[[Exception], None]] = None
):
self._on_complete = on_complete
self._on_status = on_status
self._on_error = on_error
self._thread: Optional[threading.Thread] = None
self._running = False
self._lock = threading.Lock()
@property
def is_running(self) -> bool:
"""Check if currently processing."""
with self._lock:
return self._running
def start(self, work_fn: Callable[[], str]) -> None:
"""
Start processing work in background thread.
Args:
work_fn: Function to execute (should return result string)
"""
def _worker():
try:
with self._lock:
self._running = True
result = work_fn()
with self._lock:
self._running = False
if self._on_complete:
self._on_complete(result)
except Exception as e:
with self._lock:
self._running = False
if self._on_error:
self._on_error(e)
self._thread = threading.Thread(target=_worker, daemon=True)
self._thread.start()
def update_status(self, status: str) -> None:
"""Update status (can be called from work function)."""
if self._on_status:
self._on_status(status)
# ============================================================================
# LiveStatusDisplay - Real-time status display
# ============================================================================
class LiveStatusDisplay:
"""
Tracks and displays real-time status during processing.
Shows tool calls, command executions, and current status.
"""
def __init__(self):
self._status = ""
self._tool_calls: List[dict] = []
self._commands: List[str] = []
self._lock = threading.Lock()
@property
def current_status(self) -> str:
"""Get current status message."""
with self._lock:
return self._status
@property
def tool_calls(self) -> List[dict]:
"""Get list of tool calls."""
with self._lock:
return self._tool_calls.copy()
@property
def commands(self) -> List[str]:
"""Get list of commands executed."""
with self._lock:
return self._commands.copy()
def update_status(self, status: str) -> None:
"""Update current status message."""
with self._lock:
self._status = status
def add_tool_call(self, name: str, args: dict) -> None:
"""Add a tool call to the display."""
with self._lock:
self._tool_calls.append({
'name': name,
'args': args,
'timestamp': time.time()
})
def add_command(self, command: str) -> None:
"""Add a command execution to the display."""
with self._lock:
self._commands.append(command)
def format(self) -> str:
"""Format current status for display."""
lines = []
with self._lock:
if self._status:
lines.append(f"⏳ {self._status}")
for tool in self._tool_calls[-3:]: # Show last 3 tools
lines.append(f" 🔧 {tool['name']}")
for cmd in self._commands[-2:]: # Show last 2 commands
display_cmd = cmd[:40] + "..." if len(cmd) > 40 else cmd
lines.append(f" 💻 {display_cmd}")
return "\n".join(lines)
def clear(self) -> None:
"""Clear all status."""
with self._lock:
self._status = ""
self._tool_calls.clear()
self._commands.clear()
# ============================================================================
# NonBlockingInput - Async input handling
# ============================================================================
class NonBlockingInput:
"""
Handles user input asynchronously.
Allows users to type new messages while processing is ongoing.
"""
def __init__(self):
self._queue = MessageQueue()
@property
def has_pending(self) -> bool:
"""Check if there are pending inputs."""
return not self._queue.is_empty
@property
def pending_count(self) -> int:
"""Get number of pending inputs."""
return self._queue.count
def submit(self, message: str) -> bool:
"""Submit a new input message."""
return self._queue.add(message)
def pop(self) -> Optional[str]:
"""Get next pending input."""
return self._queue.pop()
def peek(self) -> Optional[str]:
"""View next pending input without removing."""
return self._queue.peek()
def get_all(self) -> List[str]:
"""Get all pending inputs."""
return self._queue.get_all()
def clear(self) -> None:
"""Clear all pending inputs."""
self._queue.clear()
# ============================================================================
# Exports
# ============================================================================
@@ -392,4 +581,7 @@ __all__ = [
'StateManager',
'QueueDisplay',
'MessageQueueHandler',
'AsyncProcessor',
'LiveStatusDisplay',
'NonBlockingInput',
]
+173 -11
View File
@@ -3811,7 +3811,8 @@ Now, {final_instruction.lower()}:"""
# Import message queue components
from praisonai.cli.features.message_queue import (
MessageQueue, StateManager, QueueDisplay, ProcessingState
MessageQueue, StateManager, QueueDisplay, ProcessingState,
AsyncProcessor, LiveStatusDisplay
)
import threading
@@ -3819,6 +3820,7 @@ Now, {final_instruction.lower()}:"""
message_queue = MessageQueue()
state_manager = StateManager()
queue_display = QueueDisplay(message_queue, state_manager)
live_status = LiveStatusDisplay()
processing_lock = threading.Lock()
# Session state
@@ -3834,29 +3836,56 @@ Now, {final_instruction.lower()}:"""
'message_queue': message_queue,
'state_manager': state_manager,
'queue_display': queue_display,
'live_status': live_status,
'processing_lock': processing_lock,
'async_processor': None, # Will be created per-request
}
# Try to use prompt_toolkit for autocomplete
prompt_session = None
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
# Custom completer that only triggers on /
class SlashCommandCompleter(Completer):
"""Completer that only shows suggestions when input starts with /"""
def __init__(self, commands):
self.commands = commands
def get_completions(self, document, complete_event):
text = document.text_before_cursor.lstrip()
# Only complete if text starts with /
if not text.startswith('/'):
return
# Get the command part (after /)
cmd_text = text[1:].lower()
# Yield matching commands
for cmd in self.commands:
if cmd.lower().startswith(cmd_text):
# Calculate how much to complete
yield Completion(
f'/{cmd}',
start_position=-len(text),
display=f'/{cmd}'
)
# Create completer for slash commands
commands = ['help', 'exit', 'quit', 'clear', 'tools', 'profile', 'model', 'stats', 'compact', 'undo', 'queue', 'q']
slash_completer = WordCompleter(
[f'/{cmd}' for cmd in commands],
ignore_case=True
)
slash_completer = SlashCommandCompleter(commands)
prompt_session = PromptSession(
message="❯ ",
completer=slash_completer,
auto_suggest=AutoSuggestFromHistory(),
history=InMemoryHistory()
history=InMemoryHistory(),
complete_while_typing=True
)
except ImportError:
pass # Fall back to simple input
@@ -3928,12 +3957,18 @@ Now, {final_instruction.lower()}:"""
# Process @file mentions before sending to LLM
processed_input = self._process_at_mentions(user_input, console)
# Process the prompt with profiling if enabled
self._process_interactive_prompt(
# Check if already processing - queue if so
if state_manager.is_processing:
message_queue.add(processed_input)
queue_count = message_queue.count
console.print(f"[dim]📋 Queued ({queue_count}) - processing will continue in order[/dim]")
continue
# Start async processing
self._process_interactive_prompt_async(
processed_input,
tools_list,
console,
show_profiling=session_state['show_profiling'],
session_state=session_state
)
@@ -4322,6 +4357,133 @@ Provide a concise summary (max 200 words):"""
return None
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
"""
Process a prompt asynchronously with live status display.
Shows real-time status (tools, commands) while processing in background.
Allows user to queue more messages while processing.
"""
import threading
import time
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from praisonai.cli.features.message_queue import ProcessingState
state_manager = session_state['state_manager']
message_queue = session_state['message_queue']
live_status = session_state['live_status']
# Set processing state
state_manager.set_state(ProcessingState.PROCESSING)
live_status.clear()
live_status.update_status("Thinking...")
# Result container for thread communication
result_container = {'response': None, 'error': None, 'done': False}
def process_in_background():
"""Run agent processing in background thread."""
try:
from praisonaiagents import Agent
import logging
import warnings
# Suppress noisy loggers
for logger_name in ["httpx", "httpcore", "duckduckgo_search", "crawl4ai"]:
logging.getLogger(logger_name).setLevel(logging.WARNING)
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
model = session_state.get('current_model')
live_status.update_status("Creating agent...")
# Create agent with tools
agent = Agent(
name="Assistant",
role="Helpful AI Assistant",
goal="Help the user with their tasks",
backstory="You are a helpful AI assistant with access to tools.",
tools=tools_list if tools_list else None,
verbose=False,
llm=model
)
live_status.update_status("Calling LLM...")
# Get response
response = agent.chat(prompt, stream=False)
result_container['response'] = str(response) if response else ""
except Exception as e:
result_container['error'] = e
finally:
result_container['done'] = True
# Start background thread
bg_thread = threading.Thread(target=process_in_background, daemon=True)
bg_thread.start()
# Show live status while processing
try:
with Live(console=console, refresh_per_second=4, transient=True) as live:
while not result_container['done']:
# Build status display
status_text = Text()
status_text.append("⏳ ", style="cyan")
status_text.append(live_status.current_status or "Processing...", style="dim")
# Show queued messages if any
queue_count = message_queue.count
if queue_count > 0:
status_text.append(f"\n📋 Queued: {queue_count}", style="dim yellow")
live.update(Panel(status_text, border_style="dim"))
time.sleep(0.1)
except KeyboardInterrupt:
console.print("\n[dim]Processing interrupted[/dim]")
# Wait for thread to finish
bg_thread.join(timeout=1.0)
# Handle result
if result_container['error']:
console.print(f"\n[red]Error: {result_container['error']}[/red]")
elif result_container['response']:
# Print response with streaming effect
console.print()
response_str = result_container['response']
words = response_str.split()
import sys
for i, word in enumerate(words):
console.print(word + " ", end="")
sys.stdout.flush()
if i % 15 == 14:
time.sleep(0.005)
console.print()
# Update session state
input_tokens = len(prompt) // 4
output_tokens = len(response_str) // 4
session_state['total_input_tokens'] += input_tokens
session_state['total_output_tokens'] += output_tokens
session_state['request_count'] += 1
session_state['conversation_history'].append({'role': 'user', 'content': prompt})
session_state['conversation_history'].append({'role': 'assistant', 'content': response_str})
# Set state back to idle
state_manager.set_state(ProcessingState.IDLE)
live_status.clear()
# Process next queued message if any
next_msg = message_queue.pop()
if next_msg:
console.print(f"\n[dim cyan]Processing queued message...[/dim cyan]")
self._process_interactive_prompt_async(next_msg, tools_list, console, session_state)
def _process_interactive_prompt(self, prompt, tools_list, console, show_profiling=False, session_state=None):
"""Process a prompt in interactive mode with streaming."""
from rich.live import Live
@@ -4402,7 +4564,7 @@ Provide a concise summary (max 200 words):"""
sys.stdout.flush()
if i % 15 == 14: # Small pause every 15 words for streaming effect
time.sleep(0.005)
console.print("\n") # Final newline
console.print() # Final newline
timings['display_end'] = time.time()
# Update session state with token estimates and history
+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.51 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.53 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.51"
__version__ = "2.3.53"
@@ -471,3 +471,191 @@ class TestMessageQueueIntegration:
queue_str = display.format_queue()
assert "Task 1" in queue_str
assert "Task 2" in queue_str
# ============================================================================
# 9. AsyncProcessor Tests - Background processing
# ============================================================================
class TestAsyncProcessor:
"""Test AsyncProcessor for background agent execution."""
def test_processor_creation(self):
"""Should create processor with callback."""
from praisonai.cli.features.message_queue import AsyncProcessor
results = []
def on_complete(result):
results.append(result)
processor = AsyncProcessor(on_complete=on_complete)
assert processor is not None
def test_start_processing(self):
"""Should start processing in background thread."""
from praisonai.cli.features.message_queue import AsyncProcessor
import time
results = []
def work_fn():
time.sleep(0.05)
return "done"
def on_complete(result):
results.append(result)
processor = AsyncProcessor(on_complete=on_complete)
processor.start(work_fn)
# Should be processing
assert processor.is_running
# Wait for completion
time.sleep(0.15)
assert not processor.is_running
assert "done" in results
def test_is_running_property(self):
"""Should track running state."""
from praisonai.cli.features.message_queue import AsyncProcessor
import time
def slow_work():
time.sleep(0.1)
return "result"
processor = AsyncProcessor()
assert not processor.is_running
processor.start(slow_work)
assert processor.is_running
time.sleep(0.15)
assert not processor.is_running
def test_on_status_callback(self):
"""Should call status callback during processing."""
from praisonai.cli.features.message_queue import AsyncProcessor
import time
statuses = []
def on_status(status):
statuses.append(status)
def work_with_status(status_callback):
status_callback("Starting...")
time.sleep(0.02)
status_callback("Working...")
return "done"
processor = AsyncProcessor(on_status=on_status)
processor.start(lambda: work_with_status(on_status))
time.sleep(0.1)
assert len(statuses) >= 1
# ============================================================================
# 10. LiveStatusDisplay Tests - Real-time status
# ============================================================================
class TestLiveStatusDisplay:
"""Test LiveStatusDisplay for real-time tool/command status."""
def test_display_creation(self):
"""Should create display."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
assert display is not None
def test_update_status(self):
"""Should update current status."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.update_status("Processing...")
assert display.current_status == "Processing..."
def test_add_tool_call(self):
"""Should track tool calls."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.add_tool_call("read_file", {"path": "test.py"})
assert len(display.tool_calls) == 1
assert display.tool_calls[0]['name'] == "read_file"
def test_add_command_execution(self):
"""Should track command executions."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.add_command("ls -la")
assert len(display.commands) == 1
assert display.commands[0] == "ls -la"
def test_format_live_status(self):
"""Should format live status for display."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.update_status("Thinking...")
display.add_tool_call("read_file", {"path": "main.py"})
formatted = display.format()
assert "Thinking" in formatted or "read_file" in formatted
def test_clear_status(self):
"""Should clear all status."""
from praisonai.cli.features.message_queue import LiveStatusDisplay
display = LiveStatusDisplay()
display.update_status("Working")
display.add_tool_call("test", {})
display.add_command("echo hi")
display.clear()
assert display.current_status == ""
assert len(display.tool_calls) == 0
assert len(display.commands) == 0
# ============================================================================
# 11. NonBlockingInput Tests - Async input handling
# ============================================================================
class TestNonBlockingInput:
"""Test NonBlockingInput for async user input."""
def test_input_handler_creation(self):
"""Should create input handler."""
from praisonai.cli.features.message_queue import NonBlockingInput
handler = NonBlockingInput()
assert handler is not None
def test_submit_input(self):
"""Should accept input while processing."""
from praisonai.cli.features.message_queue import NonBlockingInput
handler = NonBlockingInput()
handler.submit("new message")
assert handler.has_pending
assert handler.pop() == "new message"
def test_multiple_inputs(self):
"""Should queue multiple inputs."""
from praisonai.cli.features.message_queue import NonBlockingInput
handler = NonBlockingInput()
handler.submit("first")
handler.submit("second")
assert handler.pending_count == 2
assert handler.pop() == "first"
assert handler.pop() == "second"