Compare commits

...
6 Commits
Author SHA1 Message Date
MervinPraison 61e53754d4 Release v4.6.16 2026-04-19 01:38:41 +01:00
MervinPraison bbcdf65b5a feat(cli): introduce configurable bot and environment paths
- Updated bot command to resolve configuration paths from environment variables.
- Added new path utility functions for clarity and maintainability.
- Updated version to 1.6.16 for package consistency.
- Enhanced help messages for commands to reflect new default path behavior.
2026-04-19 01:37:53 +01:00
MervinPraison dd03c817b7 feat(tutorial): enhance animation instructions in example script
Updated the motion graphics agent factory example with detailed steps
for creating a title-card animation, including necessary function
calls and content requirements. Added a new installation command
to the README for easier setup.
2026-04-19 01:34:10 +01:00
MervinPraison 3ff99487d6 Release v4.6.15 2026-04-19 01:25:00 +01:00
MervinPraison c47f572655 docs: auto-update feature parity trackers [skip ci] 2026-04-19 00:24:29 +00:00
MervinPraison 9c970df301 feat(onboard): persist GATEWAY_AUTH_TOKEN for stable gateway access
Added functionality to read and persist the GATEWAY_AUTH_TOKEN across daemon restarts. This ensures the dashboard link remains consistent, improving user experience. Updated version to 1.6.15 and made necessary adjustments to related files.
2026-04-19 01:24:08 +01:00
23 changed files with 242 additions and 47 deletions

No files matched your search

