Compare commits

...
5 Commits
Author SHA1 Message Date
Mervin Praison f5d4db4262 Release v3.1.2 2026-01-02 08:17:39 +00:00
Mervin Praison 87d691c616 Release v3.1.1 2026-01-02 07:54:02 +00:00
Mervin Praison 8ea15dee0f Release v3.1.0 2026-01-02 07:35:57 +00:00
Mervin Praison 4f56842e08 Release v3.0.9 2026-01-02 07:22:27 +00:00
Mervin Praison a24836aaee Release v3.0.8 2026-01-02 06:59:48 +00:00
11 changed files with 1716 additions and 970 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>=3.0.7" \
"praisonai>=3.1.2" \
"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>=3.0.7" \
"praisonai>=3.1.2" \
"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>=3.0.7" \
"praisonai>=3.1.2" \
"praisonai[ui]" \
"praisonai[crewai]"
+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/v3.0.7.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.0.7.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.2.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.2.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
@@ -4,6 +4,7 @@ ACP health checks for PraisonAI Doctor.
import asyncio
import logging
import os
from ..models import CheckCategory, CheckResult, CheckStatus
from ..registry import register_check
@@ -17,7 +18,7 @@ logger = logging.getLogger(__name__)
category=CheckCategory.TOOLS,
description="Check if ACP module is installed"
)
def check_acp_module() -> CheckResult:
def check_acp_module(config=None) -> CheckResult:
"""Check if ACP module is available."""
try:
import importlib.util
@@ -25,21 +26,30 @@ def check_acp_module() -> CheckResult:
if spec is not None:
return CheckResult(
id="acp_module",
title="ACP Module Available",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="ACP module is available",
details={"module": "praisonai.acp"}
metadata={"module": "praisonai.acp"}
)
else:
return CheckResult(
id="acp_module",
title="ACP Module Available",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message="ACP module not found",
details={"hint": "ACP module should be part of praisonai package"}
remediation="ACP module should be part of praisonai package"
)
except Exception as e:
return CheckResult(
id="acp_module",
title="ACP Module Available",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"Error checking ACP module: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
@@ -49,7 +59,7 @@ def check_acp_module() -> CheckResult:
category=CheckCategory.TOOLS,
description="Check if agent-client-protocol SDK is installed"
)
def check_acp_sdk() -> CheckResult:
def check_acp_sdk(config=None) -> CheckResult:
"""Check if ACP SDK is available."""
try:
import importlib.util
@@ -57,21 +67,30 @@ def check_acp_sdk() -> CheckResult:
if spec is not None:
return CheckResult(
id="acp_sdk",
title="ACP SDK Available",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="ACP SDK (agent-client-protocol) is installed",
details={"package": "agent-client-protocol"}
metadata={"package": "agent-client-protocol"}
)
else:
return CheckResult(
status=CheckStatus.INFO,
id="acp_sdk",
title="ACP SDK Available",
category=CheckCategory.TOOLS,
status=CheckStatus.SKIP,
message="ACP SDK not installed (optional)",
details={"hint": "Install with: pip install agent-client-protocol"}
remediation="Install with: pip install agent-client-protocol"
)
except Exception as e:
return CheckResult(
id="acp_sdk",
title="ACP SDK Available",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"Error checking ACP SDK: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
@@ -81,27 +100,36 @@ def check_acp_sdk() -> CheckResult:
category=CheckCategory.TOOLS,
description="Check if ACP server can be imported"
)
def check_acp_server() -> CheckResult:
def check_acp_server(config=None) -> CheckResult:
"""Check if ACP server can be imported."""
try:
from praisonai.acp import ACPServer # noqa: F401
return CheckResult(
id="acp_server",
title="ACP Server Import",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="ACP server can be imported",
details={"class": "ACPServer"}
metadata={"class": "ACPServer"}
)
except ImportError as e:
return CheckResult(
id="acp_server",
title="ACP Server Import",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message=f"ACP server import failed: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
except Exception as e:
return CheckResult(
id="acp_server",
title="ACP Server Import",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"Error importing ACP server: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
@@ -111,20 +139,21 @@ def check_acp_server() -> CheckResult:
category=CheckCategory.CONFIG,
description="Check ACP configuration"
)
def check_acp_config() -> CheckResult:
def check_acp_config(config=None) -> CheckResult:
"""Check ACP configuration."""
import os
config = {
acp_conf = {
"approval_mode": os.environ.get("PRAISONAI_APPROVAL_MODE", "manual"),
"allow_write": os.environ.get("PRAISONAI_ALLOW_WRITE", "false"),
"allow_shell": os.environ.get("PRAISONAI_ALLOW_SHELL", "false"),
}
return CheckResult(
id="acp_config",
title="ACP Configuration",
category=CheckCategory.CONFIG,
status=CheckStatus.PASS,
message=f"ACP configured with approval_mode={config['approval_mode']}",
details=config
message=f"ACP configured with approval_mode={acp_conf['approval_mode']}",
metadata=acp_conf
)
@@ -135,7 +164,7 @@ def check_acp_config() -> CheckResult:
description="Check if ACP runtime can start",
requires_deep=True
)
def check_acp_runtime() -> CheckResult:
def check_acp_runtime(config=None) -> CheckResult:
"""Check if ACP runtime can start."""
try:
from praisonai.cli.features.interactive_runtime import create_runtime
@@ -150,88 +179,39 @@ def check_acp_runtime() -> CheckResult:
await runtime.start()
ready = runtime.acp_ready
error = runtime._acp_state.error
read_only = runtime.read_only
return ready, error, read_only
return ready, error
finally:
await runtime.stop()
ready, error, read_only = asyncio.run(_check())
ready, error = asyncio.run(_check())
if ready:
return CheckResult(
id="acp_runtime",
title="ACP Runtime Check",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="ACP runtime started successfully",
details={"ready": True, "read_only": read_only}
metadata={"ready": True}
)
else:
return CheckResult(
id="acp_runtime",
title="ACP Runtime Check",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message=f"ACP runtime failed to start: {error}",
details={"ready": False, "error": error, "read_only": read_only}
metadata={"ready": False, "error": str(error)}
)
except Exception as e:
return CheckResult(
id="acp_runtime",
title="ACP Runtime Check",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"ACP runtime check failed: {e}",
details={"error": str(e)}
)
@register_check(
id="interactive_runtime",
title="Interactive Runtime Check",
category=CheckCategory.TOOLS,
description="Check if full interactive runtime can start",
requires_deep=True
)
def check_interactive_runtime() -> CheckResult:
"""Check if full interactive runtime can start."""
try:
from praisonai.cli.features.interactive_runtime import create_runtime
async def _check():
runtime = create_runtime(
workspace=".",
lsp=True,
acp=True
)
try:
await runtime.start()
status = runtime.get_status()
return status
finally:
await runtime.stop()
status = asyncio.run(_check())
lsp_ready = status.get("lsp", {}).get("ready", False)
acp_ready = status.get("acp", {}).get("ready", False)
if lsp_ready and acp_ready:
return CheckResult(
status=CheckStatus.PASS,
message="Interactive runtime fully operational",
details=status
)
elif lsp_ready or acp_ready:
return CheckResult(
status=CheckStatus.WARN,
message="Interactive runtime partially operational",
details=status
)
else:
return CheckResult(
status=CheckStatus.FAIL,
message="Interactive runtime failed to start",
details=status
)
except Exception as e:
return CheckResult(
status=CheckStatus.FAIL,
message=f"Interactive runtime check failed: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
@@ -4,6 +4,7 @@ LSP health checks for PraisonAI Doctor.
import asyncio
import logging
import shutil
from ..models import CheckCategory, CheckResult, CheckStatus
from ..registry import register_check
@@ -17,7 +18,7 @@ logger = logging.getLogger(__name__)
category=CheckCategory.TOOLS,
description="Check if LSP module is installed"
)
def check_lsp_module() -> CheckResult:
def check_lsp_module(config=None) -> CheckResult:
"""Check if LSP module is available."""
try:
import importlib.util
@@ -25,21 +26,30 @@ def check_lsp_module() -> CheckResult:
if spec is not None:
return CheckResult(
id="lsp_module",
title="LSP Module Available",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="LSP module is available",
details={"module": "praisonaiagents.lsp"}
metadata={"module": "praisonaiagents.lsp"}
)
else:
return CheckResult(
id="lsp_module",
title="LSP Module Available",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message="LSP module not found",
details={"hint": "Install praisonaiagents with LSP support"}
remediation="Install praisonaiagents with LSP support"
)
except Exception as e:
return CheckResult(
id="lsp_module",
title="LSP Module Available",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"Error checking LSP module: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
@@ -49,27 +59,36 @@ def check_lsp_module() -> CheckResult:
category=CheckCategory.TOOLS,
description="Check if LSP client can be imported"
)
def check_lsp_client() -> CheckResult:
def check_lsp_client(config=None) -> CheckResult:
"""Check if LSP client can be imported."""
try:
from praisonaiagents.lsp import LSPClient # noqa: F401
return CheckResult(
id="lsp_client",
title="LSP Client Import",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="LSP client can be imported",
details={"class": "LSPClient"}
metadata={"class": "LSPClient"}
)
except ImportError as e:
return CheckResult(
id="lsp_client",
title="LSP Client Import",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message=f"LSP client import failed: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
except Exception as e:
return CheckResult(
id="lsp_client",
title="LSP Client Import",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"Error importing LSP client: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
@@ -79,11 +98,8 @@ def check_lsp_client() -> CheckResult:
category=CheckCategory.TOOLS,
description="Check if Python language server is available"
)
def check_lsp_server_python() -> CheckResult:
def check_lsp_server_python(config=None) -> CheckResult:
"""Check if Python language server is available."""
import shutil
# Check for common Python language servers
servers = [
("pyright-langserver", "pyright"),
("pylsp", "python-lsp-server"),
@@ -97,18 +113,21 @@ def check_lsp_server_python() -> CheckResult:
if found:
return CheckResult(
id="lsp_server_python",
title="Python LSP Server",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message=f"Python LSP server available: {found[0]['command']}",
details={"servers": found}
metadata={"servers": found}
)
else:
return CheckResult(
id="lsp_server_python",
title="Python LSP Server",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message="No Python LSP server found",
details={
"hint": "Install with: npm install -g pyright OR pip install python-lsp-server",
"checked": [s[0] for s in servers]
}
remediation="Install with: npm install -g pyright OR pip install python-lsp-server"
)
@@ -118,30 +137,34 @@ def check_lsp_server_python() -> CheckResult:
category=CheckCategory.TOOLS,
description="Check if TypeScript language server is available"
)
def check_lsp_server_typescript() -> CheckResult:
def check_lsp_server_typescript(config=None) -> CheckResult:
"""Check if TypeScript language server is available."""
import shutil
# Check for TypeScript language server
if shutil.which("typescript-language-server"):
return CheckResult(
id="lsp_server_typescript",
title="TypeScript LSP Server",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="TypeScript LSP server available",
details={"command": "typescript-language-server"}
metadata={"command": "typescript-language-server"}
)
elif shutil.which("tsserver"):
return CheckResult(
id="lsp_server_typescript",
title="TypeScript LSP Server",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="TypeScript server available (tsserver)",
details={"command": "tsserver"}
metadata={"command": "tsserver"}
)
else:
return CheckResult(
status=CheckStatus.INFO,
id="lsp_server_typescript",
title="TypeScript LSP Server",
category=CheckCategory.TOOLS,
status=CheckStatus.SKIP,
message="TypeScript LSP server not found (optional)",
details={
"hint": "Install with: npm install -g typescript-language-server typescript"
}
remediation="Install with: npm install -g typescript-language-server typescript"
)
@@ -152,7 +175,7 @@ def check_lsp_server_typescript() -> CheckResult:
description="Check if LSP runtime can start",
requires_deep=True
)
def check_lsp_runtime() -> CheckResult:
def check_lsp_runtime(config=None) -> CheckResult:
"""Check if LSP runtime can start."""
try:
from praisonai.cli.features.interactive_runtime import create_runtime
@@ -175,22 +198,31 @@ def check_lsp_runtime() -> CheckResult:
if ready:
return CheckResult(
id="lsp_runtime",
title="LSP Runtime Check",
category=CheckCategory.TOOLS,
status=CheckStatus.PASS,
message="LSP runtime started successfully",
details={"ready": True}
metadata={"ready": True}
)
else:
return CheckResult(
id="lsp_runtime",
title="LSP Runtime Check",
category=CheckCategory.TOOLS,
status=CheckStatus.WARN,
message=f"LSP runtime failed to start: {error}",
details={"ready": False, "error": error}
metadata={"ready": False, "error": str(error)}
)
except Exception as e:
return CheckResult(
id="lsp_runtime",
title="LSP Runtime Check",
category=CheckCategory.TOOLS,
status=CheckStatus.FAIL,
message=f"LSP runtime check failed: {e}",
details={"error": str(e)}
metadata={"error": str(e)}
)
+483 -150
View File
@@ -127,6 +127,46 @@ AUTOGEN_AVAILABLE = importlib.util.find_spec("autogen") is not None
PRAISONAI_AVAILABLE = importlib.util.find_spec("praisonaiagents") is not None
TRAIN_AVAILABLE = importlib.util.find_spec("unsloth") is not None
# Lazy import helpers for optional dependencies (defined after availability flags)
def _get_call_module():
"""Lazy import call module only when call feature is used.
Raises:
ImportError: If praisonai.api.call is not installed
"""
if not CALL_MODULE_AVAILABLE:
raise ImportError(
"Call feature is not installed. Install with: pip install \"praisonai[call]\""
)
from praisonai.api import call as call_module
return call_module
def _get_gradio():
"""Lazy import gradio only when gradio UI is used.
Raises:
ImportError: If gradio is not installed
"""
if not GRADIO_AVAILABLE:
raise ImportError(
"Gradio is not installed. Install with: pip install gradio"
)
import gradio as gr
return gr
def _get_autogen():
"""Lazy import autogen only when autogen framework is used.
Raises:
ImportError: If autogen is not installed
"""
if not AUTOGEN_AVAILABLE:
raise ImportError(
"AutoGen is not installed. Install with: pip install \"praisonai[autogen]\""
)
import autogen
return autogen
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'WARNING') or 'WARNING', format='%(asctime)s - %(levelname)s - %(message)s')
logging.getLogger('alembic').setLevel(logging.ERROR)
logging.getLogger('gradio').setLevel(logging.ERROR)
@@ -473,10 +513,14 @@ class PraisonAI:
return
if getattr(args, 'call', False):
if not CALL_MODULE_AVAILABLE:
print("[red]ERROR: Call feature is not installed. Install with:[/red]")
print("\npip install \"praisonai[call]\"\n")
return
call_args = []
if args.public:
call_args.append('--public')
call_module.main(call_args)
_get_call_module().main(call_args)
return
if args.command == 'train':
@@ -837,6 +881,7 @@ class PraisonAI:
# Interactive TUI - terminal interface with slash commands
parser.add_argument("--interactive", "-i", action="store_true", help="Start interactive terminal mode with slash commands")
parser.add_argument("--chat-mode", "--chat", action="store_true", dest="chat_mode", help="Run single prompt in interactive style (non-interactive, for testing)")
parser.add_argument("--resume", type=str, dest="resume_session", metavar="SESSION", help="Resume a session (use 'last' for most recent, or session ID)")
# Direct prompt flag - alternative to positional command
parser.add_argument("-p", "--prompt", type=str, dest="prompt_flag", help="Direct prompt to execute (alternative to positional argument)")
@@ -955,7 +1000,7 @@ class PraisonAI:
call_args = []
if args.public:
call_args.append('--public')
call_module.main(call_args)
_get_call_module().main(call_args)
sys.exit(0)
# Handle special commands
@@ -993,7 +1038,7 @@ class PraisonAI:
print("[red]ERROR: Call feature is not installed. Install with:[/red]")
print("\npip install \"praisonai[call]\"\n")
sys.exit(1)
call_module.main()
_get_call_module().main()
sys.exit(0)
elif args.command == 'realtime':
@@ -2126,10 +2171,10 @@ class PraisonAI:
# Save output if requested
if workflow_save and final_results:
import datetime
from datetime import datetime
output_dir = os.path.join(os.getcwd(), "output", "workflows")
os.makedirs(output_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
safe_name = workflow_name.replace(" ", "_").lower()
output_file = os.path.join(output_dir, f"{timestamp}_{safe_name}.md")
@@ -3444,10 +3489,10 @@ Provide ONLY the commit message, no explanations."""
# Save if requested
if workflow_save:
import datetime
from datetime import datetime
output_dir = os.path.join(os.getcwd(), "output", "workflows")
os.makedirs(output_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
output_file = os.path.join(output_dir, f"{timestamp}_inline_workflow.md")
with open(output_file, "w") as f:
@@ -3931,6 +3976,8 @@ Now, {final_instruction.lower()}:"""
)
return crew.kickoff()
elif AUTOGEN_AVAILABLE:
# Lazy import autogen only when needed
autogen = _get_autogen()
config_list = self.config_list
# Add llm if specified
if hasattr(self, 'args') and self.args.llm:
@@ -4021,8 +4068,13 @@ Now, {final_instruction.lower()}:"""
console = Console()
# Import callback registration from praisonaiagents
# Store in local variable for safe access throughout the function
_sync_display_callbacks = None
_register_display_callback = None
try:
from praisonaiagents import register_display_callback, sync_display_callbacks
_sync_display_callbacks = sync_display_callbacks
_register_display_callback = register_display_callback
except ImportError:
# Fallback if callbacks not available - just run agent directly
try:
@@ -4074,8 +4126,8 @@ Now, {final_instruction.lower()}:"""
# Tool execution completed, update status
status_info['status'] = "Processing result..."
# Register callback for tool calls
register_display_callback('tool_call', tool_call_callback)
# Register callback for tool calls (use local variable)
_register_display_callback('tool_call', tool_call_callback)
def build_status_display():
"""Build the status display text."""
@@ -4194,17 +4246,17 @@ Now, {final_instruction.lower()}:"""
time.sleep(0.1)
except KeyboardInterrupt:
console.print("\n[dim]Interrupted[/dim]")
# Unregister callback
if 'tool_call' in sync_display_callbacks:
del sync_display_callbacks['tool_call']
# Unregister callback (use local variable with None check)
if _sync_display_callbacks is not None and 'tool_call' in _sync_display_callbacks:
del _sync_display_callbacks['tool_call']
return None
# Wait for thread to complete
thread.join(timeout=1.0)
# Unregister callback to avoid memory leaks
if 'tool_call' in sync_display_callbacks:
del sync_display_callbacks['tool_call']
# Unregister callback to avoid memory leaks (use local variable with None check)
if _sync_display_callbacks is not None and 'tool_call' in _sync_display_callbacks:
del _sync_display_callbacks['tool_call']
# Handle result
if status_info['error']:
@@ -4481,6 +4533,9 @@ Now, {final_instruction.lower()}:"""
Create a Gradio interface for generating agents and performing tasks.
"""
if GRADIO_AVAILABLE:
# Lazy import gradio only when needed
gr = _get_gradio()
def generate_crew_and_kickoff_interface(auto_args, framework):
self.framework = framework
self.agent_file = "test.yaml"
@@ -4802,9 +4857,10 @@ Now, {final_instruction.lower()}:"""
# Import message queue components
from praisonai.cli.features.message_queue import (
MessageQueue, StateManager, QueueDisplay, ProcessingState,
AsyncProcessor, LiveStatusDisplay
LiveStatusDisplay
)
import threading
import queue as queue_module
# Create message queue and state manager
message_queue = MessageQueue()
@@ -4813,6 +4869,30 @@ Now, {final_instruction.lower()}:"""
live_status = LiveStatusDisplay()
processing_lock = threading.Lock()
# Create execution queue for worker thread (TRUE ASYNC)
execution_queue = queue_module.Queue()
approval_request_queue = queue_module.Queue()
approval_response_queue = queue_module.Queue()
# Worker state for cross-thread communication
# Enhanced with task-bound context for proper Q/A mapping
worker_state = {
'running': True,
'current_task': None, # Full task context object (FIFO head)
'approval_pending': False,
'waiting_for_approval_input': False,
'completed_tasks': [], # Queue of completed tasks to display (FIFO order)
'error_tasks': [], # Queue of error tasks to display
'tool_activity': None, # Current tool being used
'last_status_line': None, # For transient status updates
}
# Task counter for unique IDs (FIFO position tracking)
task_counter = {'value': 0}
# Check for verbose mode
verbose_mode = getattr(args, 'verbose', False) if hasattr(args, 'verbose') else False
# Initialize persistent session
from praisonai.cli.session import get_session_store
session_store = get_session_store()
@@ -4847,11 +4927,17 @@ Now, {final_instruction.lower()}:"""
'queue_display': queue_display,
'live_status': live_status,
'processing_lock': processing_lock,
'async_processor': None, # Will be created per-request
'execution_queue': execution_queue, # Queue for worker thread
'approval_request_queue': approval_request_queue,
'approval_response_queue': approval_response_queue,
'worker_state': worker_state,
'unified_session': unified_session, # Reference to persistent session
'session_store': session_store, # Reference to store for saving
}
# Start the execution worker thread (TRUE ASYNC - runs in background)
worker_thread = self._start_execution_worker(tools_list, console, session_state)
# Try to use prompt_toolkit for autocomplete
prompt_session = None
try:
@@ -4885,10 +4971,155 @@ Now, {final_instruction.lower()}:"""
console.print(f"[dim]Resumed session with {unified_session.message_count} messages[/dim]")
console.print("[dim]Use @file.txt to include file content | Queue messages while processing[/dim]\n")
# Start display thread for status updates (non-blocking)
import time
from rich.panel import Panel
display_running = {'value': True}
def display_loop():
"""Background thread for status display - NEVER blocks input.
FIFO-aligned display:
- Default mode: Minimal, calm output. Just show response.
- Verbose mode: Full task lifecycle with IDs, times, word counts.
"""
while display_running['value']:
try:
# Check for completed tasks to display (FIFO order guaranteed)
if worker_state['completed_tasks']:
task = worker_state['completed_tasks'].pop(0)
response = task.get('response', '')
# Clear any transient status line
if worker_state.get('last_status_line'):
console.print("\r" + " " * 80 + "\r", end="")
worker_state['last_status_line'] = None
console.print() # New line before response
if verbose_mode:
# VERBOSE: Full task lifecycle with metadata
question = task.get('question', '')
task_id = task.get('task_id', 0)
elapsed = task.get('elapsed', 0)
word_count = len(response.split()) if response else 0
q_display = question[:80] + "..." if len(question) > 80 else question
console.print(f"[bold cyan]─── Task #{task_id} completed ({elapsed:.1f}s, {word_count} words) ───[/bold cyan]")
console.print(f"[bold green]Q:[/bold green] {q_display}")
console.print(f"[bold blue]A:[/bold blue] ", end="")
# Stream response (both modes)
if response:
words = response.split()
for i, word in enumerate(words):
console.print(word + " ", end="")
if i % 20 == 19:
time.sleep(0.003)
console.print()
if verbose_mode:
task_id = task.get('task_id', 0)
console.print(f"[dim]─── End Task #{task_id} ───[/dim]")
console.print()
# Check for error tasks to display
if worker_state['error_tasks']:
task = worker_state['error_tasks'].pop(0)
error = task.get('error', '')
# Clear transient status
if worker_state.get('last_status_line'):
console.print("\r" + " " * 80 + "\r", end="")
worker_state['last_status_line'] = None
console.print()
if verbose_mode:
question = task.get('question', '')
task_id = task.get('task_id', 0)
q_display = question[:80] + "..." if len(question) > 80 else question
console.print(f"[bold red]─── Task #{task_id} failed ───[/bold red]")
console.print(f"[bold green]Q:[/bold green] {q_display}")
console.print(f"[red]Error: {error}[/red]")
console.print()
# Check for approval requests
try:
approval_info = approval_request_queue.get_nowait()
function_name = approval_info['function_name']
arguments = approval_info['arguments']
risk_level = approval_info['risk_level']
risk_colors = {"critical": "bold red", "high": "red", "medium": "yellow", "low": "blue"}
risk_color = risk_colors.get(risk_level, "white")
# Clear transient status
if worker_state.get('last_status_line'):
console.print("\r" + " " * 80 + "\r", end="")
worker_state['last_status_line'] = None
console.print()
# Show approval panel (both modes)
tool_info = f"[bold]Function:[/] {function_name}\n"
tool_info += f"[bold]Risk Level:[/] [{risk_color}]{risk_level.upper()}[/{risk_color}]\n"
tool_info += "[bold]Arguments:[/]\n"
for key, value in arguments.items():
str_value = str(value)[:100] + "..." if len(str(value)) > 100 else str(value)
tool_info += f" {key}: {str_value}\n"
console.print(Panel(tool_info.strip(), title="🔒 Tool Approval Required", border_style=risk_color))
console.print(f"[{risk_color}]Type 'y' to approve, 'n' to reject:[/{risk_color}] ", end="")
worker_state['waiting_for_approval_input'] = True
except queue_module.Empty:
pass
# Show FIFO head status (transient, both modes)
current_task = worker_state.get('current_task')
if current_task and not worker_state.get('waiting_for_approval_input'):
queue_size = execution_queue.qsize()
prompt_preview = current_task.get('question', '')[:50]
if len(current_task.get('question', '')) > 50:
prompt_preview += "..."
# Build status line
tool_activity = worker_state.get('tool_activity')
if tool_activity and tool_activity.get('status') == 'started':
tool_name = tool_activity.get('name', '')
status_line = f"⚙️ {tool_name}"
else:
status_line = f"⏳ Processing: {prompt_preview}"
if queue_size > 0:
status_line += f" | 📋 {queue_size} waiting"
# Only update if changed (avoid flicker)
if status_line != worker_state.get('last_status_line'):
console.print(f"\r[dim cyan]{status_line}[/dim cyan]" + " " * 20, end="\r")
worker_state['last_status_line'] = status_line
elif not current_task and worker_state.get('last_status_line'):
# Clear status when idle
console.print("\r" + " " * 80 + "\r", end="")
worker_state['last_status_line'] = None
time.sleep(0.1)
except Exception:
pass
# Start display thread
display_thread = threading.Thread(target=display_loop, daemon=True, name="DisplayLoop")
display_thread.start()
running = True
while running:
try:
# Get user input (with autocomplete if available)
# This is NON-BLOCKING relative to LLM execution
# Status is shown by the display thread, not here
if prompt_session:
user_input = prompt_session.prompt().strip()
else:
@@ -4897,6 +5128,21 @@ Now, {final_instruction.lower()}:"""
if not user_input:
continue
# Check if this is an approval response
if worker_state.get('waiting_for_approval_input'):
try:
from praisonaiagents.approval import ApprovalDecision
if user_input.lower() in ['y', 'yes', 'approve']:
console.print("[green]✅ Approved[/green]")
approval_response_queue.put(ApprovalDecision(approved=True, reason="User approved"))
else:
console.print("[red]❌ Denied[/red]")
approval_response_queue.put(ApprovalDecision(approved=False, reason="User denied"))
worker_state['waiting_for_approval_input'] = False
continue
except ImportError:
pass
# Handle slash commands
if user_input.startswith("/"):
cmd_parts = user_input[1:].split(maxsplit=1)
@@ -4906,6 +5152,8 @@ Now, {final_instruction.lower()}:"""
if cmd in ["exit", "quit", "q"]:
console.print("[dim]Goodbye![/dim]")
running = False
worker_state['running'] = False
display_running['value'] = False
continue
elif cmd == "help":
self._print_interactive_help(console)
@@ -4976,6 +5224,17 @@ Now, {final_instruction.lower()}:"""
session_state['request_count'] = 0
console.print(f"[cyan]Started new session: {new_session.session_id}[/cyan]")
continue
elif cmd == "status":
# Show current processing status
current = worker_state.get('current_prompt')
queue_size = execution_queue.qsize()
if current:
console.print(f"[cyan]Processing:[/cyan] {current}")
if queue_size > 0:
console.print(f"[cyan]Queued:[/cyan] {queue_size} messages")
if not current and queue_size == 0:
console.print("[dim]Idle - no messages processing[/dim]")
continue
else:
console.print(f"[yellow]Unknown command: /{cmd}. Type /help for available commands.[/yellow]")
continue
@@ -4983,20 +5242,35 @@ Now, {final_instruction.lower()}:"""
# Process @file mentions before sending to LLM
processed_input = self._process_at_mentions(user_input, console)
# 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
# Create task with unique ID and full context
task_counter['value'] += 1
task_id = task_counter['value']
# Start async processing
self._process_interactive_prompt_async(
processed_input,
tools_list,
console,
session_state=session_state
)
task = {
'task_id': task_id,
'prompt': processed_input,
'question': processed_input, # Full question for Q/A mapping
'status': 'queued',
'queued_at': time.time(),
}
# Submit to execution queue - THIS IS NON-BLOCKING (FIFO)
queue_size = execution_queue.qsize()
execution_queue.put(task)
# Show queue status (minimal by default, verbose shows task IDs)
if verbose_mode:
q_preview = processed_input[:60] + "..." if len(processed_input) > 60 else processed_input
if queue_size > 0:
console.print(f"[dim cyan]📋 Task #{task_id} queued (FIFO position {queue_size + 1})[/dim cyan]")
console.print(f"[dim] └─ {q_preview}[/dim]")
else:
console.print(f"[dim cyan]▶ Task #{task_id} started (FIFO head)[/dim cyan]")
console.print(f"[dim] └─ {q_preview}[/dim]")
else:
# DEFAULT: Minimal, calm output
if queue_size > 0:
console.print(f"[dim]📋 Queued ({queue_size + 1} in queue)[/dim]")
except KeyboardInterrupt:
console.print("\n[dim]Use /exit to quit[/dim]")
@@ -5439,142 +5713,201 @@ Provide a concise summary (max 200 words):"""
return None
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
def _start_execution_worker(self, tools_list, console, session_state):
"""
Process a prompt asynchronously with non-blocking status display.
Start the background execution worker thread.
Shows status updates without blocking user input.
Allows user to queue more messages while processing.
This worker continuously processes messages from the queue,
allowing the main input loop to remain non-blocking.
"""
import threading
import time
import sys
import queue as queue_module
from rich.panel import Panel
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']
execution_queue = session_state['execution_queue']
approval_request_queue = session_state['approval_request_queue']
approval_response_queue = session_state['approval_response_queue']
worker_state = session_state['worker_state']
# Set processing state
state_manager.set_state(ProcessingState.PROCESSING)
live_status.clear()
live_status.update_status("Thinking...")
# Check if trust mode is enabled
trust_mode = getattr(self.args, 'trust', False) if hasattr(self, 'args') else False
# Print initial status using Rich console (proper rendering)
console.print(f"⏳ {live_status.current_status}", style="dim cyan")
# Result container for thread communication
result_container = {'response': None, 'error': None, 'done': False, 'last_status': ''}
def process_in_background():
"""Run agent processing in background thread."""
try:
from praisonaiagents import Agent
import logging
import warnings
def worker_loop():
"""Main worker loop - processes execution queue."""
while worker_state['running']:
try:
# Wait for a message with timeout (allows checking running flag)
try:
task = execution_queue.get(timeout=0.5)
except queue_module.Empty:
continue
# Extract task context
prompt = task.get('prompt', '')
task_id = task.get('task_id', 0)
question = task.get('question', prompt)
start_time = time.time()
# Set processing state with full task context
state_manager.set_state(ProcessingState.PROCESSING)
task['status'] = 'running'
task['start_time'] = start_time
worker_state['current_task'] = task # Full task context
live_status.clear()
live_status.update_status(f"Task #{task_id}: Thinking...")
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)
# Set up approval callback
try:
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
if trust_mode:
def auto_approve_all(function_name, arguments, risk_level):
return ApprovalDecision(approved=True, reason="Auto-approved via --trust flag")
set_approval_callback(auto_approve_all)
else:
def interactive_approval_callback(function_name, arguments, risk_level):
"""Request approval from main thread via queue."""
approval_request_queue.put({
'function_name': function_name,
'arguments': arguments,
'risk_level': risk_level
})
worker_state['approval_pending'] = True
try:
response = approval_response_queue.get(timeout=120)
worker_state['approval_pending'] = False
return response
except queue_module.Empty:
worker_state['approval_pending'] = False
return ApprovalDecision(approved=False, reason="Approval timeout")
set_approval_callback(interactive_approval_callback)
except ImportError:
pass
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
model = session_state.get('current_model')
conversation_history = session_state.get('conversation_history', [])
live_status.update_status("Creating agent...")
# Build backstory with context
backstory = "You are a helpful AI assistant with access to tools for file operations, code intelligence, and shell commands."
if conversation_history:
recent = conversation_history[-10:]
context_lines = []
for msg in recent:
role = msg.get('role', 'unknown')
content = msg.get('content', '')[:200]
context_lines.append(f"{role}: {content}")
if context_lines:
backstory += "\n\nRecent conversation context:\n" + "\n".join(context_lines)
agent = Agent(
name="Assistant",
role="Helpful AI Assistant",
goal="Help the user with their tasks",
backstory=backstory,
tools=tools_list if tools_list else None,
verbose=False,
llm=model
)
live_status.update_status(f"Task #{task_id}: Calling LLM...")
response = agent.chat(prompt, stream=False)
response_str = str(response) if response else ""
# Calculate elapsed time
elapsed = time.time() - start_time
# Store completed task for display with Q/A mapping
completed_task = {
'task_id': task_id,
'question': task.get('question', prompt),
'response': response_str,
'elapsed': elapsed,
'status': 'completed',
}
worker_state['completed_tasks'].append(completed_task)
# 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})
# Persist to UnifiedSession
if 'unified_session' in session_state and 'session_store' in session_state:
unified_session = session_state['unified_session']
unified_session.add_user_message(prompt)
unified_session.add_assistant_message(response_str)
unified_session.update_stats(input_tokens, output_tokens)
session_state['session_store'].save(unified_session)
except Exception as e:
# Store error task for display with Q/A mapping
error_task = {
'task_id': task_id,
'question': task.get('question', prompt),
'error': str(e),
'status': 'failed',
}
worker_state['error_tasks'].append(error_task)
finally:
worker_state['current_task'] = None
worker_state['tool_activity'] = None
state_manager.set_state(ProcessingState.IDLE)
live_status.clear()
execution_queue.task_done()
# 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
except Exception as e:
import traceback
traceback.print_exc()
# Start background thread
bg_thread = threading.Thread(target=process_in_background, daemon=True)
bg_thread.start()
# Start worker thread
worker_thread = threading.Thread(target=worker_loop, daemon=True, name="ExecutionWorker")
worker_thread.start()
return worker_thread
def _submit_prompt_to_worker(self, prompt, session_state):
"""
Submit a prompt to the execution worker queue.
# Wait for completion with periodic status updates (non-blocking style)
start_time = time.time()
last_status_update = ""
while not result_container['done']:
time.sleep(0.2)
current_status = live_status.current_status
# Only print status if it changed
if current_status != last_status_update:
# Use carriage return to update in place
elapsed = time.time() - start_time
queue_count = message_queue.count
queue_info = f" | 📋 Queued: {queue_count}" if queue_count > 0 else ""
# Use carriage return to update in place - plain text only (no Rich markup)
status_text = f"\r⏳ {current_status} ({elapsed:.1f}s){queue_info}"
sys.stdout.write(status_text + " " * 10) # Pad to clear previous text
sys.stdout.flush()
last_status_update = current_status
# Clear the status line properly
sys.stdout.write("\r" + " " * 80 + "\r")
sys.stdout.flush()
# Wait for thread to finish
bg_thread.join(timeout=1.0)
# Handle result
if result_container['error']:
console.print(f"[red]Error: {result_container['error']}[/red]")
elif result_container['response']:
# Print response with streaming effect
response_str = result_container['response']
words = response_str.split()
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})
# Persist to UnifiedSession
if 'unified_session' in session_state and 'session_store' in session_state:
unified_session = session_state['unified_session']
unified_session.add_user_message(prompt)
unified_session.add_assistant_message(response_str)
unified_session.update_stats(input_tokens, output_tokens)
session_state['session_store'].save(unified_session)
# 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("\nProcessing queued message...", style="dim cyan")
self._process_interactive_prompt_async(next_msg, tools_list, console, session_state)
This is NON-BLOCKING - returns immediately after queuing.
"""
execution_queue = session_state['execution_queue']
execution_queue.put({'prompt': prompt})
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
"""
DEPRECATED: This method is kept for backward compatibility.
Use _submit_prompt_to_worker instead for true non-blocking behavior.
"""
# Just submit to worker queue - non-blocking
self._submit_prompt_to_worker(prompt, 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."""
+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==3.0.7 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==3.1.2 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+462 -160
View File
@@ -1,34 +1,24 @@
# Standard library imports
"""PraisonAI Chat - Optimized Chainlit Application
This is the optimized chat application with:
- Lazy imports for fast startup
- Agent reuse between messages (persistent agent per session)
- Default ACP/LSP tools with trust mode
- Session management
- Profiling hooks (optional via PRAISON_CHAT_PROFILE=1)
Performance improvements over original chat.py:
- Deferred database initialization
- Lazy loading of PIL, Tavily, crawl4ai, litellm
- Agent reuse (no re-creation per message)
- Interactive tools loaded once per session
"""
# Standard library imports (minimal at top level)
import os
from datetime import datetime
from typing import Dict, Optional
import logging
import json
import asyncio
import io
import base64
import importlib.util
import inspect
# Third-party imports
from dotenv import load_dotenv
from PIL import Image
from tavily import TavilyClient
from crawl4ai import AsyncWebCrawler
# Local application/library imports
import chainlit as cl
from chainlit.input_widget import TextInput
from chainlit.types import ThreadDict
import chainlit.data as cl_data
from litellm import acompletion
from literalai.helper import utc_now
from db import DatabaseManager
# Load environment variables
load_dotenv()
# Logging configuration
# Set up minimal logging first
logger = logging.getLogger(__name__)
log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO"
logger.handlers = []
@@ -39,52 +29,204 @@ console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
logger.setLevel(log_level)
# Chainlit must be imported early (required by decorators)
import chainlit as cl
from chainlit.input_widget import TextInput, Switch
from chainlit.types import ThreadDict
import chainlit.data as cl_data
# Profiling support (optional)
PROFILING_ENABLED = os.getenv("PRAISON_CHAT_PROFILE", "").lower() in ("1", "true", "yes")
_profile_data = {}
def _profile_start(name: str):
"""Start profiling a section."""
if PROFILING_ENABLED:
import time
_profile_data[name] = {"start": time.perf_counter()}
def _profile_end(name: str):
"""End profiling a section and log."""
if PROFILING_ENABLED and name in _profile_data:
import time
elapsed = time.perf_counter() - _profile_data[name]["start"]
_profile_data[name]["elapsed"] = elapsed
logger.info(f"[PROFILE] {name}: {elapsed*1000:.2f}ms")
# Lazy import cache
_cached_modules = {}
def _get_json():
if 'json' not in _cached_modules:
import json
_cached_modules['json'] = json
return _cached_modules['json']
def _get_asyncio():
if 'asyncio' not in _cached_modules:
import asyncio
_cached_modules['asyncio'] = asyncio
return _cached_modules['asyncio']
def _get_datetime():
if 'datetime' not in _cached_modules:
from datetime import datetime
_cached_modules['datetime'] = datetime
return _cached_modules['datetime']
def _get_io():
if 'io' not in _cached_modules:
import io
_cached_modules['io'] = io
return _cached_modules['io']
def _get_base64():
if 'base64' not in _cached_modules:
import base64
_cached_modules['base64'] = base64
return _cached_modules['base64']
def _get_importlib():
if 'importlib' not in _cached_modules:
import importlib.util
_cached_modules['importlib'] = importlib
return _cached_modules['importlib']
def _get_inspect():
if 'inspect' not in _cached_modules:
import inspect
_cached_modules['inspect'] = inspect
return _cached_modules['inspect']
def _get_pil_image():
if 'PIL.Image' not in _cached_modules:
_profile_start("import_pil")
from PIL import Image
_cached_modules['PIL.Image'] = Image
_profile_end("import_pil")
return _cached_modules['PIL.Image']
def _get_acompletion():
if 'acompletion' not in _cached_modules:
_profile_start("import_litellm")
from litellm import acompletion
_cached_modules['acompletion'] = acompletion
_profile_end("import_litellm")
return _cached_modules['acompletion']
def _get_tavily_client():
if 'TavilyClient' not in _cached_modules:
try:
_profile_start("import_tavily")
from tavily import TavilyClient
_cached_modules['TavilyClient'] = TavilyClient
_profile_end("import_tavily")
except ImportError:
_cached_modules['TavilyClient'] = None
return _cached_modules['TavilyClient']
def _get_async_web_crawler():
if 'AsyncWebCrawler' not in _cached_modules:
try:
_profile_start("import_crawl4ai")
from crawl4ai import AsyncWebCrawler
_cached_modules['AsyncWebCrawler'] = AsyncWebCrawler
_profile_end("import_crawl4ai")
except ImportError:
_cached_modules['AsyncWebCrawler'] = None
return _cached_modules['AsyncWebCrawler']
def _get_praisonai_agent():
"""Lazy load PraisonAI Agent for reuse."""
if 'Agent' not in _cached_modules:
try:
_profile_start("import_praisonai_agent")
from praisonaiagents import Agent
_cached_modules['Agent'] = Agent
_profile_end("import_praisonai_agent")
except ImportError:
_cached_modules['Agent'] = None
return _cached_modules['Agent']
def _get_interactive_tools():
"""Lazy load interactive tools (ACP, LSP, basic) with trust mode."""
if 'interactive_tools' not in _cached_modules:
try:
_profile_start("load_interactive_tools")
from praisonai.cli.features.interactive_tools import get_interactive_tools, ToolConfig
config = ToolConfig.from_env()
config.workspace = os.getcwd()
config.approval_mode = "auto" # Trust mode - auto-approve all tool executions
tools = get_interactive_tools(config=config)
_cached_modules['interactive_tools'] = tools
_profile_end("load_interactive_tools")
logger.info(f"Loaded {len(tools)} interactive tools (ACP, LSP, basic) with trust mode")
except ImportError as e:
logger.debug(f"Interactive tools not available: {e}")
_cached_modules['interactive_tools'] = []
return _cached_modules['interactive_tools']
# Deferred database initialization
_db_manager = None
def _get_db_manager():
"""Lazy initialize database manager."""
global _db_manager
if _db_manager is None:
_profile_start("init_database")
# Import from the same directory as this module
import sys
ui_dir = os.path.dirname(os.path.abspath(__file__))
if ui_dir not in sys.path:
sys.path.insert(0, ui_dir)
from db import DatabaseManager
_db_manager = DatabaseManager()
_db_manager.initialize()
cl_data._data_layer = _db_manager
_profile_end("init_database")
return _db_manager
# Load environment variables lazily
def _ensure_env_loaded():
if 'env_loaded' not in _cached_modules:
from dotenv import load_dotenv
load_dotenv()
_cached_modules['env_loaded'] = True
# Auth secret setup (required early)
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
if not CHAINLIT_AUTH_SECRET:
os.environ["CHAINLIT_AUTH_SECRET"] = "p8BPhQChpg@J>jBz$wGxqLX2V>yTVgP*7Ky9H$aV:axW~ANNX-7_T:o@lnyCBu^U"
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
now = utc_now()
create_step_counter = 0
# Initialize database
db_manager = DatabaseManager()
db_manager.initialize()
def save_setting(key: str, value: str):
"""Save a setting to the database"""
asyncio = _get_asyncio()
db_manager = _get_db_manager()
asyncio.run(db_manager.save_setting(key, value))
def load_setting(key: str) -> str:
"""Load a setting from the database"""
asyncio = _get_asyncio()
db_manager = _get_db_manager()
return asyncio.run(db_manager.load_setting(key))
cl_data._data_layer = db_manager
def load_custom_tools():
"""
Load custom tools from tools.py if it exists.
Resolution order:
1. PRAISONAI_TOOLS_PATH environment variable (file or directory)
2. ./tools.py in current working directory
3. No tools (silent, no warning)
"""
"""Load custom tools from tools.py if it exists."""
custom_tools = {}
importlib = _get_importlib()
inspect = _get_inspect()
# Determine tools path
tools_path = os.getenv("PRAISONAI_TOOLS_PATH")
if tools_path:
if not os.path.exists(tools_path):
logger.warning(f"PRAISONAI_TOOLS_PATH set but path does not exist: {tools_path}")
return custom_tools
else:
# Check current working directory for tools.py
cwd_tools = os.path.join(os.getcwd(), "tools.py")
if os.path.exists(cwd_tools):
tools_path = cwd_tools
else:
# No tools configured - this is fine, return silently
logger.debug("No tools.py found in current directory (this is normal)")
return custom_tools
@@ -97,27 +239,23 @@ def load_custom_tools():
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Load all functions from tools.py
for name, obj in inspect.getmembers(module):
if not name.startswith('_') and callable(obj) and not inspect.isclass(obj):
# Store function in globals for access
globals()[name] = obj
# Get function signature to build parameters
sig = inspect.signature(obj)
params_properties = {}
required_params = []
for param_name, param in sig.parameters.items():
if param_name != 'self': # Skip self parameter
# Get type annotation if available
param_type = "string" # Default type
if param.annotation != inspect.Parameter.empty:
if param.annotation == int:
if param_name != 'self':
param_type = "string"
if param.annotation is not inspect.Parameter.empty:
if param.annotation is int:
param_type = "integer"
elif param.annotation == float:
elif param.annotation is float:
param_type = "number"
elif param.annotation == bool:
elif param.annotation is bool:
param_type = "boolean"
params_properties[param_name] = {
@@ -125,11 +263,9 @@ def load_custom_tools():
"description": f"Parameter {param_name}"
}
# Add to required if no default value
if param.default == inspect.Parameter.empty:
if param.default is inspect.Parameter.empty:
required_params.append(param_name)
# Build tool definition
tool_def = {
"type": "function",
"function": {
@@ -149,20 +285,63 @@ def load_custom_tools():
if custom_tools:
logger.info(f"Loaded {len(custom_tools)} custom tools from {tools_path}")
except FileNotFoundError:
# File doesn't exist - this is fine, no warning needed
logger.debug(f"Tools file not found: {tools_path}")
except Exception as e:
logger.warning(f"Error loading custom tools from {tools_path}: {e}")
return custom_tools
# Load custom tools
custom_tools_dict = load_custom_tools()
# Deferred tool loading
_custom_tools_dict = None
_tavily_client = None
_tools_list = None
tavily_api_key = os.getenv("TAVILY_API_KEY")
tavily_client = TavilyClient(api_key=tavily_api_key) if tavily_api_key else None
def _get_custom_tools():
global _custom_tools_dict
if _custom_tools_dict is None:
_custom_tools_dict = load_custom_tools()
return _custom_tools_dict
def _get_tavily():
global _tavily_client
if _tavily_client is None:
tavily_api_key = os.getenv("TAVILY_API_KEY")
if tavily_api_key:
TavilyClient = _get_tavily_client()
if TavilyClient:
_tavily_client = TavilyClient(api_key=tavily_api_key)
return _tavily_client
def _get_tools_list():
"""Get the combined tools list (Tavily + custom tools)."""
global _tools_list
if _tools_list is None:
_tools_list = []
tavily_api_key = os.getenv("TAVILY_API_KEY")
if tavily_api_key:
_tools_list.append({
"type": "function",
"function": {
"name": "tavily_web_search",
"description": "Search the web using Tavily API and crawl the resulting URLs",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
})
custom_tools = _get_custom_tools()
_tools_list.extend(list(custom_tools.values()))
return _tools_list
async def tavily_web_search(query):
"""Search the web using Tavily API."""
json = _get_json()
tavily_client = _get_tavily()
if not tavily_client:
return json.dumps({
"query": query,
@@ -172,62 +351,39 @@ async def tavily_web_search(query):
response = tavily_client.search(query)
logger.debug(f"Tavily search response: {response}")
async with AsyncWebCrawler() as crawler:
results = []
for result in response.get('results', []):
url = result.get('url')
if url:
try:
crawl_result = await crawler.arun(url=url)
results.append({
"content": result.get('content'),
"url": url,
"full_content": crawl_result.markdown
})
except Exception as e:
logger.error(f"Error crawling {url}: {str(e)}")
results.append({
"content": result.get('content'),
"url": url,
"full_content": "Error: Unable to crawl this URL"
})
AsyncWebCrawler = _get_async_web_crawler()
if AsyncWebCrawler:
async with AsyncWebCrawler() as crawler:
results = []
for result in response.get('results', []):
url = result.get('url')
if url:
try:
crawl_result = await crawler.arun(url=url)
results.append({
"content": result.get('content'),
"url": url,
"full_content": crawl_result.markdown
})
except Exception as e:
logger.error(f"Error crawling {url}: {str(e)}")
results.append({
"content": result.get('content'),
"url": url,
"full_content": "Error: Unable to crawl this URL"
})
else:
results = [{"content": r.get('content'), "url": r.get('url')} for r in response.get('results', [])]
return json.dumps({
"query": query,
"results": results
})
# Build tools list with Tavily and custom tools
tools = []
# Add Tavily tool if API key is available
if tavily_api_key:
tools.append({
"type": "function",
"function": {
"name": "tavily_web_search",
"description": "Search the web using Tavily API and crawl the resulting URLs",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
})
# Add custom tools from tools.py
tools.extend(list(custom_tools_dict.values()))
# Authentication configuration
AUTH_PASSWORD_ENABLED = os.getenv("AUTH_PASSWORD_ENABLED", "true").lower() == "true" # Password authentication enabled by default
AUTH_OAUTH_ENABLED = os.getenv("AUTH_OAUTH_ENABLED", "false").lower() == "true" # OAuth authentication disabled by default
expected_username = os.getenv("CHAINLIT_USERNAME", "admin")
expected_password = os.getenv("CHAINLIT_PASSWORD", "admin")
# Warn if using default credentials
if expected_username == "admin" and expected_password == "admin":
logger.warning("⚠️ Using default admin credentials. Set CHAINLIT_USERNAME and CHAINLIT_PASSWORD environment variables for production.")
@@ -241,38 +397,122 @@ def auth_callback(username: str, password: str):
logger.warning(f"Login failed for user: {username}")
return None
def _get_or_create_agent(model_name: str, tools_enabled: bool = True):
"""Get or create a reusable agent for the session."""
Agent = _get_praisonai_agent()
if Agent is None:
return None
# Get cached agent from session
cached_agent = cl.user_session.get("_cached_agent")
cached_model = cl.user_session.get("_cached_agent_model")
# Reuse if model matches
if cached_agent is not None and cached_model == model_name:
return cached_agent
# Create new agent with interactive tools
_profile_start("create_agent")
tools = []
if tools_enabled:
tools = _get_interactive_tools()
# Add Tavily if available
if os.getenv("TAVILY_API_KEY"):
tools.append(tavily_web_search)
agent = Agent(
name="PraisonAI Assistant",
instructions="""You are a helpful AI assistant with access to powerful tools.
async def send_count():
await cl.Message(
f"Create step counter: {create_step_counter}", disable_feedback=True
).send()
Available capabilities:
- File operations (read, write, create, edit, delete files)
- Code intelligence (find symbols, definitions, references)
- Command execution (run shell commands)
- Web search (if Tavily API key is set)
Use tools when needed to help the user. For file modifications, use the ACP tools which provide safe, reviewable changes.
Always be helpful, accurate, and concise.""",
llm=model_name,
tools=tools if tools else None,
verbose=False,
)
# Cache the agent
cl.user_session.set("_cached_agent", agent)
cl.user_session.set("_cached_agent_model", model_name)
_profile_end("create_agent")
return agent
@cl.on_chat_start
async def start():
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-5-nano")
_profile_start("on_chat_start")
_ensure_env_loaded()
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
tools_enabled = (load_setting("tools_enabled") or "true").lower() == "true"
cl.user_session.set("model_name", model_name)
logger.debug(f"Model name: {model_name}")
cl.user_session.set("tools_enabled", tools_enabled)
logger.debug(f"Model name: {model_name}, Tools enabled: {tools_enabled}")
# Pre-create agent for faster first response
_get_or_create_agent(model_name, tools_enabled)
settings = cl.ChatSettings(
[
TextInput(
id="model_name",
label="Enter the Model Name",
placeholder="e.g., gpt-5-nano",
placeholder="e.g., gpt-4o-mini",
initial=model_name
),
Switch(
id="tools_enabled",
label="Enable Tools (ACP, LSP, Web Search)",
initial=tools_enabled
)
]
)
cl.user_session.set("settings", settings)
await settings.send()
# Show loaded tools info
tools = _get_interactive_tools()
tool_names = [t.__name__ for t in tools[:5]]
if len(tools) > 5:
tool_names.append(f"... and {len(tools) - 5} more")
await cl.Message(
content=f"🚀 **PraisonAI Chat Ready**\n\n"
f"**Model:** {model_name}\n"
f"**Tools:** {len(tools)} loaded ({', '.join(tool_names)})\n"
f"**Trust Mode:** Enabled (auto-approve tool executions)\n\n"
f"Type your message to get started!"
).send()
_profile_end("on_chat_start")
@cl.on_settings_update
async def setup_agent(settings):
json = _get_json()
logger.debug(settings)
cl.user_session.set("settings", settings)
model_name = settings["model_name"]
tools_enabled = settings.get("tools_enabled", True)
cl.user_session.set("model_name", model_name)
cl.user_session.set("tools_enabled", tools_enabled)
# Invalidate cached agent if model changed
cached_model = cl.user_session.get("_cached_agent_model")
if cached_model != model_name:
cl.user_session.set("_cached_agent", None)
cl.user_session.set("_cached_agent_model", None)
save_setting("model_name", model_name)
save_setting("tools_enabled", str(tools_enabled).lower())
thread_id = cl.user_session.get("thread_id")
if thread_id:
@@ -285,17 +525,26 @@ async def setup_agent(settings):
except json.JSONDecodeError:
metadata = {}
metadata["model_name"] = model_name
metadata["tools_enabled"] = tools_enabled
await cl_data.update_thread(thread_id, metadata=metadata)
cl.user_session.set("metadata", metadata)
@cl.on_message
async def main(message: cl.Message):
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-5-nano")
_profile_start("on_message_total")
json = _get_json()
asyncio = _get_asyncio()
datetime = _get_datetime()
model_name = cl.user_session.get("model_name") or load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
tools_enabled = cl.user_session.get("tools_enabled", True)
message_history = cl.user_session.get("message_history", [])
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Handle image uploads
image = None
if message.elements and isinstance(message.elements[0], cl.Image):
Image = _get_pil_image()
image_element = message.elements[0]
try:
image = Image.open(image_element.path)
@@ -306,8 +555,7 @@ async def main(message: cl.Message):
await cl.Message(content="Error processing the image. Please try again.").send()
return
user_message = f"""
Answer the question and use tools if needed:
user_message = f"""Answer the question and use tools if needed:
Current Date and Time: {now}
@@ -320,6 +568,58 @@ User Question: {message.content}
message_history.append({"role": "user", "content": user_message})
msg = cl.Message(content="")
# Try PraisonAI Agent first (faster, with tool reuse)
agent = _get_or_create_agent(model_name, tools_enabled) if tools_enabled else None
if agent is not None:
_profile_start("agent_response")
await msg.send()
try:
# Use async chat for streaming
result = await agent.achat(message.content)
# Get response text
if hasattr(result, 'raw'):
response_text = result.raw
else:
response_text = str(result)
# Stream in word chunks for better UX
words = response_text.split(' ')
for i, word in enumerate(words):
token = word + (' ' if i < len(words) - 1 else '')
await msg.stream_token(token)
msg.content = response_text
await msg.update()
message_history.append({"role": "assistant", "content": response_text})
cl.user_session.set("message_history", message_history)
except Exception as e:
logger.error(f"Agent error: {e}")
# Fallback to litellm
await _handle_with_litellm(message, user_message, model_name, message_history, msg, image)
_profile_end("agent_response")
else:
# Fallback to litellm
await _handle_with_litellm(message, user_message, model_name, message_history, msg, image)
_profile_end("on_message_total")
async def _handle_with_litellm(message, user_message, model_name, message_history, msg, image):
"""Fallback handler using litellm for backward compatibility."""
json = _get_json()
asyncio = _get_asyncio()
io = _get_io()
base64 = _get_base64()
acompletion = _get_acompletion()
tools = _get_tools_list()
_profile_start("litellm_response")
completion_params = {
"model": model_name,
"messages": message_history,
@@ -338,7 +638,6 @@ User Question: {message.content}
]
}
# Pass tools if we have any (Tavily or custom)
if tools:
completion_params["tools"] = tools
completion_params["tool_choice"] = "auto"
@@ -356,7 +655,6 @@ User Question: {message.content}
if 'content' in delta and delta['content'] is not None:
token = delta['content']
# Send message on first token to avoid delay
if not msg_sent:
await msg.send()
msg_sent = True
@@ -386,28 +684,25 @@ User Question: {message.content}
if current_tool_call:
tool_calls.append(current_tool_call)
# Ensure message is sent even if no content (e.g., tool calls only)
if not msg_sent:
await msg.send()
logger.debug(f"Full response: {full_response}")
logger.debug(f"Tool calls: {tool_calls}")
message_history.append({"role": "assistant", "content": full_response})
logger.debug(f"Message history: {message_history}")
cl.user_session.set("message_history", message_history)
await msg.update()
if tool_calls and tools: # Check if we have any tools and tool calls
# Handle tool calls
if tool_calls and tools:
custom_tools = _get_custom_tools()
available_functions = {}
# Add Tavily function if available
if tavily_api_key:
if os.getenv("TAVILY_API_KEY"):
available_functions["tavily_web_search"] = tavily_web_search
# Add all custom tool functions from globals
for tool_name in custom_tools_dict:
for tool_name in custom_tools:
if tool_name in globals():
available_functions[tool_name] = globals()[tool_name]
messages = message_history + [{"role": "assistant", "content": None, "function_call": {
"name": tool_calls[0]['function']['name'],
"arguments": tool_calls[0]['function']['arguments']
@@ -422,49 +717,37 @@ User Question: {message.content}
try:
function_args = json.loads(function_args)
# Call function based on whether it's async or sync
if asyncio.iscoroutinefunction(function_to_call):
# For async functions like tavily_web_search
if function_name == "tavily_web_search":
function_response = await function_to_call(
query=function_args.get("query"),
)
function_response = await function_to_call(query=function_args.get("query"))
else:
# For custom async functions, pass all arguments
function_response = await function_to_call(**function_args)
else:
# For sync functions (most custom tools)
function_response = function_to_call(**function_args)
# Convert response to string if needed
if not isinstance(function_response, str):
function_response = json.dumps(function_response)
messages.append(
{
"role": "function",
"name": function_name,
"content": function_response,
}
)
messages.append({
"role": "function",
"name": function_name,
"content": function_response,
})
except json.JSONDecodeError:
logger.error(f"Failed to parse function arguments: {function_args}")
except Exception as e:
logger.error(f"Error calling function {function_name}: {str(e)}")
messages.append(
{
"role": "function",
"name": function_name,
"content": f"Error: {str(e)}",
}
)
messages.append({
"role": "function",
"name": function_name,
"content": f"Error: {str(e)}",
})
second_response = await acompletion(
model=model_name,
stream=True,
messages=messages,
)
logger.debug(f"Second LLM response: {second_response}")
full_response = ""
async for part in second_response:
@@ -480,25 +763,41 @@ User Question: {message.content}
else:
msg.content = full_response
await msg.update()
_profile_end("litellm_response")
@cl.on_chat_resume
async def on_chat_resume(thread: ThreadDict):
json = _get_json()
io = _get_io()
base64 = _get_base64()
Image = _get_pil_image()
logger.info(f"Resuming chat: {thread['id']}")
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-5-nano")
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
tools_enabled = (load_setting("tools_enabled") or "true").lower() == "true"
logger.debug(f"Model name: {model_name}")
settings = cl.ChatSettings(
[
TextInput(
id="model_name",
label="Enter the Model Name",
placeholder="e.g., gpt-5-nano",
placeholder="e.g., gpt-4o-mini",
initial=model_name
),
Switch(
id="tools_enabled",
label="Enable Tools (ACP, LSP, Web Search)",
initial=tools_enabled
)
]
)
await settings.send()
thread_id = thread["id"]
cl.user_session.set("thread_id", thread_id)
cl.user_session.set("thread_id", thread["id"])
cl.user_session.set("model_name", model_name)
cl.user_session.set("tools_enabled", tools_enabled)
metadata = thread.get("metadata", {})
if isinstance(metadata, str):
@@ -524,6 +823,9 @@ async def on_chat_resume(thread: ThreadDict):
logger.warning(f"Message without recognized type: {m}")
cl.user_session.set("message_history", message_history)
# Pre-create agent for faster first response
_get_or_create_agent(model_name, tools_enabled)
image_data = metadata.get("image")
if image_data:
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.0.7"
__version__ = "3.1.2"