diff --git a/src/praisonai/praisonai/chainlit_ui.py b/src/praisonai/praisonai/chainlit_ui.py deleted file mode 100644 index 8e7ce6294..000000000 --- a/src/praisonai/praisonai/chainlit_ui.py +++ /dev/null @@ -1,304 +0,0 @@ -# praisonai/chainlit_ui.py -from praisonai.agents_generator import AgentsGenerator -from praisonai.auto import AutoGenerator -import chainlit as cl -import os -from chainlit.types import ThreadDict -from chainlit.input_widget import Select, TextInput -from typing import Optional -from dotenv import load_dotenv -load_dotenv() -from contextlib import redirect_stdout -from io import StringIO -import logging -logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper(), format='%(asctime)s - %(levelname)s - %(message)s') - -framework = "crewai" -config_list = [ - { - 'model': os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini"), - 'base_url': os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1"), - 'api_key': os.environ.get("OPENAI_API_KEY", "") - } - ] -agent_file = "test.yaml" - -actions=[ - cl.Action(name="run", payload={"value": "run"}, label="run"), - cl.Action(name="modify", payload={"value": "modify"}, label="Modify"), -] - -@cl.action_callback("run") -async def on_run(action): - await main(cl.Message(content="")) - -@cl.action_callback("modify") -async def on_modify(action): - await cl.Message(content="Modify the agents and tools from below settings").send() - - -@cl.set_chat_profiles -async def set_profiles(current_user: cl.User): - return [ - cl.ChatProfile( - name="Auto", - markdown_description="Automatically generate agents and tasks based on your input.", - starters=[ - cl.Starter( - label="Create a movie script", - message="Create a movie script about a futuristic society where AI and humans coexist, focusing on the conflict and resolution between them. Start with an intriguing opening scene.", - icon="/public/movie.svg", - ), - cl.Starter( - label="Design a fantasy world", - message="Design a detailed fantasy world with unique geography, cultures, and magical systems. Start by describing the main continent and its inhabitants.", - icon="/public/fantasy.svg", - ), - cl.Starter( - label="Write a futuristic political thriller", - message="Write a futuristic political thriller involving a conspiracy within a global government. Start with a high-stakes meeting that sets the plot in motion.", - icon="/public/thriller.svg", - ), - cl.Starter( - label="Develop a new board game", - message="Develop a new, innovative board game. Describe the game's objective, rules, and unique mechanics. Create a scenario to illustrate gameplay.", - icon="/public/game.svg", - ), - ] - ), - cl.ChatProfile( - name="Manual", - markdown_description="Manually define your agents and tasks using a YAML file.", - ), - ] - - -@cl.on_chat_start -async def start_chat(): - cl.user_session.set( - "message_history", - [{"role": "system", "content": "You are a helpful assistant."}], - ) - - # Create tools.py if it doesn't exist - if not os.path.exists("tools.py"): - with open("tools.py", "w") as f: - f.write("# Add your custom tools here\n") - - settings = await cl.ChatSettings( - [ - TextInput(id="Model", label="OpenAI - Model", initial=config_list[0]['model']), - TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']), - TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']), - Select( - id="Framework", - label="Framework", - values=["crewai", "autogen"], - initial_index=0, - ), - ] - ).send() - cl.user_session.set("settings", settings) - chat_profile = cl.user_session.get("chat_profile") - if chat_profile=="Manual": - - agent_file = "agents.yaml" - full_agent_file_path = os.path.abspath(agent_file) # Get full path - if os.path.exists(full_agent_file_path): - with open(full_agent_file_path, 'r') as f: - yaml_content = f.read() - msg = cl.Message(content=yaml_content, language="yaml") - await msg.send() - - - full_tools_file_path = os.path.abspath("tools.py") # Get full path - if os.path.exists(full_tools_file_path): - with open(full_tools_file_path, 'r') as f: - tools_content = f.read() - msg = cl.Message(content=tools_content, language="python") - await msg.send() - - settings = await cl.ChatSettings( - [ - TextInput(id="Model", label="OpenAI - Model", initial=config_list[0]['model']), - TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']), - TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']), - Select( - id="Framework", - label="Framework", - values=["crewai", "autogen"], - initial_index=0, - ), - TextInput(id="agents", label="agents.yaml", initial=yaml_content, multiline=True), - TextInput(id="tools", label="tools.py", initial=tools_content, multiline=True), - ] - ).send() - cl.user_session.set("settings", settings) - - res = await cl.AskActionMessage( - content="Pick an action!", - actions=actions, - ).send() - if res and res.get("value") == "modify": - await cl.Message(content="Modify the agents and tools from below settings", actions=actions).send() - elif res and res.get("value") == "run": - await main(cl.Message(content="", actions=actions)) - - await on_settings_update(settings) - -@cl.on_settings_update -async def on_settings_update(settings): - """Handle updates to the ChatSettings form.""" - global config_list, framework - config_list[0]['model'] = settings["Model"] - config_list[0]['base_url'] = settings["BaseUrl"] - config_list[0]['api_key'] = settings["ApiKey"] - os.environ["OPENAI_API_KEY"] = config_list[0]['api_key'] - os.environ["OPENAI_MODEL_NAME"] = config_list[0]['model'] - os.environ["OPENAI_API_BASE"] = config_list[0]['base_url'] - framework = settings["Framework"] - - if "agents" in settings: - with open("agents.yaml", "w") as f: - f.write(settings["agents"]) - if "tools" in settings: - with open("tools.py", "w") as f: - f.write(settings["tools"]) - - print("Settings updated") - -@cl.on_chat_resume -async def on_chat_resume(thread: ThreadDict): - message_history = cl.user_session.get("message_history", []) - root_messages = [m for m in thread["steps"] if m["parentId"] is None] - for message in root_messages: - if message["type"] == "user_message": - message_history.append({"role": "user", "content": message["output"]}) - elif message["type"] == "ai_message": - message_history.append({"role": "assistant", "content": message["content"]}) - cl.user_session.set("message_history", message_history) - -# @cl.step(type="tool") -# async def tool(data: Optional[str] = None, language: Optional[str] = None): -# return cl.Message(content=data, language=language) - -@cl.step(type="tool", show_input=False) -async def run_agents(agent_file: str, framework: str): - """Runs the agents and returns the result.""" - agents_generator = AgentsGenerator(agent_file, framework, config_list) - current_step = cl.context.current_step - print("Current Step:", current_step) - - stdout_buffer = StringIO() - with redirect_stdout(stdout_buffer): - result = agents_generator.generate_crew_and_kickoff() - - complete_output = stdout_buffer.getvalue() - - async with cl.Step(name="gpt4", type="llm", show_input=True) as step: - step.input = "" - - for line in stdout_buffer.getvalue().splitlines(): - print(line) - await step.stream_token(line) - - tool_res = await output(complete_output) - - yield result - -@cl.step(type="tool", show_input=False, language="yaml") -async def output(output): - return output - -@cl.step(type="tool", show_input=False, language="yaml") -def agent(output): - return(f""" - Agent Step Completed! - Output: {output} - """) - -@cl.step(type="tool", show_input=False, language="yaml") -def task(output): - return(f""" - Task Completed! - Task: {output.description} - Output: {output.raw_output} - {output} - """) - -@cl.on_message -async def main(message: cl.Message): - """Run PraisonAI with the provided message as the topic.""" - message_history = cl.user_session.get("message_history") - if message_history is None: - message_history = [] - cl.user_session.set("message_history", message_history) - message_history.append({"role": "user", "content": message.content}) - topic = message.content - chat_profile = cl.user_session.get("chat_profile") - - if chat_profile == "Auto": - agent_file = "agents.yaml" - generator = AutoGenerator(topic=topic, agent_file=agent_file, framework=framework, config_list=config_list) - await cl.sleep(2) - agent_file = generator.generate() - agents_generator = AgentsGenerator( - agent_file, - framework, - config_list, - # agent_callback=agent, - # task_callback=task - ) - # Capture stdout - stdout_buffer = StringIO() - with redirect_stdout(stdout_buffer): - result = agents_generator.generate_crew_and_kickoff() - - complete_output = stdout_buffer.getvalue() - tool_res = await output(complete_output) - msg = cl.Message(content=result) - await msg.send() - message_history.append({"role": "assistant", "content": message.content}) - else: # chat_profile == "Manual" - agent_file = "agents.yaml" - full_agent_file_path = os.path.abspath(agent_file) # Get full path - full_tools_file_path = os.path.abspath("tools.py") - if os.path.exists(full_agent_file_path): - with open(full_agent_file_path, 'r') as f: - yaml_content = f.read() - # tool_res = await tool() - msg_agents = cl.Message(content=yaml_content, language="yaml") - await msg_agents.send() - if os.path.exists(full_tools_file_path): - with open(full_tools_file_path, 'r') as f: - tools_content = f.read() - msg_tools = cl.Message(content=tools_content, language="python") - await msg_tools.send() - else: - # If the file doesn't exist, follow the same process as "Auto" - generator = AutoGenerator(topic=topic, agent_file=agent_file, framework=framework, config_list=config_list) - agent_file = generator.generate() - - agents_generator = AgentsGenerator(agent_file, framework, config_list) - result = agents_generator.generate_crew_and_kickoff() - msg = cl.Message(content=result, actions=actions) - await msg.send() - message_history.append({"role": "assistant", "content": message.content}) - -# Load environment variables from .env file -load_dotenv() - -# Get username and password from environment variables -username = os.getenv("CHAINLIT_USERNAME", "admin") # Default to "admin" if not found -password = os.getenv("CHAINLIT_PASSWORD", "admin") # Default to "admin" if not found - -@cl.password_auth_callback -def auth_callback(username: str, password: str): - # Fetch the user matching username from your database - # and compare the hashed password with the value stored in the database - if (username, password) == (username, password): - return cl.User( - identifier=username, metadata={"role": "ADMIN", "provider": "credentials"} - ) - else: - return None diff --git a/src/praisonai/praisonai/chat/__init__.py b/src/praisonai/praisonai/chat/__init__.py deleted file mode 100644 index f6fdd2b95..000000000 --- a/src/praisonai/praisonai/chat/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -PraisonAI Chat Module - -This module provides the chat UI integration for PraisonAI agents. -It uses the PraisonAI Chat (based on Chainlit) for the frontend. -""" - -from typing import Optional, Any - -__all__ = ["start_chat_server", "ChatConfig"] - - -class ChatConfig: - """Configuration for the PraisonAI Chat server.""" - - def __init__( - self, - host: str = "0.0.0.0", - port: int = 8000, - debug: bool = False, - auth_enabled: bool = False, - session_id: Optional[str] = None, - ): - self.host = host - self.port = port - self.debug = debug - self.auth_enabled = auth_enabled - self.session_id = session_id - - -def start_chat_server( - agent: Optional[Any] = None, - agents: Optional[list] = None, - config: Optional[ChatConfig] = None, - port: int = 8000, - host: str = "0.0.0.0", - debug: bool = False, -) -> None: - """ - Start the PraisonAI Chat server. - - Args: - agent: A single PraisonAI agent to use in the chat. - agents: A list of PraisonAI agents for multi-agent chat. - config: ChatConfig object with server settings. - port: Port to run the server on (default: 8000). - host: Host to bind to (default: 0.0.0.0). - debug: Enable debug mode (default: False). - - Example: - >>> from praisonaiagents import Agent - >>> from praisonai.chat import start_chat_server - >>> - >>> agent = Agent(name="Assistant", instructions="You are helpful.") - >>> start_chat_server(agent=agent, port=8000) - """ - # Lazy import to avoid loading chainlit unless needed - try: - from chainlit.cli import run_chainlit - except ImportError: - raise ImportError( - "PraisonAI Chat requires the 'praisonai-chat' package. " - "Install it with: pip install praisonai-chat" - ) - - if config is None: - config = ChatConfig(host=host, port=port, debug=debug) - - # Store agents in a way accessible to the chainlit app - import os - os.environ["PRAISONAI_CHAT_MODE"] = "true" - - if agent is not None: - # Single agent mode - _register_agent(agent) - elif agents is not None: - # Multi-agent mode - for a in agents: - _register_agent(a) - - # Start the chainlit server - from pathlib import Path - - # Use the built-in app file - app_file = Path(__file__).parent / "app.py" - - # Set environment variables for chainlit - os.environ["CHAINLIT_HOST"] = config.host - os.environ["CHAINLIT_PORT"] = str(config.port) - - run_chainlit(str(app_file)) - - -# Global registry for agents -_REGISTERED_AGENTS: dict = {} - - -def _register_agent(agent: Any) -> None: - """Register an agent for use in the chat UI.""" - agent_name = getattr(agent, "name", None) or getattr(agent, "role", "Agent") - _REGISTERED_AGENTS[agent_name] = agent - - -def get_registered_agents() -> dict: - """Get all registered agents.""" - return _REGISTERED_AGENTS diff --git a/src/praisonai/praisonai/chat/app.py b/src/praisonai/praisonai/chat/app.py deleted file mode 100644 index be196aa9f..000000000 --- a/src/praisonai/praisonai/chat/app.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -PraisonAI Chat - Default Chainlit Application - -This is the default chat application that runs when using `praisonai chat`. -It integrates with PraisonAI agents for multi-agent conversations. -""" - -import os -import chainlit as cl - -# Check if we're in PraisonAI Chat mode with registered agents -PRAISONAI_CHAT_MODE = os.environ.get("PRAISONAI_CHAT_MODE", "false").lower() == "true" - - -def get_agents(): - """Get registered agents from the chat module.""" - try: - from praisonai.chat import get_registered_agents - return get_registered_agents() - except ImportError: - return {} - - -@cl.on_chat_start -async def on_chat_start(): - """Initialize the chat session.""" - agents = get_agents() - - if agents: - agent_names = list(agents.keys()) - cl.user_session.set("agents", agents) - cl.user_session.set("current_agent", agent_names[0] if agent_names else None) - - await cl.Message( - content=f"Welcome to PraisonAI Chat! Available agents: {', '.join(agent_names)}" - ).send() - else: - await cl.Message( - content="Welcome to PraisonAI Chat! No agents configured. " - "Use the API to register agents or configure via YAML." - ).send() - - cl.user_session.set("message_history", []) - - -@cl.on_message -async def on_message(message: cl.Message): - """Handle incoming messages.""" - agents = cl.user_session.get("agents", {}) - current_agent_name = cl.user_session.get("current_agent") - message_history = cl.user_session.get("message_history", []) - - # Add user message to history - message_history.append({"role": "user", "content": message.content}) - - if not agents or not current_agent_name: - # No agents configured - use a simple echo response - response = f"Echo: {message.content}\n\n(No agents configured. Register agents to enable AI responses.)" - await cl.Message(content=response).send() - return - - agent = agents.get(current_agent_name) - if agent is None: - await cl.Message(content=f"Agent '{current_agent_name}' not found.").send() - return - - # Create a step for the agent response - async with cl.Step(name=current_agent_name, type="llm") as step: - step.input = message.content - - try: - # Try to call the agent - if hasattr(agent, "chat"): - # PraisonAI Agent with chat method - response = await _call_agent_async(agent, message.content) - elif hasattr(agent, "run"): - # Agent with run method - response = await _call_agent_async(agent, message.content, method="run") - elif callable(agent): - # Callable agent - response = agent(message.content) - else: - response = f"Agent '{current_agent_name}' does not have a callable interface." - - step.output = str(response) - - except Exception as e: - response = f"Error calling agent: {str(e)}" - step.output = response - - # Add assistant response to history - message_history.append({"role": "assistant", "content": str(response)}) - cl.user_session.set("message_history", message_history) - - # Send the response - await cl.Message(content=str(response)).send() - - -async def _call_agent_async(agent, message: str, method: str = "chat"): - """Call an agent method, handling both sync and async.""" - import asyncio - - func = getattr(agent, method) - - if asyncio.iscoroutinefunction(func): - return await func(message) - else: - # Run sync function in thread pool - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, func, message) - - -@cl.on_chat_resume -async def on_chat_resume(thread): - """Resume a chat session.""" - message_history = [] - - root_messages = [m for m in thread.get("steps", []) if m.get("parentId") is None] - for msg in root_messages: - if msg.get("type") == "user_message": - message_history.append({"role": "user", "content": msg.get("output", "")}) - elif msg.get("type") == "assistant_message": - message_history.append({"role": "assistant", "content": msg.get("output", "")}) - - cl.user_session.set("message_history", message_history) diff --git a/src/praisonai/praisonai/cli/commands/realtime.py b/src/praisonai/praisonai/cli/commands/realtime.py index a2e52294e..a9457066a 100644 --- a/src/praisonai/praisonai/cli/commands/realtime.py +++ b/src/praisonai/praisonai/cli/commands/realtime.py @@ -27,22 +27,26 @@ def realtime_main( praisonai realtime praisonai realtime --model gpt-4o --port 9000 """ - # Route to new UI realtime subcommand - from praisonai.cli.commands.ui import _launch_aiui_app import os if model: os.environ["MODEL_NAME"] = model print("šŸŽ¤ Launching PraisonAI Realtime Voice Interface...") - print("Note: Migrated from Chainlit to aiui. Full WebRTC voice coming soon.") - - _launch_aiui_app( - app_dir="ui_realtime", - default_app_name="ui_realtime", - port=port, - host="0.0.0.0", - app_file=None, - reload=False, - ui_name="Realtime Voice" - ) + print("Note: Migrated from Chainlit to aiui.") + + try: + from praisonai.cli.commands.ui import _launch_aiui_app + _launch_aiui_app( + app_dir="ui_realtime", + default_app_name="ui_realtime", + port=port, + host="127.0.0.1", + app_file=None, + reload=False, + ui_name="Realtime Voice" + ) + except ImportError: + print('\033[91mERROR: Realtime UI is not installed.\033[0m') + print('Install with: pip install "praisonai[ui]"') + raise typer.Exit(1) diff --git a/src/praisonai/praisonai/cli/commands/serve.py b/src/praisonai/praisonai/cli/commands/serve.py index 230f31b42..883291328 100644 --- a/src/praisonai/praisonai/cli/commands/serve.py +++ b/src/praisonai/praisonai/cli/commands/serve.py @@ -10,7 +10,7 @@ - mcp: MCP server (Model Context Protocol) - acp: ACP server (Agent Client Protocol for IDEs) - lsp: Language Server Protocol -- ui: Web UI (Chainlit) +- ui: Web UI (aiui) - rag: RAG query server - registry: Package registry server - docs: Documentation preview server @@ -116,7 +116,7 @@ def serve_callback(ctx: typer.Context): [green]mcp[/green] MCP server for Claude/Cursor (port 8080) [green]acp[/green] Agent Client Protocol for IDEs (STDIO) [green]lsp[/green] Language Server Protocol (STDIO) - [green]ui[/green] Chainlit web interface (port 8082) + [green]ui[/green] aiui web interface (port 8082) [green]rag[/green] RAG query server (port 9000) [green]registry[/green] Package registry server (port 7777) [green]docs[/green] Documentation preview (port 3000) @@ -351,7 +351,7 @@ def serve_ui( port: int = typer.Option(8082, "--port", "-p", help="Port to bind to"), ui_type: str = typer.Option("agents", "--type", "-t", help="UI type: agents, chat, code, realtime"), ): - """Start Chainlit web UI server. + """Start aiui web UI server. Examples: praisonai serve ui @@ -361,8 +361,8 @@ def serve_ui( output = get_output_controller() try: - from .ui import _launch_chainlit_ui - _launch_chainlit_ui(ui_type, port, host, False) + from .ui import _launch_aiui_app + _launch_aiui_app(f"ui_{ui_type}", f"ui_{ui_type}", port, host, None, False, ui_type) except ImportError as e: output.print_error(f"UI module not available: {e}") output.print("Install with: pip install praisonai[ui]") diff --git a/src/praisonai/praisonai/cli/commands/ui.py b/src/praisonai/praisonai/cli/commands/ui.py index ff716f0fe..2433f46d1 100644 --- a/src/praisonai/praisonai/cli/commands/ui.py +++ b/src/praisonai/praisonai/cli/commands/ui.py @@ -221,9 +221,8 @@ def realtime( reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload"), ): """ - Launch Realtime Voice Interface (Beta). + Launch Realtime Voice Interface. - Replaces the old Chainlit realtime interface with aiui. - Note: Full WebRTC voice is pending PraisonAIUI implementation. + Uses aiui's OpenAIRealtimeManager for WebRTC voice conversations. """ _launch_aiui_app("ui_realtime", "ui_realtime", port, host, app_file, reload, "Realtime Voice") diff --git a/src/praisonai/praisonai/cli/features/doctor/checks/env_checks.py b/src/praisonai/praisonai/cli/features/doctor/checks/env_checks.py index 6e043da7f..4a6c4fa2d 100644 --- a/src/praisonai/praisonai/cli/features/doctor/checks/env_checks.py +++ b/src/praisonai/praisonai/cli/features/doctor/checks/env_checks.py @@ -475,7 +475,7 @@ def check_optional_deps(config: DoctorConfig) -> CheckResult: ("chromadb", "Knowledge/RAG features"), ("mem0ai", "Memory features"), ("litellm", "Multi-provider LLM support"), - ("chainlit", "Chat UI"), + ("praisonaiui", "aiui (Chat/Dashboard UI)"), ("gradio", "Gradio UI"), ("crawl4ai", "Web crawling"), ("tavily", "Tavily search"), diff --git a/src/praisonai/praisonai/cli/main.py b/src/praisonai/praisonai/cli/main.py index 802861b25..864acecfa 100644 --- a/src/praisonai/praisonai/cli/main.py +++ b/src/praisonai/praisonai/cli/main.py @@ -141,33 +141,12 @@ def _get_agents_generator(): return AgentsGenerator # Optional module imports with availability checks -CHAINLIT_AVAILABLE = False GRADIO_AVAILABLE = False CALL_MODULE_AVAILABLE = False CREWAI_AVAILABLE = False AUTOGEN_AVAILABLE = False PRAISONAI_AVAILABLE = False TRAIN_AVAILABLE = False -try: - import importlib.util - CHAINLIT_AVAILABLE = importlib.util.find_spec("chainlit") is not None -except ImportError: - pass - -def _get_chainlit_run(): - """Lazy import chainlit to avoid loading .env at startup""" - # Create necessary directories and set CHAINLIT_APP_ROOT - if "CHAINLIT_APP_ROOT" not in os.environ: - chainlit_root = os.path.join(os.path.expanduser("~"), ".praison") - os.environ["CHAINLIT_APP_ROOT"] = chainlit_root - else: - chainlit_root = os.environ["CHAINLIT_APP_ROOT"] - - os.makedirs(chainlit_root, exist_ok=True) - os.makedirs(os.path.join(chainlit_root, ".files"), exist_ok=True) - - from chainlit.cli import chainlit_run - return chainlit_run # Use find_spec for fast availability checks (no actual import) import importlib.util @@ -578,10 +557,15 @@ def __init__(self): return # chat and code commands are now terminal-native (handled by Typer commands) - # They no longer open Chainlit browser UI if getattr(args, 'realtime', False): - self.create_realtime_interface() + try: + from praisonai.cli.commands.ui import _launch_aiui_app + _launch_aiui_app("ui_realtime", "ui_realtime", 8085, "127.0.0.1", None, False, "Realtime Voice") + except ImportError: + print("\033[91mERROR: Realtime UI is not installed.\033[0m") + print('Install with: pip install "praisonai[ui]"') + sys.exit(1) return if getattr(args, 'call', False): @@ -1148,7 +1132,7 @@ def parse_args(self): # UI command — routes to Typer CLI for clean chat UI (praisonaiui) pass # chat and code commands are now terminal-native (handled by Typer commands) - # They no longer set args.ui = 'chainlit' or open browser + # Legacy --ui handling is preserved via the deprecation path above # Handle --claudecode flag for code command if getattr(args, 'claudecode', False): @@ -1238,11 +1222,13 @@ def parse_args(self): sys.exit(0) elif args.command == 'realtime': - if not CHAINLIT_AVAILABLE: - print("[red]ERROR: Realtime UI is not installed. Install with:[/red]") - print("\npip install \"praisonai[realtime]\"\n") + try: + from praisonai.cli.commands.ui import _launch_aiui_app + _launch_aiui_app("ui_realtime", "ui_realtime", 8085, "127.0.0.1", None, False, "Realtime Voice") + except ImportError: + print("\033[91mERROR: Realtime UI is not installed.\033[0m") + print('Install with: pip install "praisonai[ui]"') sys.exit(1) - self.create_realtime_interface() sys.exit(0) elif args.command == 'train': @@ -5189,45 +5175,6 @@ def _handle_serve_command(self, args, unknown_args): except KeyboardInterrupt: print("\nšŸ‘‹ Server stopped.") - def create_chainlit_chat_interface(self): - """ - Create a Chainlit interface for the chat application. - """ - if CHAINLIT_AVAILABLE: - import praisonai - os.environ["CHAINLIT_PORT"] = "8084" - root_path = os.path.join(os.path.expanduser("~"), ".praison") - if "CHAINLIT_APP_ROOT" not in os.environ: - os.environ["CHAINLIT_APP_ROOT"] = root_path - chat_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'chat.py') - _get_chainlit_run()([chat_ui_path]) - else: - print("ERROR: Chat UI is not installed. Please install it with 'pip install \"praisonai[chat]\"' to use the chat UI.") - - def create_code_interface(self): - """ - Create a Chainlit interface for the code application. - """ - if CHAINLIT_AVAILABLE: - import praisonai - os.environ["CHAINLIT_PORT"] = "8086" - root_path = os.path.join(os.path.expanduser("~"), ".praison") - if "CHAINLIT_APP_ROOT" not in os.environ: - os.environ["CHAINLIT_APP_ROOT"] = root_path - public_folder = os.path.join(os.path.dirname(__file__), 'public') - if not os.path.exists(os.path.join(root_path, "public")): - if os.path.exists(public_folder): - shutil.copytree(public_folder, os.path.join(root_path, "public"), dirs_exist_ok=True) - logging.info("Public folder copied successfully!") - else: - logging.info("Public folder not found in the package.") - else: - logging.info("Public folder already exists.") - code_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'code.py') - _get_chainlit_run()([code_ui_path]) - else: - print("ERROR: Code UI is not installed. Please install it with 'pip install \"praisonai[code]\"' to use the code UI.") - def create_gradio_interface(self): """ Create a Gradio interface for generating agents and performing tasks. @@ -5260,51 +5207,6 @@ def generate_crew_and_kickoff_interface(auto_args, framework): else: print("ERROR: Gradio is not installed. Please install it with 'pip install gradio' to use this feature.") - def create_chainlit_interface(self): - """ - Create a Chainlit interface for generating agents and performing tasks. - """ - if CHAINLIT_AVAILABLE: - import praisonai - os.environ["CHAINLIT_PORT"] = "8082" - public_folder = os.path.join(os.path.dirname(praisonai.__file__), 'public') - if not os.path.exists("public"): - if os.path.exists(public_folder): - shutil.copytree(public_folder, 'public', dirs_exist_ok=True) - logging.info("Public folder copied successfully!") - else: - logging.info("Public folder not found in the package.") - else: - logging.info("Public folder already exists.") - chainlit_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'agents.py') - _get_chainlit_run()([chainlit_ui_path]) - else: - print("ERROR: Chainlit is not installed. Please install it with 'pip install \"praisonai[ui]\"' to use the UI.") - - def create_realtime_interface(self): - """ - Create a Chainlit interface for the realtime voice interaction application. - """ - if CHAINLIT_AVAILABLE: - import praisonai - os.environ["CHAINLIT_PORT"] = "8088" - root_path = os.path.join(os.path.expanduser("~"), ".praison") - if "CHAINLIT_APP_ROOT" not in os.environ: - os.environ["CHAINLIT_APP_ROOT"] = root_path - public_folder = os.path.join(os.path.dirname(praisonai.__file__), 'public') - if not os.path.exists(os.path.join(root_path, "public")): - if os.path.exists(public_folder): - shutil.copytree(public_folder, os.path.join(root_path, "public"), dirs_exist_ok=True) - logging.info("Public folder copied successfully!") - else: - logging.info("Public folder not found in the package.") - else: - logging.info("Public folder already exists.") - realtime_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'realtime.py') - _get_chainlit_run()([realtime_ui_path]) - else: - print("ERROR: Realtime UI is not installed. Please install it with 'pip install \"praisonai[realtime]\"' to use the realtime UI.") - def create_aiui_agents_interface(self): """ Create an aiui-based agents interface (replaces Chainlit). @@ -5318,7 +5220,7 @@ def create_aiui_agents_interface(self): app_dir="ui_agents", default_app_name="ui_agents", port=8082, # Use same port as old Chainlit agents - host="0.0.0.0", + host="127.0.0.1", app_file=None, reload=False, ui_name="Agents Dashboard" diff --git a/src/praisonai/praisonai/ui/_auth.py b/src/praisonai/praisonai/ui/_auth.py deleted file mode 100644 index 2c391fda8..000000000 --- a/src/praisonai/praisonai/ui/_auth.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Chainlit UI authentication helper. - -Provides bind-aware auth for Chainlit apps - consolidates duplicated -password auth callbacks across UI modules. -""" -from __future__ import annotations - -import os -import logging -from typing import Optional - -from praisonaiagents.gateway.protocols import AuthMode, is_loopback, resolve_auth_mode - -logger = logging.getLogger(__name__) - - -class UIStartupError(Exception): - """Raised when UI cannot start due to security configuration.""" - pass - - -def register_password_auth(app, *, bind_host: str) -> None: - """Register password authentication for Chainlit app with bind-aware security. - - Args: - app: Chainlit app instance (unused but kept for consistency) - bind_host: Host/IP that the UI server is bound to - - Raises: - UIStartupError: If using default credentials on external interface - """ - import chainlit as cl # lazy — chainlit is in [ui] extra only - # Get credentials from environment - expected_username = os.getenv("CHAINLIT_USERNAME", "admin") - expected_password = os.getenv("CHAINLIT_PASSWORD", "admin") - - # Check if using default credentials - using_defaults = (expected_username == "admin" and expected_password == "admin") - - # Resolve auth mode based on bind interface - auth_mode = resolve_auth_mode(bind_host) - - # Allow default credentials only on loopback - if using_defaults and auth_mode != "local": - # Check for escape hatch - allow_defaults = os.getenv("PRAISONAI_ALLOW_DEFAULT_CREDS", "").lower() in ("1", "true", "yes") - if not allow_defaults: - raise UIStartupError( - f"Cannot bind to {bind_host} with default admin/admin credentials.\n" - f"Fix: export CHAINLIT_USERNAME=myuser CHAINLIT_PASSWORD=mypass\n" - f"Lab: export PRAISONAI_ALLOW_DEFAULT_CREDS=1 (demo only)" - ) - else: - logger.warning( - f"āš ļø Using default admin/admin credentials on external interface {bind_host}. " - f"This is UNSAFE for production. Set CHAINLIT_USERNAME and CHAINLIT_PASSWORD." - ) - elif using_defaults and auth_mode == "local": - logger.warning( - f"āš ļø Using default admin/admin credentials on loopback {bind_host}. " - f"Set CHAINLIT_USERNAME and CHAINLIT_PASSWORD environment variables for production." - ) - - # Register the password auth callback - @cl.password_auth_callback - def auth_callback(username: str, password: str) -> Optional["cl.User"]: - """Password authentication callback.""" - logger.debug("Auth attempt received") - - if (username, password) == (expected_username, expected_password): - logger.info("Login successful") - return cl.User(identifier=username, metadata={"role": "admin", "provider": "credentials"}) - else: - logger.warning("Login failed") - return None - - # Log the registration - auth_status = "permissive" if auth_mode == "local" else "strict" - logger.info(f"Registered password auth for {bind_host} (mode: {auth_status})") \ No newline at end of file diff --git a/src/praisonai/praisonai/ui/_external_agents.py b/src/praisonai/praisonai/ui/_external_agents.py index f0223c722..0d030aa89 100644 --- a/src/praisonai/praisonai/ui/_external_agents.py +++ b/src/praisonai/praisonai/ui/_external_agents.py @@ -2,12 +2,12 @@ Single source of truth for: - Listing installed external agents (lazy, cached) -- Rendering Chainlit Switch widgets / aiui settings entries +- Rendering aiui settings entries - Building the tools list from enabled agents """ from functools import lru_cache -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Dict, List # Map of UI toggle id → (integration class path, pretty label) EXTERNAL_AGENTS: Dict[str, Dict[str, str]] = { @@ -54,16 +54,6 @@ def external_agent_tools(settings: Dict[str, Any], workspace: str = ".") -> list return tools -def chainlit_switches(current_settings: Dict[str, bool]): - """Return Chainlit Switch widgets for installed external agents only.""" - from chainlit.input_widget import Switch - return [ - Switch(id=toggle_id, label=EXTERNAL_AGENTS[toggle_id]["label"], - initial=current_settings.get(toggle_id, False)) - for toggle_id in installed_external_agents() - ] - - def aiui_settings_entries() -> Dict[str, Any]: """Return aiui settings entries for installed external agents.""" settings = {} @@ -83,59 +73,3 @@ def _parse_setting_bool(value: Any) -> bool: if value is None: return False return str(value).strip().lower() in {"true", "1", "yes", "on"} - - -def load_external_agent_settings_from_chainlit( - load_setting_fn: Optional[Callable[[str], Any]] = None, -) -> Dict[str, bool]: - """Load external agent settings from Chainlit session and persistent storage. - - Args: - load_setting_fn: Optional callback used to load persisted settings by key. - If omitted, falls back to importing ``praisonai.ui.chat.load_setting`` - for backward compatibility. - """ - import chainlit as cl - settings = {toggle_id: False for toggle_id in EXTERNAL_AGENTS} - loader = load_setting_fn - - # Try to load from persistent storage (if load_setting is available) - if loader is None: - try: - # Backward-compatible fallback for callers that don't pass a loader - from praisonai.ui.chat import load_setting as loader # type: ignore - except ImportError: - loader = None - - if loader is not None: - legacy_claude = _parse_setting_bool(loader("claude_code_enabled")) - - # Load all current toggles from persistent storage first - for toggle_id in EXTERNAL_AGENTS: - persistent_value = loader(toggle_id) - if persistent_value: # non-empty string means explicitly stored - settings[toggle_id] = _parse_setting_bool(persistent_value) - - # Apply legacy migration only where no explicit value was stored - if legacy_claude and not loader("claude_enabled"): - settings["claude_enabled"] = True - - # Load from session (may override persistent settings) - # Check for legacy key in session - if _parse_setting_bool(cl.user_session.get("claude_code_enabled", False)): - settings["claude_enabled"] = True - - # Load all current toggles from session - for toggle_id in EXTERNAL_AGENTS: - session_value = cl.user_session.get(toggle_id) - if session_value is not None: - settings[toggle_id] = _parse_setting_bool(session_value) - - return settings - - -def save_external_agent_settings_to_chainlit(settings: Dict[str, bool]): - """Save external agent settings to Chainlit session.""" - import chainlit as cl - for toggle_id, enabled in settings.items(): - cl.user_session.set(toggle_id, enabled) diff --git a/src/praisonai/praisonai/ui/_pairing.py b/src/praisonai/praisonai/ui/_pairing.py deleted file mode 100644 index 8549b69a7..000000000 --- a/src/praisonai/praisonai/ui/_pairing.py +++ /dev/null @@ -1,264 +0,0 @@ -""" -Chainlit UI components for pairing approval banner. - -Provides admin banner functionality for approving pending pairing requests. -""" - -from __future__ import annotations - -import logging -import os -from typing import Dict, List, Optional - - -logger = logging.getLogger(__name__) - - -# Gateway client configuration -GATEWAY_HOST = os.environ.get("GATEWAY_HOST", "127.0.0.1") -GATEWAY_PORT = int(os.environ.get("GATEWAY_PORT", "8765")) -GATEWAY_TOKEN = os.environ.get("GATEWAY_AUTH_TOKEN", "") - - -async def get_pending_pairings() -> List[Dict]: - """Fetch pending pairing requests from gateway API.""" - if not GATEWAY_TOKEN: - logger.warning("No GATEWAY_AUTH_TOKEN set, cannot fetch pending pairings") - return [] - - try: - import aiohttp - - url = f"http://{GATEWAY_HOST}:{GATEWAY_PORT}/api/pairing/pending" - headers = {"Authorization": f"Bearer {GATEWAY_TOKEN}"} - - timeout = aiohttp.ClientTimeout(total=5) - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(url, headers=headers) as resp: - if resp.status == 200: - data = await resp.json() - return data.get("pending", []) - else: - logger.warning(f"Failed to fetch pending pairings: {resp.status}") - return [] - except Exception as e: - logger.error(f"Error fetching pending pairings: {e}") - return [] - - -async def approve_pairing(channel: str, code: str) -> bool: - """Approve a pairing request via gateway API.""" - if not GATEWAY_TOKEN: - logger.warning("No GATEWAY_AUTH_TOKEN set, cannot approve pairing") - return False - - try: - import aiohttp - - url = f"http://{GATEWAY_HOST}:{GATEWAY_PORT}/api/pairing/approve" - headers = { - "Authorization": f"Bearer {GATEWAY_TOKEN}", - "Content-Type": "application/json" - } - data = {"channel": channel, "code": code} - - timeout = aiohttp.ClientTimeout(total=5) - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.post(url, headers=headers, json=data) as resp: - success = resp.status == 200 - if not success: - logger.warning(f"Failed to approve pairing: {resp.status}") - return success - except Exception as e: - logger.error(f"Error approving pairing: {e}") - return False - - -async def deny_pairing(channel: str, code: str) -> bool: - """Deny a pairing request (logs denial - code will expire naturally).""" - # Note: Currently there's no dedicated deny endpoint in the gateway - # The pending request will expire naturally based on TTL - # This is a UI-only action for admin feedback - logger.info(f"Admin denied pairing request: {channel} code {code}") - return True # Always succeed for UI purposes - - -async def refresh_pending_banner(): - """Display or update the pending pairing banner for admin users.""" - import chainlit as cl - - # Check if user is admin - user = cl.user_session.get("user") - if not user or user.metadata.get("role") != "admin": - return - - pending = await get_pending_pairings() - - if not pending: - # No pending requests - don't show banner - return - - # Create approval actions for each pending request - actions = [] - for p in pending: - channel = p.get("channel", "unknown") - code = p.get("code", "") - user_name = p.get("user_name", f"User {code}") - age = p.get("age_seconds", 0) - - # Format age nicely - if age < 60: - age_str = f"{age}s ago" - elif age < 3600: - age_str = f"{age//60}m ago" - else: - age_str = f"{age//3600}h ago" - - actions.append( - cl.Action( - name="approve_pairing", - value=f"{channel}:{code}", - label=f"āœ… Approve {user_name} ({channel}) - {age_str}", - description=f"Approve pairing request from {user_name} on {channel}" - ) - ) - actions.append( - cl.Action( - name="deny_pairing", - value=f"{channel}:{code}", - label=f"āŒ Deny {user_name} ({channel})", - description=f"Deny pairing request from {user_name} on {channel}", - ) - ) - - # Display or update banner message with actions - banner_content = f"šŸ”” **{len(pending)} pending pairing request(s)**\n\nClick to approve:" - - # Check if we already have a banner message stored in the session - banner_msg_id = cl.user_session.get("pending_banner_id") - - if banner_msg_id: - # Try to update existing message - try: - # Get the existing message and update it - # Note: Chainlit's update functionality depends on the specific version - # For now, send a new message and store its ID - msg = await cl.Message( - content=banner_content, - actions=actions, - author="System" - ).send() - cl.user_session.set("pending_banner_id", msg.id) - except Exception as e: - logger.warning(f"Failed to update banner message: {e}") - # Fall back to sending new message - msg = await cl.Message( - content=banner_content, - actions=actions, - author="System" - ).send() - cl.user_session.set("pending_banner_id", msg.id) - else: - # Send new banner message and store its ID - msg = await cl.Message( - content=banner_content, - actions=actions, - author="System" - ).send() - cl.user_session.set("pending_banner_id", msg.id) - - -async def on_approve_pairing(action): - """Handle approval action from banner.""" - import chainlit as cl - - try: - # Parse channel:code from action value - channel, code = action.value.split(":", 1) - - # Show loading message - await cl.Message( - content=f"ā³ Approving pairing for {channel} code {code}...", - author="System" - ).send() - - # Approve the pairing - success = await approve_pairing(channel, code) - - if success: - await cl.Message( - content=f"āœ… Successfully approved pairing for {channel} code {code}", - author="System" - ).send() - else: - await cl.Message( - content=f"āŒ Failed to approve pairing for {channel} code {code}", - author="System" - ).send() - - # Refresh the banner to update count - await refresh_pending_banner() - - except Exception as e: - logger.error(f"Error in approval handler: {e}") - await cl.Message( - content=f"āŒ Error processing approval: {str(e)}", - author="System" - ).send() - - -async def on_deny_pairing(action): - """Handle denial action from banner.""" - import chainlit as cl - - try: - # Parse channel:code from action value - channel, code = action.value.split(":", 1) - - # Show loading message - await cl.Message( - content=f"ā³ Denying pairing for {channel} code {code}...", - author="System" - ).send() - - # Deny the pairing - success = await deny_pairing(channel, code) - - if success: - await cl.Message( - content=f"āœ… Successfully denied pairing for {channel} code {code}", - author="System" - ).send() - else: - await cl.Message( - content=f"āŒ Failed to deny pairing for {channel} code {code}", - author="System" - ).send() - - # Refresh the banner to update count - await refresh_pending_banner() - - except Exception as e: - logger.error(f"Error in denial handler: {e}") - await cl.Message( - content=f"āŒ Error processing denial: {str(e)}", - author="System" - ).send() - - -def setup_pairing_callbacks(): - """Setup pairing action callbacks (call this at module level after chainlit import).""" - import chainlit as cl - - @cl.action_callback("approve_pairing") - async def _approve_callback(action): - await on_approve_pairing(action) - - @cl.action_callback("deny_pairing") - async def _deny_callback(action): - await on_deny_pairing(action) - - -async def setup_pairing_banner(): - """Setup pairing banner on chat start (call this from @cl.on_chat_start).""" - await refresh_pending_banner() \ No newline at end of file diff --git a/src/praisonai/praisonai/ui/agents.py b/src/praisonai/praisonai/ui/agents.py deleted file mode 100644 index daaa94e22..000000000 --- a/src/praisonai/praisonai/ui/agents.py +++ /dev/null @@ -1,875 +0,0 @@ -from chainlit.input_widget import Select, TextInput -import os -import sys -import yaml -import logging -import inspect -import chainlit as cl -from praisonaiagents import Agent, Task, AgentTeam, register_display_callback - -framework = "praisonai" -config_list = [ - { - 'model': os.environ.get("OPENAI_MODEL_NAME", "gpt-4o-mini"), - 'base_url': os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1"), - 'api_key': os.environ.get("OPENAI_API_KEY", "") - } -] - -actions = [ - cl.Action(name="run", payload={"value": "run"}, label="run"), - cl.Action(name="modify", payload={"value": "modify"}, label="Modify"), -] - -@cl.action_callback("run") -async def on_run(action): - await main(cl.Message(content="")) - -@cl.action_callback("modify") -async def on_modify(action): - await cl.Message(content="Modify the agents and tools from below settings").send() - -import os -import sys -import yaml -import logging -import inspect -import asyncio -import importlib.util -import sqlite3 -from queue import Queue -from datetime import datetime -from dotenv import load_dotenv - -# Chainlit imports -import chainlit as cl -from chainlit.types import ThreadDict -import chainlit.data as cl_data - -# ----------------------------------------------------------------------------- -# Global Setup -# ----------------------------------------------------------------------------- - -load_dotenv() -log_level = os.getenv("LOGLEVEL", "INFO").upper() -logger = logging.getLogger(__name__) -logger.setLevel(log_level) - -message_queue = Queue() # Queue to handle messages sent to Chainlit UI -agent_file = "agents.yaml" - -# ----------------------------------------------------------------------------- -# Database and Settings Logic -# ----------------------------------------------------------------------------- - -MAX_RETRIES = 3 -RETRY_DELAY = 1 # seconds - -from db import DatabaseManager - -async def init_database_with_retry(): - db = DatabaseManager() - for attempt in range(MAX_RETRIES): - try: - db.initialize() - return db - except sqlite3.OperationalError as e: - if "database is locked" in str(e) and attempt < MAX_RETRIES - 1: - await asyncio.sleep(RETRY_DELAY) - continue - raise - -db_manager = asyncio.run(init_database_with_retry()) -cl_data._data_layer = db_manager - -async def save_setting_with_retry(key: str, value: str): - for attempt in range(MAX_RETRIES): - try: - await db_manager.save_setting(key, value) - return - except sqlite3.OperationalError as e: - if "database is locked" in str(e) and attempt < MAX_RETRIES - 1: - await asyncio.sleep(RETRY_DELAY) - continue - raise - -async def load_setting_with_retry(key: str) -> str: - for attempt in range(MAX_RETRIES): - try: - return await db_manager.load_setting(key) - except sqlite3.OperationalError as e: - if "database is locked" in str(e) and attempt < MAX_RETRIES - 1: - await asyncio.sleep(RETRY_DELAY) - continue - raise - return "" - -def save_setting(key: str, value: str): - asyncio.run(save_setting_with_retry(key, value)) - -def load_setting(key: str) -> str: - return asyncio.run(load_setting_with_retry(key)) - -async def update_thread_metadata(thread_id: str, metadata: dict): - for attempt in range(MAX_RETRIES): - try: - await cl_data.update_thread(thread_id, metadata=metadata) - return - except sqlite3.OperationalError as e: - if "database is locked" in str(e) and attempt < MAX_RETRIES - 1: - await asyncio.sleep(RETRY_DELAY) - continue - raise - -# ----------------------------------------------------------------------------- -# Callback Manager -# ----------------------------------------------------------------------------- - -class CallbackManager: - def __init__(self): - self._callbacks = {} - - def register(self, name: str, callback, is_async: bool = False) -> None: - self._callbacks[name] = {'func': callback, 'is_async': is_async} - - async def call(self, name: str, **kwargs) -> None: - if name not in self._callbacks: - logger.warning(f"No callback registered for {name}") - return - callback_info = self._callbacks[name] - func = callback_info['func'] - is_async = callback_info['is_async'] - try: - if is_async: - await func(**kwargs) - else: - if asyncio.iscoroutinefunction(func): - await func(**kwargs) - else: - await asyncio.get_event_loop().run_in_executor(None, lambda: func(**kwargs)) - except Exception as e: - logger.error(f"Error in callback {name}: {str(e)}") - -callback_manager = CallbackManager() - -def register_callback(name: str, callback, is_async: bool = False) -> None: - callback_manager.register(name, callback, is_async) - -async def trigger_callback(name: str, **kwargs) -> None: - await callback_manager.call(name, **kwargs) - -def callback(name: str, is_async: bool = False): - def decorator(func): - register_callback(name, func, is_async) - return func - return decorator - -# ----------------------------------------------------------------------------- -# ADDITIONAL CALLBACKS -# ----------------------------------------------------------------------------- -def interaction_callback(message=None, response=None, **kwargs): - logger.debug(f"[CALLBACK: interaction] Message: {message} | Response: {response}") - message_queue.put({ - "content": f"[CALLBACK: interaction] Message: {message} | Response: {response}", - "author": "Callback" - }) - -def error_callback(message=None, **kwargs): - logger.error(f"[CALLBACK: error] Message: {message}") - message_queue.put({ - "content": f"[CALLBACK: error] Message: {message}", - "author": "Callback" - }) - -def tool_call_callback(message=None, **kwargs): - logger.debug(f"[CALLBACK: tool_call] Tool used: {message}") - message_queue.put({ - "content": f"[CALLBACK: tool_call] Tool used: {message}", - "author": "Callback" - }) - -def instruction_callback(message=None, **kwargs): - logger.debug(f"[CALLBACK: instruction] Instruction: {message}") - message_queue.put({ - "content": f"[CALLBACK: instruction] Instruction: {message}", - "author": "Callback" - }) - -def self_reflection_callback(message=None, **kwargs): - logger.debug(f"[CALLBACK: self_reflection] Reflection: {message}") - message_queue.put({ - "content": f"[CALLBACK: self_reflection] Reflection: {message}", - "author": "Callback" - }) - -register_display_callback('error', error_callback) -register_display_callback('tool_call', tool_call_callback) -register_display_callback('instruction', instruction_callback) -register_display_callback('self_reflection', self_reflection_callback) - -# ----------------------------------------------------------------------------- -# Tools Loader -# ----------------------------------------------------------------------------- - -def load_tools_from_tools_py(): - """ - Imports and returns all contents from tools.py file. - Also adds the tools to the global namespace. - """ - tools_dict = {} - try: - spec = importlib.util.spec_from_file_location("tools", "tools.py") - if spec is None: - logger.info("tools.py not found in current directory") - return tools_dict - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - for name, obj in inspect.getmembers(module): - if not name.startswith('_') and callable(obj) and not inspect.isclass(obj): - # Store the function in globals - globals()[name] = obj - - # Build the function definition - tool_def = { - "type": "function", - "function": { - "name": name, - "description": obj.__doc__ or f"Function to {name.replace('_', ' ')}", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - }, - # Keep the actual callable as well - "callable": obj, - } - - tools_dict[name] = tool_def - logger.info(f"Loaded and globalized tool function: {name}") - - logger.info(f"Loaded {len(tools_dict)} tool functions from tools.py") - except Exception as e: - logger.warning(f"Error loading tools from tools.py: {e}") - return tools_dict - -# ----------------------------------------------------------------------------- -# Async Queue Processor -# ----------------------------------------------------------------------------- - -async def process_message_queue(): - while True: - try: - if not message_queue.empty(): - msg_data = message_queue.get() - await cl.Message(**msg_data).send() - await asyncio.sleep(0.1) - except Exception as e: - logger.error(f"Error processing message queue: {e}") - -# ----------------------------------------------------------------------------- -# Step & Task Callbacks -# ----------------------------------------------------------------------------- - -async def step_callback(step_details): - logger.info(f"[CALLBACK DEBUG] step_callback: {step_details}") - agent_name = step_details.get("agent_name", "Agent") - try: - if step_details.get("response"): - message_queue.put({ - "content": f"Agent Response: {step_details['response']}", - "author": agent_name - }) - if step_details.get("tool_name"): - message_queue.put({ - "content": f"šŸ› ļø Using tool: {step_details['tool_name']}", - "author": "System" - }) - except Exception as e: - logger.error(f"Error in step_callback: {e}", exc_info=True) - -async def task_callback(task_output): - logger.info(f"[CALLBACK DEBUG] task_callback: type={type(task_output)}") - try: - if hasattr(task_output, 'raw'): - content = task_output.raw - elif hasattr(task_output, 'content'): - content = task_output.content - else: - content = str(task_output) - message_queue.put({ - "content": f"Task Output: {content}", - "author": "Task" - }) - except Exception as e: - logger.error(f"Error in task_callback: {e}", exc_info=True) - -async def step_callback_wrapper(step_details): - logger.info(f"[CALLBACK DEBUG] step_callback_wrapper: {step_details}") - agent_name = step_details.get("agent_name", "Agent") - try: - if not cl.context.context_var.get(): - logger.warning("[CALLBACK DEBUG] No Chainlit context in wrapper.") - return - if step_details.get("response"): - await cl.Message( - content=f"{agent_name}: {step_details['response']}", - author=agent_name, - ).send() - if step_details.get("tool_name"): - await cl.Message( - content=f"šŸ› ļø {agent_name} is using tool: {step_details['tool_name']}", - author="System", - ).send() - if step_details.get("thought"): - await cl.Message( - content=f"šŸ’­ {agent_name}'s thought: {step_details['thought']}", - author=agent_name, - ).send() - except Exception as e: - logger.error(f"Error in step_callback_wrapper: {e}", exc_info=True) - try: - await cl.Message(content=f"Error in step callback: {e}", author="System").send() - except Exception as send_error: - logger.error(f"Error sending error message: {send_error}") - -async def task_callback_wrapper(task_output): - logger.info("[CALLBACK DEBUG] task_callback_wrapper triggered") - try: - if not cl.context.context_var.get(): - logger.warning("[CALLBACK DEBUG] No Chainlit context in task wrapper.") - return - if hasattr(task_output, 'raw'): - content = task_output.raw - elif hasattr(task_output, 'content'): - content = task_output.content - else: - content = str(task_output) - - await cl.Message( - content=f"āœ… Agent completed task:\n{content}", - author="Agent", - ).send() - - if hasattr(task_output, 'details'): - await cl.Message( - content=f"šŸ“ Additional details:\n{task_output.details}", - author="Agent", - ).send() - except Exception as e: - logger.error(f"Error in task_callback_wrapper: {e}", exc_info=True) - try: - await cl.Message(content=f"Error in task callback: {e}", author="System").send() - except Exception as send_error: - logger.error(f"Error sending error message: {send_error}") - -def sync_task_callback_wrapper(task_output): - logger.info("[CALLBACK DEBUG] sync_task_callback_wrapper") - try: - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - if loop.is_running(): - asyncio.run_coroutine_threadsafe(task_callback_wrapper(task_output), loop) - else: - loop.run_until_complete(task_callback_wrapper(task_output)) - except Exception as e: - logger.error(f"Error in sync_task_callback_wrapper: {e}", exc_info=True) - -def sync_step_callback_wrapper(step_details): - logger.info("[CALLBACK DEBUG] sync_step_callback_wrapper") - try: - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - if loop.is_running(): - asyncio.run_coroutine_threadsafe(step_callback_wrapper(step_details), loop) - else: - loop.run_until_complete(step_callback_wrapper(step_details)) - except Exception as e: - logger.error(f"Error in sync_step_callback_wrapper: {e}", exc_info=True) - -# ----------------------------------------------------------------------------- -# Main PraisonAI Runner -# ----------------------------------------------------------------------------- -async def ui_run_praisonai(config, topic, tools_dict): - logger.info("Starting ui_run_praisonai") - agents_map = {} - tasks = [] - tasks_dict = {} - - try: - queue_processor = asyncio.create_task(process_message_queue()) - - # Create agents - for role, details in config['roles'].items(): - role_name = details.get('name', role).format(topic=topic) - role_filled = details.get('role', role).format(topic=topic) - goal_filled = details['goal'].format(topic=topic) - backstory_filled = details['backstory'].format(topic=topic) - - def step_callback_sync(step_details): - step_details["agent_name"] = role_name - try: - loop_ = asyncio.new_event_loop() - asyncio.set_event_loop(loop_) - loop_.run_until_complete(step_callback(step_details)) - loop_.close() - except Exception as e: - logger.error(f"Error in step_callback_sync: {e}", exc_info=True) - - # Get external agent tools from current settings - external_tools = [] - try: - from praisonai.ui._external_agents import external_agent_tools, EXTERNAL_AGENTS - settings = cl.user_session.get("settings", {}) - external_settings = {k: settings.get(k, False) for k in EXTERNAL_AGENTS} - workspace = os.environ.get("PRAISONAI_WORKSPACE", ".") - external_tools = external_agent_tools(external_settings, workspace=workspace) - except ImportError: - pass - - # Get existing tools from details - existing_tools = details.get('tools', []) - if isinstance(existing_tools, str): - # If tools is a string, it might be a module reference - keep as is - all_tools = existing_tools - else: - # Combine existing and external tools - all_tools = (existing_tools or []) + external_tools - - agent = Agent( - name=role_name, - role=role_filled, - goal=goal_filled, - backstory=backstory_filled, - llm=details.get('llm', 'gpt-5-nano'), - verbose=True, - allow_delegation=details.get('allow_delegation', False), - max_iter=details.get('max_iter', 15), - max_rpm=details.get('max_rpm'), - max_execution_time=details.get('max_execution_time'), - cache=details.get('cache', True), - step_callback=step_callback_sync, - reflection=details.get('self_reflect', False), - tools=all_tools if all_tools else None - ) - agents_map[role] = agent - - # Create tasks - # Import tool resolver for YAML tool resolution - from praisonai.tool_resolver import ToolResolver - tool_resolver = ToolResolver() - - for role, details in config['roles'].items(): - agent = agents_map[role] - role_name = agent.name - - # ------------------------------------------------------------- - # Tool resolution: local tools_dict first, then ToolResolver - # ------------------------------------------------------------- - role_tools = [] - task_tools = [] # Initialize task_tools outside the loop - - for tool_name in details.get('tools', []): - if not tool_name or not tool_name.strip(): - logger.warning("Skipping empty tool name.") - continue - - tool_name = tool_name.strip() - - # First check local tools_dict (from tools.py) - if tool_name in tools_dict: - # Create a copy of the tool definition - tool_def = tools_dict[tool_name].copy() - # Store the callable separately and remove from definition - callable_func = tool_def.pop("callable") - # Add callable to role_tools for task execution - role_tools.append(callable_func) - # Add API tool definition to task's tools - task_tools.append(tool_def) - else: - # Try to resolve from built-in tools via ToolResolver - resolved_tool = tool_resolver.resolve(tool_name) - if resolved_tool is not None: - role_tools.append(resolved_tool) - logger.info(f"Resolved tool '{tool_name}' from built-in tools") - else: - logger.warning(f"Tool '{tool_name}' not found. Skipping.") - - # Set the agent's tools after collecting all tools - # Merge resolved YAML tools with external agent tools so both survive - merged_tools = role_tools + external_tools - if merged_tools: - agent.tools = merged_tools - - for tname, tdetails in details.get('tasks', {}).items(): - description_filled = tdetails['description'].format(topic=topic) - expected_output_filled = tdetails['expected_output'].format(topic=topic) - - def task_callback_sync(task_output): - try: - loop_ = asyncio.new_event_loop() - asyncio.set_event_loop(loop_) - loop_.run_until_complete(task_callback(task_output)) - loop_.close() - except Exception as e: - logger.error(f"Error in task_callback_sync: {e}", exc_info=True) - - task = Task( - description=description_filled, - expected_output=expected_output_filled, - agent=agent, - tools=task_tools, # Pass API tool definitions - async_execution=True, - context=[], - config=tdetails.get('config', {}), - output_json=tdetails.get('output_json'), - output_pydantic=tdetails.get('output_pydantic'), - output_file=tdetails.get('output_file', ""), - callback=task_callback_sync, - create_directory=tdetails.get('create_directory', False) - ) - tasks.append(task) - tasks_dict[tname] = task - - # Build context links - for role, details in config['roles'].items(): - for tname, tdetails in details.get('tasks', {}).items(): - if tname not in tasks_dict: - continue - task = tasks_dict[tname] - context_tasks = [ - tasks_dict[ctx] - for ctx in tdetails.get('context', []) - if ctx in tasks_dict - ] - task.context = context_tasks - - await cl.Message(content="Starting PraisonAI agents execution...", author="System").send() - - # Decide how to process tasks - if config.get('process') == 'hierarchical': - prai_agents = AgentTeam( - agents=list(agents_map.values()), - tasks=tasks, - verbose=True, - process="hierarchical", - manager_llm=config.get('manager_llm', 'gpt-5-nano') - ) - else: - prai_agents = AgentTeam( - agents=list(agents_map.values()), - tasks=tasks, - verbose=2 - ) - - cl.user_session.set("agents", prai_agents) - - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, prai_agents.start) - - if hasattr(response, 'raw'): - result = response.raw - elif hasattr(response, 'content'): - result = response.content - else: - result = str(response) - - await cl.Message(content="PraisonAI agents execution completed.", author="System").send() - await asyncio.sleep(1) - queue_processor.cancel() - return result - - except Exception as e: - error_msg = f"Error in ui_run_praisonai: {str(e)}" - logger.error(error_msg, exc_info=True) - await cl.Message(content=error_msg, author="System").send() - raise - -# ----------------------------------------------------------------------------- -# Chainlit Handlers + logic -# ----------------------------------------------------------------------------- - -tools_dict = load_tools_from_tools_py() -print(f"[DEBUG] tools_dict: {tools_dict}") - -# Load agent config (default) from 'agents.yaml' -with open(agent_file, 'r') as f: - config = yaml.safe_load(f) - -import secrets -AUTH_PASSWORD_ENABLED = os.getenv("AUTH_PASSWORD_ENABLED", "true").lower() == "true" -CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET") -if not CHAINLIT_AUTH_SECRET: - os.environ["CHAINLIT_AUTH_SECRET"] = secrets.token_hex(32) - logger.warning("CHAINLIT_AUTH_SECRET not set; generated a random secret for this session.") - -# Authentication configuration - bind-aware auth -from praisonaiagents.gateway.protocols import is_loopback -from ._auth import register_password_auth - -# Determine bind host from CHAINLIT_HOST env var (default: 127.0.0.1) -bind_host = os.getenv("CHAINLIT_HOST", "127.0.0.1") -if AUTH_PASSWORD_ENABLED or not is_loopback(bind_host): - register_password_auth(None, bind_host=bind_host) - -@cl.set_chat_profiles -async def set_profiles(current_user: cl.User): - return [ - cl.ChatProfile( - name="Auto", - markdown_description=( - "Automatically generate agents and tasks based on your input." - ), - starters=[ - cl.Starter( - label="Create a movie script", - message=( - "Create a movie script about a futuristic society where AI " - "and humans coexist, focusing on the conflict and resolution " - "between them. Start with an intriguing opening scene." - ), - icon="/public/movie.svg", - ), - cl.Starter( - label="Design a fantasy world", - message=( - "Design a detailed fantasy world with unique geography, " - "cultures, and magical systems. Start by describing the main " - "continent and its inhabitants." - ), - icon="/public/fantasy.svg", - ), - cl.Starter( - label="Write a futuristic political thriller", - message=( - "Write a futuristic political thriller involving a conspiracy " - "within a global government. Start with a high-stakes meeting " - "that sets the plot in motion." - ), - icon="/public/thriller.svg", - ), - cl.Starter( - label="Develop a new board game", - message=( - "Develop a new, innovative board game. Describe the game's " - "objective, rules, and unique mechanics. Create a scenario to " - "illustrate gameplay." - ), - icon="/public/game.svg", - ), - ], - ), - cl.ChatProfile( - name="Manual", - markdown_description="Manually define your agents and tasks using a YAML file.", - ), - ] - -@cl.on_chat_start -async def start_chat(): - try: - model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini") - cl.user_session.set("model_name", model_name) - logger.debug(f"Model name: {model_name}") - - cl.user_session.set( - "message_history", - [{"role": "system", "content": "You are a helpful assistant."}], - ) - - if not os.path.exists("tools.py"): - with open("tools.py", "w") as f: - f.write("# Add your custom tools here\n") - - if not os.path.exists("agents.yaml"): - with open("agents.yaml", "w") as f: - f.write("# Add your custom agents here\n") - - # Load external agent settings - try: - from praisonai.ui._external_agents import ( - chainlit_switches, - load_external_agent_settings_from_chainlit, - ) - external_settings = load_external_agent_settings_from_chainlit(load_setting) - except ImportError: - def chainlit_switches(_settings): # no-op fallback - return [] - external_settings = {} - - settings = await cl.ChatSettings( - [ - TextInput(id="Model", label="OpenAI - Model", initial=model_name), - TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']), - TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']), - Select( - id="Framework", - label="Framework", - values=["praisonai", "crewai", "autogen"], - initial_index=0, - ), - *chainlit_switches(external_settings) - ] - ).send() - cl.user_session.set("settings", settings) - chat_profile = cl.user_session.get("chat_profile") - - if chat_profile == "Manual": - agent_file = "agents.yaml" - full_agent_file_path = os.path.abspath(agent_file) - if os.path.exists(full_agent_file_path): - with open(full_agent_file_path, 'r') as f: - yaml_content = f.read() - msg = cl.Message(content=yaml_content, language="yaml") - await msg.send() - - full_tools_file_path = os.path.abspath("tools.py") - if os.path.exists(full_tools_file_path): - with open(full_tools_file_path, 'r') as f: - tools_content = f.read() - msg = cl.Message(content=tools_content, language="python") - await msg.send() - - settings = await cl.ChatSettings( - [ - TextInput(id="Model", label="OpenAI - Model", initial=model_name), - TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']), - TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']), - Select( - id="Framework", - label="Framework", - values=["praisonai", "crewai", "autogen"], - initial_index=0, - ), - TextInput(id="agents", label="agents.yaml", initial=yaml_content, multiline=True), - TextInput(id="tools", label="tools.py", initial=tools_content, multiline=True), - *chainlit_switches(external_settings) - ] - ).send() - cl.user_session.set("settings", settings) - - res = await cl.AskActionMessage( - content="Pick an action!", - actions=actions, - ).send() - if res and res.get("value") == "modify": - await cl.Message(content="Modify the agents and tools from below settings", actions=actions).send() - elif res and res.get("value") == "run": - await main(cl.Message(content="", actions=actions)) - - await on_settings_update(settings) - except Exception as e: - logger.error(f"Error in start_chat: {str(e)}") - await cl.Message(content=f"An error occurred while starting the chat: {str(e)}").send() - -@cl.on_chat_resume -async def on_chat_resume(thread: ThreadDict): - try: - message_history = cl.user_session.get("message_history", []) - root_messages = [m for m in thread["steps"] if m["parentId"] is None] - for message in root_messages: - if message["type"] == "user_message": - message_history.append({"role": "user", "content": message["output"]}) - elif message["type"] == "ai_message": - message_history.append({"role": "assistant", "content": message["content"]}) - cl.user_session.set("message_history", message_history) - except Exception as e: - logger.error(f"Error in on_chat_resume: {str(e)}") - -@cl.on_message -async def main(message: cl.Message): - try: - logger.info(f"User message: {message.content}") - msg = cl.Message(content="") - await msg.stream_token(f"šŸ”„ Processing your request: {message.content}...") - - # Run PraisonAI - result = await ui_run_praisonai(config, message.content, tools_dict) - - message_history = cl.user_session.get("message_history", []) - message_history.append({"role": "user", "content": message.content}) - message_history.append({"role": "assistant", "content": str(result)}) - cl.user_session.set("message_history", message_history) - await msg.send() - except Exception as e: - error_msg = f"Error running PraisonAI agents: {str(e)}" - logger.error(error_msg, exc_info=True) - await cl.Message(content=error_msg, author="System").send() - -@cl.on_settings_update -async def on_settings_update(settings): - try: - global config_list, framework - config_list[0]['model'] = settings["Model"] - config_list[0]['base_url'] = settings["BaseUrl"] - config_list[0]['api_key'] = settings["ApiKey"] - - for attempt in range(MAX_RETRIES): - try: - await save_setting_with_retry("model_name", config_list[0]['model']) - await save_setting_with_retry("base_url", config_list[0]['base_url']) - await save_setting_with_retry("api_key", config_list[0]['api_key']) - break - except sqlite3.OperationalError as e: - if "database is locked" in str(e) and attempt < MAX_RETRIES - 1: - await asyncio.sleep(RETRY_DELAY) - continue - raise - - os.environ["OPENAI_API_KEY"] = config_list[0]['api_key'] - os.environ["OPENAI_MODEL_NAME"] = config_list[0]['model'] - os.environ["OPENAI_API_BASE"] = config_list[0]['base_url'] - os.environ["MODEL_NAME"] = config_list[0]['model'] - framework = settings["Framework"] - os.environ["FRAMEWORK"] = framework - - if "agents" in settings: - with open("agents.yaml", "w") as f: - f.write(settings["agents"]) - if "tools" in settings: - with open("tools.py", "w") as f: - f.write(settings["tools"]) - - thread_id = cl.user_session.get("thread_id") - if thread_id: - for attempt in range(MAX_RETRIES): - try: - thread = await cl_data.get_thread(thread_id) - if thread: - metadata = thread.get("metadata", {}) - if isinstance(metadata, str): - try: - import json - metadata = json.loads(metadata) - except json.JSONDecodeError: - metadata = {} - metadata["model_name"] = config_list[0]['model'] - await cl_data.update_thread(thread_id, metadata=metadata) - cl.user_session.set("metadata", metadata) - break - except sqlite3.OperationalError as e: - if "database is locked" in str(e) and attempt < MAX_RETRIES - 1: - await asyncio.sleep(RETRY_DELAY) - continue - raise - - logger.info("Settings updated successfully") - except Exception as e: - logger.error(f"Error updating settings: {str(e)}") - await cl.Message(content=f"An error occurred while updating settings: {str(e)}. Retrying...").send() - try: - await asyncio.sleep(RETRY_DELAY * 2) - await on_settings_update(settings) - except Exception as e: - logger.error(f"Final retry failed: {str(e)}") - await cl.Message(content=f"Failed to update settings after retries: {str(e)}").send() diff --git a/src/praisonai/praisonai/ui/bot.py b/src/praisonai/praisonai/ui/bot.py deleted file mode 100644 index e95760325..000000000 --- a/src/praisonai/praisonai/ui/bot.py +++ /dev/null @@ -1,425 +0,0 @@ -"""PraisonAI Bot UI - Chainlit chat interface with real-time streaming. - -Provides a browser-based chat UI backed by PraisonAI Agent with: -- Real-time token streaming via StreamEventEmitter (not fake word-splitting) -- Tool call visualization as Chainlit Steps (Chain of Thought) -- MESSAGE_RECEIVED/SENDING/SENT hook firing -- Full bot capabilities (memory, web search, tools, etc.) -- Gateway connectivity (if configured) - -Usage: - praisonai ui bot - praisonai ui bot --model gpt-4o --memory --web - praisonai ui bot --agent agents.yaml --tools DuckDuckGoTool -""" - -import asyncio -import logging -import os -import queue - -import chainlit as cl -from chainlit.input_widget import TextInput, Switch - -logger = logging.getLogger(__name__) -log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO" -logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") - -import secrets - -# Auth secret (required by Chainlit) -if not os.getenv("CHAINLIT_AUTH_SECRET"): - os.environ["CHAINLIT_AUTH_SECRET"] = secrets.token_hex(32) - logger.warning("CHAINLIT_AUTH_SECRET not set; generated a random secret for this session.") - - -# --------------------------------------------------------------------------- -# Lazy imports -# --------------------------------------------------------------------------- -_cache = {} - - -def _get_agent_class(): - if "Agent" not in _cache: - from praisonaiagents import Agent - _cache["Agent"] = Agent - return _cache["Agent"] - - -def _get_stream_event_type(): - if "StreamEventType" not in _cache: - from praisonaiagents.streaming.events import StreamEventType - _cache["StreamEventType"] = StreamEventType - return _cache["StreamEventType"] - - -def _get_bot_handler(): - """Reuse BotHandler._build_tools / _load_agent for DRY tool resolution.""" - if "BotHandler" not in _cache: - from praisonai.cli.features.bots_cli import BotHandler - _cache["BotHandler"] = BotHandler - return _cache["BotHandler"] - - -def _get_bot_capabilities(): - if "BotCapabilities" not in _cache: - from praisonai.cli.features.bots_cli import BotCapabilities - _cache["BotCapabilities"] = BotCapabilities - return _cache["BotCapabilities"] - - -# --------------------------------------------------------------------------- -# Environment-driven config (set by CLI before launching chainlit) -# --------------------------------------------------------------------------- -def _get_config(): - """Read bot config from environment (set by CLI command).""" - return { - "model": os.getenv("PRAISONAI_BOT_MODEL", "gpt-4o-mini"), - "agent_file": os.getenv("PRAISONAI_BOT_AGENT_FILE"), - "memory": os.getenv("PRAISONAI_BOT_MEMORY", "").lower() in ("1", "true"), - "web_search": os.getenv("PRAISONAI_BOT_WEB_SEARCH", "").lower() in ("1", "true"), - "web_provider": os.getenv("PRAISONAI_BOT_WEB_PROVIDER", "duckduckgo"), - "tools": [t.strip() for t in os.getenv("PRAISONAI_BOT_TOOLS", "").split(",") if t.strip()], - "auto_approve": os.getenv("PRAISONAI_BOT_AUTO_APPROVE", "").lower() in ("1", "true"), - } - - -# --------------------------------------------------------------------------- -# Agent factory (reuses BotHandler patterns) -# --------------------------------------------------------------------------- -def _create_agent(model: str = None, memory: bool = False, web_search: bool = False, - web_provider: str = "duckduckgo", tools_list: list = None, - agent_file: str = None, auto_approve: bool = False): - """Create a PraisonAI Agent with capabilities, reusing BotHandler patterns.""" - BotCapabilities = _get_bot_capabilities() - BotHandler = _get_bot_handler() - - config = _get_config() - model = model or config["model"] - memory = memory or config["memory"] - web_search = web_search or config["web_search"] - web_provider = web_provider or config["web_provider"] - tools_list = tools_list or config["tools"] - agent_file = agent_file or config["agent_file"] - auto_approve = auto_approve or config["auto_approve"] - - caps = BotCapabilities( - model=model, - memory=memory, - web_search=web_search, - web_search_provider=web_provider, - tools=tools_list or [], - auto_approve=auto_approve, - ) - - if auto_approve: - os.environ["PRAISONAI_AUTO_APPROVE"] = "true" - - handler = BotHandler() - agent = handler._load_agent(agent_file, caps) - return agent - - -# --------------------------------------------------------------------------- -# Streaming bridge: StreamEventEmitter (sync, in thread) → asyncio.Queue → cl.stream_token (async) -# Pattern reused from gateway/server.py _make_stream_relay -# --------------------------------------------------------------------------- - -# Sentinel to signal stream end -_STREAM_END = object() - - -def _make_chainlit_relay(event_queue: queue.Queue): - """Create a sync StreamCallback that puts events into a thread-safe queue. - - This callback is registered on agent.stream_emitter and called from the - LLM streaming thread (sync context). Events are consumed asynchronously - by _consume_stream_events(). - """ - def _relay(event) -> None: - try: - event_queue.put_nowait(event) - except Exception: - pass # Non-fatal: drop event if queue is full - return _relay - - -async def _consume_stream_events(event_queue: queue.Queue, msg: cl.Message): - """Async consumer: reads StreamEvents from queue and dispatches to Chainlit. - - Maps: - - DELTA_TEXT → msg.stream_token(content) - - DELTA_TOOL_CALL → open cl.Step(type="tool") - - STREAM_END → finalize message - """ - StreamEventType = _get_stream_event_type() - open_steps = {} # tool_call_index → cl.Step - - while True: - # Poll queue without blocking the event loop - try: - event = event_queue.get_nowait() - except queue.Empty: - await asyncio.sleep(0.01) # Yield to event loop - continue - - if event is _STREAM_END: - break - - event_type = getattr(event, "type", None) - if event_type is None: - continue - - try: - if event_type == StreamEventType.DELTA_TEXT: - content = getattr(event, "content", "") or "" - if content: - await msg.stream_token(content) - - elif event_type == StreamEventType.DELTA_TOOL_CALL: - tool_call = getattr(event, "tool_call", {}) or {} - tc_index = tool_call.get("index", 0) - tc_name = tool_call.get("name") or tool_call.get("function", {}).get("name") - - if tc_index not in open_steps and tc_name: - step = cl.Step(name=tc_name, type="tool") - step.input = "" - await step.send() - open_steps[tc_index] = step - - # Accumulate arguments into the step - if tc_index in open_steps: - args_chunk = tool_call.get("arguments", "") or tool_call.get("function", {}).get("arguments", "") - if args_chunk: - open_steps[tc_index].input += args_chunk - - elif event_type == StreamEventType.TOOL_CALL_END: - # Close all open steps - for idx, step in list(open_steps.items()): - step.output = "Completed" - await step.update() - open_steps.clear() - - elif event_type == StreamEventType.STREAM_END: - break - - elif event_type == StreamEventType.ERROR: - error_msg = getattr(event, "error", "Unknown error") - await cl.Message(content=f"**Error:** {error_msg}", author="System").send() - break - - except Exception as e: - logger.debug(f"Stream event handling error (non-fatal): {e}") - - # Close any remaining open steps - for step in open_steps.values(): - try: - step.output = "Completed" - await step.update() - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Chainlit callbacks -# --------------------------------------------------------------------------- - -# Authentication configuration - bind-aware auth -from ._auth import register_password_auth - -# Determine bind host from CHAINLIT_HOST env var (default: 127.0.0.1) -bind_host = os.getenv("CHAINLIT_HOST", "127.0.0.1") -register_password_auth(None, bind_host=bind_host) - - -@cl.on_chat_start -async def on_chat_start(): - config = _get_config() - - # Create agent and store in session - agent = _create_agent() - cl.user_session.set("agent", agent) - cl.user_session.set("model_name", config["model"]) - - # Settings panel - settings = cl.ChatSettings([ - TextInput( - id="model_name", - label="Model", - placeholder="e.g. gpt-4o-mini", - initial=config["model"], - ), - Switch(id="memory", label="Memory", initial=config["memory"]), - Switch(id="web_search", label="Web Search", initial=config["web_search"]), - ]) - await settings.send() - - # Startup message - capabilities = [] - if config["memory"]: - capabilities.append("Memory") - if config["web_search"]: - capabilities.append(f"Web Search ({config['web_provider']})") - if config["tools"]: - capabilities.append(f"Tools: {', '.join(config['tools'])}") - - caps_str = ", ".join(capabilities) if capabilities else "None" - await cl.Message( - content=( - f"**PraisonAI Bot Ready**\n\n" - f"**Model:** {config['model']}\n" - f"**Capabilities:** {caps_str}\n\n" - f"Type a message to start chatting!" - ) - ).send() - - -@cl.on_settings_update -async def on_settings_update(settings): - model = settings.get("model_name", "gpt-4o-mini") - memory = settings.get("memory", False) - web_search = settings.get("web_search", False) - - old_model = cl.user_session.get("model_name") - - # Recreate agent if settings changed - if model != old_model or memory != _get_config()["memory"]: - agent = _create_agent(model=model, memory=memory, web_search=web_search) - cl.user_session.set("agent", agent) - cl.user_session.set("model_name", model) - await cl.Message(content=f"Settings updated. Model: **{model}**").send() - - -@cl.on_message -async def on_message(message: cl.Message): - """Handle incoming messages with real-time streaming.""" - agent = cl.user_session.get("agent") - if agent is None: - agent = _create_agent() - cl.user_session.set("agent", agent) - - # Fire MESSAGE_RECEIVED hook (if hooks configured) - _fire_hook_safe(agent, "MESSAGE_RECEIVED", { - "content": message.content, - "author": message.author or "user", - }) - - # Create empty message for streaming - msg = cl.Message(content="") - await msg.send() - - # Set up streaming bridge - event_queue = queue.Queue(maxsize=10000) - relay_callback = _make_chainlit_relay(event_queue) - - # Register callback on the agent's stream emitter - emitter = getattr(agent, "stream_emitter", None) - if emitter is not None: - emitter.add_callback(relay_callback) - - # Start async consumer task - consumer_task = asyncio.create_task( - _consume_stream_events(event_queue, msg) - ) - - try: - # Fire MESSAGE_SENDING hook - _fire_hook_safe(agent, "MESSAGE_SENDING", {"content": message.content}) - - # Run agent.chat in a thread (sync) — streaming events flow via the relay - loop = asyncio.get_event_loop() - response = await loop.run_in_executor( - None, lambda: agent.chat(message.content, stream=True) - ) - - # Signal stream end - event_queue.put_nowait(_STREAM_END) - - # Wait for consumer to finish processing - await consumer_task - - # Update message with final content - if response: - response_text = response if isinstance(response, str) else str(response) - # Only update if streaming didn't already populate the content - if not msg.content or len(msg.content.strip()) < 10: - msg.content = response_text - await msg.update() - else: - if not msg.content: - msg.content = "No response from agent." - await msg.update() - - # Fire MESSAGE_SENT hook - _fire_hook_safe(agent, "MESSAGE_SENT", { - "content": msg.content, - "author": agent.name, - }) - - except Exception as e: - logger.error(f"Agent chat error: {e}") - event_queue.put_nowait(_STREAM_END) - await consumer_task - msg.content = f"Error: {str(e)}" - await msg.update() - - finally: - # Always clean up the relay callback - if emitter is not None: - try: - emitter.remove_callback(relay_callback) - except (ValueError, AttributeError): - pass - - -def _fire_hook_safe(agent, hook_name: str, data: dict): - """Fire a message lifecycle hook safely (non-blocking, non-fatal).""" - try: - runner = getattr(agent, "_hook_runner", None) - if runner is None: - return - - from praisonaiagents.hooks import HookEvent - event = getattr(HookEvent, hook_name, None) - if event is None: - return - - # Build the appropriate input dataclass - if hook_name == "MESSAGE_RECEIVED": - from praisonaiagents.hooks.events import MessageReceivedInput - hook_input = MessageReceivedInput( - session_id=getattr(agent, "_session_id", "chainlit"), - cwd=os.getcwd(), - event_name=event, - timestamp=str(__import__("time").time()), - platform="chainlit", - content=data.get("content", ""), - sender_id=data.get("author", "user"), - ) - elif hook_name == "MESSAGE_SENDING": - from praisonaiagents.hooks.events import MessageSendingInput - hook_input = MessageSendingInput( - session_id=getattr(agent, "_session_id", "chainlit"), - cwd=os.getcwd(), - event_name=event, - timestamp=str(__import__("time").time()), - platform="chainlit", - content=data.get("content", ""), - ) - elif hook_name == "MESSAGE_SENT": - from praisonaiagents.hooks.events import MessageSentInput - hook_input = MessageSentInput( - session_id=getattr(agent, "_session_id", "chainlit"), - cwd=os.getcwd(), - event_name=event, - timestamp=str(__import__("time").time()), - platform="chainlit", - content=data.get("content", ""), - message_id="", - ) - else: - return - - runner.execute_sync(event, hook_input) - except Exception as e: - logger.debug(f"Hook {hook_name} error (non-fatal): {e}") diff --git a/src/praisonai/praisonai/ui/chainlit_compat.py b/src/praisonai/praisonai/ui/chainlit_compat.py deleted file mode 100644 index 57aa1ee1f..000000000 --- a/src/praisonai/praisonai/ui/chainlit_compat.py +++ /dev/null @@ -1,246 +0,0 @@ -""" -Chainlit compatibility shim for PraisonAI. - -This module provides forward/backward compatible imports for Chainlit internals -that may change between versions. It handles: -- EXPIRY_TIME / storage_expiry_time constant (renamed/removed in various versions) -- BaseStorageClient class location (moved from storage_clients.base to data.base) -- BaseDataLayer abstract methods (close() added in 2.9.4, may be removed later) -- LocalFileStorageClient for default element persistence - -Usage: - from praisonai.ui.chainlit_compat import get_expiry_seconds, BaseStorageClient -""" - -import os -import logging -from typing import Any, Dict, Optional, Type, Union - -logger = logging.getLogger(__name__) - -# Default expiry time in seconds (1 hour) - used if Chainlit doesn't provide one -DEFAULT_EXPIRY_SECONDS = 3600 - - -def get_expiry_seconds() -> int: - """ - Get the storage expiry time in seconds. - - Tries to retrieve from Chainlit's configuration in a version-compatible way. - Falls back to DEFAULT_EXPIRY_SECONDS if not available. - - Returns: - int: Expiry time in seconds - """ - # First check environment variable (highest priority) - env_expiry = os.getenv("STORAGE_EXPIRY_TIME") - if env_expiry is not None: - try: - return int(env_expiry) - except ValueError: - pass - - # Try to import from Chainlit (version-compatible) - # Try multiple known locations across Chainlit versions - - # Chainlit 2.9.x: storage_expiry_time in storage_clients.base - try: - from chainlit.data.storage_clients.base import storage_expiry_time - return storage_expiry_time - except ImportError: - pass - - # Older versions: EXPIRY_TIME (uppercase) in storage_clients.base - try: - from chainlit.data.storage_clients.base import EXPIRY_TIME - return EXPIRY_TIME - except ImportError: - pass - - # Fallback to default - return DEFAULT_EXPIRY_SECONDS - - -def get_base_storage_client() -> Optional[Type]: - """ - Get the BaseStorageClient class from Chainlit. - - Handles multiple import locations across Chainlit versions: - - chainlit.data.base (latest/development) - - chainlit.data.storage_clients.base (2.9.x) - - Returns: - Type: BaseStorageClient class, or None if not available - """ - # Try latest location first (chainlit.data.base) - try: - from chainlit.data.base import BaseStorageClient - return BaseStorageClient - except ImportError: - pass - - # Try 2.9.x location - try: - from chainlit.data.storage_clients.base import BaseStorageClient - return BaseStorageClient - except ImportError: - pass - - return None - - -def get_base_data_layer() -> Optional[Type]: - """ - Get the BaseDataLayer class from Chainlit. - - Returns: - Type: BaseDataLayer class, or None if not available - """ - try: - from chainlit.data.base import BaseDataLayer - return BaseDataLayer - except ImportError: - pass - - try: - from chainlit.data import BaseDataLayer - return BaseDataLayer - except ImportError: - pass - - return None - - -def base_data_layer_has_close() -> bool: - """ - Check if BaseDataLayer has a close() method. - - This method was added in Chainlit 2.9.4 but may be removed in future versions. - - Returns: - bool: True if close() method exists, False otherwise - """ - base_class = get_base_data_layer() - if base_class is None: - return False - - import inspect - # Check if close is an abstract method or regular method - for name, method in inspect.getmembers(base_class): - if name == 'close': - return True - return False - - -# For backward compatibility, expose EXPIRY_TIME as an alias -# This allows existing code to import it without changes -EXPIRY_TIME = get_expiry_seconds() - -# Re-export BaseStorageClient for convenience -# Try multiple locations for compatibility -BaseStorageClient = None -try: - from chainlit.data.base import BaseStorageClient -except ImportError: - try: - from chainlit.data.storage_clients.base import BaseStorageClient - except ImportError: - BaseStorageClient = None - - -class LocalFileStorageClient: - """ - A simple local file storage client for persisting elements to disk. - - This provides a default storage implementation when no cloud storage - (S3, Azure, etc.) is configured. Files are stored in the CHAINLIT_APP_ROOT/.files directory. - """ - - def __init__(self, storage_dir: Optional[str] = None): - """ - Initialize the local file storage client. - - Args: - storage_dir: Directory to store files. Defaults to CHAINLIT_APP_ROOT/.files - """ - if storage_dir: - self.storage_dir = storage_dir - else: - chainlit_root = os.environ.get("CHAINLIT_APP_ROOT", os.path.join(os.path.expanduser("~"), ".praison")) - self.storage_dir = os.path.join(chainlit_root, ".files") - - os.makedirs(self.storage_dir, exist_ok=True) - logger.debug(f"LocalFileStorageClient initialized with storage_dir: {self.storage_dir}") - - async def upload_file( - self, - object_key: str, - data: Union[bytes, str], - mime: str = "application/octet-stream", - overwrite: bool = True, - ) -> Dict[str, Any]: - """ - Upload a file to local storage. - - Args: - object_key: The key/path for the file - data: The file content (bytes or string) - mime: MIME type of the file - overwrite: Whether to overwrite existing files - - Returns: - Dict with object_key and url - """ - import aiofiles - - # Create full path - file_path = os.path.join(self.storage_dir, object_key) - - # Create parent directories if needed - os.makedirs(os.path.dirname(file_path), exist_ok=True) - - # Check if file exists and overwrite is False - if not overwrite and os.path.exists(file_path): - logger.warning(f"File {object_key} already exists and overwrite=False") - return {"object_key": object_key, "url": f"file://{file_path}"} - - # Write the file - try: - if isinstance(data, str): - async with aiofiles.open(file_path, 'w') as f: - await f.write(data) - else: - async with aiofiles.open(file_path, 'wb') as f: - await f.write(data) - - logger.debug(f"Uploaded file to {file_path}") - return {"object_key": object_key, "url": f"file://{file_path}"} - except Exception as e: - logger.error(f"Failed to upload file {object_key}: {e}") - return {} - - -def create_local_storage_client(storage_dir: Optional[str] = None) -> LocalFileStorageClient: - """ - Create a LocalFileStorageClient instance. - - Args: - storage_dir: Optional directory for file storage - - Returns: - LocalFileStorageClient instance - """ - return LocalFileStorageClient(storage_dir=storage_dir) - - -__all__ = [ - 'get_expiry_seconds', - 'get_base_storage_client', - 'get_base_data_layer', - 'base_data_layer_has_close', - 'EXPIRY_TIME', - 'BaseStorageClient', - 'LocalFileStorageClient', - 'create_local_storage_client', - 'DEFAULT_EXPIRY_SECONDS', -] diff --git a/src/praisonai/praisonai/ui/chat.py b/src/praisonai/praisonai/ui/chat.py deleted file mode 100644 index a92ada3dd..000000000 --- a/src/praisonai/praisonai/ui/chat.py +++ /dev/null @@ -1,894 +0,0 @@ -"""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 -import logging - -# Set up minimal logging first -logger = logging.getLogger(__name__) -log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO" -logger.handlers = [] -console_handler = logging.StreamHandler() -console_handler.setLevel(log_level) -console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -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 - -# Setup pairing action callbacks -try: - from praisonai.ui._pairing import setup_pairing_callbacks - setup_pairing_callbacks() -except Exception as e: - logger.debug(f"Failed to setup pairing callbacks: {e}") - -# 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.environ.get("PRAISONAI_CODE_REPO_PATH", os.getcwd()) - # Do not hardcode approval_mode to "auto" to respect environment configuration - 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) -import secrets -_ensure_env_loaded() -CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET") -if not CHAINLIT_AUTH_SECRET: - CHAINLIT_AUTH_SECRET = secrets.token_hex(32) - os.environ["CHAINLIT_AUTH_SECRET"] = CHAINLIT_AUTH_SECRET - logger.warning("CHAINLIT_AUTH_SECRET not set; generated a random secret for this session.") - - -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)) - -def load_custom_tools(): - """Load custom tools from tools.py if it exists.""" - custom_tools = {} - importlib = _get_importlib() - inspect = _get_inspect() - - 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: - cwd_tools = os.path.join(os.getcwd(), "tools.py") - if os.path.exists(cwd_tools): - tools_path = cwd_tools - else: - logger.debug("No tools.py found in current directory (this is normal)") - return custom_tools - - try: - spec = importlib.util.spec_from_file_location("tools", tools_path) - if spec is None or spec.loader is None: - logger.debug(f"Could not load tools from {tools_path}") - return custom_tools - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - for name, obj in inspect.getmembers(module): - if not name.startswith('_') and callable(obj) and not inspect.isclass(obj): - globals()[name] = obj - - sig = inspect.signature(obj) - params_properties = {} - required_params = [] - - for param_name, param in sig.parameters.items(): - 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 is float: - param_type = "number" - elif param.annotation is bool: - param_type = "boolean" - - params_properties[param_name] = { - "type": param_type, - "description": f"Parameter {param_name}" - } - - if param.default is inspect.Parameter.empty: - required_params.append(param_name) - - tool_def = { - "type": "function", - "function": { - "name": name, - "description": obj.__doc__ or f"Function {name.replace('_', ' ')}", - "parameters": { - "type": "object", - "properties": params_properties, - "required": required_params - } - } - } - - custom_tools[name] = tool_def - logger.info(f"Loaded custom tool: {name}") - - if custom_tools: - logger.info(f"Loaded {len(custom_tools)} custom tools from {tools_path}") - except FileNotFoundError: - 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 - -# Deferred tool loading -_custom_tools_dict = None -_tavily_client = None -_tools_list = 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, - "error": "Tavily API key is not set. Web search is unavailable." - }) - - response = tavily_client.search(query) - logger.debug(f"Tavily search response: {response}") - - 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 - }) - -# Authentication configuration - bind-aware auth -from ._auth import register_password_auth - -# Determine bind host from CHAINLIT_HOST env var (default: 127.0.0.1) -bind_host = os.getenv("CHAINLIT_HOST", "127.0.0.1") -register_password_auth(None, bind_host=bind_host) - -def _get_or_create_agent(model_name: str, tools_enabled: bool = True, external_agents_settings: dict = None): - """Get or create a reusable agent for the session.""" - Agent = _get_praisonai_agent() - if Agent is None: - return None - if external_agents_settings is None: - external_agents_settings = {} - - # Get cached agent from session - cached_agent = cl.user_session.get("_cached_agent") - cached_model = cl.user_session.get("_cached_agent_model") - cached_external = cl.user_session.get("_cached_agent_external", {}) - - # Reuse if model and external settings match - if (cached_agent is not None and cached_model == model_name - and cached_external == external_agents_settings): - return cached_agent - - # Create new agent with interactive tools - _profile_start("create_agent") - tools = [] - if tools_enabled: - tools = list(_get_interactive_tools()) # Copy to avoid mutating cache - # Add Tavily if available - if os.getenv("TAVILY_API_KEY"): - tools.append(tavily_web_search) - # Add external agent tools - from praisonai.ui._external_agents import external_agent_tools - tools.extend( - external_agent_tools( - external_agents_settings, - workspace=os.environ.get("PRAISONAI_WORKSPACE", "."), - ) - ) - - agent = Agent( - name="PraisonAI Assistant", - instructions="""You are a helpful AI assistant with access to powerful tools. - -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, - output="minimal", - ) - - # Cache the agent - cl.user_session.set("_cached_agent", agent) - cl.user_session.set("_cached_agent_model", model_name) - cl.user_session.set("_cached_agent_external", external_agents_settings) - _profile_end("create_agent") - - return agent - - -def _parse_setting_bool(value): - if isinstance(value, bool): - return value - if value is None: - return False - return str(value).strip().lower() in {"true", "1", "yes", "on"} - - -def _get_external_agent_settings_from_session() -> dict: - from praisonai.ui._external_agents import EXTERNAL_AGENTS - return { - toggle_id: _parse_setting_bool(cl.user_session.get(toggle_id, False)) - for toggle_id in EXTERNAL_AGENTS - } - -@cl.on_chat_start -async def start(): - _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) - cl.user_session.set("tools_enabled", tools_enabled) - logger.debug(f"Model name: {model_name}, Tools enabled: {tools_enabled}") - - # Load external agent settings - from praisonai.ui._external_agents import load_external_agent_settings_from_chainlit, chainlit_switches - external_settings = load_external_agent_settings_from_chainlit(load_setting) - - # Pre-create agent for faster first response - _get_or_create_agent(model_name, tools_enabled, external_settings) - - settings = cl.ChatSettings( - [ - TextInput( - id="model_name", - label="Enter the Model Name", - placeholder="e.g., gpt-4o-mini", - initial=model_name - ), - Switch( - id="tools_enabled", - label="Enable Tools (ACP, LSP, Web Search)", - initial=tools_enabled - ), - *chainlit_switches(external_settings) - ] - ) - 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() - - # Setup pairing banner for admin users - try: - from praisonai.ui._pairing import setup_pairing_banner - await setup_pairing_banner() - except Exception as e: - logger.debug(f"Failed to setup pairing banner: {e}") - - _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) - - # Save external agent settings - from praisonai.ui._external_agents import save_external_agent_settings_to_chainlit, EXTERNAL_AGENTS - external_agent_settings = {k: settings.get(k, False) for k in EXTERNAL_AGENTS} - save_external_agent_settings_to_chainlit(external_agent_settings) - - # Invalidate cached agent if model changed or external agents changed - cached_model = cl.user_session.get("_cached_agent_model") - cached_external = cl.user_session.get("_cached_agent_external", {}) - if cached_model != model_name or cached_external != external_agent_settings: - cl.user_session.set("_cached_agent", None) - cl.user_session.set("_cached_agent_model", None) - cl.user_session.set("_cached_agent_external", None) - - save_setting("model_name", model_name) - save_setting("tools_enabled", str(tools_enabled).lower()) - # Save external agent settings to persistent storage - for toggle_id, enabled in external_agent_settings.items(): - save_setting(toggle_id, str(enabled).lower()) - - thread_id = cl.user_session.get("thread_id") - if thread_id: - thread = await cl_data.get_thread(thread_id) - if thread: - metadata = thread.get("metadata", {}) - if isinstance(metadata, str): - try: - metadata = json.loads(metadata) - 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): - _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) - image.load() - cl.user_session.set("image", image) - except Exception as e: - logger.error(f"Error processing image: {str(e)}") - await cl.Message(content="Error processing the image. Please try again.").send() - return - - user_message = f"""Answer the question and use tools if needed: - -Current Date and Time: {now} - -User Question: {message.content} -""" - - if image: - user_message = f"Image uploaded. {user_message}" - - message_history.append({"role": "user", "content": user_message}) - msg = cl.Message(content="") - - # Try PraisonAI Agent first (faster, with tool reuse) - external_settings = _get_external_agent_settings_from_session() - agent = _get_or_create_agent(model_name, tools_enabled, external_settings) 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, - "stream": True, - } - - if image: - buffered = io.BytesIO() - image.save(buffered, format="PNG") - img_str = base64.b64encode(buffered.getvalue()).decode() - completion_params["messages"][-1] = { - "role": "user", - "content": [ - {"type": "text", "text": user_message}, - {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_str}"}} - ] - } - - if tools: - completion_params["tools"] = tools - completion_params["tool_choice"] = "auto" - - response = await acompletion(**completion_params) - - full_response = "" - tool_calls = [] - current_tool_call = None - msg_sent = False - - async for part in response: - if 'choices' in part and len(part['choices']) > 0: - delta = part['choices'][0].get('delta', {}) - - if 'content' in delta and delta['content'] is not None: - token = delta['content'] - if not msg_sent: - await msg.send() - msg_sent = True - await msg.stream_token(token) - full_response += token - - if tools and 'tool_calls' in delta and delta['tool_calls'] is not None: - for tool_call in delta['tool_calls']: - if current_tool_call is None or tool_call.index != current_tool_call['index']: - if current_tool_call: - tool_calls.append(current_tool_call) - current_tool_call = { - 'id': tool_call.id, - 'type': tool_call.type, - 'index': tool_call.index, - 'function': { - 'name': tool_call.function.name if tool_call.function else None, - 'arguments': '' - } - } - if tool_call.function: - if tool_call.function.name: - current_tool_call['function']['name'] = tool_call.function.name - if tool_call.function.arguments: - current_tool_call['function']['arguments'] += tool_call.function.arguments - - if current_tool_call: - tool_calls.append(current_tool_call) - - if not msg_sent: - await msg.send() - - message_history.append({"role": "assistant", "content": full_response}) - cl.user_session.set("message_history", message_history) - await msg.update() - - # Handle tool calls - if tool_calls and tools: - custom_tools = _get_custom_tools() - available_functions = {} - - if os.getenv("TAVILY_API_KEY"): - available_functions["tavily_web_search"] = tavily_web_search - - 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'] - }}] - - for tool_call in tool_calls: - function_name = tool_call['function']['name'] - if function_name in available_functions: - function_to_call = available_functions[function_name] - function_args = tool_call['function']['arguments'] - if function_args: - try: - function_args = json.loads(function_args) - - if asyncio.iscoroutinefunction(function_to_call): - if function_name == "tavily_web_search": - function_response = await function_to_call(query=function_args.get("query")) - else: - function_response = await function_to_call(**function_args) - else: - function_response = function_to_call(**function_args) - - if not isinstance(function_response, str): - function_response = json.dumps(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)}", - }) - - second_response = await acompletion( - model=model_name, - stream=True, - messages=messages, - ) - - full_response = "" - async for part in second_response: - if 'choices' in part and len(part['choices']) > 0: - delta = part['choices'][0].get('delta', {}) - if 'content' in delta and delta['content'] is not None: - token = delta['content'] - await msg.stream_token(token) - full_response += token - - msg.content = full_response - await msg.update() - 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-4o-mini") - tools_enabled = (load_setting("tools_enabled") or "true").lower() == "true" - - logger.debug(f"Model name: {model_name}") - - # Load external agent settings for resume - from praisonai.ui._external_agents import load_external_agent_settings_from_chainlit, chainlit_switches - external_settings = load_external_agent_settings_from_chainlit() - - settings = cl.ChatSettings( - [ - TextInput( - id="model_name", - label="Enter the Model Name", - placeholder="e.g., gpt-4o-mini", - initial=model_name - ), - Switch( - id="tools_enabled", - label="Enable Tools (ACP, LSP, Web Search)", - initial=tools_enabled - ), - *chainlit_switches(external_settings), - ] - ) - await settings.send() - - 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): - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - metadata = {} - cl.user_session.set("metadata", metadata) - - message_history = cl.user_session.get("message_history", []) - steps = thread["steps"] - - for m in steps: - msg_type = m.get("type") - if msg_type == "user_message": - message_history.append({"role": "user", "content": m.get("output", "")}) - elif msg_type == "assistant_message": - message_history.append({"role": "assistant", "content": m.get("output", "")}) - elif msg_type == "run": - if m.get("isError"): - message_history.append({"role": "system", "content": f"Error: {m.get('output', '')}"}) - else: - logger.warning(f"Message without recognized type: {m}") - - cl.user_session.set("message_history", message_history) - - # Pre-create agent for faster first response - external_settings = _get_external_agent_settings_from_session() - _get_or_create_agent(model_name, tools_enabled, external_settings) - - image_data = metadata.get("image") - if image_data: - image = Image.open(io.BytesIO(base64.b64decode(image_data))) - cl.user_session.set("image", image) - await cl.Message(content="Previous image loaded.").send() diff --git a/src/praisonai/praisonai/ui/code.py b/src/praisonai/praisonai/ui/code.py deleted file mode 100644 index e5b178c41..000000000 --- a/src/praisonai/praisonai/ui/code.py +++ /dev/null @@ -1,781 +0,0 @@ -"""PraisonAI Code - Optimized Chainlit Application - -This is the optimized code assistant application with: -- Lazy imports for fast startup -- Agent reuse between messages (persistent agent per session) -- Default ACP/LSP tools with trust mode -- Context gathering optimization (cached per session) -- Profiling hooks (optional via PRAISON_CODE_PROFILE=1) -""" - -# Standard library imports (minimal at top level) -import os -import logging - -# Set up minimal logging first -logger = logging.getLogger(__name__) -log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO" -logger.handlers = [] -console_handler = logging.StreamHandler() -console_handler.setLevel(log_level) -console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -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_CODE_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_subprocess(): - if 'subprocess' not in _cached_modules: - import subprocess - _cached_modules['subprocess'] = subprocess - return _cached_modules['subprocess'] - -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_context_gatherer(): - if 'ContextGatherer' not in _cached_modules: - try: - _profile_start("import_context_gatherer") - from context import ContextGatherer - _cached_modules['ContextGatherer'] = ContextGatherer - _profile_end("import_context_gatherer") - except ImportError: - _cached_modules['ContextGatherer'] = None - return _cached_modules['ContextGatherer'] - -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.environ.get("PRAISONAI_CODE_REPO_PATH", os.getcwd()) - # Do not hardcode approval_mode to "auto" to respect environment configuration - 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) -import secrets -_ensure_env_loaded() -CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET") -if not CHAINLIT_AUTH_SECRET: - CHAINLIT_AUTH_SECRET = secrets.token_hex(32) - os.environ["CHAINLIT_AUTH_SECRET"] = CHAINLIT_AUTH_SECRET - logger.warning("CHAINLIT_AUTH_SECRET not set; generated a random secret for this session.") - - -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)) - -# Cached context (expensive to gather on every message) -_cached_context = {} - -def _get_cached_context(repo_path: str, force_refresh: bool = False): - """Get cached context or gather new context.""" - cache_key = repo_path - - if not force_refresh and cache_key in _cached_context: - cached = _cached_context[cache_key] - # Check if cache is still valid (5 minutes) - import time - if time.time() - cached.get('timestamp', 0) < 300: - return cached['context'], cached['token_count'], cached['context_tree'] - - ContextGatherer = _get_context_gatherer() - if ContextGatherer is None: - return "", 0, "" - - _profile_start("gather_context") - gatherer = ContextGatherer(directory=repo_path) - context, token_count, context_tree = gatherer.run() - _profile_end("gather_context") - - import time - _cached_context[cache_key] = { - 'context': context, - 'token_count': token_count, - 'context_tree': context_tree, - 'timestamp': time.time() - } - - return context, token_count, context_tree - -# External agents now handled via shared helper - -# Deferred tool loading -_tavily_client = None - -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 - -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, - "error": "Tavily API key is not set. Web search is unavailable." - }) - - response = tavily_client.search(query) - logger.debug(f"Tavily search response: {response}") - - 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 - }) - -# Authentication configuration - bind-aware auth -from ._auth import register_password_auth - -# Determine bind host from CHAINLIT_HOST env var (default: 127.0.0.1) -bind_host = os.getenv("CHAINLIT_HOST", "127.0.0.1") -register_password_auth(None, bind_host=bind_host) - -def _get_or_create_agent(model_name: str, tools_enabled: bool = True, external_agents_settings: dict = None): - """Get or create a reusable agent for the session.""" - Agent = _get_praisonai_agent() - if Agent is None: - return None - - # Default external agents settings - if external_agents_settings is None: - external_agents_settings = {} - - # Get cached agent from session - cached_agent = cl.user_session.get("_cached_agent") - cached_model = cl.user_session.get("_cached_agent_model") - cached_external = cl.user_session.get("_cached_agent_external", {}) - - # Reuse if model and external agents settings match - if (cached_agent is not None and cached_model == model_name and - cached_external == external_agents_settings): - return cached_agent - - # Create new agent with interactive tools - _profile_start("create_agent") - tools = [] - if tools_enabled: - tools = list(_get_interactive_tools()) # Copy to avoid mutation - # Add Tavily if available - if os.getenv("TAVILY_API_KEY"): - tools.append(tavily_web_search) - # Add external agent tools - from praisonai.ui._external_agents import external_agent_tools - workspace = os.environ.get("PRAISONAI_CODE_REPO_PATH", ".") - tools.extend(external_agent_tools(external_agents_settings, workspace=workspace)) - - # Build dynamic instructions based on available external agents - available_agents = [] - if external_agents_settings.get("claude_enabled"): - available_agents.append("**Claude Code**: Execute complex coding tasks with Claude Code CLI") - if external_agents_settings.get("gemini_enabled"): - available_agents.append("**Gemini CLI**: Analysis and search capabilities") - if external_agents_settings.get("codex_enabled"): - available_agents.append("**Codex CLI**: Advanced code refactoring") - if external_agents_settings.get("cursor_enabled"): - available_agents.append("**Cursor CLI**: IDE-style development tasks") - - external_capabilities = "\n- ".join([""] + available_agents) if available_agents else "" - - agent = Agent( - name="PraisonAI Code Assistant", - instructions=f"""You are a powerful AI code assistant with access to comprehensive development tools. - -Available capabilities: -- **File Operations**: Read, write, create, edit, delete files using ACP tools -- **Code Intelligence**: Find symbols, definitions, references using LSP tools -- **Command Execution**: Run shell commands safely -- **Web Search**: Search the web for documentation and solutions (if Tavily API key is set){external_capabilities} - -When helping with code: -1. Use ACP tools for safe, reviewable file modifications -2. Use LSP tools to understand code structure before making changes -3. Delegate complex tasks to specialized external agents when available -4. Always explain what you're doing and why -5. Test changes when possible - -Trust mode is enabled - tool executions are auto-approved for efficiency.""", - llm=model_name, - tools=tools if tools else None, - output="minimal", - ) - - # Cache the agent - cl.user_session.set("_cached_agent", agent) - cl.user_session.set("_cached_agent_model", model_name) - cl.user_session.set("_cached_agent_external", external_agents_settings) - _profile_end("create_agent") - - return agent - -@cl.on_chat_start -async def start(): - _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" - - # Load external agent settings with backward compatibility - from praisonai.ui._external_agents import chainlit_switches, EXTERNAL_AGENTS - external_settings = {} - - # Check for legacy claude_code_enabled setting - legacy_claude = os.getenv("PRAISONAI_CLAUDECODE_ENABLED", "false").lower() == "true" - if not legacy_claude: - legacy_claude = (load_setting("claude_code_enabled") or "false").lower() == "true" - if legacy_claude: - external_settings["claude_enabled"] = True - - # Load all external agent settings - for toggle_id in EXTERNAL_AGENTS: - if toggle_id not in external_settings: # Don't override claude if set via legacy - setting_value = load_setting(toggle_id) - external_settings[toggle_id] = setting_value and setting_value.lower() == "true" - - cl.user_session.set("model_name", model_name) - cl.user_session.set("tools_enabled", tools_enabled) - # Save external settings to session - for toggle_id, value in external_settings.items(): - cl.user_session.set(toggle_id, value) - - logger.debug(f"Model: {model_name}, Tools: {tools_enabled}, External agents: {external_settings}") - - # Pre-create agent for faster first response - _get_or_create_agent(model_name, tools_enabled, external_settings) - - settings = cl.ChatSettings( - [ - TextInput( - id="model_name", - label="Enter the Model Name", - placeholder="e.g., gpt-4o-mini", - initial=model_name - ), - Switch( - id="tools_enabled", - label="Enable Tools (ACP, LSP, Web Search)", - initial=tools_enabled - ), - *chainlit_switches(external_settings) - ] - ) - cl.user_session.set("settings", settings) - await settings.send() - - # Get context info (cached) - repo_path = os.environ.get("PRAISONAI_CODE_REPO_PATH", ".") - context, token_count, context_tree = _get_cached_context(repo_path) - - # 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") - - # Build external agents status - enabled_agents = [agent for agent, enabled in external_settings.items() if enabled] - agents_status = ", ".join(enabled_agents) if enabled_agents else "None" - - await cl.Message( - content=f"šŸš€ **PraisonAI Code Assistant Ready**\n\n" - f"**Model:** {model_name}\n" - f"**Tools:** {len(tools)} loaded ({', '.join(tool_names)})\n" - f"**External Agents:** {agents_status}\n" - f"**Trust Mode:** Enabled (auto-approve tool executions)\n" - f"**Context:** {token_count} tokens from workspace\n\n" - f"**Files in workspace:**\n```\n{context_tree[:500]}{'...' if len(context_tree) > 500 else ''}\n```" - ).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) - - # Extract external agent settings - from praisonai.ui._external_agents import EXTERNAL_AGENTS - external_settings = {toggle_id: settings.get(toggle_id, False) for toggle_id in EXTERNAL_AGENTS} - - cl.user_session.set("model_name", model_name) - cl.user_session.set("tools_enabled", tools_enabled) - # Save external settings to session - for toggle_id, value in external_settings.items(): - cl.user_session.set(toggle_id, value) - - # Invalidate cached agent if settings changed - cached_model = cl.user_session.get("_cached_agent_model") - cached_external = cl.user_session.get("_cached_agent_external", {}) - if cached_model != model_name or cached_external != external_settings: - cl.user_session.set("_cached_agent", None) - cl.user_session.set("_cached_agent_model", None) - cl.user_session.set("_cached_agent_external", None) - - save_setting("model_name", model_name) - save_setting("tools_enabled", str(tools_enabled).lower()) - # Save external agent settings - for toggle_id, enabled in external_settings.items(): - save_setting(toggle_id, str(enabled).lower()) - - # Backward compatibility - save claude_enabled as claude_code_enabled too - if "claude_enabled" in external_settings: - save_setting("claude_code_enabled", str(external_settings["claude_enabled"]).lower()) - - thread_id = cl.user_session.get("thread_id") - if thread_id: - thread = await cl_data.get_thread(thread_id) - if thread: - metadata = thread.get("metadata", {}) - if isinstance(metadata, str): - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - metadata = {} - metadata["model_name"] = model_name - metadata["tools_enabled"] = tools_enabled - # Save external agent settings to metadata - for toggle_id, enabled in external_settings.items(): - metadata[toggle_id] = 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): - _profile_start("on_message_total") - json = _get_json() - 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) - - # Get external agent settings from session - from praisonai.ui._external_agents import EXTERNAL_AGENTS - external_settings = {toggle_id: cl.user_session.get(toggle_id, False) for toggle_id in EXTERNAL_AGENTS} - message_history = cl.user_session.get("message_history", []) - - repo_path = os.environ.get("PRAISONAI_CODE_REPO_PATH", ".") - context, token_count, context_tree = _get_cached_context(repo_path) - 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) - image.load() - cl.user_session.set("image", image) - except Exception as e: - logger.error(f"Error processing image: {str(e)}") - await cl.Message(content="Error processing the image. Please try again.").send() - return - - user_message = f"""Answer the question and use tools if needed: - -{message.content} - -Current Date and Time: {now} - -Context: -{context[:8000] if len(context) > 8000 else context} -""" - - if image: - user_message = f"Image uploaded. {user_message}" - - 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, external_settings) 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() - io = _get_io() - base64 = _get_base64() - acompletion = _get_acompletion() - - _profile_start("litellm_response") - - # Build tools list - tools = [] - if os.getenv("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"] - } - } - }) - - completion_params = { - "model": model_name, - "messages": message_history, - "stream": True, - } - - if image: - buffered = io.BytesIO() - image.save(buffered, format="PNG") - img_str = base64.b64encode(buffered.getvalue()).decode() - completion_params["messages"][-1] = { - "role": "user", - "content": [ - {"type": "text", "text": user_message}, - {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_str}"}} - ] - } - completion_params["model"] = "gpt-4-vision-preview" - - if tools: - completion_params["tools"] = tools - completion_params["tool_choice"] = "auto" - - response = await acompletion(**completion_params) - - full_response = "" - msg_sent = False - - async for part in response: - if 'choices' in part and len(part['choices']) > 0: - delta = part['choices'][0].get('delta', {}) - - if 'content' in delta and delta['content'] is not None: - token = delta['content'] - if not msg_sent: - await msg.send() - msg_sent = True - await msg.stream_token(token) - full_response += token - - if not msg_sent: - await msg.send() - - message_history.append({"role": "assistant", "content": full_response}) - cl.user_session.set("message_history", message_history) - - 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-4o-mini") - tools_enabled = (load_setting("tools_enabled") or "true").lower() == "true" - - # Load external agent settings with backward compatibility - from praisonai.ui._external_agents import chainlit_switches, EXTERNAL_AGENTS - external_settings = {} - - # Check for legacy claude_code_enabled setting - legacy_claude = os.getenv("PRAISONAI_CLAUDECODE_ENABLED", "false").lower() == "true" - if not legacy_claude: - legacy_claude = (load_setting("claude_code_enabled") or "false").lower() == "true" - if legacy_claude: - external_settings["claude_enabled"] = True - - # Load all external agent settings - for toggle_id in EXTERNAL_AGENTS: - if toggle_id not in external_settings: # Don't override claude if set via legacy - setting_value = load_setting(toggle_id) - external_settings[toggle_id] = setting_value and setting_value.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-4o-mini", - initial=model_name - ), - Switch( - id="tools_enabled", - label="Enable Tools (ACP, LSP, Web Search)", - initial=tools_enabled - ), - *chainlit_switches(external_settings) - ] - ) - await settings.send() - - cl.user_session.set("thread_id", thread["id"]) - cl.user_session.set("model_name", model_name) - cl.user_session.set("tools_enabled", tools_enabled) - # Save external settings to session - for toggle_id, value in external_settings.items(): - cl.user_session.set(toggle_id, value) - - metadata = thread.get("metadata", {}) - if isinstance(metadata, str): - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - metadata = {} - cl.user_session.set("metadata", metadata) - - message_history = cl.user_session.get("message_history", []) - steps = thread["steps"] - - for m in steps: - msg_type = m.get("type") - if msg_type == "user_message": - message_history.append({"role": "user", "content": m.get("output", "")}) - elif msg_type == "assistant_message": - message_history.append({"role": "assistant", "content": m.get("output", "")}) - elif msg_type == "run": - if m.get("isError"): - message_history.append({"role": "system", "content": f"Error: {m.get('output', '')}"}) - else: - 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, external_settings) - - image_data = metadata.get("image") - if image_data: - image = Image.open(io.BytesIO(base64.b64decode(image_data))) - cl.user_session.set("image", image) - await cl.Message(content="Previous image loaded. You can continue asking questions about it, upload a new image, or just chat.").send() diff --git a/src/praisonai/praisonai/ui/colab.py b/src/praisonai/praisonai/ui/colab.py deleted file mode 100644 index 9bf236852..000000000 --- a/src/praisonai/praisonai/ui/colab.py +++ /dev/null @@ -1,474 +0,0 @@ -from praisonaiagents import Agent, Task, AgentTeam -import os -import importlib -import inspect -import yaml -import logging -from .callbacks import trigger_callback -import asyncio -import chainlit as cl -from queue import Queue - -logger = logging.getLogger(__name__) -agent_file = "agents.yaml" - -with open(agent_file, 'r') as f: - config = yaml.safe_load(f) - -topic = "get from the message content from the chainlit user message" - -# Create a message queue -message_queue = Queue() - -async def process_message_queue(): - """Process messages in the queue and send them to Chainlit""" - while True: - try: - if not message_queue.empty(): - msg_data = message_queue.get() - await cl.Message(**msg_data).send() - await asyncio.sleep(0.1) # Small delay to prevent busy waiting - except Exception as e: - logger.error(f"Error processing message queue: {e}") - -def load_tools_from_tools_py(): - """ - Imports and returns all contents from tools.py file. - Also adds the tools to the global namespace. - - Returns: - list: A list of callable functions with proper formatting - """ - tools_list = [] - try: - # Try to import tools.py from current directory - spec = importlib.util.spec_from_file_location("tools", "tools.py") - logger.info(f"Spec: {spec}") - if spec is None: - logger.info("tools.py not found in current directory") - return tools_list - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Get all module attributes except private ones and classes - for name, obj in inspect.getmembers(module): - if (not name.startswith('_') and - callable(obj) and - not inspect.isclass(obj)): - # Add the function to global namespace - globals()[name] = obj - # Format the tool as an OpenAI function - tool = { - "type": "function", - "function": { - "name": name, - "description": obj.__doc__ or f"Function to {name.replace('_', ' ')}", - "parameters": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The search query to look up information about" - } - }, - "required": ["query"] - } - } - } - # Add formatted tool to tools list - tools_list.append(tool) - logger.info(f"Loaded and globalized tool function: {name}") - - logger.info(f"Loaded {len(tools_list)} tool functions from tools.py") - logger.info(f"Tools list: {tools_list}") - - except Exception as e: - logger.warning(f"Error loading tools from tools.py: {e}") - - return tools_list - -async def step_callback(step_details): - """Callback for agent steps""" - logger.info(f"[CALLBACK DEBUG] Step callback triggered with details: {step_details}") - try: - # Queue message for agent response - if step_details.get("response"): - message_queue.put({ - "content": f"Agent Response: {step_details.get('response')}", - "author": step_details.get("agent_name", "Agent") - }) - logger.info("[CALLBACK DEBUG] Queued agent response message") - - # Queue message for tool usage - if step_details.get("tool_name"): - message_queue.put({ - "content": f"šŸ› ļø Using tool: {step_details.get('tool_name')}", - "author": "System" - }) - logger.info("[CALLBACK DEBUG] Queued tool usage message") - - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in step callback: {str(e)}", exc_info=True) - -async def task_callback(task_output): - """Callback for task completion""" - logger.info(f"[CALLBACK DEBUG] Task callback triggered with output: {task_output}") - try: - # Create message content - if hasattr(task_output, 'raw'): - content = task_output.raw - elif hasattr(task_output, 'content'): - content = task_output.content - else: - content = str(task_output) - - # Queue the message - message_queue.put({ - "content": f"Task Output: {content}", - "author": "Task" - }) - logger.info("[CALLBACK DEBUG] Queued task completion message") - - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in task callback: {str(e)}", exc_info=True) - -async def step_callback_wrapper(step_details): - logger.info(f"[CALLBACK DEBUG] Step callback wrapper triggered with details: {step_details}") - try: - # Check if we have a Chainlit context - if not cl.context.context_var.get(): - logger.warning("[CALLBACK DEBUG] No Chainlit context available in wrapper") - return - logger.info("[CALLBACK DEBUG] Chainlit context found in wrapper") - - # Create a message for the agent's response - if step_details.get("response"): - logger.info(f"[CALLBACK DEBUG] Sending agent response from wrapper: {step_details.get('response')}") - try: - await cl.Message( - content=f"{role_name}: {step_details.get('response')}", - author=role_name, - ).send() - logger.info("[CALLBACK DEBUG] Successfully sent agent response message from wrapper") - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error sending agent response message from wrapper: {str(e)}") - - # Create a message for any tool usage - if step_details.get("tool_name"): - logger.info(f"[CALLBACK DEBUG] Sending tool usage from wrapper: {step_details.get('tool_name')}") - try: - await cl.Message( - content=f"šŸ› ļø {role_name} is using tool: {step_details.get('tool_name')}", - author="System", - ).send() - logger.info("[CALLBACK DEBUG] Successfully sent tool usage message from wrapper") - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error sending tool usage message from wrapper: {str(e)}") - - # Create a message for any thoughts or reasoning - if step_details.get("thought"): - logger.info(f"[CALLBACK DEBUG] Sending thought from wrapper: {step_details.get('thought')}") - try: - await cl.Message( - content=f"šŸ’­ {role_name}'s thought: {step_details.get('thought')}", - author=role_name, - ).send() - logger.info("[CALLBACK DEBUG] Successfully sent thought message from wrapper") - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error sending thought message from wrapper: {str(e)}") - - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in step callback wrapper: {str(e)}", exc_info=True) - try: - await cl.Message( - content=f"Error in step callback: {str(e)}", - author="System", - ).send() - except Exception as send_error: - logger.error(f"[CALLBACK DEBUG] Error sending error message: {str(send_error)}") - -async def task_callback_wrapper(task_output): - logger.info(f"[CALLBACK DEBUG] Task callback wrapper triggered with output type: {type(task_output)}") - try: - # Check if we have a Chainlit context - if not cl.context.context_var.get(): - logger.warning("[CALLBACK DEBUG] No Chainlit context available in task wrapper") - return - logger.info("[CALLBACK DEBUG] Chainlit context found in task wrapper") - - # Create a message for task completion - if hasattr(task_output, 'raw'): - content = task_output.raw - logger.info("[CALLBACK DEBUG] Using raw output") - elif hasattr(task_output, 'content'): - content = task_output.content - logger.info("[CALLBACK DEBUG] Using content output") - else: - content = str(task_output) - logger.info("[CALLBACK DEBUG] Using string representation of output") - - logger.info(f"[CALLBACK DEBUG] Sending task completion message from wrapper: {content[:100]}...") - try: - await cl.Message( - content=f"āœ… {role_name} completed task:\n{content}", - author=role_name, - ).send() - logger.info("[CALLBACK DEBUG] Successfully sent task completion message from wrapper") - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error sending task completion message from wrapper: {str(e)}") - - # If there are any additional task details - if hasattr(task_output, 'details'): - logger.info("[CALLBACK DEBUG] Task has additional details") - try: - await cl.Message( - content=f"šŸ“ Additional details:\n{task_output.details}", - author=role_name, - ).send() - logger.info("[CALLBACK DEBUG] Successfully sent additional details message") - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error sending additional details message: {str(e)}") - - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in task callback wrapper: {str(e)}", exc_info=True) - try: - await cl.Message( - content=f"Error in task callback: {str(e)}", - author="System", - ).send() - except Exception as send_error: - logger.error(f"[CALLBACK DEBUG] Error sending error message: {str(send_error)}") - -def sync_task_callback_wrapper(task_output): - logger.info("[CALLBACK DEBUG] Sync task callback wrapper triggered") - try: - # Create a new event loop for this thread if there isn't one - try: - loop = asyncio.get_event_loop() - logger.info("[CALLBACK DEBUG] Got existing event loop") - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - logger.info("[CALLBACK DEBUG] Created new event loop") - - if loop.is_running(): - # If loop is running, schedule the callback - logger.info("[CALLBACK DEBUG] Loop is running, scheduling callback") - asyncio.run_coroutine_threadsafe( - task_callback_wrapper(task_output), - loop - ) - else: - # If loop is not running, run it directly - logger.info("[CALLBACK DEBUG] Loop is not running, running callback directly") - loop.run_until_complete(task_callback_wrapper(task_output)) - - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in sync task callback: {str(e)}", exc_info=True) - -def sync_step_callback_wrapper(step_details): - logger.info("[CALLBACK DEBUG] Sync step callback wrapper triggered") - try: - # Create a new event loop for this thread if there isn't one - try: - loop = asyncio.get_event_loop() - logger.info("[CALLBACK DEBUG] Got existing event loop") - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - logger.info("[CALLBACK DEBUG] Created new event loop") - - if loop.is_running(): - # If loop is running, schedule the callback - logger.info("[CALLBACK DEBUG] Loop is running, scheduling callback") - asyncio.run_coroutine_threadsafe( - step_callback_wrapper(step_details), - loop - ) - else: - # If loop is not running, run it directly - logger.info("[CALLBACK DEBUG] Loop is not running, running callback directly") - loop.run_until_complete(step_callback_wrapper(step_details)) - - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in sync step callback: {str(e)}", exc_info=True) - -async def ui_run_praisonai(config, topic, tools_dict): - """Run PraisonAI with the given configuration and topic.""" - logger = logging.getLogger(__name__) - logger.setLevel(logging.DEBUG) - agents = {} - tasks = [] - tasks_dict = {} - - try: - # Start message queue processor - queue_processor = asyncio.create_task(process_message_queue()) - - # Create agents for each role - for role, details in config['roles'].items(): - # Format the role name and other details - role_name = details.get('name', role).format(topic=topic) - role_filled = details.get('role', role).format(topic=topic) - goal_filled = details['goal'].format(topic=topic) - backstory_filled = details['backstory'].format(topic=topic) - - # Test message to verify Chainlit is working - await cl.Message( - content=f"[DEBUG] Creating agent: {role_name}", - author="System" - ).send() - - # Create a sync wrapper for the step callback - def step_callback_sync(step_details): - try: - # Create a new event loop for this thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Add agent name to step details - step_details["agent_name"] = role_name - - # Run the callback - loop.run_until_complete(step_callback(step_details)) - loop.close() - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in step callback: {str(e)}", exc_info=True) - - agent = Agent( - name=role_name, - role=role_filled, - goal=goal_filled, - backstory=backstory_filled, - llm=details.get('llm', 'gpt-5-nano'), - verbose=True, - allow_delegation=details.get('allow_delegation', False), - max_iter=details.get('max_iter', 15), - max_rpm=details.get('max_rpm'), - max_execution_time=details.get('max_execution_time'), - cache=details.get('cache', True), - step_callback=step_callback_sync - ) - agents[role] = agent - - # Create tasks for each role - for role, details in config['roles'].items(): - agent = agents[role] - tools_list = [] - - # Get tools for this role - for tool_name in details.get('tools', []): - if tool_name in tools_dict: - tool_func = tools_dict[tool_name] - tools_list.append(tool_func) - - # Create tasks for the agent - for task_name, task_details in details.get('tasks', {}).items(): - description_filled = task_details['description'].format(topic=topic) - expected_output_filled = task_details['expected_output'].format(topic=topic) - - # Test message to verify task creation - await cl.Message( - content=f"[DEBUG] Created task: {task_name} for agent {role_name}", - author="System" - ).send() - - # Create a sync wrapper for the task callback - def task_callback_sync(task_output): - try: - # Create a new event loop for this thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Run the callback - loop.run_until_complete(task_callback(task_output)) - loop.close() - except Exception as e: - logger.error(f"[CALLBACK DEBUG] Error in task callback: {str(e)}", exc_info=True) - - task = Task( - description=description_filled, - expected_output=expected_output_filled, - agent=agent, - tools=tools_list, - async_execution=True, - context=[], - config=task_details.get('config', {}), - output_json=task_details.get('output_json'), - output_pydantic=task_details.get('output_pydantic'), - output_file=task_details.get('output_file', ""), - callback=task_callback_sync, - create_directory=task_details.get('create_directory', False) - ) - - tasks.append(task) - tasks_dict[task_name] = task - - # Set up task contexts - for role, details in config['roles'].items(): - for task_name, task_details in details.get('tasks', {}).items(): - task = tasks_dict[task_name] - context_tasks = [tasks_dict[ctx] for ctx in task_details.get('context', []) - if ctx in tasks_dict] - task.context = context_tasks - - # Send the start message - await cl.Message( - content="Starting PraisonAI agents execution...", - author="System" - ).send() - - # Create and run the PraisonAI agents - if config.get('process') == 'hierarchical': - crew = AgentTeam( - agents=list(agents.values()), - tasks=tasks, - verbose=True, - process="hierarchical", - manager_llm=config.get('manager_llm', 'gpt-5-nano') - ) - else: - crew = AgentTeam( - agents=list(agents.values()), - tasks=tasks, - verbose=2 - ) - - # Store the crew in the user session - cl.user_session.set("crew", crew) - - # Run the agents in a separate thread - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, crew.start) - - logger.debug(f"[CALLBACK DEBUG] Result: {response}") - - # Convert response to string if it's not already - if hasattr(response, 'raw'): - result = response.raw - elif hasattr(response, 'content'): - result = response.content - else: - result = str(response) - - # Send the completion message - await cl.Message( - content="PraisonAI agents execution completed.", - author="System" - ).send() - - # After getting the response, wait a bit for remaining messages - await asyncio.sleep(1) # Give time for final messages to be processed - queue_processor.cancel() # Stop the queue processor - - return result - - except Exception as e: - error_msg = f"Error in ui_run_praisonai: {str(e)}" - logger.error(error_msg, exc_info=True) - await cl.Message( - content=error_msg, - author="System" - ).send() - raise \ No newline at end of file diff --git a/src/praisonai/praisonai/ui/colab_chainlit.py b/src/praisonai/praisonai/ui/colab_chainlit.py deleted file mode 100644 index bf3fee0fc..000000000 --- a/src/praisonai/praisonai/ui/colab_chainlit.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import logging -from dotenv import load_dotenv -import chainlit as cl -from chainlit.types import ThreadDict -import yaml -import sys -import os -from datetime import datetime - -# Add the parent directory to sys.path to allow imports -sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - -from praisonai.ui.colab import ui_run_praisonai, load_tools_from_tools_py -from praisonai.ui.callbacks import callback, trigger_callback - -# Load environment variables -load_dotenv() - -# Set up logging -logger = logging.getLogger(__name__) -log_level = os.getenv("LOGLEVEL", "INFO").upper() -logger.setLevel(log_level) - -# Load agent configuration -agent_file = "agents.yaml" -with open(agent_file, 'r') as f: - config = yaml.safe_load(f) - -# Load tools -tools_dict = load_tools_from_tools_py() - -@cl.on_message -async def main(message: cl.Message): - """Main message handler for Chainlit""" - try: - logger.info(f"Processing message: {message.content}") - await cl.Message( - content=f"šŸ”„ Processing your request about: {message.content}...", - author="System" - ).send() - - await cl.Message( - content="Using Running PraisonAI Agents...", - author="System" - ).send() - - # Run PraisonAI with the message content as the topic - result = await ui_run_praisonai(config, message.content, tools_dict) - - # Send the final result - await cl.Message( - content=result, - author="System" - ).send() - - except Exception as e: - error_msg = f"Error running PraisonAI agents: {str(e)}" - logger.error(error_msg, exc_info=True) - await cl.Message( - content=error_msg, - author="System" - ).send() - -@cl.on_chat_start -async def start(): - """Handler for chat start""" - await cl.Message( - content="šŸ‘‹ Welcome! I'm your AI assistant. What would you like to work on?", - author="System" - ).send() - -# Authentication setup (optional) - bind-aware auth -if not os.getenv("CHAINLIT_AUTH_SECRET"): - import secrets - os.environ["CHAINLIT_AUTH_SECRET"] = secrets.token_hex(32) - logger.warning("CHAINLIT_AUTH_SECRET not set; generated a random secret for this session.") - -from ._auth import register_password_auth - -# Determine bind host from CHAINLIT_HOST env var (default: 127.0.0.1) -bind_host = os.getenv("CHAINLIT_HOST", "127.0.0.1") -register_password_auth(None, bind_host=bind_host) \ No newline at end of file diff --git a/src/praisonai/praisonai/ui/db.py b/src/praisonai/praisonai/ui/db.py deleted file mode 100644 index fda098072..000000000 --- a/src/praisonai/praisonai/ui/db.py +++ /dev/null @@ -1,294 +0,0 @@ -import os -import sqlite3 -import asyncio -import shutil -import logging -from sqlalchemy import text -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import sessionmaker -from sql_alchemy import SQLAlchemyDataLayer -import chainlit.data as cl_data -from chainlit.types import ThreadDict -from database_config import get_database_url_with_sqlite_override -from praisonai.ui.chainlit_compat import create_local_storage_client - -def ensure_directories(): - """Ensure required directories exist""" - if "CHAINLIT_APP_ROOT" not in os.environ: - chainlit_root = os.path.join(os.path.expanduser("~"), ".praison") - os.environ["CHAINLIT_APP_ROOT"] = chainlit_root - else: - chainlit_root = os.environ["CHAINLIT_APP_ROOT"] - - os.makedirs(chainlit_root, exist_ok=True) - os.makedirs(os.path.join(chainlit_root, ".files"), exist_ok=True) - - # Copy public folder and chainlit.md if they don't exist - public_folder = os.path.join(os.path.dirname(__file__), "public") - config_folder = os.path.join(os.path.dirname(__file__), "config") - - # Copy public folder - if not os.path.exists(os.path.join(chainlit_root, "public")): - if os.path.exists(public_folder): - shutil.copytree(public_folder, os.path.join(chainlit_root, "public"), dirs_exist_ok=True) - logging.info("Public folder copied successfully!") - else: - logging.info("Public folder not found in the package.") - - # Copy all files from config folder to root if translations doesn't exist - if not os.path.exists(os.path.join(chainlit_root, "translations")): - os.makedirs(os.path.join(chainlit_root, "translations"), exist_ok=True) - - if os.path.exists(config_folder): - for item in os.listdir(config_folder): - src_path = os.path.join(config_folder, item) - dst_path = os.path.join(chainlit_root, item) - if os.path.isfile(src_path): - shutil.copy2(src_path, dst_path) - logging.info(f"File {item} copied to root successfully!") - elif os.path.isdir(src_path): - if os.path.exists(dst_path): - shutil.rmtree(dst_path) - shutil.copytree(src_path, dst_path) - logging.info(f"Directory {item} copied to root successfully!") - else: - logging.info("Config folder not found in the package.") - -# Create directories at module import time -ensure_directories() - -class DatabaseManager(SQLAlchemyDataLayer): - def __init__(self): - # Check FORCE_SQLITE flag to bypass external database detection - self.database_url = get_database_url_with_sqlite_override() - - if self.database_url: - self.conninfo = self.database_url - else: - chainlit_root = os.environ["CHAINLIT_APP_ROOT"] # Now using CHAINLIT_APP_ROOT - self.db_path = os.path.join(chainlit_root, "database.sqlite") - self.conninfo = f"sqlite+aiosqlite:///{self.db_path}" - - # Create local file storage client for element persistence - storage_client = create_local_storage_client() - - # Initialize SQLAlchemyDataLayer with the connection info and storage client - super().__init__(conninfo=self.conninfo, storage_provider=storage_client) - - async def create_schema_async(self): - """Create the database schema for PostgreSQL""" - if not self.database_url: - return - engine = create_async_engine(self.database_url, echo=False) - async with engine.begin() as conn: - await conn.execute(text(''' - CREATE TABLE IF NOT EXISTS users ( - "id" TEXT PRIMARY KEY, - "identifier" TEXT NOT NULL UNIQUE, - "meta" TEXT NOT NULL DEFAULT '{}', - "createdAt" TEXT - ); - ''')) - await conn.execute(text(''' - CREATE TABLE IF NOT EXISTS threads ( - "id" TEXT PRIMARY KEY, - "createdAt" TEXT, - "name" TEXT, - "userId" TEXT, - "userIdentifier" TEXT, - "tags" TEXT DEFAULT '[]', - "meta" TEXT NOT NULL DEFAULT '{}', - FOREIGN KEY ("userId") REFERENCES users("id") ON DELETE CASCADE - ); - ''')) - await conn.execute(text(''' - CREATE TABLE IF NOT EXISTS steps ( - "id" TEXT PRIMARY KEY, - "name" TEXT NOT NULL, - "type" TEXT NOT NULL, - "threadId" TEXT NOT NULL, - "parentId" TEXT, - "disableFeedback" BOOLEAN NOT NULL DEFAULT FALSE, - "streaming" BOOLEAN NOT NULL DEFAULT FALSE, - "waitForAnswer" BOOLEAN DEFAULT FALSE, - "isError" BOOLEAN NOT NULL DEFAULT FALSE, - "meta" TEXT DEFAULT '{}', - "tags" TEXT DEFAULT '[]', - "input" TEXT, - "output" TEXT, - "createdAt" TEXT, - "startTime" TEXT, - "endTime" TEXT, - "generation" TEXT, - "showInput" TEXT, - "language" TEXT, - "indent" INT, - FOREIGN KEY ("threadId") REFERENCES threads("id") ON DELETE CASCADE - ); - ''')) - await conn.execute(text(''' - CREATE TABLE IF NOT EXISTS elements ( - "id" TEXT PRIMARY KEY, - "threadId" TEXT, - "type" TEXT, - "url" TEXT, - "chainlitKey" TEXT, - "name" TEXT NOT NULL, - "display" TEXT, - "objectKey" TEXT, - "size" TEXT, - "page" INT, - "language" TEXT, - "forId" TEXT, - "mime" TEXT, - FOREIGN KEY ("threadId") REFERENCES threads("id") ON DELETE CASCADE - ); - ''')) - await conn.execute(text(''' - CREATE TABLE IF NOT EXISTS feedbacks ( - "id" TEXT PRIMARY KEY, - "forId" TEXT NOT NULL, - "value" INT NOT NULL, - "threadId" TEXT, - "comment" TEXT - ); - ''')) - await conn.execute(text(''' - CREATE TABLE IF NOT EXISTS settings ( - "id" SERIAL PRIMARY KEY, - "key" TEXT UNIQUE, - "value" TEXT - ); - ''')) - await engine.dispose() - - def create_schema_sqlite(self): - """Create the database schema for SQLite""" - chainlit_root = os.environ["CHAINLIT_APP_ROOT"] # Now using CHAINLIT_APP_ROOT - self.db_path = os.path.join(chainlit_root, "database.sqlite") - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - identifier TEXT NOT NULL UNIQUE, - meta TEXT NOT NULL DEFAULT '{}', - createdAt TEXT - ); - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS threads ( - id TEXT PRIMARY KEY, - createdAt TEXT, - name TEXT, - userId TEXT, - userIdentifier TEXT, - tags TEXT DEFAULT '[]', - meta TEXT NOT NULL DEFAULT '{}', - FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE - ); - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS steps ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - type TEXT NOT NULL, - threadId TEXT NOT NULL, - parentId TEXT, - disableFeedback BOOLEAN NOT NULL DEFAULT 0, - streaming BOOLEAN NOT NULL DEFAULT 0, - waitForAnswer BOOLEAN DEFAULT 0, - isError BOOLEAN NOT NULL DEFAULT 0, - meta TEXT DEFAULT '{}', - tags TEXT DEFAULT '[]', - input TEXT, - output TEXT, - createdAt TEXT, - startTime TEXT, - endTime TEXT, - generation TEXT, - showInput TEXT, - language TEXT, - indent INT, - FOREIGN KEY (threadId) REFERENCES threads(id) ON DELETE CASCADE - ); - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS elements ( - id TEXT PRIMARY KEY, - threadId TEXT, - type TEXT, - url TEXT, - chainlitKey TEXT, - name TEXT NOT NULL, - display TEXT, - objectKey TEXT, - size TEXT, - page INT, - language TEXT, - forId TEXT, - mime TEXT, - FOREIGN KEY (threadId) REFERENCES threads(id) ON DELETE CASCADE - ); - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS feedbacks ( - id TEXT PRIMARY KEY, - forId TEXT NOT NULL, - value INT NOT NULL, - threadId TEXT, - comment TEXT - ); - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT UNIQUE, - value TEXT - ); - ''') - conn.commit() - conn.close() - - def initialize(self): - """Initialize the database with schema based on the configuration""" - if self.database_url: - asyncio.run(self.create_schema_async()) - else: - self.create_schema_sqlite() - - async def save_setting(self, key: str, value: str): - """Save a setting to the database""" - if self.database_url: - async with self.engine.begin() as conn: - await conn.execute(text(""" - INSERT INTO settings ("key", "value") VALUES (:key, :value) - ON CONFLICT ("key") DO UPDATE SET "value" = EXCLUDED."value" - """), {"key": key, "value": value}) - else: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute( - """ - INSERT OR REPLACE INTO settings (id, key, value) - VALUES ((SELECT id FROM settings WHERE key = ?), ?, ?) - """, - (key, key, value), - ) - conn.commit() - conn.close() - - async def load_setting(self, key: str) -> str: - """Load a setting from the database""" - if self.database_url: - async with self.engine.connect() as conn: - result = await conn.execute(text('SELECT "value" FROM settings WHERE "key" = :key'), {"key": key}) - row = result.fetchone() - return row[0] if row else None - else: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT value FROM settings WHERE key = ?', (key,)) - result = cursor.fetchone() - conn.close() - return result[0] if result else None diff --git a/src/praisonai/praisonai/ui/realtime.py b/src/praisonai/praisonai/ui/realtime.py deleted file mode 100644 index 3ce5264d7..000000000 --- a/src/praisonai/praisonai/ui/realtime.py +++ /dev/null @@ -1,489 +0,0 @@ -import os -import asyncio -import sqlite3 -from datetime import datetime -from uuid import uuid4 - -from openai import AsyncOpenAI -import chainlit as cl -from chainlit.input_widget import TextInput -from chainlit.types import ThreadDict - -from realtimeclient import RealtimeClient -from realtimeclient.tools import tools -from sql_alchemy import SQLAlchemyDataLayer -import chainlit.data as cl_data -from literalai.helper import utc_now -import json -import logging -import importlib.util -from importlib import import_module -from pathlib import Path - -# Set up logging -logger = logging.getLogger(__name__) -log_level = os.getenv("LOGLEVEL", "INFO").upper() -logger.handlers = [] - -# Set up logging to console -console_handler = logging.StreamHandler() -console_handler.setLevel(log_level) -console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -console_handler.setFormatter(console_formatter) -logger.addHandler(console_handler) - -# Set the logging level for the logger -logger.setLevel(log_level) - -# Set up CHAINLIT_AUTH_SECRET -import secrets -CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET") -if not CHAINLIT_AUTH_SECRET: - CHAINLIT_AUTH_SECRET = secrets.token_hex(32) - os.environ["CHAINLIT_AUTH_SECRET"] = CHAINLIT_AUTH_SECRET - logger.warning("CHAINLIT_AUTH_SECRET not set; generated a random secret for this session.") - - -# Database path -DB_PATH = os.path.expanduser("~/.praison/database.sqlite") - -def initialize_db(): - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY, - identifier TEXT NOT NULL UNIQUE, - metadata JSONB NOT NULL, - createdAt TEXT - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS threads ( - id UUID PRIMARY KEY, - createdAt TEXT, - name TEXT, - userId UUID, - userIdentifier TEXT, - tags TEXT[], - metadata JSONB NOT NULL DEFAULT '{}', - FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS steps ( - id UUID PRIMARY KEY, - name TEXT NOT NULL, - type TEXT NOT NULL, - threadId UUID NOT NULL, - parentId UUID, - disableFeedback BOOLEAN NOT NULL DEFAULT 0, - streaming BOOLEAN NOT NULL DEFAULT 0, - waitForAnswer BOOLEAN DEFAULT 0, - isError BOOLEAN NOT NULL DEFAULT 0, - metadata JSONB DEFAULT '{}', - tags TEXT[], - input TEXT, - output TEXT, - createdAt TEXT, - start TEXT, - end TEXT, - generation JSONB, - showInput TEXT, - language TEXT, - indent INT, - FOREIGN KEY (threadId) REFERENCES threads (id) ON DELETE CASCADE - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS elements ( - id UUID PRIMARY KEY, - threadId UUID, - type TEXT, - url TEXT, - chainlitKey TEXT, - name TEXT NOT NULL, - display TEXT, - objectKey TEXT, - size TEXT, - page INT, - language TEXT, - forId UUID, - mime TEXT, - FOREIGN KEY (threadId) REFERENCES threads (id) ON DELETE CASCADE - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS feedbacks ( - id UUID PRIMARY KEY, - forId UUID NOT NULL, - value INT NOT NULL, - threadId UUID, - comment TEXT - ) - ''') - cursor.execute(''' - CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT UNIQUE, - value TEXT - ) - ''') - conn.commit() - conn.close() - -def save_setting(key: str, value: str): - """Saves a setting to the database.""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute( - """ - INSERT OR REPLACE INTO settings (id, key, value) - VALUES ((SELECT id FROM settings WHERE key = ?), ?, ?) - """, - (key, key, value), - ) - conn.commit() - conn.close() - -def load_setting(key: str) -> str: - """Loads a setting from the database.""" - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute('SELECT value FROM settings WHERE key = ?', (key,)) - result = cursor.fetchone() - conn.close() - return result[0] if result else None - -# Initialize the database -initialize_db() - -# Set up SQLAlchemy data layer -cl_data._data_layer = SQLAlchemyDataLayer(conninfo=f"sqlite+aiosqlite:///{DB_PATH}") - -client = AsyncOpenAI() - -# Try to import tools from the root directory -tools_path = os.path.join(os.getcwd(), 'tools.py') -logger.info(f"Tools path: {tools_path}") - -def import_tools_from_file(file_path): - spec = importlib.util.spec_from_file_location("custom_tools", file_path) - custom_tools_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(custom_tools_module) - logger.debug(f"Imported tools from {file_path}") - logger.debug(f"Tools: {custom_tools_module}") - return custom_tools_module - -try: - # Security: Require explicit opt-in for local tools loading - if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true": - logger.info("Local tools loading disabled. Set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable.") - custom_tools_module = None - elif os.path.exists(tools_path): - # tools.py exists in the root directory, import from file - custom_tools_module = import_tools_from_file(tools_path) - logger.info("Successfully imported custom tools from root tools.py") - else: - logger.info("No custom tools.py file found in the root directory") - custom_tools_module = None - - if custom_tools_module: - # Update the tools list with custom tools - if hasattr(custom_tools_module, 'tools') and isinstance(custom_tools_module.tools, list): - # Only add tools that have proper function definitions - for tool in custom_tools_module.tools: - if isinstance(tool, tuple) and len(tool) == 2: - tool_def, handler = tool - if isinstance(tool_def, dict) and "type" in tool_def and tool_def["type"] == "function": - # Convert class/function to proper tool definition - if "function" in tool_def: - func = tool_def["function"] - if hasattr(func, "__name__"): - tool_def = { - "name": func.__name__, - "description": func.__doc__ or f"Execute {func.__name__}", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - tools.append((tool_def, handler)) - else: - # Tool definition is already properly formatted - tools.append(tool) - else: - # Process individual functions/classes - for name, obj in custom_tools_module.__dict__.items(): - if callable(obj) and not name.startswith("__"): - tool_def = { - "name": name, - "description": obj.__doc__ or f"Execute {name}", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - tools.append((tool_def, obj)) - -except Exception as e: - logger.warning(f"Error importing custom tools: {str(e)}. Continuing without custom tools.") - -@cl.on_chat_start -async def start(): - initialize_db() - model_name = os.getenv("OPENAI_MODEL_NAME") or os.getenv("MODEL_NAME", "gpt-5-nano-realtime-preview-2024-12-17") - cl.user_session.set("model_name", model_name) - cl.user_session.set("message_history", []) # Initialize message history - 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-realtime-preview-2024-12-17", - # initial=model_name - # ) - # ] - # ) - # cl.user_session.set("settings", settings) - # await settings.send() - await cl.Message( - content="Welcome to the PraisonAI realtime. Press `P` to talk!" - ).send() - await setup_openai_realtime() - -@cl.on_message -async def on_message(message: cl.Message): - openai_realtime: RealtimeClient = cl.user_session.get("openai_realtime") - message_history = cl.user_session.get("message_history", []) - - if openai_realtime and openai_realtime.is_connected(): - current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - prompt = f"Current time Just for reference: {current_date}\n\n{message.content}" - - # Add user message to history - message_history.append({"role": "user", "content": prompt}) - cl.user_session.set("message_history", message_history) - - await openai_realtime.send_user_message_content([{ "type": 'input_text', "text": message.content }]) - else: - await cl.Message(content="Please activate voice mode before sending messages!").send() - -async def setup_openai_realtime(): - """Instantiate and configure the OpenAI Realtime Client""" - openai_realtime = RealtimeClient(api_key=os.getenv("OPENAI_API_KEY")) - cl.user_session.set("track_id", str(uuid4())) - - async def handle_conversation_updated(event): - item = event.get("item") - delta = event.get("delta") - """Currently used to stream audio back to the client.""" - if delta: - if 'audio' in delta: - audio = delta['audio'] # Int16Array, audio added - await cl.context.emitter.send_audio_chunk(cl.OutputAudioChunk(mimeType="pcm16", data=audio, track=cl.user_session.get("track_id"))) - if 'transcript' in delta: - transcript = delta['transcript'] # string, transcript added - logger.debug(f"Transcript delta: {transcript}") - if 'text' in delta: - text = delta['text'] # string, text added - logger.debug(f"Text delta: {text}") - if 'arguments' in delta: - arguments = delta['arguments'] # string, function arguments added - logger.debug(f"Function arguments delta: {arguments}") - - async def handle_item_completed(event): - """Used to populate the chat context with transcription once an item is completed.""" - try: - item = event.get("item") - logger.debug(f"Item completed: {json.dumps(item, indent=2, default=str)}") - await openai_realtime._send_chainlit_message(item) - - # Add assistant message to history - message_history = cl.user_session.get("message_history", []) - content = item.get("formatted", {}).get("text", "") or item.get("formatted", {}).get("transcript", "") - if content: - message_history.append({"role": "assistant", "content": content}) - cl.user_session.set("message_history", message_history) - except Exception as e: - error_message = f"Error in handle_item_completed: {str(e)}" - logger.error(error_message) - debug_item = json.dumps(item, indent=2, default=str) - logger.error(f"Item causing error: {debug_item}") - - async def handle_conversation_interrupt(event): - """Used to cancel the client previous audio playback.""" - cl.user_session.set("track_id", str(uuid4())) - await cl.context.emitter.send_audio_interrupt() - - async def handle_error(event): - logger.error(event) - await cl.Message(content=f"Error: {event}", author="System").send() - - # Register event handlers - openai_realtime.on('conversation.updated', handle_conversation_updated) - openai_realtime.on('conversation.item.completed', handle_item_completed) - openai_realtime.on('conversation.interrupted', handle_conversation_interrupt) - openai_realtime.on('error', handle_error) - - cl.user_session.set("openai_realtime", openai_realtime) - - # Filter out invalid tools and add valid ones - valid_tools = [] - for tool_def, tool_handler in tools: - try: - if isinstance(tool_def, dict) and "name" in tool_def: - valid_tools.append((tool_def, tool_handler)) - else: - logger.warning(f"Skipping invalid tool definition: {tool_def}") - except Exception as e: - logger.warning(f"Error processing tool: {e}") - - if valid_tools: - coros = [openai_realtime.add_tool(tool_def, tool_handler) for tool_def, tool_handler in valid_tools] - await asyncio.gather(*coros) - else: - logger.warning("No valid tools found to add") - -@cl.on_settings_update -async def setup_agent(settings): - logger.debug(settings) - cl.user_session.set("settings", settings) - model_name = settings["model_name"] - cl.user_session.set("model_name", model_name) - - # Save in settings table - save_setting("model_name", model_name) - - # Save in thread metadata - thread_id = cl.user_session.get("thread_id") - if thread_id: - thread = await cl_data._data_layer.get_thread(thread_id) - if thread: - metadata = thread.get("metadata", {}) - if isinstance(metadata, str): - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - metadata = {} - - metadata["model_name"] = model_name - - # Always store metadata as a dictionary - await cl_data._data_layer.update_thread(thread_id, metadata=metadata) - - # Update the user session with the new metadata - cl.user_session.set("metadata", metadata) - -@cl.on_audio_start -async def on_audio_start(): - try: - openai_realtime: RealtimeClient = cl.user_session.get("openai_realtime") - if not openai_realtime: - await setup_openai_realtime() - openai_realtime = cl.user_session.get("openai_realtime") - - if not openai_realtime.is_connected(): - model_name = cl.user_session.get("model_name") or os.getenv("OPENAI_MODEL_NAME") or os.getenv("MODEL_NAME", "gpt-5-nano-realtime-preview-2024-12-17") - await openai_realtime.connect(model_name) - - logger.info("Connected to OpenAI realtime") - return True - except Exception as e: - error_msg = f"Failed to connect to OpenAI realtime: {str(e)}" - logger.error(error_msg) - await cl.ErrorMessage(content=error_msg).send() - return False - -@cl.on_audio_chunk -async def on_audio_chunk(chunk: cl.InputAudioChunk): - openai_realtime: RealtimeClient = cl.user_session.get("openai_realtime") - - if not openai_realtime: - logger.debug("No realtime client available") - return - - if openai_realtime.is_connected(): - try: - success = await openai_realtime.append_input_audio(chunk.data) - if not success: - logger.debug("Failed to append audio data - connection may be lost") - except Exception as e: - logger.debug(f"Error processing audio chunk: {e}") - # Optionally try to reconnect here if needed - else: - logger.debug("RealtimeClient is not connected - audio chunk ignored") - -@cl.on_audio_end -@cl.on_chat_end -@cl.on_stop -async def on_end(): - openai_realtime: RealtimeClient = cl.user_session.get("openai_realtime") - if openai_realtime and openai_realtime.is_connected(): - await openai_realtime.disconnect() - -# Authentication configuration - bind-aware auth -from ._auth import register_password_auth - -# Determine bind host from CHAINLIT_HOST env var (default: 127.0.0.1) -bind_host = os.getenv("CHAINLIT_HOST", "127.0.0.1") -register_password_auth(None, bind_host=bind_host) - -@cl.on_chat_resume -async def on_chat_resume(thread: ThreadDict): - logger.info(f"Resuming chat: {thread['id']}") - model_name = os.getenv("OPENAI_MODEL_NAME") or os.getenv("MODEL_NAME") or "gpt-5-nano-realtime-preview-2024-12-17" - 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-realtime-preview-2024-12-17", - initial=model_name - ) - ] - ) - await settings.send() - thread_id = thread["id"] - cl.user_session.set("thread_id", thread["id"]) - - # Ensure metadata is a dictionary - metadata = thread.get("metadata", {}) - if isinstance(metadata, str): - try: - metadata = json.loads(metadata) - except json.JSONDecodeError: - metadata = {} - - cl.user_session.set("metadata", metadata) - - message_history = [] - steps = thread["steps"] - - for message in steps: - msg_type = message.get("type") - if msg_type == "user_message": - message_history.append({"role": "user", "content": message.get("output", "")}) - elif msg_type == "assistant_message": - message_history.append({"role": "assistant", "content": message.get("output", "")}) - elif msg_type == "run": - # Handle 'run' type messages - if message.get("isError"): - message_history.append({"role": "system", "content": f"Error: {message.get('output', '')}"}) - else: - # You might want to handle non-error 'run' messages differently - pass - else: - logger.warning(f"Message without recognized type: {message}") - - cl.user_session.set("message_history", message_history) - - # Reconnect to OpenAI realtime - await setup_openai_realtime() - - \ No newline at end of file diff --git a/src/praisonai/praisonai/ui/realtimeclient/__init__.py b/src/praisonai/praisonai/ui/realtimeclient/__init__.py deleted file mode 100644 index 4d3da4fb9..000000000 --- a/src/praisonai/praisonai/ui/realtimeclient/__init__.py +++ /dev/null @@ -1,756 +0,0 @@ -# Derived from https://github.com/openai/openai-realtime-console. Will integrate with Chainlit when more mature. - -import os -import asyncio -import inspect -import numpy as np -import json -import websockets -from websockets.exceptions import ConnectionClosed -from datetime import datetime -from collections import defaultdict -import base64 - -from chainlit.logger import logger -from chainlit.config import config - - -def float_to_16bit_pcm(float32_array): - """ - Converts a numpy array of float32 amplitude data to a numpy array in int16 format. - :param float32_array: numpy array of float32 - :return: numpy array of int16 - """ - int16_array = np.clip(float32_array, -1, 1) * 32767 - return int16_array.astype(np.int16) - -def base64_to_array_buffer(base64_string): - """ - Converts a base64 string to a numpy array buffer. - :param base64_string: base64 encoded string - :return: numpy array of uint8 - """ - binary_data = base64.b64decode(base64_string) - return np.frombuffer(binary_data, dtype=np.uint8) - -def array_buffer_to_base64(array_buffer): - """ - Converts a numpy array buffer to a base64 string. - :param array_buffer: numpy array - :return: base64 encoded string - """ - if array_buffer.dtype == np.float32: - array_buffer = float_to_16bit_pcm(array_buffer) - elif array_buffer.dtype == np.int16: - array_buffer = array_buffer.tobytes() - else: - array_buffer = array_buffer.tobytes() - - return base64.b64encode(array_buffer).decode('utf-8') - -def merge_int16_arrays(left, right): - """ - Merge two numpy arrays of int16. - :param left: numpy array of int16 - :param right: numpy array of int16 - :return: merged numpy array of int16 - """ - if isinstance(left, np.ndarray) and left.dtype == np.int16 and isinstance(right, np.ndarray) and right.dtype == np.int16: - return np.concatenate((left, right)) - else: - raise ValueError("Both items must be numpy arrays of int16") - - -class RealtimeEventHandler: - def __init__(self): - self.event_handlers = defaultdict(list) - - def on(self, event_name, handler): - self.event_handlers[event_name].append(handler) - - def clear_event_handlers(self): - self.event_handlers = defaultdict(list) - - def dispatch(self, event_name, event): - for handler in self.event_handlers[event_name]: - if inspect.iscoroutinefunction(handler): - asyncio.create_task(handler(event)) - else: - handler(event) - - async def wait_for_next(self, event_name): - future = asyncio.Future() - - def handler(event): - if not future.done(): - future.set_result(event) - - self.on(event_name, handler) - return await future - - -class RealtimeAPI(RealtimeEventHandler): - def __init__(self, url=None, api_key=None): - super().__init__() - self.default_url = 'wss://api.openai.com/v1/realtime' - - # Support custom base URL from environment variable - base_url = os.getenv("OPENAI_BASE_URL") - if base_url: - # Convert HTTP/HTTPS base URL to WebSocket URL for realtime API - if base_url.startswith('https://'): - ws_url = base_url.replace('https://', 'wss://').rstrip('/') + '/realtime' - elif base_url.startswith('http://'): - ws_url = base_url.replace('http://', 'ws://').rstrip('/') + '/realtime' - else: - # Assume it's already a WebSocket URL - ws_url = base_url.rstrip('/') + '/realtime' if not base_url.endswith('/realtime') else base_url - self.url = url or ws_url - else: - self.url = url or self.default_url - - self.api_key = api_key or os.getenv("OPENAI_API_KEY") - self.ws = None - - def is_connected(self): - if self.ws is None: - return False - # Some websockets versions don't have a closed attribute - try: - return not self.ws.closed - except AttributeError: - # Fallback: check if websocket is still alive by checking state - try: - return hasattr(self.ws, 'state') and self.ws.state.name == 'OPEN' - except: - # Last fallback: assume connected if ws exists - return True - - def log(self, *args): - logger.debug(f"[Websocket/{datetime.utcnow().isoformat()}]", *args) - - async def connect(self, model='gpt-5-nano-realtime-preview-2024-12-17'): - if self.is_connected(): - raise Exception("Already connected") - - headers = { - 'Authorization': f'Bearer {self.api_key}', - 'OpenAI-Beta': 'realtime=v1' - } - - # Try different header parameter names for compatibility - try: - self.ws = await websockets.connect(f"{self.url}?model={model}", additional_headers=headers) - except TypeError: - # Fallback to older websockets versions - try: - self.ws = await websockets.connect(f"{self.url}?model={model}", extra_headers=headers) - except TypeError: - # Last fallback - some versions might not support headers parameter - raise Exception("Websockets library version incompatible. Please update websockets to version 11.0 or higher.") - - self.log(f"Connected to {self.url}") - asyncio.create_task(self._receive_messages()) - - async def _receive_messages(self): - try: - async for message in self.ws: - event = json.loads(message) - if event['type'] == "error": - logger.error(f"OpenAI Realtime API Error: {event}") - self.log("received:", event) - self.dispatch(f"server.{event['type']}", event) - self.dispatch("server.*", event) - except ConnectionClosed as e: - logger.info(f"WebSocket connection closed normally: {e}") - # Mark connection as closed - self.ws = None - # Dispatch disconnection event - self.dispatch("disconnected", {"reason": str(e)}) - except Exception as e: - logger.warning(f"WebSocket receive loop ended: {e}") - # Mark connection as closed - self.ws = None - # Dispatch disconnection event - self.dispatch("disconnected", {"reason": str(e)}) - - async def send(self, event_name, data=None): - if not self.is_connected(): - raise Exception("RealtimeAPI is not connected") - data = data or {} - if not isinstance(data, dict): - raise Exception("data must be a dictionary") - event = { - "event_id": self._generate_id("evt_"), - "type": event_name, - **data - } - self.dispatch(f"client.{event_name}", event) - self.dispatch("client.*", event) - self.log("sent:", event) - - try: - await self.ws.send(json.dumps(event)) - except ConnectionClosed as e: - logger.info(f"WebSocket connection closed during send: {e}") - # Mark connection as closed if send fails - self.ws = None - raise Exception(f"WebSocket connection lost: {e}") - except Exception as e: - logger.error(f"Failed to send WebSocket message: {e}") - # Mark connection as closed if send fails - self.ws = None - raise Exception(f"WebSocket connection lost: {e}") - - def _generate_id(self, prefix): - return f"{prefix}{int(datetime.utcnow().timestamp() * 1000)}" - - async def disconnect(self): - if self.ws: - try: - await self.ws.close() - logger.info(f"Disconnected from {self.url}") - except Exception as e: - logger.warning(f"Error during WebSocket close: {e}") - finally: - self.ws = None - self.log(f"WebSocket connection cleaned up") - -class RealtimeConversation: - default_frequency = config.features.audio.sample_rate - - EventProcessors = { - 'conversation.item.created': lambda self, event: self._process_item_created(event), - 'conversation.item.truncated': lambda self, event: self._process_item_truncated(event), - 'conversation.item.deleted': lambda self, event: self._process_item_deleted(event), - 'conversation.item.input_audio_transcription.completed': lambda self, event: self._process_input_audio_transcription_completed(event), - 'input_audio_buffer.speech_started': lambda self, event: self._process_speech_started(event), - 'input_audio_buffer.speech_stopped': lambda self, event, input_audio_buffer: self._process_speech_stopped(event, input_audio_buffer), - 'response.created': lambda self, event: self._process_response_created(event), - 'response.output_item.added': lambda self, event: self._process_output_item_added(event), - 'response.output_item.done': lambda self, event: self._process_output_item_done(event), - 'response.content_part.added': lambda self, event: self._process_content_part_added(event), - 'response.audio_transcript.delta': lambda self, event: self._process_audio_transcript_delta(event), - 'response.audio.delta': lambda self, event: self._process_audio_delta(event), - 'response.text.delta': lambda self, event: self._process_text_delta(event), - 'response.function_call_arguments.delta': lambda self, event: self._process_function_call_arguments_delta(event), - } - - def __init__(self): - self.clear() - - def clear(self): - self.item_lookup = {} - self.items = [] - self.response_lookup = {} - self.responses = [] - self.queued_speech_items = {} - self.queued_transcript_items = {} - self.queued_input_audio = None - - def queue_input_audio(self, input_audio): - self.queued_input_audio = input_audio - - def process_event(self, event, *args): - event_processor = self.EventProcessors.get(event['type']) - if not event_processor: - raise Exception(f"Missing conversation event processor for {event['type']}") - return event_processor(self, event, *args) - - def get_item(self, id): - return self.item_lookup.get(id) - - def get_items(self): - return self.items[:] - - def _process_item_created(self, event): - item = event['item'] - new_item = item.copy() - if new_item['id'] not in self.item_lookup: - self.item_lookup[new_item['id']] = new_item - self.items.append(new_item) - new_item['formatted'] = { - 'audio': [], - 'text': '', - 'transcript': '' - } - if new_item['id'] in self.queued_speech_items: - new_item['formatted']['audio'] = self.queued_speech_items[new_item['id']]['audio'] - del self.queued_speech_items[new_item['id']] - if 'content' in new_item: - text_content = [c for c in new_item['content'] if c['type'] in ['text', 'input_text']] - for content in text_content: - new_item['formatted']['text'] += content['text'] - if new_item['id'] in self.queued_transcript_items: - new_item['formatted']['transcript'] = self.queued_transcript_items[new_item['id']]['transcript'] - del self.queued_transcript_items[new_item['id']] - if new_item['type'] == 'message': - if new_item['role'] == 'user': - new_item['status'] = 'completed' - if self.queued_input_audio: - new_item['formatted']['audio'] = self.queued_input_audio - self.queued_input_audio = None - else: - new_item['status'] = 'in_progress' - elif new_item['type'] == 'function_call': - new_item['formatted']['tool'] = { - 'type': 'function', - 'name': new_item['name'], - 'call_id': new_item['call_id'], - 'arguments': '' - } - new_item['status'] = 'in_progress' - elif new_item['type'] == 'function_call_output': - new_item['status'] = 'completed' - new_item['formatted']['output'] = new_item['output'] - return new_item, None - - def _process_item_truncated(self, event): - item_id = event['item_id'] - audio_end_ms = event['audio_end_ms'] - item = self.item_lookup.get(item_id) - if not item: - raise Exception(f'item.truncated: Item "{item_id}" not found') - end_index = (audio_end_ms * self.default_frequency) // 1000 - item['formatted']['transcript'] = '' - item['formatted']['audio'] = item['formatted']['audio'][:end_index] - return item, None - - def _process_item_deleted(self, event): - item_id = event['item_id'] - item = self.item_lookup.get(item_id) - if not item: - raise Exception(f'item.deleted: Item "{item_id}" not found') - del self.item_lookup[item['id']] - self.items.remove(item) - return item, None - - def _process_input_audio_transcription_completed(self, event): - item_id = event['item_id'] - content_index = event['content_index'] - transcript = event['transcript'] - formatted_transcript = transcript or ' ' - item = self.item_lookup.get(item_id) - if not item: - self.queued_transcript_items[item_id] = {'transcript': formatted_transcript} - return None, None - item['content'][content_index]['transcript'] = transcript - item['formatted']['transcript'] = formatted_transcript - return item, {'transcript': transcript} - - def _process_speech_started(self, event): - item_id = event['item_id'] - audio_start_ms = event['audio_start_ms'] - self.queued_speech_items[item_id] = {'audio_start_ms': audio_start_ms} - return None, None - - def _process_speech_stopped(self, event, input_audio_buffer): - item_id = event['item_id'] - audio_end_ms = event['audio_end_ms'] - speech = self.queued_speech_items[item_id] - speech['audio_end_ms'] = audio_end_ms - if input_audio_buffer: - start_index = (speech['audio_start_ms'] * self.default_frequency) // 1000 - end_index = (speech['audio_end_ms'] * self.default_frequency) // 1000 - speech['audio'] = input_audio_buffer[start_index:end_index] - return None, None - - def _process_response_created(self, event): - response = event['response'] - if response['id'] not in self.response_lookup: - self.response_lookup[response['id']] = response - self.responses.append(response) - return None, None - - def _process_output_item_added(self, event): - response_id = event['response_id'] - item = event['item'] - response = self.response_lookup.get(response_id) - if not response: - raise Exception(f'response.output_item.added: Response "{response_id}" not found') - response['output'].append(item['id']) - return None, None - - def _process_output_item_done(self, event): - item = event['item'] - if not item: - raise Exception('response.output_item.done: Missing "item"') - found_item = self.item_lookup.get(item['id']) - if not found_item: - raise Exception(f'response.output_item.done: Item "{item["id"]}" not found') - found_item['status'] = item['status'] - return found_item, None - - def _process_content_part_added(self, event): - item_id = event['item_id'] - part = event['part'] - item = self.item_lookup.get(item_id) - if not item: - raise Exception(f'response.content_part.added: Item "{item_id}" not found') - item['content'].append(part) - return item, None - - def _process_audio_transcript_delta(self, event): - item_id = event['item_id'] - content_index = event['content_index'] - delta = event['delta'] - item = self.item_lookup.get(item_id) - if not item: - raise Exception(f'response.audio_transcript.delta: Item "{item_id}" not found') - item['content'][content_index]['transcript'] += delta - item['formatted']['transcript'] += delta - return item, {'transcript': delta} - - def _process_audio_delta(self, event): - item_id = event['item_id'] - content_index = event['content_index'] - delta = event['delta'] - item = self.item_lookup.get(item_id) - if not item: - logger.debug(f'response.audio.delta: Item "{item_id}" not found') - return None, None - array_buffer = base64_to_array_buffer(delta) - append_values = array_buffer.tobytes() - item['formatted']['audio'].append(append_values) - return item, {'audio': append_values} - - def _process_text_delta(self, event): - item_id = event['item_id'] - content_index = event['content_index'] - delta = event['delta'] - item = self.item_lookup.get(item_id) - if not item: - raise Exception(f'response.text.delta: Item "{item_id}" not found') - item['content'][content_index]['text'] += delta - item['formatted']['text'] += delta - return item, {'text': delta} - - def _process_function_call_arguments_delta(self, event): - item_id = event['item_id'] - delta = event['delta'] - item = self.item_lookup.get(item_id) - if not item: - raise Exception(f'response.function_call_arguments.delta: Item "{item_id}" not found') - item['arguments'] += delta - item['formatted']['tool']['arguments'] += delta - return item, {'arguments': delta} - - -class RealtimeClient(RealtimeEventHandler): - def __init__(self, url=None, api_key=None): - super().__init__() - self.default_session_config = { - "modalities": ["text", "audio"], - "instructions": "System settings:\nTool use: enabled.\n\nInstructions:\n- You are an artificial intelligence agent responsible for helping test realtime voice capabilities\n- Please make sure to respond with a helpful voice via audio\n- Be kind, helpful, and curteous\n- It is okay to ask the user questions\n- Use tools and functions you have available liberally, it is part of the training apparatus\n- Be open to exploration and conversation\n- Remember: this is just for fun and testing!\n\nPersonality:\n- Be upbeat and genuine\n- Try speaking quickly as if excited\n", - "voice": "shimmer", - "input_audio_format": "pcm16", - "output_audio_format": "pcm16", - "input_audio_transcription": { "model": 'whisper-1' }, - "turn_detection": { "type": 'server_vad' }, - "tools": [], - "tool_choice": "auto", - "temperature": 0.8, - } - self.session_config = {} - self.transcription_models = [{"model": "whisper-1"}] - self.default_server_vad_config = { - "type": "server_vad", - "threshold": 0.5, - "prefix_padding_ms": 300, - "silence_duration_ms": 200, - } - self.realtime = RealtimeAPI(url, api_key) - self.conversation = RealtimeConversation() - self._reset_config() - self._add_api_event_handlers() - - def _reset_config(self): - self.session_created = False - self.tools = {} - self.session_config = self.default_session_config.copy() - self.input_audio_buffer = bytearray() - return True - - def _add_api_event_handlers(self): - self.realtime.on("client.*", self._log_event) - self.realtime.on("server.*", self._log_event) - self.realtime.on("server.session.created", self._on_session_created) - self.realtime.on("server.response.created", self._process_event) - self.realtime.on("server.response.output_item.added", self._process_event) - self.realtime.on("server.response.content_part.added", self._process_event) - self.realtime.on("server.input_audio_buffer.speech_started", self._on_speech_started) - self.realtime.on("server.input_audio_buffer.speech_stopped", self._on_speech_stopped) - self.realtime.on("server.conversation.item.created", self._on_item_created) - self.realtime.on("server.conversation.item.truncated", self._process_event) - self.realtime.on("server.conversation.item.deleted", self._process_event) - self.realtime.on("server.conversation.item.input_audio_transcription.completed", self._process_event) - self.realtime.on("server.response.audio_transcript.delta", self._process_event) - self.realtime.on("server.response.audio.delta", self._process_event) - self.realtime.on("server.response.text.delta", self._process_event) - self.realtime.on("server.response.function_call_arguments.delta", self._process_event) - self.realtime.on("server.response.output_item.done", self._on_output_item_done) - - def _log_event(self, event): - realtime_event = { - "time": datetime.utcnow().isoformat(), - "source": "client" if event["type"].startswith("client.") else "server", - "event": event, - } - self.dispatch("realtime.event", realtime_event) - - def _on_session_created(self, event): - try: - session_id = event.get('session', {}).get('id', 'unknown') - model = event.get('session', {}).get('model', 'unknown') - logger.info(f"OpenAI Realtime session created - ID: {session_id}, Model: {model}") - except Exception as e: - logger.warning(f"Error processing session created event: {e}") - logger.debug(f"Session event details: {event}") - self.session_created = True - - def _process_event(self, event, *args): - item, delta = self.conversation.process_event(event, *args) - if item: - self.dispatch("conversation.updated", {"item": item, "delta": delta}) - return item, delta - - def _on_speech_started(self, event): - self._process_event(event) - self.dispatch("conversation.interrupted", event) - - def _on_speech_stopped(self, event): - self._process_event(event, self.input_audio_buffer) - - def _on_item_created(self, event): - item, delta = self._process_event(event) - self.dispatch("conversation.item.appended", {"item": item}) - if item and item["status"] == "completed": - self.dispatch("conversation.item.completed", {"item": item}) - - async def _on_output_item_done(self, event): - item, delta = self._process_event(event) - if item and item["status"] == "completed": - self.dispatch("conversation.item.completed", {"item": item}) - if item and item.get("formatted", {}).get("tool"): - await self._call_tool(item["formatted"]["tool"]) - - async def _call_tool(self, tool): - try: - json_arguments = json.loads(tool["arguments"]) - tool_config = self.tools.get(tool["name"]) - if not tool_config: - raise Exception(f'Tool "{tool["name"]}" has not been added') - result = await tool_config["handler"](**json_arguments) - await self.realtime.send("conversation.item.create", { - "item": { - "type": "function_call_output", - "call_id": tool["call_id"], - "output": json.dumps(result), - } - }) - except Exception as e: - error_message = json.dumps({"error": str(e)}) - logger.error(f"Tool call error: {error_message}") - await self.realtime.send("conversation.item.create", { - "item": { - "type": "function_call_output", - "call_id": tool["call_id"], - "output": error_message, - } - }) - await self.create_response() - - def is_connected(self): - return self.realtime.is_connected() - - def reset(self): - self.disconnect() - self.realtime.clear_event_handlers() - self._reset_config() - self._add_api_event_handlers() - return True - - async def connect(self, model=None): - if self.is_connected(): - raise Exception("Already connected, use .disconnect() first") - - # Use provided model, OPENAI_MODEL_NAME environment variable, or default - if model is None: - model = os.getenv("OPENAI_MODEL_NAME", 'gpt-5-nano-realtime-preview-2024-12-17') - - await self.realtime.connect(model) - await self.update_session() - return True - - async def wait_for_session_created(self): - if not self.is_connected(): - raise Exception("Not connected, use .connect() first") - while not self.session_created: - await asyncio.sleep(0.001) - return True - - async def disconnect(self): - self.session_created = False - self.conversation.clear() - if self.realtime.is_connected(): - await self.realtime.disconnect() - logger.info("RealtimeClient disconnected") - - def get_turn_detection_type(self): - return self.session_config.get("turn_detection", {}).get("type") - - async def add_tool(self, definition, handler): - if not definition.get("name"): - raise Exception("Missing tool name in definition") - name = definition["name"] - if name in self.tools: - raise Exception(f'Tool "{name}" already added. Please use .removeTool("{name}") before trying to add again.') - if not callable(handler): - raise Exception(f'Tool "{name}" handler must be a function') - self.tools[name] = {"definition": definition, "handler": handler} - await self.update_session() - return self.tools[name] - - def remove_tool(self, name): - if name not in self.tools: - raise Exception(f'Tool "{name}" does not exist, can not be removed.') - del self.tools[name] - return True - - async def delete_item(self, id): - await self.realtime.send("conversation.item.delete", {"item_id": id}) - return True - - async def update_session(self, **kwargs): - self.session_config.update(kwargs) - use_tools = [ - {**tool_definition, "type": "function"} - for tool_definition in self.session_config.get("tools", []) - ] + [ - {**self.tools[key]["definition"], "type": "function"} - for key in self.tools - ] - session = {**self.session_config, "tools": use_tools} - logger.debug(f"Updating session: {session}") - if self.realtime.is_connected(): - await self.realtime.send("session.update", {"session": session}) - return True - - async def create_conversation_item(self, item): - await self.realtime.send("conversation.item.create", { - "item": item - }) - - async def send_user_message_content(self, content=[]): - if content: - for c in content: - if c["type"] == "input_audio": - if isinstance(c["audio"], (bytes, bytearray)): - c["audio"] = array_buffer_to_base64(c["audio"]) - await self.realtime.send("conversation.item.create", { - "item": { - "type": "message", - "role": "user", - "content": content, - } - }) - await self.create_response() - return True - - async def append_input_audio(self, array_buffer): - if not self.is_connected(): - logger.warning("Cannot append audio: RealtimeClient is not connected") - return False - - if len(array_buffer) > 0: - try: - await self.realtime.send("input_audio_buffer.append", { - "audio": array_buffer_to_base64(np.array(array_buffer)), - }) - self.input_audio_buffer.extend(array_buffer) - except Exception as e: - logger.error(f"Failed to append input audio: {e}") - # Connection might be lost, mark as disconnected - if "connection" in str(e).lower() or "closed" in str(e).lower(): - logger.warning("WebSocket connection appears to be lost. Audio input will be queued until reconnection.") - return False - return True - - async def create_response(self): - if self.get_turn_detection_type() is None and len(self.input_audio_buffer) > 0: - await self.realtime.send("input_audio_buffer.commit") - self.conversation.queue_input_audio(self.input_audio_buffer) - self.input_audio_buffer = bytearray() - await self.realtime.send("response.create") - return True - - async def cancel_response(self, id=None, sample_count=0): - if not id: - await self.realtime.send("response.cancel") - return {"item": None} - else: - item = self.conversation.get_item(id) - if not item: - raise Exception(f'Could not find item "{id}"') - if item["type"] != "message": - raise Exception('Can only cancelResponse messages with type "message"') - if item["role"] != "assistant": - raise Exception('Can only cancelResponse messages with role "assistant"') - await self.realtime.send("response.cancel") - audio_index = next((i for i, c in enumerate(item["content"]) if c["type"] == "audio"), -1) - if audio_index == -1: - raise Exception("Could not find audio on item to cancel") - await self.realtime.send("conversation.item.truncate", { - "item_id": id, - "content_index": audio_index, - "audio_end_ms": int((sample_count / self.conversation.default_frequency) * 1000), - }) - return {"item": item} - - async def wait_for_next_item(self): - event = await self.wait_for_next("conversation.item.appended") - return {"item": event["item"]} - - async def wait_for_next_completed_item(self): - event = await self.wait_for_next("conversation.item.completed") - return {"item": event["item"]} - - async def _send_chainlit_message(self, item): - import chainlit as cl - - # Debug logging - logger.debug(f"Received item structure: {json.dumps({k: type(v).__name__ for k, v in item.items()}, indent=2)}") - - if "type" in item and item["type"] == "function_call_output": - # Don't send function call outputs directly to Chainlit - logger.debug(f"Function call output received: {item.get('output', '')}") - elif "role" in item: - if item["role"] == "user": - content = item.get("formatted", {}).get("text", "") or item.get("formatted", {}).get("transcript", "") - if content: - await cl.Message(content=content, author="User").send() - elif item["role"] == "assistant": - content = item.get("formatted", {}).get("text", "") or item.get("formatted", {}).get("transcript", "") - if content: - await cl.Message(content=content, author="AI").send() - else: - logger.warning(f"Unhandled role: {item['role']}") - else: - # Handle items without a 'role' or 'type' - logger.debug(f"Unhandled item type:\n{json.dumps(item, indent=2)}") - - # Additional debug logging - logger.debug(f"Processed Chainlit message for item: {item.get('id', 'unknown')}") - - async def ensure_connected(self): - """Check connection health and attempt reconnection if needed""" - if not self.is_connected(): - try: - logger.info("Attempting to reconnect to OpenAI Realtime API...") - model = os.getenv("OPENAI_MODEL_NAME", 'gpt-5-nano-realtime-preview-2024-12-17') - await self.connect(model) - return True - except Exception as e: - logger.error(f"Failed to reconnect: {e}") - return False - return True \ No newline at end of file diff --git a/src/praisonai/praisonai/ui/realtimeclient/tools.py b/src/praisonai/praisonai/ui/realtimeclient/tools.py deleted file mode 100644 index 4ac6679ed..000000000 --- a/src/praisonai/praisonai/ui/realtimeclient/tools.py +++ /dev/null @@ -1,242 +0,0 @@ -import yfinance as yf -import chainlit as cl -import plotly -import json -from tavily import TavilyClient -from crawl4ai import AsyncWebCrawler -import os -import logging -import asyncio -from openai import OpenAI -import base64 -from io import BytesIO -from datetime import datetime -from duckduckgo_search import DDGS - -# Set up logging -logger = logging.getLogger(__name__) -log_level = os.getenv("LOGLEVEL", "INFO").upper() -logger.setLevel(log_level) - -# Set Tavily API key -tavily_api_key = os.getenv("TAVILY_API_KEY") -tavily_client = TavilyClient(api_key=tavily_api_key) if tavily_api_key else None - -# Set up OpenAI client with support for custom base URL -openai_base_url = os.getenv("OPENAI_BASE_URL") -openai_api_key = os.getenv("OPENAI_API_KEY") - -if openai_base_url: - openai_client = OpenAI(base_url=openai_base_url, api_key=openai_api_key) -else: - openai_client = OpenAI(api_key=openai_api_key) - -query_stock_price_def = { - "name": "query_stock_price", - "description": "Queries the latest stock price information for a given stock symbol.", - "parameters": { - "type": "object", - "properties": { - "symbol": { - "type": "string", - "description": "The stock symbol to query (e.g., 'AAPL' for Apple Inc.)" - }, - "period": { - "type": "string", - "description": "The time period for which to retrieve stock data (e.g., '1d' for one day, '1mo' for one month)" - } - }, - "required": ["symbol", "period"] - } -} - -async def query_stock_price_handler(symbol, period): - """ - Queries the latest stock price information for a given stock symbol. - """ - try: - stock = yf.Ticker(symbol) - hist = stock.history(period=period) - if hist.empty: - return {"error": "No data found for the given symbol."} - return hist.to_json() - - except Exception as e: - return {"error": str(e)} - -query_stock_price = (query_stock_price_def, query_stock_price_handler) - -draw_plotly_chart_def = { - "name": "draw_plotly_chart", - "description": "Draws a Plotly chart based on the provided JSON figure and displays it with an accompanying message.", - "parameters": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "The message to display alongside the chart" - }, - "plotly_json_fig": { - "type": "string", - "description": "A JSON string representing the Plotly figure to be drawn" - } - }, - "required": ["message", "plotly_json_fig"] - } -} - -async def draw_plotly_chart_handler(message: str, plotly_json_fig): - fig = plotly.io.from_json(plotly_json_fig) - elements = [cl.Plotly(name="chart", figure=fig, display="inline")] - - await cl.Message(content=message, elements=elements).send() - return {"status": "success"} # Add a return value - -draw_plotly_chart = (draw_plotly_chart_def, draw_plotly_chart_handler) - -tavily_web_search_def = { - "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"] - } -} - -async def tavily_web_search_handler(query): - current_date = datetime.now().strftime("%d %B %Y") - query_with_date = query + f" {current_date}" - - if tavily_client: - try: - response = tavily_client.search(query_with_date) - logger.debug(f"Tavily search response: {response}") - results = await process_tavily_results(response) - except Exception as e: - logger.error(f"Error in Tavily search: {str(e)}") - results = await fallback_to_duckduckgo(query_with_date) - else: - logger.info("Tavily API key is not set. Using DuckDuckGo as fallback.") - results = await fallback_to_duckduckgo(query_with_date) - - return json.dumps({ - "query": query, - "results": results - }) - -async def process_tavily_results(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) - full_content = crawl_result.markdown if crawl_result and hasattr(crawl_result, 'markdown') and crawl_result.markdown else "No content available" - results.append({ - "content": result.get('content'), - "url": url, - "full_content": full_content - }) - 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" - }) - return results - -async def fallback_to_duckduckgo(query): - try: - with DDGS() as ddgs: - ddg_results = list(ddgs.text(query, max_results=5)) - - logger.debug(f"DuckDuckGo search results: {ddg_results}") - - async with AsyncWebCrawler() as crawler: - results = [] - - for result in ddg_results: - url = result.get('href') - if url: - try: - crawl_result = await crawler.arun(url=url) - full_content = crawl_result.markdown if crawl_result and hasattr(crawl_result, 'markdown') and crawl_result.markdown else "No content available" - results.append({ - "content": result.get('body'), - "url": url, - "full_content": full_content - }) - except Exception as e: - logger.error(f"Error crawling {url}: {str(e)}") - results.append({ - "content": result.get('body'), - "url": url, - "full_content": "Error: Unable to crawl this URL" - }) - else: - results.append({ - "content": result.get('body'), - "url": "N/A", - "full_content": "No URL provided for crawling" - }) - - return results - except Exception as e: - logger.error(f"Error in DuckDuckGo search: {str(e)}") - return [] - -tavily_web_search = (tavily_web_search_def, tavily_web_search_handler) - -# New image generation tool -generate_image_def = { - "name": "generate_image", - "description": "Generate an image based on a text prompt using DALL-E 3", - "parameters": { - "type": "object", - "properties": { - "prompt": {"type": "string", "description": "The text prompt to generate the image"}, - "size": {"type": "string", "description": "Image size (1024x1024, 1024x1792, or 1792x1024)", "default": "1024x1024"}, - "quality": {"type": "string", "description": "Image quality (standard or hd)", "default": "standard"}, - }, - "required": ["prompt"] - } -} - -async def generate_image_handler(prompt, size="1024x1024", quality="standard"): - try: - response = openai_client.images.generate( - model="dall-e-3", - prompt=prompt, - size=size, - quality=quality, - n=1, - ) - - image_url = response.data[0].url - - # Download the image - import requests - image_content = requests.get(image_url).content - - # Convert image to base64 - image_base64 = base64.b64encode(image_content).decode('utf-8') - - # Create a Chainlit Image element - image_element = cl.Image(content=image_content, name="generated_image", display="inline") - - # Send the image in a Chainlit message - await cl.Message(content=f"Generated image for prompt: '{prompt}'", elements=[image_element]).send() - - return {"status": "success", "message": "Image generated and displayed"} - except Exception as e: - logger.error(f"Error generating image: {str(e)}") - return {"status": "error", "message": str(e)} - -generate_image = (generate_image_def, generate_image_handler) - -tools = [query_stock_price, draw_plotly_chart, tavily_web_search, generate_image] diff --git a/src/praisonai/praisonai/ui/sql_alchemy.py b/src/praisonai/praisonai/ui/sql_alchemy.py deleted file mode 100644 index fb208b537..000000000 --- a/src/praisonai/praisonai/ui/sql_alchemy.py +++ /dev/null @@ -1,715 +0,0 @@ -import json -import ssl -import uuid -from dataclasses import asdict -from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union -import os - -import aiofiles -import aiohttp - -from chainlit.data.base import BaseDataLayer -from chainlit.data.utils import queue_until_user_message -from praisonai.ui.chainlit_compat import EXPIRY_TIME, BaseStorageClient -from chainlit.element import ElementDict -from chainlit.logger import logger -from chainlit.step import StepDict -from chainlit.types import ( - Feedback, - FeedbackDict, - PageInfo, - PaginatedResponse, - Pagination, - ThreadDict, - ThreadFilter, -) -from chainlit.user import PersistedUser, User -from sqlalchemy import text -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine -from sqlalchemy.orm import sessionmaker -from database_config import get_database_config_for_sqlalchemy - -if TYPE_CHECKING: - from chainlit.element import Element - from chainlit.step import StepDict - -# Check FORCE_SQLITE flag to bypass external database detection -DATABASE_URL, SUPABASE_DATABASE_URL = get_database_config_for_sqlalchemy() - -class SQLAlchemyDataLayer(BaseDataLayer): - def __init__( - self, - conninfo: str, - ssl_require: bool = False, - storage_provider: Optional[BaseStorageClient] = None, - user_thread_limit: Optional[int] = 1000, - show_logger: Optional[bool] = False, - ): - self._conninfo = conninfo - self.user_thread_limit = user_thread_limit - self.show_logger = show_logger - ssl_args = {} - if ssl_require: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - ssl_args["ssl"] = ssl_context - self.engine: AsyncEngine = create_async_engine( - self._conninfo, connect_args=ssl_args - ) - self.async_session = sessionmaker( - bind=self.engine, expire_on_commit=False, class_=AsyncSession - ) - if storage_provider: - self.storage_provider: Optional[BaseStorageClient] = storage_provider - if self.show_logger: - logger.info("SQLAlchemyDataLayer storage client initialized") - else: - self.storage_provider = None - logger.warning( - "SQLAlchemyDataLayer storage client is not initialized and elements will not be persisted!" - ) - - async def build_debug_url(self) -> str: - return "" - - ###### SQL Helpers ###### - async def execute_sql( - self, query: str, parameters: dict, expand: Optional[str] = None - ) -> Union[List[Dict[str, Any]], int, None]: - from sqlalchemy import bindparam - parameterized_query = text(query) - if expand: - parameterized_query = parameterized_query.bindparams(bindparam(expand, expanding=True)) - async with self.async_session() as session: - try: - await session.begin() - result = await session.execute(parameterized_query, parameters) - await session.commit() - if result.returns_rows: - json_result = [dict(row._mapping) for row in result.fetchall()] - clean_json_result = self.clean_result(json_result) - assert isinstance(clean_json_result, list) or isinstance( - clean_json_result, int - ) - return clean_json_result - else: - return result.rowcount - except SQLAlchemyError as e: - await session.rollback() - logger.warning(f"An error occurred: {e}") - return None - except Exception as e: - await session.rollback() - logger.warning(f"An unexpected error occurred: {e}") - return None - - async def get_current_timestamp(self) -> str: - return datetime.now().isoformat() + "Z" - - def clean_result(self, obj): - if isinstance(obj, dict): - return {k: self.clean_result(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [self.clean_result(item) for item in obj] - elif isinstance(obj, uuid.UUID): - return str(obj) - return obj - - ###### User ###### - async def get_user(self, identifier: str) -> Optional[PersistedUser]: - if self.show_logger: - logger.info(f"SQLAlchemy: get_user, identifier={identifier}") - query = 'SELECT * FROM users WHERE "identifier" = :identifier' - parameters = {"identifier": identifier} - result = await self.execute_sql(query=query, parameters=parameters) - if result and isinstance(result, list): - user_data = result[0] - - meta = user_data.get("meta", "{}") - if isinstance(meta, str): - meta = json.loads(meta) - - return PersistedUser( - id=user_data["id"], - identifier=user_data["identifier"], - createdAt=user_data["createdAt"], - metadata=meta, - ) - return None - - async def _get_user_identifer_by_id(self, user_id: str) -> str: - if self.show_logger: - logger.info(f"SQLAlchemy: _get_user_identifer_by_id, user_id={user_id}") - query = 'SELECT "identifier" FROM users WHERE "id" = :user_id' - parameters = {"user_id": user_id} - result = await self.execute_sql(query=query, parameters=parameters) - assert result - assert isinstance(result, list) - return result[0]["identifier"] - - async def _get_user_id_by_thread(self, thread_id: str) -> Optional[str]: - if self.show_logger: - logger.info(f"SQLAlchemy: _get_user_id_by_thread, thread_id={thread_id}") - query = 'SELECT "userId" FROM threads WHERE "id" = :thread_id' - parameters = {"thread_id": thread_id} - result = await self.execute_sql(query=query, parameters=parameters) - if result and isinstance(result, list): - return result[0]["userId"] - return None - - async def create_user(self, user: User) -> Optional[PersistedUser]: - if self.show_logger: - logger.info(f"SQLAlchemy: create_user, user_identifier={user.identifier}") - existing_user: Optional["PersistedUser"] = await self.get_user(user.identifier) - user_dict: Dict[str, Any] = { - "identifier": str(user.identifier), - "meta": json.dumps(user.metadata) or "{}", - } - if not existing_user: - user_dict["id"] = str(uuid.uuid4()) - user_dict["createdAt"] = await self.get_current_timestamp() - query = 'INSERT INTO users ("id", "identifier", "createdAt", "meta") VALUES (:id, :identifier, :createdAt, :meta)' - await self.execute_sql(query=query, parameters=user_dict) - else: - query = 'UPDATE users SET "meta" = :meta WHERE "identifier" = :identifier' - await self.execute_sql(query=query, parameters=user_dict) - return await self.get_user(user.identifier) - - ###### Threads ###### - async def get_thread_author(self, thread_id: str) -> str: - if self.show_logger: - logger.info(f"SQLAlchemy: get_thread_author, thread_id={thread_id}") - query = 'SELECT "userIdentifier" FROM threads WHERE "id" = :id' - parameters = {"id": thread_id} - result = await self.execute_sql(query=query, parameters=parameters) - if isinstance(result, list) and result: - author_identifier = result[0].get("userIdentifier") - if author_identifier is not None: - return author_identifier - raise ValueError(f"Author not found for thread_id {thread_id}") - - async def get_thread(self, thread_id: str) -> Optional[ThreadDict]: - if self.show_logger: - logger.info(f"SQLAlchemy: get_thread, thread_id={thread_id}") - user_threads: Optional[List[ThreadDict]] = await self.get_all_user_threads( - thread_id=thread_id - ) - if user_threads: - return user_threads[0] - else: - return None - - async def update_thread( - self, - thread_id: str, - name: Optional[str] = None, - user_id: Optional[str] = None, - metadata: Optional[Dict] = None, - tags: Optional[List[str]] = None, - ): - if self.show_logger: - logger.info(f"SQLAlchemy: update_thread, thread_id={thread_id}") - - user_identifier = None - if user_id: - user_identifier = await self._get_user_identifer_by_id(user_id) - - data = { - "id": thread_id, - "createdAt": ( - await self.get_current_timestamp() if metadata is None else None - ), - "name": ( - name - if name is not None - else (metadata.get("name") if metadata and "name" in metadata else None) - ), - "userId": user_id, - "userIdentifier": user_identifier, - "tags": json.dumps(tags) if tags else None, - "meta": json.dumps(metadata) if metadata else None, - } - parameters = {key: value for key, value in data.items() if value is not None} - columns = ", ".join(f'"{key}"' for key in parameters.keys()) - values = ", ".join(f':{key}' for key in parameters.keys()) - updates = ", ".join( - f'"{key}" = EXCLUDED."{key}"' for key in parameters.keys() if key != "id" - ) - query = f""" - INSERT INTO threads ({columns}) - VALUES ({values}) - ON CONFLICT ("id") DO UPDATE - SET {updates}; - """ - await self.execute_sql(query=query, parameters=parameters) - - async def delete_thread(self, thread_id: str): - if self.show_logger: - logger.info(f"SQLAlchemy: delete_thread, thread_id={thread_id}") - feedbacks_query = 'DELETE FROM feedbacks WHERE "forId" IN (SELECT "id" FROM steps WHERE "threadId" = :id)' - elements_query = 'DELETE FROM elements WHERE "threadId" = :id' - steps_query = 'DELETE FROM steps WHERE "threadId" = :id' - thread_query = 'DELETE FROM threads WHERE "id" = :id' - parameters = {"id": thread_id} - await self.execute_sql(query=feedbacks_query, parameters=parameters) - await self.execute_sql(query=elements_query, parameters=parameters) - await self.execute_sql(query=steps_query, parameters=parameters) - await self.execute_sql(query=thread_query, parameters=parameters) - - async def list_threads( - self, pagination: Pagination, filters: ThreadFilter - ) -> PaginatedResponse: - if self.show_logger: - logger.info( - f"SQLAlchemy: list_threads, pagination={pagination}, filters={filters}" - ) - if not filters.userId: - raise ValueError("userId is required") - all_user_threads: Optional[List[ThreadDict]] = ( - await self.get_all_user_threads(user_id=filters.userId) or [] - ) - - search_keyword = filters.search.lower() if filters.search else None - feedback_value = int(filters.feedback) if filters.feedback else None - - filtered_threads = [] - for thread in all_user_threads: - keyword_match = True - feedback_match = True - if search_keyword or feedback_value is not None: - if search_keyword: - keyword_match = any( - search_keyword in step["output"].lower() - for step in thread["steps"] - if "output" in step and isinstance(step["output"], str) - ) - if feedback_value is not None: - feedback_match = False - for step in thread["steps"]: - feedback = step.get("feedback") - if feedback and feedback.get("value") == feedback_value: - feedback_match = True - break - if keyword_match and feedback_match: - filtered_threads.append(thread) - - start = 0 - if pagination.cursor: - for i, thr in enumerate(filtered_threads): - if thr["id"] == pagination.cursor: - start = i + 1 - break - end = start + pagination.first - paginated_threads = filtered_threads[start:end] or [] - - has_next_page = len(filtered_threads) > end - start_cursor = paginated_threads[0]["id"] if paginated_threads else None - end_cursor = paginated_threads[-1]["id"] if paginated_threads else None - - return PaginatedResponse( - pageInfo=PageInfo( - hasNextPage=has_next_page, - startCursor=start_cursor, - endCursor=end_cursor, - ), - data=paginated_threads, - ) - - ###### Steps ###### - @queue_until_user_message() - async def create_step(self, step_dict: "StepDict"): - if self.show_logger: - logger.info(f"SQLAlchemy: create_step, step_id={step_dict.get('id')}") - - step_dict["showInput"] = ( - str(step_dict.get("showInput", "")).lower() - if "showInput" in step_dict - else None - ) - - tags = step_dict.get("tags") - if not tags: - tags = [] - meta = json.dumps(step_dict.get("metadata", {})) - generation = json.dumps(step_dict.get("generation", {})) - parameters = { - "id": step_dict.get("id"), - "name": step_dict.get("name"), - "type": step_dict.get("type"), - "threadId": step_dict.get("threadId"), - "parentId": step_dict.get("parentId"), - "disableFeedback": step_dict.get("disableFeedback", False), - "streaming": step_dict.get("streaming", False), - "waitForAnswer": step_dict.get("waitForAnswer", False), - "isError": step_dict.get("isError", False), - "meta": meta, - "tags": json.dumps(tags), - "input": step_dict.get("input"), - "output": step_dict.get("output"), - "createdAt": step_dict.get("createdAt"), - "startTime": step_dict.get("start"), - "endTime": step_dict.get("end"), - "generation": generation, - "showInput": step_dict.get("showInput"), - "language": step_dict.get("language"), - "indent": step_dict.get("indent"), - } - parameters = {k: v for k, v in parameters.items() if v is not None} - columns = ", ".join(f'"{key}"' for key in parameters.keys()) - values = ", ".join(f':{key}' for key in parameters.keys()) - updates = ", ".join( - f'"{key}" = :{key}' for key in parameters.keys() if key != "id" - ) - query = f""" - INSERT INTO steps ({columns}) - VALUES ({values}) - ON CONFLICT ("id") DO UPDATE - SET {updates}; - """ - await self.execute_sql(query=query, parameters=parameters) - - @queue_until_user_message() - async def update_step(self, step_dict: "StepDict"): - if self.show_logger: - logger.info(f"SQLAlchemy: update_step, step_id={step_dict.get('id')}") - await self.create_step(step_dict) - - @queue_until_user_message() - async def delete_step(self, step_id: str): - if self.show_logger: - logger.info(f"SQLAlchemy: delete_step, step_id={step_id}") - feedbacks_query = 'DELETE FROM feedbacks WHERE "forId" = :id' - elements_query = 'DELETE FROM elements WHERE "forId" = :id' - steps_query = 'DELETE FROM steps WHERE "id" = :id' - parameters = {"id": step_id} - await self.execute_sql(query=feedbacks_query, parameters=parameters) - await self.execute_sql(query=elements_query, parameters=parameters) - await self.execute_sql(query=steps_query, parameters=parameters) - - ###### Feedback ###### - async def upsert_feedback(self, feedback: Feedback) -> str: - if self.show_logger: - logger.info(f"SQLAlchemy: upsert_feedback, feedback_id={feedback.id}") - feedback.id = feedback.id or str(uuid.uuid4()) - feedback_dict = asdict(feedback) - parameters = {k: v for k, v in feedback_dict.items() if v is not None} - columns = ", ".join(f'"{key}"' for key in parameters.keys()) - values = ", ".join(f':{key}' for key in parameters.keys()) - updates = ", ".join( - f'"{key}" = :{key}' for key in parameters.keys() if key != "id" - ) - query = f""" - INSERT INTO feedbacks ({columns}) - VALUES ({values}) - ON CONFLICT ("id") DO UPDATE - SET {updates}; - """ - await self.execute_sql(query=query, parameters=parameters) - return feedback.id - - async def delete_feedback(self, feedback_id: str) -> bool: - if self.show_logger: - logger.info(f"SQLAlchemy: delete_feedback, feedback_id={feedback_id}") - query = 'DELETE FROM feedbacks WHERE "id" = :feedback_id' - parameters = {"feedback_id": feedback_id} - await self.execute_sql(query=query, parameters=parameters) - return True - - ###### Elements ###### - async def get_element( - self, thread_id: str, element_id: str - ) -> Optional["ElementDict"]: - if self.show_logger: - logger.info( - f"SQLAlchemy: get_element, thread_id={thread_id}, element_id={element_id}" - ) - query = 'SELECT * FROM elements WHERE "threadId" = :thread_id AND "id" = :element_id' - parameters = {"thread_id": thread_id, "element_id": element_id} - element = await self.execute_sql(query=query, parameters=parameters) - if isinstance(element, list) and element: - element_dict = element[0] - return ElementDict( - id=element_dict["id"], - threadId=element_dict.get("threadId"), - type=element_dict.get("type"), - chainlitKey=element_dict.get("chainlitKey"), - url=element_dict.get("url"), - objectKey=element_dict.get("objectKey"), - name=element_dict["name"], - display=element_dict["display"], - size=element_dict.get("size"), - language=element_dict.get("language"), - page=element_dict.get("page"), - autoPlay=element_dict.get("autoPlay"), - playerConfig=element_dict.get("playerConfig"), - forId=element_dict.get("forId"), - mime=element_dict.get("mime"), - ) - else: - return None - - @queue_until_user_message() - async def create_element(self, element: "Element"): - if self.show_logger: - logger.info(f"SQLAlchemy: create_element, element_id = {element.id}") - - if not self.storage_provider: - logger.warning("SQLAlchemy: create_element error. No storage client!") - return - if not element.for_id: - return - - content: Optional[Union[bytes, str]] = None - - if element.path: - async with aiofiles.open(element.path, "rb") as f: - content = await f.read() - elif element.url: - async with aiohttp.ClientSession() as session: - async with session.get(element.url) as response: - if response.status == 200: - content = await response.read() - else: - content = None - elif element.content: - content = element.content - else: - raise ValueError("Element url, path or content must be provided") - if content is None: - raise ValueError("Content is None, cannot upload file") - - user_id: str = await self._get_user_id_by_thread(element.thread_id) or "unknown" - file_object_key = f"{user_id}/{element.id}" + ( - f"/{element.name}" if element.name else "" - ) - - if not element.mime: - element.mime = "application/octet-stream" - - uploaded_file = await self.storage_provider.upload_file( - object_key=file_object_key, data=content, mime=element.mime, overwrite=True - ) - if not uploaded_file: - raise ValueError("Failed to persist data in storage_provider") - - element_dict: ElementDict = element.to_dict() - element_dict["url"] = uploaded_file.get("url") - element_dict["objectKey"] = uploaded_file.get("object_key") - element_dict_cleaned = {k: v for k, v in element_dict.items() if v is not None} - - columns = ", ".join(f'"{column}"' for column in element_dict_cleaned.keys()) - placeholders = ", ".join(f':{column}' for column in element_dict_cleaned.keys()) - query = f"INSERT INTO elements ({columns}) VALUES ({placeholders})" - await self.execute_sql(query=query, parameters=element_dict_cleaned) - - @queue_until_user_message() - async def delete_element(self, element_id: str, thread_id: Optional[str] = None): - if self.show_logger: - logger.info(f"SQLAlchemy: delete_element, element_id={element_id}") - query = 'DELETE FROM elements WHERE "id" = :id' - parameters = {"id": element_id} - await self.execute_sql(query=query, parameters=parameters) - - async def get_all_user_threads( - self, user_id: Optional[str] = None, thread_id: Optional[str] = None - ) -> Optional[List[ThreadDict]]: - if self.show_logger: - logger.info("SQLAlchemy: get_all_user_threads") - user_threads_query = """ - SELECT - "id" AS thread_id, - "createdAt" AS thread_createdat, - "name" AS thread_name, - "userId" AS user_id, - "userIdentifier" AS user_identifier, - "tags" AS thread_tags, - "meta" AS thread_meta - FROM threads - WHERE ("userId" = :user_id OR :user_id IS NULL) - AND ("id" = :thread_id OR :thread_id IS NULL) - ORDER BY "createdAt" DESC - LIMIT :limit - """ - params = { - "user_id": user_id, - "thread_id": thread_id, - "limit": self.user_thread_limit, - } - user_threads = await self.execute_sql( - query=user_threads_query, - parameters=params, - ) - if not isinstance(user_threads, list): - return None - if not user_threads: - return [] - else: - # Build parameterized IN clause using expanding to prevent SQL injection - thread_id_list = [t["thread_id"] for t in user_threads] - tid_params = {"thread_ids": thread_id_list} - - steps_feedbacks_query = """ - SELECT - s."id" AS step_id, - s."name" AS step_name, - s."type" AS step_type, - s."threadId" AS step_threadid, - s."parentId" AS step_parentid, - s."streaming" AS step_streaming, - s."waitForAnswer" AS step_waitforanswer, - s."isError" AS step_iserror, - s."meta" AS step_meta, - s."tags" AS step_tags, - s."input" AS step_input, - s."output" AS step_output, - s."createdAt" AS step_createdat, - s."startTime" AS step_start, - s."endTime" AS step_end, - s."generation" AS step_generation, - s."showInput" AS step_showinput, - s."language" AS step_language, - s."indent" AS step_indent, - f."value" AS feedback_value, - f."comment" AS feedback_comment, - f."id" AS feedback_id - FROM steps s LEFT JOIN feedbacks f ON s."id" = f."forId" - WHERE s."threadId" IN :thread_ids - ORDER BY s."createdAt" ASC - """ - steps_feedbacks = await self.execute_sql( - query=steps_feedbacks_query, parameters=tid_params, expand="thread_ids" - ) - - elements_query = """ - SELECT - e."id" AS element_id, - e."threadId" as element_threadid, - e."type" AS element_type, - e."chainlitKey" AS element_chainlitkey, - e."url" AS element_url, - e."objectKey" as element_objectkey, - e."name" AS element_name, - e."display" AS element_display, - e."size" AS element_size, - e."language" AS element_language, - e."page" AS element_page, - e."forId" AS element_forid, - e."mime" AS element_mime - FROM elements e - WHERE e."threadId" IN :thread_ids - """ - elements = await self.execute_sql( - query=elements_query, parameters=tid_params, expand="thread_ids" - ) - - thread_dicts = {} - for thread in user_threads: - t_id = thread["thread_id"] - meta = thread["thread_meta"] - if isinstance(meta, str): - try: - meta = json.loads(meta) - except: - meta = {} - tags = thread["thread_tags"] - if isinstance(tags, str): - try: - tags = json.loads(tags) - except: - tags = [] - thread_dicts[t_id] = ThreadDict( - id=t_id, - createdAt=thread["thread_createdat"], - name=thread["thread_name"], - userId=thread["user_id"], - userIdentifier=thread["user_identifier"], - tags=tags, - metadata=meta, - steps=[], - elements=[], - ) - - if isinstance(steps_feedbacks, list): - for step_feedback in steps_feedbacks: - t_id = step_feedback["step_threadid"] - if t_id in thread_dicts: - meta = step_feedback["step_meta"] - if isinstance(meta, str): - try: - meta = json.loads(meta) - except: - meta = {} - tags = step_feedback["step_tags"] - if isinstance(tags, str): - try: - tags = json.loads(tags) - except: - tags = [] - feedback = None - if step_feedback["feedback_value"] is not None: - feedback = FeedbackDict( - forId=step_feedback["step_id"], - id=step_feedback.get("feedback_id"), - value=step_feedback["feedback_value"], - comment=step_feedback.get("feedback_comment"), - ) - input_val = step_feedback.get("step_input", "") - show_input = step_feedback.get("step_showinput", "false") - if show_input == "false": - input_val = "" - step_dict = StepDict( - id=step_feedback["step_id"], - name=step_feedback["step_name"], - type=step_feedback["step_type"], - threadId=t_id, - parentId=step_feedback.get("step_parentid"), - streaming=step_feedback.get("step_streaming", False), - waitForAnswer=step_feedback.get("step_waitforanswer"), - isError=step_feedback.get("step_iserror"), - metadata=meta, - tags=tags, - input=input_val, - output=step_feedback.get("step_output", ""), - createdAt=step_feedback.get("step_createdat"), - start=step_feedback.get("step_start"), - end=step_feedback.get("step_end"), - generation=step_feedback.get("step_generation"), - showInput=step_feedback.get("step_showinput"), - language=step_feedback.get("step_language"), - indent=step_feedback.get("step_indent"), - feedback=feedback, - ) - thread_dicts[t_id]["steps"].append(step_dict) - - if isinstance(elements, list): - for element in elements: - t_id = element["element_threadid"] - if t_id in thread_dicts: - element_dict = ElementDict( - id=element["element_id"], - threadId=t_id, - type=element["element_type"], - chainlitKey=element.get("element_chainlitkey"), - url=element.get("element_url"), - objectKey=element.get("element_objectkey"), - name=element["element_name"], - display=element["element_display"], - size=element.get("element_size"), - language=element.get("element_language"), - autoPlay=element.get("element_autoPlay"), - playerConfig=element.get("element_playerconfig"), - page=element.get("element_page"), - forId=element.get("element_forid"), - mime=element.get("element_mime"), - ) - thread_dicts[t_id]["elements"].append(element_dict) - - return list(thread_dicts.values()) - - async def close(self) -> None: - """Close the database connection and cleanup resources.""" - if hasattr(self, 'engine') and self.engine: - await self.engine.dispose() diff --git a/src/praisonai/praisonai/ui_realtime/default_app.py b/src/praisonai/praisonai/ui_realtime/default_app.py index 3b6372b0c..c41473f96 100644 --- a/src/praisonai/praisonai/ui_realtime/default_app.py +++ b/src/praisonai/praisonai/ui_realtime/default_app.py @@ -3,22 +3,23 @@ Loaded by ``praisonai ui realtime`` → ``aiui run app.py``. Replaces ui/realtime.py (Chainlit) with aiui implementation. -NOTE: This is a placeholder implementation. Full WebRTC voice realtime -requires PraisonAIUI WebRTC feature to be implemented. +Uses aiui's OpenAIRealtimeManager for WebRTC voice functionality. Requires: pip install "praisonai[ui]" """ import os import praisonaiui as aiui +from praisonaiui.features.realtime import OpenAIRealtimeManager from praisonai.ui._aiui_datastore import PraisonAISessionDataStore -# ── Set up datastore bridge ───────────────────────────────── +# ── Set up datastore bridge and realtime manager ─────────── aiui.set_datastore(PraisonAISessionDataStore()) +aiui.set_realtime_manager(OpenAIRealtimeManager()) # ── Dashboard style ───────────────────────────────────────── aiui.set_style("dashboard") -aiui.set_branding(title="PraisonAI Realtime (Beta)", logo="šŸŽ¤") +aiui.set_branding(title="PraisonAI Realtime Voice", logo="šŸŽ¤") aiui.set_theme(preset="red", dark_mode=True, radius="lg") aiui.set_pages([ "chat", @@ -37,13 +38,14 @@ async def get_starters(): @aiui.welcome async def on_welcome(): """Welcome with realtime status.""" - await aiui.say("""šŸŽ¤ **PraisonAI Realtime Voice Interface (Beta)** + await aiui.say("""šŸŽ¤ **PraisonAI Realtime Voice Interface** -āš ļø **Note**: Full WebRTC voice realtime is pending PraisonAIUI feature implementation. +Welcome to PraisonAI's voice-powered realtime chat! Use the microphone button to start voice conversations with AI agents. -For now, this provides a text-based interface that simulates realtime interactions. - -See: https://github.com/MervinPraison/PraisonAIUI/issues for WebRTC voice realtime status. +✨ Features: +- Real-time voice input/output via WebRTC +- Session persistence across restarts +- Dashboard with chat history and usage logs """) # Session-scoped realtime agent cache @@ -51,7 +53,7 @@ async def on_welcome(): @aiui.reply async def on_message(message: str): - """Handle realtime interactions (text-based for now).""" + """Handle realtime interactions via voice or text.""" session_id = getattr(aiui.current_session, 'id', 'default') await aiui.think("šŸŽ¤ Processing realtime request...") @@ -69,20 +71,16 @@ async def on_message(message: str): agent = _realtime_cache[session_id] - # For now, process as text (voice coming in future PraisonAIUI release) + # Process the message with the agent result = await agent.achat(str(message)) - # Stream response (simulating voice output) + # Stream response naturally (aiui handles voice output via WebRTC) response_text = str(result) if result else "I'm sorry, I couldn't process that." - await aiui.say("šŸ”Š **Voice Output Simulation:**\n") - - # Stream more slowly to simulate speech + # Stream tokens for smooth output words = response_text.split(" ") for i, word in enumerate(words): await aiui.stream_token(word + (" " if i < len(words) - 1 else "")) - - await aiui.say("\n\n*Note: Actual voice I/O will be available when PraisonAIUI WebRTC feature lands.*") except Exception as e: await aiui.say(f"āŒ Realtime Error: {e}") diff --git a/src/praisonai/tests/cl-test.py b/src/praisonai/tests/cl-test.py deleted file mode 100644 index b31486855..000000000 --- a/src/praisonai/tests/cl-test.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import chainlit as cl -import google.generativeai as genai -from typing import List, Optional, Dict, Any, Union, Literal, Type -from pydantic import BaseModel -import json - -# Configure the Gemini API -genai.configure(api_key=os.environ["GEMINI_API_KEY"]) - -# Define generation configuration -generation_config = { - "temperature": 1, - "top_p": 0.95, - "top_k": 40, - "max_output_tokens": 8192, - "response_mime_type": "text/plain", -} - -def read_system_instructions(): - try: - with open("/Users/praison/praisonai-package/.cursorrules", "r") as file: - return file.read() - except Exception as e: - print(f"Error reading .cursorrules file: {e}") - return "" - -def read_current_file(): - try: - with open("/Users/praison/praisonai-package/praisonai/test.py", "r") as file: - return file.read() - except Exception as e: - print(f"Error reading test.py file: {e}") - return "" - -# Create the GenerativeModel instance with system instructions -model = genai.GenerativeModel( - model_name="gemini-2.0-flash-exp", - generation_config=generation_config, - system_instruction=read_system_instructions(), -) - -@cl.on_chat_start -async def start(): - # Initialize chat session - chat = model.start_chat(history=[]) - - # Store chat in user session - cl.user_session.set("chat", chat) - -@cl.on_message -async def main(message: str): - # Retrieve chat session - chat = cl.user_session.get("chat") - - # Append current file content to user message - file_content = read_current_file() - full_message = f""" -Current file: -{file_content} - -User message: -{message} -""" - - # Send message and get streaming response - response = chat.send_message(full_message, stream=True) - - # Create message placeholder for streaming - msg = cl.Message(content="") - - # Stream the response token by token - for chunk in response: - await msg.stream_token(chunk.text) - - # Send final message - await msg.send() - -@cl.on_chat_end -def end(): - print("Chat ended") - -# Error handling for missing files -@cl.on_stop -def on_stop(): - print("Stopped by user") - -if __name__ == "__main__": - # Verify system files exist - if not os.path.exists("/Users/praison/praisonai-package/.cursorrules"): - print("Warning: .cursorrules file not found") - if not os.path.exists("/Users/praison/praisonai-package/praisonai/test.py"): - print("Warning: test.py file not found") \ No newline at end of file diff --git a/src/praisonai/tests/integration/test_ui_external_agents.py b/src/praisonai/tests/integration/test_ui_external_agents.py deleted file mode 100644 index 45296ba6a..000000000 --- a/src/praisonai/tests/integration/test_ui_external_agents.py +++ /dev/null @@ -1,196 +0,0 @@ -""" -Integration tests for external agents UI functionality. - -Tests the shared helper and UI integration for external agents (Claude/Gemini/Codex/Cursor). -""" - -import pytest -import shutil -from unittest.mock import patch, MagicMock -import sys -import os - -# Mark most tests as unit tests to bypass provider gating -# Only TestRealAgenticIntegration is truly integration-level -pytestmark = pytest.mark.unit - -# Add src path for imports -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) - -from praisonai.ui._external_agents import ( - EXTERNAL_AGENTS, - installed_external_agents, - external_agent_tools, - chainlit_switches, - aiui_settings_entries, -) - - -class TestExternalAgentsHelper: - """Test the shared external agents helper.""" - - def test_external_agents_config(self): - """Test that EXTERNAL_AGENTS is properly configured.""" - assert isinstance(EXTERNAL_AGENTS, dict) - assert "claude_enabled" in EXTERNAL_AGENTS - assert "gemini_enabled" in EXTERNAL_AGENTS - assert "codex_enabled" in EXTERNAL_AGENTS - assert "cursor_enabled" in EXTERNAL_AGENTS - - # Check structure of each agent config - for toggle_id, config in EXTERNAL_AGENTS.items(): - assert "module" in config - assert "cls" in config - assert "label" in config - assert "cli" in config - - @patch('shutil.which') - def test_installed_external_agents_caching(self, mock_which): - """Test that installed_external_agents properly caches results.""" - # Clear cache - from praisonai.ui._external_agents import installed_external_agents - installed_external_agents.cache_clear() - - # Mock some CLIs as available - def mock_which_side_effect(cli): - return "/usr/bin/claude" if cli == "claude" else None - - mock_which.side_effect = mock_which_side_effect - - # First call should check all CLIs - result1 = installed_external_agents() - assert result1 == ["claude_enabled"] - assert mock_which.call_count == 4 # Called for each CLI - - # Second call should use cache - mock_which.reset_mock() - result2 = installed_external_agents() - assert result2 == ["claude_enabled"] - assert mock_which.call_count == 0 # Not called due to cache - - @patch('importlib.import_module') - def test_external_agent_tools(self, mock_import): - """Test external_agent_tools builds tools correctly.""" - # Mock integration classes - mock_integration = MagicMock() - mock_integration.is_available = True - mock_tool_func = MagicMock() - mock_integration.as_tool.return_value = mock_tool_func - - mock_module = MagicMock() - mock_module.ClaudeCodeIntegration.return_value = mock_integration - mock_import.return_value = mock_module - - settings = {"claude_enabled": True, "gemini_enabled": False} - tools = external_agent_tools(settings) - - assert len(tools) == 1 - assert tools[0] == mock_tool_func - mock_import.assert_called_once_with("praisonai.integrations.claude_code") - mock_integration.as_tool.assert_called_once() - - def test_external_agent_tools_unavailable_integration(self): - """Test external_agent_tools handles missing integrations gracefully.""" - settings = {"nonexistent_enabled": True} - tools = external_agent_tools(settings) - assert tools == [] - - @patch('chainlit.input_widget.Switch') - @patch('praisonai.ui._external_agents.installed_external_agents') - def test_chainlit_switches(self, mock_installed, mock_switch): - """Test chainlit_switches generates correct switches.""" - mock_installed.return_value = ["claude_enabled", "gemini_enabled"] - mock_switch_instance = MagicMock() - mock_switch.return_value = mock_switch_instance - - current_settings = {"claude_enabled": True, "gemini_enabled": False} - switches = chainlit_switches(current_settings) - - assert len(switches) == 2 - assert all(s == mock_switch_instance for s in switches) - assert mock_switch.call_count == 2 - - @patch('praisonai.ui._external_agents.installed_external_agents') - def test_aiui_settings_entries(self, mock_installed): - """Test aiui_settings_entries generates correct settings.""" - mock_installed.return_value = ["claude_enabled", "gemini_enabled"] - - settings = aiui_settings_entries() - - assert isinstance(settings, dict) - assert "claude_enabled" in settings - assert "gemini_enabled" in settings - assert settings["claude_enabled"]["type"] == "checkbox" - assert settings["claude_enabled"]["default"] is False - - -class TestBackwardCompatibility: - """Test backward compatibility with legacy settings.""" - - def test_claude_code_enabled_legacy_support(self): - """Test that claude_code_enabled is mapped to claude_enabled.""" - # This would require mocking chainlit session which is complex - # In practice, this is tested by the load_external_agent_settings_from_chainlit function - # which handles the legacy mapping - pass - - -class TestAvailabilityGating: - """Test that unavailable CLIs are properly hidden.""" - - @patch('shutil.which') - def test_only_available_agents_shown(self, mock_which): - """Test that only available external agents are shown in UI.""" - # Clear cache - from praisonai.ui._external_agents import installed_external_agents - installed_external_agents.cache_clear() - - # Mock only Claude as available - mock_which.side_effect = lambda cli: "/usr/bin/claude" if cli == "claude" else None - - available = installed_external_agents() - assert available == ["claude_enabled"] - - # Verify all CLIs were checked - expected_calls = [cli_config["cli"] for cli_config in EXTERNAL_AGENTS.values()] - actual_calls = [call[0][0] for call in mock_which.call_args_list] - assert set(actual_calls) == set(expected_calls) - - -@pytest.mark.integration -@pytest.mark.provider_google # Only this class needs provider gating -class TestRealAgenticIntegration: - """Real agentic tests - requires actual integrations to be available.""" - - @pytest.mark.skipif(not shutil.which("echo"), reason="echo command not available") - def test_external_agent_tools_real_execution(self): - """Test that external agent tools can be created and are callable.""" - # This is a simplified test using echo command to simulate external CLI - # In a real test environment with Claude Code installed, this would test actual execution - - settings = {"claude_enabled": False} # Disable to avoid actual execution - tools = external_agent_tools(settings) - - # Should return empty list when all disabled - assert tools == [] - - # Test with mock available integration would go here if Claude Code was installed - - def test_settings_to_tools_mapping_consistency(self): - """Test that settings keys map consistently to integration modules.""" - for toggle_id, config in EXTERNAL_AGENTS.items(): - # Verify the toggle_id pattern is consistent - assert toggle_id.endswith("_enabled") - - # Verify module name is valid identifier - module_name = config["module"] - assert module_name.replace("_", "").isalnum() - - # Verify class name follows PascalCase Integration pattern - class_name = config["cls"] - assert class_name.endswith("Integration") - assert class_name[0].isupper() - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/src/praisonai/tests/integration/ui/test_ui_pairing_approval.py b/src/praisonai/tests/integration/ui/test_ui_pairing_approval.py deleted file mode 100644 index 40ac28730..000000000 --- a/src/praisonai/tests/integration/ui/test_ui_pairing_approval.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -Integration tests for UI pairing approval. - -Tests the end-to-end flow: pending request → banner shown → approve action → PairingStore.is_approved()==True -""" - -import pytest -import asyncio -import tempfile -import os -from unittest.mock import Mock, AsyncMock, patch - -# Skip if chainlit is not available (optional [ui] extra) -pytest.importorskip("chainlit", reason="chainlit is an optional [ui] extra") - -from praisonai.gateway.pairing import PairingStore -from praisonai.ui._pairing import ( - get_pending_pairings, - approve_pairing, - refresh_pending_banner, - setup_pairing_banner, -) - - -@pytest.fixture -def temp_pairing_store(): - """Create a temporary PairingStore for testing.""" - with tempfile.TemporaryDirectory() as temp_dir: - store = PairingStore(store_dir=temp_dir) - yield store - - -@pytest.fixture -def mock_gateway_server(temp_pairing_store): - """Mock gateway server with actual PairingStore.""" - server = Mock() - server.pairing_store = temp_pairing_store - return server - - -@pytest.fixture -def mock_chainlit_user_admin(): - """Mock Chainlit user session with admin role.""" - with patch("chainlit.user_session") as mock_session: - mock_user = Mock() - mock_user.metadata = {"role": "admin"} - mock_session.get.return_value = mock_user - yield mock_session - - -@pytest.fixture -def mock_chainlit_user_regular(): - """Mock Chainlit user session with regular role.""" - with patch("chainlit.user_session") as mock_session: - mock_user = Mock() - mock_user.metadata = {"role": "user"} - mock_session.get.return_value = mock_user - yield mock_session - - -@pytest.fixture -def mock_chainlit_message(): - """Mock Chainlit Message class.""" - with patch("chainlit.Message") as mock_message_class: - mock_message = Mock() - mock_message.send = AsyncMock() - mock_message_class.return_value = mock_message - yield mock_message_class - - -@pytest.fixture -def mock_chainlit_action(): - """Mock Chainlit Action class.""" - with patch("chainlit.Action") as mock_action_class: - yield mock_action_class - - -class TestGetPendingPairings: - """Test the get_pending_pairings function.""" - - @patch("praisonai.ui._pairing.GATEWAY_TOKEN", "test-token") - @patch("aiohttp.ClientSession") - async def test_get_pending_success(self, mock_session): - """Test successful retrieval of pending pairings.""" - # Mock successful HTTP response - mock_response = AsyncMock() - mock_response.status = 200 - mock_response.json = AsyncMock(return_value={ - "pending": [ - { - "channel": "telegram", - "code": "ABCD1234", - "user_id": "ABCD1234", - "user_name": "User ABCD1234", - "age_seconds": 30 - } - ] - }) - - mock_session_instance = AsyncMock() - mock_session_instance.__aenter__ = AsyncMock(return_value=mock_session_instance) - mock_session_instance.__aexit__ = AsyncMock(return_value=None) - mock_session_instance.get = AsyncMock() - mock_session_instance.get.return_value.__aenter__ = AsyncMock(return_value=mock_response) - mock_session_instance.get.return_value.__aexit__ = AsyncMock(return_value=None) - mock_session.return_value = mock_session_instance - - result = await get_pending_pairings() - - assert len(result) == 1 - assert result[0]["channel"] == "telegram" - assert result[0]["code"] == "ABCD1234" - - @patch("praisonai.ui._pairing.GATEWAY_TOKEN", "") - async def test_get_pending_no_token(self, caplog): - """Test get_pending_pairings with no token.""" - result = await get_pending_pairings() - - assert result == [] - assert "No GATEWAY_AUTH_TOKEN set" in caplog.text - - @patch("praisonai.ui._pairing.GATEWAY_TOKEN", "test-token") - @patch("aiohttp.ClientSession") - async def test_get_pending_http_error(self, mock_session): - """Test get_pending_pairings with HTTP error.""" - # Mock HTTP error response - mock_response = AsyncMock() - mock_response.status = 401 - - mock_session_instance = AsyncMock() - mock_session_instance.__aenter__ = AsyncMock(return_value=mock_session_instance) - mock_session_instance.__aexit__ = AsyncMock(return_value=None) - mock_session_instance.get = AsyncMock() - mock_session_instance.get.return_value.__aenter__ = AsyncMock(return_value=mock_response) - mock_session_instance.get.return_value.__aexit__ = AsyncMock(return_value=None) - mock_session.return_value = mock_session_instance - - result = await get_pending_pairings() - - assert result == [] - - -class TestApprovePairing: - """Test the approve_pairing function.""" - - @patch("praisonai.ui._pairing.GATEWAY_TOKEN", "test-token") - @patch("aiohttp.ClientSession") - async def test_approve_success(self, mock_session): - """Test successful pairing approval.""" - # Mock successful HTTP response - mock_response = AsyncMock() - mock_response.status = 200 - - mock_session_instance = AsyncMock() - mock_session_instance.__aenter__ = AsyncMock(return_value=mock_session_instance) - mock_session_instance.__aexit__ = AsyncMock(return_value=None) - mock_session_instance.post = AsyncMock() - mock_session_instance.post.return_value.__aenter__ = AsyncMock(return_value=mock_response) - mock_session_instance.post.return_value.__aexit__ = AsyncMock(return_value=None) - mock_session.return_value = mock_session_instance - - result = await approve_pairing("telegram", "ABCD1234") - - assert result is True - - @patch("praisonai.ui._pairing.GATEWAY_TOKEN", "") - async def test_approve_no_token(self): - """Test approve_pairing with no token.""" - result = await approve_pairing("telegram", "ABCD1234") - - assert result is False - - @patch("praisonai.ui._pairing.GATEWAY_TOKEN", "test-token") - @patch("aiohttp.ClientSession") - async def test_approve_http_error(self, mock_session): - """Test approve_pairing with HTTP error.""" - # Mock HTTP error response - mock_response = AsyncMock() - mock_response.status = 404 - - mock_session_instance = AsyncMock() - mock_session_instance.__aenter__ = AsyncMock(return_value=mock_session_instance) - mock_session_instance.__aexit__ = AsyncMock(return_value=None) - mock_session_instance.post = AsyncMock() - mock_session_instance.post.return_value.__aenter__ = AsyncMock(return_value=mock_response) - mock_session_instance.post.return_value.__aexit__ = AsyncMock(return_value=None) - mock_session.return_value = mock_session_instance - - result = await approve_pairing("telegram", "ABCD1234") - - assert result is False - - -class TestRefreshPendingBanner: - """Test the refresh_pending_banner function.""" - - async def test_banner_not_shown_for_non_admin( - self, - mock_chainlit_user_regular, - mock_chainlit_message - ): - """Test that banner is not shown for non-admin users.""" - await refresh_pending_banner() - - # Message should not be sent for non-admin - mock_chainlit_message.assert_not_called() - - async def test_banner_not_shown_with_no_pending( - self, - mock_chainlit_user_admin, - mock_chainlit_message - ): - """Test that banner is not shown when no pending requests.""" - with patch("praisonai.ui._pairing.get_pending_pairings", return_value=[]): - await refresh_pending_banner() - - # Message should not be sent when no pending - mock_chainlit_message.assert_not_called() - - async def test_banner_shown_for_admin_with_pending( - self, - mock_chainlit_user_admin, - mock_chainlit_message, - mock_chainlit_action - ): - """Test that banner is shown for admin with pending requests.""" - mock_pending = [ - { - "channel": "telegram", - "code": "ABCD1234", - "user_name": "User ABCD1234", - "age_seconds": 30 - } - ] - - with patch("praisonai.ui._pairing.get_pending_pairings", return_value=mock_pending): - await refresh_pending_banner() - - # Message should be created and sent - mock_chainlit_message.assert_called_once() - mock_message_call = mock_chainlit_message.call_args - - # Check message content - assert "šŸ””" in mock_message_call[1]["content"] - assert "1 pending pairing request(s)" in mock_message_call[1]["content"] - - # Verify action contract - actions = mock_message_call[1]["actions"] - assert len(actions) == 2 # Should have both approve and deny actions - - # Check approve action - approve_action = actions[0] - assert approve_action.name == "approve_pairing" - assert approve_action.value == "telegram:ABCD1234" - assert "āœ… Approve" in approve_action.label - - # Check deny action - deny_action = actions[1] - assert deny_action.name == "deny_pairing" - assert deny_action.value == "telegram:ABCD1234" - assert "āŒ Deny" in deny_action.label - - async def test_banner_multiple_pending( - self, - mock_chainlit_user_admin, - mock_chainlit_message, - mock_chainlit_action - ): - """Test banner with multiple pending requests.""" - mock_pending = [ - { - "channel": "telegram", - "code": "ABCD1234", - "user_name": "User ABCD1234", - "age_seconds": 30 - }, - { - "channel": "slack", - "code": "EFGH5678", - "user_name": "User EFGH5678", - "age_seconds": 120 - } - ] - - with patch("praisonai.ui._pairing.get_pending_pairings", return_value=mock_pending): - await refresh_pending_banner() - - # Message should be created with 2 actions - mock_chainlit_message.assert_called_once() - mock_message_call = mock_chainlit_message.call_args - - assert "2 pending pairing request(s)" in mock_message_call[1]["content"] - - # Should have 4 actions total (2 pending Ɨ 2 actions each) - actions = mock_message_call[1]["actions"] - assert len(actions) == 4 - - # Verify first pairing actions - assert actions[0].name == "approve_pairing" - assert actions[0].value == "telegram:ABCD1234" - assert actions[1].name == "deny_pairing" - assert actions[1].value == "telegram:ABCD1234" - - # Verify second pairing actions - assert actions[2].name == "approve_pairing" - assert actions[2].value == "slack:EFGH5678" - assert actions[3].name == "deny_pairing" - assert actions[3].value == "slack:EFGH5678" - - -class TestIntegrationFlow: - """Test the complete integration flow.""" - - async def test_end_to_end_pairing_flow(self, temp_pairing_store): - """Test complete flow: generate code → pending request → approve → is_paired.""" - # 1. Generate a pairing code - code = temp_pairing_store.generate_code(channel_type="telegram") - assert len(code) == 8 - - # 2. Verify it shows up in pending list - pending = temp_pairing_store.list_pending() - assert len(pending) == 1 - assert pending[0]["code"] == code - assert pending[0]["channel"] == "telegram" - - # 3. Approve the pairing - success = temp_pairing_store.approve("telegram", code, user_id="12345") - assert success is True - - # 4. Verify it's now paired - assert temp_pairing_store.is_paired("12345", "telegram") is True - - # 5. Verify no longer in pending - pending_after = temp_pairing_store.list_pending() - assert len(pending_after) == 0 - - async def test_invalid_code_approval_flow(self, temp_pairing_store): - """Test approval flow with invalid code.""" - # Try to approve non-existent code - success = temp_pairing_store.approve("telegram", "INVALID", user_id="12345") - assert success is False - - # Verify not paired - assert temp_pairing_store.is_paired("12345", "telegram") is False - - async def test_expired_code_flow(self, temp_pairing_store): - """Test that expired codes are cleaned up.""" - # Generate a code with very short TTL - store_short_ttl = PairingStore( - store_dir=temp_pairing_store._dir, - code_ttl=0.1 # 100ms TTL - ) - - code = store_short_ttl.generate_code(channel_type="telegram") - - # Wait for expiration - await asyncio.sleep(0.2) - - # Try to approve expired code - success = store_short_ttl.approve("telegram", code, user_id="12345") - assert success is False - - # Verify not paired - assert store_short_ttl.is_paired("12345", "telegram") is False \ No newline at end of file diff --git a/src/praisonai/tests/unit/test_chainlit_compat.py b/src/praisonai/tests/unit/test_chainlit_compat.py deleted file mode 100644 index f9d9ab06a..000000000 --- a/src/praisonai/tests/unit/test_chainlit_compat.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -Unit tests for the Chainlit compatibility shim. - -Tests ensure forward/backward compatibility with Chainlit versions -that may have different locations for EXPIRY_TIME and BaseStorageClient. -""" - -import os -import sys -import pytest -from unittest.mock import patch - - -class TestChainlitCompat: - """Tests for praisonai.ui.chainlit_compat module.""" - - def test_get_expiry_seconds_returns_int(self): - """Test that get_expiry_seconds returns an integer.""" - from praisonai.ui.chainlit_compat import get_expiry_seconds - result = get_expiry_seconds() - assert isinstance(result, int) - assert result > 0 - - def test_get_expiry_seconds_default_value(self): - """Test that default expiry is 3600 seconds (1 hour).""" - from praisonai.ui.chainlit_compat import DEFAULT_EXPIRY_SECONDS - assert DEFAULT_EXPIRY_SECONDS == 3600 - - def test_expiry_time_alias_exists(self): - """Test that EXPIRY_TIME is exported for backward compatibility.""" - from praisonai.ui.chainlit_compat import EXPIRY_TIME - assert isinstance(EXPIRY_TIME, int) - assert EXPIRY_TIME > 0 - - def test_get_expiry_seconds_env_override(self): - """Test that STORAGE_EXPIRY_TIME env var overrides default.""" - with patch.dict(os.environ, {"STORAGE_EXPIRY_TIME": "7200"}): - # Need to reimport to pick up env change - from praisonai.ui import chainlit_compat - import importlib - importlib.reload(chainlit_compat) - result = chainlit_compat.get_expiry_seconds() - assert result == 7200 - - def test_get_expiry_seconds_invalid_env_fallback(self): - """Test that invalid STORAGE_EXPIRY_TIME falls back to default.""" - with patch.dict(os.environ, {"STORAGE_EXPIRY_TIME": "invalid"}): - from praisonai.ui import chainlit_compat - import importlib - importlib.reload(chainlit_compat) - result = chainlit_compat.get_expiry_seconds() - # Should fall back to chainlit value or default - assert isinstance(result, int) - assert result > 0 - - def test_base_storage_client_import(self): - """Test that BaseStorageClient can be imported.""" - from praisonai.ui.chainlit_compat import BaseStorageClient - # May be None if chainlit not installed, or a class if installed - if BaseStorageClient is not None: - assert hasattr(BaseStorageClient, '__mro__') - - def test_get_base_storage_client_function(self): - """Test get_base_storage_client returns class or None.""" - from praisonai.ui.chainlit_compat import get_base_storage_client - result = get_base_storage_client() - # Should be None or a class - assert result is None or hasattr(result, '__mro__') - - def test_module_exports_all(self): - """Test that __all__ contains expected exports.""" - from praisonai.ui import chainlit_compat - expected = [ - 'get_expiry_seconds', - 'get_base_storage_client', - 'get_base_data_layer', - 'base_data_layer_has_close', - 'EXPIRY_TIME', - 'BaseStorageClient', - 'DEFAULT_EXPIRY_SECONDS', - ] - for name in expected: - assert name in chainlit_compat.__all__ - - def test_get_base_data_layer_function(self): - """Test get_base_data_layer returns class or None.""" - from praisonai.ui.chainlit_compat import get_base_data_layer - result = get_base_data_layer() - # Should be None or a class - assert result is None or hasattr(result, '__mro__') - - def test_base_data_layer_has_close_function(self): - """Test base_data_layer_has_close returns bool.""" - from praisonai.ui.chainlit_compat import base_data_layer_has_close - result = base_data_layer_has_close() - assert isinstance(result, bool) - - -class TestChainlitCompatWithMocking: - """Tests with mocked chainlit imports to simulate version differences.""" - - def test_fallback_when_storage_expiry_time_not_found(self): - """Test fallback when storage_expiry_time doesn't exist in chainlit.""" - # This tests the try/except fallback logic - from praisonai.ui.chainlit_compat import get_expiry_seconds - result = get_expiry_seconds() - # Should not raise, should return a valid int - assert isinstance(result, int) - - def test_fallback_when_expiry_time_uppercase_not_found(self): - """Test fallback when EXPIRY_TIME (uppercase) doesn't exist.""" - from praisonai.ui.chainlit_compat import get_expiry_seconds - result = get_expiry_seconds() - assert isinstance(result, int) - - -class TestSQLAlchemyDataLayerCompat: - """Tests for SQLAlchemyDataLayer compatibility with Chainlit 2.9.4.""" - - def test_sql_alchemy_imports_from_compat_shim(self): - """Test that sql_alchemy.py imports from chainlit_compat.""" - # Read the file directly to check imports without executing it - import os - sql_alchemy_path = os.path.join( - os.path.dirname(__file__), - '..', '..', 'praisonai', 'ui', 'sql_alchemy.py' - ) - with open(sql_alchemy_path, 'r') as f: - content = f.read() - - # Should import from chainlit_compat, not directly from chainlit - assert 'from praisonai.ui.chainlit_compat import' in content - # Should NOT have direct EXPIRY_TIME import from chainlit - assert 'from chainlit.data.storage_clients.base import EXPIRY_TIME' not in content - - def test_sql_alchemy_has_close_method(self): - """Test that SQLAlchemyDataLayer has close method for Chainlit 2.9.4.""" - # Import the module to check the class - import sys - # Add UI path for local imports - ui_path = '/Users/praison/praisonai-package/src/praisonai/praisonai/ui' - if ui_path not in sys.path: - sys.path.insert(0, ui_path) - - try: - from praisonai.ui.sql_alchemy import SQLAlchemyDataLayer - # Check that close method exists - assert hasattr(SQLAlchemyDataLayer, 'close') - # Check it's an async method - import inspect - assert inspect.iscoroutinefunction(SQLAlchemyDataLayer.close) - except ImportError: - # If chainlit not installed, skip - pytest.skip("Chainlit not installed") - - -class TestCLILazyImports: - """Tests to ensure CLI doesn't import chainlit at startup.""" - - def test_praisonai_import_no_chainlit(self): - """Test that importing praisonai doesn't import chainlit.""" - # Clear any cached chainlit imports - chainlit_modules = [k for k in sys.modules.keys() if 'chainlit' in k.lower()] - for mod in chainlit_modules: - del sys.modules[mod] - - # Import praisonai - import praisonai - - # chainlit should not be in sys.modules yet - # (unless it was imported by something else) - # We just verify the import doesn't crash - assert praisonai is not None - - def test_praisonai_cli_import_no_crash(self): - """Test that importing praisonai.cli.main doesn't crash.""" - import praisonai.cli.main - assert praisonai.cli.main is not None - - -class TestChatPyLogLevel: - """Tests for chat.py LOGLEVEL handling.""" - - def test_empty_loglevel_handled(self): - """Test that empty LOGLEVEL string doesn't crash.""" - # The fix: log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO" - # This ensures empty string falls back to INFO - with patch.dict(os.environ, {"LOGLEVEL": ""}): - log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO" - assert log_level == "INFO" - - def test_valid_loglevel_preserved(self): - """Test that valid LOGLEVEL is preserved.""" - with patch.dict(os.environ, {"LOGLEVEL": "DEBUG"}): - log_level = os.getenv("LOGLEVEL", "INFO").upper() or "INFO" - assert log_level == "DEBUG" - - -class TestLocalFileStorageClient: - """Tests for LocalFileStorageClient.""" - - def test_local_storage_client_creation(self): - """Test that LocalFileStorageClient can be created.""" - from praisonai.ui.chainlit_compat import LocalFileStorageClient - client = LocalFileStorageClient(storage_dir="/tmp/test_storage") - assert client is not None - assert client.storage_dir == "/tmp/test_storage" - - def test_create_local_storage_client_function(self): - """Test create_local_storage_client helper function.""" - from praisonai.ui.chainlit_compat import create_local_storage_client - client = create_local_storage_client(storage_dir="/tmp/test_storage2") - assert client is not None - - def test_local_storage_client_default_dir(self): - """Test LocalFileStorageClient uses default directory.""" - from praisonai.ui.chainlit_compat import LocalFileStorageClient - with patch.dict(os.environ, {"CHAINLIT_APP_ROOT": "/tmp/test_chainlit"}): - client = LocalFileStorageClient() - assert ".files" in client.storage_dir - - -class TestToolsLoading: - """Tests for custom tools loading.""" - - def test_tools_loading_no_file_no_warning(self): - """Test that no warning is shown when no tools.py exists.""" - # The fix ensures no warning when tools.py doesn't exist - # This is tested by the fact that praisonai chat runs without tools warning - pass - - def test_praisonai_tools_path_env_var(self): - """Test PRAISONAI_TOOLS_PATH environment variable is respected.""" - # This tests the resolution order in load_custom_tools - assert os.getenv("PRAISONAI_TOOLS_PATH") is None or True - - -class TestAuthDefaults: - """Tests for authentication defaults.""" - - def test_default_credentials_warning(self): - """Test that default credentials trigger a warning.""" - # The warning is: "Using default admin credentials..." - # This is verified by running praisonai chat and seeing the warning - pass - - def test_chainlit_username_env_var(self): - """Test CHAINLIT_USERNAME environment variable.""" - with patch.dict(os.environ, {"CHAINLIT_USERNAME": "testuser"}): - assert os.getenv("CHAINLIT_USERNAME") == "testuser" - - def test_chainlit_password_env_var(self): - """Test CHAINLIT_PASSWORD environment variable.""" - with patch.dict(os.environ, {"CHAINLIT_PASSWORD": "testpass"}): - assert os.getenv("CHAINLIT_PASSWORD") == "testpass" - - -class TestDBPersistence: - """Tests for database persistence.""" - - def test_sqlite_database_exists(self): - """Test that SQLite database file exists.""" - db_path = os.path.expanduser("~/.praison/database.sqlite") - # Database should exist after running praisonai chat - if os.path.exists(db_path): - assert os.path.isfile(db_path) - - def test_sqlite_tables_exist(self): - """Test that required tables exist in SQLite database.""" - import sqlite3 - db_path = os.path.expanduser("~/.praison/database.sqlite") - if os.path.exists(db_path): - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") - tables = [t[0] for t in cursor.fetchall()] - conn.close() - - expected_tables = ['users', 'threads', 'steps', 'elements', 'feedbacks', 'settings'] - for table in expected_tables: - assert table in tables, f"Table {table} not found" - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/src/praisonai/tests/unit/test_tools_and_ui.py b/src/praisonai/tests/unit/test_tools_and_ui.py index 994f02d4e..566573689 100644 --- a/src/praisonai/tests/unit/test_tools_and_ui.py +++ b/src/praisonai/tests/unit/test_tools_and_ui.py @@ -193,21 +193,6 @@ def test_streamlit_app_config(self): assert streamlit_config["page_icon"] == "šŸ¤–" assert streamlit_config["layout"] == "wide" - def test_chainlit_app_config(self): - """Test Chainlit app configuration.""" - chainlit_config = { - "name": "PraisonAI Agent", - "description": "Interact with PraisonAI agents", - "author": "PraisonAI Team", - "tags": ["ai", "agents", "chat"], - "public": False, - "authentication": True - } - - assert chainlit_config["name"] == "PraisonAI Agent" - assert chainlit_config["authentication"] is True - assert "ai" in chainlit_config["tags"] - def test_ui_agent_wrapper(self, sample_agent_config): """Test UI agent wrapper functionality.""" class UIAgentWrapper: diff --git a/src/praisonai/tests/unit/test_ui_external_agents_helper.py b/src/praisonai/tests/unit/test_ui_external_agents_helper.py deleted file mode 100644 index 108bd9244..000000000 --- a/src/praisonai/tests/unit/test_ui_external_agents_helper.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -import types - -from praisonai.ui import _external_agents as ext - - -class _FakeUserSession: - def __init__(self, values): - self._values = values - - def get(self, key, default=None): - return self._values.get(key, default) - - -def test_load_external_agent_settings_uses_explicit_loader(monkeypatch): - fake_chainlit = types.SimpleNamespace(user_session=_FakeUserSession({})) - monkeypatch.setitem(sys.modules, "chainlit", fake_chainlit) - - values = { - "claude_code_enabled": "true", - "claude_enabled": "false", - "gemini_enabled": "true", - "codex_enabled": "1", - } - settings = ext.load_external_agent_settings_from_chainlit(lambda key: values.get(key)) - - assert set(settings.keys()) == set(ext.EXTERNAL_AGENTS.keys()) - assert settings["claude_enabled"] is False - assert settings["gemini_enabled"] is True - assert settings["codex_enabled"] is True - - -def test_load_external_agent_settings_session_overrides_persistent(monkeypatch): - fake_chainlit = types.SimpleNamespace( - user_session=_FakeUserSession({"gemini_enabled": True, "claude_code_enabled": False}) - ) - monkeypatch.setitem(sys.modules, "chainlit", fake_chainlit) - - values = {"gemini_enabled": "false"} - settings = ext.load_external_agent_settings_from_chainlit(lambda key: values.get(key)) - - assert settings["gemini_enabled"] is True diff --git a/src/praisonai/tests/unit/ui/test_ui_bind_aware_creds.py b/src/praisonai/tests/unit/ui/test_ui_bind_aware_creds.py deleted file mode 100644 index 1b2e0ca76..000000000 --- a/src/praisonai/tests/unit/ui/test_ui_bind_aware_creds.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Unit tests for UI bind-aware credential enforcement. - -Tests that UI refuses default admin/admin credentials on external interfaces. -""" - -import os -import pytest -from unittest.mock import patch, MagicMock - -pytest.importorskip("chainlit", reason="chainlit is an optional [ui] extra") - -from praisonai.ui._auth import register_password_auth, UIStartupError - - -class TestUIAuthValidation: - """Test UI authentication validation logic.""" - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "admin", - "PRAISONAI_ALLOW_DEFAULT_CREDS": "" - }) - def test_default_creds_on_loopback_warns_only(self): - """Test default credentials on loopback show warning but allow start.""" - mock_app = MagicMock() - - # Should not raise exception on loopback - register_password_auth(mock_app, bind_host="127.0.0.1") - register_password_auth(mock_app, bind_host="localhost") - register_password_auth(mock_app, bind_host="::1") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "admin", - "PRAISONAI_ALLOW_DEFAULT_CREDS": "" - }) - def test_default_creds_on_external_blocks_start(self): - """Test default credentials on external interface block start.""" - mock_app = MagicMock() - - with pytest.raises(UIStartupError) as exc_info: - register_password_auth(mock_app, bind_host="0.0.0.0") - - assert "Cannot bind to 0.0.0.0 with default admin/admin credentials" in str(exc_info.value) - assert "CHAINLIT_USERNAME" in str(exc_info.value) - assert "CHAINLIT_PASSWORD" in str(exc_info.value) - - # Test other external interfaces - external_hosts = ["192.168.1.1", "10.0.0.1", "8.8.8.8"] - for host in external_hosts: - with pytest.raises(UIStartupError): - register_password_auth(mock_app, bind_host=host) - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "admin", - "PRAISONAI_ALLOW_DEFAULT_CREDS": "1" - }) - def test_escape_hatch_allows_default_creds(self): - """Test escape hatch allows default credentials on external interface.""" - mock_app = MagicMock() - - # Should not raise with escape hatch - register_password_auth(mock_app, bind_host="0.0.0.0") - register_password_auth(mock_app, bind_host="192.168.1.1") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "myuser", - "CHAINLIT_PASSWORD": "mypass" - }) - def test_custom_creds_always_allowed(self): - """Test custom credentials are always allowed.""" - mock_app = MagicMock() - - # Should not raise on any interface with custom creds - register_password_auth(mock_app, bind_host="127.0.0.1") - register_password_auth(mock_app, bind_host="0.0.0.0") - register_password_auth(mock_app, bind_host="192.168.1.1") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "different" - }) - def test_non_default_admin_allowed(self): - """Test non-default admin credentials are allowed.""" - mock_app = MagicMock() - - # Should not raise - only username is default - register_password_auth(mock_app, bind_host="0.0.0.0") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "different", - "CHAINLIT_PASSWORD": "admin" - }) - def test_non_default_password_allowed(self): - """Test non-default password with admin username is allowed.""" - mock_app = MagicMock() - - # Should not raise - only password is default - register_password_auth(mock_app, bind_host="0.0.0.0") - - -class TestChainlitAuthCallback: - """Test the actual Chainlit auth callback behavior.""" - - def test_auth_callback_registration(self): - """Test that auth callback gets registered with Chainlit.""" - mock_app = MagicMock() - - with patch('chainlit.password_auth_callback') as mock_decorator: - mock_decorator.return_value = lambda f: f # Mock decorator - - register_password_auth(mock_app, bind_host="127.0.0.1") - - # Verify decorator was called - mock_decorator.assert_called_once() - - -class TestUISecurityScenarios: - """Test real-world UI security scenarios.""" - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "admin" - }) - def test_local_development_scenario(self): - """Test local development scenario (default creds, loopback).""" - mock_app = MagicMock() - - # Should warn but allow (typical development scenario) - register_password_auth(mock_app, bind_host="127.0.0.1") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "admin", - "PRAISONAI_ALLOW_DEFAULT_CREDS": "" - }) - def test_accidental_production_deploy_blocked(self): - """Test accidental production deploy with default creds is blocked.""" - mock_app = MagicMock() - - # Should block (prevents shipping with admin/admin to LAN/internet) - with pytest.raises(UIStartupError): - register_password_auth(mock_app, bind_host="0.0.0.0") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "prod-user", - "CHAINLIT_PASSWORD": "secure-pass-123" - }) - def test_secure_production_deploy_allowed(self): - """Test secure production deploy with custom creds is allowed.""" - mock_app = MagicMock() - - # Should allow (secure production deployment) - register_password_auth(mock_app, bind_host="0.0.0.0") - - @patch.dict(os.environ, { - "CHAINLIT_USERNAME": "admin", - "CHAINLIT_PASSWORD": "admin", - "PRAISONAI_ALLOW_DEFAULT_CREDS": "1" - }) - def test_lab_demo_escape_hatch(self): - """Test lab/demo escape hatch scenario.""" - mock_app = MagicMock() - - # Should allow with warning (demo/lab scenario) - register_password_auth(mock_app, bind_host="0.0.0.0") \ No newline at end of file