Cleanup: Delete ~5,337 LOC dead Chainlit code and wire aiui realtime (fixes #1524)

- Fix serve.py:364 broken import (_launch_chainlit_ui → _launch_aiui_app)
- Wire ui_realtime/default_app.py to use OpenAIRealtimeManager from aiui
- Replace create_realtime_interface calls with _launch_aiui_app in cli/main.py
- Delete dead Chainlit scaffolding: CHAINLIT_AVAILABLE, _get_chainlit_run(), 4 dead interface methods
- Delete 12 dead files (~5,337 LOC): chainlit_compat.py, db.py, sql_alchemy.py, etc.
- Update doctor check: chainlit → praisonaiui
- Remove 'pending' messaging from realtime UI (aiui already ships WebRTC)
- Keep --ui chainlit deprecation warning for backward compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: MervinPraison <[email protected]>
This commit is contained in:
praisonai-triage-agent[bot]andMervinPraison committed 2026-04-22 17:09:37 +00:00
1 parent f8fe440789
commit 648291e3e3
20 files changed
+24 -6741

No files matched your search

-304
View File
@@ -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
-125
View File
@@ -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)
@@ -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]")
+2 -3
View File
@@ -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")
@@ -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"),
+4 -111
View File
@@ -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
@@ -581,7 +560,8 @@ class PraisonAI:
# They no longer open Chainlit browser UI
if getattr(args, 'realtime', False):
self.create_realtime_interface()
from praisonai.cli.commands.ui import _launch_aiui_app
_launch_aiui_app("ui_realtime", "ui_realtime", 8085, "0.0.0.0", None, False, "Realtime Voice")
return
if getattr(args, 'call', False):
@@ -1238,11 +1218,8 @@ class PraisonAI:
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")
sys.exit(1)
self.create_realtime_interface()
from praisonai.cli.commands.ui import _launch_aiui_app
_launch_aiui_app("ui_realtime", "ui_realtime", 8085, "0.0.0.0", None, False, "Realtime Voice")
sys.exit(0)
elif args.command == 'train':
@@ -5189,45 +5166,6 @@ Now, {final_instruction.lower()}:"""
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 +5198,6 @@ Now, {final_instruction.lower()}:"""
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).
-875
View File
@@ -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()
-425
View File
@@ -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}")
@@ -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',
]
-894
View File
@@ -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()
-781
View File
@@ -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()
@@ -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)
-294
View File
@@ -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
-489
View File
@@ -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()
@@ -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
@@ -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]
-715
View File
@@ -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()
@@ -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 @@ _realtime_cache = {}
@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}")
-93
View File
@@ -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")
@@ -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"])