mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 11:41:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5d4db4262 | ||
|
|
87d691c616 | ||
|
|
8ea15dee0f | ||
|
|
4f56842e08 | ||
|
|
a24836aaee |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.0.7" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.0.7" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.0.7" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
+638
-539
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
__version__ = "3.0.7"
|
||||
__version__ = "3.1.2"
|
||||
Reference in new issue
Block a user