diff --git a/src/praisonai/praisonai/claw/default_app.py b/src/praisonai/praisonai/claw/default_app.py index dc3003575..5421cd914 100644 --- a/src/praisonai/praisonai/claw/default_app.py +++ b/src/praisonai/praisonai/claw/default_app.py @@ -9,6 +9,10 @@ import os import praisonaiui as aiui +from praisonai.ui._aiui_datastore import PraisonAISessionDataStore + +# ── Set up datastore bridge ───────────────────────────────── +aiui.set_datastore(PraisonAISessionDataStore()) # ── Dashboard style ───────────────────────────────────────── aiui.set_style("dashboard") diff --git a/src/praisonai/praisonai/cli/commands/realtime.py b/src/praisonai/praisonai/cli/commands/realtime.py index 69f304a82..a2e52294e 100644 --- a/src/praisonai/praisonai/cli/commands/realtime.py +++ b/src/praisonai/praisonai/cli/commands/realtime.py @@ -16,30 +16,33 @@ def realtime_main( ctx: typer.Context, model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"), + port: int = typer.Option(8085, "--port", "-p", help="Port for realtime UI"), ): """ Start realtime interaction mode. + Now routes to the new aiui-based realtime interface. + Examples: praisonai realtime - praisonai realtime --model gpt-4o + praisonai realtime --model gpt-4o --port 9000 """ - from praisonai.cli.main import PraisonAI - import sys + # Route to new UI realtime subcommand + from praisonai.cli.commands.ui import _launch_aiui_app + import os - argv = ['realtime'] if model: - argv.extend(['--model', model]) - if verbose: - argv.append('--verbose') + os.environ["MODEL_NAME"] = model - original_argv = sys.argv - sys.argv = ['praisonai'] + argv + print("🎤 Launching PraisonAI Realtime Voice Interface...") + print("Note: Migrated from Chainlit to aiui. Full WebRTC voice coming soon.") - try: - praison = PraisonAI() - praison.main() - except SystemExit: - pass - finally: - sys.argv = original_argv + _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" + ) diff --git a/src/praisonai/praisonai/cli/commands/ui.py b/src/praisonai/praisonai/cli/commands/ui.py index fcf425b52..ff716f0fe 100644 --- a/src/praisonai/praisonai/cli/commands/ui.py +++ b/src/praisonai/praisonai/cli/commands/ui.py @@ -37,6 +37,71 @@ def _ensure_default_app() -> Path: return DEFAULT_APP +def _launch_aiui_app( + app_dir: str, + default_app_name: str, + port: int, + host: str, + app_file: Optional[str], + reload: bool, + ui_name: str +) -> None: + """Common function to launch aiui apps.""" + # 1. Check praisonaiui is installed + try: + import importlib.util + if importlib.util.find_spec("praisonaiui") is None: + raise ImportError + except ImportError: + print(f"\n\033[91mERROR: PraisonAI UI (aiui) is not installed.\033[0m") + print('\nInstall with:\n pip install "praisonai[ui]"\n') + sys.exit(1) + + # 2. Resolve app file + if app_file: + resolved = Path(app_file) + if not resolved.exists(): + print(f"\033[91mERROR: App file not found: {app_file}\033[0m") + sys.exit(1) + else: + ui_dir = Path.home() / ".praisonai" / app_dir + default_app = ui_dir / "app.py" + + # Ensure default app exists + if not default_app.exists(): + ui_dir.mkdir(parents=True, exist_ok=True) + bundled = Path(__file__).parent.parent.parent / default_app_name / "default_app.py" + if not bundled.exists(): + print(f"\033[91mERROR: Bundled default_app.py not found at {bundled}\033[0m") + sys.exit(1) + default_app.write_text(bundled.read_text()) + print(f" ✓ Created default {ui_name} config: {default_app}") + + resolved = default_app + + # 3. Launch via aiui run + import subprocess + + cmd = ["aiui", "run", str(resolved), "--port", str(port), "--host", host] + if reload: + cmd.append("--reload") + + print(f"\n🤖 PraisonAI {ui_name} starting at http://{host}:{port}") + print(f" App: {resolved}\n") + + try: + subprocess.run(cmd, check=True) + except FileNotFoundError: + # Fallback: python -m praisonaiui.cli + cmd = [sys.executable, "-m", "praisonaiui.cli", "run", str(resolved), + "--port", str(port), "--host", host] + if reload: + cmd.append("--reload") + subprocess.run(cmd, check=True) + except KeyboardInterrupt: + print(f"\n🤖 {ui_name} stopped.") + + @app.callback(invoke_without_command=True) def ui( ctx: typer.Context, @@ -60,10 +125,14 @@ def ui( praisonai ui praisonai ui --port 9000 praisonai ui --app my-chat.py + praisonai ui agents # YAML agents dashboard + praisonai ui bot # Bot interface + praisonai ui realtime # Voice realtime """ if ctx.invoked_subcommand is not None: return + # Use legacy implementation for backward compatibility # 1. Check praisonaiui is installed try: import importlib.util @@ -104,3 +173,57 @@ def ui( subprocess.run(cmd, check=True) except KeyboardInterrupt: print("\n🤖 Chat stopped.") + + +@app.command() +def agents( + port: int = typer.Option(8083, "--port", "-p", help="Port to run agents UI on"), + host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), + app_file: Optional[str] = typer.Option( + None, "--app", "-a", help="Custom app.py file" + ), + reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload"), +): + """ + Launch YAML Agents Dashboard. + + Replaces the old Chainlit agents interface with aiui. + Loads agents from agents.yaml in the current directory. + """ + _launch_aiui_app("ui_agents", "ui_agents", port, host, app_file, reload, "Agents Dashboard") + + +@app.command() +def bot( + port: int = typer.Option(8084, "--port", "-p", help="Port to run bot UI on"), + host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), + app_file: Optional[str] = typer.Option( + None, "--app", "-a", help="Custom app.py file" + ), + reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload"), +): + """ + Launch Bot Interface. + + Replaces the old Chainlit bot interface with aiui. + Provides step-by-step interaction visualization. + """ + _launch_aiui_app("ui_bot", "ui_bot", port, host, app_file, reload, "Bot Interface") + + +@app.command() +def realtime( + port: int = typer.Option(8085, "--port", "-p", help="Port to run realtime UI on"), + host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), + app_file: Optional[str] = typer.Option( + None, "--app", "-a", help="Custom app.py file" + ), + reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload"), +): + """ + Launch Realtime Voice Interface (Beta). + + Replaces the old Chainlit realtime interface with aiui. + Note: Full WebRTC voice is pending PraisonAIUI implementation. + """ + _launch_aiui_app("ui_realtime", "ui_realtime", port, host, app_file, reload, "Realtime Voice") diff --git a/src/praisonai/praisonai/cli/main.py b/src/praisonai/praisonai/cli/main.py index 6a30995fd..5d6897038 100644 --- a/src/praisonai/praisonai/cli/main.py +++ b/src/praisonai/praisonai/cli/main.py @@ -726,7 +726,10 @@ def __init__(self): if args.ui == "gradio": self.create_gradio_interface() elif args.ui == "chainlit": - self.create_chainlit_interface() + # Deprecation warning and route to new aiui agents interface + print("\n\033[93mWARNING: --ui chainlit is deprecated and will be removed in a future release.\033[0m") + print("Launching the new aiui-based agents interface instead...") + self.create_aiui_agents_interface() else: # Modify code to allow default UI AgentsGenerator = _get_agents_generator() @@ -5296,6 +5299,27 @@ def create_realtime_interface(self): 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). + + Routes to the new `praisonai ui agents` subcommand. + """ + try: + from praisonai.cli.commands.ui import _launch_aiui_app + print("🤖 Launching PraisonAI Agents Dashboard (aiui)...") + _launch_aiui_app( + app_dir="ui_agents", + default_app_name="ui_agents", + port=8082, # Use same port as old Chainlit agents + host="0.0.0.0", + app_file=None, + reload=False, + ui_name="Agents Dashboard" + ) + except ImportError: + print("ERROR: PraisonAI UI (aiui) is not installed. Please install it with 'pip install \"praisonai[ui]\"' to use the agents dashboard.") + def handle_context_command(self, url: str, goal: str, auto_analyze: bool = False) -> str: """ Handle the context command by creating a ContextAgent and running it. diff --git a/src/praisonai/praisonai/ui/_aiui_datastore.py b/src/praisonai/praisonai/ui/_aiui_datastore.py new file mode 100644 index 000000000..ec1857c68 --- /dev/null +++ b/src/praisonai/praisonai/ui/_aiui_datastore.py @@ -0,0 +1,108 @@ +"""Bridge praisonaiagents SessionStore → aiui BaseDataStore. + +Allows any SessionStoreProtocol implementation (file/Redis/Mongo) to back +the aiui dashboard's Sessions page. No Chainlit required. +""" +from __future__ import annotations + +import logging +import uuid +from typing import Any, Optional + +logger = logging.getLogger(__name__) + +# Fail loudly on missing optional dependencies per AGENTS.md §4.2 +try: + from praisonaiui.datastore import BaseDataStore +except ImportError as e: + raise ImportError( + "praisonaiui is required for PraisonAISessionDataStore. " + "Install with: pip install 'praisonai[ui]'" + ) from e + +try: + from praisonaiagents.session import SessionStoreProtocol + from praisonaiagents.session import get_hierarchical_session_store +except ImportError as e: + raise ImportError( + "praisonaiagents is required for PraisonAISessionDataStore. " + "Install with: pip install praisonaiagents" + ) from e + + +class PraisonAISessionDataStore(BaseDataStore): + """Adapter that bridges PraisonAI SessionStoreProtocol to aiui BaseDataStore.""" + + def __init__(self, store: Optional[SessionStoreProtocol] = None): + """Initialize with an optional session store, defaults to hierarchical store.""" + self._store = store or get_hierarchical_session_store() + + def _new_id(self) -> str: + """Generate a new session ID.""" + return str(uuid.uuid4()) + + async def list_sessions(self) -> list[dict[str, Any]]: + """List all available sessions.""" + # Check if store supports listing (DefaultSessionStore/HierarchicalSessionStore do) + list_fn = getattr(self._store, "list_sessions", None) + if list_fn is None: + return [] # Protocol implementation doesn't support listing + + try: + # DefaultSessionStore/HierarchicalSessionStore return list[dict] + return list_fn(limit=50) or [] + except Exception: + logger.exception("Failed to list sessions") + return [] + + async def get_session(self, session_id: str) -> Optional[dict[str, Any]]: + """Get a specific session by ID.""" + if not self._store.session_exists(session_id): + return None + + try: + chat_history = self._store.get_chat_history(session_id) + return { + "id": session_id, + "messages": chat_history or [], + } + except Exception: + logger.exception("Failed to load session %s", session_id) + return None + + async def create_session(self, session_id: Optional[str] = None) -> dict[str, Any]: + """Create a new session.""" + sid = session_id or self._new_id() + # Sessions are created lazily on first add_message + return { + "id": sid, + "messages": [] + } + + async def delete_session(self, session_id: str) -> bool: + """Delete a session and return success status.""" + try: + return self._store.delete_session(session_id) + except Exception: + logger.exception("Failed to delete session %s", session_id) + return False + + async def add_message(self, session_id: str, message: dict[str, Any]): + """Add a message to a session.""" + self._store.add_message( + session_id=session_id, + role=message.get("role", "user"), + content=message.get("content", ""), + metadata=message.get("metadata") + ) + + async def get_messages(self, session_id: str) -> list[dict[str, Any]]: + """Get all messages for a session.""" + if not self._store.session_exists(session_id): + return [] + + try: + return self._store.get_chat_history(session_id) or [] + except Exception: + logger.exception("Failed to load messages for session %s", session_id) + return [] diff --git a/src/praisonai/praisonai/ui_agents/__init__.py b/src/praisonai/praisonai/ui_agents/__init__.py new file mode 100644 index 000000000..8c2d5293d --- /dev/null +++ b/src/praisonai/praisonai/ui_agents/__init__.py @@ -0,0 +1 @@ +"""UI Agents module - YAML agents runner interface.""" \ No newline at end of file diff --git a/src/praisonai/praisonai/ui_agents/default_app.py b/src/praisonai/praisonai/ui_agents/default_app.py new file mode 100644 index 000000000..96e2cbed2 --- /dev/null +++ b/src/praisonai/praisonai/ui_agents/default_app.py @@ -0,0 +1,139 @@ +"""PraisonAI Agents UI — YAML-defined agents dashboard. + +Loaded by ``praisonai ui agents`` → ``aiui run app.py``. +Replaces ui/agents.py (Chainlit) with aiui implementation. + +Requires: pip install "praisonai[ui]" +""" + +import os +import praisonaiui as aiui +from praisonai.ui._aiui_datastore import PraisonAISessionDataStore + +# ── Set up datastore bridge ───────────────────────────────── +aiui.set_datastore(PraisonAISessionDataStore()) + +# ── Dashboard style ───────────────────────────────────────── +aiui.set_style("dashboard") +aiui.set_branding(title="PraisonAI Agents", logo="🤖") +aiui.set_theme(preset="blue", dark_mode=True, radius="lg") +aiui.set_pages([ + "chat", + "agents", + "memory", + "sessions", +]) + +# ── Load agents from YAML ─────────────────────────────────── +def _load_agents_from_yaml(): + """Load agents from agents.yaml file if available.""" + agents_file = os.path.join(os.getcwd(), "agents.yaml") + if not os.path.exists(agents_file): + return [] + + try: + import yaml + with open(agents_file, 'r') as f: + config = yaml.safe_load(f) + + agents = [] + if 'agents' in config: + for agent_def in config['agents']: + agents.append({ + "name": agent_def.get('name', 'Agent'), + "description": agent_def.get('description', 'YAML-defined agent'), + "instructions": agent_def.get('instructions', agent_def.get('role', '')), + "model": agent_def.get('model', os.getenv("PRAISONAI_MODEL", "gpt-4o-mini")), + "icon": "🤖", + }) + return agents + except Exception as e: + print(f"⚠️ Failed to load agents.yaml: {e}") + return [] + +# ── Register YAML agents ──────────────────────────────────── +yaml_agents = _load_agents_from_yaml() +if yaml_agents: + try: + from praisonaiui.features.agents import get_agent_registry + registry = get_agent_registry() + + for agent_def in yaml_agents: + registry.create(agent_def) + print(f" ✓ Agent: {agent_def['icon']} {agent_def['name']}") + except Exception as e: + print(f" ⚠️ Agent registration failed: {e}") +else: + print(" ℹ️ No agents.yaml found, using default agents") + +# ── Default agents if no YAML ─────────────────────────────── +if not yaml_agents: + DEFAULT_AGENTS = [ + { + "name": "Assistant", + "description": "General purpose AI assistant", + "instructions": "You are a helpful AI assistant. Provide clear and accurate responses.", + "model": os.getenv("PRAISONAI_MODEL", "gpt-4o-mini"), + "icon": "🤖", + }, + ] + + try: + from praisonaiui.features.agents import get_agent_registry + registry = get_agent_registry() + + for agent_def in DEFAULT_AGENTS: + registry.create(agent_def) + print(f" ✓ Default Agent: {agent_def['icon']} {agent_def['name']}") + except Exception as e: + print(f" ⚠️ Default agent registration failed: {e}") + +@aiui.starters +async def get_starters(): + """Suggest conversation starters for agents.""" + return [ + {"label": "Run Task", "message": "Execute the task defined in agents.yaml", "icon": "▶️"}, + {"label": "List Agents", "message": "What agents are available?", "icon": "👥"}, + {"label": "Agent Status", "message": "Show status of all agents", "icon": "📊"}, + ] + +@aiui.welcome +async def on_welcome(): + """Welcome message.""" + await aiui.say("👋 Welcome to PraisonAI Agents! This interface runs YAML-defined multi-agent workflows.") + +# Session-scoped agent cache +_agents_cache = {} + +@aiui.reply +async def on_message(message: str): + """Handle messages with agent execution.""" + await aiui.think("Processing...") + + try: + from praisonaiagents import Agent + + # Create or get cached agent + session_id = getattr(aiui.current_session, 'id', 'default') + if session_id not in _agents_cache: + _agents_cache[session_id] = Agent( + name="AgentsRunner", + instructions="You are an agent runner that coordinates YAML-defined workflows.", + llm=os.getenv("MODEL_NAME", "gpt-4o-mini"), + ) + + agent = _agents_cache[session_id] + result = await agent.achat(str(message)) + + # Stream the response + response_text = str(result) if result else "No response" + words = response_text.split(" ") + for i, word in enumerate(words): + await aiui.stream_token(word + (" " if i < len(words) - 1 else "")) + + except Exception as e: + await aiui.say(f"❌ Error: {e}") + +@aiui.cancel +async def on_cancel(): + await aiui.say("⏹️ Stopped.") \ No newline at end of file diff --git a/src/praisonai/praisonai/ui_bot/__init__.py b/src/praisonai/praisonai/ui_bot/__init__.py new file mode 100644 index 000000000..2adc7d0b7 --- /dev/null +++ b/src/praisonai/praisonai/ui_bot/__init__.py @@ -0,0 +1 @@ +"""UI Bot module - Bot interface with streaming.""" \ No newline at end of file diff --git a/src/praisonai/praisonai/ui_bot/default_app.py b/src/praisonai/praisonai/ui_bot/default_app.py new file mode 100644 index 000000000..556dfe7cf --- /dev/null +++ b/src/praisonai/praisonai/ui_bot/default_app.py @@ -0,0 +1,89 @@ +"""PraisonAI Bot UI — Bot interface with step visualization. + +Loaded by ``praisonai ui bot`` → ``aiui run app.py``. +Replaces ui/bot.py (Chainlit) with aiui implementation. + +Requires: pip install "praisonai[ui]" +""" + +import os +import praisonaiui as aiui +from praisonai.ui._aiui_datastore import PraisonAISessionDataStore + +# ── Set up datastore bridge ───────────────────────────────── +aiui.set_datastore(PraisonAISessionDataStore()) + +# ── Dashboard style ───────────────────────────────────────── +aiui.set_style("dashboard") +aiui.set_branding(title="PraisonAI Bot", logo="🤖") +aiui.set_theme(preset="green", dark_mode=True, radius="lg") +aiui.set_pages([ + "chat", + "sessions", +]) + +@aiui.starters +async def get_starters(): + """Suggest bot interaction starters.""" + return [ + {"label": "Help", "message": "What can you help me with?", "icon": "❓"}, + {"label": "Status", "message": "What's your current status?", "icon": "📊"}, + {"label": "Commands", "message": "What commands do you understand?", "icon": "⌨️"}, + {"label": "About", "message": "Tell me about yourself", "icon": "ℹ️"}, + ] + +@aiui.welcome +async def on_welcome(): + """Welcome message for bot interface.""" + await aiui.say("🤖 Hi! I'm your PraisonAI Bot. I can help with various tasks and show you step-by-step reasoning.") + +# Session-scoped bot cache +_bots_cache = {} + +@aiui.reply +async def on_message(message: str): + """Handle bot interactions with step visualization.""" + session_id = getattr(aiui.current_session, 'id', 'default') + + # Show initial thinking step + await aiui.think("🤔 Analyzing request...") + + try: + from praisonaiagents import Agent + + # Create or get cached bot agent + if session_id not in _bots_cache: + _bots_cache[session_id] = Agent( + name="PraisonBot", + instructions="You are a helpful bot assistant. Break down your reasoning into clear steps.", + llm=os.getenv("MODEL_NAME", "gpt-4o-mini"), + ) + + bot = _bots_cache[session_id] + + # Execute with step visualization + await aiui.think("⚙️ Processing...") + + result = await bot.achat(str(message)) + + # Stream the response with step markers + response_text = str(result) if result else "No response from bot" + + # Add step visualization + await aiui.say("**🔄 Step 1: Analysis Complete**") + await aiui.think("📤 Generating response...") + + # Stream the main response + 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**✅ Step 2: Response Generated**") + + except Exception as e: + await aiui.say(f"❌ Bot Error: {e}") + await aiui.say("Please check your configuration and try again.") + +@aiui.cancel +async def on_cancel(): + await aiui.say("⏹️ Bot interaction cancelled.") \ No newline at end of file diff --git a/src/praisonai/praisonai/ui_chat/default_app.py b/src/praisonai/praisonai/ui_chat/default_app.py index 7045569f1..3fe51a0fc 100644 --- a/src/praisonai/praisonai/ui_chat/default_app.py +++ b/src/praisonai/praisonai/ui_chat/default_app.py @@ -13,6 +13,10 @@ import os import praisonaiui as aiui +from praisonai.ui._aiui_datastore import PraisonAISessionDataStore + +# ── Set up datastore bridge ───────────────────────────────── +aiui.set_datastore(PraisonAISessionDataStore()) # ── Dashboard style, but no sidebar navigation ───────────── aiui.set_style("dashboard") diff --git a/src/praisonai/praisonai/ui_realtime/__init__.py b/src/praisonai/praisonai/ui_realtime/__init__.py new file mode 100644 index 000000000..ae923e8a4 --- /dev/null +++ b/src/praisonai/praisonai/ui_realtime/__init__.py @@ -0,0 +1 @@ +"""UI Realtime module - Voice realtime interface.""" \ No newline at end of file diff --git a/src/praisonai/praisonai/ui_realtime/default_app.py b/src/praisonai/praisonai/ui_realtime/default_app.py new file mode 100644 index 000000000..3b6372b0c --- /dev/null +++ b/src/praisonai/praisonai/ui_realtime/default_app.py @@ -0,0 +1,92 @@ +"""PraisonAI Realtime UI — Voice realtime interface. + +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. + +Requires: pip install "praisonai[ui]" +""" + +import os +import praisonaiui as aiui +from praisonai.ui._aiui_datastore import PraisonAISessionDataStore + +# ── Set up datastore bridge ───────────────────────────────── +aiui.set_datastore(PraisonAISessionDataStore()) + +# ── Dashboard style ───────────────────────────────────────── +aiui.set_style("dashboard") +aiui.set_branding(title="PraisonAI Realtime (Beta)", logo="🎤") +aiui.set_theme(preset="red", dark_mode=True, radius="lg") +aiui.set_pages([ + "chat", + "sessions", +]) + +@aiui.starters +async def get_starters(): + """Realtime conversation starters.""" + return [ + {"label": "Voice Test", "message": "Test voice interaction", "icon": "🎤"}, + {"label": "Realtime Help", "message": "How does realtime voice work?", "icon": "❓"}, + {"label": "Features", "message": "What realtime features are available?", "icon": "⚡"}, + ] + +@aiui.welcome +async def on_welcome(): + """Welcome with realtime status.""" + await aiui.say("""🎤 **PraisonAI Realtime Voice Interface (Beta)** + +⚠️ **Note**: Full WebRTC voice realtime is pending PraisonAIUI feature implementation. + +For now, this provides a text-based interface that simulates realtime interactions. + +See: https://github.com/MervinPraison/PraisonAIUI/issues for WebRTC voice realtime status. +""") + +# Session-scoped realtime agent cache +_realtime_cache = {} + +@aiui.reply +async def on_message(message: str): + """Handle realtime interactions (text-based for now).""" + session_id = getattr(aiui.current_session, 'id', 'default') + + await aiui.think("🎤 Processing realtime request...") + + try: + from praisonaiagents import Agent + + # Create or get cached realtime agent + if session_id not in _realtime_cache: + _realtime_cache[session_id] = Agent( + name="RealtimeAssistant", + instructions="You are a voice-optimized assistant. Keep responses conversational and concise for voice interaction.", + llm=os.getenv("MODEL_NAME", "gpt-4o-mini"), + ) + + agent = _realtime_cache[session_id] + + # For now, process as text (voice coming in future PraisonAIUI release) + result = await agent.achat(str(message)) + + # Stream response (simulating voice output) + 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 + 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}") + +@aiui.cancel +async def on_cancel(): + await aiui.say("🔇 Realtime interaction stopped.") \ No newline at end of file diff --git a/src/praisonai/pyproject.toml b/src/praisonai/pyproject.toml index c5a696ed3..bd1ac7aea 100644 --- a/src/praisonai/pyproject.toml +++ b/src/praisonai/pyproject.toml @@ -47,22 +47,16 @@ agentops = ["agentops>=0.3.12"] langfuse = ["langfuse>=3.0.0"] langextract = ["langextract>=1.0.0"] chat = [ - "chainlit>=2.8.5,<=2.9.4", - "aiosqlite>=0.20.0", - "greenlet>=3.0.3", + "aiui>=0.3.100", "tavily-python==0.5.0", "crawl4ai>=0.7.0", - "sqlalchemy>=2.0.36", "playwright>=1.47.0", "rich" ] code = [ - "chainlit>=2.8.5,<=2.9.4", - "aiosqlite>=0.20.0", - "greenlet>=3.0.3", + "aiui>=0.3.100", "tavily-python==0.5.0", "crawl4ai>=0.7.0", - "sqlalchemy>=2.0.36", "playwright>=1.47.0" ] sandbox = [ @@ -79,9 +73,7 @@ daytona = [ # Install via: pip install git+https://github.com/daytonaio/daytona-python ] realtime = [ - "chainlit>=2.8.5,<=2.9.4", - "aiosqlite>=0.20.0", - "greenlet>=3.0.3", + "aiui>=0.3.100", "tavily-python==0.5.0", "crawl4ai>=0.7.0", "playwright>=1.47.0", # required by crawl4ai — browsers still need: playwright install chromium @@ -89,7 +81,6 @@ realtime = [ "plotly>=5.24.0", "yfinance>=0.2.44", "ddgs>=9.0.0", - "sqlalchemy>=2.0.36" ] call = [ "twilio>=7.0.0", @@ -120,10 +111,7 @@ lite = [ ] all = [ # Full installation with all main features - "chainlit>=2.8.5,<=2.9.4", - "sqlalchemy>=2.0.36", - "aiosqlite>=0.20.0", - "greenlet>=3.0.3", + "aiui>=0.3.100", "gradio>=4.26.0", "flask>=3.0.0", "fastapi>=0.115.0", @@ -195,10 +183,6 @@ claw = [ "aiui[all]>=0.3.100", "praisonaiagents[all]>=1.5.40", # UI + persistence - "chainlit>=2.8.5,<=2.9.4", - "sqlalchemy>=2.0.36", - "aiosqlite>=0.20.0", - "greenlet>=3.0.3", "flask>=3.0.0", # Gateway "fastapi>=0.115.0", diff --git a/src/praisonai/tests/unit/test_aiui_datastore.py b/src/praisonai/tests/unit/test_aiui_datastore.py new file mode 100644 index 000000000..7944e0bae --- /dev/null +++ b/src/praisonai/tests/unit/test_aiui_datastore.py @@ -0,0 +1,234 @@ +"""Tests for PraisonAI → aiui datastore adapter.""" + +import pytest +from unittest.mock import Mock, patch +from praisonai.ui._aiui_datastore import PraisonAISessionDataStore + + +class TestPraisonAISessionDataStore: + """Test the datastore adapter that bridges PraisonAI sessions to aiui.""" + + def test_init_with_default_store(self): + """Test initialization with default hierarchical store.""" + with patch('praisonai.ui._aiui_datastore.get_hierarchical_session_store') as mock_get_store: + mock_store = Mock() + mock_get_store.return_value = mock_store + + adapter = PraisonAISessionDataStore() + + assert adapter._store == mock_store + mock_get_store.assert_called_once() + + def test_init_with_custom_store(self): + """Test initialization with custom session store.""" + mock_store = Mock() + adapter = PraisonAISessionDataStore(store=mock_store) + + assert adapter._store == mock_store + + @pytest.mark.asyncio + async def test_get_session_exists(self): + """Test getting an existing session.""" + mock_store = Mock() + mock_store.session_exists.return_value = True + mock_store.get_chat_history.return_value = [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi there"} + ] + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.get_session("test-session") + + assert result == { + "id": "test-session", + "messages": [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi there"} + ] + } + mock_store.session_exists.assert_called_once_with("test-session") + mock_store.get_chat_history.assert_called_once_with("test-session") + + @pytest.mark.asyncio + async def test_get_session_not_exists(self): + """Test getting a non-existent session.""" + mock_store = Mock() + mock_store.session_exists.return_value = False + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.get_session("nonexistent") + + assert result is None + mock_store.session_exists.assert_called_once_with("nonexistent") + mock_store.get_chat_history.assert_not_called() + + @pytest.mark.asyncio + async def test_get_session_corrupted(self): + """Test getting a corrupted session.""" + mock_store = Mock() + mock_store.session_exists.return_value = True + mock_store.get_chat_history.side_effect = Exception("Corrupted session") + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.get_session("corrupted-session") + + assert result is None + + @pytest.mark.asyncio + async def test_create_session_with_id(self): + """Test creating a session with specified ID.""" + mock_store = Mock() + adapter = PraisonAISessionDataStore(store=mock_store) + + result = await adapter.create_session("my-session") + + assert result == {"id": "my-session", "messages": []} + + @pytest.mark.asyncio + async def test_create_session_auto_id(self): + """Test creating a session with auto-generated ID.""" + mock_store = Mock() + adapter = PraisonAISessionDataStore(store=mock_store) + + result = await adapter.create_session() + + assert "id" in result + assert result["messages"] == [] + # Check that ID is a valid UUID format + assert len(result["id"]) == 36 # UUID length + assert result["id"].count("-") == 4 # UUID has 4 dashes + + @pytest.mark.asyncio + async def test_delete_session_success(self): + """Test successful session deletion.""" + mock_store = Mock() + mock_store.delete_session.return_value = True + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.delete_session("test-session") + + assert result is True + mock_store.delete_session.assert_called_once_with("test-session") + + @pytest.mark.asyncio + async def test_delete_session_failure(self): + """Test failed session deletion.""" + mock_store = Mock() + mock_store.delete_session.side_effect = Exception("Delete failed") + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.delete_session("test-session") + + assert result is False + + @pytest.mark.asyncio + async def test_add_message(self): + """Test adding a message to a session.""" + mock_store = Mock() + adapter = PraisonAISessionDataStore(store=mock_store) + + message = { + "role": "user", + "content": "Hello world", + "metadata": {"timestamp": "2024-01-01"} + } + + await adapter.add_message("test-session", message) + + mock_store.add_message.assert_called_once_with( + session_id="test-session", + role="user", + content="Hello world", + metadata={"timestamp": "2024-01-01"} + ) + + @pytest.mark.asyncio + async def test_add_message_minimal(self): + """Test adding a message with minimal fields.""" + mock_store = Mock() + adapter = PraisonAISessionDataStore(store=mock_store) + + message = {"content": "Hello"} + + await adapter.add_message("test-session", message) + + mock_store.add_message.assert_called_once_with( + session_id="test-session", + role="user", # default + content="Hello", + metadata=None + ) + + @pytest.mark.asyncio + async def test_get_messages_success(self): + """Test getting messages for a session.""" + mock_store = Mock() + mock_store.session_exists.return_value = True + mock_store.get_chat_history.return_value = [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"} + ] + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.get_messages("test-session") + + assert result == [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"} + ] + + @pytest.mark.asyncio + async def test_get_messages_nonexistent(self): + """Test getting messages for non-existent session.""" + mock_store = Mock() + mock_store.session_exists.return_value = False + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.get_messages("nonexistent") + + assert result == [] + + @pytest.mark.asyncio + async def test_get_messages_error(self): + """Test getting messages when store throws error.""" + mock_store = Mock() + mock_store.session_exists.return_value = True + mock_store.get_chat_history.side_effect = Exception("Store error") + + adapter = PraisonAISessionDataStore(store=mock_store) + result = await adapter.get_messages("test-session") + + assert result == [] + + @pytest.mark.asyncio + async def test_list_sessions_delegates_to_store(self): + """Test listing sessions delegates to store when available.""" + mock_store = Mock() + mock_store.list_sessions.return_value = [ + {"session_id": "a", "created_at": 1, "message_count": 3}, + {"session_id": "b", "created_at": 2, "message_count": 5}, + ] + adapter = PraisonAISessionDataStore(store=mock_store) + + result = await adapter.list_sessions() + + assert len(result) == 2 + assert result[0]["session_id"] == "a" + assert result[1]["session_id"] == "b" + mock_store.list_sessions.assert_called_once_with(limit=50) + + @pytest.mark.asyncio + async def test_list_sessions_store_without_list(self): + """Test listing sessions when store doesn't support listing.""" + # Custom SessionStoreProtocol impls don't require list_sessions + mock_store = Mock(spec=["add_message", "get_chat_history", + "clear_session", "delete_session", "session_exists"]) + adapter = PraisonAISessionDataStore(store=mock_store) + + result = await adapter.list_sessions() + + assert result == [] + + +# Removed test_import_fallback - was a no-op test that passed regardless of behavior +# After Blocker 1 fix, imports now fail loudly instead of falling back silently \ No newline at end of file