+4
View File
@@ -25,6 +25,10 @@
PraisonAI 🦞 — **Hire a 24/7 AI Workforce.** Stop writing boilerplate and start shipping autonomous agents that research, plan, and execute tasks across your apps. From one agent to an entire organization, deployed in 5 lines of code.
```bash
curl -fsSL https://praison.ai/install.sh | bash
```
<div align="center">
<br>
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.14" \
"praisonai>=4.6.16" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.14" \
"praisonai>=4.6.16" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.6.14" \
"praisonai>=4.6.16" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -40,8 +40,18 @@ def main() -> None:
print("Starting agent...\n")
agent.start(
"Create a 6-second title-card animation with the text 'Hello Motion' "
"fading in and a cyan underline drawing across. Save as intro.mp4."
"Create a short title-card animation.\n"
"Steps you MUST follow, in order:\n"
" 1. Call write_file with filepath='index.html' and content containing "
" a complete HTML document with GSAP 3.12 from cdnjs, a #stage div "
" with data-duration='3.0', visible text 'Hello Motion' that fades "
" in, and a cyan underline that draws across. The file MUST set "
" window.__timelines = [tl] at the end of its <script> block, and "
" the timeline MUST be created with { paused: true }.\n"
" 2. Call lint_composition() and verify ok=True.\n"
" 3. Call render_composition(output_name='intro.mp4', fps=30, "
" quality='standard') and report the resulting output_path.\n"
"Return the concrete output_path of the MP4 or the exact stderr."
)
print("\nArtifacts in workspace:")
@@ -1270,7 +1270,7 @@ Your Goal: {self.goal}"""
task_description=task_description,
task_id=task_id,
execute_tool_fn=self.execute_tool,
parallel_tool_calls=getattr(self.execution, "parallel_tool_calls", False),
parallel_tool_calls=getattr(getattr(self, "execution", None), "parallel_tool_calls", False),
reasoning_steps=reasoning_steps,
stream=stream
)
@@ -1740,7 +1740,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
task_description=task_description,
task_id=task_id,
execute_tool_fn=self.execute_tool_async,
parallel_tool_calls=getattr(self.execution, "parallel_tool_calls", False),
parallel_tool_calls=getattr(getattr(self, "execution", None), "parallel_tool_calls", False),
reasoning_steps=reasoning_steps,
stream=stream
)
@@ -2271,7 +2271,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
task_description=kwargs.get('task_description'),
task_id=kwargs.get('task_id'),
execute_tool_fn=self.execute_tool,
parallel_tool_calls=getattr(self.execution, "parallel_tool_calls", False)
parallel_tool_calls=getattr(getattr(self, "execution", None), "parallel_tool_calls", False)
):
response_content += chunk
yield chunk
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "1.6.14"
version = "1.6.16"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -2951,7 +2951,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "1.6.14"
version = "1.6.16"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -1,6 +1,6 @@
{
"version": "1.5.87",
"lastUpdated": "2026-04-18",
"lastUpdated": "2026-04-19",
"generatedBy": "praisonai._dev.parity.generator",
"sourceOfTruth": "Python SDK (praisonaiagents)",
"status": "PARITY_ACHIEVED",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": "1.5.87",
"lastUpdated": "2026-04-18",
"lastUpdated": "2026-04-19",
"generatedBy": "praisonai._dev.parity.generator",
"sourceOfTruth": "Python SDK (praisonaiagents)",
"summary": {
+1 -1
View File
@@ -1,6 +1,6 @@
# Feature Parity Tracker
> **Version:** 1.5.87 | **Last Updated:** 2026-04-18
> **Version:** 1.5.87 | **Last Updated:** 2026-04-19
> **Source of Truth:** Python SDK (praisonaiagents)
## Summary
+4
View File
@@ -25,6 +25,10 @@
PraisonAI 🦞 — **Hire a 24/7 AI Workforce.** Stop writing boilerplate and start shipping autonomous agents that research, plan, and execute tasks across your apps. From one agent to an entire organization, deployed in 5 lines of code.
```bash
curl -fsSL https://praison.ai/install.sh | bash
```
<div align="center">
<br>
<a href="https://x.com/elonmusk/status/1893870468249141688" target="_blank">
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.14.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.14.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.16.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.16.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+88
View File
@@ -0,0 +1,88 @@
"""Canonical filesystem paths for PraisonAI user state.
Mirrors the conventions used by ``hermes-agent`` (``~/.hermes``) and
``openclaw`` (``~/.openclaw``): one user-home directory per tool, with
configurable overrides via environment variables, so ``bot.yaml`` and
``.env`` don't pollute whatever working directory the user happened to
run ``praisonai onboard`` from.
Layout::
~/.praisonai/
├── .env # secrets (chmod 600) — managed by onboard
├── bot.yaml # default bot / gateway config
└── logs/ # daemon stdout / stderr (created by launchd helper)
Overrides (first match wins for each key):
* ``PRAISONAI_HOME`` — full path for the directory (e.g. Nix store)
* ``PRAISONAI_ENV_FILE`` — full path for the ``.env`` file
* ``PRAISONAI_BOT_CONFIG`` — full path for the default ``bot.yaml``
"""
from __future__ import annotations
import os
from pathlib import Path
__all__ = [
"praison_home",
"default_env_file",
"default_bot_config_path",
"resolve_bot_config_path",
]
def praison_home() -> Path:
"""Return the canonical PraisonAI home directory.
Honours ``PRAISONAI_HOME`` so packaging systems (Nix, Docker) can
relocate state. Defaults to ``~/.praisonai``.
"""
override = os.environ.get("PRAISONAI_HOME")
return Path(override).expanduser() if override else Path.home() / ".praisonai"
def default_env_file() -> Path:
"""Return the path to the canonical ``.env`` file."""
override = os.environ.get("PRAISONAI_ENV_FILE")
return Path(override).expanduser() if override else praison_home() / ".env"
def default_bot_config_path() -> Path:
"""Return the path where ``praisonai onboard`` writes ``bot.yaml``.
Users can force a custom path with ``PRAISONAI_BOT_CONFIG``.
"""
override = os.environ.get("PRAISONAI_BOT_CONFIG")
return Path(override).expanduser() if override else praison_home() / "bot.yaml"
def resolve_bot_config_path(cli_value: str = "bot.yaml") -> str:
"""Resolve the bot config path for CLI commands with sensible fallback.
Precedence:
1. ``cli_value`` if it is not the sentinel ``"bot.yaml"`` (i.e. user
passed an explicit ``--config X``).
2. ``./bot.yaml`` if it exists in the current working directory
(back-compat for projects that already keep bot.yaml checked in).
3. ``~/.praisonai/bot.yaml`` (or ``PRAISONAI_BOT_CONFIG``) — the
canonical location written by ``praisonai onboard``.
4. Fall back to ``cli_value`` unchanged so existing error messages
("config not found at bot.yaml") keep working.
"""
# (1) explicit override from the caller
if cli_value and cli_value != "bot.yaml":
return cli_value
# (2) honour an existing cwd file (back-compat)
cwd_path = Path.cwd() / "bot.yaml"
if cwd_path.is_file():
return str(cwd_path)
# (3) canonical home location
home_path = default_bot_config_path()
if home_path.is_file():
return str(home_path)
# (4) give up — return the caller's value so their own error handling
# surfaces a useful "not found" message.
return cli_value
+20 -7
View File
@@ -22,20 +22,28 @@ def _common_options():
@app.command("start")
def bot_start(
config: str = typer.Option(..., "--config", "-c", help="Path to bot YAML config file"),
config: str = typer.Option(
"bot.yaml", "--config", "-c",
help="Path to bot YAML config file (defaults to ./bot.yaml if present, else ~/.praisonai/bot.yaml)",
),
):
"""Start a bot from a single YAML config file (zero-code).
The config file contains platform, token, and agent settings all in one place.
Resolution order when ``--config`` is not given:
``./bot.yaml`` (back-compat) → ``~/.praisonai/bot.yaml`` (canonical).
Examples:
praisonai bot start --config bot.yaml
praisonai bot start # auto-detect
praisonai bot start --config ~/.praisonai/bot.yaml
praisonai bot start -c my-telegram-bot.yaml
"""
from .._paths import resolve_bot_config_path
from ..features.bots_cli import BotHandler
resolved = resolve_bot_config_path(config)
handler = BotHandler()
handler.start_from_config(config)
handler.start_from_config(resolved)
@app.command("telegram")
@@ -440,7 +448,10 @@ def bot_agentmail(
@app.command("install-daemon")
def bot_install_daemon(
config: str = typer.Option("bot.yaml", "--config", help="Path to bot.yaml"),
config: str = typer.Option(
"bot.yaml", "--config",
help="Path to bot.yaml (defaults to ./bot.yaml → ~/.praisonai/bot.yaml)",
),
start: bool = typer.Option(True, "--start/--no-start", help="Start after install"),
):
"""Install bot as OS daemon service (alias for 'praisonai gateway install').
@@ -450,9 +461,11 @@ def bot_install_daemon(
praisonai bot install-daemon --config my-bot.yaml --no-start
"""
from praisonai.daemon import install_daemon
from .._paths import resolve_bot_config_path
from ..output.console import get_output_controller
output = get_output_controller()
config = resolve_bot_config_path(config)
try:
result = install_daemon(config_path=config)
@@ -149,7 +149,10 @@ def gateway_channels(
@app.command("install")
def gateway_install(
config: str = typer.Option("bot.yaml", "--config", help="Path to bot.yaml"),
config: str = typer.Option(
"bot.yaml", "--config",
help="Path to bot.yaml (defaults to ./bot.yaml → ~/.praisonai/bot.yaml)",
),
start: bool = typer.Option(True, "--start/--no-start", help="Start after install"),
):
"""Install the gateway as an OS daemon (LaunchAgent / systemd).
@@ -159,9 +162,11 @@ def gateway_install(
praisonai gateway install --config my-bot.yaml --no-start
"""
from praisonai.daemon import install_daemon
from .._paths import resolve_bot_config_path
from ..output.console import get_output_controller
output = get_output_controller()
config = resolve_bot_config_path(config)
try:
result = install_daemon(config_path=config)
@@ -23,7 +23,8 @@ from ..registry import register_check
)
def check_bot_tokens(config: DoctorConfig) -> CheckResult:
"""Check bot token environment variables."""
config_path = getattr(config, 'config_file', None) or "bot.yaml"
from praisonai.cli._paths import resolve_bot_config_path
config_path = getattr(config, 'config_file', None) or resolve_bot_config_path("bot.yaml")
token_vars = {
"TELEGRAM_BOT_TOKEN": "Telegram",
"DISCORD_BOT_TOKEN": "Discord",
@@ -63,7 +64,8 @@ def check_bot_tokens(config: DoctorConfig) -> CheckResult:
def check_bot_config(config: DoctorConfig) -> CheckResult:
"""Check bot.yaml exists and is valid."""
start = time.time()
config_path = getattr(config, 'config_file', None) or "bot.yaml"
from praisonai.cli._paths import resolve_bot_config_path
config_path = getattr(config, 'config_file', None) or resolve_bot_config_path("bot.yaml")
if not os.path.exists(config_path):
return CheckResult(
id="bot_config",
@@ -107,7 +109,8 @@ def check_bot_config(config: DoctorConfig) -> CheckResult:
def check_bot_security(config: DoctorConfig) -> CheckResult:
"""Check bot security configuration for safe defaults."""
start = time.time()
config_path = getattr(config, 'config_file', None) or "bot.yaml"
from praisonai.cli._paths import resolve_bot_config_path
config_path = getattr(config, 'config_file', None) or resolve_bot_config_path("bot.yaml")
channel_warnings = []
global_warnings = []
@@ -127,6 +127,24 @@ def _save_env_vars(env_vars: Dict[str, Optional[str]]) -> Optional[Path]:
return env_file
def _read_env_value(key: str) -> Optional[str]:
"""Read a single value from ``~/.praisonai/.env`` (if present)."""
env_file = _praison_home() / ".env"
if not env_file.exists():
return None
try:
for line in env_file.read_text().splitlines():
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
continue
k, v = s.split("=", 1)
if k.strip() == key:
return v.strip().strip('"').strip("'") or None
except OSError:
return None
return None
def _generate_bot_yaml(platforms: List[str], agent_name: str = "assistant", agent_instructions: str = "") -> str:
"""Generate bot.yaml content compatible with BOTH ``praisonai bot start``
and ``praisonai gateway start``.
@@ -437,6 +455,20 @@ class OnboardWizard:
default="You are a helpful AI assistant.",
)
# Step 4b: Ensure a stable GATEWAY_AUTH_TOKEN is persisted. Without
# this the gateway server auto-generates a random token on every
# start (see server.py _check_auth), which means the dashboard link
# we show in the Done panel would rotate each restart. Persisting
# one value to ~/.praisonai/.env keeps clicks working across
# daemon restarts.
existing_gateway_token = _read_env_value("GATEWAY_AUTH_TOKEN")
if not existing_gateway_token:
import secrets as _secrets
self._gateway_token = _secrets.token_hex(16)
_save_env_vars({"GATEWAY_AUTH_TOKEN": self._gateway_token})
else:
self._gateway_token = existing_gateway_token
# Step 5: Generate config
console.print("\n[bold]Step 5: Generate configuration[/bold]\n")
yaml_content = _generate_bot_yaml(
@@ -445,7 +477,11 @@ class OnboardWizard:
agent_instructions=self.agent_instructions,
)
self.config_path = Prompt.ask("Config file path", default="bot.yaml")
from praisonai.cli._paths import default_bot_config_path
default_cfg = str(default_bot_config_path())
self.config_path = Prompt.ask("Config file path", default=default_cfg)
# Ensure parent dir exists (e.g. ~/.praisonai/) before writing.
os.makedirs(os.path.dirname(os.path.abspath(self.config_path)) or ".", exist_ok=True)
if os.path.exists(self.config_path):
if not Confirm.ask(f" {self.config_path} exists. Overwrite?", default=False):
@@ -486,13 +522,23 @@ class OnboardWizard:
console.print(f" [red]✗[/red] {str(e)[:200]}")
# Done — commands referenced here must exist in `praisonai --help`.
_host = "127.0.0.1"
_port = 8765
_tok = getattr(self, "_gateway_token", "")
_masked = (_tok[:4] + "…" + _tok[-4:]) if len(_tok) >= 10 else "(set)"
_dashboard_url = f"http://{_host}:{_port}/info?token={_tok}" if _tok else f"http://{_host}:{_port}/info"
_health_url = f"http://{_host}:{_port}/health"
console.print(Panel(
f"[bold green]Setup complete![/bold green]\n\n"
f"Start your bot:\n"
f"[bold]Dashboard link (with token):[/bold]\n"
f" [cyan]{_dashboard_url}[/cyan]\n"
f" [dim]Token {_masked} stored in ~/.praisonai/.env as GATEWAY_AUTH_TOKEN[/dim]\n\n"
f"[bold]Public health (no token):[/bold]\n"
f" [cyan]{_health_url}[/cyan]\n\n"
f"[bold]Start your bot (foreground):[/bold]\n"
f" [cyan]praisonai bot start --config {self.config_path}[/cyan]\n\n"
f"Check health:\n"
f" [cyan]praisonai doctor[/cyan]\n\n"
f"View gateway status:\n"
f"[bold]Check everything:[/bold]\n"
f" [cyan]praisonai doctor[/cyan]\n"
f" [cyan]praisonai gateway status[/cyan]",
title="✅ Done",
border_style="green",
@@ -549,12 +595,15 @@ class OnboardWizard:
env_file = _save_env_vars(env_to_save)
yaml_content = _generate_bot_yaml(self.selected_platforms)
with open("bot.yaml", "w") as f:
from praisonai.cli._paths import default_bot_config_path
cfg_path = default_bot_config_path()
os.makedirs(cfg_path.parent, exist_ok=True)
with open(cfg_path, "w") as f:
f.write(yaml_content)
print("\n✓ Written to bot.yaml")
print(f"\n✓ Written to {cfg_path}")
if env_file:
print(f"✓ Secrets saved to {env_file} (chmod 600)")
print("Start with: praisonai bot start --config bot.yaml")
print(f"Start with: praisonai bot start --config {cfg_path}")
def run_onboard() -> None:
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==4.6.14 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==4.6.16 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+28 -9
View File
@@ -203,12 +203,21 @@ class WebSocketGateway:
"""
self.config = config or GatewayConfig(host=host, port=port)
if hasattr(self.config, 'auth_token') and not self.config.auth_token:
import secrets
self.config.auth_token = secrets.token_hex(16)
logger.warning(
f"No auth_token provided for Gateway server. Generated temporary token: {self.config.auth_token}. "
"For production, set GATEWAY_AUTH_TOKEN."
)
# Prefer a user-configured token (persisted by `praisonai onboard`
# to ~/.praisonai/.env as GATEWAY_AUTH_TOKEN) so the dashboard
# URL stays stable across daemon restarts. Only fall back to
# generating a random ephemeral token if nothing is set.
env_tok = os.environ.get("GATEWAY_AUTH_TOKEN", "").strip()
if env_tok:
self.config.auth_token = env_tok
logger.info("Gateway using GATEWAY_AUTH_TOKEN from environment")
else:
import secrets
self.config.auth_token = secrets.token_hex(16)
logger.warning(
f"No auth_token provided for Gateway server. Generated temporary token: {self.config.auth_token}. "
"For production, set GATEWAY_AUTH_TOKEN."
)
self._host = self.config.host
self._port = self.config.port
@@ -267,17 +276,27 @@ class WebSocketGateway:
return JSONResponse(self.health())
def _check_auth(request) -> Optional[JSONResponse]:
"""Validate auth token if configured. Returns error response or None."""
"""Validate auth token if configured. Returns error response or None.
Accepts either:
- ``Authorization: Bearer <token>`` header (preferred for APIs)
- ``?token=<token>`` query parameter (so the dashboard URL from
``praisonai onboard`` is clickable in a browser)
"""
if not self.config.auth_token:
return None
auth_header = request.headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
token: str = ""
if auth_header.startswith("Bearer "):
token = auth_header[7:]
else:
token = request.query_params.get("token", "")
if not token:
return JSONResponse(
{"error": "Authentication required"},
status_code=401,
headers={"WWW-Authenticate": "Bearer"},
)
token = auth_header[7:]
if not secrets.compare_digest(token, self.config.auth_token):
return JSONResponse(
{"error": "Invalid authentication token"},
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.6.14"
__version__ = "4.6.16"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=1.6.14",
"praisonaiagents>=1.6.16",
"python-dotenv>=0.19.0",
"litellm>=1.81.0,<=1.82.6",
"PyYAML>=6.0",
+1 -1
View File
@@ -4808,7 +4808,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "1.6.14"
version = "1.6.16"
source = { directory = "../praisonai-agents" }
dependencies = [
{ name = "aiohttp" },