mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61e53754d4 | ||
|
|
bbcdf65b5a | ||
|
|
dd03c817b7 | ||
|
|
3ff99487d6 | ||
|
|
c47f572655 | ||
|
|
9c970df301 | ||
|
|
6f1d55c912 | ||
|
|
3967dd164f |
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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.13" \
|
||||
"praisonai>=4.6.16" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.13" \
|
||||
"praisonai>=4.6.16" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.13" \
|
||||
"praisonai>=4.6.16" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Motion Graphics — Basic HTML/GSAP → MP4 render (no LLM required).
|
||||
|
||||
This example drives the `HtmlRenderBackend` directly with a hand-authored
|
||||
HTML/GSAP composition. It renders to a real MP4 via headless Chromium +
|
||||
bundled ffmpeg, with no API keys needed.
|
||||
|
||||
Requirements:
|
||||
pip install praisonai-tools[video-motion]
|
||||
playwright install chromium
|
||||
|
||||
Verified end-to-end: produces 1920x1080 @ 30fps H.264 MP4 in ~5–10 seconds.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from praisonai_tools.video.motion_graphics import HtmlRenderBackend, RenderOpts
|
||||
|
||||
|
||||
HTML = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; background:#0f172a; overflow:hidden;
|
||||
font-family: -apple-system, Arial, sans-serif; }
|
||||
#stage { width:1920px; height:1080px; position:relative; }
|
||||
.title { font-size:96px; font-weight:800; color:#f8fafc;
|
||||
position:absolute; top:50%; left:50%;
|
||||
transform:translate(-50%,-50%); opacity:0; }
|
||||
.subtitle { font-size:44px; color:#94a3b8;
|
||||
position:absolute; top:62%; left:50%;
|
||||
transform:translate(-50%,-50%); opacity:0; }
|
||||
.dot { width:32px; height:32px; border-radius:50%; background:#38bdf8;
|
||||
position:absolute; top:78%; left:50%;
|
||||
transform:translate(-50%,-50%); opacity:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="stage" data-duration="4.0">
|
||||
<div class="title">PraisonAI</div>
|
||||
<div class="subtitle">Motion Graphics Pipeline</div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<script>
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.to(".title", { duration: 0.8, opacity: 1, y: -30, ease: "power3.out" })
|
||||
.to(".subtitle", { duration: 0.7, opacity: 1, y: -20, ease: "power3.out" }, "-=0.3")
|
||||
.to(".dot", { duration: 0.5, opacity: 1, scale: 2, ease: "back.out(2)" }, "-=0.2")
|
||||
.to(".dot", { duration: 0.4, x: 200, ease: "power2.inOut" })
|
||||
.to(".dot", { duration: 0.4, x: -200, ease: "power2.inOut" })
|
||||
.to(".dot", { duration: 0.4, x: 0, ease: "power2.inOut" })
|
||||
.to([".title",".subtitle",".dot"],
|
||||
{ duration: 0.5, opacity: 0, ease: "power2.in" });
|
||||
// Required: export timeline(s) for the render backend to drive.
|
||||
window.__timelines = [tl];
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
out = Path("/tmp/motion_graphics_demo")
|
||||
out.mkdir(exist_ok=True)
|
||||
(out / "index.html").write_text(HTML)
|
||||
|
||||
backend = HtmlRenderBackend(base_dir=out)
|
||||
|
||||
lint = await backend.lint(out)
|
||||
print(f"Lint: ok={lint.ok} messages={lint.messages}")
|
||||
|
||||
opts = RenderOpts(output_name="praisonai_demo.mp4", fps=30, quality="standard")
|
||||
print("Render: ...", flush=True)
|
||||
result = await backend.render(out, opts)
|
||||
|
||||
if result.ok:
|
||||
print(f"Render: OK path={result.output_path} size={result.size_kb} KB")
|
||||
else:
|
||||
print(f"Render: FAIL stderr={result.stderr}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Motion Graphics — GitTools safety demo (no network required for this demo).
|
||||
|
||||
GitTools is the read-only toolkit used by the code-exploration specialist in
|
||||
`motion_graphics_team`. It clones repos on demand, validates paths against
|
||||
traversal, and restricts to GitHub URLs or `owner/repo` format.
|
||||
|
||||
This example exercises the pure-Python safety helpers (URL parsing + file
|
||||
path validation). Cloning is network-bound and demonstrated separately.
|
||||
|
||||
Requirements:
|
||||
pip install praisonai-tools
|
||||
"""
|
||||
|
||||
from praisonai_tools.tools.git_tools import GitTools
|
||||
|
||||
|
||||
def main() -> None:
|
||||
tools = GitTools(base_dir="/tmp/praison_git_repos_demo")
|
||||
|
||||
print("URL parsing")
|
||||
print("-----------")
|
||||
for repo_input in [
|
||||
"octocat/Hello-World", # owner/repo
|
||||
"https://github.com/octocat/Hello-World.git", # https URL
|
||||
"[email protected]:octocat/Hello-World.git", # ssh URL
|
||||
]:
|
||||
try:
|
||||
url, name = tools._parse_repo_input(repo_input)
|
||||
print(f" {repo_input:<50} -> name={name}")
|
||||
except ValueError as exc:
|
||||
print(f" {repo_input:<50} -> REJECTED: {exc}")
|
||||
|
||||
print("\nFile-path validation")
|
||||
print("--------------------")
|
||||
for path in [
|
||||
"README.md", # safe
|
||||
"src/main.py", # safe
|
||||
"../etc/passwd", # traversal
|
||||
"../../secret.txt", # traversal
|
||||
"/etc/passwd", # absolute
|
||||
]:
|
||||
try:
|
||||
safe = tools._validate_file_path(path)
|
||||
print(f" {path:<30} -> SAFE: {safe}")
|
||||
except ValueError as exc:
|
||||
print(f" {path:<30} -> REJECT: {exc}")
|
||||
|
||||
print("\nListing any previously cloned repos:", tools.list_repos())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Motion Graphics — single-agent factory (requires praisonaiagents + LLM key).
|
||||
|
||||
Creates a single motion-graphics authoring agent that writes HTML/GSAP and
|
||||
renders to MP4 via the HTML backend. Good for quick, one-shot prompts.
|
||||
|
||||
Requirements:
|
||||
pip install praisonai-tools[video-motion] praisonaiagents[llm]
|
||||
playwright install chromium
|
||||
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, etc.
|
||||
|
||||
Set a working model via env (LiteLLM routing):
|
||||
export MOTION_LLM=anthropic/claude-sonnet-4-20250514
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from praisonai_tools.video.motion_graphics import create_motion_graphics_agent
|
||||
|
||||
|
||||
def main() -> None:
|
||||
workspace = Path("/tmp/motion_graphics_agent_demo")
|
||||
workspace.mkdir(exist_ok=True)
|
||||
|
||||
llm = os.getenv("MOTION_LLM", "anthropic/claude-sonnet-4-20250514")
|
||||
|
||||
agent = create_motion_graphics_agent(
|
||||
backend="html",
|
||||
workspace=workspace,
|
||||
max_retries=3,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
print(f"Agent: {agent.name}")
|
||||
print(f"Backend: {agent._motion_graphics_backend.__class__.__name__}")
|
||||
print(f"Workspace: {agent._motion_graphics_workspace}")
|
||||
print(f"Retries: {agent._motion_graphics_max_retries}")
|
||||
print(f"LLM: {llm}")
|
||||
print()
|
||||
print("Starting agent...\n")
|
||||
|
||||
agent.start(
|
||||
"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:")
|
||||
for p in sorted(workspace.rglob("*")):
|
||||
if p.is_file():
|
||||
print(f" {p.relative_to(workspace)} ({p.stat().st_size} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Motion Graphics — full team pipeline (requires praisonaiagents + LLM key).
|
||||
|
||||
Runs the coordinator + animator team (optionally with researcher and
|
||||
code-explorer specialists) under a hierarchical process, so the coordinator
|
||||
validates outputs before marking a render as successful.
|
||||
|
||||
Requirements:
|
||||
pip install praisonai-tools[video-motion] praisonaiagents[llm]
|
||||
playwright install chromium
|
||||
export ANTHROPIC_API_KEY=...
|
||||
export MOTION_LLM=anthropic/claude-sonnet-4-20250514
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from praisonai_tools.video.motion_graphics import motion_graphics_team
|
||||
|
||||
|
||||
def main() -> None:
|
||||
workspace = Path("/tmp/motion_graphics_team_demo")
|
||||
workspace.mkdir(exist_ok=True)
|
||||
|
||||
llm = os.getenv("MOTION_LLM", "anthropic/claude-sonnet-4-20250514")
|
||||
|
||||
print(f"[setup] workspace={workspace} llm={llm}")
|
||||
|
||||
team = motion_graphics_team(
|
||||
research=False, # set True to add a web-search researcher
|
||||
code_exploration=False, # set True to add a GitTools code-explorer
|
||||
workspace=workspace,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
print(f"[setup] {len(team.agents)} agents: {[a.name for a in team.agents]}")
|
||||
|
||||
prompt = "Animate Dijkstra's algorithm on a small weighted graph, 30s."
|
||||
print(f"[run] prompt: {prompt}")
|
||||
|
||||
t0 = time.time()
|
||||
result = team.start(prompt)
|
||||
elapsed = time.time() - t0
|
||||
|
||||
print(f"[done] elapsed={elapsed:.1f}s")
|
||||
print(f"[result] {result!r}")
|
||||
|
||||
print("\n[artifacts]")
|
||||
for p in sorted(workspace.rglob("*.mp4")):
|
||||
print(f" mp4 {p} ({p.stat().st_size} bytes)")
|
||||
for p in sorted(workspace.rglob("*.html")):
|
||||
print(f" html {p} ({p.stat().st_size} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.13"
|
||||
version = "1.6.16"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2951,7 +2951,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.13"
|
||||
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,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,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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.13.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.13.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]"
|
||||
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -8,11 +8,55 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_praisonai_env_file() -> Dict[str, str]:
|
||||
"""Load ``~/.praisonai/.env`` into ``os.environ`` (without overwriting).
|
||||
|
||||
Daemons launched by ``launchd`` / ``systemd`` don't inherit the user's
|
||||
shell env and don't auto-source dotfiles, so secrets written by
|
||||
``praisonai onboard`` (e.g. ``TELEGRAM_BOT_TOKEN``) are missing when
|
||||
the gateway starts in the background. We load them here so the
|
||||
YAML ``${VAR}`` substitution in ``GatewayServer.load_gateway_config``
|
||||
resolves correctly.
|
||||
|
||||
Existing ``os.environ`` values take precedence (so user-set shell
|
||||
vars always win). Returns the dict of keys we loaded (for logging).
|
||||
"""
|
||||
env_path = Path(os.environ.get("PRAISONAI_ENV_FILE")
|
||||
or (Path.home() / ".praisonai" / ".env"))
|
||||
loaded: Dict[str, str] = {}
|
||||
if not env_path.exists():
|
||||
return loaded
|
||||
try:
|
||||
for raw in env_path.read_text().splitlines():
|
||||
s = raw.strip()
|
||||
if not s or s.startswith("#") or "=" not in s:
|
||||
continue
|
||||
k, v = s.split("=", 1)
|
||||
k = k.strip()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
if not k:
|
||||
continue
|
||||
if k in os.environ:
|
||||
continue # don't clobber existing env
|
||||
os.environ[k] = v
|
||||
loaded[k] = v
|
||||
except OSError as exc:
|
||||
logger.warning("Could not read %s: %s", env_path, exc)
|
||||
if loaded:
|
||||
logger.info(
|
||||
"Loaded %d env var(s) from %s: %s",
|
||||
len(loaded), env_path, ", ".join(sorted(loaded.keys())),
|
||||
)
|
||||
return loaded
|
||||
|
||||
|
||||
class GatewayHandler:
|
||||
"""Handler for gateway CLI commands."""
|
||||
|
||||
@@ -34,6 +78,28 @@ class GatewayHandler:
|
||||
agent_file: Optional path to agent configuration file
|
||||
config_file: Optional path to gateway.yaml for multi-bot mode
|
||||
"""
|
||||
# Ensure INFO-level logs surface to bot-stdout.log / bot-stderr.log
|
||||
# when running under launchd / systemd. Many key lifecycle events
|
||||
# (bot start, channel routing, scheduler tick, retries) are already
|
||||
# emitted via `logger.info()` — they just weren't visible with the
|
||||
# default WARNING root level. Only configure if nothing is set yet,
|
||||
# so users/embedders keep control.
|
||||
_root = logging.getLogger()
|
||||
if not _root.handlers:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
||||
)
|
||||
if _root.level > logging.INFO or _root.level == logging.NOTSET:
|
||||
_root.setLevel(logging.INFO)
|
||||
|
||||
# Load ~/.praisonai/.env BEFORE any config parsing or ${VAR}
|
||||
# substitution — daemons don't inherit shell env.
|
||||
_load_praisonai_env_file()
|
||||
logger.info(
|
||||
"Gateway starting (host=%s port=%s config=%s agents=%s)",
|
||||
host, port, config_file or "-", agent_file or "-",
|
||||
)
|
||||
try:
|
||||
from praisonai.gateway import WebSocketGateway
|
||||
from praisonaiagents.gateway import GatewayConfig
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.13 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')
|
||||
|
||||
|
||||
@@ -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"},
|
||||
@@ -1030,15 +1049,26 @@ class WebSocketGateway:
|
||||
|
||||
@staticmethod
|
||||
def _substitute_env_vars(value: str) -> str:
|
||||
"""Replace ${VAR_NAME} patterns with environment variable values."""
|
||||
"""Replace ``${VAR_NAME}`` patterns with environment variable values.
|
||||
|
||||
Previously, missing env vars returned the literal ``${VAR_NAME}`` which
|
||||
caused downstream APIs (e.g. Telegram) to receive a broken token
|
||||
string and fail with opaque 404s. We now substitute missing vars with
|
||||
an **empty string** so the schema validator's required-field checks
|
||||
(e.g. ``token`` must be truthy) trip cleanly instead.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
def _replacer(match):
|
||||
var_name = match.group(1)
|
||||
env_val = os.environ.get(var_name)
|
||||
if env_val is None:
|
||||
logger.warning(f"Environment variable {var_name} not set")
|
||||
return match.group(0)
|
||||
logger.warning(
|
||||
f"Environment variable {var_name} not set "
|
||||
f"— substituting empty string. "
|
||||
f"Run `praisonai onboard` or set it in ~/.praisonai/.env."
|
||||
)
|
||||
return ""
|
||||
return env_val
|
||||
return re.sub(r'\$\{([^}]+)\}', _replacer, value)
|
||||
|
||||
|
||||
@@ -222,13 +222,26 @@ class ScheduledAgentExecutor:
|
||||
if delivery else None
|
||||
)
|
||||
|
||||
# Build chat kwargs
|
||||
# Build chat kwargs — only pass session_id if the agent
|
||||
# actually accepts it (Core SDK's Agent.chat signature does
|
||||
# not currently expose session_id; older assumption caused a
|
||||
# TypeError on every scheduled job).
|
||||
import inspect as _inspect
|
||||
chat_kwargs: Dict[str, Any] = {}
|
||||
if session_target == "main" and session_id:
|
||||
chat_kwargs["session_id"] = session_id
|
||||
else:
|
||||
# Isolated mode: use a cron-specific session
|
||||
chat_kwargs["session_id"] = f"cron_{job.id}"
|
||||
try:
|
||||
chat_params = _inspect.signature(agent.chat).parameters
|
||||
except (TypeError, ValueError): # C-callable or builtin
|
||||
chat_params = {}
|
||||
supports_session_id = (
|
||||
"session_id" in chat_params
|
||||
or any(p.kind == _inspect.Parameter.VAR_KEYWORD
|
||||
for p in chat_params.values())
|
||||
)
|
||||
if supports_session_id:
|
||||
if session_target == "main" and session_id:
|
||||
chat_kwargs["session_id"] = session_id
|
||||
else:
|
||||
chat_kwargs["session_id"] = f"cron_{job.id}"
|
||||
|
||||
result = await asyncio.to_thread(
|
||||
agent.chat, message, **chat_kwargs,
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.6.13"
|
||||
__version__ = "4.6.16"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.6.13",
|
||||
"praisonaiagents>=1.6.16",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0,<=1.82.6",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+1
-1
@@ -4808,7 +4808,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.13"
|
||||
version = "1.6.16"
|
||||
source = { directory = "../praisonai-agents" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Reference in new issue
Block a user